diff --git a/.agents/skills/write-discoverable-code/SKILL.md b/.agents/skills/write-discoverable-code/SKILL.md new file mode 100644 index 0000000000..67effe82d6 --- /dev/null +++ b/.agents/skills/write-discoverable-code/SKILL.md @@ -0,0 +1,69 @@ +--- +name: write-discoverable-code +description: Write Rust/Substrate code so grep-first coding agents can find, parse, and trust it — distinctive names, precise types, definition-site docs, concept-named files. Use when adding or refactoring code in subtensor, or when running a discoverability migration shard. +--- + +# Write discoverable code (subtensor) + +Coding agents navigate this repo with `rg` / filename search, not a dependency graph. Names and paths are the reverse index. Types and compiler errors are the feedback loop agents cannot skip. + +## Naming + +1. **Exported / crate-visible symbols: 2–3 words, one domain word.** Prefer `calculate_subnet_emission` over `calculate`. Package/module path counts as a word only when call sites use the qualified path in source (`swap::NewClient`-style); FRAME dispatchables and free functions need the domain word in the identifier itself. +2. **One spelling per concept.** Follow the glossary in root `AGENTS.md`. Do not introduce `organization` beside `org`, or `network_id` beside `NetUid`. +3. **Spell out common abbreviations in new helpers.** Prefer `subnet` over `sn`, `rate_limit` over `rl`, and full domain words over opaque stubs (`Custom`, `I`, `Impl` as the only distinguisher). Boolean helpers should read as predicates (`subnet_exists`, `should_accumulate_…`), not `if_*`. Extrinsic-body helpers should not share a `do_*` prefix with the dispatchable — use `perform_*` / a concept verb. +4. **Files and modules are search terms.** Name files after the concept they own (`smtp_settings.py` → here: `folder_fallbacks.rs`, `hmac_payload_signer.rs`). Prefer `email/message_rendering.rs` over a 5k-line grab-bag. Precompile `INDEX` / Solidity selectors are path-agnostic in the metadata fingerprint, so `foo.rs` → `foo/mod.rs` splits are safe when those values stay put. +5. **Tests named after source.** `staking/add_stake.rs` → tests that cover it live under a discoverable name (`tests/add_stake.rs` or a module clearly about add-stake). Avoid dumping unrelated cases into one monolith when splitting is cheap. +6. **Mark legacy `@deprecated` / `#[deprecated]`** when keeping a path temporarily. Prefer deletion. + +### Do not rename (Tier A–D) + +| Tier | Surface | Why | +|------|---------|-----| +| **A** | Storage item **type names**; `construct_runtime!` pallet names and indices | Twox128 storage keys / module prefix | +| **B** | `call_index` numbers; Event/Error **variant order**; SCALE field **order**; `freeze_struct` layouts | Wire / codec compatibility | +| **C** | Extrinsic **fn names**; Event/Error **names**; RPC `#[method(name = "…")]` strings; runtime API trait/method names; precompile `INDEX` + Solidity `public("…")` selectors | Clients, SDKs, EVM, explorers | +| **D** | `WeightInfo` method names (must match calls); applied migration **name strings**; hardcoded `"SubtensorModule"` / pallet prefix strings | Benchmarks, migration idempotency, EVM storage query | + +**Never edit** generated [`pallets/subtensor/src/weights.rs`](../../../pallets/subtensor/src/weights.rs). + +**Generally safe:** private helpers, `pub(crate)` types not in storage/RPC/metadata, module paths under a pallet, file splits, test helpers, doc comments (see freeze_struct rule). + +If a rename would touch files outside your shard, append a line to `refactor/rename-proposals.md` instead of doing it. + +## Types + +- Annotate inputs/outputs; avoid `any`-equivalent opacity (`impl Trait` only when the trait name is itself a good search term). +- Prefer newtypes / distinct ID types (`NetUid`, account kinds) over raw `u16`/`AccountId` soup at API boundaries so the compiler catches swaps. +- Type names obey the same uniqueness rule as function names (`SubnetEmissionResult`, not `Result` aliases that collide). + +## Comments + +- **One sentence on the definition** — storage item, dispatchable, important helper — saying what the code cannot say (invariants, units, “deliberately does not sanitize”, migration constraints). +- Agents land on definitions via search; definition-site docs are the highest-leverage docs. +- Do not restate the signature. Do not invent behavior that is not true. + +### freeze_struct caveat + +`#[freeze_struct]` blanks doc text before hashing but keeps doc attributes. **Adding or removing** docs on a frozen struct/field requires updating the hash; **editing existing** doc text does not. Only update the hash when the non-doc token stream is unchanged. + +## File structure + +- Target **≤ ~1000 lines** per file for new splits; existing giants should be split by concept when touched. +- Split pattern: `foo.rs` → `foo/mod.rs` + `foo/.rs`. Update `mod` declarations in the owning tree only. +- Do not create barrel `export *` equivalents that erase names without need; keep re-exports explicit when agents must follow them. + +## Deliberate absences + +If the codebase intentionally does not do something readers will search for (e.g. “HTML email is not sanitized”), document that at the definition or module the search will hit. Grep cannot prove absence. + +## Exit checklist (shard agents) + +```bash +cargo fmt --all +cargo clippy -p --all-targets -- --deny warnings +SKIP_WASM_BUILD=1 cargo nextest run -p +./scripts/check_metadata_unchanged.sh +``` + +Metadata fingerprint must match `refactor/metadata-baseline.txt`. If it fails, you changed a frozen surface — revert that part. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..3ab0b68028 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,78 @@ +# Agent guide — subtensor + +This repository is a Bittensor Substrate node (Rust / FRAME). Agents navigate primarily by text search (`rg`). Write code so names, paths, and definition-site docs are good search terms. + +For the full discoverability conventions, load [`.agents/skills/write-discoverable-code/SKILL.md`](.agents/skills/write-discoverable-code/SKILL.md). + +## Repo map (search anchors) + +| Path | Role | +|------|------| +| `pallets/subtensor/` | Core: staking, subnets, emissions, epoch/consensus, registration | +| `pallets/swap/` | TAO↔alpha AMM / liquidity | +| `pallets/admin-utils/` | Sudo/admin hyperparameters and toggles | +| `pallets/limit-orders/` | Signed limit / take-profit / stop-loss orders | +| `pallets/{commitments,crowdloan,drand,shield,proxy,utility,transaction-fee,alpha-assets}/` | Supporting pallets | +| `runtime/` | `construct_runtime!`, migrations tuple, `spec_version`, runtime APIs | +| `node/` | Binary, RPC wiring, chain specs | +| `precompiles/` | EVM precompiles (addresses + Solidity selectors are frozen) | +| `common/`, `primitives/`, `support/` | Shared types, math, lints, macros | +| `sdk/` | Client core + language bindings (metadata-driven) | + +Canonical layout doc: [`docs/internals/repo-layout.mdx`](docs/internals/repo-layout.mdx). + +## Glossary (one spelling per concept) + +| Prefer | Avoid | +|--------|--------| +| `netuid` / `NetUid` | `net_uid`, `network_id`, `subnet_id` (for the same type) | +| `hotkey` / `coldkey` | `hot_key`, `cold_key` | +| `tao` / `alpha` | inventing synonyms for the same asset | +| `stake` | mixing `bond` / `delegation` for the same storage amount | +| `subnet` | `network` when you mean a Bittensor subnet (except historical names); never abbreviate as `sn` in new helpers (`ensure_sn_owner` → `ensure_subnet_owner`) | +| `uid` | `neuron_index` for the per-subnet uid | +| `tempo` | `epoch_length` for the subnet tempo parameter | +| `emission` | `reward_mint` for coinbase emission | +| `rate_limit` / `rate_limits` | `rl` in identifiers (`record_owner_rl` → `record_owner_rate_limits`) | +| `*_exists` / `should_*` predicates | `if_*` boolean helpers (`if_subnet_exist` → `subnet_exists`) | +| `perform_*` / concept verb for extrinsic bodies | bare `do_*` next to a same-named dispatchable (`do_swap_hotkey` → `perform_hotkey_swap`) | +| domain word in RPC/handler types | opaque `Custom` alone (`SubtensorCustom` → `SubtensorCustomRpc`) | + +When a frozen on-chain name already uses a different spelling, keep the frozen name; do not invent a parallel alias. + +## Frozen surface (do not rename) + +See the Tier A–D table in the write-discoverable-code skill. Short form: + +- **Never rename:** storage item type names, `construct_runtime!` pallet names/indices, `call_index` numbers, Event/Error **variant order**, SCALE field order, RPC method strings, runtime API trait/method names, precompile addresses / Solidity selectors, applied migration name strings, `"SubtensorModule"` hardcodes, `WeightInfo` method names. +- **Do not edit:** [`pallets/subtensor/src/weights.rs`](pallets/subtensor/src/weights.rs) (generated). +- **Safe:** private/crate helpers, internal types not in storage/RPC, file splits (`foo.rs` → `foo/mod.rs`), test layout, doc comments (with `freeze_struct` caveat below). + +Safety oracle: `scripts/check_metadata_unchanged.sh` (docs-stripped structural fingerprint must match `refactor/metadata-baseline.txt`). + +## freeze_struct and docs + +`#[freeze_struct("…")]` hashes the struct after blanking doc *text*, but **keeps** doc attributes. Therefore: + +- Changing the text of an existing `///` comment: safe, no hash update. +- Adding or removing a doc comment on a frozen struct/field: changes the hash — update the hash **only** when the non-doc token stream is unchanged. + +## Swarm / shard rules + +During the discoverability migration (`refactor/discoverability`): + +1. Own only the files listed in your shard in [`refactor/refactor-manifest.json`](refactor/refactor-manifest.json). +2. Rename a symbol only if `rg -w OldName` (repo-wide, excl. `target/`/`vendor/`) hits exclusively inside your owned files. Otherwise append to [`refactor/rename-proposals.md`](refactor/rename-proposals.md). +3. File splits must use `foo.rs` → `foo/mod.rs` (+ siblings) so other shards never need to edit your parent. +4. Before finishing: `cargo fmt`, clippy `-D warnings` on owned crates, tests on owned crates, and `scripts/check_metadata_unchanged.sh`. + +## Local checks + +```bash +just fmt +just clippy +SKIP_WASM_BUILD=1 cargo nextest run -p +./scripts/check_metadata_unchanged.sh +``` + +Runtime PRs normally bump `spec_version`; pure discoverability work that leaves metadata (minus docs) identical should use the `no-spec-version-bump` label and say so in the PR. diff --git a/chain-extensions/src/contracts_env.rs b/chain-extensions/src/contracts_env.rs new file mode 100644 index 0000000000..2eef34ee94 --- /dev/null +++ b/chain-extensions/src/contracts_env.rs @@ -0,0 +1,93 @@ +//! Contracts VM adapter: bridges `pallet-contracts` [`Environment`] to [`SubtensorExtensionEnv`]. + +use codec::{Decode, MaxEncodedLen}; +use frame_system::RawOrigin; +use pallet_contracts::chain_extension::{BufInBufOutState, Environment, Ext, InitState}; +use sp_runtime::{DispatchError, Weight}; +use sp_std::marker::PhantomData; + +/// Environment surface used by [`crate::SubtensorChainExtension`] dispatch helpers. +/// +/// Production code uses [`ContractsEnvAdapter`]; unit tests supply an in-memory mock. +pub(crate) trait SubtensorExtensionEnv +where + T: pallet_contracts::Config, +{ + fn func_id(&self) -> u16; + fn charge_weight(&mut self, weight: Weight) -> Result<(), DispatchError>; + fn read_as(&mut self) -> Result; + fn write_output(&mut self, data: &[u8]) -> Result<(), DispatchError>; + /// Contract address (`ext.address()`), used by non-`Caller*` function ids as the signed origin. + fn caller(&mut self) -> T::AccountId; + /// Transaction origin (`ext.caller()`), used by `Caller*` function ids. + #[allow(dead_code)] + fn origin(&mut self) -> pallet_contracts::Origin; +} + +/// Map a `pallet-contracts` origin into a FRAME [`RawOrigin`] for pallet dispatch. +pub(crate) fn contracts_origin_as_raw( + origin: pallet_contracts::Origin, +) -> RawOrigin +where + T: pallet_contracts::Config, +{ + match origin { + pallet_contracts::Origin::Signed(caller) => RawOrigin::Signed(caller), + pallet_contracts::Origin::Root => RawOrigin::Root, + } +} + +/// Buf-in/buf-out wrapper around the contracts chain-extension [`Environment`]. +pub(crate) struct ContractsEnvAdapter<'a, 'b, T, E> +where + T: pallet_subtensor::Config + pallet_contracts::Config, + E: Ext, +{ + env: Environment<'a, 'b, E, BufInBufOutState>, + _marker: PhantomData, +} + +impl<'a, 'b, T, E> ContractsEnvAdapter<'a, 'b, T, E> +where + T: pallet_subtensor::Config + pallet_contracts::Config, + T::AccountId: Clone, + E: Ext, +{ + pub(crate) fn new(env: Environment<'a, 'b, E, InitState>) -> Self { + Self { + env: env.buf_in_buf_out(), + _marker: PhantomData, + } + } +} + +impl<'a, 'b, T, E> SubtensorExtensionEnv for ContractsEnvAdapter<'a, 'b, T, E> +where + T: pallet_subtensor::Config + pallet_contracts::Config, + T::AccountId: Clone, + E: Ext, +{ + fn func_id(&self) -> u16 { + self.env.func_id() + } + + fn charge_weight(&mut self, weight: Weight) -> Result<(), DispatchError> { + self.env.charge_weight(weight).map(|_| ()) + } + + fn read_as(&mut self) -> Result { + self.env.read_as() + } + + fn write_output(&mut self, data: &[u8]) -> Result<(), DispatchError> { + self.env.write(data, false, None) + } + + fn caller(&mut self) -> T::AccountId { + self.env.ext().address().clone() + } + + fn origin(&mut self) -> pallet_contracts::Origin { + self.env.ext().caller() + } +} diff --git a/chain-extensions/src/lib.rs b/chain-extensions/src/lib.rs index 0315668441..bad3ec46df 100644 --- a/chain-extensions/src/lib.rs +++ b/chain-extensions/src/lib.rs @@ -1,5 +1,26 @@ +//! # Subtensor chain extensions (`subtensor-chain-extensions`) +//! +//! `pallet-contracts` chain extension that exposes staking, proxy, alpha recycle/burn, and +//! read-only subnet/stake queries to ink! contracts. +//! +//! ## Wire-facing surface (do not renumber) +//! +//! - Runtime registers this extension at contracts id **`0x1000`** (see runtime `ChainExtension`). +//! - [`types::FunctionId`] `u16` discriminants are the ink `#[ink(function = N)]` selectors — +//! mirrored in `ink-contract/`. Never reorder or reuse ids. +//! - [`types::Output`] status codes are returned as `RetVal::Converging(code)` and mapped by +//! ink `FromStatusCode`. Variant discriminants are ABI. +//! +//! ## Origin modes +//! +//! - **Contract-as-signer** (`AddStakeV1`, …): origin is `RawOrigin::Signed(env.caller())` +//! (the contract address). +//! - **Caller-as-signer** (`CallerAddStakeV1`, …): origin is +//! `contracts_origin_as_raw(env.origin())` (the extrinsic signer / nested caller). + #![cfg_attr(not(feature = "std"), no_std)] +mod contracts_env; #[cfg(test)] mod mock; #[cfg(test)] @@ -7,22 +28,27 @@ mod tests; pub mod types; +pub(crate) use contracts_env::{ + ContractsEnvAdapter, SubtensorExtensionEnv, contracts_origin_as_raw, +}; + use crate::types::{ColdkeyLock, FunctionId, Output, StakeAvailability, SubnetRegistrationState}; -use codec::{Decode, Encode, MaxEncodedLen}; +use codec::Encode; use frame_support::{DebugNoBound, traits::Get}; use frame_system::RawOrigin; use pallet_contracts::chain_extension::{ - BufInBufOutState, ChainExtension, Environment, Ext, InitState, RetVal, SysConfig, + ChainExtension, Environment, Ext, InitState, RetVal, SysConfig, }; use pallet_subtensor::weights::WeightInfo as SubtensorWeightInfo; use pallet_subtensor_proxy as pallet_proxy; use pallet_subtensor_proxy::WeightInfo; -use sp_runtime::{DispatchError, Weight, traits::StaticLookup}; +use sp_runtime::{DispatchError, traits::StaticLookup}; use sp_std::marker::PhantomData; use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaBalance, NetUid, ProxyType, TaoBalance}; use subtensor_swap_interface::SwapHandler; +/// `pallet-contracts` chain extension entry for Subtensor staking / proxy / query helpers. #[derive(DebugNoBound)] pub struct SubtensorChainExtension(PhantomData); @@ -566,7 +592,8 @@ where } } - fn dispatch(env: &mut Env) -> Result + /// Route a chain-extension call by [`FunctionId`], charging weight and mapping errors to [`Output`]. + pub(crate) fn dispatch(env: &mut Env) -> Result where Env: SubtensorExtensionEnv, <::Lookup as StaticLookup>::Source: From<::AccountId>, @@ -602,7 +629,7 @@ where let state = SubnetRegistrationState { netuid, - exists: pallet_subtensor::Pallet::::if_subnet_exist(netuid), + exists: pallet_subtensor::Pallet::::subnet_exists(netuid), registered_subnet_counter: pallet_subtensor::Pallet::::get_registered_subnet_counter(netuid), }; @@ -656,7 +683,7 @@ where } FunctionId::CallerAddStakeV1 => { - let origin = convert_origin(env.origin()); + let origin = contracts_origin_as_raw(env.origin()); Self::dispatch_add_stake_v1(env, origin) } @@ -665,7 +692,7 @@ where Self::dispatch_remove_stake_v1(env, origin) } FunctionId::CallerRemoveStakeV1 => { - let origin = convert_origin(env.origin()); + let origin = contracts_origin_as_raw(env.origin()); Self::dispatch_remove_stake_v1(env, origin) } FunctionId::UnstakeAllV1 => { @@ -673,7 +700,7 @@ where Self::dispatch_unstake_all_v1(env, origin) } FunctionId::CallerUnstakeAllV1 => { - let origin = convert_origin(env.origin()); + let origin = contracts_origin_as_raw(env.origin()); Self::dispatch_unstake_all_v1(env, origin) } FunctionId::UnstakeAllAlphaV1 => { @@ -681,7 +708,7 @@ where Self::dispatch_unstake_all_alpha_v1(env, origin) } FunctionId::CallerUnstakeAllAlphaV1 => { - let origin = convert_origin(env.origin()); + let origin = contracts_origin_as_raw(env.origin()); Self::dispatch_unstake_all_alpha_v1(env, origin) } FunctionId::MoveStakeV1 => { @@ -689,7 +716,7 @@ where Self::dispatch_move_stake_v1(env, origin) } FunctionId::CallerMoveStakeV1 => { - let origin = convert_origin(env.origin()); + let origin = contracts_origin_as_raw(env.origin()); Self::dispatch_move_stake_v1(env, origin) } FunctionId::TransferStakeV1 => { @@ -697,7 +724,7 @@ where Self::dispatch_transfer_stake_v1(env, origin) } FunctionId::CallerTransferStakeV1 => { - let origin = convert_origin(env.origin()); + let origin = contracts_origin_as_raw(env.origin()); Self::dispatch_transfer_stake_v1(env, origin) } FunctionId::SwapStakeV1 => { @@ -705,7 +732,7 @@ where Self::dispatch_swap_stake_v1(env, origin) } FunctionId::CallerSwapStakeV1 => { - let origin = convert_origin(env.origin()); + let origin = contracts_origin_as_raw(env.origin()); Self::dispatch_swap_stake_v1(env, origin) } FunctionId::AddStakeLimitV1 => { @@ -713,7 +740,7 @@ where Self::dispatch_add_stake_limit_v1(env, origin) } FunctionId::CallerAddStakeLimitV1 => { - let origin = convert_origin(env.origin()); + let origin = contracts_origin_as_raw(env.origin()); Self::dispatch_add_stake_limit_v1(env, origin) } FunctionId::RemoveStakeLimitV1 => { @@ -721,7 +748,7 @@ where Self::dispatch_remove_stake_limit_v1(env, origin) } FunctionId::CallerRemoveStakeLimitV1 => { - let origin = convert_origin(env.origin()); + let origin = contracts_origin_as_raw(env.origin()); Self::dispatch_remove_stake_limit_v1(env, origin) } FunctionId::SwapStakeLimitV1 => { @@ -729,7 +756,7 @@ where Self::dispatch_swap_stake_limit_v1(env, origin) } FunctionId::CallerSwapStakeLimitV1 => { - let origin = convert_origin(env.origin()); + let origin = contracts_origin_as_raw(env.origin()); Self::dispatch_swap_stake_limit_v1(env, origin) } FunctionId::RemoveStakeFullLimitV1 => { @@ -737,7 +764,7 @@ where Self::dispatch_remove_stake_full_limit_v1(env, origin) } FunctionId::CallerRemoveStakeFullLimitV1 => { - let origin = convert_origin(env.origin()); + let origin = contracts_origin_as_raw(env.origin()); Self::dispatch_remove_stake_full_limit_v1(env, origin) } FunctionId::SetColdkeyAutoStakeHotkeyV1 => { @@ -745,7 +772,7 @@ where Self::dispatch_set_coldkey_auto_stake_hotkey_v1(env, origin) } FunctionId::CallerSetColdkeyAutoStakeHotkeyV1 => { - let origin = convert_origin(env.origin()); + let origin = contracts_origin_as_raw(env.origin()); Self::dispatch_set_coldkey_auto_stake_hotkey_v1(env, origin) } FunctionId::AddProxyV1 => { @@ -753,7 +780,7 @@ where Self::dispatch_add_proxy_v1(env, origin) } FunctionId::CallerAddProxyV1 => { - let origin = convert_origin(env.origin()); + let origin = contracts_origin_as_raw(env.origin()); Self::dispatch_add_proxy_v1(env, origin) } FunctionId::RemoveProxyV1 => { @@ -761,7 +788,7 @@ where Self::dispatch_remove_proxy_v1(env, origin) } FunctionId::CallerRemoveProxyV1 => { - let origin = convert_origin(env.origin()); + let origin = contracts_origin_as_raw(env.origin()); Self::dispatch_remove_proxy_v1(env, origin) } FunctionId::GetAlphaPriceV1 => { @@ -909,81 +936,3 @@ where } } } - -// Convert from the contract origin to the raw origin -fn convert_origin(origin: pallet_contracts::Origin) -> RawOrigin -where - T: pallet_contracts::Config, -{ - match origin { - pallet_contracts::Origin::Signed(caller) => RawOrigin::Signed(caller), - pallet_contracts::Origin::Root => RawOrigin::Root, - } -} - -trait SubtensorExtensionEnv -where - T: pallet_contracts::Config, -{ - fn func_id(&self) -> u16; - fn charge_weight(&mut self, weight: Weight) -> Result<(), DispatchError>; - fn read_as(&mut self) -> Result; - fn write_output(&mut self, data: &[u8]) -> Result<(), DispatchError>; - fn caller(&mut self) -> T::AccountId; - #[allow(dead_code)] - fn origin(&mut self) -> pallet_contracts::Origin; -} - -struct ContractsEnvAdapter<'a, 'b, T, E> -where - T: pallet_subtensor::Config + pallet_contracts::Config, - E: Ext, -{ - env: Environment<'a, 'b, E, BufInBufOutState>, - _marker: PhantomData, -} - -impl<'a, 'b, T, E> ContractsEnvAdapter<'a, 'b, T, E> -where - T: pallet_subtensor::Config + pallet_contracts::Config, - T::AccountId: Clone, - E: Ext, -{ - fn new(env: Environment<'a, 'b, E, InitState>) -> Self { - Self { - env: env.buf_in_buf_out(), - _marker: PhantomData, - } - } -} - -impl<'a, 'b, T, E> SubtensorExtensionEnv for ContractsEnvAdapter<'a, 'b, T, E> -where - T: pallet_subtensor::Config + pallet_contracts::Config, - T::AccountId: Clone, - E: Ext, -{ - fn func_id(&self) -> u16 { - self.env.func_id() - } - - fn charge_weight(&mut self, weight: Weight) -> Result<(), DispatchError> { - self.env.charge_weight(weight).map(|_| ()) - } - - fn read_as(&mut self) -> Result { - self.env.read_as() - } - - fn write_output(&mut self, data: &[u8]) -> Result<(), DispatchError> { - self.env.write(data, false, None) - } - - fn caller(&mut self) -> T::AccountId { - self.env.ext().address().clone() - } - - fn origin(&mut self) -> pallet_contracts::Origin { - self.env.ext().caller() - } -} diff --git a/chain-extensions/src/mock.rs b/chain-extensions/src/mock.rs index 6887dc5822..ab6b3913d1 100644 --- a/chain-extensions/src/mock.rs +++ b/chain-extensions/src/mock.rs @@ -1,3 +1,8 @@ +//! Test runtime wiring `SubtensorChainExtension` into `pallet-contracts` for unit tests. +//! +//! Constructs a minimal `Test` runtime with Subtensor, Swap, Proxy, and Contracts so +//! [`crate::SubtensorChainExtension`] can be exercised via [`crate::tests`] mocks. + #![allow( clippy::arithmetic_side_effects, clippy::expect_used, @@ -437,7 +442,7 @@ impl pallet_subtensor::Config for Test { type LeaseDividendsDistributionInterval = LeaseDividendsDistributionInterval; type GetCommitments = (); type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; - type CommitmentsInterface = CommitmentsI; + type CommitmentsInterface = CommitmentsPurgeBridge; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; type AuthorshipProvider = MockAuthorshipProvider; type SubtensorPalletId = SubtensorPalletId; @@ -476,8 +481,8 @@ impl PrivilegeCmp for OriginPrivilegeCmp { } } -pub struct CommitmentsI; -impl CommitmentsInterface for CommitmentsI { +pub struct CommitmentsPurgeBridge; +impl CommitmentsInterface for CommitmentsPurgeBridge { fn purge_netuid( _netuid: NetUid, _weight_meter: &mut frame_support::weights::WeightMeter, @@ -651,8 +656,8 @@ pub fn init_logs_for_tests() { let _ = TEST_LOGS_INIT.set(()); } +/// Build genesis storage and set the current block number for a test. #[allow(dead_code)] -// Build genesis storage according to the mock runtime. pub fn new_test_ext(block_number: BlockNumber) -> sp_io::TestExternalities { init_logs_for_tests(); let t = frame_system::GenesisConfig::::default() @@ -663,6 +668,7 @@ pub fn new_test_ext(block_number: BlockNumber) -> sp_io::TestExternalities { ext } +/// Register `hotkey` on `netuid` via burned registration, topping up coldkey balance as needed. #[allow(dead_code)] pub fn register_ok_neuron( netuid: NetUid, @@ -744,6 +750,7 @@ pub fn remove_balance_from_coldkey_account(coldkey: &U256, tao: TaoBalance) { let _ = SubtensorModule::burn_tao(coldkey, tao); } +/// Register a new dynamic subnet owned by `coldkey`/`hotkey` and enable subtoken trading. #[allow(dead_code)] pub fn add_dynamic_network(hotkey: &U256, coldkey: &U256) -> NetUid { let netuid = SubtensorModule::get_next_netuid(); @@ -761,6 +768,7 @@ pub fn add_dynamic_network(hotkey: &U256, coldkey: &U256) -> NetUid { netuid } +/// Seed AMM reserves for `netuid` (TAO in / alpha in) used by stake and swap paths. #[allow(dead_code)] pub(crate) fn setup_reserves(netuid: NetUid, tao: TaoBalance, alpha: AlphaBalance) { SubnetTAO::::set(netuid, tao); diff --git a/chain-extensions/src/tests.rs b/chain-extensions/src/tests.rs deleted file mode 100644 index 3136cdc812..0000000000 --- a/chain-extensions/src/tests.rs +++ /dev/null @@ -1,2724 +0,0 @@ -#![allow(clippy::unwrap_used)] - -use super::{SubtensorChainExtension, SubtensorExtensionEnv, mock}; -use crate::types::{ColdkeyLock, FunctionId, Output, StakeAvailability, SubnetRegistrationState}; -use codec::{Decode, Encode}; -use frame_support::pallet_prelude::Zero; -use frame_support::{assert_ok, weights::Weight}; -use frame_system::RawOrigin; -use pallet_contracts::chain_extension::RetVal; -use pallet_subtensor::DefaultMinStake; -use pallet_subtensor::weights::WeightInfo as SubtensorWeightInfo; -use sp_core::Get; -use sp_core::U256; -use sp_runtime::DispatchError; -use substrate_fixed::types::U64F64; -use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; -use subtensor_swap_interface::SwapHandler; - -type AccountId = ::AccountId; - -#[derive(Clone)] -struct MockEnv { - func_id: u16, - caller: AccountId, - input: Vec, - output: Vec, - charged_weight: Option, - expected_weight: Option, -} - -#[allow(dead_code)] -pub fn add_balance_to_coldkey_account(coldkey: &U256, tao: TaoBalance) { - let credit = pallet_subtensor::Pallet::::mint_tao(tao); - let _ = pallet_subtensor::Pallet::::spend_tao(coldkey, credit, tao).unwrap(); -} - -#[test] -fn set_coldkey_auto_stake_hotkey_success_sets_destination() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(4901); - let owner_coldkey = U256::from(4902); - let coldkey = U256::from(5901); - let hotkey = U256::from(5902); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - - pallet_subtensor::Owner::::insert(hotkey, coldkey); - pallet_subtensor::OwnedHotkeys::::insert(coldkey, vec![hotkey]); - pallet_subtensor::Uids::::insert(netuid, hotkey, 0u16); - - assert_eq!( - pallet_subtensor::AutoStakeDestination::::get(coldkey, netuid), - None - ); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::set_coldkey_auto_stake_hotkey(); - - let mut env = MockEnv::new( - FunctionId::SetColdkeyAutoStakeHotkeyV1, - coldkey, - (netuid, hotkey).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert_eq!(env.charged_weight(), Some(expected_weight)); - - assert_eq!( - pallet_subtensor::AutoStakeDestination::::get(coldkey, netuid), - Some(hotkey) - ); - let coldkeys = - pallet_subtensor::AutoStakeDestinationColdkeys::::get(hotkey, netuid); - assert!(coldkeys.contains(&coldkey)); - }); -} - -#[test] -fn remove_stake_full_limit_success_with_limit_price() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(4801); - let owner_coldkey = U256::from(4802); - let coldkey = U256::from(5801); - let hotkey = U256::from(5802); - let stake_amount_raw: u64 = 340_000_000_000; - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - mock::setup_reserves( - netuid, - TaoBalance::from(130_000_000_000_u64), - AlphaBalance::from(110_000_000_000_u64), - ); - - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - - add_balance_to_coldkey_account( - &coldkey, - TaoBalance::from(stake_amount_raw + 1_000_000_000), - ); - - assert_ok!(pallet_subtensor::Pallet::::add_stake( - RawOrigin::Signed(coldkey).into(), - hotkey, - netuid, - stake_amount_raw.into(), - )); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::remove_stake_full_limit(); - - let balance_before = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); - - let mut env = MockEnv::new( - FunctionId::RemoveStakeFullLimitV1, - coldkey, - (hotkey, netuid, Option::::None).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert_eq!(env.charged_weight(), Some(expected_weight)); - - let alpha_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - let balance_after = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); - - assert!(alpha_after.is_zero()); - assert!(balance_after > balance_before); - }); -} - -#[test] -fn swap_stake_limit_with_tight_price_returns_slippage_error() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey_a = U256::from(4701); - let owner_coldkey_a = U256::from(4702); - let owner_hotkey_b = U256::from(4703); - let owner_coldkey_b = U256::from(4704); - let coldkey = U256::from(5701); - let hotkey = U256::from(5702); - - let stake_alpha = AlphaBalance::from(150_000_000_000u64); - - let netuid_a = mock::add_dynamic_network(&owner_hotkey_a, &owner_coldkey_a); - let netuid_b = mock::add_dynamic_network(&owner_hotkey_b, &owner_coldkey_b); - - mock::setup_reserves( - netuid_a, - TaoBalance::from(150_000_000_000_u64), - AlphaBalance::from(110_000_000_000_u64), - ); - mock::setup_reserves( - netuid_b, - TaoBalance::from(120_000_000_000_u64), - AlphaBalance::from(90_000_000_000_u64), - ); - - mock::register_ok_neuron(netuid_a, hotkey, coldkey, 0); - mock::register_ok_neuron(netuid_b, hotkey, coldkey, 1); - - pallet_subtensor::Pallet::::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - netuid_a, - stake_alpha, - ); - - let alpha_origin_before = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid_a, - ); - let alpha_destination_before = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid_b, - ); - - let alpha_to_swap: AlphaBalance = (alpha_origin_before.to_u64() / 8).into(); - let limit_price: TaoBalance = 100u64.into(); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::swap_stake_limit(); - - let mut env = MockEnv::new( - FunctionId::SwapStakeLimitV1, - coldkey, - (hotkey, netuid_a, netuid_b, alpha_to_swap, limit_price, true).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert_eq!(env.charged_weight(), Some(expected_weight)); - - let alpha_origin_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid_a, - ); - let alpha_destination_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid_b, - ); - - assert!(alpha_origin_after <= alpha_origin_before); - assert!(alpha_destination_after >= alpha_destination_before); - }); -} - -#[test] -fn remove_stake_limit_success_respects_price_limit() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(4601); - let owner_coldkey = U256::from(4602); - let coldkey = U256::from(5601); - let hotkey = U256::from(5602); - let stake_amount_raw: u64 = 320_000_000_000; - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - mock::setup_reserves( - netuid, - TaoBalance::from(120_000_000_000_u64), - AlphaBalance::from(100_000_000_000_u64), - ); - - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - - add_balance_to_coldkey_account( - &coldkey, - TaoBalance::from(stake_amount_raw + 1_000_000_000), - ); - - assert_ok!(pallet_subtensor::Pallet::::add_stake( - RawOrigin::Signed(coldkey).into(), - hotkey, - netuid, - stake_amount_raw.into(), - )); - - let alpha_before = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - - let current_price = - ::SwapInterface::current_alpha_price( - netuid.into(), - ); - let limit_price_value = (current_price.to_num::() * 990_000_000f64).round() as u64; - let limit_price: TaoBalance = limit_price_value.into(); - - let alpha_to_unstake: AlphaBalance = (alpha_before.to_u64() / 2).into(); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::remove_stake_limit(); - - let balance_before = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); - - let mut env = MockEnv::new( - FunctionId::RemoveStakeLimitV1, - coldkey, - (hotkey, netuid, alpha_to_unstake, limit_price, true).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert_eq!(env.charged_weight(), Some(expected_weight)); - - let alpha_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - let balance_after = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); - - assert!(alpha_after < alpha_before); - assert!(balance_after > balance_before); - }); -} - -#[test] -fn add_stake_limit_success_executes_within_price_guard() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(4501); - let owner_coldkey = U256::from(4502); - let coldkey = U256::from(5501); - let hotkey = U256::from(5502); - let amount_raw: u64 = 900_000_000_000; - let limit_price: TaoBalance = 24_000_000_000u64.into(); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - - mock::setup_reserves( - netuid, - TaoBalance::from(150_000_000_000_u64), - AlphaBalance::from(100_000_000_000_u64), - ); - - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - - add_balance_to_coldkey_account(&coldkey, (amount_raw + 1_000_000_000).into()); - - let stake_before = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - let balance_before = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::add_stake_limit(); - - let mut env = MockEnv::new( - FunctionId::AddStakeLimitV1, - coldkey, - ( - hotkey, - netuid, - TaoBalance::from(amount_raw), - limit_price, - true, - ) - .encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert_eq!(env.charged_weight(), Some(expected_weight)); - - let stake_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - let balance_after = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); - - assert!(stake_after > stake_before); - assert!(stake_after > AlphaBalance::ZERO); - assert!(balance_after < balance_before); - }); -} - -#[test] -fn swap_stake_success_moves_between_subnets() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey_a = U256::from(4401); - let owner_coldkey_a = U256::from(4402); - let owner_hotkey_b = U256::from(4403); - let owner_coldkey_b = U256::from(4404); - let coldkey = U256::from(5401); - let hotkey = U256::from(5402); - - let min_stake = DefaultMinStake::::get(); - let stake_amount_raw = min_stake.to_u64().saturating_mul(260); - - let netuid_a = mock::add_dynamic_network(&owner_hotkey_a, &owner_coldkey_a); - let netuid_b = mock::add_dynamic_network(&owner_hotkey_b, &owner_coldkey_b); - - mock::setup_reserves( - netuid_a, - stake_amount_raw.saturating_mul(18).into(), - AlphaBalance::from(stake_amount_raw.saturating_mul(30)), - ); - mock::setup_reserves( - netuid_b, - stake_amount_raw.saturating_mul(20).into(), - AlphaBalance::from(stake_amount_raw.saturating_mul(28)), - ); - - mock::register_ok_neuron(netuid_a, hotkey, coldkey, 0); - mock::register_ok_neuron(netuid_b, hotkey, coldkey, 1); - - add_balance_to_coldkey_account(&coldkey, (stake_amount_raw + 1_000_000_000).into()); - - assert_ok!(pallet_subtensor::Pallet::::add_stake( - RawOrigin::Signed(coldkey).into(), - hotkey, - netuid_a, - stake_amount_raw.into(), - )); - - let alpha_origin_before = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid_a, - ); - let alpha_destination_before = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid_b, - ); - let alpha_to_swap: AlphaBalance = (alpha_origin_before.to_u64() / 3).into(); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::swap_stake(); - - let mut env = MockEnv::new( - FunctionId::SwapStakeV1, - coldkey, - (hotkey, netuid_a, netuid_b, alpha_to_swap).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert_eq!(env.charged_weight(), Some(expected_weight)); - - let alpha_origin_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid_a, - ); - let alpha_destination_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid_b, - ); - - assert!(alpha_origin_after < alpha_origin_before); - assert!( - alpha_destination_after > alpha_destination_before, - "destination stake should increase" - ); - }); -} - -#[test] -fn transfer_stake_success_moves_between_coldkeys() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(4301); - let owner_coldkey = U256::from(4302); - let origin_coldkey = U256::from(5301); - let destination_coldkey = U256::from(5302); - let hotkey = U256::from(5303); - - let min_stake = DefaultMinStake::::get(); - let stake_amount_raw = min_stake.to_u64().saturating_mul(250); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - mock::setup_reserves( - netuid, - stake_amount_raw.saturating_mul(15).into(), - AlphaBalance::from(stake_amount_raw.saturating_mul(25)), - ); - - mock::register_ok_neuron(netuid, hotkey, origin_coldkey, 0); - - add_balance_to_coldkey_account(&origin_coldkey, (stake_amount_raw + 1_000_000_000).into()); - - assert_ok!(pallet_subtensor::Pallet::::add_stake( - RawOrigin::Signed(origin_coldkey).into(), - hotkey, - netuid, - stake_amount_raw.into(), - )); - - let alpha_before = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &origin_coldkey, - netuid, - ); - let alpha_to_transfer: AlphaBalance = (alpha_before.to_u64() / 3).into(); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::transfer_stake(); - - let mut env = MockEnv::new( - FunctionId::TransferStakeV1, - origin_coldkey, - ( - destination_coldkey, - hotkey, - netuid, - netuid, - alpha_to_transfer, - ) - .encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert_eq!(env.charged_weight(), Some(expected_weight)); - - let origin_alpha_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &origin_coldkey, - netuid, - ); - let destination_alpha_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &destination_coldkey, - netuid, - ); - - assert_eq!(origin_alpha_after, alpha_before - alpha_to_transfer); - assert_eq!(destination_alpha_after, alpha_to_transfer); - }); -} - -#[test] -fn move_stake_success_moves_alpha_between_hotkeys() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(4201); - let owner_coldkey = U256::from(4202); - let coldkey = U256::from(5201); - let origin_hotkey = U256::from(5202); - let destination_hotkey = U256::from(5203); - - let min_stake = DefaultMinStake::::get(); - let stake_amount_raw = min_stake.to_u64().saturating_mul(240); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - mock::setup_reserves( - netuid, - stake_amount_raw.saturating_mul(15).into(), - AlphaBalance::from(stake_amount_raw.saturating_mul(25)), - ); - - mock::register_ok_neuron(netuid, origin_hotkey, coldkey, 0); - mock::register_ok_neuron(netuid, destination_hotkey, coldkey, 1); - - add_balance_to_coldkey_account(&coldkey, (stake_amount_raw + 1_000_000_000).into()); - - assert_ok!(pallet_subtensor::Pallet::::add_stake( - RawOrigin::Signed(coldkey).into(), - origin_hotkey, - netuid, - stake_amount_raw.into(), - )); - - let alpha_before = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &origin_hotkey, - &coldkey, - netuid, - ); - let alpha_to_move: AlphaBalance = (alpha_before.to_u64() / 2).into(); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::move_stake(); - - let mut env = MockEnv::new( - FunctionId::MoveStakeV1, - coldkey, - ( - origin_hotkey, - destination_hotkey, - netuid, - netuid, - alpha_to_move, - ) - .encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert_eq!(env.charged_weight(), Some(expected_weight)); - - let origin_alpha_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &origin_hotkey, - &coldkey, - netuid, - ); - let destination_alpha_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &destination_hotkey, - &coldkey, - netuid, - ); - - assert_eq!(origin_alpha_after, alpha_before - alpha_to_move); - assert_eq!(destination_alpha_after, alpha_to_move); - }); -} - -#[test] -fn unstake_all_alpha_success_moves_stake_to_root() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(4101); - let owner_coldkey = U256::from(4102); - let coldkey = U256::from(5101); - let hotkey = U256::from(5102); - let min_stake = DefaultMinStake::::get(); - let stake_amount_raw = min_stake.to_u64().saturating_mul(220); - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - - mock::setup_reserves( - netuid, - stake_amount_raw.saturating_mul(20).into(), - AlphaBalance::from(stake_amount_raw.saturating_mul(30)), - ); - - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - add_balance_to_coldkey_account(&coldkey, (stake_amount_raw + 1_000_000_000).into()); - - assert_ok!(pallet_subtensor::Pallet::::add_stake( - RawOrigin::Signed(coldkey).into(), - hotkey, - netuid, - stake_amount_raw.into(), - )); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::unstake_all_alpha(); - - let mut env = MockEnv::new(FunctionId::UnstakeAllAlphaV1, coldkey, hotkey.encode()) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert_eq!(env.charged_weight(), Some(expected_weight)); - - let subnet_alpha = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - assert!(subnet_alpha <= AlphaBalance::from(1_000)); - - let root_alpha = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - NetUid::ROOT, - ); - assert!(root_alpha > AlphaBalance::ZERO); - }); -} - -#[test] -fn add_proxy_success_creates_proxy_relationship() { - mock::new_test_ext(1).execute_with(|| { - let delegator = U256::from(6001); - let delegate = U256::from(6002); - - add_balance_to_coldkey_account(&delegator, 1_000_000_000.into()); - - assert_eq!( - pallet_subtensor_proxy::Proxies::::get(delegator) - .0 - .len(), - 0 - ); - - let mut env = MockEnv::new(FunctionId::AddProxyV1, delegator, delegate.encode()); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - - let proxies = pallet_subtensor_proxy::Proxies::::get(delegator).0; - assert_eq!(proxies.len(), 1); - if let Some(proxy) = proxies.first() { - assert_eq!(proxy.delegate, delegate); - assert_eq!( - proxy.proxy_type, - subtensor_runtime_common::ProxyType::Staking - ); - assert_eq!(proxy.delay, 0u64); - } else { - panic!("proxies should contain one element"); - } - }); -} - -#[test] -fn remove_proxy_success_removes_proxy_relationship() { - mock::new_test_ext(1).execute_with(|| { - let delegator = U256::from(7001); - let delegate = U256::from(7002); - - add_balance_to_coldkey_account(&delegator, 1_000_000_000.into()); - - let mut add_env = MockEnv::new(FunctionId::AddProxyV1, delegator, delegate.encode()); - let ret = SubtensorChainExtension::::dispatch(&mut add_env).unwrap(); - assert_success(ret); - - let proxies_before = pallet_subtensor_proxy::Proxies::::get(delegator).0; - assert_eq!(proxies_before.len(), 1); - - let mut remove_env = MockEnv::new(FunctionId::RemoveProxyV1, delegator, delegate.encode()); - let ret = SubtensorChainExtension::::dispatch(&mut remove_env).unwrap(); - assert_success(ret); - - let proxies_after = pallet_subtensor_proxy::Proxies::::get(delegator).0; - assert_eq!(proxies_after.len(), 0); - }); -} - -#[test] -fn recycle_alpha_success_reduces_stake_and_returns_actual_amount() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(9001); - let owner_coldkey = U256::from(9002); - let coldkey = U256::from(9101); - let hotkey = U256::from(9102); - let min_stake = DefaultMinStake::::get(); - let stake_amount_raw = min_stake.to_u64().saturating_mul(200); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - mock::setup_reserves( - netuid, - TaoBalance::from(130_000_000_000_u64), - AlphaBalance::from(110_000_000_000_u64), - ); - - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - - add_balance_to_coldkey_account( - &coldkey, - TaoBalance::from(stake_amount_raw.saturating_add(1_000_000_000)), - ); - - assert_ok!(pallet_subtensor::Pallet::::add_stake( - RawOrigin::Signed(coldkey).into(), - hotkey, - netuid, - stake_amount_raw.into(), - )); - - let alpha_before = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - assert!(alpha_before > AlphaBalance::ZERO); - - let alpha_out_before = pallet_subtensor::SubnetAlphaOut::::get(netuid); - - let recycle_amount: AlphaBalance = (alpha_before.to_u64() / 2).into(); - - let expected_weight = - <::WeightInfo as SubtensorWeightInfo>::recycle_alpha(); - - let mut env = MockEnv::new( - FunctionId::RecycleAlphaV1, - coldkey, - (hotkey, netuid, recycle_amount).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert_eq!(env.charged_weight(), Some(expected_weight)); - - let returned_amount = AlphaBalance::decode(&mut env.output()).unwrap(); - assert_eq!(returned_amount, recycle_amount); - - let alpha_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - assert!(alpha_after < alpha_before); - - let alpha_out_after = pallet_subtensor::SubnetAlphaOut::::get(netuid); - assert!(alpha_out_after < alpha_out_before); - }); -} - -#[test] -fn recycle_alpha_on_root_subnet_returns_error() { - mock::new_test_ext(1).execute_with(|| { - let coldkey = U256::from(9201); - let hotkey = U256::from(9202); - - pallet_subtensor::Owner::::insert(hotkey, coldkey); - - let expected_weight = - <::WeightInfo as SubtensorWeightInfo>::recycle_alpha(); - - let mut env = MockEnv::new( - FunctionId::RecycleAlphaV1, - coldkey, - (hotkey, NetUid::ROOT, AlphaBalance::from(1_000u64)).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - match ret { - RetVal::Converging(code) => { - assert_ne!( - code, - Output::Success as u32, - "should not succeed on root subnet" - ) - } - _ => panic!("unexpected return value"), - } - }); -} - -#[test] -fn burn_alpha_success_reduces_stake_and_returns_actual_amount() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(9301); - let owner_coldkey = U256::from(9302); - let coldkey = U256::from(9401); - let hotkey = U256::from(9402); - let min_stake = DefaultMinStake::::get(); - let stake_amount_raw = min_stake.to_u64().saturating_mul(200); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - mock::setup_reserves( - netuid, - TaoBalance::from(130_000_000_000_u64), - AlphaBalance::from(110_000_000_000_u64), - ); - - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - - add_balance_to_coldkey_account( - &coldkey, - TaoBalance::from(stake_amount_raw.saturating_add(1_000_000_000)), - ); - - assert_ok!(pallet_subtensor::Pallet::::add_stake( - RawOrigin::Signed(coldkey).into(), - hotkey, - netuid, - stake_amount_raw.into(), - )); - - let alpha_before = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - assert!(alpha_before > AlphaBalance::ZERO); - - let alpha_out_before = pallet_subtensor::SubnetAlphaOut::::get(netuid); - - let burn_amount: AlphaBalance = (alpha_before.to_u64() / 2).into(); - - let expected_weight = - <::WeightInfo as SubtensorWeightInfo>::burn_alpha(); - - let mut env = MockEnv::new( - FunctionId::BurnAlphaV1, - coldkey, - (hotkey, netuid, burn_amount).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert_eq!(env.charged_weight(), Some(expected_weight)); - - let returned_amount = AlphaBalance::decode(&mut env.output()).unwrap(); - assert_eq!(returned_amount, burn_amount); - - let alpha_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - assert!(alpha_after < alpha_before); - - // Burn should NOT decrease SubnetAlphaOut (unlike recycle) - let alpha_out_after = pallet_subtensor::SubnetAlphaOut::::get(netuid); - assert_eq!(alpha_out_after, alpha_out_before); - }); -} - -#[test] -fn burn_alpha_on_nonexistent_subnet_returns_error() { - mock::new_test_ext(1).execute_with(|| { - let coldkey = U256::from(9501); - let hotkey = U256::from(9502); - - let expected_weight = - <::WeightInfo as SubtensorWeightInfo>::burn_alpha(); - - let mut env = MockEnv::new( - FunctionId::BurnAlphaV1, - coldkey, - (hotkey, NetUid::from(999u16), AlphaBalance::from(1_000u64)).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - match ret { - RetVal::Converging(code) => { - assert_eq!( - code, - Output::SubnetNotExists as u32, - "expected subnet not exists error" - ) - } - _ => panic!("unexpected return value"), - } - }); -} - -#[test] -fn add_stake_recycle_success_atomically_stakes_and_recycles() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(9601); - let owner_coldkey = U256::from(9602); - let coldkey = U256::from(9701); - let hotkey = U256::from(9702); - let min_stake = DefaultMinStake::::get(); - let tao_amount_raw = min_stake.to_u64().saturating_mul(200); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - mock::setup_reserves( - netuid, - TaoBalance::from(130_000_000_000_u64), - AlphaBalance::from(110_000_000_000_u64), - ); - - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - - add_balance_to_coldkey_account( - &coldkey, - TaoBalance::from(tao_amount_raw.saturating_add(1_000_000_000)), - ); - - let alpha_out_before = pallet_subtensor::SubnetAlphaOut::::get(netuid); - - let expected_weight = - <::WeightInfo as SubtensorWeightInfo>::add_stake() - .saturating_add( - <::WeightInfo as SubtensorWeightInfo>::recycle_alpha(), - ); - - let mut env = MockEnv::new( - FunctionId::AddStakeRecycleV1, - coldkey, - (hotkey, netuid, TaoBalance::from(tao_amount_raw)).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert_eq!(env.charged_weight(), Some(expected_weight)); - - let returned_alpha = AlphaBalance::decode(&mut env.output()).unwrap(); - assert!(returned_alpha > AlphaBalance::ZERO); - - // After atomic add+recycle, the stake should be zero (we recycled everything we added) - let alpha_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - assert!(alpha_after.is_zero()); - - // SubnetAlphaOut should not have increased (recycle cancels out the add) - let alpha_out_after = pallet_subtensor::SubnetAlphaOut::::get(netuid); - assert!(alpha_out_after <= alpha_out_before); - }); -} - -#[test] -fn add_stake_burn_success_atomically_stakes_and_burns() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(9801); - let owner_coldkey = U256::from(9802); - let coldkey = U256::from(9901); - let hotkey = U256::from(9902); - let min_stake = DefaultMinStake::::get(); - let tao_amount_raw = min_stake.to_u64().saturating_mul(200); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - mock::setup_reserves( - netuid, - TaoBalance::from(130_000_000_000_u64), - AlphaBalance::from(110_000_000_000_u64), - ); - - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - - add_balance_to_coldkey_account( - &coldkey, - TaoBalance::from(tao_amount_raw.saturating_add(1_000_000_000)), - ); - - let alpha_out_before = pallet_subtensor::SubnetAlphaOut::::get(netuid); - - let expected_weight = - <::WeightInfo as SubtensorWeightInfo>::add_stake() - .saturating_add( - <::WeightInfo as SubtensorWeightInfo>::burn_alpha(), - ); - - let mut env = MockEnv::new( - FunctionId::AddStakeBurnV1, - coldkey, - (hotkey, netuid, TaoBalance::from(tao_amount_raw)).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert_eq!(env.charged_weight(), Some(expected_weight)); - - let returned_alpha = AlphaBalance::decode(&mut env.output()).unwrap(); - assert!(returned_alpha > AlphaBalance::ZERO); - - // After atomic add+burn, the stake should be zero - let alpha_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - assert!(alpha_after.is_zero()); - - // SubnetAlphaOut should have increased (burn does NOT reduce AlphaOut) - let alpha_out_after = pallet_subtensor::SubnetAlphaOut::::get(netuid); - assert!(alpha_out_after > alpha_out_before); - }); -} - -#[test] -fn add_stake_recycle_with_insufficient_balance_returns_error() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(10001); - let owner_coldkey = U256::from(10002); - let coldkey = U256::from(10101); - let hotkey = U256::from(10102); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - mock::setup_reserves( - netuid, - TaoBalance::from(130_000_000_000_u64), - AlphaBalance::from(110_000_000_000_u64), - ); - - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Don't fund the coldkey - should fail with balance error - - let expected_weight = - <::WeightInfo as SubtensorWeightInfo>::add_stake() - .saturating_add( - <::WeightInfo as SubtensorWeightInfo>::recycle_alpha(), - ); - - let mut env = MockEnv::new( - FunctionId::AddStakeRecycleV1, - coldkey, - (hotkey, netuid, TaoBalance::from(100_000_000_000_u64)).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - match ret { - RetVal::Converging(code) => { - assert_ne!(code, Output::Success as u32, "should not succeed") - } - _ => panic!("unexpected return value"), - } - assert_eq!(env.charged_weight(), Some(expected_weight)); - }); -} - -#[test] -fn recycle_alpha_clamps_to_available_when_amount_exceeds_stake() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(11001); - let owner_coldkey = U256::from(11002); - let coldkey = U256::from(11101); - let hotkey = U256::from(11102); - let min_stake = DefaultMinStake::::get(); - let stake_amount_raw = min_stake.to_u64().saturating_mul(200); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - mock::setup_reserves( - netuid, - TaoBalance::from(130_000_000_000_u64), - AlphaBalance::from(110_000_000_000_u64), - ); - - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - - add_balance_to_coldkey_account( - &coldkey, - TaoBalance::from(stake_amount_raw.saturating_add(1_000_000_000)), - ); - - assert_ok!(pallet_subtensor::Pallet::::add_stake( - RawOrigin::Signed(coldkey).into(), - hotkey, - netuid, - stake_amount_raw.into(), - )); - - let alpha_before = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - assert!(alpha_before > AlphaBalance::ZERO); - - // Request way more than available — should clamp to alpha_before - let huge_amount = AlphaBalance::from(u64::MAX); - - let expected_weight = - <::WeightInfo as SubtensorWeightInfo>::recycle_alpha(); - - let mut env = MockEnv::new( - FunctionId::RecycleAlphaV1, - coldkey, - (hotkey, netuid, huge_amount).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - - let returned_amount = AlphaBalance::decode(&mut env.output()).unwrap(); - assert_eq!( - returned_amount, alpha_before, - "should return actual clamped amount, not requested amount" - ); - - let alpha_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - assert!(alpha_after.is_zero(), "all alpha should be recycled"); - }); -} - -#[test] -fn burn_alpha_on_root_subnet_returns_error() { - mock::new_test_ext(1).execute_with(|| { - let coldkey = U256::from(11201); - let hotkey = U256::from(11202); - - pallet_subtensor::Owner::::insert(hotkey, coldkey); - - let expected_weight = - <::WeightInfo as SubtensorWeightInfo>::burn_alpha(); - - let mut env = MockEnv::new( - FunctionId::BurnAlphaV1, - coldkey, - (hotkey, NetUid::ROOT, AlphaBalance::from(1_000u64)).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - match ret { - RetVal::Converging(code) => { - assert_ne!( - code, - Output::Success as u32, - "should not succeed on root subnet" - ) - } - _ => panic!("unexpected return value"), - } - }); -} - -#[test] -fn burn_alpha_clamps_to_available_when_amount_exceeds_stake() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(11301); - let owner_coldkey = U256::from(11302); - let coldkey = U256::from(11401); - let hotkey = U256::from(11402); - let min_stake = DefaultMinStake::::get(); - let stake_amount_raw = min_stake.to_u64().saturating_mul(200); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - mock::setup_reserves( - netuid, - TaoBalance::from(130_000_000_000_u64), - AlphaBalance::from(110_000_000_000_u64), - ); - - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - - add_balance_to_coldkey_account( - &coldkey, - TaoBalance::from(stake_amount_raw.saturating_add(1_000_000_000)), - ); - - assert_ok!(pallet_subtensor::Pallet::::add_stake( - RawOrigin::Signed(coldkey).into(), - hotkey, - netuid, - stake_amount_raw.into(), - )); - - let alpha_before = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - assert!(alpha_before > AlphaBalance::ZERO); - - // Request way more than available — should clamp to alpha_before - let huge_amount = AlphaBalance::from(u64::MAX); - - let expected_weight = - <::WeightInfo as SubtensorWeightInfo>::burn_alpha(); - - let mut env = MockEnv::new( - FunctionId::BurnAlphaV1, - coldkey, - (hotkey, netuid, huge_amount).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - - let returned_amount = AlphaBalance::decode(&mut env.output()).unwrap(); - assert_eq!( - returned_amount, alpha_before, - "should return actual clamped amount, not requested amount" - ); - - let alpha_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - assert!(alpha_after.is_zero(), "all alpha should be burned"); - }); -} - -impl MockEnv { - fn new(func_id: FunctionId, caller: AccountId, input: Vec) -> Self { - Self { - func_id: func_id as u16, - caller, - input, - output: Vec::new(), - charged_weight: None, - expected_weight: None, - } - } - - fn with_expected_weight(mut self, weight: Weight) -> Self { - self.expected_weight = Some(weight); - self - } - - fn charged_weight(&self) -> Option { - self.charged_weight - } - - fn output(&self) -> &[u8] { - &self.output - } -} - -impl SubtensorExtensionEnv for MockEnv { - fn func_id(&self) -> u16 { - self.func_id - } - - fn charge_weight(&mut self, weight: Weight) -> Result<(), DispatchError> { - let prev = self.charged_weight.unwrap_or_default(); - let cumulative = Weight::from_parts( - prev.ref_time().checked_add(weight.ref_time()).unwrap(), - prev.proof_size().checked_add(weight.proof_size()).unwrap(), - ); - if let Some(expected) = self.expected_weight - && (cumulative.ref_time() > expected.ref_time() - || cumulative.proof_size() > expected.proof_size()) - { - return Err(DispatchError::Other( - "unexpected weight charged by mock env", - )); - } - self.charged_weight = Some(cumulative); - Ok(()) - } - - fn read_as(&mut self) -> Result { - U::decode(&mut &self.input[..]).map_err(|_| DispatchError::Other("mock env decode failure")) - } - - fn write_output(&mut self, data: &[u8]) -> Result<(), DispatchError> { - self.output.clear(); - self.output.extend_from_slice(data); - Ok(()) - } - - fn caller(&mut self) -> AccountId { - self.caller - } - - fn origin(&mut self) -> pallet_contracts::Origin { - pallet_contracts::Origin::Signed(self.caller) - } -} - -fn assert_success(ret: RetVal) { - match ret { - RetVal::Converging(code) => { - assert_eq!(code, Output::Success as u32, "expected success code") - } - _ => panic!("unexpected return value"), - } -} - -#[test] -fn add_stake_recycle_rollback_on_recycle_failure() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(12001); - let owner_coldkey = U256::from(12002); - let coldkey = U256::from(12101); - let hotkey = U256::from(12102); - let min_stake = DefaultMinStake::::get(); - let tao_amount_raw = min_stake.to_u64().saturating_mul(200); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - pallet_subtensor::Pallet::::insert_lock_state( - &coldkey, - netuid, - &hotkey, - pallet_subtensor::staking::lock::LockState { - locked_mass: AlphaBalance::from(u64::MAX / 4), - conviction: U64F64::saturating_from_num(0), - last_update: pallet_subtensor::Pallet::::get_current_block_as_u64(), - }, - ); - - // Leave enough input-side liquidity for add_stake to pass the 1000x swap input cap. - // The lock above makes the recycle leg fail, exercising atomic rollback. - mock::setup_reserves( - netuid, - TaoBalance::from(tao_amount_raw / 1000 + 1), - AlphaBalance::from(1_000_u64), - ); - - add_balance_to_coldkey_account( - &coldkey, - TaoBalance::from(tao_amount_raw.saturating_add(1_000_000_000)), - ); - - let balance_before = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); - let alpha_before = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - - let expected_weight = - <::WeightInfo as SubtensorWeightInfo>::add_stake() - .saturating_add( - <::WeightInfo as SubtensorWeightInfo>::recycle_alpha(), - ); - - let mut env = MockEnv::new( - FunctionId::AddStakeRecycleV1, - coldkey, - (hotkey, netuid, TaoBalance::from(tao_amount_raw)).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - match ret { - RetVal::Converging(code) => { - assert_ne!(code, Output::Success as u32, "should not succeed") - } - _ => panic!("unexpected return value"), - } - - // Verify full rollback: balance and stake unchanged - let balance_after = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); - let alpha_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - - assert_eq!( - balance_before, balance_after, - "balance should be unchanged after rollback" - ); - assert_eq!( - alpha_before, alpha_after, - "stake should be unchanged after rollback" - ); - }); -} - -#[test] -fn add_stake_burn_rollback_on_burn_failure() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(12201); - let owner_coldkey = U256::from(12202); - let coldkey = U256::from(12301); - let hotkey = U256::from(12302); - let min_stake = DefaultMinStake::::get(); - let tao_amount_raw = min_stake.to_u64().saturating_mul(200); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - pallet_subtensor::Pallet::::insert_lock_state( - &coldkey, - netuid, - &hotkey, - pallet_subtensor::staking::lock::LockState { - locked_mass: AlphaBalance::from(u64::MAX / 4), - conviction: U64F64::saturating_from_num(0), - last_update: pallet_subtensor::Pallet::::get_current_block_as_u64(), - }, - ); - - // Leave enough input-side liquidity for add_stake to pass the 1000x swap input cap. - // The lock above makes the burn leg fail, exercising atomic rollback. - mock::setup_reserves( - netuid, - TaoBalance::from(tao_amount_raw / 1000 + 1), - AlphaBalance::from(1_000_u64), - ); - - add_balance_to_coldkey_account( - &coldkey, - TaoBalance::from(tao_amount_raw.saturating_add(1_000_000_000)), - ); - - let balance_before = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); - let alpha_before = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - - let expected_weight = - <::WeightInfo as SubtensorWeightInfo>::add_stake() - .saturating_add( - <::WeightInfo as SubtensorWeightInfo>::burn_alpha(), - ); - - let mut env = MockEnv::new( - FunctionId::AddStakeBurnV1, - coldkey, - (hotkey, netuid, TaoBalance::from(tao_amount_raw)).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - match ret { - RetVal::Converging(code) => { - assert_ne!(code, Output::Success as u32, "should not succeed") - } - _ => panic!("unexpected return value"), - } - - // Verify full rollback: balance and stake unchanged - let balance_after = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); - let alpha_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - - assert_eq!( - balance_before, balance_after, - "balance should be unchanged after rollback" - ); - assert_eq!( - alpha_before, alpha_after, - "stake should be unchanged after rollback" - ); - }); -} - -#[test] -fn get_stake_info_returns_encoded_runtime_value() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(1); - let owner_coldkey = U256::from(2); - let hotkey = U256::from(11); - let coldkey = U256::from(22); - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - - let expected = - pallet_subtensor::Pallet::::get_stake_info_for_hotkey_coldkey_netuid( - hotkey, coldkey, netuid, - ) - .encode(); - - let mut env = MockEnv::new( - FunctionId::GetStakeInfoForHotkeyColdkeyNetuidV1, - coldkey, - (hotkey, coldkey, netuid).encode(), - ); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - - assert_success(ret); - assert_eq!(env.output(), expected.as_slice()); - assert!(env.charged_weight().is_none()); - }); -} - -#[test] -fn add_stake_success_updates_stake_and_returns_success_code() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(1); - let owner_coldkey = U256::from(2); - let coldkey = U256::from(101); - let hotkey = U256::from(202); - let min_stake = DefaultMinStake::::get(); - let amount_raw = min_stake.to_u64().saturating_mul(10); - let amount: TaoBalance = amount_raw.into(); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - mock::setup_reserves( - netuid, - (amount_raw * 1_000_000).into(), - AlphaBalance::from(amount_raw * 10_000_000), - ); - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - - add_balance_to_coldkey_account(&coldkey, amount_raw.into()); - - assert!( - pallet_subtensor::Pallet::::get_total_stake_for_hotkey(&hotkey).is_zero() - ); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::add_stake(); - - let mut env = MockEnv::new( - FunctionId::AddStakeV1, - coldkey, - (hotkey, netuid, amount).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - - assert_success(ret); - assert_eq!(env.charged_weight(), Some(expected_weight)); - - let total_stake = - pallet_subtensor::Pallet::::get_total_stake_for_hotkey(&hotkey); - assert!(total_stake > TaoBalance::ZERO); - }); -} - -#[test] -fn remove_stake_with_no_stake_returns_amount_too_low() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(1); - let owner_coldkey = U256::from(2); - let coldkey = U256::from(301); - let hotkey = U256::from(302); - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - - let min_stake = DefaultMinStake::::get(); - let amount: AlphaBalance = AlphaBalance::from(min_stake.to_u64()); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::remove_stake(); - let mut env = MockEnv::new( - FunctionId::RemoveStakeV1, - coldkey, - (hotkey, netuid, amount).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - - match ret { - RetVal::Converging(code) => { - assert_eq!(code, Output::AmountTooLow as u32, "mismatched error output") - } - _ => panic!("unexpected return value"), - } - assert_eq!(env.charged_weight(), Some(expected_weight)); - assert!( - pallet_subtensor::Pallet::::get_total_stake_for_hotkey(&hotkey).is_zero() - ); - }); -} - -#[test] -fn unstake_all_success_unstakes_balance() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(4001); - let owner_coldkey = U256::from(4002); - let coldkey = U256::from(5001); - let hotkey = U256::from(5002); - let min_stake = DefaultMinStake::::get(); - let stake_amount_raw = min_stake.to_u64().saturating_mul(200); - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - - mock::setup_reserves( - netuid, - stake_amount_raw.saturating_mul(10).into(), - AlphaBalance::from(stake_amount_raw.saturating_mul(20)), - ); - - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - add_balance_to_coldkey_account(&coldkey, (stake_amount_raw + 1_000_000_000).into()); - - assert_ok!(pallet_subtensor::Pallet::::add_stake( - RawOrigin::Signed(coldkey).into(), - hotkey, - netuid, - stake_amount_raw.into(), - )); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::unstake_all(); - - let pre_balance = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); - - let mut env = MockEnv::new(FunctionId::UnstakeAllV1, coldkey, hotkey.encode()) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert_eq!(env.charged_weight(), Some(expected_weight)); - - let remaining_alpha = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - assert!(remaining_alpha <= AlphaBalance::from(1_000)); - - let post_balance = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); - assert!(post_balance > pre_balance); - }); -} - -#[test] -fn get_alpha_price_returns_encoded_price() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(8001); - let owner_coldkey = U256::from(8002); - let caller = U256::from(8003); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - - // Set up reserves to establish a price - let tao_reserve = TaoBalance::from(150_000_000_000u64); - let alpha_reserve = AlphaBalance::from(100_000_000_000u64); - mock::setup_reserves(netuid, tao_reserve, alpha_reserve); - - // Get expected price from swap handler - let expected_price = - as SwapHandler>::current_alpha_price( - netuid.into(), - ); - let expected_price_scaled = expected_price.saturating_mul(U64F64::from_num(1_000_000_000)); - let expected_price_u64: u64 = expected_price_scaled.saturating_to_num(); - - let mut env = MockEnv::new(FunctionId::GetAlphaPriceV1, caller, netuid.encode()); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert!(env.charged_weight().is_none()); - - // Decode the output - let output_price: u64 = Decode::decode(&mut &env.output()[..]).unwrap(); - - assert_eq!( - output_price, expected_price_u64, - "Price should match expected value" - ); - }); -} - -#[test] -fn get_subnet_registration_state_returns_existing_subnet_counter() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(8101); - let owner_coldkey = U256::from(8102); - let caller = U256::from(8103); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - let expected_counter = - pallet_subtensor::Pallet::::get_registered_subnet_counter(netuid); - - let mut env = MockEnv::new( - FunctionId::GetSubnetRegistrationStateV1, - caller, - netuid.encode(), - ); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert!(env.charged_weight().is_none()); - - let state = SubnetRegistrationState::decode(&mut &env.output()[..]).unwrap(); - assert_eq!( - state, - SubnetRegistrationState { - netuid, - exists: true, - registered_subnet_counter: expected_counter, - } - ); - }); -} - -#[test] -fn get_subnet_registration_state_preserves_counter_after_dissolve() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(8201); - let owner_coldkey = U256::from(8202); - let caller = U256::from(8203); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - let registered_counter = - pallet_subtensor::Pallet::::get_registered_subnet_counter(netuid); - - assert_ok!(pallet_subtensor::Pallet::::do_dissolve_network( - netuid - )); - - let mut env = MockEnv::new( - FunctionId::GetSubnetRegistrationStateV1, - caller, - netuid.encode(), - ); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - - let state = SubnetRegistrationState::decode(&mut &env.output()[..]).unwrap(); - assert_eq!(state.netuid, netuid); - assert!(!state.exists); - assert_eq!(state.registered_subnet_counter, registered_counter); - }); -} - -#[test] -fn get_subnet_registration_state_detects_reused_netuid_generation() { - mock::new_test_ext(1).execute_with(|| { - let first_hotkey = U256::from(8301); - let first_coldkey = U256::from(8302); - let second_hotkey = U256::from(8303); - let second_coldkey = U256::from(8304); - let caller = U256::from(8305); - - let netuid = mock::add_dynamic_network(&first_hotkey, &first_coldkey); - let first_counter = - pallet_subtensor::Pallet::::get_registered_subnet_counter(netuid); - - assert_ok!(pallet_subtensor::Pallet::::do_dissolve_network( - netuid - )); - - pallet_subtensor::Pallet::::remove_data_for_dissolved_networks( - Weight::from_parts(u64::MAX, u64::MAX), - ); - - let reused_netuid = mock::add_dynamic_network(&second_hotkey, &second_coldkey); - assert_eq!(reused_netuid, netuid); - - let mut env = MockEnv::new( - FunctionId::GetSubnetRegistrationStateV1, - caller, - netuid.encode(), - ); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - - let state = SubnetRegistrationState::decode(&mut &env.output()[..]).unwrap(); - assert_eq!(state.netuid, netuid); - assert!(state.exists); - assert!(state.registered_subnet_counter > first_counter); - }); -} - -#[test] -fn get_coldkey_lock_returns_none_without_lock() { - mock::new_test_ext(1).execute_with(|| { - let caller = U256::from(8401); - let coldkey = U256::from(8402); - let netuid = NetUid::from(1); - - let mut env = MockEnv::new( - FunctionId::GetColdkeyLockV1, - caller, - (coldkey, netuid).encode(), - ); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert!(env.charged_weight().is_none()); - - let lock = Option::::decode(&mut &env.output()[..]).unwrap(); - assert_eq!(lock, None); - }); -} - -#[test] -fn get_coldkey_lock_returns_rolled_lock() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(8501); - let owner_coldkey = U256::from(8502); - let coldkey = U256::from(8503); - let hotkey = U256::from(8504); - let stake = AlphaBalance::from(10_000u64); - let lock_amount = AlphaBalance::from(5_000u64); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - pallet_subtensor::Pallet::::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, stake, - ); - - assert_ok!(pallet_subtensor::Pallet::::do_lock_stake( - &coldkey, - netuid, - &hotkey, - lock_amount, - )); - - frame_system::Pallet::::set_block_number(1_001); - let expected = - pallet_subtensor::Pallet::::get_coldkey_lock(&coldkey, netuid).unwrap(); - - let mut env = MockEnv::new( - FunctionId::GetColdkeyLockV1, - coldkey, - (coldkey, netuid).encode(), - ); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert!(env.charged_weight().is_none()); - - let lock = Option::::decode(&mut &env.output()[..]) - .unwrap() - .unwrap(); - assert_eq!(lock.locked_mass, expected.locked_mass); - assert_eq!(lock.conviction_bits, expected.conviction.to_bits()); - assert!(lock.conviction_bits > 0); - assert_eq!( - lock.last_update, - pallet_subtensor::Pallet::::get_current_block_as_u64() - ); - }); -} - -#[test] -fn get_stake_availability_returns_zeroes_without_stake_or_lock() { - mock::new_test_ext(1).execute_with(|| { - let caller = U256::from(8601); - let coldkey = U256::from(8602); - let netuid = NetUid::from(1); - - let mut env = MockEnv::new( - FunctionId::GetStakeAvailabilityV1, - caller, - (coldkey, netuid).encode(), - ); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert!(env.charged_weight().is_none()); - - let availability = StakeAvailability::decode(&mut &env.output()[..]).unwrap(); - assert_eq!( - availability, - StakeAvailability { - netuid, - total: AlphaBalance::ZERO, - locked: AlphaBalance::ZERO, - available: AlphaBalance::ZERO, - } - ); - }); -} - -#[test] -fn get_stake_availability_returns_partial_lock_breakdown() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(8701); - let owner_coldkey = U256::from(8702); - let coldkey = U256::from(8703); - let hotkey = U256::from(8704); - let stake = AlphaBalance::from(10_000u64); - let lock_amount = AlphaBalance::from(4_000u64); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - pallet_subtensor::Pallet::::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, stake, - ); - assert_ok!(pallet_subtensor::Pallet::::do_lock_stake( - &coldkey, - netuid, - &hotkey, - lock_amount, - )); - - let mut env = MockEnv::new( - FunctionId::GetStakeAvailabilityV1, - coldkey, - (coldkey, netuid).encode(), - ); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert!(env.charged_weight().is_none()); - - let availability = StakeAvailability::decode(&mut &env.output()[..]).unwrap(); - assert_eq!( - availability, - StakeAvailability { - netuid, - total: stake, - locked: lock_amount, - available: stake.saturating_sub(lock_amount), - } - ); - }); -} - -#[test] -fn get_stake_availability_uses_rolled_forward_lock() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(8801); - let owner_coldkey = U256::from(8802); - let coldkey = U256::from(8803); - let hotkey = U256::from(8804); - let stake = AlphaBalance::from(10_000u64); - let lock_amount = AlphaBalance::from(5_000u64); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - pallet_subtensor::Pallet::::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, stake, - ); - assert_ok!(pallet_subtensor::Pallet::::do_lock_stake( - &coldkey, - netuid, - &hotkey, - lock_amount, - )); - - frame_system::Pallet::::set_block_number(1_001); - let expected_locked = - pallet_subtensor::Pallet::::get_current_locked(&coldkey, netuid); - assert!(expected_locked < lock_amount); - - let mut env = MockEnv::new( - FunctionId::GetStakeAvailabilityV1, - coldkey, - (coldkey, netuid).encode(), - ); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert!(env.charged_weight().is_none()); - - let availability = StakeAvailability::decode(&mut &env.output()[..]).unwrap(); - assert_eq!(availability.netuid, netuid); - assert_eq!(availability.total, stake); - assert_eq!(availability.locked, expected_locked); - assert_eq!( - availability.available, - stake.saturating_sub(expected_locked) - ); - }); -} - -/// `Caller*` dispatch uses `env.origin()` via `convert_origin`; with [`MockEnv`] both match -/// `Signed(caller)`, so outcomes align with non-`Caller` arms. Weight expectations match the shared -/// `dispatch_*_v1` helpers used by each pair. -mod caller_dispatch_tests { - use super::*; - - #[test] - fn caller_add_stake_success_updates_stake_and_returns_success_code() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(1); - let owner_coldkey = U256::from(2); - let coldkey = U256::from(10101); - let hotkey = U256::from(10202); - let min_stake = DefaultMinStake::::get(); - let amount_raw = min_stake.to_u64().saturating_mul(10); - let amount: TaoBalance = amount_raw.into(); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - mock::setup_reserves( - netuid, - (amount_raw * 1_000_000).into(), - AlphaBalance::from(amount_raw * 10_000_000), - ); - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - - mock::add_balance_to_coldkey_account( - &coldkey, - amount_raw.into(), - ); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::add_stake(); - - let mut env = MockEnv::new( - FunctionId::CallerAddStakeV1, - coldkey, - (hotkey, netuid, amount).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert_eq!(env.charged_weight(), Some(expected_weight)); - - let total_stake = - pallet_subtensor::Pallet::::get_total_stake_for_hotkey(&hotkey); - assert!(total_stake > TaoBalance::ZERO); - }); - } - - #[test] - fn caller_remove_stake_with_no_stake_returns_amount_too_low() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(1); - let owner_coldkey = U256::from(2); - let coldkey = U256::from(30301); - let hotkey = U256::from(30302); - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - - let min_stake = DefaultMinStake::::get(); - let amount: AlphaBalance = AlphaBalance::from(min_stake.to_u64()); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::remove_stake(); - let mut env = MockEnv::new( - FunctionId::CallerRemoveStakeV1, - coldkey, - (hotkey, netuid, amount).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - match ret { - RetVal::Converging(code) => { - assert_eq!(code, Output::AmountTooLow as u32, "mismatched error output") - } - _ => panic!("unexpected return value"), - } - assert_eq!(env.charged_weight(), Some(expected_weight)); - }); - } - - #[test] - fn caller_unstake_all_success_unstakes_balance() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(40001); - let owner_coldkey = U256::from(40002); - let coldkey = U256::from(50001); - let hotkey = U256::from(50002); - let min_stake = DefaultMinStake::::get(); - let stake_amount_raw = min_stake.to_u64().saturating_mul(200); - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - - mock::setup_reserves( - netuid, - stake_amount_raw.saturating_mul(10).into(), - AlphaBalance::from(stake_amount_raw.saturating_mul(20)), - ); - - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - mock::add_balance_to_coldkey_account( - &coldkey, - (stake_amount_raw + 1_000_000_000).into(), - ); - - assert_ok!(pallet_subtensor::Pallet::::add_stake( - RawOrigin::Signed(coldkey).into(), - hotkey, - netuid, - stake_amount_raw.into(), - )); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::unstake_all(); - - let pre_balance = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); - - let mut env = MockEnv::new(FunctionId::CallerUnstakeAllV1, coldkey, hotkey.encode()) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - - let remaining_alpha = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - assert!(remaining_alpha <= AlphaBalance::from(1_000)); - - let post_balance = - pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); - assert!(post_balance > pre_balance); - }); - } - - #[test] - fn caller_unstake_all_alpha_success_moves_stake_to_root() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(41001); - let owner_coldkey = U256::from(41002); - let coldkey = U256::from(51001); - let hotkey = U256::from(51002); - let min_stake = DefaultMinStake::::get(); - let stake_amount_raw = min_stake.to_u64().saturating_mul(220); - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - - mock::setup_reserves( - netuid, - stake_amount_raw.saturating_mul(20).into(), - AlphaBalance::from(stake_amount_raw.saturating_mul(30)), - ); - - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - mock::add_balance_to_coldkey_account( - &coldkey, - (stake_amount_raw + 1_000_000_000).into(), - ); - - assert_ok!(pallet_subtensor::Pallet::::add_stake( - RawOrigin::Signed(coldkey).into(), - hotkey, - netuid, - stake_amount_raw.into(), - )); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::unstake_all_alpha(); - - let mut env = MockEnv::new( - FunctionId::CallerUnstakeAllAlphaV1, - coldkey, - hotkey.encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - - let subnet_alpha = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - assert!(subnet_alpha <= AlphaBalance::from(1_000)); - - let root_alpha = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - NetUid::ROOT, - ); - assert!(root_alpha > AlphaBalance::ZERO); - }); - } - - #[test] - fn caller_move_stake_success_moves_alpha_between_hotkeys() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(42001); - let owner_coldkey = U256::from(42002); - let coldkey = U256::from(52001); - let origin_hotkey = U256::from(52002); - let destination_hotkey = U256::from(52003); - - let min_stake = DefaultMinStake::::get(); - let stake_amount_raw = min_stake.to_u64().saturating_mul(240); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - mock::setup_reserves( - netuid, - stake_amount_raw.saturating_mul(15).into(), - AlphaBalance::from(stake_amount_raw.saturating_mul(25)), - ); - - mock::register_ok_neuron(netuid, origin_hotkey, coldkey, 0); - mock::register_ok_neuron(netuid, destination_hotkey, coldkey, 1); - - mock::add_balance_to_coldkey_account( - &coldkey, - (stake_amount_raw + 1_000_000_000).into(), - ); - - assert_ok!(pallet_subtensor::Pallet::::add_stake( - RawOrigin::Signed(coldkey).into(), - origin_hotkey, - netuid, - stake_amount_raw.into(), - )); - - let alpha_before = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &origin_hotkey, - &coldkey, - netuid, - ); - let alpha_to_move: AlphaBalance = (alpha_before.to_u64() / 2).into(); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::move_stake(); - - let mut env = MockEnv::new( - FunctionId::CallerMoveStakeV1, - coldkey, - ( - origin_hotkey, - destination_hotkey, - netuid, - netuid, - alpha_to_move, - ) - .encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - - let origin_alpha_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &origin_hotkey, - &coldkey, - netuid, - ); - let destination_alpha_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &destination_hotkey, - &coldkey, - netuid, - ); - - assert_eq!(origin_alpha_after, alpha_before - alpha_to_move); - assert_eq!(destination_alpha_after, alpha_to_move); - }); - } - - #[test] - fn caller_transfer_stake_success_moves_between_coldkeys() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(43001); - let owner_coldkey = U256::from(43002); - let origin_coldkey = U256::from(53001); - let destination_coldkey = U256::from(53002); - let hotkey = U256::from(53003); - - let min_stake = DefaultMinStake::::get(); - let stake_amount_raw = min_stake.to_u64().saturating_mul(250); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - mock::setup_reserves( - netuid, - stake_amount_raw.saturating_mul(15).into(), - AlphaBalance::from(stake_amount_raw.saturating_mul(25)), - ); - - mock::register_ok_neuron(netuid, hotkey, origin_coldkey, 0); - - mock::add_balance_to_coldkey_account( - &origin_coldkey, - (stake_amount_raw + 1_000_000_000).into(), - ); - - assert_ok!(pallet_subtensor::Pallet::::add_stake( - RawOrigin::Signed(origin_coldkey).into(), - hotkey, - netuid, - stake_amount_raw.into(), - )); - - let alpha_before = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &origin_coldkey, - netuid, - ); - let alpha_to_transfer: AlphaBalance = (alpha_before.to_u64() / 3).into(); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::transfer_stake(); - - let mut env = MockEnv::new( - FunctionId::CallerTransferStakeV1, - origin_coldkey, - ( - destination_coldkey, - hotkey, - netuid, - netuid, - alpha_to_transfer, - ) - .encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - - let origin_alpha_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &origin_coldkey, - netuid, - ); - let destination_alpha_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &destination_coldkey, - netuid, - ); - - assert_eq!(origin_alpha_after, alpha_before - alpha_to_transfer); - assert_eq!(destination_alpha_after, alpha_to_transfer); - }); - } - - #[test] - fn caller_swap_stake_success_moves_between_subnets() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey_a = U256::from(44001); - let owner_coldkey_a = U256::from(44002); - let owner_hotkey_b = U256::from(44003); - let owner_coldkey_b = U256::from(44004); - let coldkey = U256::from(54001); - let hotkey = U256::from(54002); - - let min_stake = DefaultMinStake::::get(); - let stake_amount_raw = min_stake.to_u64().saturating_mul(260); - - let netuid_a = mock::add_dynamic_network(&owner_hotkey_a, &owner_coldkey_a); - let netuid_b = mock::add_dynamic_network(&owner_hotkey_b, &owner_coldkey_b); - - mock::setup_reserves( - netuid_a, - stake_amount_raw.saturating_mul(18).into(), - AlphaBalance::from(stake_amount_raw.saturating_mul(30)), - ); - mock::setup_reserves( - netuid_b, - stake_amount_raw.saturating_mul(20).into(), - AlphaBalance::from(stake_amount_raw.saturating_mul(28)), - ); - - mock::register_ok_neuron(netuid_a, hotkey, coldkey, 0); - mock::register_ok_neuron(netuid_b, hotkey, coldkey, 1); - - mock::add_balance_to_coldkey_account( - &coldkey, - (stake_amount_raw + 1_000_000_000).into(), - ); - - assert_ok!(pallet_subtensor::Pallet::::add_stake( - RawOrigin::Signed(coldkey).into(), - hotkey, - netuid_a, - stake_amount_raw.into(), - )); - - let alpha_origin_before = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid_a, - ); - let alpha_destination_before = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid_b, - ); - let alpha_to_swap: AlphaBalance = (alpha_origin_before.to_u64() / 3).into(); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::swap_stake(); - - let mut env = MockEnv::new( - FunctionId::CallerSwapStakeV1, - coldkey, - (hotkey, netuid_a, netuid_b, alpha_to_swap).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - - let alpha_origin_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid_a, - ); - let alpha_destination_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid_b, - ); - - assert!(alpha_origin_after < alpha_origin_before); - assert!(alpha_destination_after > alpha_destination_before); - }); - } - - #[test] - fn caller_add_stake_limit_success_executes_within_price_guard() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(45001); - let owner_coldkey = U256::from(45002); - let coldkey = U256::from(55001); - let hotkey = U256::from(55002); - let amount_raw: u64 = 900_000_000_000; - let limit_price: TaoBalance = 24_000_000_000u64.into(); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - - mock::setup_reserves( - netuid, - TaoBalance::from(150_000_000_000_u64), - AlphaBalance::from(100_000_000_000_u64), - ); - - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - - mock::add_balance_to_coldkey_account( - &coldkey, - (amount_raw + 1_000_000_000).into(), - ); - - let stake_before = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - let balance_before = - pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::add_stake_limit(); - - let mut env = MockEnv::new( - FunctionId::CallerAddStakeLimitV1, - coldkey, - ( - hotkey, - netuid, - TaoBalance::from(amount_raw), - limit_price, - true, - ) - .encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - - let stake_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - let balance_after = - pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); - - assert!(stake_after > stake_before); - assert!(stake_after > AlphaBalance::ZERO); - assert!(balance_after < balance_before); - }); - } - - #[test] - fn caller_remove_stake_limit_success_respects_price_limit() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(46001); - let owner_coldkey = U256::from(46002); - let coldkey = U256::from(56001); - let hotkey = U256::from(56002); - let stake_amount_raw: u64 = 320_000_000_000; - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - mock::setup_reserves( - netuid, - TaoBalance::from(120_000_000_000_u64), - AlphaBalance::from(100_000_000_000_u64), - ); - - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - - mock::add_balance_to_coldkey_account( - &coldkey, - TaoBalance::from(stake_amount_raw + 1_000_000_000), - ); - - assert_ok!(pallet_subtensor::Pallet::::add_stake( - RawOrigin::Signed(coldkey).into(), - hotkey, - netuid, - stake_amount_raw.into(), - )); - - let alpha_before = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - - let current_price = - ::SwapInterface::current_alpha_price( - netuid.into(), - ); - let limit_price_value = (current_price.to_num::() * 990_000_000f64).round() as u64; - let limit_price: TaoBalance = limit_price_value.into(); - - let alpha_to_unstake: AlphaBalance = (alpha_before.to_u64() / 2).into(); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::remove_stake_limit(); - - let balance_before = - pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); - - let mut env = MockEnv::new( - FunctionId::CallerRemoveStakeLimitV1, - coldkey, - (hotkey, netuid, alpha_to_unstake, limit_price, true).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert_eq!(env.charged_weight(), Some(expected_weight)); - - let alpha_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - let balance_after = - pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); - - assert!(alpha_after < alpha_before); - assert!(balance_after > balance_before); - }); - } - - #[test] - fn caller_swap_stake_limit_matches_standard_slippage_path() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey_a = U256::from(47001); - let owner_coldkey_a = U256::from(47002); - let owner_hotkey_b = U256::from(47003); - let owner_coldkey_b = U256::from(47004); - let coldkey = U256::from(57001); - let hotkey = U256::from(57002); - - let stake_alpha = AlphaBalance::from(150_000_000_000u64); - - let netuid_a = mock::add_dynamic_network(&owner_hotkey_a, &owner_coldkey_a); - let netuid_b = mock::add_dynamic_network(&owner_hotkey_b, &owner_coldkey_b); - - mock::setup_reserves( - netuid_a, - TaoBalance::from(150_000_000_000_u64), - AlphaBalance::from(110_000_000_000_u64), - ); - mock::setup_reserves( - netuid_b, - TaoBalance::from(120_000_000_000_u64), - AlphaBalance::from(90_000_000_000_u64), - ); - - mock::register_ok_neuron(netuid_a, hotkey, coldkey, 0); - mock::register_ok_neuron(netuid_b, hotkey, coldkey, 1); - - pallet_subtensor::Pallet::::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - netuid_a, - stake_alpha, - ); - - let alpha_origin_before = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid_a, - ); - let alpha_destination_before = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid_b, - ); - - let alpha_to_swap: AlphaBalance = (alpha_origin_before.to_u64() / 8).into(); - let limit_price: TaoBalance = 100u64.into(); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::swap_stake_limit(); - - let mut env = MockEnv::new( - FunctionId::CallerSwapStakeLimitV1, - coldkey, - (hotkey, netuid_a, netuid_b, alpha_to_swap, limit_price, true).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - - let alpha_origin_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid_a, - ); - let alpha_destination_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid_b, - ); - - assert!(alpha_origin_after <= alpha_origin_before); - assert!(alpha_destination_after >= alpha_destination_before); - }); - } - - #[test] - fn caller_remove_stake_full_limit_success_with_limit_price() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(48001); - let owner_coldkey = U256::from(48002); - let coldkey = U256::from(58001); - let hotkey = U256::from(58002); - let stake_amount_raw: u64 = 340_000_000_000; - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - mock::setup_reserves( - netuid, - TaoBalance::from(130_000_000_000_u64), - AlphaBalance::from(110_000_000_000_u64), - ); - - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - - mock::add_balance_to_coldkey_account( - &coldkey, - TaoBalance::from(stake_amount_raw + 1_000_000_000), - ); - - assert_ok!(pallet_subtensor::Pallet::::add_stake( - RawOrigin::Signed(coldkey).into(), - hotkey, - netuid, - stake_amount_raw.into(), - )); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::remove_stake_full_limit(); - - let balance_before = - pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); - - let mut env = MockEnv::new( - FunctionId::CallerRemoveStakeFullLimitV1, - coldkey, - (hotkey, netuid, Option::::None).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert_eq!(env.charged_weight(), Some(expected_weight)); - - let alpha_after = - pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid, - ); - let balance_after = - pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); - - assert!(alpha_after.is_zero()); - assert!(balance_after > balance_before); - }); - } - - #[test] - fn caller_set_coldkey_auto_stake_hotkey_success_sets_destination() { - mock::new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(49001); - let owner_coldkey = U256::from(49002); - let coldkey = U256::from(59001); - let hotkey = U256::from(59002); - - let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - - pallet_subtensor::Owner::::insert(hotkey, coldkey); - pallet_subtensor::OwnedHotkeys::::insert(coldkey, vec![hotkey]); - pallet_subtensor::Uids::::insert(netuid, hotkey, 0u16); - - assert_eq!( - pallet_subtensor::AutoStakeDestination::::get(coldkey, netuid), - None - ); - - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::set_coldkey_auto_stake_hotkey(); - - let mut env = MockEnv::new( - FunctionId::CallerSetColdkeyAutoStakeHotkeyV1, - coldkey, - (netuid, hotkey).encode(), - ) - .with_expected_weight(expected_weight); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - assert_eq!(env.charged_weight(), Some(expected_weight)); - - assert_eq!( - pallet_subtensor::AutoStakeDestination::::get(coldkey, netuid), - Some(hotkey) - ); - }); - } - - #[test] - fn caller_add_proxy_success_creates_proxy_relationship() { - mock::new_test_ext(1).execute_with(|| { - let delegator = U256::from(60001); - let delegate = U256::from(60002); - - mock::add_balance_to_coldkey_account(&delegator, 1_000_000_000.into()); - - let mut env = MockEnv::new(FunctionId::CallerAddProxyV1, delegator, delegate.encode()); - - let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); - assert_success(ret); - - let proxies = pallet_subtensor_proxy::Proxies::::get(delegator).0; - assert_eq!(proxies.len(), 1); - }); - } - - #[test] - fn caller_remove_proxy_success_removes_proxy_relationship() { - mock::new_test_ext(1).execute_with(|| { - let delegator = U256::from(70001); - let delegate = U256::from(70002); - - mock::add_balance_to_coldkey_account(&delegator, 1_000_000_000.into()); - - let mut add_env = - MockEnv::new(FunctionId::CallerAddProxyV1, delegator, delegate.encode()); - assert_success(SubtensorChainExtension::::dispatch(&mut add_env).unwrap()); - - let mut remove_env = MockEnv::new( - FunctionId::CallerRemoveProxyV1, - delegator, - delegate.encode(), - ); - let ret = SubtensorChainExtension::::dispatch(&mut remove_env).unwrap(); - assert_success(ret); - - let proxies_after = pallet_subtensor_proxy::Proxies::::get(delegator).0; - assert_eq!(proxies_after.len(), 0); - }); - } -} diff --git a/chain-extensions/src/tests/alpha_recycle_burn.rs b/chain-extensions/src/tests/alpha_recycle_burn.rs new file mode 100644 index 0000000000..76a75c5566 --- /dev/null +++ b/chain-extensions/src/tests/alpha_recycle_burn.rs @@ -0,0 +1,694 @@ +//! Recycle/burn alpha and atomic add-stake+recycle/burn dispatch paths. + +use super::*; + +#[test] +fn recycle_alpha_success_reduces_stake_and_returns_actual_amount() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(9001); + let owner_coldkey = U256::from(9002); + let coldkey = U256::from(9101); + let hotkey = U256::from(9102); + let min_stake = DefaultMinStake::::get(); + let stake_amount_raw = min_stake.to_u64().saturating_mul(200); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + TaoBalance::from(130_000_000_000_u64), + AlphaBalance::from(110_000_000_000_u64), + ); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + add_balance_to_coldkey_account( + &coldkey, + TaoBalance::from(stake_amount_raw.saturating_add(1_000_000_000)), + ); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(coldkey).into(), + hotkey, + netuid, + stake_amount_raw.into(), + )); + + let alpha_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + assert!(alpha_before > AlphaBalance::ZERO); + + let alpha_out_before = pallet_subtensor::SubnetAlphaOut::::get(netuid); + + let recycle_amount: AlphaBalance = (alpha_before.to_u64() / 2).into(); + + let expected_weight = + <::WeightInfo as SubtensorWeightInfo>::recycle_alpha(); + + let mut env = MockEnv::new( + FunctionId::RecycleAlphaV1, + coldkey, + (hotkey, netuid, recycle_amount).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let returned_amount = AlphaBalance::decode(&mut env.output()).unwrap(); + assert_eq!(returned_amount, recycle_amount); + + let alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + assert!(alpha_after < alpha_before); + + let alpha_out_after = pallet_subtensor::SubnetAlphaOut::::get(netuid); + assert!(alpha_out_after < alpha_out_before); + }); +} + +#[test] +fn recycle_alpha_on_root_subnet_returns_error() { + mock::new_test_ext(1).execute_with(|| { + let coldkey = U256::from(9201); + let hotkey = U256::from(9202); + + pallet_subtensor::Owner::::insert(hotkey, coldkey); + + let expected_weight = + <::WeightInfo as SubtensorWeightInfo>::recycle_alpha(); + + let mut env = MockEnv::new( + FunctionId::RecycleAlphaV1, + coldkey, + (hotkey, NetUid::ROOT, AlphaBalance::from(1_000u64)).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + match ret { + RetVal::Converging(code) => { + assert_ne!( + code, + Output::Success as u32, + "should not succeed on root subnet" + ) + } + _ => panic!("unexpected return value"), + } + }); +} + +#[test] +fn burn_alpha_success_reduces_stake_and_returns_actual_amount() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(9301); + let owner_coldkey = U256::from(9302); + let coldkey = U256::from(9401); + let hotkey = U256::from(9402); + let min_stake = DefaultMinStake::::get(); + let stake_amount_raw = min_stake.to_u64().saturating_mul(200); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + TaoBalance::from(130_000_000_000_u64), + AlphaBalance::from(110_000_000_000_u64), + ); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + add_balance_to_coldkey_account( + &coldkey, + TaoBalance::from(stake_amount_raw.saturating_add(1_000_000_000)), + ); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(coldkey).into(), + hotkey, + netuid, + stake_amount_raw.into(), + )); + + let alpha_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + assert!(alpha_before > AlphaBalance::ZERO); + + let alpha_out_before = pallet_subtensor::SubnetAlphaOut::::get(netuid); + + let burn_amount: AlphaBalance = (alpha_before.to_u64() / 2).into(); + + let expected_weight = + <::WeightInfo as SubtensorWeightInfo>::burn_alpha(); + + let mut env = MockEnv::new( + FunctionId::BurnAlphaV1, + coldkey, + (hotkey, netuid, burn_amount).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let returned_amount = AlphaBalance::decode(&mut env.output()).unwrap(); + assert_eq!(returned_amount, burn_amount); + + let alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + assert!(alpha_after < alpha_before); + + // Burn should NOT decrease SubnetAlphaOut (unlike recycle) + let alpha_out_after = pallet_subtensor::SubnetAlphaOut::::get(netuid); + assert_eq!(alpha_out_after, alpha_out_before); + }); +} + +#[test] +fn burn_alpha_on_nonexistent_subnet_returns_error() { + mock::new_test_ext(1).execute_with(|| { + let coldkey = U256::from(9501); + let hotkey = U256::from(9502); + + let expected_weight = + <::WeightInfo as SubtensorWeightInfo>::burn_alpha(); + + let mut env = MockEnv::new( + FunctionId::BurnAlphaV1, + coldkey, + (hotkey, NetUid::from(999u16), AlphaBalance::from(1_000u64)).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + match ret { + RetVal::Converging(code) => { + assert_eq!( + code, + Output::SubnetNotExists as u32, + "expected subnet not exists error" + ) + } + _ => panic!("unexpected return value"), + } + }); +} + +#[test] +fn add_stake_recycle_success_atomically_stakes_and_recycles() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(9601); + let owner_coldkey = U256::from(9602); + let coldkey = U256::from(9701); + let hotkey = U256::from(9702); + let min_stake = DefaultMinStake::::get(); + let tao_amount_raw = min_stake.to_u64().saturating_mul(200); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + TaoBalance::from(130_000_000_000_u64), + AlphaBalance::from(110_000_000_000_u64), + ); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + add_balance_to_coldkey_account( + &coldkey, + TaoBalance::from(tao_amount_raw.saturating_add(1_000_000_000)), + ); + + let alpha_out_before = pallet_subtensor::SubnetAlphaOut::::get(netuid); + + let expected_weight = + <::WeightInfo as SubtensorWeightInfo>::add_stake() + .saturating_add( + <::WeightInfo as SubtensorWeightInfo>::recycle_alpha(), + ); + + let mut env = MockEnv::new( + FunctionId::AddStakeRecycleV1, + coldkey, + (hotkey, netuid, TaoBalance::from(tao_amount_raw)).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let returned_alpha = AlphaBalance::decode(&mut env.output()).unwrap(); + assert!(returned_alpha > AlphaBalance::ZERO); + + // After atomic add+recycle, the stake should be zero (we recycled everything we added) + let alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + assert!(alpha_after.is_zero()); + + // SubnetAlphaOut should not have increased (recycle cancels out the add) + let alpha_out_after = pallet_subtensor::SubnetAlphaOut::::get(netuid); + assert!(alpha_out_after <= alpha_out_before); + }); +} + +#[test] +fn add_stake_burn_success_atomically_stakes_and_burns() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(9801); + let owner_coldkey = U256::from(9802); + let coldkey = U256::from(9901); + let hotkey = U256::from(9902); + let min_stake = DefaultMinStake::::get(); + let tao_amount_raw = min_stake.to_u64().saturating_mul(200); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + TaoBalance::from(130_000_000_000_u64), + AlphaBalance::from(110_000_000_000_u64), + ); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + add_balance_to_coldkey_account( + &coldkey, + TaoBalance::from(tao_amount_raw.saturating_add(1_000_000_000)), + ); + + let alpha_out_before = pallet_subtensor::SubnetAlphaOut::::get(netuid); + + let expected_weight = + <::WeightInfo as SubtensorWeightInfo>::add_stake() + .saturating_add( + <::WeightInfo as SubtensorWeightInfo>::burn_alpha(), + ); + + let mut env = MockEnv::new( + FunctionId::AddStakeBurnV1, + coldkey, + (hotkey, netuid, TaoBalance::from(tao_amount_raw)).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let returned_alpha = AlphaBalance::decode(&mut env.output()).unwrap(); + assert!(returned_alpha > AlphaBalance::ZERO); + + // After atomic add+burn, the stake should be zero + let alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + assert!(alpha_after.is_zero()); + + // SubnetAlphaOut should have increased (burn does NOT reduce AlphaOut) + let alpha_out_after = pallet_subtensor::SubnetAlphaOut::::get(netuid); + assert!(alpha_out_after > alpha_out_before); + }); +} + +#[test] +fn add_stake_recycle_with_insufficient_balance_returns_error() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(10001); + let owner_coldkey = U256::from(10002); + let coldkey = U256::from(10101); + let hotkey = U256::from(10102); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + TaoBalance::from(130_000_000_000_u64), + AlphaBalance::from(110_000_000_000_u64), + ); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Don't fund the coldkey - should fail with balance error + + let expected_weight = + <::WeightInfo as SubtensorWeightInfo>::add_stake() + .saturating_add( + <::WeightInfo as SubtensorWeightInfo>::recycle_alpha(), + ); + + let mut env = MockEnv::new( + FunctionId::AddStakeRecycleV1, + coldkey, + (hotkey, netuid, TaoBalance::from(100_000_000_000_u64)).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + match ret { + RetVal::Converging(code) => { + assert_ne!(code, Output::Success as u32, "should not succeed") + } + _ => panic!("unexpected return value"), + } + assert_eq!(env.charged_weight(), Some(expected_weight)); + }); +} + +#[test] +fn recycle_alpha_clamps_to_available_when_amount_exceeds_stake() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(11001); + let owner_coldkey = U256::from(11002); + let coldkey = U256::from(11101); + let hotkey = U256::from(11102); + let min_stake = DefaultMinStake::::get(); + let stake_amount_raw = min_stake.to_u64().saturating_mul(200); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + TaoBalance::from(130_000_000_000_u64), + AlphaBalance::from(110_000_000_000_u64), + ); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + add_balance_to_coldkey_account( + &coldkey, + TaoBalance::from(stake_amount_raw.saturating_add(1_000_000_000)), + ); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(coldkey).into(), + hotkey, + netuid, + stake_amount_raw.into(), + )); + + let alpha_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + assert!(alpha_before > AlphaBalance::ZERO); + + // Request way more than available — should clamp to alpha_before + let huge_amount = AlphaBalance::from(u64::MAX); + + let expected_weight = + <::WeightInfo as SubtensorWeightInfo>::recycle_alpha(); + + let mut env = MockEnv::new( + FunctionId::RecycleAlphaV1, + coldkey, + (hotkey, netuid, huge_amount).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + + let returned_amount = AlphaBalance::decode(&mut env.output()).unwrap(); + assert_eq!( + returned_amount, alpha_before, + "should return actual clamped amount, not requested amount" + ); + + let alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + assert!(alpha_after.is_zero(), "all alpha should be recycled"); + }); +} + +#[test] +fn burn_alpha_on_root_subnet_returns_error() { + mock::new_test_ext(1).execute_with(|| { + let coldkey = U256::from(11201); + let hotkey = U256::from(11202); + + pallet_subtensor::Owner::::insert(hotkey, coldkey); + + let expected_weight = + <::WeightInfo as SubtensorWeightInfo>::burn_alpha(); + + let mut env = MockEnv::new( + FunctionId::BurnAlphaV1, + coldkey, + (hotkey, NetUid::ROOT, AlphaBalance::from(1_000u64)).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + match ret { + RetVal::Converging(code) => { + assert_ne!( + code, + Output::Success as u32, + "should not succeed on root subnet" + ) + } + _ => panic!("unexpected return value"), + } + }); +} + +#[test] +fn burn_alpha_clamps_to_available_when_amount_exceeds_stake() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(11301); + let owner_coldkey = U256::from(11302); + let coldkey = U256::from(11401); + let hotkey = U256::from(11402); + let min_stake = DefaultMinStake::::get(); + let stake_amount_raw = min_stake.to_u64().saturating_mul(200); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + TaoBalance::from(130_000_000_000_u64), + AlphaBalance::from(110_000_000_000_u64), + ); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + add_balance_to_coldkey_account( + &coldkey, + TaoBalance::from(stake_amount_raw.saturating_add(1_000_000_000)), + ); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(coldkey).into(), + hotkey, + netuid, + stake_amount_raw.into(), + )); + + let alpha_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + assert!(alpha_before > AlphaBalance::ZERO); + + // Request way more than available — should clamp to alpha_before + let huge_amount = AlphaBalance::from(u64::MAX); + + let expected_weight = + <::WeightInfo as SubtensorWeightInfo>::burn_alpha(); + + let mut env = MockEnv::new( + FunctionId::BurnAlphaV1, + coldkey, + (hotkey, netuid, huge_amount).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + + let returned_amount = AlphaBalance::decode(&mut env.output()).unwrap(); + assert_eq!( + returned_amount, alpha_before, + "should return actual clamped amount, not requested amount" + ); + + let alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + assert!(alpha_after.is_zero(), "all alpha should be burned"); + }); +} +#[test] +fn add_stake_recycle_rollback_on_recycle_failure() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(12001); + let owner_coldkey = U256::from(12002); + let coldkey = U256::from(12101); + let hotkey = U256::from(12102); + let min_stake = DefaultMinStake::::get(); + let tao_amount_raw = min_stake.to_u64().saturating_mul(200); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + pallet_subtensor::Pallet::::insert_lock_state( + &coldkey, + netuid, + &hotkey, + pallet_subtensor::staking::lock::LockState { + locked_mass: AlphaBalance::from(u64::MAX / 4), + conviction: U64F64::saturating_from_num(0), + last_update: pallet_subtensor::Pallet::::get_current_block_as_u64(), + }, + ); + + // Leave enough input-side liquidity for add_stake to pass the 1000x swap input cap. + // The lock above makes the recycle leg fail, exercising atomic rollback. + mock::setup_reserves( + netuid, + TaoBalance::from(tao_amount_raw / 1000 + 1), + AlphaBalance::from(1_000_u64), + ); + + add_balance_to_coldkey_account( + &coldkey, + TaoBalance::from(tao_amount_raw.saturating_add(1_000_000_000)), + ); + + let balance_before = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + let alpha_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + + let expected_weight = + <::WeightInfo as SubtensorWeightInfo>::add_stake() + .saturating_add( + <::WeightInfo as SubtensorWeightInfo>::recycle_alpha(), + ); + + let mut env = MockEnv::new( + FunctionId::AddStakeRecycleV1, + coldkey, + (hotkey, netuid, TaoBalance::from(tao_amount_raw)).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + match ret { + RetVal::Converging(code) => { + assert_ne!(code, Output::Success as u32, "should not succeed") + } + _ => panic!("unexpected return value"), + } + + // Verify full rollback: balance and stake unchanged + let balance_after = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + let alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + + assert_eq!( + balance_before, balance_after, + "balance should be unchanged after rollback" + ); + assert_eq!( + alpha_before, alpha_after, + "stake should be unchanged after rollback" + ); + }); +} + +#[test] +fn add_stake_burn_rollback_on_burn_failure() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(12201); + let owner_coldkey = U256::from(12202); + let coldkey = U256::from(12301); + let hotkey = U256::from(12302); + let min_stake = DefaultMinStake::::get(); + let tao_amount_raw = min_stake.to_u64().saturating_mul(200); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + pallet_subtensor::Pallet::::insert_lock_state( + &coldkey, + netuid, + &hotkey, + pallet_subtensor::staking::lock::LockState { + locked_mass: AlphaBalance::from(u64::MAX / 4), + conviction: U64F64::saturating_from_num(0), + last_update: pallet_subtensor::Pallet::::get_current_block_as_u64(), + }, + ); + + // Leave enough input-side liquidity for add_stake to pass the 1000x swap input cap. + // The lock above makes the burn leg fail, exercising atomic rollback. + mock::setup_reserves( + netuid, + TaoBalance::from(tao_amount_raw / 1000 + 1), + AlphaBalance::from(1_000_u64), + ); + + add_balance_to_coldkey_account( + &coldkey, + TaoBalance::from(tao_amount_raw.saturating_add(1_000_000_000)), + ); + + let balance_before = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + let alpha_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + + let expected_weight = + <::WeightInfo as SubtensorWeightInfo>::add_stake() + .saturating_add( + <::WeightInfo as SubtensorWeightInfo>::burn_alpha(), + ); + + let mut env = MockEnv::new( + FunctionId::AddStakeBurnV1, + coldkey, + (hotkey, netuid, TaoBalance::from(tao_amount_raw)).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + match ret { + RetVal::Converging(code) => { + assert_ne!(code, Output::Success as u32, "should not succeed") + } + _ => panic!("unexpected return value"), + } + + // Verify full rollback: balance and stake unchanged + let balance_after = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + let alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + + assert_eq!( + balance_before, balance_after, + "balance should be unchanged after rollback" + ); + assert_eq!( + alpha_before, alpha_after, + "stake should be unchanged after rollback" + ); + }); +} diff --git a/chain-extensions/src/tests/auto_stake_hotkey.rs b/chain-extensions/src/tests/auto_stake_hotkey.rs new file mode 100644 index 0000000000..c94402acbf --- /dev/null +++ b/chain-extensions/src/tests/auto_stake_hotkey.rs @@ -0,0 +1,45 @@ +//! Chain-extension dispatch for `SetColdkeyAutoStakeHotkeyV1`. + +use super::*; + +#[test] +fn set_coldkey_auto_stake_hotkey_success_sets_destination() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(4901); + let owner_coldkey = U256::from(4902); + let coldkey = U256::from(5901); + let hotkey = U256::from(5902); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + + pallet_subtensor::Owner::::insert(hotkey, coldkey); + pallet_subtensor::OwnedHotkeys::::insert(coldkey, vec![hotkey]); + pallet_subtensor::Uids::::insert(netuid, hotkey, 0u16); + + assert_eq!( + pallet_subtensor::AutoStakeDestination::::get(coldkey, netuid), + None + ); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::set_coldkey_auto_stake_hotkey(); + + let mut env = MockEnv::new( + FunctionId::SetColdkeyAutoStakeHotkeyV1, + coldkey, + (netuid, hotkey).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + assert_eq!( + pallet_subtensor::AutoStakeDestination::::get(coldkey, netuid), + Some(hotkey) + ); + let coldkeys = + pallet_subtensor::AutoStakeDestinationColdkeys::::get(hotkey, netuid); + assert!(coldkeys.contains(&coldkey)); + }); +} diff --git a/chain-extensions/src/tests/caller_dispatch.rs b/chain-extensions/src/tests/caller_dispatch.rs new file mode 100644 index 0000000000..487ed14469 --- /dev/null +++ b/chain-extensions/src/tests/caller_dispatch.rs @@ -0,0 +1,777 @@ +//! `Caller*` function ids resolve origin via `contracts_origin_as_raw(env.origin())`. +//! +//! With [`MockEnv`] both `caller()` and `origin()` are `Signed(caller)`, so outcomes align +//! with non-`Caller` arms. Weight expectations match the shared `dispatch_*_v1` helpers. + +use super::*; + +#[test] +fn caller_add_stake_success_updates_stake_and_returns_success_code() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let coldkey = U256::from(10101); + let hotkey = U256::from(10202); + let min_stake = DefaultMinStake::::get(); + let amount_raw = min_stake.to_u64().saturating_mul(10); + let amount: TaoBalance = amount_raw.into(); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + (amount_raw * 1_000_000).into(), + AlphaBalance::from(amount_raw * 10_000_000), + ); + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + mock::add_balance_to_coldkey_account( + &coldkey, + amount_raw.into(), + ); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::add_stake(); + + let mut env = MockEnv::new( + FunctionId::CallerAddStakeV1, + coldkey, + (hotkey, netuid, amount).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let total_stake = + pallet_subtensor::Pallet::::get_total_stake_for_hotkey(&hotkey); + assert!(total_stake > TaoBalance::ZERO); + }); +} + +#[test] +fn caller_remove_stake_with_no_stake_returns_amount_too_low() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let coldkey = U256::from(30301); + let hotkey = U256::from(30302); + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + let min_stake = DefaultMinStake::::get(); + let amount: AlphaBalance = AlphaBalance::from(min_stake.to_u64()); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::remove_stake(); + let mut env = MockEnv::new( + FunctionId::CallerRemoveStakeV1, + coldkey, + (hotkey, netuid, amount).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + match ret { + RetVal::Converging(code) => { + assert_eq!(code, Output::AmountTooLow as u32, "mismatched error output") + } + _ => panic!("unexpected return value"), + } + assert_eq!(env.charged_weight(), Some(expected_weight)); + }); +} + +#[test] +fn caller_unstake_all_success_unstakes_balance() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(40001); + let owner_coldkey = U256::from(40002); + let coldkey = U256::from(50001); + let hotkey = U256::from(50002); + let min_stake = DefaultMinStake::::get(); + let stake_amount_raw = min_stake.to_u64().saturating_mul(200); + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + + mock::setup_reserves( + netuid, + stake_amount_raw.saturating_mul(10).into(), + AlphaBalance::from(stake_amount_raw.saturating_mul(20)), + ); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + mock::add_balance_to_coldkey_account( + &coldkey, + (stake_amount_raw + 1_000_000_000).into(), + ); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(coldkey).into(), + hotkey, + netuid, + stake_amount_raw.into(), + )); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::unstake_all(); + + let pre_balance = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + + let mut env = MockEnv::new(FunctionId::CallerUnstakeAllV1, coldkey, hotkey.encode()) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + + let remaining_alpha = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + assert!(remaining_alpha <= AlphaBalance::from(1_000)); + + let post_balance = + pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + assert!(post_balance > pre_balance); + }); +} + +#[test] +fn caller_unstake_all_alpha_success_moves_stake_to_root() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(41001); + let owner_coldkey = U256::from(41002); + let coldkey = U256::from(51001); + let hotkey = U256::from(51002); + let min_stake = DefaultMinStake::::get(); + let stake_amount_raw = min_stake.to_u64().saturating_mul(220); + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + + mock::setup_reserves( + netuid, + stake_amount_raw.saturating_mul(20).into(), + AlphaBalance::from(stake_amount_raw.saturating_mul(30)), + ); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + mock::add_balance_to_coldkey_account( + &coldkey, + (stake_amount_raw + 1_000_000_000).into(), + ); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(coldkey).into(), + hotkey, + netuid, + stake_amount_raw.into(), + )); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::unstake_all_alpha(); + + let mut env = MockEnv::new( + FunctionId::CallerUnstakeAllAlphaV1, + coldkey, + hotkey.encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + + let subnet_alpha = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + assert!(subnet_alpha <= AlphaBalance::from(1_000)); + + let root_alpha = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + NetUid::ROOT, + ); + assert!(root_alpha > AlphaBalance::ZERO); + }); +} + +#[test] +fn caller_move_stake_success_moves_alpha_between_hotkeys() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(42001); + let owner_coldkey = U256::from(42002); + let coldkey = U256::from(52001); + let origin_hotkey = U256::from(52002); + let destination_hotkey = U256::from(52003); + + let min_stake = DefaultMinStake::::get(); + let stake_amount_raw = min_stake.to_u64().saturating_mul(240); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + stake_amount_raw.saturating_mul(15).into(), + AlphaBalance::from(stake_amount_raw.saturating_mul(25)), + ); + + mock::register_ok_neuron(netuid, origin_hotkey, coldkey, 0); + mock::register_ok_neuron(netuid, destination_hotkey, coldkey, 1); + + mock::add_balance_to_coldkey_account( + &coldkey, + (stake_amount_raw + 1_000_000_000).into(), + ); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(coldkey).into(), + origin_hotkey, + netuid, + stake_amount_raw.into(), + )); + + let alpha_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &origin_hotkey, + &coldkey, + netuid, + ); + let alpha_to_move: AlphaBalance = (alpha_before.to_u64() / 2).into(); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::move_stake(); + + let mut env = MockEnv::new( + FunctionId::CallerMoveStakeV1, + coldkey, + ( + origin_hotkey, + destination_hotkey, + netuid, + netuid, + alpha_to_move, + ) + .encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + + let origin_alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &origin_hotkey, + &coldkey, + netuid, + ); + let destination_alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &destination_hotkey, + &coldkey, + netuid, + ); + + assert_eq!(origin_alpha_after, alpha_before - alpha_to_move); + assert_eq!(destination_alpha_after, alpha_to_move); + }); +} + +#[test] +fn caller_transfer_stake_success_moves_between_coldkeys() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(43001); + let owner_coldkey = U256::from(43002); + let origin_coldkey = U256::from(53001); + let destination_coldkey = U256::from(53002); + let hotkey = U256::from(53003); + + let min_stake = DefaultMinStake::::get(); + let stake_amount_raw = min_stake.to_u64().saturating_mul(250); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + stake_amount_raw.saturating_mul(15).into(), + AlphaBalance::from(stake_amount_raw.saturating_mul(25)), + ); + + mock::register_ok_neuron(netuid, hotkey, origin_coldkey, 0); + + mock::add_balance_to_coldkey_account( + &origin_coldkey, + (stake_amount_raw + 1_000_000_000).into(), + ); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(origin_coldkey).into(), + hotkey, + netuid, + stake_amount_raw.into(), + )); + + let alpha_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &origin_coldkey, + netuid, + ); + let alpha_to_transfer: AlphaBalance = (alpha_before.to_u64() / 3).into(); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::transfer_stake(); + + let mut env = MockEnv::new( + FunctionId::CallerTransferStakeV1, + origin_coldkey, + ( + destination_coldkey, + hotkey, + netuid, + netuid, + alpha_to_transfer, + ) + .encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + + let origin_alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &origin_coldkey, + netuid, + ); + let destination_alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &destination_coldkey, + netuid, + ); + + assert_eq!(origin_alpha_after, alpha_before - alpha_to_transfer); + assert_eq!(destination_alpha_after, alpha_to_transfer); + }); +} + +#[test] +fn caller_swap_stake_success_moves_between_subnets() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey_a = U256::from(44001); + let owner_coldkey_a = U256::from(44002); + let owner_hotkey_b = U256::from(44003); + let owner_coldkey_b = U256::from(44004); + let coldkey = U256::from(54001); + let hotkey = U256::from(54002); + + let min_stake = DefaultMinStake::::get(); + let stake_amount_raw = min_stake.to_u64().saturating_mul(260); + + let netuid_a = mock::add_dynamic_network(&owner_hotkey_a, &owner_coldkey_a); + let netuid_b = mock::add_dynamic_network(&owner_hotkey_b, &owner_coldkey_b); + + mock::setup_reserves( + netuid_a, + stake_amount_raw.saturating_mul(18).into(), + AlphaBalance::from(stake_amount_raw.saturating_mul(30)), + ); + mock::setup_reserves( + netuid_b, + stake_amount_raw.saturating_mul(20).into(), + AlphaBalance::from(stake_amount_raw.saturating_mul(28)), + ); + + mock::register_ok_neuron(netuid_a, hotkey, coldkey, 0); + mock::register_ok_neuron(netuid_b, hotkey, coldkey, 1); + + mock::add_balance_to_coldkey_account( + &coldkey, + (stake_amount_raw + 1_000_000_000).into(), + ); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(coldkey).into(), + hotkey, + netuid_a, + stake_amount_raw.into(), + )); + + let alpha_origin_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_a, + ); + let alpha_destination_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_b, + ); + let alpha_to_swap: AlphaBalance = (alpha_origin_before.to_u64() / 3).into(); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::swap_stake(); + + let mut env = MockEnv::new( + FunctionId::CallerSwapStakeV1, + coldkey, + (hotkey, netuid_a, netuid_b, alpha_to_swap).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + + let alpha_origin_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_a, + ); + let alpha_destination_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_b, + ); + + assert!(alpha_origin_after < alpha_origin_before); + assert!(alpha_destination_after > alpha_destination_before); + }); +} + +#[test] +fn caller_add_stake_limit_success_executes_within_price_guard() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(45001); + let owner_coldkey = U256::from(45002); + let coldkey = U256::from(55001); + let hotkey = U256::from(55002); + let amount_raw: u64 = 900_000_000_000; + let limit_price: TaoBalance = 24_000_000_000u64.into(); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + + mock::setup_reserves( + netuid, + TaoBalance::from(150_000_000_000_u64), + AlphaBalance::from(100_000_000_000_u64), + ); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + mock::add_balance_to_coldkey_account( + &coldkey, + (amount_raw + 1_000_000_000).into(), + ); + + let stake_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + let balance_before = + pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::add_stake_limit(); + + let mut env = MockEnv::new( + FunctionId::CallerAddStakeLimitV1, + coldkey, + ( + hotkey, + netuid, + TaoBalance::from(amount_raw), + limit_price, + true, + ) + .encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + + let stake_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + let balance_after = + pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + + assert!(stake_after > stake_before); + assert!(stake_after > AlphaBalance::ZERO); + assert!(balance_after < balance_before); + }); +} + +#[test] +fn caller_remove_stake_limit_success_respects_price_limit() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(46001); + let owner_coldkey = U256::from(46002); + let coldkey = U256::from(56001); + let hotkey = U256::from(56002); + let stake_amount_raw: u64 = 320_000_000_000; + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + TaoBalance::from(120_000_000_000_u64), + AlphaBalance::from(100_000_000_000_u64), + ); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + mock::add_balance_to_coldkey_account( + &coldkey, + TaoBalance::from(stake_amount_raw + 1_000_000_000), + ); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(coldkey).into(), + hotkey, + netuid, + stake_amount_raw.into(), + )); + + let alpha_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + + let current_price = + ::SwapInterface::current_alpha_price( + netuid.into(), + ); + let limit_price_value = (current_price.to_num::() * 990_000_000f64).round() as u64; + let limit_price: TaoBalance = limit_price_value.into(); + + let alpha_to_unstake: AlphaBalance = (alpha_before.to_u64() / 2).into(); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::remove_stake_limit(); + + let balance_before = + pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + + let mut env = MockEnv::new( + FunctionId::CallerRemoveStakeLimitV1, + coldkey, + (hotkey, netuid, alpha_to_unstake, limit_price, true).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + let balance_after = + pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + + assert!(alpha_after < alpha_before); + assert!(balance_after > balance_before); + }); +} + +#[test] +fn caller_swap_stake_limit_matches_standard_slippage_path() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey_a = U256::from(47001); + let owner_coldkey_a = U256::from(47002); + let owner_hotkey_b = U256::from(47003); + let owner_coldkey_b = U256::from(47004); + let coldkey = U256::from(57001); + let hotkey = U256::from(57002); + + let stake_alpha = AlphaBalance::from(150_000_000_000u64); + + let netuid_a = mock::add_dynamic_network(&owner_hotkey_a, &owner_coldkey_a); + let netuid_b = mock::add_dynamic_network(&owner_hotkey_b, &owner_coldkey_b); + + mock::setup_reserves( + netuid_a, + TaoBalance::from(150_000_000_000_u64), + AlphaBalance::from(110_000_000_000_u64), + ); + mock::setup_reserves( + netuid_b, + TaoBalance::from(120_000_000_000_u64), + AlphaBalance::from(90_000_000_000_u64), + ); + + mock::register_ok_neuron(netuid_a, hotkey, coldkey, 0); + mock::register_ok_neuron(netuid_b, hotkey, coldkey, 1); + + pallet_subtensor::Pallet::::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid_a, + stake_alpha, + ); + + let alpha_origin_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_a, + ); + let alpha_destination_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_b, + ); + + let alpha_to_swap: AlphaBalance = (alpha_origin_before.to_u64() / 8).into(); + let limit_price: TaoBalance = 100u64.into(); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::swap_stake_limit(); + + let mut env = MockEnv::new( + FunctionId::CallerSwapStakeLimitV1, + coldkey, + (hotkey, netuid_a, netuid_b, alpha_to_swap, limit_price, true).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + + let alpha_origin_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_a, + ); + let alpha_destination_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_b, + ); + + assert!(alpha_origin_after <= alpha_origin_before); + assert!(alpha_destination_after >= alpha_destination_before); + }); +} + +#[test] +fn caller_remove_stake_full_limit_success_with_limit_price() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(48001); + let owner_coldkey = U256::from(48002); + let coldkey = U256::from(58001); + let hotkey = U256::from(58002); + let stake_amount_raw: u64 = 340_000_000_000; + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + TaoBalance::from(130_000_000_000_u64), + AlphaBalance::from(110_000_000_000_u64), + ); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + mock::add_balance_to_coldkey_account( + &coldkey, + TaoBalance::from(stake_amount_raw + 1_000_000_000), + ); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(coldkey).into(), + hotkey, + netuid, + stake_amount_raw.into(), + )); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::remove_stake_full_limit(); + + let balance_before = + pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + + let mut env = MockEnv::new( + FunctionId::CallerRemoveStakeFullLimitV1, + coldkey, + (hotkey, netuid, Option::::None).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + let balance_after = + pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + + assert!(alpha_after.is_zero()); + assert!(balance_after > balance_before); + }); +} + +#[test] +fn caller_set_coldkey_auto_stake_hotkey_success_sets_destination() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(49001); + let owner_coldkey = U256::from(49002); + let coldkey = U256::from(59001); + let hotkey = U256::from(59002); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + + pallet_subtensor::Owner::::insert(hotkey, coldkey); + pallet_subtensor::OwnedHotkeys::::insert(coldkey, vec![hotkey]); + pallet_subtensor::Uids::::insert(netuid, hotkey, 0u16); + + assert_eq!( + pallet_subtensor::AutoStakeDestination::::get(coldkey, netuid), + None + ); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::set_coldkey_auto_stake_hotkey(); + + let mut env = MockEnv::new( + FunctionId::CallerSetColdkeyAutoStakeHotkeyV1, + coldkey, + (netuid, hotkey).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + assert_eq!( + pallet_subtensor::AutoStakeDestination::::get(coldkey, netuid), + Some(hotkey) + ); + }); +} + +#[test] +fn caller_add_proxy_success_creates_proxy_relationship() { + mock::new_test_ext(1).execute_with(|| { + let delegator = U256::from(60001); + let delegate = U256::from(60002); + + mock::add_balance_to_coldkey_account(&delegator, 1_000_000_000.into()); + + let mut env = MockEnv::new(FunctionId::CallerAddProxyV1, delegator, delegate.encode()); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + + let proxies = pallet_subtensor_proxy::Proxies::::get(delegator).0; + assert_eq!(proxies.len(), 1); + }); +} + +#[test] +fn caller_remove_proxy_success_removes_proxy_relationship() { + mock::new_test_ext(1).execute_with(|| { + let delegator = U256::from(70001); + let delegate = U256::from(70002); + + mock::add_balance_to_coldkey_account(&delegator, 1_000_000_000.into()); + + let mut add_env = MockEnv::new(FunctionId::CallerAddProxyV1, delegator, delegate.encode()); + assert_extension_success( + SubtensorChainExtension::::dispatch(&mut add_env).unwrap(), + ); + + let mut remove_env = MockEnv::new( + FunctionId::CallerRemoveProxyV1, + delegator, + delegate.encode(), + ); + let ret = SubtensorChainExtension::::dispatch(&mut remove_env).unwrap(); + assert_extension_success(ret); + + let proxies_after = pallet_subtensor_proxy::Proxies::::get(delegator).0; + assert_eq!(proxies_after.len(), 0); + }); +} diff --git a/chain-extensions/src/tests/extension_queries.rs b/chain-extensions/src/tests/extension_queries.rs new file mode 100644 index 0000000000..934566e4bb --- /dev/null +++ b/chain-extensions/src/tests/extension_queries.rs @@ -0,0 +1,370 @@ +//! Read-only chain-extension queries: stake info, alpha price, registration, locks. + +use super::*; + +#[test] +fn get_stake_info_returns_encoded_runtime_value() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let hotkey = U256::from(11); + let coldkey = U256::from(22); + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + let expected = + pallet_subtensor::Pallet::::get_stake_info_for_hotkey_coldkey_netuid( + hotkey, coldkey, netuid, + ) + .encode(); + + let mut env = MockEnv::new( + FunctionId::GetStakeInfoForHotkeyColdkeyNetuidV1, + coldkey, + (hotkey, coldkey, netuid).encode(), + ); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + + assert_extension_success(ret); + assert_eq!(env.output(), expected.as_slice()); + assert!(env.charged_weight().is_none()); + }); +} + +#[test] +fn get_alpha_price_returns_encoded_price() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(8001); + let owner_coldkey = U256::from(8002); + let caller = U256::from(8003); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + + // Set up reserves to establish a price + let tao_reserve = TaoBalance::from(150_000_000_000u64); + let alpha_reserve = AlphaBalance::from(100_000_000_000u64); + mock::setup_reserves(netuid, tao_reserve, alpha_reserve); + + // Get expected price from swap handler + let expected_price = + as SwapHandler>::current_alpha_price( + netuid.into(), + ); + let expected_price_scaled = expected_price.saturating_mul(U64F64::from_num(1_000_000_000)); + let expected_price_u64: u64 = expected_price_scaled.saturating_to_num(); + + let mut env = MockEnv::new(FunctionId::GetAlphaPriceV1, caller, netuid.encode()); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert!(env.charged_weight().is_none()); + + // Decode the output + let output_price: u64 = Decode::decode(&mut &env.output()[..]).unwrap(); + + assert_eq!( + output_price, expected_price_u64, + "Price should match expected value" + ); + }); +} + +#[test] +fn get_subnet_registration_state_returns_existing_subnet_counter() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(8101); + let owner_coldkey = U256::from(8102); + let caller = U256::from(8103); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + let expected_counter = + pallet_subtensor::Pallet::::get_registered_subnet_counter(netuid); + + let mut env = MockEnv::new( + FunctionId::GetSubnetRegistrationStateV1, + caller, + netuid.encode(), + ); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert!(env.charged_weight().is_none()); + + let state = SubnetRegistrationState::decode(&mut &env.output()[..]).unwrap(); + assert_eq!( + state, + SubnetRegistrationState { + netuid, + exists: true, + registered_subnet_counter: expected_counter, + } + ); + }); +} + +#[test] +fn get_subnet_registration_state_preserves_counter_after_dissolve() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(8201); + let owner_coldkey = U256::from(8202); + let caller = U256::from(8203); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + let registered_counter = + pallet_subtensor::Pallet::::get_registered_subnet_counter(netuid); + + assert_ok!(pallet_subtensor::Pallet::::do_dissolve_network( + netuid + )); + + let mut env = MockEnv::new( + FunctionId::GetSubnetRegistrationStateV1, + caller, + netuid.encode(), + ); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + + let state = SubnetRegistrationState::decode(&mut &env.output()[..]).unwrap(); + assert_eq!(state.netuid, netuid); + assert!(!state.exists); + assert_eq!(state.registered_subnet_counter, registered_counter); + }); +} + +#[test] +fn get_subnet_registration_state_detects_reused_netuid_generation() { + mock::new_test_ext(1).execute_with(|| { + let first_hotkey = U256::from(8301); + let first_coldkey = U256::from(8302); + let second_hotkey = U256::from(8303); + let second_coldkey = U256::from(8304); + let caller = U256::from(8305); + + let netuid = mock::add_dynamic_network(&first_hotkey, &first_coldkey); + let first_counter = + pallet_subtensor::Pallet::::get_registered_subnet_counter(netuid); + + assert_ok!(pallet_subtensor::Pallet::::do_dissolve_network( + netuid + )); + + pallet_subtensor::Pallet::::remove_data_for_dissolved_networks( + Weight::from_parts(u64::MAX, u64::MAX), + ); + + let reused_netuid = mock::add_dynamic_network(&second_hotkey, &second_coldkey); + assert_eq!(reused_netuid, netuid); + + let mut env = MockEnv::new( + FunctionId::GetSubnetRegistrationStateV1, + caller, + netuid.encode(), + ); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + + let state = SubnetRegistrationState::decode(&mut &env.output()[..]).unwrap(); + assert_eq!(state.netuid, netuid); + assert!(state.exists); + assert!(state.registered_subnet_counter > first_counter); + }); +} + +#[test] +fn get_coldkey_lock_returns_none_without_lock() { + mock::new_test_ext(1).execute_with(|| { + let caller = U256::from(8401); + let coldkey = U256::from(8402); + let netuid = NetUid::from(1); + + let mut env = MockEnv::new( + FunctionId::GetColdkeyLockV1, + caller, + (coldkey, netuid).encode(), + ); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert!(env.charged_weight().is_none()); + + let lock = Option::::decode(&mut &env.output()[..]).unwrap(); + assert_eq!(lock, None); + }); +} + +#[test] +fn get_coldkey_lock_returns_rolled_lock() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(8501); + let owner_coldkey = U256::from(8502); + let coldkey = U256::from(8503); + let hotkey = U256::from(8504); + let stake = AlphaBalance::from(10_000u64); + let lock_amount = AlphaBalance::from(5_000u64); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + pallet_subtensor::Pallet::::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, stake, + ); + + assert_ok!(pallet_subtensor::Pallet::::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount, + )); + + frame_system::Pallet::::set_block_number(1_001); + let expected = + pallet_subtensor::Pallet::::get_coldkey_lock(&coldkey, netuid).unwrap(); + + let mut env = MockEnv::new( + FunctionId::GetColdkeyLockV1, + coldkey, + (coldkey, netuid).encode(), + ); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert!(env.charged_weight().is_none()); + + let lock = Option::::decode(&mut &env.output()[..]) + .unwrap() + .unwrap(); + assert_eq!(lock.locked_mass, expected.locked_mass); + assert_eq!(lock.conviction_bits, expected.conviction.to_bits()); + assert!(lock.conviction_bits > 0); + assert_eq!( + lock.last_update, + pallet_subtensor::Pallet::::get_current_block_as_u64() + ); + }); +} + +#[test] +fn get_stake_availability_returns_zeroes_without_stake_or_lock() { + mock::new_test_ext(1).execute_with(|| { + let caller = U256::from(8601); + let coldkey = U256::from(8602); + let netuid = NetUid::from(1); + + let mut env = MockEnv::new( + FunctionId::GetStakeAvailabilityV1, + caller, + (coldkey, netuid).encode(), + ); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert!(env.charged_weight().is_none()); + + let availability = StakeAvailability::decode(&mut &env.output()[..]).unwrap(); + assert_eq!( + availability, + StakeAvailability { + netuid, + total: AlphaBalance::ZERO, + locked: AlphaBalance::ZERO, + available: AlphaBalance::ZERO, + } + ); + }); +} + +#[test] +fn get_stake_availability_returns_partial_lock_breakdown() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(8701); + let owner_coldkey = U256::from(8702); + let coldkey = U256::from(8703); + let hotkey = U256::from(8704); + let stake = AlphaBalance::from(10_000u64); + let lock_amount = AlphaBalance::from(4_000u64); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + pallet_subtensor::Pallet::::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, stake, + ); + assert_ok!(pallet_subtensor::Pallet::::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount, + )); + + let mut env = MockEnv::new( + FunctionId::GetStakeAvailabilityV1, + coldkey, + (coldkey, netuid).encode(), + ); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert!(env.charged_weight().is_none()); + + let availability = StakeAvailability::decode(&mut &env.output()[..]).unwrap(); + assert_eq!( + availability, + StakeAvailability { + netuid, + total: stake, + locked: lock_amount, + available: stake.saturating_sub(lock_amount), + } + ); + }); +} + +#[test] +fn get_stake_availability_uses_rolled_forward_lock() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(8801); + let owner_coldkey = U256::from(8802); + let coldkey = U256::from(8803); + let hotkey = U256::from(8804); + let stake = AlphaBalance::from(10_000u64); + let lock_amount = AlphaBalance::from(5_000u64); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + pallet_subtensor::Pallet::::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, stake, + ); + assert_ok!(pallet_subtensor::Pallet::::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount, + )); + + frame_system::Pallet::::set_block_number(1_001); + let expected_locked = + pallet_subtensor::Pallet::::get_current_locked(&coldkey, netuid); + assert!(expected_locked < lock_amount); + + let mut env = MockEnv::new( + FunctionId::GetStakeAvailabilityV1, + coldkey, + (coldkey, netuid).encode(), + ); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert!(env.charged_weight().is_none()); + + let availability = StakeAvailability::decode(&mut &env.output()[..]).unwrap(); + assert_eq!(availability.netuid, netuid); + assert_eq!(availability.total, stake); + assert_eq!(availability.locked, expected_locked); + assert_eq!( + availability.available, + stake.saturating_sub(expected_locked) + ); + }); +} diff --git a/chain-extensions/src/tests/mod.rs b/chain-extensions/src/tests/mod.rs new file mode 100644 index 0000000000..43fd52b5b4 --- /dev/null +++ b/chain-extensions/src/tests/mod.rs @@ -0,0 +1,124 @@ +//! Unit tests for `subtensor-chain-extensions`, split by concept for discoverability. +#![allow(clippy::unwrap_used)] +// Shared imports are consumed by concept modules via `use super::*`. +#![allow(unused_imports)] + +use super::{SubtensorChainExtension, SubtensorExtensionEnv, mock}; +use crate::types::{ColdkeyLock, FunctionId, Output, StakeAvailability, SubnetRegistrationState}; +use codec::{Decode, Encode}; +use frame_support::pallet_prelude::Zero; +use frame_support::{assert_ok, weights::Weight}; +use frame_system::RawOrigin; +use pallet_contracts::chain_extension::RetVal; +use pallet_subtensor::DefaultMinStake; +use pallet_subtensor::weights::WeightInfo as SubtensorWeightInfo; +use sp_core::Get; +use sp_core::U256; +use sp_runtime::DispatchError; +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; +use subtensor_swap_interface::SwapHandler; + +type AccountId = ::AccountId; + +/// In-memory [`SubtensorExtensionEnv`] for dispatch unit tests (no `pallet-contracts` VM). +#[derive(Clone)] +struct MockEnv { + func_id: u16, + caller: AccountId, + input: Vec, + output: Vec, + charged_weight: Option, + expected_weight: Option, +} + +#[allow(dead_code)] +pub fn add_balance_to_coldkey_account(coldkey: &U256, tao: TaoBalance) { + let credit = pallet_subtensor::Pallet::::mint_tao(tao); + let _ = pallet_subtensor::Pallet::::spend_tao(coldkey, credit, tao).unwrap(); +} + +impl MockEnv { + fn new(func_id: FunctionId, caller: AccountId, input: Vec) -> Self { + Self { + func_id: func_id as u16, + caller, + input, + output: Vec::new(), + charged_weight: None, + expected_weight: None, + } + } + + fn with_expected_weight(mut self, weight: Weight) -> Self { + self.expected_weight = Some(weight); + self + } + + fn charged_weight(&self) -> Option { + self.charged_weight + } + + fn output(&self) -> &[u8] { + &self.output + } +} + +impl SubtensorExtensionEnv for MockEnv { + fn func_id(&self) -> u16 { + self.func_id + } + + fn charge_weight(&mut self, weight: Weight) -> Result<(), DispatchError> { + let prev = self.charged_weight.unwrap_or_default(); + let cumulative = Weight::from_parts( + prev.ref_time().checked_add(weight.ref_time()).unwrap(), + prev.proof_size().checked_add(weight.proof_size()).unwrap(), + ); + if let Some(expected) = self.expected_weight + && (cumulative.ref_time() > expected.ref_time() + || cumulative.proof_size() > expected.proof_size()) + { + return Err(DispatchError::Other( + "unexpected weight charged by mock env", + )); + } + self.charged_weight = Some(cumulative); + Ok(()) + } + + fn read_as(&mut self) -> Result { + U::decode(&mut &self.input[..]).map_err(|_| DispatchError::Other("mock env decode failure")) + } + + fn write_output(&mut self, data: &[u8]) -> Result<(), DispatchError> { + self.output.clear(); + self.output.extend_from_slice(data); + Ok(()) + } + + fn caller(&mut self) -> AccountId { + self.caller + } + + fn origin(&mut self) -> pallet_contracts::Origin { + pallet_contracts::Origin::Signed(self.caller) + } +} + +fn assert_extension_success(ret: RetVal) { + match ret { + RetVal::Converging(code) => { + assert_eq!(code, Output::Success as u32, "expected success code") + } + _ => panic!("unexpected return value"), + } +} + +mod alpha_recycle_burn; +mod auto_stake_hotkey; +mod caller_dispatch; +mod extension_queries; +mod proxy_ops; +mod stake_limit; +mod stake_ops; diff --git a/chain-extensions/src/tests/proxy_ops.rs b/chain-extensions/src/tests/proxy_ops.rs new file mode 100644 index 0000000000..c33828b079 --- /dev/null +++ b/chain-extensions/src/tests/proxy_ops.rs @@ -0,0 +1,62 @@ +//! Staking-proxy add/remove via chain-extension function ids. + +use super::*; + +#[test] +fn add_proxy_success_creates_proxy_relationship() { + mock::new_test_ext(1).execute_with(|| { + let delegator = U256::from(6001); + let delegate = U256::from(6002); + + add_balance_to_coldkey_account(&delegator, 1_000_000_000.into()); + + assert_eq!( + pallet_subtensor_proxy::Proxies::::get(delegator) + .0 + .len(), + 0 + ); + + let mut env = MockEnv::new(FunctionId::AddProxyV1, delegator, delegate.encode()); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + + let proxies = pallet_subtensor_proxy::Proxies::::get(delegator).0; + assert_eq!(proxies.len(), 1); + if let Some(proxy) = proxies.first() { + assert_eq!(proxy.delegate, delegate); + assert_eq!( + proxy.proxy_type, + subtensor_runtime_common::ProxyType::Staking + ); + assert_eq!(proxy.delay, 0u64); + } else { + panic!("proxies should contain one element"); + } + }); +} + +#[test] +fn remove_proxy_success_removes_proxy_relationship() { + mock::new_test_ext(1).execute_with(|| { + let delegator = U256::from(7001); + let delegate = U256::from(7002); + + add_balance_to_coldkey_account(&delegator, 1_000_000_000.into()); + + let mut add_env = MockEnv::new(FunctionId::AddProxyV1, delegator, delegate.encode()); + let ret = SubtensorChainExtension::::dispatch(&mut add_env).unwrap(); + assert_extension_success(ret); + + let proxies_before = pallet_subtensor_proxy::Proxies::::get(delegator).0; + assert_eq!(proxies_before.len(), 1); + + let mut remove_env = MockEnv::new(FunctionId::RemoveProxyV1, delegator, delegate.encode()); + let ret = SubtensorChainExtension::::dispatch(&mut remove_env).unwrap(); + assert_extension_success(ret); + + let proxies_after = pallet_subtensor_proxy::Proxies::::get(delegator).0; + assert_eq!(proxies_after.len(), 0); + }); +} diff --git a/chain-extensions/src/tests/stake_limit.rs b/chain-extensions/src/tests/stake_limit.rs new file mode 100644 index 0000000000..340c8014af --- /dev/null +++ b/chain-extensions/src/tests/stake_limit.rs @@ -0,0 +1,264 @@ +//! Price-limited stake dispatch: add/remove/swap limit and remove-full-limit. + +use super::*; + +#[test] +fn remove_stake_full_limit_success_with_limit_price() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(4801); + let owner_coldkey = U256::from(4802); + let coldkey = U256::from(5801); + let hotkey = U256::from(5802); + let stake_amount_raw: u64 = 340_000_000_000; + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + TaoBalance::from(130_000_000_000_u64), + AlphaBalance::from(110_000_000_000_u64), + ); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + add_balance_to_coldkey_account( + &coldkey, + TaoBalance::from(stake_amount_raw + 1_000_000_000), + ); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(coldkey).into(), + hotkey, + netuid, + stake_amount_raw.into(), + )); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::remove_stake_full_limit(); + + let balance_before = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + + let mut env = MockEnv::new( + FunctionId::RemoveStakeFullLimitV1, + coldkey, + (hotkey, netuid, Option::::None).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + let balance_after = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + + assert!(alpha_after.is_zero()); + assert!(balance_after > balance_before); + }); +} + +#[test] +fn swap_stake_limit_with_tight_price_returns_slippage_error() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey_a = U256::from(4701); + let owner_coldkey_a = U256::from(4702); + let owner_hotkey_b = U256::from(4703); + let owner_coldkey_b = U256::from(4704); + let coldkey = U256::from(5701); + let hotkey = U256::from(5702); + + let stake_alpha = AlphaBalance::from(150_000_000_000u64); + + let netuid_a = mock::add_dynamic_network(&owner_hotkey_a, &owner_coldkey_a); + let netuid_b = mock::add_dynamic_network(&owner_hotkey_b, &owner_coldkey_b); + + mock::setup_reserves( + netuid_a, + TaoBalance::from(150_000_000_000_u64), + AlphaBalance::from(110_000_000_000_u64), + ); + mock::setup_reserves( + netuid_b, + TaoBalance::from(120_000_000_000_u64), + AlphaBalance::from(90_000_000_000_u64), + ); + + mock::register_ok_neuron(netuid_a, hotkey, coldkey, 0); + mock::register_ok_neuron(netuid_b, hotkey, coldkey, 1); + + pallet_subtensor::Pallet::::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid_a, + stake_alpha, + ); + + let alpha_origin_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_a, + ); + let alpha_destination_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_b, + ); + + let alpha_to_swap: AlphaBalance = (alpha_origin_before.to_u64() / 8).into(); + let limit_price: TaoBalance = 100u64.into(); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::swap_stake_limit(); + + let mut env = MockEnv::new( + FunctionId::SwapStakeLimitV1, + coldkey, + (hotkey, netuid_a, netuid_b, alpha_to_swap, limit_price, true).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let alpha_origin_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_a, + ); + let alpha_destination_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_b, + ); + + assert!(alpha_origin_after <= alpha_origin_before); + assert!(alpha_destination_after >= alpha_destination_before); + }); +} + +#[test] +fn remove_stake_limit_success_respects_price_limit() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(4601); + let owner_coldkey = U256::from(4602); + let coldkey = U256::from(5601); + let hotkey = U256::from(5602); + let stake_amount_raw: u64 = 320_000_000_000; + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + TaoBalance::from(120_000_000_000_u64), + AlphaBalance::from(100_000_000_000_u64), + ); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + add_balance_to_coldkey_account( + &coldkey, + TaoBalance::from(stake_amount_raw + 1_000_000_000), + ); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(coldkey).into(), + hotkey, + netuid, + stake_amount_raw.into(), + )); + + let alpha_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + + let current_price = + ::SwapInterface::current_alpha_price( + netuid.into(), + ); + let limit_price_value = (current_price.to_num::() * 990_000_000f64).round() as u64; + let limit_price: TaoBalance = limit_price_value.into(); + + let alpha_to_unstake: AlphaBalance = (alpha_before.to_u64() / 2).into(); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::remove_stake_limit(); + + let balance_before = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + + let mut env = MockEnv::new( + FunctionId::RemoveStakeLimitV1, + coldkey, + (hotkey, netuid, alpha_to_unstake, limit_price, true).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + let balance_after = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + + assert!(alpha_after < alpha_before); + assert!(balance_after > balance_before); + }); +} + +#[test] +fn add_stake_limit_success_executes_within_price_guard() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(4501); + let owner_coldkey = U256::from(4502); + let coldkey = U256::from(5501); + let hotkey = U256::from(5502); + let amount_raw: u64 = 900_000_000_000; + let limit_price: TaoBalance = 24_000_000_000u64.into(); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + + mock::setup_reserves( + netuid, + TaoBalance::from(150_000_000_000_u64), + AlphaBalance::from(100_000_000_000_u64), + ); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + add_balance_to_coldkey_account(&coldkey, (amount_raw + 1_000_000_000).into()); + + let stake_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + let balance_before = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::add_stake_limit(); + + let mut env = MockEnv::new( + FunctionId::AddStakeLimitV1, + coldkey, + ( + hotkey, + netuid, + TaoBalance::from(amount_raw), + limit_price, + true, + ) + .encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let stake_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + let balance_after = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + + assert!(stake_after > stake_before); + assert!(stake_after > AlphaBalance::ZERO); + assert!(balance_after < balance_before); + }); +} diff --git a/chain-extensions/src/tests/stake_ops.rs b/chain-extensions/src/tests/stake_ops.rs new file mode 100644 index 0000000000..ea7dbb886e --- /dev/null +++ b/chain-extensions/src/tests/stake_ops.rs @@ -0,0 +1,417 @@ +//! Core stake dispatch: add/remove/move/transfer/swap and unstake-all variants. + +use super::*; + +#[test] +fn swap_stake_success_moves_between_subnets() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey_a = U256::from(4401); + let owner_coldkey_a = U256::from(4402); + let owner_hotkey_b = U256::from(4403); + let owner_coldkey_b = U256::from(4404); + let coldkey = U256::from(5401); + let hotkey = U256::from(5402); + + let min_stake = DefaultMinStake::::get(); + let stake_amount_raw = min_stake.to_u64().saturating_mul(260); + + let netuid_a = mock::add_dynamic_network(&owner_hotkey_a, &owner_coldkey_a); + let netuid_b = mock::add_dynamic_network(&owner_hotkey_b, &owner_coldkey_b); + + mock::setup_reserves( + netuid_a, + stake_amount_raw.saturating_mul(18).into(), + AlphaBalance::from(stake_amount_raw.saturating_mul(30)), + ); + mock::setup_reserves( + netuid_b, + stake_amount_raw.saturating_mul(20).into(), + AlphaBalance::from(stake_amount_raw.saturating_mul(28)), + ); + + mock::register_ok_neuron(netuid_a, hotkey, coldkey, 0); + mock::register_ok_neuron(netuid_b, hotkey, coldkey, 1); + + add_balance_to_coldkey_account(&coldkey, (stake_amount_raw + 1_000_000_000).into()); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(coldkey).into(), + hotkey, + netuid_a, + stake_amount_raw.into(), + )); + + let alpha_origin_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_a, + ); + let alpha_destination_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_b, + ); + let alpha_to_swap: AlphaBalance = (alpha_origin_before.to_u64() / 3).into(); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::swap_stake(); + + let mut env = MockEnv::new( + FunctionId::SwapStakeV1, + coldkey, + (hotkey, netuid_a, netuid_b, alpha_to_swap).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let alpha_origin_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_a, + ); + let alpha_destination_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_b, + ); + + assert!(alpha_origin_after < alpha_origin_before); + assert!( + alpha_destination_after > alpha_destination_before, + "destination stake should increase" + ); + }); +} + +#[test] +fn transfer_stake_success_moves_between_coldkeys() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(4301); + let owner_coldkey = U256::from(4302); + let origin_coldkey = U256::from(5301); + let destination_coldkey = U256::from(5302); + let hotkey = U256::from(5303); + + let min_stake = DefaultMinStake::::get(); + let stake_amount_raw = min_stake.to_u64().saturating_mul(250); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + stake_amount_raw.saturating_mul(15).into(), + AlphaBalance::from(stake_amount_raw.saturating_mul(25)), + ); + + mock::register_ok_neuron(netuid, hotkey, origin_coldkey, 0); + + add_balance_to_coldkey_account(&origin_coldkey, (stake_amount_raw + 1_000_000_000).into()); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(origin_coldkey).into(), + hotkey, + netuid, + stake_amount_raw.into(), + )); + + let alpha_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &origin_coldkey, + netuid, + ); + let alpha_to_transfer: AlphaBalance = (alpha_before.to_u64() / 3).into(); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::transfer_stake(); + + let mut env = MockEnv::new( + FunctionId::TransferStakeV1, + origin_coldkey, + ( + destination_coldkey, + hotkey, + netuid, + netuid, + alpha_to_transfer, + ) + .encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let origin_alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &origin_coldkey, + netuid, + ); + let destination_alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &destination_coldkey, + netuid, + ); + + assert_eq!(origin_alpha_after, alpha_before - alpha_to_transfer); + assert_eq!(destination_alpha_after, alpha_to_transfer); + }); +} + +#[test] +fn move_stake_success_moves_alpha_between_hotkeys() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(4201); + let owner_coldkey = U256::from(4202); + let coldkey = U256::from(5201); + let origin_hotkey = U256::from(5202); + let destination_hotkey = U256::from(5203); + + let min_stake = DefaultMinStake::::get(); + let stake_amount_raw = min_stake.to_u64().saturating_mul(240); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + stake_amount_raw.saturating_mul(15).into(), + AlphaBalance::from(stake_amount_raw.saturating_mul(25)), + ); + + mock::register_ok_neuron(netuid, origin_hotkey, coldkey, 0); + mock::register_ok_neuron(netuid, destination_hotkey, coldkey, 1); + + add_balance_to_coldkey_account(&coldkey, (stake_amount_raw + 1_000_000_000).into()); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(coldkey).into(), + origin_hotkey, + netuid, + stake_amount_raw.into(), + )); + + let alpha_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &origin_hotkey, + &coldkey, + netuid, + ); + let alpha_to_move: AlphaBalance = (alpha_before.to_u64() / 2).into(); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::move_stake(); + + let mut env = MockEnv::new( + FunctionId::MoveStakeV1, + coldkey, + ( + origin_hotkey, + destination_hotkey, + netuid, + netuid, + alpha_to_move, + ) + .encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let origin_alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &origin_hotkey, + &coldkey, + netuid, + ); + let destination_alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &destination_hotkey, + &coldkey, + netuid, + ); + + assert_eq!(origin_alpha_after, alpha_before - alpha_to_move); + assert_eq!(destination_alpha_after, alpha_to_move); + }); +} + +#[test] +fn unstake_all_alpha_success_moves_stake_to_root() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(4101); + let owner_coldkey = U256::from(4102); + let coldkey = U256::from(5101); + let hotkey = U256::from(5102); + let min_stake = DefaultMinStake::::get(); + let stake_amount_raw = min_stake.to_u64().saturating_mul(220); + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + + mock::setup_reserves( + netuid, + stake_amount_raw.saturating_mul(20).into(), + AlphaBalance::from(stake_amount_raw.saturating_mul(30)), + ); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + add_balance_to_coldkey_account(&coldkey, (stake_amount_raw + 1_000_000_000).into()); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(coldkey).into(), + hotkey, + netuid, + stake_amount_raw.into(), + )); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::unstake_all_alpha(); + + let mut env = MockEnv::new(FunctionId::UnstakeAllAlphaV1, coldkey, hotkey.encode()) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let subnet_alpha = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + assert!(subnet_alpha <= AlphaBalance::from(1_000)); + + let root_alpha = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + NetUid::ROOT, + ); + assert!(root_alpha > AlphaBalance::ZERO); + }); +} + +#[test] +fn add_stake_success_updates_stake_and_returns_success_code() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let coldkey = U256::from(101); + let hotkey = U256::from(202); + let min_stake = DefaultMinStake::::get(); + let amount_raw = min_stake.to_u64().saturating_mul(10); + let amount: TaoBalance = amount_raw.into(); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + (amount_raw * 1_000_000).into(), + AlphaBalance::from(amount_raw * 10_000_000), + ); + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + add_balance_to_coldkey_account(&coldkey, amount_raw.into()); + + assert!( + pallet_subtensor::Pallet::::get_total_stake_for_hotkey(&hotkey).is_zero() + ); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::add_stake(); + + let mut env = MockEnv::new( + FunctionId::AddStakeV1, + coldkey, + (hotkey, netuid, amount).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + + assert_extension_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let total_stake = + pallet_subtensor::Pallet::::get_total_stake_for_hotkey(&hotkey); + assert!(total_stake > TaoBalance::ZERO); + }); +} + +#[test] +fn remove_stake_with_no_stake_returns_amount_too_low() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let coldkey = U256::from(301); + let hotkey = U256::from(302); + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + let min_stake = DefaultMinStake::::get(); + let amount: AlphaBalance = AlphaBalance::from(min_stake.to_u64()); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::remove_stake(); + let mut env = MockEnv::new( + FunctionId::RemoveStakeV1, + coldkey, + (hotkey, netuid, amount).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + + match ret { + RetVal::Converging(code) => { + assert_eq!(code, Output::AmountTooLow as u32, "mismatched error output") + } + _ => panic!("unexpected return value"), + } + assert_eq!(env.charged_weight(), Some(expected_weight)); + assert!( + pallet_subtensor::Pallet::::get_total_stake_for_hotkey(&hotkey).is_zero() + ); + }); +} + +#[test] +fn unstake_all_success_unstakes_balance() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(4001); + let owner_coldkey = U256::from(4002); + let coldkey = U256::from(5001); + let hotkey = U256::from(5002); + let min_stake = DefaultMinStake::::get(); + let stake_amount_raw = min_stake.to_u64().saturating_mul(200); + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + + mock::setup_reserves( + netuid, + stake_amount_raw.saturating_mul(10).into(), + AlphaBalance::from(stake_amount_raw.saturating_mul(20)), + ); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + add_balance_to_coldkey_account(&coldkey, (stake_amount_raw + 1_000_000_000).into()); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(coldkey).into(), + hotkey, + netuid, + stake_amount_raw.into(), + )); + + let expected_weight = <::WeightInfo as SubtensorWeightInfo>::unstake_all(); + + let pre_balance = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + + let mut env = MockEnv::new(FunctionId::UnstakeAllV1, coldkey, hotkey.encode()) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_extension_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let remaining_alpha = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + assert!(remaining_alpha <= AlphaBalance::from(1_000)); + + let post_balance = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + assert!(post_balance > pre_balance); + }); +} diff --git a/chain-extensions/src/types.rs b/chain-extensions/src/types.rs index 46a2e9fde5..dd7a52ca65 100644 --- a/chain-extensions/src/types.rs +++ b/chain-extensions/src/types.rs @@ -1,9 +1,17 @@ +//! Wire types for the Subtensor contracts chain extension: function ids, status codes, query payloads. +//! +//! [`FunctionId`] discriminants and [`Output`] status codes are ABI-stable for ink! contracts +//! (see `ink-contract/`). Do not renumber, reorder, or reuse ids/codes. + use codec::{Decode, Encode}; use num_enum::{IntoPrimitive, TryFromPrimitive}; use sp_runtime::{DispatchError, ModuleError}; use subtensor_macros::freeze_struct; use subtensor_runtime_common::{AlphaBalance, NetUid}; +/// Chain-extension function selector (`u16`), matching ink `#[ink(function = N)]`. +/// +/// Discriminants are **frozen wire ABI** — append only; never reuse retired values. #[repr(u16)] #[derive(TryFromPrimitive, IntoPrimitive, Decode, Encode)] pub enum FunctionId { @@ -27,6 +35,7 @@ pub enum FunctionId { BurnAlphaV1 = 17, AddStakeRecycleV1 = 18, AddStakeBurnV1 = 19, + /// Like [`Self::AddStakeV1`] but signs as `env.origin()` (extrinsic caller), not the contract. CallerAddStakeV1 = 20, CallerRemoveStakeV1 = 21, CallerUnstakeAllV1 = 22, @@ -46,31 +55,53 @@ pub enum FunctionId { GetStakeAvailabilityV1 = 36, } -#[freeze_struct("5dc33d60abed5c08")] +/// Whether `netuid` currently exists and its registration-generation counter. +/// +/// SCALE layout is frozen; field order must not change without a contract ABI bump. +#[freeze_struct("4dde9cfa4daec13")] #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, scale_info::TypeInfo)] pub struct SubnetRegistrationState { + /// Subnet identifier queried by the contract. pub netuid: NetUid, + /// `true` when the subnet is currently registered. pub exists: bool, + /// Monotonic generation counter for this netuid slot (survives dissolve/reuse). pub registered_subnet_counter: u64, } -#[freeze_struct("bf4c1e249109618")] +/// Coldkey lock snapshot returned to contracts (conviction stored as fixed-point bits). +/// +/// SCALE layout is frozen; field order must not change without a contract ABI bump. +#[freeze_struct("99a43eb00de9d491")] #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, scale_info::TypeInfo)] pub struct ColdkeyLock { + /// Alpha mass currently locked under conviction. pub locked_mass: AlphaBalance, + /// Conviction as `U64F64` bit pattern (not a human-readable float). pub conviction_bits: u128, + /// Block number of the last lock update. pub last_update: u64, } -#[freeze_struct("fb12f00479cf6990")] +/// Stake total / locked / available breakdown for a coldkey on a subnet. +/// +/// SCALE layout is frozen; field order must not change without a contract ABI bump. +#[freeze_struct("f6860805b7a1f2cc")] #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, scale_info::TypeInfo)] pub struct StakeAvailability { + /// Subnet whose stake availability was queried. pub netuid: NetUid, + /// Total alpha stake for the coldkey on this subnet. pub total: AlphaBalance, + /// Portion locked (unavailable to withdraw/recycle freely). pub locked: AlphaBalance, + /// `total - locked` (clamped); spendable for remove/recycle/burn paths. pub available: AlphaBalance, } +/// Converging status code returned to ink via `RetVal::Converging(code as u32)`. +/// +/// Discriminants are **frozen wire ABI** (ink `FromStatusCode`). Do not reorder variants. #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug)] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] pub enum Output { @@ -123,6 +154,10 @@ pub enum Output { } impl From for Output { + /// Map pallet/`DispatchError` messages onto the frozen [`Output`] ABI codes. + /// + /// Unmapped module errors become [`Output::RuntimeError`]. Proxy pallet errors use short + /// FRAME names (`TooMany`, `Duplicate`, …) rather than the `Proxy*` Output labels. fn from(input: DispatchError) -> Self { let error_text = match input { DispatchError::Module(ModuleError { message, .. }) => message, diff --git a/common/src/currency.rs b/common/src/currency.rs index d32a8cbddc..4b2b8a7836 100644 --- a/common/src/currency.rs +++ b/common/src/currency.rs @@ -1,3 +1,8 @@ +//! Typed TAO and alpha balances (`TaoBalance` / `AlphaBalance`) and the shared [`Token`] trait. +//! +//! Both amounts are `u64` RAO-scale newtypes (1e9 = 1 whole unit). SCALE encoding matches `u64`; +//! `TypeInfo` keeps distinct metadata paths so SDKs do not collapse them to bare integers. + use core::fmt::{self, Display, Formatter}; use core::ops::{ Add, AddAssign, BitAnd, BitOr, BitXor, Div, DivAssign, Mul, MulAssign, Not, Rem, RemAssign, @@ -28,7 +33,11 @@ use sp_arithmetic::traits::{ #[cfg(feature = "std")] use sp_rpc::number::NumberOrHex; -#[freeze_struct("fe2aa2d7fcb480e8")] +/// Subnet alpha (subtoken) amount in RAO-scale units (`u64` newtype). +/// +/// Distinct from [`TaoBalance`] at the type level so stake/swap APIs cannot mix TAO and alpha +/// without an explicit conversion. SCALE-identical to `u64`. +#[freeze_struct("20f2a04b8529989")] #[repr(transparent)] #[derive( Deserialize, @@ -50,7 +59,11 @@ use sp_rpc::number::NumberOrHex; )] pub struct AlphaBalance(u64); -#[freeze_struct("a99f2483a97121fc")] +/// Native TAO amount in RAO-scale units (`u64` newtype; 1e9 RAO = 1 TAO). +/// +/// Implements the broad numeric surface expected by Substrate `Currency` / `Balance` bounds +/// (`PrimInt`, checked ops, shifts). Prefer this over raw `u64` at pallet boundaries. +#[freeze_struct("5a6e308ca46b1e42")] #[repr(transparent)] #[derive( Deserialize, @@ -72,10 +85,9 @@ pub struct AlphaBalance(u64); )] pub struct TaoBalance(u64); -// implements traits required by the Currency trait (ToFixed + Into + From) and CompactAs -// and Display. It expects a wrapper structure for u64 (CurrencyT(u64)). -// TypeInfo is derived on the structs themselves so the type identity (path) is preserved in the -// runtime metadata, letting SDKs generate distinct TaoBalance/AlphaBalance types instead of bare u64. +// Implements Currency-facing traits (ToFixed + Into/From), CompactAs, and Display for a +// `CurrencyT(u64)` newtype. TypeInfo is derived on the structs themselves so the type identity +// (path) is preserved in runtime metadata. macro_rules! impl_currency_reqs { ($currency_type:ident) => { impl $currency_type { @@ -293,6 +305,10 @@ macro_rules! impl_approx { }; } +/// Shared numeric surface for [`TaoBalance`] and [`AlphaBalance`] (AMM reserves, stake math). +/// +/// Deliberately does not unify the two currency types into one enum: call sites must pick TAO +/// or alpha explicitly via the concrete type parameter. pub trait Token: ToFixed + Into @@ -308,26 +324,33 @@ pub trait Token: + Zero + One { + /// Maximum representable amount (`u64::MAX` RAO). const MAX: Self; + /// Zero amount. const ZERO: Self; + /// Inner RAO amount as `u64`. fn to_u64(&self) -> u64 { (*self).into() } + /// Saturating addition in RAO units. fn saturating_add(&self, rhv: Self) -> Self { Into::::into(*self).saturating_add(rhv.into()).into() } + /// Saturating division in RAO units. #[allow(clippy::arithmetic_side_effects)] fn saturating_div(&self, rhv: Self) -> Self { Into::::into(*self).saturating_div(rhv.into()).into() } + /// Saturating subtraction in RAO units. fn saturating_sub(&self, rhv: Self) -> Self { Into::::into(*self).saturating_sub(rhv.into()).into() } + /// Saturating multiplication in RAO units. fn saturating_mul(&self, rhv: Self) -> Self { Into::::into(*self).saturating_mul(rhv.into()).into() } @@ -703,6 +726,7 @@ impl Into for TaoBalance { } } +/// `Get` wrapper for const RAO amounts in runtime configs (`ConstTao::`). pub struct ConstTao; impl Get for ConstTao { diff --git a/common/src/evm_context.rs b/common/src/evm_context.rs index 62ae973866..71453fcc75 100644 --- a/common/src/evm_context.rs +++ b/common/src/evm_context.rs @@ -1,3 +1,8 @@ +//! Thread-local flag marking dispatch that originated from an EVM precompile. +//! +//! Precompiles wrap `dispatch` in [`with_evm_context`] so fee / origin logic can distinguish +//! EVM-driven calls from native extrinsics via [`is_in_evm`]. + environmental::environmental!(IN_EVM: bool); /// Returns `true` if the current dispatch originated from an EVM precompile. diff --git a/common/src/lib.rs b/common/src/lib.rs index aa92e318ee..bf0676dfc8 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,3 +1,7 @@ +//! Shared runtime types for Subtensor: subnet IDs, balances, proxy filters, and cross-pallet traits. +//! +//! Crate name is `subtensor-runtime-common`. Prefer this module for `NetUid`, `TaoBalance`, +//! `AlphaBalance`, and proxy metadata rather than raw `u16`/`u64` at API boundaries. #![cfg_attr(not(feature = "std"), no_std)] use core::fmt::{self, Display, Formatter}; @@ -28,7 +32,7 @@ mod evm_context; mod proxy; mod transaction_error; -/// Balance of an account. +/// Account free-balance type; alias of [`TaoBalance`] (RAO / 1e9 TAO units). pub type Balance = TaoBalance; /// An index to a block. @@ -47,13 +51,19 @@ pub type Index = u32; /// A hash of some data used by the chain. pub type Hash = sp_core::H256; +/// Account extrinsic nonce (same width as [`Index`]). pub type Nonce = u32; -/// Transfers below SMALL_TRANSFER_LIMIT are considered small transfers +/// Max TAO amount (inclusive bound exclusive of limit) treated as a "small" proxy transfer: 0.5 TAO. pub const SMALL_TRANSFER_LIMIT: Balance = TaoBalance::new(500_000_000); // 0.5 TAO +/// Max alpha amount treated as a "small" proxy stake transfer: 0.5 alpha (same RAO scale as TAO). pub const SMALL_ALPHA_TRANSFER_LIMIT: AlphaBalance = AlphaBalance::new(500_000_000); // 0.5 Alpha -#[freeze_struct("4184c565055c66a7")] +/// Subnet identifier (`netuid`): opaque `u16` newtype used as the canonical subnet key. +/// +/// `0` is the root network ([`NetUid::ROOT`]). SCALE-identical to `u16`; metadata keeps the path +/// so SDKs emit a distinct type rather than a bare integer. +#[freeze_struct("a4e7d2e5110c5c67")] #[repr(transparent)] #[derive( Deserialize, @@ -77,20 +87,25 @@ pub const SMALL_ALPHA_TRANSFER_LIMIT: AlphaBalance = AlphaBalance::new(500_000_0 pub struct NetUid(u16); impl NetUid { + /// Root network (`netuid == 0`); emission/weight parent of non-root subnets. pub const ROOT: NetUid = Self(0); + /// Returns `true` when this is the root network. pub fn is_root(&self) -> bool { *self == Self::ROOT } + /// Next netuid (`saturating_add(1)`); used when allocating sequential subnet IDs. pub fn next(&self) -> NetUid { Self(self.0.saturating_add(1)) } + /// Previous netuid (`saturating_sub(1)`). pub fn prev(&self) -> NetUid { Self(self.0.saturating_sub(1)) } + /// Inner `u16` for storage math that still takes raw integers. pub fn inner(&self) -> u16 { self.0 } @@ -132,31 +147,49 @@ impl From for NetUid { } } +/// Read-only subnet metadata used by swap, leases, and other pallets that must not depend on +/// the full subtensor pallet. pub trait SubnetInfo { + /// Whether a subnet with this netuid exists. fn exists(netuid: NetUid) -> bool; + /// Subnet mechanism / consensus mode code for `netuid`. fn mechanism(netuid: NetUid) -> u16; + /// Whether `account_id` is the subnet owner coldkey. fn is_owner(account_id: &AccountId, netuid: NetUid) -> bool; + /// Whether the subnet's alpha (subtoken) is enabled for trading/staking. fn is_subtoken_enabled(netuid: NetUid) -> bool; + /// Per-uid validator trust vector for the subnet. fn get_validator_trust(netuid: NetUid) -> Vec; + /// Per-uid validator permit flags for the subnet. fn get_validator_permit(netuid: NetUid) -> Vec; + /// Hotkey registered at `uid` on `netuid`, if any. fn hotkey_of_uid(netuid: NetUid, uid: u16) -> Option; } +/// AMM / pool reserve accessors for a subnet's TAO or alpha side (`C: Token`). pub trait TokenReserve { + /// Current reserve amount for `netuid`. fn reserve(netuid: NetUid) -> C; + /// Credit supply provided into the reserve (liquidity in). fn increase_provided(netuid: NetUid, amount: C); + /// Debit supply provided from the reserve (liquidity out). fn decrease_provided(netuid: NetUid, amount: C); } +/// Cross-pallet stake and free-balance operations used by swap and limit-order flows. pub trait BalanceOps { + /// Free TAO balance of `account_id`. fn tao_balance(account_id: &AccountId) -> TaoBalance; + /// Alpha stake of (`coldkey`, `hotkey`) on `netuid`. fn alpha_balance(netuid: NetUid, coldkey: &AccountId, hotkey: &AccountId) -> AlphaBalance; + /// Increase alpha stake for (`coldkey`, `hotkey`) on `netuid`. fn increase_stake( coldkey: &AccountId, hotkey: &AccountId, netuid: NetUid, alpha: AlphaBalance, ) -> Result<(), DispatchError>; + /// Decrease alpha stake for (`coldkey`, `hotkey`) on `netuid`. fn decrease_stake( coldkey: &AccountId, hotkey: &AccountId, @@ -171,6 +204,7 @@ pub trait AuthorshipInfo { fn author() -> Option; } +/// Block-time constants shared by the runtime (slot duration, minutes/hours/days in blocks). pub mod time { use super::*; @@ -192,7 +226,11 @@ pub mod time { pub const DAYS: BlockNumber = HOURS * 24; } -#[freeze_struct("2477c9af9b0c5c26")] +/// Sub-subnet / mechanism identifier within a parent [`NetUid`]. +/// +/// `0` ([`MechId::MAIN`]) is the primary mechanism. Combined with netuid into +/// [`NetUidStorageIndex`] for per-mechanism epoch maps (`index = netuid + mecid * 4096`). +#[freeze_struct("56b975909205713f")] #[repr(transparent)] #[derive( Deserialize, @@ -216,6 +254,7 @@ pub mod time { pub struct MechId(u8); impl MechId { + /// Primary mechanism for a subnet (`mecid == 0`). pub const MAIN: MechId = Self(0); } @@ -267,7 +306,12 @@ impl From> for MechId { } } -#[freeze_struct("c6bf75ee25c00b9")] +/// Storage key for per-mechanism epoch maps (weights, bonds, incentives, …). +/// +/// Encoding: `netuid + mecid * GLOBAL_MAX_SUBNET_COUNT` (4096). For `mecid == 0` this equals +/// [`NetUid`], preserving pre-mechanism storage layout. Prefer +/// `Pallet::get_mechanism_storage_index` over hand-rolled arithmetic. +#[freeze_struct("ca534527653f6010")] #[repr(transparent)] #[derive( Deserialize, @@ -291,6 +335,7 @@ impl From> for MechId { pub struct NetUidStorageIndex(u16); impl NetUidStorageIndex { + /// Root-network storage index (`0`), same as [`NetUid::ROOT`]. pub const ROOT: NetUidStorageIndex = Self(0); } @@ -412,7 +457,7 @@ mod tests { } #[test] - fn test_clear_prefix_with_meter_respects_budget() { + fn clear_prefix_with_meter_respects_budget() { let netuid = NetUid::from(42); let entry_weight = Weight::from_parts(REF_TIME_WEIGHT, PROOF_SIZE_WEIGHT); let mut ext = sp_io::TestExternalities::default(); @@ -442,7 +487,7 @@ mod tests { } #[test] - fn test_clear_prefix_with_meter_zero_budget_is_noop() { + fn clear_prefix_with_meter_zero_budget_is_noop() { let netuid = NetUid::from(43); let entry_weight = Weight::from_parts(REF_TIME_WEIGHT, PROOF_SIZE_WEIGHT); let mut ext = sp_io::TestExternalities::default(); @@ -463,7 +508,7 @@ mod tests { } #[test] - fn test_clear_prefix_with_meter_completes_with_enough_budget() { + fn clear_prefix_with_meter_completes_with_enough_budget() { let netuid = NetUid::from(44); let entry_weight = Weight::from_parts(REF_TIME_WEIGHT, PROOF_SIZE_WEIGHT); let mut ext = sp_io::TestExternalities::default(); diff --git a/common/src/proxy.rs b/common/src/proxy.rs index f40b3f2076..d8b4270f4f 100644 --- a/common/src/proxy.rs +++ b/common/src/proxy.rs @@ -1,14 +1,20 @@ +//! Proxy-type identifiers and client-facing call-filter metadata for pallet-proxy. +//! +//! Runtime filtering in `runtime/src/proxy_filters` remains the source of truth. +//! Types here are the on-chain / RPC view of the same allowlists (`ProxyType`, +//! [`CallInfo`], [`ProxyFilterInfo`]). + use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; use frame_support::traits::{Contains, GetCallIndex, GetCallName, PalletInfoAccess}; use scale_info::TypeInfo; use sp_runtime::Vec; use subtensor_macros::freeze_struct; -/// Shared proxy filter model exposed by the runtime API. +/// Stable proxy-type identifiers used on-chain and by RPC clients. /// -/// Runtime filtering remains the source of truth. This metadata is the client-facing -/// allowlist view of the same rules. -/// Stable proxy type identifiers used on-chain and by RPC clients. +/// Variant order and the explicit `u8` mapping below are part of the wire / +/// announcement surface — do not reorder variants or renumber discriminants. +/// Deprecated variants ([`ProxyType::is_deprecated`]) always deny calls. #[derive( Copy, Clone, @@ -24,23 +30,41 @@ use subtensor_macros::freeze_struct; TypeInfo, )] pub enum ProxyType { + /// Unrestricted proxy: all runtime calls allowed. Any, + /// Subnet-owner call set. Owner, + /// Non-critical / low-risk call set. NonCritical, + /// Calls that do not move free TAO or stake. NonTransfer, + /// Deprecated senate governance proxy (always denies). Senate, - NonFungible, // Nothing involving moving TAO + /// Calls that do not move fungible TAO (NFTs / non-value paths). + NonFungible, + /// Deprecated triumvirate governance proxy (always denies). Triumvirate, + /// Deprecated governance proxy (always denies). Governance, + /// Staking / unstaking / swap stake call set. Staking, + /// Neuron / subnet registration call set. Registration, + /// Free-balance and stake transfer call set. Transfer, + /// Transfers bounded by [`crate::SMALL_TRANSFER_LIMIT`] / [`crate::SMALL_ALPHA_TRANSFER_LIMIT`]. SmallTransfer, - RootWeights, // Deprecated + /// Deprecated root-weights proxy (always denies). + RootWeights, + /// Child-hotkey relationship call set. ChildKeys, + /// Sudo `set_code` only (high privilege). SudoUncheckedSetCode, + /// Hotkey swap call set. SwapHotkey, + /// Subnet lease beneficiary call set. SubnetLeaseBeneficiary, + /// Root-claim call set. RootClaim, } @@ -98,6 +122,7 @@ impl From for u8 { } impl ProxyType { + /// Whether this proxy type is retired and always filters to deny. pub fn is_deprecated(&self) -> bool { matches!( self, @@ -112,7 +137,7 @@ impl Default for ProxyType { } } -/// Extra constraint attached to an allowed call. +/// Extra constraint attached to an allowed call in filter metadata. #[derive(Clone, PartialEq, Eq, Encode, Decode, Debug, TypeInfo)] pub enum CallConstraint { /// The named numeric parameter must be lower than `limit`. @@ -125,7 +150,7 @@ pub enum CallConstraint { }, } -/// Runtime call identity exposed in proxy filter metadata. +/// Runtime call identity exposed in proxy filter metadata (pallet + call + optional constraint). #[freeze_struct("85f86877d3d9b870")] #[derive(Clone, PartialEq, Eq, Encode, Decode, Debug, TypeInfo)] pub struct CallInfo { @@ -141,6 +166,9 @@ pub struct CallInfo { pub constraint: Option, } +/// Builds a [`CallInfo`] for pallet `P` call named `name` (no constraint). +/// +/// Panics if `name` is not a call on `P` — intended for const/filter-group construction. pub fn call_info_by_name( name: &str, ) -> CallInfo { @@ -168,6 +196,7 @@ pub fn call_info_by_name( /// Implementations should be generated from the same rules as the executable /// filter so clients and runtime behavior cannot drift. pub trait CallFilterMetadata { + /// Flat list of allowed calls (and constraints) for this filter group. fn call_infos() -> Vec; } @@ -191,7 +220,7 @@ impl CallFilterMetadata for Tuple { } } -/// Public metadata model for a proxy filter. +/// Public metadata model for a proxy filter allowlist shape. #[derive(Clone, PartialEq, Eq, Encode, Decode, Debug, TypeInfo)] pub enum FilterMode { /// All runtime calls are allowed. @@ -200,20 +229,28 @@ pub enum FilterMode { Allow(Vec), } -/// Runtime API response for one proxy type. -#[freeze_struct("288413f4da5ab4ee")] +/// Runtime API response describing one [`ProxyType`]'s filter. +#[freeze_struct("13eab55e0c9576a8")] #[derive(Clone, PartialEq, Eq, Encode, Decode, Debug, TypeInfo)] pub struct ProxyFilterInfo { + /// [`ProxyType`] as its stable `u8` discriminant. pub proxy_type: u8, + /// Human-readable proxy type name (UTF-8 bytes). pub name: Vec, + /// Whether the proxy type is deprecated and always denies. pub deprecated: bool, + /// Allow-all vs allow-list filter mode. pub filter_mode: FilterMode, } -#[freeze_struct("b0cce66ed9b2451b")] +/// Compact name/index/deprecated triple for listing proxy types over RPC. +#[freeze_struct("d8933caab5cdc1e")] #[derive(Clone, PartialEq, Eq, Encode, Decode, Debug, TypeInfo)] pub struct ProxyTypeInfo { + /// Human-readable proxy type name (UTF-8 bytes). pub name: Vec, + /// Stable `u8` discriminant matching [`ProxyType`]. pub index: u8, + /// Whether the proxy type is deprecated. pub deprecated: bool, } diff --git a/common/src/transaction_error.rs b/common/src/transaction_error.rs index 0b735429b0..ecfd69a5ae 100644 --- a/common/src/transaction_error.rs +++ b/common/src/transaction_error.rs @@ -1,37 +1,75 @@ +//! Custom `InvalidTransaction::Custom(u8)` codes shared across Subtensor signed extensions. +//! +//! The `u8` values in [`From for u8`] are part of the client-facing +//! validity surface — do not renumber existing variants. Variant order in the enum is not the +//! wire encoding; the explicit match arms are. + use sp_runtime::transaction_validity::{InvalidTransaction, TransactionValidityError}; +/// Custom transaction-validity error codes returned by Subtensor signed extensions / checks. +/// +/// Converted to `InvalidTransaction::Custom(u8)` via [`From`]. Prefer matching on this enum +/// in runtime code; explorers and SDKs decode the raw `u8`. #[derive(Debug, PartialEq)] pub enum CustomTransactionError { - /// Deprecated: coldkey swap now uses announcements and check moved to DispatchGuard + /// Deprecated: coldkey swap now uses announcements; check moved to DispatchGuard. #[deprecated] ColdkeyInSwapSchedule, + /// Stake amount below the extrinsic minimum. StakeAmountTooLow, + /// Free balance too low for the requested operation. BalanceTooLow, + /// Target subnet (netuid) does not exist. SubnetNotExists, + /// Hotkey account is not known / not registered where required. HotkeyAccountDoesntExist, + /// Stake balance insufficient for withdraw / unstake. NotEnoughStakeToWithdraw, + /// Caller exceeded a rate limit. RateLimitExceeded, + /// AMM / swap pool lacks liquidity for the trade. InsufficientLiquidity, + /// Swap slippage exceeds the caller's limit. SlippageTooHigh, + /// Transfer path is disabled for this account or subnet. TransferDisallowed, + /// Hotkey is not registered on the target subnet. HotKeyNotRegisteredInNetwork, + /// Axon / serve endpoint IP is invalid. InvalidIpAddress, + /// Axon serve rate limit exceeded. ServingRateLimitExceeded, + /// Axon / serve endpoint port is invalid. InvalidPort, + /// Generic malformed request (maps to custom code `255`). BadRequest, + /// `max_amount` / similar bound was zero when a positive limit is required. ZeroMaxAmount, + /// Commit-reveal round is invalid for the current window. InvalidRevealRound, + /// Expected commit was not found in storage. CommitNotFound, + /// Commit block is outside the allowed reveal range. CommitBlockNotInRevealRange, + /// Parallel input vectors have unequal lengths. InputLengthsUnequal, + /// Neuron uid not found on the subnet. UidNotFound, + /// EVM↔coldkey association rate limit exceeded. EvmKeyAssociateRateLimitExceeded, + /// Coldkey swap is blocked by an active dispute. ColdkeySwapDisputed, + /// Proxy / nested origin real account is invalid. InvalidRealAccount, + /// Shielded transaction bytes failed to parse. FailedShieldedTxParsing, + /// Shielded transaction public-key hash is invalid. InvalidShieldedTxPubKeyHash, + /// Coldkey is not associated with the required hotkey / EVM key. NonAssociatedColdKey, + /// Delegate take is below the allowed minimum. DelegateTakeTooLow, + /// Delegate take is above the allowed maximum. DelegateTakeTooHigh, } diff --git a/docs/errors/chain/CannotUseSystemAccount.mdx b/docs/errors/chain/CannotUseSystemAccount.mdx index 7ca4b4b9c9..da34ec1f63 100644 --- a/docs/errors/chain/CannotUseSystemAccount.mdx +++ b/docs/errors/chain/CannotUseSystemAccount.mdx @@ -5,7 +5,7 @@ description: "Sign with the key or origin that owns the target object, then retr {/* GENERATED by scripts/generate.py from the SDK registries — do not edit. */} -The hotkey supplied for registration, hotkey swap, or subnet-owner-hotkey assignment is a reserved subnet system account. Use a regular user-generated hotkey instead; system accounts are derived per-subnet and rejected by `is_subnet_account_id`. +The hotkey supplied for registration, hotkey swap, or subnet-owner-hotkey assignment is a reserved subnet system account. Use a regular user-generated hotkey instead; system accounts are derived per-subnet and rejected by `netuid_for_subnet_account`. Declared by the `SubtensorModule` pallet; it classifies to the semantic code [`not_authorized`](/docs/errors/not-authorized). diff --git a/docs/errors/chain/index.mdx b/docs/errors/chain/index.mdx index 7a007673be..3765b6e673 100644 --- a/docs/errors/chain/index.mdx +++ b/docs/errors/chain/index.mdx @@ -46,7 +46,7 @@ The exact chain error name (from the extrinsic receipt) maps to a semantic [code | [`CannotBurnOrRecycleOnRootSubnet`](/docs/errors/chain/CannotBurnOrRecycleOnRootSubnet) | [`invalid_argument`](/docs/errors/invalid-argument) | `recycle_alpha` or `burn_alpha` was called with netuid 0, and TAO on the root subnet cannot be burned or recycled. Pass a non-root `netuid` argument for the subnet whose alpha you want to recycle or burn. | | [`CannotEndInPast`](/docs/errors/chain/CannotEndInPast) | [`invalid_argument`](/docs/errors/invalid-argument) | The `end` block passed to `create` or `update_end` is not after the current block. Compare the `end` argument against the current block number; it must be strictly greater. | | [`CannotReleaseYet`](/docs/errors/chain/CannotReleaseYet) | [`too_early`](/docs/errors/too-early) | `release_deposit` was called too early: the current block must exceed the deposit's block plus `ReleaseDelay`, and safe-mode must be exited. Check the block key of the entry in `Deposits` against the `ReleaseDelay` config. | -| [`CannotUseSystemAccount`](/docs/errors/chain/CannotUseSystemAccount) | [`not_authorized`](/docs/errors/not-authorized) | The hotkey supplied for registration, hotkey swap, or subnet-owner-hotkey assignment is a reserved subnet system account. Use a regular user-generated hotkey instead; system accounts are derived per-subnet and rejected by `is_subnet_account_id`. | +| [`CannotUseSystemAccount`](/docs/errors/chain/CannotUseSystemAccount) | [`not_authorized`](/docs/errors/not-authorized) | The hotkey supplied for registration, hotkey swap, or subnet-owner-hotkey assignment is a reserved subnet system account. Use a regular user-generated hotkey instead; system accounts are derived per-subnet and rejected by `netuid_for_subnet_account`. | | [`CapNotRaised`](/docs/errors/chain/CapNotRaised) | [`too_early`](/docs/errors/too-early) | `finalize` was called before the crowdloan's `raised` amount equals its `cap`. Compare the `raised` and `cap` fields of the `Crowdloans` entry; contribute the remainder or lower the cap with `update_cap` before finalizing. | | [`CapRaised`](/docs/errors/chain/CapRaised) | [`limit_exceeded`](/docs/errors/limit-exceeded) | A contribution was attempted on a crowdloan whose `raised` amount has already reached its `cap`, so no further contributions are accepted. Compare the `raised` and `cap` fields of the `Crowdloans` entry for the `crowdloan_id`. | | [`CapTooLow`](/docs/errors/chain/CapTooLow) | [`invalid_argument`](/docs/errors/invalid-argument) | On `create` the `cap` is not strictly greater than the initial `deposit`, or on `update_cap` the new cap is below the amount already raised. Compare the cap argument against the `deposit` or the `raised` field of the `Crowdloans` entry. | diff --git a/docs/errors/not-authorized.mdx b/docs/errors/not-authorized.mdx index f2f2ab34a0..f5df08ae0d 100644 --- a/docs/errors/not-authorized.mdx +++ b/docs/errors/not-authorized.mdx @@ -22,7 +22,7 @@ The exact chain error names (from the extrinsic receipt) that classify to `not_a | [`AccountNotAllowedCommit`](/docs/errors/chain/AccountNotAllowedCommit) | Raised by `set_commitment` when the runtime commit check fails: the subnet must exist and the signing hotkey must be registered on it. Verify the `netuid` and that the hotkey has a UID on that subnet. | | [`BeneficiaryDoesNotOwnHotkey`](/docs/errors/chain/BeneficiaryDoesNotOwnHotkey) | When ending a subnet lease, the hotkey passed for the ownership handover is not owned by the lease's beneficiary coldkey. Check the `Owner` storage for that hotkey and pass a hotkey the beneficiary coldkey actually owns. | | [`CallFiltered`](/docs/errors/chain/CallFiltered) | The runtime's origin call filter (e.g. `BaseCallFilter` or a restricted origin) rejected this call before dispatch. Check whether the specific call is permitted for the origin you used, including any proxy or safe-mode filtering in effect. | -| [`CannotUseSystemAccount`](/docs/errors/chain/CannotUseSystemAccount) | The hotkey supplied for registration, hotkey swap, or subnet-owner-hotkey assignment is a reserved subnet system account. Use a regular user-generated hotkey instead; system accounts are derived per-subnet and rejected by `is_subnet_account_id`. | +| [`CannotUseSystemAccount`](/docs/errors/chain/CannotUseSystemAccount) | The hotkey supplied for registration, hotkey swap, or subnet-owner-hotkey assignment is a reserved subnet system account. Use a regular user-generated hotkey instead; system accounts are derived per-subnet and rejected by `netuid_for_subnet_account`. | | [`ColdkeySwapDisputed`](/docs/errors/chain/ColdkeySwapDisputed) | All extrinsics from this coldkey are blocked because its pending coldkey swap is under dispute. Check the `ColdkeySwapDisputes` storage for the coldkey; the dispute must be resolved by root before the account can transact. | | [`CreateOriginNotAllowed`](/docs/errors/chain/CreateOriginNotAllowed) | A CREATE, or a CALL that performs a nested CREATE, was attempted from an EVM address not permitted to deploy contracts. Check whether the deploying address is in the chain's allowed-deployers list. | | [`ExpectedBeneficiaryOrigin`](/docs/errors/chain/ExpectedBeneficiaryOrigin) | A lease operation such as terminating a subnet lease was signed by an account other than the lease's beneficiary coldkey. Check the beneficiary recorded in the `SubnetLeases` storage for the lease id and sign with that coldkey. | diff --git a/docs/hyperparameters/bonds-reset-enabled.mdx b/docs/hyperparameters/bonds-reset-enabled.mdx index 4c645a74a2..3ab4a2e7e5 100644 --- a/docs/hyperparameters/bonds-reset-enabled.mdx +++ b/docs/hyperparameters/bonds-reset-enabled.mdx @@ -7,7 +7,7 @@ description: Whether a neuron's accrued validator bonds are wiped when it commit ## How it works -When a hotkey commits metadata (the commitments pallet's [`OnMetadataCommitment`](/code/pallets/commitments/src/lib.rs#L367) hook, wired up as [`ResetBondsOnCommit`](/code/runtime/src/lib.rs#L728-L741) in `runtime/src/lib.rs`), the chain calls [`do_reset_bonds`](/code/pallets/subtensor/src/epoch/run_epoch.rs#L1611-L1643) in `pallets/subtensor/src/epoch/run_epoch.rs` for each mechanism of the subnet. If this flag is off, the call returns immediately. If it is on, the chain looks up the committing hotkey's UID and filters that UID's column out of every validator's bond vector — every bond held **on** the committing neuron is deleted, while bonds the neuron holds as a validator on others are untouched. +When a hotkey commits metadata (the commitments pallet's [`OnMetadataCommitment`](/code/pallets/commitments/src/lib.rs#L367) hook, wired up as [`ResetBondsOnCommit`](/code/runtime/src/lib.rs#L728-L741) in `runtime/src/lib.rs`), the chain calls [`reset_bonds_column_for_hotkey`](/code/pallets/subtensor/src/epoch/run_epoch.rs#L1611-L1643) in `pallets/subtensor/src/epoch/run_epoch.rs` for each mechanism of the subnet. If this flag is off, the call returns immediately. If it is on, the chain looks up the committing hotkey's UID and filters that UID's column out of every validator's bond vector — every bond held **on** the committing neuron is deleted, while bonds the neuron holds as a validator on others are untouched. Bonds then rebuild through the normal EMA at the rate set by [`bonds_moving_avg`](/docs/hyperparameters/bonds-moving-avg) (or the liquid-alpha rate), so validators who re-endorse the refreshed miner regain dividends over subsequent epochs. diff --git a/docs/tx/announce-coldkey-swap.mdx b/docs/tx/announce-coldkey-swap.mdx index 26d4081d16..d554f43b55 100644 --- a/docs/tx/announce-coldkey-swap.mdx +++ b/docs/tx/announce-coldkey-swap.mdx @@ -91,7 +91,7 @@ pub fn announce_coldkey_swap( } else { // Only charge the swap cost on the first announcement let swap_cost = Self::get_key_swap_cost(); - Self::charge_swap_cost(&who, swap_cost)?; + Self::charge_coldkey_swap_cost(&who, swap_cost)?; } let delay = ColdkeySwapAnnouncementDelay::::get(); @@ -106,6 +106,6 @@ pub fn announce_coldkey_swap( } ``` -Delegates to [`get_key_swap_cost`](/code/pallets/subtensor/src/utils/misc.rs#L827), [`charge_swap_cost`](/code/pallets/subtensor/src/swap/swap_coldkey.rs#L68). +Delegates to [`get_key_swap_cost`](/code/pallets/subtensor/src/utils/misc.rs#L827), [`charge_coldkey_swap_cost`](/code/pallets/subtensor/src/swap/swap_coldkey.rs#L68). Every file is browsable under [/code](/code) exactly as built into the runtime, or as plain text under `/code/raw/` (index: [`/code/index.json`](/code/index.json)). diff --git a/docs/tx/batch.mdx b/docs/tx/batch.mdx index 94675f5a4b..ea763bb90a 100644 --- a/docs/tx/batch.mdx +++ b/docs/tx/batch.mdx @@ -70,7 +70,7 @@ result = sub.execute_tool("batch", {...}, wallet) ```rust #[pallet::call_index(2)] #[pallet::weight({ - let (dispatch_weight, pays) = Pallet::::weight_and_dispatch_class(calls); + let (dispatch_weight, pays) = Pallet::::batch_calls_weight_and_pays(calls); let dispatch_weight = dispatch_weight.saturating_add(T::WeightInfo::batch_all(calls.len() as u32)); (dispatch_weight, DispatchClass::Normal, pays) })] diff --git a/docs/tx/execute-proxy-announced.mdx b/docs/tx/execute-proxy-announced.mdx index f86f605cd7..66d6d9e5e9 100644 --- a/docs/tx/execute-proxy-announced.mdx +++ b/docs/tx/execute-proxy-announced.mdx @@ -106,7 +106,7 @@ result = sub.execute_tool("execute_proxy_announced", {...}, wallet) }) .map_err(|_| Error::::Unannounced)?; - Self::do_proxy(def, real, *call); + Self::dispatch_filtered_proxy_call(def, real, *call); Ok(()) } diff --git a/docs/tx/set-auto-stake.mdx b/docs/tx/set-auto-stake.mdx index c098db4fd6..f04ebf15f5 100644 --- a/docs/tx/set-auto-stake.mdx +++ b/docs/tx/set-auto-stake.mdx @@ -78,7 +78,7 @@ pub fn set_coldkey_auto_stake_hotkey( hotkey: T::AccountId, ) -> DispatchResult { let coldkey = ensure_signed(origin)?; - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); ensure!( Uids::::contains_key(netuid, &hotkey), Error::::HotKeyNotRegisteredInSubNet @@ -115,6 +115,6 @@ pub fn set_coldkey_auto_stake_hotkey( } ``` -Delegates to [`if_subnet_exist`](/code/pallets/subtensor/src/subnets/subnet.rs#L39). +Delegates to [`subnet_exists`](/code/pallets/subtensor/src/subnets/subnet.rs#L39). Every file is browsable under [/code](/code) exactly as built into the runtime, or as plain text under `/code/raw/` (index: [`/code/index.json`](/code/index.json)). diff --git a/docs/tx/set-mechanism-count.mdx b/docs/tx/set-mechanism-count.mdx index e61e34b124..026bff8f3b 100644 --- a/docs/tx/set-mechanism-count.mdx +++ b/docs/tx/set-mechanism-count.mdx @@ -78,7 +78,7 @@ pub fn sudo_set_mechanism_count( netuid: NetUid, mechanism_count: MechId, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[TransactionType::MechanismCountUpdate], @@ -87,7 +87,7 @@ pub fn sudo_set_mechanism_count( pallet_subtensor::Pallet::::do_set_mechanism_count(netuid, mechanism_count)?; - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[TransactionType::MechanismCountUpdate], diff --git a/docs/tx/set-subnet-emission-enabled.mdx b/docs/tx/set-subnet-emission-enabled.mdx index de58d374ae..f3c7cff216 100644 --- a/docs/tx/set-subnet-emission-enabled.mdx +++ b/docs/tx/set-subnet-emission-enabled.mdx @@ -98,7 +98,7 @@ pub fn sudo_set_subnet_emission_enabled( ensure_root(origin)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); ensure!(!netuid.is_root(), Error::::NotPermittedOnRootSubnet); diff --git a/docs/tx/swap-coldkey-announced.mdx b/docs/tx/swap-coldkey-announced.mdx index e48ba8ec55..9ebc5e3348 100644 --- a/docs/tx/swap-coldkey-announced.mdx +++ b/docs/tx/swap-coldkey-announced.mdx @@ -86,12 +86,12 @@ pub fn swap_coldkey_announced( let now = >::block_number(); ensure!(now >= when, Error::::ColdkeySwapTooEarly); - Self::do_swap_coldkey(&who, &new_coldkey)?; + Self::perform_coldkey_swap(&who, &new_coldkey)?; Ok(()) } ``` -Delegates to [`do_swap_coldkey`](/code/pallets/subtensor/src/swap/swap_coldkey.rs#L8). +Delegates to [`perform_coldkey_swap`](/code/pallets/subtensor/src/swap/swap_coldkey.rs#L8). Every file is browsable under [/code](/code) exactly as built into the runtime, or as plain text under `/code/raw/` (index: [`/code/index.json`](/code/index.json)). diff --git a/docs/tx/swap-hotkey.mdx b/docs/tx/swap-hotkey.mdx index 9c91bca8ae..88992b9b5d 100644 --- a/docs/tx/swap-hotkey.mdx +++ b/docs/tx/swap-hotkey.mdx @@ -89,10 +89,10 @@ pub fn swap_hotkey( new_hotkey: T::AccountId, netuid: Option, ) -> DispatchResultWithPostInfo { - Self::do_swap_hotkey(origin, &hotkey, &new_hotkey, netuid, false) + Self::perform_hotkey_swap(origin, &hotkey, &new_hotkey, netuid, false) } ``` -Delegates to [`do_swap_hotkey`](/code/pallets/subtensor/src/swap/swap_hotkey.rs#L72). +Delegates to [`perform_hotkey_swap`](/code/pallets/subtensor/src/swap/swap_hotkey.rs#L72). Every file is browsable under [/code](/code) exactly as built into the runtime, or as plain text under `/code/raw/` (index: [`/code/index.json`](/code/index.json)). diff --git a/docs/tx/trim-subnet.mdx b/docs/tx/trim-subnet.mdx index 0c318f7804..287477ab9b 100644 --- a/docs/tx/trim-subnet.mdx +++ b/docs/tx/trim-subnet.mdx @@ -83,7 +83,7 @@ pub fn sudo_trim_to_max_allowed_uids( netuid: NetUid, max_n: u16, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin.clone(), netuid, &[TransactionType::MaxUidsTrimming], @@ -92,7 +92,7 @@ pub fn sudo_trim_to_max_allowed_uids( pallet_subtensor::Pallet::::trim_to_max_allowed_uids(netuid, max_n)?; - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[TransactionType::MaxUidsTrimming], diff --git a/docs/tx/update-symbol.mdx b/docs/tx/update-symbol.mdx index e028138654..a6961e4e1c 100644 --- a/docs/tx/update-symbol.mdx +++ b/docs/tx/update-symbol.mdx @@ -78,7 +78,7 @@ pub fn update_symbol( symbol: Vec, ) -> DispatchResult { Self::ensure_subnet_owner_or_root(origin, netuid)?; - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); Self::ensure_symbol_exists(&symbol)?; Self::ensure_symbol_available(&symbol)?; @@ -90,6 +90,6 @@ pub fn update_symbol( } ``` -Delegates to [`ensure_subnet_owner_or_root`](/code/pallets/subtensor/src/utils/misc.rs#L12), [`if_subnet_exist`](/code/pallets/subtensor/src/subnets/subnet.rs#L39), [`ensure_symbol_exists`](/code/pallets/subtensor/src/subnets/symbols.rs#L960). +Delegates to [`ensure_subnet_owner_or_root`](/code/pallets/subtensor/src/utils/misc.rs#L12), [`subnet_exists`](/code/pallets/subtensor/src/subnets/subnet.rs#L39), [`ensure_symbol_exists`](/code/pallets/subtensor/src/subnets/symbols.rs#L960). Every file is browsable under [/code](/code) exactly as built into the runtime, or as plain text under `/code/raw/` (index: [`/code/index.json`](/code/index.json)). diff --git a/eco-tests/src/mock.rs b/eco-tests/src/mock.rs index a4929a2cb7..d9d28bd76f 100644 --- a/eco-tests/src/mock.rs +++ b/eco-tests/src/mock.rs @@ -331,7 +331,7 @@ impl pallet_subtensor::Config for Test { type LeaseDividendsDistributionInterval = LeaseDividendsDistributionInterval; type GetCommitments = (); type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; - type CommitmentsInterface = CommitmentsI; + type CommitmentsInterface = CommitmentsPurgeBridge; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; type AuthorshipProvider = MockAuthorshipProvider; type SubtensorPalletId = SubtensorPalletId; @@ -369,8 +369,8 @@ impl PrivilegeCmp for OriginPrivilegeCmp { } } -pub struct CommitmentsI; -impl CommitmentsInterface for CommitmentsI { +pub struct CommitmentsPurgeBridge; +impl CommitmentsInterface for CommitmentsPurgeBridge { fn purge_netuid( _netuid: NetUid, _weight_meter: &mut frame_support::weights::WeightMeter, diff --git a/node/build.rs b/node/build.rs index f9d839f9be..84b1a1edc2 100644 --- a/node/build.rs +++ b/node/build.rs @@ -1,3 +1,7 @@ +//! Build script for the `node-subtensor` binary. +//! +//! Emits Substrate cargo keys (impl version / commit) and reruns when git HEAD changes. + use substrate_build_script_utils::{generate_cargo_keys, rerun_if_git_head_changed}; fn main() { diff --git a/node/src/benchmarking.rs b/node/src/benchmarking.rs index d0c0ac9a40..1e12249ce9 100644 --- a/node/src/benchmarking.rs +++ b/node/src/benchmarking.rs @@ -1,4 +1,4 @@ -//! Setup code for [`super::command`] which would otherwise bloat that module. +//! Extrinsic builders and inherent data for `benchmark overhead` / extrinsic commands. //! //! Should only be used for benchmarking as it may break in other contexts. @@ -18,15 +18,15 @@ use subtensor_runtime_common::{AccountId, Balance, Signature, TaoBalance}; use std::{sync::Arc, time::Duration}; -// Generates extrinsics for the `benchmark overhead` command. -// -// Note: Should only be used for benchmarking. +/// Builds `system.remark` extrinsics for the `benchmark overhead` command. +/// +/// Note: Should only be used for benchmarking. pub struct RemarkBuilder { client: Arc, } impl RemarkBuilder { - // Creates a new [`Self`] from the given client. + /// Creates a new [`Self`] from the given client. pub fn new(client: Arc) -> Self { Self { client } } @@ -55,9 +55,9 @@ impl frame_benchmarking_cli::ExtrinsicBuilder for RemarkBuilder { } } -// Generates `Balances::TransferKeepAlive` extrinsics for the benchmarks. -// -// Note: Should only be used for benchmarking. +/// Builds `balances.transfer_keep_alive` extrinsics for extrinsic benchmarks. +/// +/// Note: Should only be used for benchmarking. pub struct TransferKeepAliveBuilder { client: Arc, dest: AccountId, @@ -65,7 +65,7 @@ pub struct TransferKeepAliveBuilder { } impl TransferKeepAliveBuilder { - // Creates a new [`Self`] from the given client. + /// Creates a new [`Self`] from the given client. pub fn new(client: Arc, dest: AccountId, value: Balance) -> Self { Self { client, @@ -102,9 +102,9 @@ impl frame_benchmarking_cli::ExtrinsicBuilder for TransferKeepAliveBuilder { } } -// Create a transaction using the given `call`. -// -// Note: Should only be used for benchmarking. +/// Sign a runtime call into an unchecked extrinsic for benchmarking. +/// +/// Note: Should only be used for benchmarking. #[allow(clippy::expect_used)] pub fn create_benchmark_extrinsic( client: &FullClient, @@ -172,9 +172,9 @@ pub fn create_benchmark_extrinsic( ) } -// Generates inherent data for the `benchmark overhead` command. -// -// Note: Should only be used for benchmarking. +/// Generate timestamp inherent data for the `benchmark overhead` command. +/// +/// Note: Should only be used for benchmarking. pub fn inherent_benchmark_data() -> Result { let mut inherent_data = InherentData::new(); let d = Duration::from_millis(0); diff --git a/node/src/chain_spec/devnet.rs b/node/src/chain_spec/devnet.rs index cb3bc66924..c7a0b5a2bc 100644 --- a/node/src/chain_spec/devnet.rs +++ b/node/src/chain_spec/devnet.rs @@ -1,8 +1,13 @@ +//! Public development-network chain-spec (`--chain devnet`). +//! +//! Authorities are fixed SS58 keys for the shared debug validator set. + // Allowed since it's actually better to panic during chain setup when there is an error #![allow(clippy::unwrap_used)] use super::*; +/// Build the shared public-devnet `ChainSpec`. pub fn devnet_config() -> Result { let wasm_binary = WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?; diff --git a/node/src/chain_spec/finney.rs b/node/src/chain_spec/finney.rs index 4455466d26..4c42428592 100644 --- a/node/src/chain_spec/finney.rs +++ b/node/src/chain_spec/finney.rs @@ -1,9 +1,15 @@ +//! Finney mainnet chain-spec (`--chain finney`). +//! +//! Genesis state is loaded from `./snapshot.json` (mmap + serde) and patched +//! into a live-network `ChainSpec`. + // Allowed since it's actually better to panic during chain setup when there is an error #![allow(clippy::unwrap_used)] use super::*; use hex::FromHex; +/// Build the Finney mainnet `ChainSpec` from the local nakamoto snapshot file. pub fn finney_mainnet_config() -> Result { let path: PathBuf = std::path::PathBuf::from("./snapshot.json"); let wasm_binary = WASM_BINARY.ok_or("Development wasm not available".to_string())?; diff --git a/node/src/chain_spec/localnet.rs b/node/src/chain_spec/localnet.rs index 57a60bbd1b..d2f66d3969 100644 --- a/node/src/chain_spec/localnet.rs +++ b/node/src/chain_spec/localnet.rs @@ -1,8 +1,14 @@ +//! Local development chain-spec (`--chain local` / `--chain dev`). +//! +//! Uses seed-derived Alice-style authorities; `single_authority` enables a +//! one-validator topology for solo node runs. + // Allowed since it's actually better to panic during chain setup when there is an error #![allow(clippy::unwrap_used)] use super::*; +/// Build a localnet / solo-dev `ChainSpec` with seed-derived PoA authorities. pub fn localnet_config(single_authority: bool) -> Result { let wasm_binary = WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?; diff --git a/node/src/chain_spec/mod.rs b/node/src/chain_spec/mod.rs index 85ca78d353..f62a88183a 100644 --- a/node/src/chain_spec/mod.rs +++ b/node/src/chain_spec/mod.rs @@ -1,3 +1,8 @@ +//! Chain-spec builders and crypto helpers for Subtensor networks. +//! +//! Submodules produce `ChainSpec`s for localnet, devnet, Finney mainnet, and +//! testnet. Shared helpers derive Aura/Grandpa authority keys from seeds or SS58. + // Allowed since it's actually better to panic during chain setup when there is an error #![allow(clippy::expect_used, clippy::unwrap_used)] @@ -58,6 +63,7 @@ pub fn authority_keys_from_seed(s: &str) -> (AuraId, GrandpaId) { (get_from_seed::(s), get_from_seed::(s)) } +/// Parse Aura and Grandpa authority public keys from SS58 addresses. pub fn authority_keys_from_ss58(s_aura: &str, s_grandpa: &str) -> (AuraId, GrandpaId) { ( get_aura_from_ss58_addr(s_aura), @@ -65,10 +71,12 @@ pub fn authority_keys_from_ss58(s_aura: &str, s_grandpa: &str) -> (AuraId, Grand ) } +/// Decode an Aura authority id from an SS58 string (panics on invalid input). pub fn get_aura_from_ss58_addr(s: &str) -> AuraId { Ss58Codec::from_ss58check(s).unwrap() } +/// Decode a Grandpa authority id from an SS58 string (panics on invalid input). pub fn get_grandpa_from_ss58_addr(s: &str) -> GrandpaId { Ss58Codec::from_ss58check(s).unwrap() } @@ -78,7 +86,7 @@ use serde::{Deserialize, Serialize}; use serde_json as json; use std::{fs::File, path::PathBuf}; -// Configure storage from nakamoto data +/// Snapshot JSON shape used when bootstrapping Finney / testnet genesis from nakamoto data. #[derive(Deserialize, Debug)] struct ColdkeyHotkeys { stakes: std::collections::HashMap>, diff --git a/node/src/chain_spec/testnet.rs b/node/src/chain_spec/testnet.rs index a092dbfc85..a7dba25152 100644 --- a/node/src/chain_spec/testnet.rs +++ b/node/src/chain_spec/testnet.rs @@ -1,3 +1,8 @@ +//! Finney testnet chain-spec (`--chain test_finney` / default empty id). +//! +//! Genesis state is loaded from `./snapshot.json` like mainnet, with a distinct +//! protocol id for the public test network. + // Allowed since it's actually better to panic during chain setup when there is an error #![allow(clippy::unwrap_used)] @@ -5,6 +10,7 @@ use super::*; const TESTNET_PROTOCOL_ID: &str = "bittensor-testnet"; +/// Build the Finney testnet `ChainSpec` from the local nakamoto snapshot file. pub fn finney_testnet_config() -> Result { let path: PathBuf = std::path::PathBuf::from("./snapshot.json"); let wasm_binary = WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?; diff --git a/node/src/cli.rs b/node/src/cli.rs index a35ea86029..6761a778aa 100644 --- a/node/src/cli.rs +++ b/node/src/cli.rs @@ -1,3 +1,8 @@ +//! CLI argument types for the Subtensor node binary. +//! +//! Covers sealing mode, initial Aura/Babe consensus choice, Frontier eth flags, +//! history-backfill policy, and the `build-patched-spec` clone-state subcommand. + use crate::{ client::{FullBackend, FullClient}, consensus::{AuraConsensus, BabeConsensus}, @@ -12,6 +17,7 @@ use std::fmt; use std::path::PathBuf; use std::sync::Arc; +/// Top-level Subtensor node CLI (run flags + optional subcommand). #[derive(Debug, clap::Parser)] pub struct Cli { #[command(subcommand)] @@ -45,6 +51,7 @@ pub struct Cli { pub history_backfill: HistoryBackfill, } +/// Node subcommands (keys, chain ops, benchmarks, patched-spec clone). #[allow(clippy::large_enum_variant)] #[derive(Debug, clap::Subcommand)] pub enum Subcommand { @@ -147,6 +154,7 @@ pub struct CloneStateCmd { pub charlie: bool, } +/// Whether to keep or skip historical gap-backfill during sync. #[derive(Debug, Clone, Copy, clap::ValueEnum, Default)] pub enum HistoryBackfill { #[default] @@ -208,9 +216,9 @@ pub enum SupportedConsensusMechanism { Aura, } -// Convinience methods for static dispatch of different service methods with -// different consensus mechanisms. +/// Convenience methods for static dispatch of service entrypoints per consensus. impl SupportedConsensusMechanism { + /// Build chain-ops components for this consensus mechanism. pub fn new_chain_ops( &self, config: &mut Configuration, diff --git a/node/src/client.rs b/node/src/client.rs index e65683933b..af22855e64 100644 --- a/node/src/client.rs +++ b/node/src/client.rs @@ -1,3 +1,9 @@ +//! Full-client and WASM-executor type aliases for the Subtensor node. +//! +//! Host functions always include runtime-benchmark shims because historical +//! genesis state was built with them; they are no-ops unless the runtime is +//! compiled with `runtime-benchmarks`. + use node_subtensor_runtime::{RuntimeApi, opaque::Block}; use polkadot_sdk::cumulus_primitives_proof_size_hostfunction::storage_proof_size::HostFunctions as ProofSize; use sc_executor::WasmExecutor; @@ -17,4 +23,5 @@ pub type HostFunctions = ( sp_crypto_ec_utils::bls12_381::host_calls::HostFunctions, ProofSize, ); +/// WASM executor wired with [`HostFunctions`]. pub type RuntimeExecutor = WasmExecutor; diff --git a/node/src/clone_spec.rs b/node/src/clone_spec.rs index dba70bf117..8307efcb41 100644 --- a/node/src/clone_spec.rs +++ b/node/src/clone_spec.rs @@ -37,6 +37,7 @@ const RPC_POLL_INTERVAL: Duration = Duration::from_secs(2); const GRANDPA_AUTHORITIES_WELL_KNOWN_KEY: &[u8] = b":grandpa_authorities"; /// Execute `build-patched-spec`: sync network state, export raw chainspec, apply clone patch. +/// Sync, export-state, and patch a live chain into a local test chainspec. pub fn run(cmd: &CloneStateCmd, skip_history_backfill: bool) -> sc_cli::Result<()> { let runtime = tokio::runtime::Builder::new_current_thread() .enable_io() @@ -49,6 +50,7 @@ pub fn run(cmd: &CloneStateCmd, skip_history_backfill: bool) -> sc_cli::Result<( .map_err(sc_cli::Error::Application) } +/// Async body of [`run`]: spawn sync node, wait, export raw state, apply patch. async fn async_run(cmd: &CloneStateCmd, skip_history_backfill: bool) -> CloneResult<()> { let validators = selected_validators(cmd); let selected_names = validators @@ -262,6 +264,7 @@ fn export_raw_state( Ok(()) } +/// Snapshot of `system_syncState` + `system_health` used for sync completion. struct SyncStatus { current: u64, highest: u64, @@ -269,6 +272,7 @@ struct SyncStatus { is_syncing: bool, } +/// Query RPC sync/health fields used to decide when the temp node is near head. async fn query_sync_status(rpc_client: &HttpClient) -> CloneResult { let sync = rpc_call(rpc_client, "system_syncState").await?; let health = rpc_call(rpc_client, "system_health").await?; @@ -299,6 +303,7 @@ async fn rpc_call(rpc_client: &HttpClient, method: &str) -> CloneResult { .map_err(Into::into) } +/// Parse a JSON object field as `u64` (number, decimal string, or `0x` hex). fn parse_u64_field(value: &Value, field: &str) -> Option { let field_value = value.get(field)?; @@ -337,6 +342,7 @@ fn database_arg(database: sc_cli::Database) -> &'static str { } } +/// Resolve Alice/Bob/Charlie seeds from CLI flags (defaults to Alice only). fn selected_validators(cmd: &CloneStateCmd) -> Vec<&'static str> { let explicit = cmd.alice || cmd.bob || cmd.charlie; let mut selected = Vec::new(); @@ -374,6 +380,7 @@ fn patch_raw_chainspec_file( Ok(()) } +/// Patch authorities, sudo, session keys, and top-level identity fields on a raw spec. fn patch_raw_spec(spec: &mut Value, validators: &[&'static str]) -> CloneResult<()> { let sudo = validators .first() diff --git a/node/src/command.rs b/node/src/command.rs index fd3a122ec5..735a4f6257 100644 --- a/node/src/command.rs +++ b/node/src/command.rs @@ -1,3 +1,8 @@ +//! CLI dispatch for the Subtensor node: load chain-spec, run subcommands, start Aura/Babe. +//! +//! On missing BabeApi the Babe path falls back to Aura; Aura can signal a handoff +//! back to Babe via `custom_service_signal` when a Babe block appears. + use std::sync::{Arc, atomic::AtomicBool}; use crate::{ @@ -57,7 +62,7 @@ impl SubstrateCli for Cli { } } -// Parse and run command line arguments +/// Parse CLI args and dispatch the selected subcommand or full-node service. pub fn run() -> sc_cli::Result<()> { let cmd = Cli::command(); let arg_matches = cmd.get_matches(); @@ -262,6 +267,9 @@ pub fn run() -> sc_cli::Result<()> { } } +/// Resolve whether history gap-backfill should be skipped for this invocation. +/// +/// Explicit `--history-backfill` wins; otherwise `build-patched-spec` defaults to skip. fn resolve_skip_history_backfill(cli: &Cli, arg_matches: &ArgMatches) -> bool { // We keep a single global `--history-backfill` flag, but `build-patched-spec` should default to // `skip` when the operator didn't set the flag explicitly. This preserves `keep` as the default @@ -276,6 +284,7 @@ fn resolve_skip_history_backfill(cli: &Cli, arg_matches: &ArgMatches) -> bool { matches!(&cli.subcommand, Some(Subcommand::CloneState(_))) } +/// Start a Babe full node, falling back to Aura when the runtime lacks BabeApi. #[allow(clippy::expect_used)] fn start_babe_service( arg_matches: &ArgMatches, @@ -284,7 +293,7 @@ fn start_babe_service( let cli = Cli::from_arg_matches(arg_matches).expect("Bad arg_matches"); let runner = cli.create_runner(&cli.run)?; match runner.run_node_until_exit(|config| async move { - let config = customise_config(arg_matches, config); + let config = apply_node_rpc_defaults(arg_matches, config); service::build_full::( config, cli.eth, @@ -323,6 +332,7 @@ fn start_babe_service( } } +/// Start an Aura full node; restarts as Babe when `custom_service_signal` is set. #[allow(clippy::expect_used)] fn start_aura_service( arg_matches: &ArgMatches, @@ -339,7 +349,7 @@ fn start_aura_service( let custom_service_signal = Arc::new(AtomicBool::new(false)); let custom_service_signal_clone = custom_service_signal.clone(); match runner.run_node_until_exit(|config| async move { - let config = customise_config(arg_matches, config); + let config = apply_node_rpc_defaults(arg_matches, config); service::build_full::( config, cli.eth, @@ -360,8 +370,9 @@ fn start_aura_service( } } +/// Apply Subtensor RPC / heap-page defaults when the operator did not override them. #[allow(clippy::expect_used)] -fn customise_config(arg_matches: &ArgMatches, config: Configuration) -> Configuration { +fn apply_node_rpc_defaults(arg_matches: &ArgMatches, config: Configuration) -> Configuration { let cli = Cli::from_arg_matches(arg_matches).expect("Bad arg_matches"); let mut config = override_default_heap_pages(config, 60_000); @@ -388,7 +399,7 @@ fn customise_config(arg_matches: &ArgMatches, config: Configuration) -> Configur config } -/// Override default heap pages +/// Set WASM executor `default_heap_pages` while preserving the rest of `Configuration`. fn override_default_heap_pages(config: Configuration, pages: u64) -> Configuration { Configuration { impl_name: config.impl_name, diff --git a/node/src/conditional_evm_block_import.rs b/node/src/conditional_evm_block_import.rs index 0a69bdc090..a2baec9611 100644 --- a/node/src/conditional_evm_block_import.rs +++ b/node/src/conditional_evm_block_import.rs @@ -1,8 +1,15 @@ +//! Block import that routes pre-/post-Frontier eras and optional history-gap skip. +//! +//! Blocks before the mainnet Frontier upgrade (#4345557) use the inner import; +//! later blocks go through Frontier. When history backfill is skipped, initial-sync +//! imports clear `create_gap` so Substrate does not schedule gap reconstruction. + use sc_consensus::{BlockCheckParams, BlockImport, BlockImportParams, ImportResult}; use sp_consensus::{BlockOrigin, Error as ConsensusError}; use sp_runtime::traits::{Block as BlockT, Header}; use std::marker::PhantomData; +/// Dual-path block import: native before Frontier activation, Frontier afterward. pub struct ConditionalEVMBlockImport { inner: I, frontier_block_import: F, @@ -34,6 +41,7 @@ where F: BlockImport, F::Error: Into, { + /// Wrap an inner + Frontier import with optional history-gap suppression. pub fn new(inner: I, frontier_block_import: F, skip_history_backfill: bool) -> Self { Self { inner, diff --git a/node/src/consensus/aura_consensus.rs b/node/src/consensus/aura_consensus.rs index 74ec8fea1e..abb44c13c8 100644 --- a/node/src/consensus/aura_consensus.rs +++ b/node/src/consensus/aura_consensus.rs @@ -1,3 +1,8 @@ +//! Aura [`ConsensusMechanism`] implementation, including hybrid import for Babe handoff. +//! +//! Builds a hybrid Aura/Babe import queue so the node can verify the first Babe +//! digests before switching authorship to Babe. + use crate::consensus::hybrid_import_queue::HybridBlockImport; use crate::consensus::{ConsensusMechanism, StartAuthoringParams}; use crate::{ @@ -32,6 +37,7 @@ use stc_shield::InherentDataProvider as ShieldInherentDataProvider; use std::{error::Error, sync::Arc}; use stp_shield::ShieldKeystorePtr; +/// Aura consensus adapter used until the chain produces Babe digests. pub struct AuraConsensus; impl ConsensusMechanism for AuraConsensus { diff --git a/node/src/consensus/babe_consensus.rs b/node/src/consensus/babe_consensus.rs index fad204fb48..dd8446bebe 100644 --- a/node/src/consensus/babe_consensus.rs +++ b/node/src/consensus/babe_consensus.rs @@ -1,3 +1,5 @@ +//! Babe [`ConsensusMechanism`] implementation with Babe RPC and Frontier-aware import. + use crate::consensus::ConsensusMechanism; use crate::consensus::StartAuthoringParams; use crate::{ @@ -35,6 +37,7 @@ use stc_shield::InherentDataProvider as ShieldInherentDataProvider; use std::{error::Error, sync::Arc}; use stp_shield::ShieldKeystorePtr; +/// Babe consensus adapter holding Babe link / worker handles after import-queue setup. pub struct BabeConsensus { babe_link: Option>, babe_worker_handle: Option>, diff --git a/node/src/consensus/consensus_mechanism.rs b/node/src/consensus/consensus_mechanism.rs index 41cb2fb4a8..46207cead0 100644 --- a/node/src/consensus/consensus_mechanism.rs +++ b/node/src/consensus/consensus_mechanism.rs @@ -1,3 +1,5 @@ +//! Trait abstracting Aura vs Babe node wiring (import queue, IDPs, authorship, RPC). + use jsonrpsee::Methods; use node_subtensor_runtime::opaque::Block; use sc_client_api::AuxStore; @@ -30,6 +32,7 @@ use crate::client::FullClient; use crate::service::BIQ; use crate::service::FullSelectChain; +/// Parameters bundled for consensus authorship startup. pub struct StartAuthoringParams { /// The duration of a slot. pub slot_duration: SlotDuration, diff --git a/node/src/consensus/hybrid_import_queue.rs b/node/src/consensus/hybrid_import_queue.rs index 1edb46b974..c6695567ee 100644 --- a/node/src/consensus/hybrid_import_queue.rs +++ b/node/src/consensus/hybrid_import_queue.rs @@ -1,3 +1,8 @@ +//! Import queue that verifies both Aura and Babe digests during consensus migration. +//! +//! Digest inspection (`is_babe_digest`) chooses Babe vs Aura verification so the +//! node can sync across the Aura→Babe boundary without restarting mid-import. + use crate::client::FullClient; use crate::conditional_evm_block_import::ConditionalEVMBlockImport; use crate::service::GrandpaBlockImport; @@ -288,6 +293,7 @@ where )) } +/// True when the block digest carries a Babe engine id (vs Aura-only). fn is_babe_digest(digest: &Digest) -> bool { digest .logs() diff --git a/node/src/consensus/mod.rs b/node/src/consensus/mod.rs index 17c6a9292f..b1f47b1b94 100644 --- a/node/src/consensus/mod.rs +++ b/node/src/consensus/mod.rs @@ -1,3 +1,9 @@ +//! Consensus mechanism adapters for Aura, Babe, and hybrid Aura→Babe import. +//! +//! [`ConsensusMechanism`] lets `service` / `command` stay generic while Aura and +//! Babe supply import queues, inherent providers, authorship, and RPC extras. +//! [`hybrid_import_queue`] verifies either digest style during the migration. + mod aura_consensus; mod babe_consensus; mod consensus_mechanism; diff --git a/node/src/dev_keystore.rs b/node/src/dev_keystore.rs index 6011021bff..536b66dffe 100644 --- a/node/src/dev_keystore.rs +++ b/node/src/dev_keystore.rs @@ -1,3 +1,5 @@ +//! Fixed shield keystore for single-validator manual-seal / local-dev nodes. + use stc_shield::MemoryShieldKeystore; use stp_shield::{Result as TraitResult, ShieldKeystore}; @@ -19,6 +21,7 @@ pub struct DevShieldKeystore { } impl DevShieldKeystore { + /// Capture one ML-KEM pair, roll it to current, then freeze rotation. #[allow(clippy::expect_used)] pub fn new() -> Self { let inner = MemoryShieldKeystore::new(); diff --git a/node/src/ethereum.rs b/node/src/ethereum.rs index 854826f18e..f1a81f167b 100644 --- a/node/src/ethereum.rs +++ b/node/src/ethereum.rs @@ -1,3 +1,9 @@ +//! Frontier / Ethereum-compatibility wiring for the Subtensor node. +//! +//! Owns eth CLI config, Frontier backend partials, mapping-sync background tasks, +//! and the private helpers that merge Eth/Net/Web3/Debug RPC modules. +//! RPC method strings come from Frontier crates — do not rename those selectors. + use crate::rpc::EthDeps; use fc_rpc::{ Debug, DebugApiServer, Eth, EthApiServer, EthConfig, EthDevSigner, EthFilter, @@ -28,9 +34,10 @@ use std::{ use crate::client::{FullBackend, FullClient}; +/// Frontier offchain database backend for Ethereum state indexing. pub type FrontierBackend = fc_db::Backend; -/// Avalailable frontier backend types. +/// Available frontier backend types. #[derive(Debug, Copy, Clone, Default, clap::ValueEnum)] pub enum BackendType { /// Either RocksDb or ParityDb as per inherited from the global backend settings. @@ -89,16 +96,22 @@ pub struct EthConfiguration { pub frontier_sql_backend_cache_size: u64, } +/// Directory under the node base path used for Frontier DB files for this chain. pub fn db_config_dir(config: &Configuration) -> PathBuf { config.base_path.config_dir(config.chain_spec.id()) } +/// In-memory Frontier components shared by Eth RPC (filter pool + fee history). pub struct FrontierPartialComponents { + /// Optional Eth filter pool for `eth_newFilter` / log subscriptions. pub filter_pool: Option, + /// Shared fee-history cache filled by the Frontier maintenance task. pub fee_history_cache: FeeHistoryCache, + /// Cap on fee-history cache entries. pub fee_history_cache_limit: FeeHistoryCacheLimit, } +/// Allocate Frontier filter-pool and fee-history caches from eth CLI config. pub fn new_frontier_partial( config: &EthConfiguration, ) -> Result { @@ -109,6 +122,7 @@ pub fn new_frontier_partial( }) } +/// Spawn Frontier mapping-sync, filter-pool, and fee-history maintenance tasks. #[allow(clippy::too_many_arguments)] pub async fn spawn_frontier_tasks( task_manager: &TaskManager, @@ -193,7 +207,8 @@ pub async fn spawn_frontier_tasks( ); } -fn extend_rpc_aet_api( +/// Merge the core `eth_*` JSON-RPC API into `io`. +fn extend_rpc_eth_api( io: &mut RpcModule<()>, deps: &EthDeps, pending_consensus_data_provider: Option>>, @@ -240,6 +255,7 @@ where Ok(()) } +/// Merge Eth filter RPC (`eth_newFilter`, etc.) when a filter pool is configured. fn extend_rpc_eth_filter( io: &mut RpcModule<()>, deps: &EthDeps, @@ -272,7 +288,7 @@ where Ok(()) } -// Function for EthPubSub merge +/// Merge Eth pubsub subscription RPC handlers. fn extend_rpc_eth_pubsub( io: &mut RpcModule<()>, deps: &EthDeps, @@ -308,6 +324,7 @@ where Ok(()) } +/// Merge `net_*` peer-count RPC helpers. fn extend_rpc_net( io: &mut RpcModule<()>, deps: &EthDeps, @@ -334,6 +351,7 @@ where Ok(()) } +/// Merge `web3_*` client-version helpers. fn extend_rpc_web3( io: &mut RpcModule<()>, deps: &EthDeps, @@ -353,6 +371,7 @@ where Ok(()) } +/// Merge Frontier debug tracing RPC. fn extend_rpc_debug( io: &mut RpcModule<()>, deps: &EthDeps, @@ -380,7 +399,9 @@ where Ok(()) } -/// Extend RpcModule with Eth RPCs +/// Extend `RpcModule` with Frontier Eth / Net / Web3 / Debug RPCs. +/// +/// Does not change Frontier method name strings registered for clients. pub fn create_eth( mut io: RpcModule<()>, deps: EthDeps, @@ -404,7 +425,7 @@ where CIDP: CreateInherentDataProviders + Send + Clone + 'static, EC: EthConfig, { - extend_rpc_aet_api::(&mut io, &deps, pending_consensus_data_provider)?; + extend_rpc_eth_api::(&mut io, &deps, pending_consensus_data_provider)?; extend_rpc_eth_filter::(&mut io, &deps)?; extend_rpc_eth_pubsub::( &mut io, diff --git a/node/src/lib.rs b/node/src/lib.rs index d269fe583d..937154f2a7 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -1,3 +1,8 @@ +//! Library surface for the Subtensor node binary. +//! +//! Exposes chain-spec builders, CLI types, consensus adapters, Frontier/EVM wiring, +//! RPC assembly, and the service factory used by `main` / integration tests. + pub mod chain_spec; pub mod cli; pub mod client; diff --git a/node/src/rpc.rs b/node/src/rpc.rs index e34826462f..3b07f38aed 100644 --- a/node/src/rpc.rs +++ b/node/src/rpc.rs @@ -1,7 +1,7 @@ -//! A collection of node-specific RPC methods. -//! Substrate provides the `sc-rpc` crate, which defines the core RPC layer -//! used by Substrate nodes. This file extends those RPC definitions with -//! capabilities that are specific to this project's runtime configuration. +//! Assemble Subtensor full-node JSON-RPC: system, payment, custom, swap, manual-seal, Eth. +//! +//! Substrate `sc-rpc` supplies the core layer; this module merges runtime-specific +//! APIs and Frontier eth deps. Client-facing RPC method name strings must stay stable. #![warn(missing_docs)] @@ -95,7 +95,9 @@ pub struct FullDeps { pub eth: EthDeps, } -/// Instantiate all full RPC extensions. +/// Instantiate all full RPC extensions (Subtensor custom + Frontier eth + optional manual seal). +/// +/// Does not alter registered RPC method name strings. pub fn create_full( deps: FullDeps, subscription_task_executor: SubscriptionTaskExecutor, @@ -124,7 +126,7 @@ where use pallet_transaction_payment_rpc::{TransactionPayment, TransactionPaymentApiServer}; use sc_consensus_manual_seal::rpc::{ManualSeal, ManualSealApiServer}; use substrate_frame_rpc_system::{System, SystemApiServer}; - use subtensor_custom_rpc::{SubtensorCustom, SubtensorCustomApiServer}; + use subtensor_custom_rpc::{SubtensorCustomRpc, SubtensorCustomRpcApiServer}; let mut module = RpcModule::new(()); let FullDeps { @@ -135,7 +137,7 @@ where } = deps; // Custom RPC methods for Paratensor - module.merge(SubtensorCustom::new(client.clone()).into_rpc())?; + module.merge(SubtensorCustomRpc::new(client.clone()).into_rpc())?; // Swap RPC module.merge(Swap::new(client.clone()).into_rpc())?; diff --git a/node/src/service.rs b/node/src/service.rs index 4210a6fa20..22cb68dac4 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -1,4 +1,7 @@ -//! Service and ServiceFactory implementation. Specialized wrapper over substrate service. +//! Full-node service factory: partial components, network, Frontier, consensus authorship. +//! +//! Generic over [`ConsensusMechanism`] so Aura and Babe share the same service path. +//! Also hosts manual-seal authorship and Aura→Babe keystore key duplication. mod grandpa_warp_sync; @@ -46,10 +49,13 @@ const LOG_TARGET: &str = "node-service"; /// imported and generated. const GRANDPA_JUSTIFICATION_PERIOD: u32 = 512; +/// Longest-chain selection used by Grandpa and authorship. pub type FullSelectChain = sc_consensus::LongestChain; +/// Grandpa block import wired to the full client/backend. pub type GrandpaBlockImport = sc_consensus_grandpa::GrandpaBlockImport; type GrandpaLinkHalf = sc_consensus_grandpa::LinkHalf; +/// Closure type that builds the consensus import queue during [`new_partial`]. #[allow(clippy::upper_case_acronyms)] pub type BIQ<'a> = Box< dyn FnOnce( @@ -65,6 +71,7 @@ pub type BIQ<'a> = Box< + 'a, >; +/// Build partial client/backend/import-queue components shared by full node and chain-ops. #[allow(clippy::expect_used)] pub fn new_partial( config: &Configuration, @@ -107,7 +114,7 @@ pub fn new_partial( )?; // Prepare keystore for authoring Babe blocks. - copy_keys( + copy_keystore_keys_by_type( &keystore_container.local_keystore(), key_types::AURA, key_types::BABE, @@ -256,7 +263,7 @@ pub fn build_manual_seal_import_queue( )) } -/// Builds a new service for a full client. +/// Build and start a full client service for network backend `NB` and consensus `CM`. #[allow(clippy::expect_used)] pub async fn new_full( mut config: Configuration, @@ -656,6 +663,7 @@ where Ok(task_manager) } +/// Entry point that picks libp2p vs litep2p and starts a full node for consensus `CM`. pub async fn build_full( config: Configuration, eth_config: EthConfiguration, @@ -687,6 +695,7 @@ pub async fn build_full( } } +/// Lightweight client/import-queue set for CLI chain ops (export, import, revert, etc.). pub fn new_chain_ops( config: &mut Configuration, eth_config: &EthConfiguration, @@ -726,6 +735,7 @@ type SealStream = std::pin::Pin< >, >; +/// Spawn manual / instant / interval seal authorship for `--sealing` modes. #[allow(clippy::too_many_arguments)] fn run_manual_seal_authorship( sealing: Sealing, @@ -854,7 +864,7 @@ fn run_manual_seal_authorship( Ok(()) } -/// Copy `from_key_type` keys to also exist as `to_key_type`. +/// Copy keystore key phrases from `from_key_type` so they also exist as `to_key_type`. /// /// Used for the Aura to Babe migration, where Aura validators need their keystore to copy their /// Aura keys over to Babe. This works because Aura and Babe keys use identical crypto. @@ -862,7 +872,7 @@ fn run_manual_seal_authorship( /// While not required to retain beyond the initial Aura to Babe migration, it is nice to leave it /// so the node always retains the ability to perform Aura to Babe migrations in the future, in case /// there is a requirement to do something like regenesis testnet. -fn copy_keys( +fn copy_keystore_keys_by_type( keystore: &LocalKeystore, from_key_type: KeyTypeId, to_key_type: KeyTypeId, diff --git a/node/src/service/grandpa_warp_sync.rs b/node/src/service/grandpa_warp_sync.rs index 79b5cc05a1..08296f1f87 100644 --- a/node/src/service/grandpa_warp_sync.rs +++ b/node/src/service/grandpa_warp_sync.rs @@ -1,3 +1,8 @@ +//! GRANDPA warp-sync hard-fork / initial-set-id patches for known genesis hashes. +//! +//! Testnet uses authority-set checkpoints; other chain types patch an initial set id +//! so warp sync can resume across historical Grandpa set changes. + use node_subtensor_runtime::opaque::Block; use sc_chain_spec::ChainType; use sc_consensus_grandpa::{AuthoritySetHardFork, warp_proof::HardForks}; @@ -8,11 +13,15 @@ const TESTNET_GENESIS: H256 = H256(hex_literal::hex!( "8f9cf856bf558a14440e75569c9e58594757048d7b3a84b5d25f6bd978263105" )); +/// Warp-sync configuration chosen from genesis hash + chain type. pub(super) enum Config { + /// Hard-forked authority checkpoints for the known testnet genesis. TestnetCheckpoints(Vec>), + /// Initial Grandpa set id override for non-testnet chains. InitialSetId(u64), } +/// Select warp-sync hard-fork config for this chain genesis / type. pub(super) fn config(genesis_hash: H256, chain_type: ChainType) -> Config { if genesis_hash == TESTNET_GENESIS { Config::TestnetCheckpoints(testnet_checkpoints()) @@ -46,6 +55,7 @@ impl Config { } } +/// Authority list at the testnet hard-fork checkpoint used for warp proofs. #[allow(clippy::expect_used)] fn testnet_authorities() -> AuthorityList { [ diff --git a/node/tests/chain_spec.rs b/node/tests/chain_spec.rs index 42665c476b..599f879e93 100644 --- a/node/tests/chain_spec.rs +++ b/node/tests/chain_spec.rs @@ -1,3 +1,5 @@ +//! Integration tests for `chain_spec` seed / authority-key helpers. + use sp_core::sr25519; // use sp_consensus_aura::sr25519::AuthorityId as AuraId; // use sp_consensus_grandpa::AuthorityId as GrandpaId; @@ -5,7 +7,7 @@ use sp_core::sr25519; use node_subtensor::chain_spec::*; #[test] -fn test_get_from_seed() { +fn get_from_seed_returns_expected_ss58() { let seed = "WoOt"; let pare = get_from_seed::(seed); let expected = "5Gj3QEiZaFJPFK1yN4Lkj6FLM4V7GEBCewVBVniuvZ75S2Fd"; @@ -14,13 +16,13 @@ fn test_get_from_seed() { #[test] #[should_panic(expected = "static values are valid; qed: InvalidFormat")] -fn test_get_from_seed_panics() { +fn get_from_seed_panics_on_empty_seed() { let bad_seed = ""; get_from_seed::(bad_seed); } #[test] -fn test_get_account_id_from_seed() { +fn get_account_id_from_seed_returns_expected_ss58() { let seed = "WoOt"; let account_id = get_account_id_from_seed::(seed); let expected = "5Gj3QEiZaFJPFK1yN4Lkj6FLM4V7GEBCewVBVniuvZ75S2Fd"; @@ -29,13 +31,13 @@ fn test_get_account_id_from_seed() { #[test] #[should_panic(expected = "static values are valid; qed: InvalidFormat")] -fn test_get_account_id_from_seed_panics() { +fn get_account_id_from_seed_panics_on_empty_seed() { let bad_seed = ""; get_account_id_from_seed::(bad_seed); } #[test] -fn test_authority_keys_from_seed() { +fn authority_keys_from_seed_returns_aura_and_grandpa() { let seed = "WoOt"; let (aura_id, grandpa_id) = authority_keys_from_seed(seed); @@ -48,7 +50,7 @@ fn test_authority_keys_from_seed() { #[test] #[should_panic(expected = "static values are valid; qed: InvalidFormat")] -fn test_authority_keys_from_seed_panics() { +fn authority_keys_from_seed_panics_on_empty_seed() { let bad_seed = ""; authority_keys_from_seed(bad_seed); } diff --git a/pallets/admin-utils/src/benchmarking.rs b/pallets/admin-utils/src/benchmarking.rs index 9211e289c5..af2a187ebb 100644 --- a/pallets/admin-utils/src/benchmarking.rs +++ b/pallets/admin-utils/src/benchmarking.rs @@ -1,4 +1,7 @@ -//! Benchmarking setup +//! Runtime benchmarks for `pallet-admin-utils` dispatchables. +//! +//! Each benchmark clears the admin freeze window so root calls are not blocked by the +//! default freeze-window setup used elsewhere in tests. #![cfg(feature = "runtime-benchmarks")] #![allow(clippy::arithmetic_side_effects)] #![allow(clippy::unwrap_used)] diff --git a/pallets/admin-utils/src/consensus_authority_interfaces.rs b/pallets/admin-utils/src/consensus_authority_interfaces.rs new file mode 100644 index 0000000000..3c529e6f26 --- /dev/null +++ b/pallets/admin-utils/src/consensus_authority_interfaces.rs @@ -0,0 +1,45 @@ +//! Runtime bridge traits for swapping Aura authorities and scheduling GRANDPA changes. +//! +//! `pallet-admin-utils` does not depend on the Aura/GRANDPA pallets directly. The runtime +//! implements these traits so [`crate::Pallet::swap_authorities`] and +//! [`crate::Pallet::schedule_grandpa_change`] can update consensus authority sets. + +use frame_system::pallet_prelude::BlockNumberFor; +use sp_consensus_grandpa::AuthorityList; +use sp_runtime::{BoundedVec, DispatchResult}; + +/// Hook used by [`crate::Pallet::swap_authorities`] to replace the Aura authority set. +pub trait AuraInterface { + /// Replace the current Aura authorities with `new`. + fn change_authorities(new: BoundedVec); +} + +impl AuraInterface for () { + fn change_authorities(_: BoundedVec) {} +} + +/// Hook used by [`crate::Pallet::schedule_grandpa_change`] to queue a GRANDPA authority change. +pub trait GrandpaInterface +where + Runtime: frame_system::Config, +{ + /// Schedule a GRANDPA authority set change after `in_blocks`, optionally forced. + fn schedule_change( + next_authorities: AuthorityList, + in_blocks: BlockNumberFor, + forced: Option>, + ) -> DispatchResult; +} + +impl GrandpaInterface for () +where + R: frame_system::Config, +{ + fn schedule_change( + _next_authorities: AuthorityList, + _in_blocks: BlockNumberFor, + _forced: Option>, + ) -> DispatchResult { + Ok(()) + } +} diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index 02b2f60ad9..ae65f95bcf 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -1,17 +1,22 @@ #![cfg_attr(not(feature = "std"), no_std)] +//! Root and subnet-owner admin extrinsics for Bittensor hyperparameters, EVM precompiles, +//! and consensus authority updates. +//! +//! Most dispatchables authorize the origin (root and/or subnet owner, often behind the +//! admin freeze window and owner hyperparam rate limits) then write into `pallet-subtensor` +//! storage. This pallet also owns [`PrecompileEnable`] and the Aura/GRANDPA admin bridges +//! in [`consensus_authority_interfaces`]. // extern crate alloc; -use frame_system::pallet_prelude::BlockNumberFor; pub use pallet::*; -// - we could replace it with Vec<(AuthorityId, u64)>, but we would need -// `sp_consensus_grandpa` for `AuthorityId` anyway -// - we could use a type parameter for `AuthorityId`, but there is -// no sense for this as GRANDPA's `AuthorityId` is not a parameter -- it's always the same +// GRANDPA's AuthorityId is not a Config parameter — always the same concrete type. use sp_consensus_grandpa::AuthorityList; -use sp_runtime::{DispatchResult, RuntimeAppPublic, Vec, traits::Member}; +use sp_runtime::{RuntimeAppPublic, Vec, traits::Member}; mod benchmarking; +mod consensus_authority_interfaces; +pub use consensus_authority_interfaces::{AuraInterface, GrandpaInterface}; pub mod weights; pub use weights::WeightInfo; @@ -37,38 +42,38 @@ pub mod pallet { use substrate_fixed::types::{I64F64, I96F32, U64F64}; use subtensor_runtime_common::{MechId, NetUid, TaoBalance}; - /// The main data structure of the module. + /// Admin-utils pallet entry point: sudo/owner hyperparam setters and consensus bridges. #[pallet::pallet] #[pallet::without_storage_info] pub struct Pallet(_); - /// Configure the pallet by specifying the parameters and types on which it depends. + /// Runtime dependencies for admin-utils (subtensor storage, EVM chain id, Aura/GRANDPA hooks). #[pallet::config] pub trait Config: frame_system::Config + pallet_subtensor::pallet::Config + pallet_evm_chain_id::pallet::Config { - /// Implementation of the AuraInterface + /// Aura authority-set bridge used by [`Pallet::swap_authorities`]. type Aura: crate::AuraInterface<::AuthorityId, Self::MaxAuthorities>; - /// Implementation of [`GrandpaInterface`] + /// GRANDPA authority-change bridge used by [`Pallet::schedule_grandpa_change`]. type Grandpa: crate::GrandpaInterface; - /// The identifier type for an authority. + /// Authority public-key type shared with the Aura bridge. type AuthorityId: Member + Parameter + RuntimeAppPublic + MaybeSerializeDeserialize + MaxEncodedLen; - /// The maximum number of authorities that the pallet can hold. + /// Max length of the Aura authority list accepted by [`Pallet::swap_authorities`]. type MaxAuthorities: Get; - /// Unit of assets + /// Balance type used by admin paths that touch account balances. type Balance: Balance; - /// Weight information for extrinsics in this pallet. + /// Extrinsic weight functions for this pallet's calls. type WeightInfo: WeightInfo; } @@ -206,13 +211,16 @@ pub mod pallet { } #[pallet::type_value] - /// Default value for precompile enable + /// Default for [`PrecompileEnable`]: each precompile starts enabled. pub fn DefaultPrecompileEnabled() -> bool { true } #[pallet::storage] - /// Map PrecompileEnum --> enabled + /// Per-[`PrecompileEnum`] enable flag consulted by EVM precompile dispatch. + /// + /// Toggled by [`Pallet::sudo_toggle_evm_precompile`]; missing keys query as + /// [`DefaultPrecompileEnabled`] (`true`). pub type PrecompileEnable = StorageMap< _, Blake2_128Concat, @@ -222,7 +230,7 @@ pub mod pallet { DefaultPrecompileEnabled, >; - /// Dispatchable functions allows users to interact with the pallet and invoke state changes. + /// Root/owner dispatchables that set subtensor hyperparameters and admin toggles. #[pallet::call] impl Pallet { #![deny(clippy::expect_used)] @@ -280,7 +288,7 @@ pub mod pallet { netuid: NetUid, serving_rate_limit: u64, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::ServingRateLimit.into()], @@ -288,7 +296,7 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; pallet_subtensor::Pallet::::set_serving_rate_limit(netuid, serving_rate_limit); log::debug!("ServingRateLimitSet( serving_rate_limit: {serving_rate_limit:?} ) "); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::ServingRateLimit.into()], @@ -310,7 +318,7 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); pallet_subtensor::Pallet::::set_min_difficulty(netuid, min_difficulty); @@ -330,7 +338,7 @@ pub mod pallet { netuid: NetUid, max_difficulty: u64, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::MaxDifficulty.into()], @@ -338,14 +346,14 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); pallet_subtensor::Pallet::::set_max_difficulty(netuid, max_difficulty); log::debug!( "MaxDifficultySet( netuid: {netuid:?} max_difficulty: {max_difficulty:?} ) " ); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::MaxDifficulty.into()], @@ -363,7 +371,7 @@ pub mod pallet { netuid: NetUid, weights_version_key: u64, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin.clone(), netuid, &[TransactionType::SetWeightsVersionKey], @@ -371,11 +379,11 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[TransactionType::SetWeightsVersionKey], @@ -401,7 +409,7 @@ pub mod pallet { ensure_root(origin)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); pallet_subtensor::Pallet::::set_weights_set_rate_limit( @@ -427,7 +435,7 @@ pub mod pallet { ensure_root(origin)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); pallet_subtensor::Pallet::::set_adjustment_interval(netuid, adjustment_interval); @@ -447,7 +455,7 @@ pub mod pallet { netuid: NetUid, adjustment_alpha: u64, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::AdjustmentAlpha.into()], @@ -455,11 +463,11 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); pallet_subtensor::Pallet::::set_adjustment_alpha(netuid, adjustment_alpha); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::AdjustmentAlpha.into()], @@ -478,19 +486,19 @@ pub mod pallet { netuid: NetUid, immunity_period: u16, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::ImmunityPeriod.into()], )?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); pallet_subtensor::Pallet::::set_immunity_period(netuid, immunity_period); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::ImmunityPeriod.into()], @@ -511,7 +519,7 @@ pub mod pallet { netuid: NetUid, min_allowed_weights: u16, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::MinAllowedWeights.into()], @@ -519,14 +527,14 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); pallet_subtensor::Pallet::::set_min_allowed_weights(netuid, min_allowed_weights); log::debug!( "MinAllowedWeightSet( netuid: {netuid:?} min_allowed_weights: {min_allowed_weights:?} ) " ); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::MinAllowedWeights.into()], @@ -544,14 +552,14 @@ pub mod pallet { netuid: NetUid, max_allowed_uids: u16, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::MaxAllowedUids.into()], )?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); ensure!( @@ -573,7 +581,7 @@ pub mod pallet { mechanism_count.into(), )?; pallet_subtensor::Pallet::::set_max_allowed_uids(netuid, max_allowed_uids); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::MaxAllowedUids.into()], @@ -592,7 +600,7 @@ pub mod pallet { pub fn sudo_set_kappa(origin: OriginFor, netuid: NetUid, kappa: u16) -> DispatchResult { ensure_root(origin)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); pallet_subtensor::Pallet::::set_kappa(netuid, kappa); @@ -606,7 +614,7 @@ pub mod pallet { #[pallet::call_index(17)] #[pallet::weight(::WeightInfo::sudo_set_rho())] pub fn sudo_set_rho(origin: OriginFor, netuid: NetUid, rho: u16) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::Rho.into()], @@ -614,12 +622,12 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); pallet_subtensor::Pallet::::set_rho(netuid, rho); log::debug!("RhoSet( netuid: {netuid:?} rho: {rho:?} ) "); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::Rho.into()], @@ -640,7 +648,7 @@ pub mod pallet { netuid: NetUid, activity_cutoff: u16, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::ActivityCutoff.into()], @@ -648,7 +656,7 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); @@ -661,7 +669,7 @@ pub mod pallet { log::debug!( "ActivityCutoffSet( netuid: {netuid:?} activity_cutoff: {activity_cutoff:?} ) " ); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::ActivityCutoff.into()], @@ -742,7 +750,7 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); pallet_subtensor::Pallet::::set_target_registrations_per_interval( @@ -765,14 +773,14 @@ pub mod pallet { netuid: NetUid, min_burn: TaoBalance, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::MinBurn.into()], )?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); ensure!( @@ -786,7 +794,7 @@ pub mod pallet { ); pallet_subtensor::Pallet::::set_min_burn(netuid, min_burn); log::debug!("MinBurnSet( netuid: {netuid:?} min_burn: {min_burn:?} ) "); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::MinBurn.into()], @@ -804,14 +812,14 @@ pub mod pallet { netuid: NetUid, max_burn: TaoBalance, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::MaxBurn.into()], )?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); ensure!( @@ -825,7 +833,7 @@ pub mod pallet { ); pallet_subtensor::Pallet::::set_max_burn(netuid, max_burn); log::debug!("MaxBurnSet( netuid: {netuid:?} max_burn: {max_burn:?} ) "); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::MaxBurn.into()], @@ -846,7 +854,7 @@ pub mod pallet { ensure_root(origin)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); pallet_subtensor::Pallet::::set_difficulty(netuid, difficulty); @@ -867,7 +875,7 @@ pub mod pallet { ensure_root(origin)?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); ensure!( @@ -896,7 +904,7 @@ pub mod pallet { netuid: NetUid, bonds_moving_average: u64, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::BondsMovingAverage.into()], @@ -910,14 +918,14 @@ pub mod pallet { } ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); pallet_subtensor::Pallet::::set_bonds_moving_average(netuid, bonds_moving_average); log::debug!( "BondsMovingAverageSet( netuid: {netuid:?} bonds_moving_average: {bonds_moving_average:?} ) " ); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::BondsMovingAverage.into()], @@ -935,7 +943,7 @@ pub mod pallet { netuid: NetUid, bonds_penalty: u16, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::BondsPenalty.into()], @@ -943,12 +951,12 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); pallet_subtensor::Pallet::::set_bonds_penalty(netuid, bonds_penalty); log::debug!("BondsPenalty( netuid: {netuid:?} bonds_penalty: {bonds_penalty:?} ) "); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::BondsPenalty.into()], @@ -970,7 +978,7 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); pallet_subtensor::Pallet::::set_max_registrations_per_block( @@ -1123,7 +1131,7 @@ pub mod pallet { ) -> DispatchResult { ensure_root(origin)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); pallet_subtensor::Pallet::::set_rao_recycled(netuid, rao_recycled); @@ -1202,7 +1210,7 @@ pub mod pallet { netuid: NetUid, take: PerU16, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::MinChildkeyTake.into()], @@ -1210,7 +1218,7 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); ensure!( @@ -1220,7 +1228,7 @@ pub mod pallet { ); pallet_subtensor::Pallet::::set_min_childkey_take_for_subnet(netuid, take); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::MinChildkeyTake.into()], @@ -1241,7 +1249,7 @@ pub mod pallet { netuid: NetUid, enabled: bool, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::CommitRevealEnabled.into()], @@ -1249,13 +1257,13 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); pallet_subtensor::Pallet::::set_commit_reveal_weights_enabled(netuid, enabled); log::debug!("ToggleSetWeightsCommitReveal( netuid: {netuid:?} ) "); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::CommitRevealEnabled.into()], @@ -1279,7 +1287,7 @@ pub mod pallet { netuid: NetUid, enabled: bool, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::LiquidAlphaEnabled.into()], @@ -1287,7 +1295,7 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; pallet_subtensor::Pallet::::set_liquid_alpha_enabled(netuid, enabled); log::debug!("LiquidAlphaEnableToggled( netuid: {netuid:?}, Enabled: {enabled:?} ) "); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::LiquidAlphaEnabled.into()], @@ -1304,7 +1312,7 @@ pub mod pallet { alpha_low: u16, alpha_high: u16, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin.clone(), netuid, &[Hyperparameter::AlphaValues.into()], @@ -1314,7 +1322,7 @@ pub mod pallet { origin, netuid, alpha_low, alpha_high, ); if res.is_ok() { - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::AlphaValues.into()], @@ -1378,7 +1386,7 @@ pub mod pallet { netuid: NetUid, interval: u64, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::WeightCommitInterval.into()], @@ -1386,14 +1394,14 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); log::debug!("SetWeightCommitInterval( netuid: {netuid:?}, interval: {interval:?} ) "); pallet_subtensor::Pallet::::set_reveal_period(netuid, interval)?; - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::WeightCommitInterval.into()], @@ -1467,7 +1475,7 @@ pub mod pallet { netuid: NetUid, toggle: bool, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::TransferEnabled.into()], @@ -1475,7 +1483,7 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; let res = pallet_subtensor::Pallet::::toggle_transfer(netuid, toggle); if res.is_ok() { - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::TransferEnabled.into()], @@ -1500,7 +1508,7 @@ pub mod pallet { netuid: NetUid, recycle_or_burn: pallet_subtensor::RecycleOrBurnEnum, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::RecycleOrBurn.into()], @@ -1508,7 +1516,7 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; pallet_subtensor::Pallet::::set_recycle_or_burn(netuid, recycle_or_burn); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::RecycleOrBurn.into()], @@ -1629,7 +1637,7 @@ pub mod pallet { netuid: NetUid, steepness: i16, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin.clone(), netuid, &[Hyperparameter::AlphaSigmoidSteepness.into()], @@ -1637,7 +1645,7 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); @@ -1650,7 +1658,7 @@ pub mod pallet { pallet_subtensor::Pallet::::set_alpha_sigmoid_steepness(netuid, steepness); log::debug!("AlphaSigmoidSteepnessSet( netuid: {netuid:?}, steepness: {steepness:?} )"); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::AlphaSigmoidSteepness.into()], @@ -1674,7 +1682,7 @@ pub mod pallet { netuid: NetUid, enabled: bool, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::Yuma3Enabled.into()], @@ -1684,7 +1692,7 @@ pub mod pallet { Self::deposit_event(Event::Yuma3EnableToggled { netuid, enabled }); log::debug!("Yuma3EnableToggled( netuid: {netuid:?}, Enabled: {enabled:?} ) "); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::Yuma3Enabled.into()], @@ -1708,7 +1716,7 @@ pub mod pallet { netuid: NetUid, enabled: bool, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::BondsResetEnabled.into()], @@ -1718,7 +1726,7 @@ pub mod pallet { Self::deposit_event(Event::BondsResetToggled { netuid, enabled }); log::debug!("BondsResetToggled( netuid: {netuid:?} bonds_reset: {enabled:?} ) "); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::BondsResetEnabled.into()], @@ -1815,14 +1823,14 @@ pub mod pallet { netuid: NetUid, immune_neurons: u16, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::ImmuneNeuronLimit.into()], )?; pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; pallet_subtensor::Pallet::::set_owner_immune_neuron_limit(netuid, immune_neurons)?; - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::ImmuneNeuronLimit.into()], @@ -1875,7 +1883,7 @@ pub mod pallet { netuid: NetUid, mechanism_count: MechId, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[TransactionType::MechanismCountUpdate], @@ -1884,7 +1892,7 @@ pub mod pallet { pallet_subtensor::Pallet::::do_set_mechanism_count(netuid, mechanism_count)?; - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[TransactionType::MechanismCountUpdate], @@ -1900,7 +1908,7 @@ pub mod pallet { netuid: NetUid, maybe_split: Option>, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[TransactionType::MechanismEmission], @@ -1909,7 +1917,7 @@ pub mod pallet { pallet_subtensor::Pallet::::do_set_emission_split(netuid, maybe_split)?; - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[TransactionType::MechanismEmission], @@ -1929,7 +1937,7 @@ pub mod pallet { netuid: NetUid, max_n: u16, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin.clone(), netuid, &[TransactionType::MaxUidsTrimming], @@ -1938,7 +1946,7 @@ pub mod pallet { pallet_subtensor::Pallet::::trim_to_max_allowed_uids(netuid, max_n)?; - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[TransactionType::MaxUidsTrimming], @@ -1959,7 +1967,7 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); ensure!( @@ -2111,7 +2119,7 @@ pub mod pallet { netuid: NetUid, burn_half_life: u16, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::BurnHalfLife.into()], @@ -2119,7 +2127,7 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); ensure!(!netuid.is_root(), Error::::NotPermittedOnRootSubnet); @@ -2135,7 +2143,7 @@ pub mod pallet { burn_half_life, }); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::BurnHalfLife.into()], @@ -2153,7 +2161,7 @@ pub mod pallet { netuid: NetUid, burn_increase_mult: U64F64, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::BurnIncreaseMult.into()], @@ -2161,7 +2169,7 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); ensure!(!netuid.is_root(), Error::::NotPermittedOnRootSubnet); @@ -2179,7 +2187,7 @@ pub mod pallet { burn_increase_mult, }); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::BurnIncreaseMult.into()], @@ -2201,7 +2209,7 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); ensure!(!netuid.is_root(), Error::::NotPermittedOnRootSubnet); @@ -2225,7 +2233,7 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); ensure!(!netuid.is_root(), Error::::NotPermittedOnRootSubnet); @@ -2252,7 +2260,7 @@ pub mod pallet { ensure_root(origin)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); ensure!(!netuid.is_root(), Error::::NotPermittedOnRootSubnet); @@ -2277,7 +2285,7 @@ pub mod pallet { netuid: NetUid, lock_share: u16, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::CollateralLockShare.into()], @@ -2285,7 +2293,7 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); ensure!(!netuid.is_root(), Error::::NotPermittedOnRootSubnet); @@ -2298,7 +2306,7 @@ pub mod pallet { Self::deposit_event(Event::CollateralLockShareSet { netuid, lock_share }); log::debug!("CollateralLockShareSet( netuid: {netuid:?}, lock_share: {lock_share:?} )"); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::CollateralLockShare.into()], @@ -2320,7 +2328,7 @@ pub mod pallet { netuid: NetUid, drain_ratio: U64F64, ) -> DispatchResult { - let maybe_owner = pallet_subtensor::Pallet::::ensure_sn_owner_or_root_with_limits( + let maybe_owner = pallet_subtensor::Pallet::::ensure_subnet_owner_or_root_with_limits( origin, netuid, &[Hyperparameter::CollateralDrainRatio.into()], @@ -2328,7 +2336,7 @@ pub mod pallet { pallet_subtensor::Pallet::::ensure_admin_window_open(netuid)?; ensure!( - pallet_subtensor::Pallet::::if_subnet_exist(netuid), + pallet_subtensor::Pallet::::subnet_exists(netuid), Error::::SubnetDoesNotExist ); ensure!(!netuid.is_root(), Error::::NotPermittedOnRootSubnet); @@ -2347,7 +2355,7 @@ pub mod pallet { "CollateralDrainRatioSet( netuid: {netuid:?}, drain_ratio: {drain_ratio:?} )" ); - pallet_subtensor::Pallet::::record_owner_rl( + pallet_subtensor::Pallet::::record_owner_rate_limits( maybe_owner, netuid, &[Hyperparameter::CollateralDrainRatio.into()], @@ -2375,38 +2383,3 @@ pub mod pallet { impl sp_runtime::BoundToRuntimeAppPublic for Pallet { type Public = ::AuthorityId; } - -// Interfaces to interact with other pallets -use sp_runtime::BoundedVec; - -pub trait AuraInterface { - fn change_authorities(new: BoundedVec); -} - -impl AuraInterface for () { - fn change_authorities(_: BoundedVec) {} -} - -pub trait GrandpaInterface -where - Runtime: frame_system::Config, -{ - fn schedule_change( - next_authorities: AuthorityList, - in_blocks: BlockNumberFor, - forced: Option>, - ) -> DispatchResult; -} - -impl GrandpaInterface for () -where - R: frame_system::Config, -{ - fn schedule_change( - _next_authorities: AuthorityList, - _in_blocks: BlockNumberFor, - _forced: Option>, - ) -> DispatchResult { - Ok(()) - } -} diff --git a/pallets/admin-utils/src/tests/admin_windows_rate_limits.rs b/pallets/admin-utils/src/tests/admin_windows_rate_limits.rs new file mode 100644 index 0000000000..47a1848a99 --- /dev/null +++ b/pallets/admin-utils/src/tests/admin_windows_rate_limits.rs @@ -0,0 +1,357 @@ +//! Admin freeze window, owner hyperparam rate limits, and start-call delay. +#![allow( + clippy::unwrap_used, + clippy::expect_used, + clippy::indexing_slicing, + unused_imports +)] + +use super::prelude::*; + +#[test] +fn test_sudo_set_admin_freeze_window_and_rate() { + new_test_ext().execute_with(|| { + // Non-root fails + assert_eq!( + AdminUtils::sudo_set_admin_freeze_window( + <::RuntimeOrigin>::signed(U256::from(1)), + 7 + ), + Err(DispatchError::BadOrigin) + ); + // Root succeeds + assert_ok!(AdminUtils::sudo_set_admin_freeze_window( + <::RuntimeOrigin>::root(), + 7 + )); + assert_eq!(pallet_subtensor::AdminFreezeWindow::::get(), 7); + + // Owner hyperparam tempos setter + assert_eq!( + AdminUtils::sudo_set_owner_hparam_rate_limit( + <::RuntimeOrigin>::signed(U256::from(1)), + 5 + ), + Err(DispatchError::BadOrigin) + ); + assert_ok!(AdminUtils::sudo_set_owner_hparam_rate_limit( + <::RuntimeOrigin>::root(), + 5 + )); + assert_eq!(pallet_subtensor::OwnerHyperparamRateLimit::::get(), 5); + }); +} + +#[test] +fn test_freeze_window_blocks_root_and_owner() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let tempo: u16 = 10; + // Create subnet with tempo 10 + add_network(netuid, tempo); + // Set freeze window to 3 blocks + assert_ok!(AdminUtils::sudo_set_admin_freeze_window( + <::RuntimeOrigin>::root(), + 3 + )); + // Pin the state-based scheduler so the next auto-epoch lands at + // `LastEpochBlock + tempo`. Freeze window covers blocks (next_auto - 3, next_auto]. + pallet_subtensor::LastEpochBlock::::insert(netuid, 0); + let next_auto = tempo as u64; + // Advance to a block inside the freeze window (remaining < 3). + run_to_block(next_auto - 2); + + // Root should be blocked during freeze window + assert_noop!( + AdminUtils::sudo_set_min_burn( + <::RuntimeOrigin>::root(), + netuid, + 123.into() + ), + SubtensorError::::AdminActionProhibitedDuringWeightsWindow + ); + + // Owner should be blocked during freeze window as well + // Set owner + let owner: U256 = U256::from(9); + SubnetOwner::::insert(netuid, owner); + assert_noop!( + AdminUtils::sudo_set_commit_reveal_weights_interval( + <::RuntimeOrigin>::signed(owner), + netuid, + 77 + ), + SubtensorError::::AdminActionProhibitedDuringWeightsWindow + ); + }); +} + +#[test] +fn test_owner_hyperparam_update_rate_limit_enforced() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 10); + // Set owner + let owner: U256 = U256::from(5); + SubnetOwner::::insert(netuid, owner); + + // Set tempo to 1 so owner hyperparam RL = 2 tempos = 2 blocks + SubtensorModule::set_tempo_unchecked(netuid, 1); + // Disable admin freeze window to avoid blocking on small tempo + assert_ok!(AdminUtils::sudo_set_admin_freeze_window( + <::RuntimeOrigin>::root(), + 0 + )); + + // First update succeeds + assert_ok!(AdminUtils::sudo_set_commit_reveal_weights_interval( + <::RuntimeOrigin>::signed(owner), + netuid, + 11 + )); + // Immediate second update fails due to TxRateLimitExceeded + assert_noop!( + AdminUtils::sudo_set_commit_reveal_weights_interval( + <::RuntimeOrigin>::signed(owner), + netuid, + 12 + ), + SubtensorError::::TxRateLimitExceeded + ); + + // Advance less than limit still fails + run_to_block(SubtensorModule::get_current_block_as_u64() + 1); + assert_noop!( + AdminUtils::sudo_set_commit_reveal_weights_interval( + <::RuntimeOrigin>::signed(owner), + netuid, + 13 + ), + SubtensorError::::TxRateLimitExceeded + ); + + // Advance one more block to pass the limit; should succeed + run_to_block(SubtensorModule::get_current_block_as_u64() + 1); + assert_ok!(AdminUtils::sudo_set_commit_reveal_weights_interval( + <::RuntimeOrigin>::signed(owner), + netuid, + 14 + )); + }); +} + +// Verifies owner hyperparameters are rate-limited independently per parameter. +// Setting one hyperparameter should not block setting a different hyperparameter +// during the same rate-limit window, but it should still block itself. +#[test] +fn test_owner_hyperparam_rate_limit_independent_per_param() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(7); + add_network(netuid, 10); + + // Set subnet owner + let owner: U256 = U256::from(123); + SubnetOwner::::insert(netuid, owner); + + // Use small tempo to make RL short and deterministic (2 blocks when tempo=1) + SubtensorModule::set_tempo_unchecked(netuid, 1); + // Disable admin freeze window so it doesn't interfere with small tempo + assert_ok!(AdminUtils::sudo_set_admin_freeze_window( + <::RuntimeOrigin>::root(), + 0 + )); + + // First update to kappa should succeed + assert_ok!(AdminUtils::sudo_set_commit_reveal_weights_interval( + <::RuntimeOrigin>::signed(owner), + netuid, + 10 + )); + + // Immediate second update to the SAME param (kappa) should be blocked by RL + assert_noop!( + AdminUtils::sudo_set_commit_reveal_weights_interval( + <::RuntimeOrigin>::signed(owner), + netuid, + 11 + ), + SubtensorError::::TxRateLimitExceeded + ); + + // Updating a DIFFERENT param (rho) should pass immediately — independent RL key + assert_ok!(AdminUtils::sudo_set_rho( + <::RuntimeOrigin>::signed(owner), + netuid, + 5 + )); + + // kappa should still be blocked until its own RL window passes + assert_noop!( + AdminUtils::sudo_set_commit_reveal_weights_interval( + <::RuntimeOrigin>::signed(owner), + netuid, + 12 + ), + SubtensorError::::TxRateLimitExceeded + ); + + // rho should also be blocked for itself immediately after being set + assert_noop!( + AdminUtils::sudo_set_rho(<::RuntimeOrigin>::signed(owner), netuid, 6), + SubtensorError::::TxRateLimitExceeded + ); + + // Advance enough blocks to pass the RL window (2 blocks when tempo=1 and default epochs=2) + run_to_block(SubtensorModule::get_current_block_as_u64() + 2); + + // Now both hyperparameters can be updated again + assert_ok!(AdminUtils::sudo_set_commit_reveal_weights_interval( + <::RuntimeOrigin>::signed(owner), + netuid, + 13 + )); + assert_ok!(AdminUtils::sudo_set_rho( + <::RuntimeOrigin>::signed(owner), + netuid, + 7 + )); + }); +} + +#[test] +fn test_sudo_set_start_call_delay_permissions_and_zero_delay() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let tempo: u16 = 13; + let coldkey_account_id = U256::from(0); + let non_root_account = U256::from(1); + + // Get initial delay value (should be non-zero) + let initial_delay = pallet_subtensor::StartCallDelay::::get(); + assert_eq!(initial_delay, 0); + + // Test 1: Non-root account should fail to set delay + assert_noop!( + AdminUtils::sudo_set_start_call_delay( + <::RuntimeOrigin>::signed(non_root_account), + 0 + ), + DispatchError::BadOrigin + ); + + // Test 2: Create a subnet + add_network(netuid, tempo); + + if pallet_subtensor::FirstEmissionBlockNumber::::get(netuid).is_some() { + pallet_subtensor::FirstEmissionBlockNumber::::remove(netuid); + } + + assert_eq!( + pallet_subtensor::FirstEmissionBlockNumber::::get(netuid), + None, + "Emission block should not be set yet" + ); + assert_eq!( + pallet_subtensor::SubnetOwner::::get(netuid), + coldkey_account_id, + "Default owner should be account 0" + ); + + // Test 3: Can successfully start the subnet immediately + assert_ok!(pallet_subtensor::Pallet::::start_call( + <::RuntimeOrigin>::signed(coldkey_account_id), + netuid + )); + + // Verify emission has been set + assert!( + pallet_subtensor::FirstEmissionBlockNumber::::get(netuid).is_some(), + "Emission should be set" + ); + + // Test 4: Root sets delay to zero + assert_ok!(AdminUtils::sudo_set_start_call_delay( + <::RuntimeOrigin>::root(), + 0 + )); + assert_eq!( + pallet_subtensor::StartCallDelay::::get(), + 0, + "Delay should now be zero" + ); + + // Verify event was emitted + frame_system::Pallet::::assert_last_event(RuntimeEvent::SubtensorModule( + pallet_subtensor::Event::StartCallDelaySet(0), + )); + + // Test 5: Try to start the subnet again - should be FAILED (first emission block already set) + assert_err!( + pallet_subtensor::Pallet::::start_call( + <::RuntimeOrigin>::signed(coldkey_account_id), + netuid + ), + pallet_subtensor::Error::::FirstEmissionBlockNumberAlreadySet + ); + + assert_eq!( + pallet_subtensor::FirstEmissionBlockNumber::::get(netuid), + Some(frame_system::Pallet::::block_number() + 1), + "Emission should start at next block" + ); + + // Test 6: Try to start it a third time - should FAIL (already started) + assert_err!( + pallet_subtensor::Pallet::::start_call( + <::RuntimeOrigin>::signed(coldkey_account_id), + netuid + ), + pallet_subtensor::Error::::FirstEmissionBlockNumberAlreadySet + ); + }); +} + +// Verifies that owner hyperparameter rate limit is enforced based on tempo (2 tempos). +#[test] +fn test_hyperparam_rate_limit_enforced_by_tempo() { + new_test_ext().execute_with(|| { + // Setup subnet and owner + let netuid = NetUid::from(42); + add_network(netuid, 10); + let owner: U256 = U256::from(77); + SubnetOwner::::insert(netuid, owner); + + // Set tempo to 1 so RL = 2 blocks + SubtensorModule::set_tempo_unchecked(netuid, 1); + // Disable admin freeze window to avoid blocking on small tempo + assert_ok!(AdminUtils::sudo_set_admin_freeze_window( + <::RuntimeOrigin>::root(), + 0 + )); + + // First owner update should succeed + assert_ok!(AdminUtils::sudo_set_commit_reveal_weights_interval( + <::RuntimeOrigin>::signed(owner), + netuid, + 1 + )); + + // Immediate second update should fail due to tempo-based RL + assert_noop!( + AdminUtils::sudo_set_commit_reveal_weights_interval( + <::RuntimeOrigin>::signed(owner), + netuid, + 2 + ), + SubtensorError::::TxRateLimitExceeded + ); + + // Advance 2 blocks (2 tempos with tempo=1) then succeed + run_to_block(SubtensorModule::get_current_block_as_u64() + 2); + assert_ok!(AdminUtils::sudo_set_commit_reveal_weights_interval( + <::RuntimeOrigin>::signed(owner), + netuid, + 3 + )); + }); +} diff --git a/pallets/admin-utils/src/tests/alpha_commit_reveal.rs b/pallets/admin-utils/src/tests/alpha_commit_reveal.rs new file mode 100644 index 0000000000..eca7e4e849 --- /dev/null +++ b/pallets/admin-utils/src/tests/alpha_commit_reveal.rs @@ -0,0 +1,393 @@ +//! Commit-reveal, liquid alpha, alpha values/sigmoid, Yuma3, and bonds-reset toggles. +#![allow( + clippy::unwrap_used, + clippy::expect_used, + clippy::indexing_slicing, + unused_imports +)] + +use super::prelude::*; + +#[test] +fn test_sudo_set_commit_reveal_weights_enabled() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 10); + + let to_be_set: bool = false; + let init_value: bool = SubtensorModule::get_commit_reveal_weights_enabled(netuid); + + assert_ok!(AdminUtils::sudo_set_commit_reveal_weights_enabled( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + + assert!(init_value != to_be_set); + assert_eq!( + SubtensorModule::get_commit_reveal_weights_enabled(netuid), + to_be_set + ); + }); +} + +#[test] +fn test_sudo_set_liquid_alpha_enabled() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let enabled: bool = true; + NetworksAdded::::insert(netuid, true); + assert_eq!(!enabled, SubtensorModule::get_liquid_alpha_enabled(netuid)); + + assert_ok!(AdminUtils::sudo_set_liquid_alpha_enabled( + <::RuntimeOrigin>::root(), + netuid, + enabled + )); + + assert_eq!(enabled, SubtensorModule::get_liquid_alpha_enabled(netuid)); + }); +} + +#[test] +fn test_sudo_set_alpha_sigmoid_steepness() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: i16 = 5000; + add_network(netuid, 10); + let init_value = SubtensorModule::get_alpha_sigmoid_steepness(netuid); + assert_eq!( + AdminUtils::sudo_set_alpha_sigmoid_steepness( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!( + AdminUtils::sudo_set_alpha_sigmoid_steepness( + <::RuntimeOrigin>::root(), + netuid.next(), + to_be_set + ), + Err(Error::::SubnetDoesNotExist.into()) + ); + + let owner = U256::from(10); + pallet_subtensor::SubnetOwner::::insert(netuid, owner); + assert_eq!( + AdminUtils::sudo_set_alpha_sigmoid_steepness( + <::RuntimeOrigin>::signed(owner), + netuid, + -to_be_set + ), + Err(Error::::NegativeSigmoidSteepness.into()) + ); + assert_eq!( + SubtensorModule::get_alpha_sigmoid_steepness(netuid), + init_value + ); + assert_ok!(AdminUtils::sudo_set_alpha_sigmoid_steepness( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!( + SubtensorModule::get_alpha_sigmoid_steepness(netuid), + to_be_set + ); + assert_ok!(AdminUtils::sudo_set_alpha_sigmoid_steepness( + <::RuntimeOrigin>::root(), + netuid, + -to_be_set + )); + assert_eq!( + SubtensorModule::get_alpha_sigmoid_steepness(netuid), + -to_be_set + ); + }); +} + +#[test] +fn test_set_alpha_values_dispatch_info_ok() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let alpha_low: u16 = 1638_u16; + let alpha_high: u16 = u16::MAX - 10; + let call = RuntimeCall::AdminUtils(crate::Call::sudo_set_alpha_values { + netuid, + alpha_low, + alpha_high, + }); + + let dispatch_info = call.get_dispatch_info(); + + assert_eq!(dispatch_info.class, DispatchClass::Normal); + assert_eq!(dispatch_info.pays_fee, Pays::Yes); + }); +} + +#[test] +fn test_sudo_get_set_alpha() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let alpha_low: u16 = 1638_u16; + let alpha_high: u16 = u16::MAX - 10; + + let hotkey: U256 = U256::from(1); + let coldkey: U256 = U256::from(1 + 456); + let signer = <::RuntimeOrigin>::signed(coldkey); + + // Enable Liquid Alpha and setup + SubtensorModule::set_liquid_alpha_enabled(netuid, true); + pallet_subtensor::migrations::migrate_create_root_network::migrate_create_root_network::< + Test, + >(); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_000_u64.into()); + assert_ok!(SubtensorModule::root_register(signer.clone(), hotkey,)); + + // Should fail as signer does not own the subnet + assert_err!( + AdminUtils::sudo_set_alpha_values(signer.clone(), netuid, alpha_low, alpha_high), + DispatchError::BadOrigin + ); + + assert_ok!(SubtensorModule::register_network(signer.clone(), hotkey)); + + assert_ok!(AdminUtils::sudo_set_alpha_values( + signer.clone(), + netuid, + alpha_low, + alpha_high + )); + let (grabbed_alpha_low, grabbed_alpha_high): (u16, u16) = + SubtensorModule::get_alpha_values(netuid); + + log::info!("alpha_low: {grabbed_alpha_low:?} alpha_high: {grabbed_alpha_high:?}"); + assert_eq!(grabbed_alpha_low, alpha_low); + assert_eq!(grabbed_alpha_high, alpha_high); + + // Convert the u16 values to decimal values + fn unnormalize_u16_to_float(normalized_value: u16) -> f32 { + const MAX_U16: u16 = 65535; + normalized_value as f32 / MAX_U16 as f32 + } + + let alpha_low_decimal = unnormalize_u16_to_float(alpha_low); + let alpha_high_decimal = unnormalize_u16_to_float(alpha_high); + + let (alpha_low_32, alpha_high_32) = SubtensorModule::get_alpha_values_32(netuid); + + let tolerance: f32 = 1e-6; // 0.000001 + + // Check if the values are equal to the sixth decimal + assert!( + (alpha_low_32.to_num::() - alpha_low_decimal).abs() < tolerance, + "alpha_low mismatch: {} != {}", + alpha_low_32.to_num::(), + alpha_low_decimal + ); + assert!( + (alpha_high_32.to_num::() - alpha_high_decimal).abs() < tolerance, + "alpha_high mismatch: {} != {}", + alpha_high_32.to_num::(), + alpha_high_decimal + ); + + // 1. Liquid alpha disabled + SubtensorModule::set_liquid_alpha_enabled(netuid, false); + assert_err!( + AdminUtils::sudo_set_alpha_values(signer.clone(), netuid, alpha_low, alpha_high), + SubtensorError::::LiquidAlphaDisabled + ); + // Correct scenario after error + SubtensorModule::set_liquid_alpha_enabled(netuid, true); // Re-enable for further tests + assert_ok!(AdminUtils::sudo_set_alpha_values( + signer.clone(), + netuid, + alpha_low, + alpha_high + )); + + // 2. Alpha high too low + let alpha_high_too_low = (u16::MAX as u32 / 40) as u16 - 1; // One less than the minimum acceptable value + assert_err!( + AdminUtils::sudo_set_alpha_values( + signer.clone(), + netuid, + alpha_low, + alpha_high_too_low + ), + SubtensorError::::AlphaHighTooLow + ); + // Correct scenario after error + assert_ok!(AdminUtils::sudo_set_alpha_values( + signer.clone(), + netuid, + alpha_low, + alpha_high + )); + + // 3. Alpha low too low or too high + let alpha_low_too_low = 0_u16; + assert_err!( + AdminUtils::sudo_set_alpha_values( + signer.clone(), + netuid, + alpha_low_too_low, + alpha_high + ), + SubtensorError::::AlphaLowOutOfRange + ); + // Correct scenario after error + assert_ok!(AdminUtils::sudo_set_alpha_values( + signer.clone(), + netuid, + alpha_low, + alpha_high + )); + + let alpha_low_too_high = alpha_high + 1; + assert_err!( + AdminUtils::sudo_set_alpha_values( + signer.clone(), + netuid, + alpha_low_too_high, + alpha_high + ), + SubtensorError::::AlphaLowOutOfRange + ); + // Correct scenario after error + assert_ok!(AdminUtils::sudo_set_alpha_values( + signer.clone(), + netuid, + alpha_low, + alpha_high + )); + }); +} + +#[test] +fn sudo_set_commit_reveal_weights_interval() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 10); + + let too_high = 101; + assert_err!( + AdminUtils::sudo_set_commit_reveal_weights_interval( + <::RuntimeOrigin>::root(), + netuid, + too_high + ), + pallet_subtensor::Error::::RevealPeriodTooLarge + ); + + let to_be_set = 55; + let init_value = SubtensorModule::get_reveal_period(netuid); + + assert_ok!(AdminUtils::sudo_set_commit_reveal_weights_interval( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + + assert!(init_value != to_be_set); + assert_eq!(SubtensorModule::get_reveal_period(netuid), to_be_set); + }); +} + +#[test] +fn test_sudo_set_bonds_reset_enabled() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: bool = true; + let sn_owner = U256::from(1); + add_network(netuid, 10); + let init_value: bool = SubtensorModule::get_bonds_reset(netuid); + + assert_eq!( + AdminUtils::sudo_set_bonds_reset_enabled( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + + assert_ok!(AdminUtils::sudo_set_bonds_reset_enabled( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!(SubtensorModule::get_bonds_reset(netuid), to_be_set); + assert_ne!(SubtensorModule::get_bonds_reset(netuid), init_value); + + pallet_subtensor::SubnetOwner::::insert(netuid, sn_owner); + + assert_ok!(AdminUtils::sudo_set_bonds_reset_enabled( + <::RuntimeOrigin>::signed(sn_owner), + netuid, + !to_be_set + )); + assert_eq!(SubtensorModule::get_bonds_reset(netuid), !to_be_set); + }); +} + +#[test] +fn test_sudo_set_yuma3_enabled() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: bool = false; + let sn_owner = U256::from(1); + add_network(netuid, 10); + let init_value: bool = SubtensorModule::get_yuma3_enabled(netuid); + + assert_eq!( + AdminUtils::sudo_set_yuma3_enabled( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + + assert_ok!(AdminUtils::sudo_set_yuma3_enabled( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!(SubtensorModule::get_yuma3_enabled(netuid), to_be_set); + assert_ne!(SubtensorModule::get_yuma3_enabled(netuid), init_value); + + pallet_subtensor::SubnetOwner::::insert(netuid, sn_owner); + + assert_ok!(AdminUtils::sudo_set_yuma3_enabled( + <::RuntimeOrigin>::signed(sn_owner), + netuid, + !to_be_set + )); + assert_eq!(SubtensorModule::get_yuma3_enabled(netuid), !to_be_set); + }); +} + +#[test] +fn test_sudo_set_commit_reveal_version() { + new_test_ext().execute_with(|| { + add_network(NetUid::from(1), 10); + + let to_be_set: u16 = 5; + let init_value: u16 = SubtensorModule::get_commit_reveal_weights_version(); + + assert_ok!(AdminUtils::sudo_set_commit_reveal_version( + <::RuntimeOrigin>::root(), + to_be_set + )); + + assert!(init_value != to_be_set); + assert_eq!( + SubtensorModule::get_commit_reveal_weights_version(), + to_be_set + ); + }); +} diff --git a/pallets/admin-utils/src/tests/consensus_hyperparams.rs b/pallets/admin-utils/src/tests/consensus_hyperparams.rs new file mode 100644 index 0000000000..9793fda794 --- /dev/null +++ b/pallets/admin-utils/src/tests/consensus_hyperparams.rs @@ -0,0 +1,378 @@ +//! Consensus hyperparams: kappa, rho, activity cutoff, immunity, tempo, bonds averages/penalties. +#![allow( + clippy::unwrap_used, + clippy::expect_used, + clippy::indexing_slicing, + unused_imports +)] + +use super::prelude::*; + +#[test] +fn test_sudo_set_immunity_period() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: u16 = 10; + add_network(netuid, 10); + let init_value: u16 = SubtensorModule::get_immunity_period(netuid); + assert_eq!( + AdminUtils::sudo_set_immunity_period( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!( + AdminUtils::sudo_set_immunity_period( + <::RuntimeOrigin>::root(), + netuid.next(), + to_be_set + ), + Err(Error::::SubnetDoesNotExist.into()) + ); + assert_eq!(SubtensorModule::get_immunity_period(netuid), init_value); + assert_ok!(AdminUtils::sudo_set_immunity_period( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!(SubtensorModule::get_immunity_period(netuid), to_be_set); + }); +} + +#[test] +fn test_sudo_set_min_allowed_weights() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: u16 = 10; + add_network(netuid, 10); + let init_value: u16 = SubtensorModule::get_min_allowed_weights(netuid); + assert_eq!( + AdminUtils::sudo_set_min_allowed_weights( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!( + AdminUtils::sudo_set_min_allowed_weights( + <::RuntimeOrigin>::root(), + netuid.next(), + to_be_set + ), + Err(Error::::SubnetDoesNotExist.into()) + ); + assert_eq!(SubtensorModule::get_min_allowed_weights(netuid), init_value); + assert_ok!(AdminUtils::sudo_set_min_allowed_weights( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!(SubtensorModule::get_min_allowed_weights(netuid), to_be_set); + }); +} + +#[test] +fn test_sudo_set_kappa() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: u16 = 10; + add_network(netuid, 10); + let init_value: u16 = SubtensorModule::get_kappa(netuid); + assert_eq!( + AdminUtils::sudo_set_kappa( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!( + AdminUtils::sudo_set_kappa( + <::RuntimeOrigin>::root(), + netuid.next(), + to_be_set + ), + Err(Error::::SubnetDoesNotExist.into()) + ); + assert_eq!(SubtensorModule::get_kappa(netuid), init_value); + assert_ok!(AdminUtils::sudo_set_kappa( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!(SubtensorModule::get_kappa(netuid), to_be_set); + }); +} + +#[test] +fn test_sudo_set_rho() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: u16 = 10; + add_network(netuid, 10); + let init_value: u16 = SubtensorModule::get_rho(netuid); + assert_eq!( + AdminUtils::sudo_set_rho( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!( + AdminUtils::sudo_set_rho( + <::RuntimeOrigin>::root(), + netuid.next(), + to_be_set + ), + Err(Error::::SubnetDoesNotExist.into()) + ); + assert_eq!(SubtensorModule::get_rho(netuid), init_value); + assert_ok!(AdminUtils::sudo_set_rho( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!(SubtensorModule::get_rho(netuid), to_be_set); + }); +} + +#[test] +fn test_sudo_set_activity_cutoff() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: u16 = pallet_subtensor::MinActivityCutoff::::get(); + add_network(netuid, 10); + let init_value: u16 = SubtensorModule::get_activity_cutoff(netuid); + assert_eq!( + AdminUtils::sudo_set_activity_cutoff( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!( + AdminUtils::sudo_set_activity_cutoff( + <::RuntimeOrigin>::root(), + netuid.next(), + to_be_set + ), + Err(Error::::SubnetDoesNotExist.into()) + ); + assert_eq!(SubtensorModule::get_activity_cutoff(netuid), init_value); + assert_ok!(AdminUtils::sudo_set_activity_cutoff( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!(SubtensorModule::get_activity_cutoff(netuid), to_be_set); + }); +} + +#[test] +fn test_sudo_set_activity_cutoff_factor() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 10); + let owner = U256::from(5); + SubnetOwner::::insert(netuid, owner); + SubtensorModule::set_admin_freeze_window(0); + + // A non-owner signed origin is rejected. + assert_eq!( + AdminUtils::sudo_set_activity_cutoff_factor( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + 5_000 + ), + Err(DispatchError::BadOrigin) + ); + + // Out-of-bounds factors are rejected for owner and root alike. + assert_noop!( + AdminUtils::sudo_set_activity_cutoff_factor( + <::RuntimeOrigin>::root(), + netuid, + MAX_ACTIVITY_CUTOFF_FACTOR_MILLI + 1 + ), + SubtensorError::::ActivityCutoffFactorMilliOutOfBounds + ); + + // The owner can set a factor within bounds. + assert_ok!(AdminUtils::sudo_set_activity_cutoff_factor( + <::RuntimeOrigin>::signed(owner), + netuid, + 5_000 + )); + assert_eq!(ActivityCutoffFactorMilli::::get(netuid), 5_000); + + // A second owner change within the rate limit is rejected; root bypasses it. + assert_noop!( + AdminUtils::sudo_set_activity_cutoff_factor( + <::RuntimeOrigin>::signed(owner), + netuid, + 6_000 + ), + SubtensorError::::TxRateLimitExceeded + ); + assert_ok!(AdminUtils::sudo_set_activity_cutoff_factor( + <::RuntimeOrigin>::root(), + netuid, + 6_000 + )); + assert_eq!(ActivityCutoffFactorMilli::::get(netuid), 6_000); + }); +} + +#[test] +fn test_sudo_set_tempo_owner_and_root() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 10); + let owner = U256::from(5); + SubnetOwner::::insert(netuid, owner); + SubtensorModule::set_admin_freeze_window(0); + + // A non-owner signed origin is rejected. + assert_eq!( + AdminUtils::sudo_set_tempo( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + MIN_TEMPO + ), + Err(DispatchError::BadOrigin) + ); + + // A nonexistent subnet is rejected for root. + assert_noop!( + AdminUtils::sudo_set_tempo( + <::RuntimeOrigin>::root(), + netuid.next(), + MIN_TEMPO + ), + SubtensorError::::SubnetNotExists + ); + + // The owner is bounded to [MIN_TEMPO, MAX_TEMPO]. + assert_noop!( + AdminUtils::sudo_set_tempo( + <::RuntimeOrigin>::signed(owner), + netuid, + MIN_TEMPO - 1 + ), + SubtensorError::::TempoOutOfBounds + ); + assert_noop!( + AdminUtils::sudo_set_tempo( + <::RuntimeOrigin>::signed(owner), + netuid, + MAX_TEMPO + 1 + ), + SubtensorError::::TempoOutOfBounds + ); + + // Within bounds the owner change lands and resets the cycle. + assert_ok!(AdminUtils::sudo_set_tempo( + <::RuntimeOrigin>::signed(owner), + netuid, + MIN_TEMPO + )); + assert_eq!(Tempo::::get(netuid), MIN_TEMPO); + let now = SubtensorModule::get_current_block_as_u64(); + assert_eq!(LastEpochBlock::::get(netuid), now); + + // A second owner change within the MIN_TEMPO cooldown is rate-limited. + assert_noop!( + AdminUtils::sudo_set_tempo( + <::RuntimeOrigin>::signed(owner), + netuid, + MIN_TEMPO + 1 + ), + SubtensorError::::TxRateLimitExceeded + ); + + // Root bypasses the bounds and the rate limit. + assert_ok!(AdminUtils::sudo_set_tempo( + <::RuntimeOrigin>::root(), + netuid, + 10 + )); + assert_eq!(Tempo::::get(netuid), 10); + }); +} + +#[test] +fn test_sudo_set_bonds_moving_average() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: u64 = 10; + add_network(netuid, 10); + let init_value: u64 = SubtensorModule::get_bonds_moving_average(netuid.into()); + assert_eq!( + AdminUtils::sudo_set_bonds_moving_average( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!( + AdminUtils::sudo_set_bonds_moving_average( + <::RuntimeOrigin>::root(), + netuid.next(), + to_be_set + ), + Err(Error::::SubnetDoesNotExist.into()) + ); + assert_eq!( + SubtensorModule::get_bonds_moving_average(netuid.into()), + init_value + ); + assert_ok!(AdminUtils::sudo_set_bonds_moving_average( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!( + SubtensorModule::get_bonds_moving_average(netuid.into()), + to_be_set + ); + }); +} + +#[test] +fn test_sudo_set_bonds_penalty() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: u16 = 10; + add_network(netuid, 10); + let init_value: u16 = SubtensorModule::get_bonds_penalty(netuid); + assert_eq!( + AdminUtils::sudo_set_bonds_penalty( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!( + AdminUtils::sudo_set_bonds_penalty( + <::RuntimeOrigin>::root(), + netuid.next(), + to_be_set + ), + Err(Error::::SubnetDoesNotExist.into()) + ); + assert_eq!(SubtensorModule::get_bonds_penalty(netuid), init_value); + assert_ok!(AdminUtils::sudo_set_bonds_penalty( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!(SubtensorModule::get_bonds_penalty(netuid), to_be_set); + }); +} diff --git a/pallets/admin-utils/src/tests/evm_grandpa_precompile.rs b/pallets/admin-utils/src/tests/evm_grandpa_precompile.rs new file mode 100644 index 0000000000..20ddadb3c0 --- /dev/null +++ b/pallets/admin-utils/src/tests/evm_grandpa_precompile.rs @@ -0,0 +1,135 @@ +//! EVM chain id, GRANDPA authority schedule, and EVM precompile enable toggles. +#![allow( + clippy::unwrap_used, + clippy::expect_used, + clippy::indexing_slicing, + unused_imports +)] + +use super::prelude::*; + +#[test] +fn test_sudo_root_sets_evm_chain_id() { + new_test_ext().execute_with(|| { + let chain_id: u64 = 945; + assert_eq!(pallet_evm_chain_id::ChainId::::get(), 0); + + assert_ok!(AdminUtils::sudo_set_evm_chain_id( + <::RuntimeOrigin>::root(), + chain_id + )); + + assert_eq!(pallet_evm_chain_id::ChainId::::get(), chain_id); + }); +} + +#[test] +fn test_sudo_non_root_cannot_set_evm_chain_id() { + new_test_ext().execute_with(|| { + let chain_id: u64 = 945; + assert_eq!(pallet_evm_chain_id::ChainId::::get(), 0); + + assert_eq!( + AdminUtils::sudo_set_evm_chain_id( + <::RuntimeOrigin>::signed(U256::from(0)), + chain_id + ), + Err(DispatchError::BadOrigin) + ); + + assert_eq!(pallet_evm_chain_id::ChainId::::get(), 0); + }); +} + +#[test] +fn test_schedule_grandpa_change() { + new_test_ext().execute_with(|| { + assert_eq!(Grandpa::grandpa_authorities(), vec![]); + + let bob: GrandpaId = ed25519::Pair::from_legacy_string("//Bob", None) + .public() + .into(); + + assert_ok!(AdminUtils::schedule_grandpa_change( + RuntimeOrigin::root(), + vec![(bob.clone(), 1)], + 41, + None + )); + + Grandpa::on_finalize(42); + + assert_eq!(Grandpa::grandpa_authorities(), vec![(bob, 1)]); + }); +} + +#[test] +fn test_sudo_toggle_evm_precompile() { + new_test_ext().execute_with(|| { + let precompile_id = crate::PrecompileEnum::BalanceTransfer; + let initial_enabled = PrecompileEnable::::get(precompile_id); + assert!(initial_enabled); // Assuming the default is true + + run_to_block(1); + + assert_eq!( + AdminUtils::sudo_toggle_evm_precompile( + <::RuntimeOrigin>::signed(U256::from(0)), + precompile_id, + false + ), + Err(DispatchError::BadOrigin) + ); + + assert_ok!(AdminUtils::sudo_toggle_evm_precompile( + RuntimeOrigin::root(), + precompile_id, + false + )); + + assert_eq!( + System::events() + .iter() + .filter(|r| r.event + == RuntimeEvent::AdminUtils(crate::Event::PrecompileUpdated { + precompile_id, + enabled: false + })) + .count(), + 1 + ); + + let updated_enabled = PrecompileEnable::::get(precompile_id); + assert!(!updated_enabled); + + run_to_block(2); + + assert_ok!(AdminUtils::sudo_toggle_evm_precompile( + RuntimeOrigin::root(), + precompile_id, + false + )); + + // no event without status change + assert_eq!( + System::events() + .iter() + .filter(|r| r.event + == RuntimeEvent::AdminUtils(crate::Event::PrecompileUpdated { + precompile_id, + enabled: false + })) + .count(), + 0 + ); + + assert_ok!(AdminUtils::sudo_toggle_evm_precompile( + RuntimeOrigin::root(), + precompile_id, + true + )); + + let final_enabled = PrecompileEnable::::get(precompile_id); + assert!(final_enabled); + }); +} diff --git a/pallets/admin-utils/src/tests/mechanisms.rs b/pallets/admin-utils/src/tests/mechanisms.rs new file mode 100644 index 0000000000..13a692aa65 --- /dev/null +++ b/pallets/admin-utils/src/tests/mechanisms.rs @@ -0,0 +1,147 @@ +//! Per-subnet mechanism count, emission splits, and global max mechanism count. +#![allow( + clippy::unwrap_used, + clippy::expect_used, + clippy::indexing_slicing, + unused_imports +)] + +use super::prelude::*; + +#[test] +fn test_sudo_set_mechanism_count() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let ss_count_ok = MaxMechanismCount::::get(); + let ss_count_bad = MechId::from(u8::from(ss_count_ok) + 1); + + let sn_owner = U256::from(1324); + add_network(netuid, 10); + // Set the Subnet Owner + SubnetOwner::::insert(netuid, sn_owner); + MaxAllowedUids::::insert(netuid, 256_u16); + + assert_eq!( + AdminUtils::sudo_set_mechanism_count( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + ss_count_ok + ), + Err(DispatchError::BadOrigin) + ); + assert_noop!( + AdminUtils::sudo_set_mechanism_count(RuntimeOrigin::root(), netuid, ss_count_bad), + pallet_subtensor::Error::::InvalidValue + ); + assert_noop!( + AdminUtils::sudo_set_mechanism_count(RuntimeOrigin::root(), netuid, ss_count_ok), + pallet_subtensor::Error::::TooManyUIDsPerMechanism + ); + + // Reduce max UIDs to 128 + MaxAllowedUids::::insert(netuid, 128_u16); + assert_ok!(AdminUtils::sudo_set_mechanism_count( + <::RuntimeOrigin>::root(), + netuid, + ss_count_ok + )); + + assert_ok!(AdminUtils::sudo_set_mechanism_count( + <::RuntimeOrigin>::signed(sn_owner), + netuid, + ss_count_ok + )); + }); +} + +// cargo test --package pallet-admin-utils --lib -- tests::test_sudo_set_mechanism_count_and_emissions --exact --show-output +#[test] +fn test_sudo_set_mechanism_count_and_emissions() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let ss_count_ok = MechId::from(2); + + let sn_owner = U256::from(1324); + add_network(netuid, 10); + // Set the Subnet Owner + SubnetOwner::::insert(netuid, sn_owner); + MaxMechanismCount::::set(MechId::from(2)); + MaxAllowedUids::::set(netuid, 128_u16); + + assert_ok!(AdminUtils::sudo_set_mechanism_count( + <::RuntimeOrigin>::signed(sn_owner), + netuid, + ss_count_ok + )); + + // Cannot set emission split with wrong number of entries + // With two mechanisms the size of the split vector should be 2, not 3 + assert_noop!( + AdminUtils::sudo_set_mechanism_emission_split( + <::RuntimeOrigin>::signed(sn_owner), + netuid, + Some(vec![0xFFFF / 5 * 2, 0xFFFF / 5 * 2, 0xFFFF / 5]) + ), + pallet_subtensor::Error::::InvalidValue + ); + + // Cannot set emission split with wrong total of entries + // Split vector entries should sum up to exactly 0xFFFF + assert_noop!( + AdminUtils::sudo_set_mechanism_emission_split( + <::RuntimeOrigin>::signed(sn_owner), + netuid, + Some(vec![0xFFFF / 5 * 4, 0xFFFF / 5 - 1]) + ), + pallet_subtensor::Error::::InvalidValue + ); + + // Can set good split ok + // We also verify here that it can happen in the same block as setting mechanism counts + // or soon, without rate limiting + assert_ok!(AdminUtils::sudo_set_mechanism_emission_split( + <::RuntimeOrigin>::signed(sn_owner), + netuid, + Some(vec![0xFFFF / 5, 0xFFFF / 5 * 4]) + )); + + // Cannot set it again due to rate limits + assert_noop!( + AdminUtils::sudo_set_mechanism_emission_split( + <::RuntimeOrigin>::signed(sn_owner), + netuid, + Some(vec![0xFFFF / 5 * 4, 0xFFFF / 5]) + ), + pallet_subtensor::Error::::TxRateLimitExceeded + ); + }); +} + +#[test] +fn test_sudo_set_max_mechanism_count() { + new_test_ext().execute_with(|| { + // Normal case + assert_ok!(AdminUtils::sudo_set_max_mechanism_count( + <::RuntimeOrigin>::root(), + MechId::from(10) + )); + + // Zero fails + assert_noop!( + AdminUtils::sudo_set_max_mechanism_count( + <::RuntimeOrigin>::root(), + MechId::from(0) + ), + pallet_subtensor::Error::::InvalidValue + ); + + // Over max bound fails + assert_noop!( + AdminUtils::sudo_set_max_mechanism_count( + <::RuntimeOrigin>::root(), + MechId::from(MAX_MECHANISM_COUNT_PER_SUBNET + 1) + ), + pallet_subtensor::Error::::InvalidValue + ); + }); +} diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index ad8152b8e2..5021e30ea3 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -1,3 +1,7 @@ +//! Mock runtime and helpers for `pallet-admin-utils` unit tests and benchmarks. +//! +//! Builds a minimal `Test` runtime with Subtensor, Swap, EVM chain id, and GRANDPA so admin +//! extrinsics can be exercised without the full node runtime. #![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] use core::num::NonZeroU64; @@ -246,7 +250,7 @@ impl pallet_subtensor::Config for Test { type LeaseDividendsDistributionInterval = LeaseDividendsDistributionInterval; type GetCommitments = (); type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; - type CommitmentsInterface = CommitmentsI; + type CommitmentsInterface = CommitmentsPurgeBridge; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; type AuthorshipProvider = MockAuthorshipProvider; type SubtensorPalletId = SubtensorPalletId; @@ -381,8 +385,8 @@ impl PrivilegeCmp for OriginPrivilegeCmp { } } -pub struct CommitmentsI; -impl pallet_subtensor::CommitmentsInterface for CommitmentsI { +pub struct CommitmentsPurgeBridge; +impl pallet_subtensor::CommitmentsInterface for CommitmentsPurgeBridge { fn purge_netuid( _netuid: NetUid, _weight_meter: &mut frame_support::weights::WeightMeter, @@ -391,8 +395,8 @@ impl pallet_subtensor::CommitmentsInterface for CommitmentsI { } } -pub struct GrandpaInterfaceImpl; -impl crate::GrandpaInterface for GrandpaInterfaceImpl { +pub struct GrandpaAuthorityInterface; +impl crate::GrandpaInterface for GrandpaAuthorityInterface { fn schedule_change( next_authorities: GrandpaAuthorityList, in_blocks: BlockNumber, @@ -406,7 +410,7 @@ impl crate::Config for Test { type AuthorityId = AuraId; type MaxAuthorities = ConstU32<32>; type Aura = (); - type Grandpa = GrandpaInterfaceImpl; + type Grandpa = GrandpaAuthorityInterface; type Balance = Balance; type WeightInfo = (); } @@ -495,7 +499,7 @@ where } } -// Build genesis storage according to the mock runtime. +/// Build externalities with block 1 and a one-block admin freeze window. pub fn new_test_ext() -> sp_io::TestExternalities { sp_tracing::try_init_simple(); let t = frame_system::GenesisConfig::::default() @@ -509,6 +513,7 @@ pub fn new_test_ext() -> sp_io::TestExternalities { ext } +/// Advance the mock chain to absolute block `n`, running system/subtensor hooks each step. #[allow(dead_code)] pub(crate) fn run_to_block(n: u64) { while System::block_number() < n { @@ -521,6 +526,7 @@ pub(crate) fn run_to_block(n: u64) { } } +/// Burn-register a neuron after funding the coldkey and seeding subnet AMM reserves. #[allow(dead_code)] pub fn register_ok_neuron( netuid: NetUid, @@ -552,6 +558,7 @@ pub fn register_ok_neuron( ); } +/// Create a subnet with registration allowed, emission started, and burn params set for easy tests. #[allow(dead_code)] pub fn add_network(netuid: NetUid, tempo: u16) { SubtensorModule::init_new_network(netuid, tempo); @@ -566,23 +573,27 @@ pub fn add_network(netuid: NetUid, tempo: u16) { } use subtensor_runtime_common::AlphaBalance; + +/// Seed subnet TAO/alpha-in reserves used by registration and swap paths. pub(crate) fn setup_reserves(netuid: NetUid, tao: TaoBalance, alpha: AlphaBalance) { pallet_subtensor::SubnetTAO::::set(netuid, tao); pallet_subtensor::SubnetAlphaIn::::set(netuid, alpha); } -/// Convenience wrapper for tests that need to advance blocks incrementally. +/// Advance `n` blocks from the current block number. pub fn step_block(n: u64) { let current: u64 = frame_system::Pallet::::block_number().into(); run_to_block(current + n); } +/// Credit `tao` free balance to a coldkey via mint/spend. #[allow(dead_code)] pub fn add_balance_to_coldkey_account(coldkey: &U256, tao: TaoBalance) { let credit = SubtensorModule::mint_tao(tao); let _ = SubtensorModule::spend_tao(coldkey, credit, tao).unwrap(); } +/// Burn `tao` from a coldkey's free balance. #[allow(dead_code)] pub fn remove_balance_from_coldkey_account(coldkey: &U256, tao: TaoBalance) { let _ = SubtensorModule::burn_tao(coldkey, tao); diff --git a/pallets/admin-utils/src/tests/mod.rs b/pallets/admin-utils/src/tests/mod.rs index a0bf21ed04..48837ef668 100644 --- a/pallets/admin-utils/src/tests/mod.rs +++ b/pallets/admin-utils/src/tests/mod.rs @@ -1,3385 +1,44 @@ -use crate::{Error, pallet::PrecompileEnable}; -use frame_support::{ - assert_err, assert_noop, assert_ok, - dispatch::{DispatchClass, GetDispatchInfo, Pays}, - sp_runtime::DispatchError, - traits::{Currency as _, Hooks}, -}; -use frame_system::Config; -use pallet_subtensor::{ - Error as SubtensorError, Event, MaxRegistrationsPerBlock, SubnetOwner, - TargetRegistrationsPerInterval, Tempo, WeightsVersionKeyRateLimit, - subnets::mechanism::MAX_MECHANISM_COUNT_PER_SUBNET, utils::rate_limiting::TransactionType, *, -}; -use sp_consensus_grandpa::AuthorityId as GrandpaId; -use sp_core::{Get, Pair, U256, ed25519}; -use sp_runtime::PerU16; -use substrate_fixed::types::I96F32; -use subtensor_runtime_common::{MechId, NetUid, TaoBalance, Token}; -pub mod mock; -use mock::*; - -#[test] -fn test_sudo_set_default_take() { - new_test_ext().execute_with(|| { - let to_be_set = PerU16::from_parts(10); - let init_value: u16 = SubtensorModule::get_default_delegate_take(); - assert_eq!( - AdminUtils::sudo_set_default_take( - <::RuntimeOrigin>::signed(U256::from(0)), - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!(SubtensorModule::get_default_delegate_take(), init_value); - assert_ok!(AdminUtils::sudo_set_default_take( - <::RuntimeOrigin>::root(), - to_be_set - )); - assert_eq!( - SubtensorModule::get_default_delegate_take(), - to_be_set.deconstruct() - ); - }); -} - -#[test] -fn test_sudo_set_serving_rate_limit() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(3); - let to_be_set: u64 = 10; - let init_value: u64 = SubtensorModule::get_serving_rate_limit(netuid); - assert_eq!( - AdminUtils::sudo_set_serving_rate_limit( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!(SubtensorModule::get_serving_rate_limit(netuid), init_value); - assert_ok!(AdminUtils::sudo_set_serving_rate_limit( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!(SubtensorModule::get_serving_rate_limit(netuid), to_be_set); - }); -} - -#[test] -fn test_sudo_set_min_difficulty() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: u64 = 10; - add_network(netuid, 10); - let init_value: u64 = SubtensorModule::get_min_difficulty(netuid); - assert_eq!( - AdminUtils::sudo_set_min_difficulty( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!( - AdminUtils::sudo_set_min_difficulty( - <::RuntimeOrigin>::root(), - netuid.next(), - to_be_set - ), - Err(Error::::SubnetDoesNotExist.into()) - ); - assert_eq!(SubtensorModule::get_min_difficulty(netuid), init_value); - assert_ok!(AdminUtils::sudo_set_min_difficulty( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!(SubtensorModule::get_min_difficulty(netuid), to_be_set); - }); -} - -#[test] -fn test_sudo_set_max_difficulty() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: u64 = 10; - add_network(netuid, 10); - let init_value: u64 = SubtensorModule::get_max_difficulty(netuid); - assert_eq!( - AdminUtils::sudo_set_max_difficulty( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!( - AdminUtils::sudo_set_max_difficulty( - <::RuntimeOrigin>::root(), - netuid.next(), - to_be_set - ), - Err(Error::::SubnetDoesNotExist.into()) - ); - assert_eq!(SubtensorModule::get_max_difficulty(netuid), init_value); - assert_ok!(AdminUtils::sudo_set_max_difficulty( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!(SubtensorModule::get_max_difficulty(netuid), to_be_set); - }); -} - -#[test] -fn test_sudo_set_weights_version_key() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: u64 = 10; - add_network(netuid, 10); - let init_value: u64 = SubtensorModule::get_weights_version_key(netuid); - assert_eq!( - AdminUtils::sudo_set_weights_version_key( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!( - AdminUtils::sudo_set_weights_version_key( - <::RuntimeOrigin>::root(), - netuid.next(), - to_be_set - ), - Err(Error::::SubnetDoesNotExist.into()) - ); - assert_eq!(SubtensorModule::get_weights_version_key(netuid), init_value); - assert_ok!(AdminUtils::sudo_set_weights_version_key( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!(SubtensorModule::get_weights_version_key(netuid), to_be_set); - }); -} - -#[test] -fn test_sudo_set_weights_version_key_rate_limit() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: u64 = 10; - - let sn_owner = U256::from(1); - add_network(netuid, 10); - // Set the Subnet Owner - SubnetOwner::::insert(netuid, sn_owner); - - let rate_limit = WeightsVersionKeyRateLimit::::get(); - let tempo = Tempo::::get(netuid); - - let rate_limit_period = rate_limit * (tempo as u64); - - assert_ok!(AdminUtils::sudo_set_weights_version_key( - <::RuntimeOrigin>::signed(sn_owner), - netuid, - to_be_set - )); - assert_eq!(SubtensorModule::get_weights_version_key(netuid), to_be_set); - - // Try to set again with - // Assert rate limit not passed - assert!( - !TransactionType::SetWeightsVersionKey - .passes_rate_limit_on_subnet::(&sn_owner, netuid) - ); - - // Try transaction - assert_noop!( - AdminUtils::sudo_set_weights_version_key( - <::RuntimeOrigin>::signed(sn_owner), - netuid, - to_be_set + 1 - ), - pallet_subtensor::Error::::TxRateLimitExceeded - ); - - // Wait for rate limit to pass - run_to_block(rate_limit_period + 1); - assert!( - TransactionType::SetWeightsVersionKey - .passes_rate_limit_on_subnet::(&sn_owner, netuid) - ); - - // Try transaction - assert_ok!(AdminUtils::sudo_set_weights_version_key( - <::RuntimeOrigin>::signed(sn_owner), - netuid, - to_be_set + 1 - )); - assert_eq!( - SubtensorModule::get_weights_version_key(netuid), - to_be_set + 1 - ); - }); -} - -#[test] -fn test_sudo_set_weights_version_key_rate_limit_root() { - // root should not be effected by rate limit - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: u64 = 10; - - let sn_owner = U256::from(1); - add_network(netuid, 10); - // Set the Subnet Owner - SubnetOwner::::insert(netuid, sn_owner); - - let rate_limit = WeightsVersionKeyRateLimit::::get(); - let tempo: u16 = Tempo::::get(netuid); - - let rate_limit_period = rate_limit * (tempo as u64); - // Verify the rate limit is more than 0 blocks - assert!(rate_limit_period > 0); - - assert_ok!(AdminUtils::sudo_set_weights_version_key( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!(SubtensorModule::get_weights_version_key(netuid), to_be_set); - - // Try transaction - assert_ok!(AdminUtils::sudo_set_weights_version_key( - <::RuntimeOrigin>::signed(sn_owner), - netuid, - to_be_set + 1 - )); - assert_eq!( - SubtensorModule::get_weights_version_key(netuid), - to_be_set + 1 - ); - }); -} - -#[test] -fn test_sudo_set_weights_set_rate_limit() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: u64 = 10; - add_network(netuid, 10); - let init_value: u64 = SubtensorModule::get_weights_set_rate_limit(netuid); - assert_eq!( - AdminUtils::sudo_set_weights_set_rate_limit( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!( - AdminUtils::sudo_set_weights_set_rate_limit( - <::RuntimeOrigin>::root(), - netuid.next(), - to_be_set - ), - Err(Error::::SubnetDoesNotExist.into()) - ); - assert_eq!( - SubtensorModule::get_weights_set_rate_limit(netuid), - init_value - ); - assert_ok!(AdminUtils::sudo_set_weights_set_rate_limit( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!( - SubtensorModule::get_weights_set_rate_limit(netuid), - to_be_set - ); - }); -} - -#[test] -fn test_sudo_set_adjustment_interval() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: u16 = 10; - add_network(netuid, 10); - let init_value: u16 = SubtensorModule::get_adjustment_interval(netuid); - assert_eq!( - AdminUtils::sudo_set_adjustment_interval( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!( - AdminUtils::sudo_set_adjustment_interval( - <::RuntimeOrigin>::root(), - netuid.next(), - to_be_set - ), - Err(Error::::SubnetDoesNotExist.into()) - ); - assert_eq!(SubtensorModule::get_adjustment_interval(netuid), init_value); - assert_ok!(AdminUtils::sudo_set_adjustment_interval( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!(SubtensorModule::get_adjustment_interval(netuid), to_be_set); - }); -} - -#[test] -fn test_sudo_set_adjustment_alpha() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: u64 = 10; - add_network(netuid, 10); - let init_value: u64 = SubtensorModule::get_adjustment_alpha(netuid); - assert_eq!( - AdminUtils::sudo_set_adjustment_alpha( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!( - AdminUtils::sudo_set_adjustment_alpha( - <::RuntimeOrigin>::root(), - netuid.next(), - to_be_set - ), - Err(Error::::SubnetDoesNotExist.into()) - ); - assert_eq!(SubtensorModule::get_adjustment_alpha(netuid), init_value); - assert_ok!(AdminUtils::sudo_set_adjustment_alpha( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!(SubtensorModule::get_adjustment_alpha(netuid), to_be_set); - }); -} - -#[test] -fn test_sudo_subnet_owner_cut() { - new_test_ext().execute_with(|| { - let to_be_set: u16 = 10; - let init_value: u16 = SubtensorModule::get_subnet_owner_cut(); - assert_eq!( - AdminUtils::sudo_set_subnet_owner_cut( - <::RuntimeOrigin>::signed(U256::from(0)), - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!(SubtensorModule::get_subnet_owner_cut(), init_value); - assert_ok!(AdminUtils::sudo_set_subnet_owner_cut( - <::RuntimeOrigin>::root(), - to_be_set - )); - assert_eq!(SubtensorModule::get_subnet_owner_cut(), to_be_set); - }); -} - -#[test] -fn test_sudo_set_immunity_period() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: u16 = 10; - add_network(netuid, 10); - let init_value: u16 = SubtensorModule::get_immunity_period(netuid); - assert_eq!( - AdminUtils::sudo_set_immunity_period( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!( - AdminUtils::sudo_set_immunity_period( - <::RuntimeOrigin>::root(), - netuid.next(), - to_be_set - ), - Err(Error::::SubnetDoesNotExist.into()) - ); - assert_eq!(SubtensorModule::get_immunity_period(netuid), init_value); - assert_ok!(AdminUtils::sudo_set_immunity_period( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!(SubtensorModule::get_immunity_period(netuid), to_be_set); - }); -} - -#[test] -fn test_sudo_set_min_allowed_weights() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: u16 = 10; - add_network(netuid, 10); - let init_value: u16 = SubtensorModule::get_min_allowed_weights(netuid); - assert_eq!( - AdminUtils::sudo_set_min_allowed_weights( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!( - AdminUtils::sudo_set_min_allowed_weights( - <::RuntimeOrigin>::root(), - netuid.next(), - to_be_set - ), - Err(Error::::SubnetDoesNotExist.into()) - ); - assert_eq!(SubtensorModule::get_min_allowed_weights(netuid), init_value); - assert_ok!(AdminUtils::sudo_set_min_allowed_weights( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!(SubtensorModule::get_min_allowed_weights(netuid), to_be_set); - }); -} - -#[test] -fn test_sudo_set_max_allowed_uids() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: u16 = 12; - add_network(netuid, 10); - MaxRegistrationsPerBlock::::insert(netuid, 256); - TargetRegistrationsPerInterval::::insert(netuid, 256); - - for i in 0..=8 { - let hotkey = U256::from(i * 1000); - let coldkey = U256::from(i * 1000 + i); - - let funds: u64 = 1_000_000_000_000_000; // 1,000,000 TAO (in RAO) - let _ = Balances::deposit_creating(&coldkey, Balance::from(funds)); - let _ = Balances::deposit_creating(&hotkey, Balance::from(funds)); // defensive - - register_ok_neuron(netuid, hotkey, coldkey, 0); - step_block(1); - } - - // Bad origin that is not root or subnet owner - assert_noop!( - AdminUtils::sudo_set_max_allowed_uids( - <::RuntimeOrigin>::signed(U256::from(42)), - netuid, - to_be_set - ), - DispatchError::BadOrigin - ); - - // Random netuid that doesn't exist - assert_noop!( - AdminUtils::sudo_set_max_allowed_uids( - <::RuntimeOrigin>::root(), - NetUid::from(42), - to_be_set - ), - Error::::SubnetDoesNotExist - ); - - // Trying to set max allowed uids less than min allowed uids - assert_noop!( - AdminUtils::sudo_set_max_allowed_uids( - <::RuntimeOrigin>::root(), - netuid, - SubtensorModule::get_min_allowed_uids(netuid) - 1 - ), - Error::::MaxAllowedUidsLessThanMinAllowedUids - ); - - // Trying to set max allowed uids less than current uids - assert_noop!( - AdminUtils::sudo_set_max_allowed_uids( - <::RuntimeOrigin>::root(), - netuid, - SubtensorModule::get_subnetwork_n(netuid) - 1 - ), - Error::::MaxAllowedUIdsLessThanCurrentUIds - ); - - // Trying to set max allowed uids greater than default max allowed uids - assert_noop!( - AdminUtils::sudo_set_max_allowed_uids( - <::RuntimeOrigin>::root(), - netuid, - DefaultMaxAllowedUids::::get() + 1 - ), - Error::::MaxAllowedUidsGreaterThanDefaultMaxAllowedUids - ); - - // Trying to set max allowed uids that would cause max_allowed_uids * mechanism_count > 256 - MaxAllowedUids::::insert(netuid, 8); - MechanismCountCurrent::::insert(netuid, MechId::from(32)); - let large_max_uids = 16; - assert_noop!( - AdminUtils::sudo_set_max_allowed_uids( - <::RuntimeOrigin>::root(), - netuid, - large_max_uids - ), - SubtensorError::::TooManyUIDsPerMechanism - ); - MechanismCountCurrent::::insert(netuid, MechId::from(1)); - - // Normal case - assert_ok!(AdminUtils::sudo_set_max_allowed_uids( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!(SubtensorModule::get_max_allowed_uids(netuid), to_be_set); - - // Exact current case - assert_ok!(AdminUtils::sudo_set_max_allowed_uids( - <::RuntimeOrigin>::root(), - netuid, - SubtensorModule::get_subnetwork_n(netuid) - )); - assert_eq!( - SubtensorModule::get_max_allowed_uids(netuid), - SubtensorModule::get_subnetwork_n(netuid) - ); - - // Lower bound case - SubtensorModule::set_min_allowed_uids(netuid, SubtensorModule::get_subnetwork_n(netuid)); - assert_ok!(AdminUtils::sudo_set_max_allowed_uids( - <::RuntimeOrigin>::root(), - netuid, - SubtensorModule::get_min_allowed_uids(netuid) - )); - assert_eq!( - SubtensorModule::get_max_allowed_uids(netuid), - SubtensorModule::get_min_allowed_uids(netuid) - ); - - // Upper bound case - assert_ok!(AdminUtils::sudo_set_max_allowed_uids( - <::RuntimeOrigin>::root(), - netuid, - DefaultMaxAllowedUids::::get(), - )); - assert_eq!( - SubtensorModule::get_max_allowed_uids(netuid), - DefaultMaxAllowedUids::::get() - ); - }); -} - -#[test] -fn test_sudo_set_kappa() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: u16 = 10; - add_network(netuid, 10); - let init_value: u16 = SubtensorModule::get_kappa(netuid); - assert_eq!( - AdminUtils::sudo_set_kappa( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!( - AdminUtils::sudo_set_kappa( - <::RuntimeOrigin>::root(), - netuid.next(), - to_be_set - ), - Err(Error::::SubnetDoesNotExist.into()) - ); - assert_eq!(SubtensorModule::get_kappa(netuid), init_value); - assert_ok!(AdminUtils::sudo_set_kappa( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!(SubtensorModule::get_kappa(netuid), to_be_set); - }); -} - -#[test] -fn test_sudo_set_rho() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: u16 = 10; - add_network(netuid, 10); - let init_value: u16 = SubtensorModule::get_rho(netuid); - assert_eq!( - AdminUtils::sudo_set_rho( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!( - AdminUtils::sudo_set_rho( - <::RuntimeOrigin>::root(), - netuid.next(), - to_be_set - ), - Err(Error::::SubnetDoesNotExist.into()) - ); - assert_eq!(SubtensorModule::get_rho(netuid), init_value); - assert_ok!(AdminUtils::sudo_set_rho( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!(SubtensorModule::get_rho(netuid), to_be_set); - }); -} - -#[test] -fn test_sudo_set_activity_cutoff() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: u16 = pallet_subtensor::MinActivityCutoff::::get(); - add_network(netuid, 10); - let init_value: u16 = SubtensorModule::get_activity_cutoff(netuid); - assert_eq!( - AdminUtils::sudo_set_activity_cutoff( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!( - AdminUtils::sudo_set_activity_cutoff( - <::RuntimeOrigin>::root(), - netuid.next(), - to_be_set - ), - Err(Error::::SubnetDoesNotExist.into()) - ); - assert_eq!(SubtensorModule::get_activity_cutoff(netuid), init_value); - assert_ok!(AdminUtils::sudo_set_activity_cutoff( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!(SubtensorModule::get_activity_cutoff(netuid), to_be_set); - }); -} - -#[test] -fn test_sudo_set_activity_cutoff_factor() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 10); - let owner = U256::from(5); - SubnetOwner::::insert(netuid, owner); - SubtensorModule::set_admin_freeze_window(0); - - // A non-owner signed origin is rejected. - assert_eq!( - AdminUtils::sudo_set_activity_cutoff_factor( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - 5_000 - ), - Err(DispatchError::BadOrigin) - ); - - // Out-of-bounds factors are rejected for owner and root alike. - assert_noop!( - AdminUtils::sudo_set_activity_cutoff_factor( - <::RuntimeOrigin>::root(), - netuid, - MAX_ACTIVITY_CUTOFF_FACTOR_MILLI + 1 - ), - SubtensorError::::ActivityCutoffFactorMilliOutOfBounds - ); - - // The owner can set a factor within bounds. - assert_ok!(AdminUtils::sudo_set_activity_cutoff_factor( - <::RuntimeOrigin>::signed(owner), - netuid, - 5_000 - )); - assert_eq!(ActivityCutoffFactorMilli::::get(netuid), 5_000); - - // A second owner change within the rate limit is rejected; root bypasses it. - assert_noop!( - AdminUtils::sudo_set_activity_cutoff_factor( - <::RuntimeOrigin>::signed(owner), - netuid, - 6_000 - ), - SubtensorError::::TxRateLimitExceeded - ); - assert_ok!(AdminUtils::sudo_set_activity_cutoff_factor( - <::RuntimeOrigin>::root(), - netuid, - 6_000 - )); - assert_eq!(ActivityCutoffFactorMilli::::get(netuid), 6_000); - }); -} - -#[test] -fn test_sudo_set_tempo_owner_and_root() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 10); - let owner = U256::from(5); - SubnetOwner::::insert(netuid, owner); - SubtensorModule::set_admin_freeze_window(0); - - // A non-owner signed origin is rejected. - assert_eq!( - AdminUtils::sudo_set_tempo( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - MIN_TEMPO - ), - Err(DispatchError::BadOrigin) - ); - - // A nonexistent subnet is rejected for root. - assert_noop!( - AdminUtils::sudo_set_tempo( - <::RuntimeOrigin>::root(), - netuid.next(), - MIN_TEMPO - ), - SubtensorError::::SubnetNotExists - ); - - // The owner is bounded to [MIN_TEMPO, MAX_TEMPO]. - assert_noop!( - AdminUtils::sudo_set_tempo( - <::RuntimeOrigin>::signed(owner), - netuid, - MIN_TEMPO - 1 - ), - SubtensorError::::TempoOutOfBounds - ); - assert_noop!( - AdminUtils::sudo_set_tempo( - <::RuntimeOrigin>::signed(owner), - netuid, - MAX_TEMPO + 1 - ), - SubtensorError::::TempoOutOfBounds - ); - - // Within bounds the owner change lands and resets the cycle. - assert_ok!(AdminUtils::sudo_set_tempo( - <::RuntimeOrigin>::signed(owner), - netuid, - MIN_TEMPO - )); - assert_eq!(Tempo::::get(netuid), MIN_TEMPO); - let now = SubtensorModule::get_current_block_as_u64(); - assert_eq!(LastEpochBlock::::get(netuid), now); - - // A second owner change within the MIN_TEMPO cooldown is rate-limited. - assert_noop!( - AdminUtils::sudo_set_tempo( - <::RuntimeOrigin>::signed(owner), - netuid, - MIN_TEMPO + 1 - ), - SubtensorError::::TxRateLimitExceeded - ); - - // Root bypasses the bounds and the rate limit. - assert_ok!(AdminUtils::sudo_set_tempo( - <::RuntimeOrigin>::root(), - netuid, - 10 - )); - assert_eq!(Tempo::::get(netuid), 10); - }); -} - -#[test] -fn test_sudo_set_target_registrations_per_interval() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: u16 = 10; - add_network(netuid, 10); - let init_value: u16 = SubtensorModule::get_target_registrations_per_interval(netuid); - assert_eq!( - AdminUtils::sudo_set_target_registrations_per_interval( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!( - AdminUtils::sudo_set_target_registrations_per_interval( - <::RuntimeOrigin>::root(), - netuid.next(), - to_be_set - ), - Err(Error::::SubnetDoesNotExist.into()) - ); - assert_eq!( - SubtensorModule::get_target_registrations_per_interval(netuid), - init_value - ); - assert_ok!(AdminUtils::sudo_set_target_registrations_per_interval( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!( - SubtensorModule::get_target_registrations_per_interval(netuid), - to_be_set - ); - }); -} - -#[test] -fn test_sudo_set_difficulty() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: u64 = 10; - add_network(netuid, 10); - let init_value: u64 = SubtensorModule::get_difficulty_as_u64(netuid); - assert_eq!( - AdminUtils::sudo_set_difficulty( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!( - AdminUtils::sudo_set_difficulty( - <::RuntimeOrigin>::root(), - netuid.next(), - to_be_set - ), - Err(Error::::SubnetDoesNotExist.into()) - ); - assert_eq!(SubtensorModule::get_difficulty_as_u64(netuid), init_value); - assert_ok!(AdminUtils::sudo_set_difficulty( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!(SubtensorModule::get_difficulty_as_u64(netuid), to_be_set); - - // Test that SN owner can't set difficulty - pallet_subtensor::SubnetOwner::::insert(netuid, U256::from(1)); - assert_eq!( - AdminUtils::sudo_set_difficulty( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - init_value - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!(SubtensorModule::get_difficulty_as_u64(netuid), to_be_set); // no change - }); -} - -#[test] -fn test_sudo_set_max_allowed_validators() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: u16 = 10; - add_network(netuid, 10); - let init_value: u16 = SubtensorModule::get_max_allowed_validators(netuid); - assert_eq!( - AdminUtils::sudo_set_max_allowed_validators( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!( - AdminUtils::sudo_set_max_allowed_validators( - <::RuntimeOrigin>::root(), - netuid.next(), - to_be_set - ), - Err(Error::::SubnetDoesNotExist.into()) - ); - assert_eq!( - SubtensorModule::get_max_allowed_validators(netuid), - init_value - ); - assert_ok!(AdminUtils::sudo_set_max_allowed_validators( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!( - SubtensorModule::get_max_allowed_validators(netuid), - to_be_set - ); - }); -} - -#[test] -fn test_sudo_set_stake_threshold() { - new_test_ext().execute_with(|| { - let to_be_set: u64 = 10; - let init_value: u64 = SubtensorModule::get_stake_threshold(); - assert_eq!( - AdminUtils::sudo_set_stake_threshold( - <::RuntimeOrigin>::signed(U256::from(1)), - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!(SubtensorModule::get_stake_threshold(), init_value); - assert_ok!(AdminUtils::sudo_set_stake_threshold( - <::RuntimeOrigin>::root(), - to_be_set - )); - assert_eq!(SubtensorModule::get_stake_threshold(), to_be_set); - }); -} - -#[test] -fn test_sudo_set_bonds_moving_average() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: u64 = 10; - add_network(netuid, 10); - let init_value: u64 = SubtensorModule::get_bonds_moving_average(netuid.into()); - assert_eq!( - AdminUtils::sudo_set_bonds_moving_average( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!( - AdminUtils::sudo_set_bonds_moving_average( - <::RuntimeOrigin>::root(), - netuid.next(), - to_be_set - ), - Err(Error::::SubnetDoesNotExist.into()) - ); - assert_eq!( - SubtensorModule::get_bonds_moving_average(netuid.into()), - init_value - ); - assert_ok!(AdminUtils::sudo_set_bonds_moving_average( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!( - SubtensorModule::get_bonds_moving_average(netuid.into()), - to_be_set - ); - }); -} - -#[test] -fn test_sudo_set_bonds_penalty() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: u16 = 10; - add_network(netuid, 10); - let init_value: u16 = SubtensorModule::get_bonds_penalty(netuid); - assert_eq!( - AdminUtils::sudo_set_bonds_penalty( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!( - AdminUtils::sudo_set_bonds_penalty( - <::RuntimeOrigin>::root(), - netuid.next(), - to_be_set - ), - Err(Error::::SubnetDoesNotExist.into()) - ); - assert_eq!(SubtensorModule::get_bonds_penalty(netuid), init_value); - assert_ok!(AdminUtils::sudo_set_bonds_penalty( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!(SubtensorModule::get_bonds_penalty(netuid), to_be_set); - }); -} - -#[test] -fn test_sudo_set_rao_recycled() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set = TaoBalance::from(10); - add_network(netuid, 10); - let init_value = SubtensorModule::get_rao_recycled(netuid); - - // Need to run from genesis block - run_to_block(1); - - assert_eq!( - AdminUtils::sudo_set_rao_recycled( - <::RuntimeOrigin>::signed(U256::from(0)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!( - AdminUtils::sudo_set_rao_recycled( - <::RuntimeOrigin>::root(), - netuid.next(), - to_be_set - ), - Err(Error::::SubnetDoesNotExist.into()) - ); - assert_eq!(SubtensorModule::get_rao_recycled(netuid), init_value); - - // Verify no events emitted matching the expected event - assert_eq!( - System::events() - .iter() - .filter(|r| r.event - == RuntimeEvent::SubtensorModule(Event::RAORecycledForRegistrationSet( - netuid, to_be_set - ))) - .count(), - 0 - ); - - assert_ok!(AdminUtils::sudo_set_rao_recycled( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!(SubtensorModule::get_rao_recycled(netuid), to_be_set); - - // Verify event emitted with correct values - assert_eq!( - System::events() - .last() - .unwrap_or_else(|| panic!( - "Expected there to be events: {:?}", - System::events().to_vec() - )) - .event, - RuntimeEvent::SubtensorModule(Event::RAORecycledForRegistrationSet(netuid, to_be_set)) - ); - }); -} - -#[test] -fn test_sudo_set_network_lock_reduction_interval() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: u64 = 7200; - add_network(netuid, 10); - - let init_value: u64 = SubtensorModule::get_lock_reduction_interval(); - assert_eq!( - AdminUtils::sudo_set_lock_reduction_interval( - <::RuntimeOrigin>::signed(U256::from(1)), - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!(SubtensorModule::get_lock_reduction_interval(), init_value); - assert_ok!(AdminUtils::sudo_set_lock_reduction_interval( - <::RuntimeOrigin>::root(), - to_be_set - )); - assert_eq!(SubtensorModule::get_lock_reduction_interval(), to_be_set); - }); -} - -#[test] -fn test_sudo_set_network_pow_registration_allowed() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: bool = true; - add_network(netuid, 10); - - assert_eq!( - AdminUtils::sudo_set_network_pow_registration_allowed( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(Error::::POWRegistrationDisabled.into()) - ); - }); -} - -mod sudo_set_nominator_min_required_stake { - use super::*; - - #[test] - fn can_only_be_called_by_admin() { - new_test_ext().execute_with(|| { - let to_be_set = SubtensorModule::get_nominator_min_required_stake() + 5; - assert_eq!( - AdminUtils::sudo_set_nominator_min_required_stake( - <::RuntimeOrigin>::signed(U256::from(0)), - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - }); - } - - #[test] - fn sets_a_lower_value() { - new_test_ext().execute_with(|| { - assert_ok!(AdminUtils::sudo_set_nominator_min_required_stake( - <::RuntimeOrigin>::root(), - 10 - )); - let default_min_stake = pallet_subtensor::DefaultMinStake::::get(); - assert_eq!( - SubtensorModule::get_nominator_min_required_stake(), - 10 * default_min_stake.to_u64() / 1_000_000 - ); - - assert_ok!(AdminUtils::sudo_set_nominator_min_required_stake( - <::RuntimeOrigin>::root(), - 5 - )); - assert_eq!( - SubtensorModule::get_nominator_min_required_stake(), - 5 * default_min_stake.to_u64() / 1_000_000 - ); - }); - } - - #[test] - fn sets_a_higher_value() { - new_test_ext().execute_with(|| { - let to_be_set = SubtensorModule::get_nominator_min_required_stake() + 5; - let default_min_stake = pallet_subtensor::DefaultMinStake::::get(); - assert_ok!(AdminUtils::sudo_set_nominator_min_required_stake( - <::RuntimeOrigin>::root(), - to_be_set - )); - assert_eq!( - SubtensorModule::get_nominator_min_required_stake(), - to_be_set * default_min_stake.to_u64() / 1_000_000 - ); - }); - } -} - -#[test] -fn test_sudo_set_tx_delegate_take_rate_limit() { - new_test_ext().execute_with(|| { - let to_be_set: u64 = 10; - let init_value: u64 = SubtensorModule::get_tx_delegate_take_rate_limit(); - assert_eq!( - AdminUtils::sudo_set_tx_delegate_take_rate_limit( - <::RuntimeOrigin>::signed(U256::from(1)), - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!( - SubtensorModule::get_tx_delegate_take_rate_limit(), - init_value - ); - assert_ok!(AdminUtils::sudo_set_tx_delegate_take_rate_limit( - <::RuntimeOrigin>::root(), - to_be_set - )); - assert_eq!( - SubtensorModule::get_tx_delegate_take_rate_limit(), - to_be_set - ); - }); -} - -#[test] -fn test_sudo_set_min_delegate_take() { - new_test_ext().execute_with(|| { - let to_be_set = PerU16::from_parts(u16::MAX / 100); - let init_value = SubtensorModule::get_min_delegate_take(); - assert_eq!( - AdminUtils::sudo_set_min_delegate_take( - <::RuntimeOrigin>::signed(U256::from(1)), - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!(SubtensorModule::get_min_delegate_take(), init_value); - assert_ok!(AdminUtils::sudo_set_min_delegate_take( - <::RuntimeOrigin>::root(), - to_be_set - )); - assert_eq!( - SubtensorModule::get_min_delegate_take(), - to_be_set.deconstruct() - ); - }); -} - -#[test] -fn test_sudo_set_min_childkey_take_per_subnet() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let owner = U256::from(10); - let non_owner = U256::from(11); - let take = PerU16::from_parts(SubtensorModule::get_max_childkey_take() / 2); - - add_network(netuid, 10); - SubnetOwner::::insert(netuid, owner); - - assert_eq!( - AdminUtils::sudo_set_min_childkey_take_per_subnet( - <::RuntimeOrigin>::signed(non_owner), - netuid, - take - ), - Err(DispatchError::BadOrigin) - ); - - assert_ok!(AdminUtils::sudo_set_min_childkey_take_per_subnet( - <::RuntimeOrigin>::signed(owner), - netuid, - take - )); - assert_eq!( - SubtensorModule::get_min_childkey_take_for_subnet(netuid), - take.deconstruct() - ); - assert_eq!( - SubtensorModule::get_effective_min_childkey_take(netuid), - take.deconstruct() - ); - }); -} - -#[test] -fn test_sudo_set_min_childkey_take_per_subnet_rejects_below_global() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let global_min: u16 = 100; - - add_network(netuid, 10); - SubtensorModule::set_min_childkey_take(PerU16::from_parts(global_min)); - - assert_noop!( - AdminUtils::sudo_set_min_childkey_take_per_subnet( - <::RuntimeOrigin>::root(), - netuid, - PerU16::from_parts(global_min - 1) - ), - Error::::InvalidValue - ); - assert_ok!(AdminUtils::sudo_set_min_childkey_take_per_subnet( - <::RuntimeOrigin>::root(), - netuid, - PerU16::from_parts(global_min) - )); - }); -} - -#[test] -fn test_sudo_set_commit_reveal_weights_enabled() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 10); - - let to_be_set: bool = false; - let init_value: bool = SubtensorModule::get_commit_reveal_weights_enabled(netuid); - - assert_ok!(AdminUtils::sudo_set_commit_reveal_weights_enabled( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - - assert!(init_value != to_be_set); - assert_eq!( - SubtensorModule::get_commit_reveal_weights_enabled(netuid), - to_be_set - ); - }); -} - -#[test] -fn test_sudo_set_liquid_alpha_enabled() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let enabled: bool = true; - NetworksAdded::::insert(netuid, true); - assert_eq!(!enabled, SubtensorModule::get_liquid_alpha_enabled(netuid)); - - assert_ok!(AdminUtils::sudo_set_liquid_alpha_enabled( - <::RuntimeOrigin>::root(), - netuid, - enabled - )); - - assert_eq!(enabled, SubtensorModule::get_liquid_alpha_enabled(netuid)); - }); -} - -#[test] -fn test_sudo_set_alpha_sigmoid_steepness() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: i16 = 5000; - add_network(netuid, 10); - let init_value = SubtensorModule::get_alpha_sigmoid_steepness(netuid); - assert_eq!( - AdminUtils::sudo_set_alpha_sigmoid_steepness( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - assert_eq!( - AdminUtils::sudo_set_alpha_sigmoid_steepness( - <::RuntimeOrigin>::root(), - netuid.next(), - to_be_set - ), - Err(Error::::SubnetDoesNotExist.into()) - ); - - let owner = U256::from(10); - pallet_subtensor::SubnetOwner::::insert(netuid, owner); - assert_eq!( - AdminUtils::sudo_set_alpha_sigmoid_steepness( - <::RuntimeOrigin>::signed(owner), - netuid, - -to_be_set - ), - Err(Error::::NegativeSigmoidSteepness.into()) - ); - assert_eq!( - SubtensorModule::get_alpha_sigmoid_steepness(netuid), - init_value - ); - assert_ok!(AdminUtils::sudo_set_alpha_sigmoid_steepness( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!( - SubtensorModule::get_alpha_sigmoid_steepness(netuid), - to_be_set - ); - assert_ok!(AdminUtils::sudo_set_alpha_sigmoid_steepness( - <::RuntimeOrigin>::root(), - netuid, - -to_be_set - )); - assert_eq!( - SubtensorModule::get_alpha_sigmoid_steepness(netuid), - -to_be_set - ); - }); -} - -#[test] -fn test_set_alpha_values_dispatch_info_ok() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let alpha_low: u16 = 1638_u16; - let alpha_high: u16 = u16::MAX - 10; - let call = RuntimeCall::AdminUtils(crate::Call::sudo_set_alpha_values { - netuid, - alpha_low, - alpha_high, - }); - - let dispatch_info = call.get_dispatch_info(); - - assert_eq!(dispatch_info.class, DispatchClass::Normal); - assert_eq!(dispatch_info.pays_fee, Pays::Yes); - }); -} - -#[test] -fn test_sudo_get_set_alpha() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let alpha_low: u16 = 1638_u16; - let alpha_high: u16 = u16::MAX - 10; - - let hotkey: U256 = U256::from(1); - let coldkey: U256 = U256::from(1 + 456); - let signer = <::RuntimeOrigin>::signed(coldkey); - - // Enable Liquid Alpha and setup - SubtensorModule::set_liquid_alpha_enabled(netuid, true); - pallet_subtensor::migrations::migrate_create_root_network::migrate_create_root_network::< - Test, - >(); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_000_u64.into()); - assert_ok!(SubtensorModule::root_register(signer.clone(), hotkey,)); - - // Should fail as signer does not own the subnet - assert_err!( - AdminUtils::sudo_set_alpha_values(signer.clone(), netuid, alpha_low, alpha_high), - DispatchError::BadOrigin - ); - - assert_ok!(SubtensorModule::register_network(signer.clone(), hotkey)); - - assert_ok!(AdminUtils::sudo_set_alpha_values( - signer.clone(), - netuid, - alpha_low, - alpha_high - )); - let (grabbed_alpha_low, grabbed_alpha_high): (u16, u16) = - SubtensorModule::get_alpha_values(netuid); - - log::info!("alpha_low: {grabbed_alpha_low:?} alpha_high: {grabbed_alpha_high:?}"); - assert_eq!(grabbed_alpha_low, alpha_low); - assert_eq!(grabbed_alpha_high, alpha_high); - - // Convert the u16 values to decimal values - fn unnormalize_u16_to_float(normalized_value: u16) -> f32 { - const MAX_U16: u16 = 65535; - normalized_value as f32 / MAX_U16 as f32 - } - - let alpha_low_decimal = unnormalize_u16_to_float(alpha_low); - let alpha_high_decimal = unnormalize_u16_to_float(alpha_high); - - let (alpha_low_32, alpha_high_32) = SubtensorModule::get_alpha_values_32(netuid); - - let tolerance: f32 = 1e-6; // 0.000001 - - // Check if the values are equal to the sixth decimal - assert!( - (alpha_low_32.to_num::() - alpha_low_decimal).abs() < tolerance, - "alpha_low mismatch: {} != {}", - alpha_low_32.to_num::(), - alpha_low_decimal - ); - assert!( - (alpha_high_32.to_num::() - alpha_high_decimal).abs() < tolerance, - "alpha_high mismatch: {} != {}", - alpha_high_32.to_num::(), - alpha_high_decimal - ); - - // 1. Liquid alpha disabled - SubtensorModule::set_liquid_alpha_enabled(netuid, false); - assert_err!( - AdminUtils::sudo_set_alpha_values(signer.clone(), netuid, alpha_low, alpha_high), - SubtensorError::::LiquidAlphaDisabled - ); - // Correct scenario after error - SubtensorModule::set_liquid_alpha_enabled(netuid, true); // Re-enable for further tests - assert_ok!(AdminUtils::sudo_set_alpha_values( - signer.clone(), - netuid, - alpha_low, - alpha_high - )); - - // 2. Alpha high too low - let alpha_high_too_low = (u16::MAX as u32 / 40) as u16 - 1; // One less than the minimum acceptable value - assert_err!( - AdminUtils::sudo_set_alpha_values( - signer.clone(), - netuid, - alpha_low, - alpha_high_too_low - ), - SubtensorError::::AlphaHighTooLow - ); - // Correct scenario after error - assert_ok!(AdminUtils::sudo_set_alpha_values( - signer.clone(), - netuid, - alpha_low, - alpha_high - )); - - // 3. Alpha low too low or too high - let alpha_low_too_low = 0_u16; - assert_err!( - AdminUtils::sudo_set_alpha_values( - signer.clone(), - netuid, - alpha_low_too_low, - alpha_high - ), - SubtensorError::::AlphaLowOutOfRange - ); - // Correct scenario after error - assert_ok!(AdminUtils::sudo_set_alpha_values( - signer.clone(), - netuid, - alpha_low, - alpha_high - )); - - let alpha_low_too_high = alpha_high + 1; - assert_err!( - AdminUtils::sudo_set_alpha_values( - signer.clone(), - netuid, - alpha_low_too_high, - alpha_high - ), - SubtensorError::::AlphaLowOutOfRange - ); - // Correct scenario after error - assert_ok!(AdminUtils::sudo_set_alpha_values( - signer.clone(), - netuid, - alpha_low, - alpha_high - )); - }); -} - -#[test] -fn test_sudo_set_coldkey_swap_announcement_delay() { - new_test_ext().execute_with(|| { - // Arrange - let root = RuntimeOrigin::root(); - let non_root = RuntimeOrigin::signed(U256::from(1)); - let new_delay = 100u32.into(); - - // Act & Assert: Non-root account should fail - assert_noop!( - AdminUtils::sudo_set_coldkey_swap_announcement_delay(non_root, new_delay), - DispatchError::BadOrigin - ); - - // Act: Root account should succeed - assert_ok!(AdminUtils::sudo_set_coldkey_swap_announcement_delay( - root.clone(), - new_delay - )); - - // Assert: Check if the delay was actually set - assert_eq!( - pallet_subtensor::ColdkeySwapAnnouncementDelay::::get(), - new_delay - ); - - // Act & Assert: Setting the same value again should succeed (idempotent operation) - assert_ok!(AdminUtils::sudo_set_coldkey_swap_announcement_delay( - root, new_delay - )); - - // You might want to check for events here if your pallet emits them - System::assert_last_event(Event::ColdkeySwapAnnouncementDelaySet(new_delay).into()); - }); -} - -#[test] -fn test_sudo_set_coldkey_swap_reannouncement_delay() { - new_test_ext().execute_with(|| { - // Arrange - let root = RuntimeOrigin::root(); - let non_root = RuntimeOrigin::signed(U256::from(1)); - let new_delay = 100u32.into(); - - // Act & Assert: Non-root account should fail - assert_noop!( - AdminUtils::sudo_set_coldkey_swap_reannouncement_delay(non_root, new_delay), - DispatchError::BadOrigin - ); - - // Act: Root account should succeed - assert_ok!(AdminUtils::sudo_set_coldkey_swap_reannouncement_delay( - root.clone(), - new_delay - )); - - // Assert: Check if the delay was actually set - assert_eq!( - pallet_subtensor::ColdkeySwapReannouncementDelay::::get(), - new_delay - ); - - // Act & Assert: Setting the same value again should succeed (idempotent operation) - assert_ok!(AdminUtils::sudo_set_coldkey_swap_reannouncement_delay( - root, new_delay - )); - - // You might want to check for events here if your pallet emits them - System::assert_last_event(Event::ColdkeySwapReannouncementDelaySet(new_delay).into()); - }); -} - -#[test] -fn test_sudo_set_max_epochs_per_block() { - new_test_ext().execute_with(|| { - let root = RuntimeOrigin::root(); - let non_root = RuntimeOrigin::signed(U256::from(1)); - let init_value = SubtensorModule::get_max_epochs_per_block(); - let to_be_set: u8 = init_value.saturating_add(3); - - // Non-root is rejected and leaves the value untouched. - assert_noop!( - AdminUtils::sudo_set_max_epochs_per_block(non_root, to_be_set), - DispatchError::BadOrigin - ); - assert_eq!(SubtensorModule::get_max_epochs_per_block(), init_value); - - // Zero is rejected by the `>= 1` guard (a zero cap would halt all subnet epochs). - assert_noop!( - AdminUtils::sudo_set_max_epochs_per_block(root.clone(), 0u8), - Error::::ValueNotInBounds - ); - assert_eq!(SubtensorModule::get_max_epochs_per_block(), init_value); - - // Root succeeds: storage is updated and the event is emitted. - assert_ok!(AdminUtils::sudo_set_max_epochs_per_block(root, to_be_set)); - assert_eq!(SubtensorModule::get_max_epochs_per_block(), to_be_set); - System::assert_last_event(Event::MaxEpochsPerBlockSet(to_be_set).into()); - }); -} - -#[test] -fn test_sudo_set_max_epochs_per_block_changes_deferrals() { - new_test_ext().execute_with(|| { - let root = RuntimeOrigin::root(); - - // Create several subnets and force each to be "due this block". - let created: u16 = 4; - for i in 0..created { - let netuid = NetUid::from(i + 1); - add_network(netuid, 100 /*tempo*/); - pallet_subtensor::PendingEpochAt::::insert(netuid, 1); - } - - let block = SubtensorModule::get_current_block_as_u64(); - let subnets: Vec = SubtensorModule::get_all_subnet_netuids() - .into_iter() - .filter(|x| *x != NetUid::ROOT) - .collect(); - let due = subnets - .iter() - .filter(|n| SubtensorModule::should_run_epoch(**n, block)) - .count(); - assert!(due >= created as usize); - - // Tight cap (1): every due subnet beyond the first is deferred. - assert_ok!(AdminUtils::sudo_set_max_epochs_per_block(root.clone(), 1u8)); - let deferred_tight = SubtensorModule::epochs_deferred_this_block(&subnets, block).len(); - assert_eq!(deferred_tight, due.saturating_sub(1)); - - // Raising the cap above the due count clears all deferrals — proving the - // admin-set cap directly drives which epochs are deferred. - assert_ok!(AdminUtils::sudo_set_max_epochs_per_block( - root, - (due as u8).saturating_add(2) - )); - let deferred_loose = SubtensorModule::epochs_deferred_this_block(&subnets, block).len(); - assert_eq!(deferred_loose, 0); - assert!( - deferred_loose < deferred_tight, - "raising MaxEpochsPerBlock must defer fewer epochs" - ); - }); -} - -#[test] -fn test_sudo_set_dissolve_network_schedule_duration() { - new_test_ext().execute_with(|| { - // Arrange - let root = RuntimeOrigin::root(); - let non_root = RuntimeOrigin::signed(U256::from(1)); - let new_duration = 200u32.into(); - - // Act & Assert: Non-root account should fail - assert_noop!( - AdminUtils::sudo_set_dissolve_network_schedule_duration(non_root, new_duration), - DispatchError::BadOrigin - ); - - // Act: Root account should succeed - assert_ok!(AdminUtils::sudo_set_dissolve_network_schedule_duration( - root.clone(), - new_duration - )); - - // Assert: Check if the duration was actually set - assert_eq!( - pallet_subtensor::DissolveNetworkScheduleDuration::::get(), - new_duration - ); - - // Act & Assert: Setting the same value again should succeed (idempotent operation) - assert_ok!(AdminUtils::sudo_set_dissolve_network_schedule_duration( - root, - new_duration - )); - - // You might want to check for events here if your pallet emits them - System::assert_last_event(Event::DissolveNetworkScheduleDurationSet(new_duration).into()); - }); -} - -#[test] -fn sudo_set_commit_reveal_weights_interval() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 10); - - let too_high = 101; - assert_err!( - AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::root(), - netuid, - too_high - ), - pallet_subtensor::Error::::RevealPeriodTooLarge - ); - - let to_be_set = 55; - let init_value = SubtensorModule::get_reveal_period(netuid); - - assert_ok!(AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - - assert!(init_value != to_be_set); - assert_eq!(SubtensorModule::get_reveal_period(netuid), to_be_set); - }); -} - -#[test] -fn test_sudo_root_sets_evm_chain_id() { - new_test_ext().execute_with(|| { - let chain_id: u64 = 945; - assert_eq!(pallet_evm_chain_id::ChainId::::get(), 0); - - assert_ok!(AdminUtils::sudo_set_evm_chain_id( - <::RuntimeOrigin>::root(), - chain_id - )); - - assert_eq!(pallet_evm_chain_id::ChainId::::get(), chain_id); - }); -} - -#[test] -fn test_sudo_non_root_cannot_set_evm_chain_id() { - new_test_ext().execute_with(|| { - let chain_id: u64 = 945; - assert_eq!(pallet_evm_chain_id::ChainId::::get(), 0); - - assert_eq!( - AdminUtils::sudo_set_evm_chain_id( - <::RuntimeOrigin>::signed(U256::from(0)), - chain_id - ), - Err(DispatchError::BadOrigin) - ); - - assert_eq!(pallet_evm_chain_id::ChainId::::get(), 0); - }); -} - -#[test] -fn test_schedule_grandpa_change() { - new_test_ext().execute_with(|| { - assert_eq!(Grandpa::grandpa_authorities(), vec![]); - - let bob: GrandpaId = ed25519::Pair::from_legacy_string("//Bob", None) - .public() - .into(); - - assert_ok!(AdminUtils::schedule_grandpa_change( - RuntimeOrigin::root(), - vec![(bob.clone(), 1)], - 41, - None - )); - - Grandpa::on_finalize(42); - - assert_eq!(Grandpa::grandpa_authorities(), vec![(bob, 1)]); - }); -} - -#[test] -fn test_sudo_toggle_evm_precompile() { - new_test_ext().execute_with(|| { - let precompile_id = crate::PrecompileEnum::BalanceTransfer; - let initial_enabled = PrecompileEnable::::get(precompile_id); - assert!(initial_enabled); // Assuming the default is true - - run_to_block(1); - - assert_eq!( - AdminUtils::sudo_toggle_evm_precompile( - <::RuntimeOrigin>::signed(U256::from(0)), - precompile_id, - false - ), - Err(DispatchError::BadOrigin) - ); - - assert_ok!(AdminUtils::sudo_toggle_evm_precompile( - RuntimeOrigin::root(), - precompile_id, - false - )); +//! Unit tests for `pallet-admin-utils`, split by concept for discoverability. +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)] - assert_eq!( - System::events() - .iter() - .filter(|r| r.event - == RuntimeEvent::AdminUtils(crate::Event::PrecompileUpdated { - precompile_id, - enabled: false - })) - .count(), - 1 - ); - - let updated_enabled = PrecompileEnable::::get(precompile_id); - assert!(!updated_enabled); - - run_to_block(2); - - assert_ok!(AdminUtils::sudo_toggle_evm_precompile( - RuntimeOrigin::root(), - precompile_id, - false - )); - - // no event without status change - assert_eq!( - System::events() - .iter() - .filter(|r| r.event - == RuntimeEvent::AdminUtils(crate::Event::PrecompileUpdated { - precompile_id, - enabled: false - })) - .count(), - 0 - ); - - assert_ok!(AdminUtils::sudo_toggle_evm_precompile( - RuntimeOrigin::root(), - precompile_id, - true - )); - - let final_enabled = PrecompileEnable::::get(precompile_id); - assert!(final_enabled); - }); -} - -#[test] -fn test_sudo_root_sets_subnet_moving_alpha() { - new_test_ext().execute_with(|| { - let alpha: I96F32 = I96F32::saturating_from_num(0.5); - let initial = pallet_subtensor::SubnetMovingAlpha::::get(); - assert!(initial != alpha); - - assert_ok!(AdminUtils::sudo_set_subnet_moving_alpha( - <::RuntimeOrigin>::root(), - alpha - )); - - assert_eq!(pallet_subtensor::SubnetMovingAlpha::::get(), alpha); - }); -} - -#[test] -fn test_sets_a_lower_value_clears_small_nominations() { - new_test_ext().execute_with(|| { - let hotkey: U256 = U256::from(3); - let owner_coldkey: U256 = U256::from(1); - let staker_coldkey: U256 = U256::from(2); - - let initial_nominator_min_required_stake = 10; - let nominator_min_required_stake_0 = 5; - let nominator_min_required_stake_1 = 20; - - assert!(nominator_min_required_stake_0 < nominator_min_required_stake_1); - assert!(nominator_min_required_stake_0 < initial_nominator_min_required_stake); - - let to_stake = initial_nominator_min_required_stake + 1; - - assert!(to_stake > initial_nominator_min_required_stake); - assert!(to_stake > nominator_min_required_stake_0); // Should stay when set - assert!(to_stake < nominator_min_required_stake_1); // Should be removed when set - - // ---- FIX: fund accounts so burn-based registration + staking doesn't fail. - let funds: u64 = 1_000_000_000_000_000; // 1,000,000 TAO (in RAO) - let _ = Balances::deposit_creating(&owner_coldkey, Balance::from(funds)); - let _ = Balances::deposit_creating(&staker_coldkey, Balance::from(funds)); - let _ = Balances::deposit_creating(&hotkey, Balance::from(funds)); // defensive - - // Create network - let netuid = NetUid::from(2); - add_network(netuid, 10); - - // Register a neuron - register_ok_neuron(netuid, hotkey, owner_coldkey, 0); - - let default_min_stake = pallet_subtensor::DefaultMinStake::::get(); - assert_ok!(AdminUtils::sudo_set_nominator_min_required_stake( - RuntimeOrigin::root(), - initial_nominator_min_required_stake - )); - assert_eq!( - SubtensorModule::get_nominator_min_required_stake(), - initial_nominator_min_required_stake * default_min_stake.to_u64() / 1_000_000 - ); - - // Stake to the hotkey as staker_coldkey - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &staker_coldkey, - netuid, - to_stake.into(), - ); - - let default_min_stake = pallet_subtensor::DefaultMinStake::::get(); - assert_ok!(AdminUtils::sudo_set_nominator_min_required_stake( - RuntimeOrigin::root(), - nominator_min_required_stake_0 - )); - assert_eq!( - SubtensorModule::get_nominator_min_required_stake(), - nominator_min_required_stake_0 * default_min_stake.to_u64() / 1_000_000 - ); - - // Check this nomination is not cleared - assert!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &staker_coldkey, - netuid - ) > 0.into() - ); - - assert_ok!(AdminUtils::sudo_set_nominator_min_required_stake( - RuntimeOrigin::root(), - nominator_min_required_stake_1 - )); - assert_eq!( - SubtensorModule::get_nominator_min_required_stake(), - nominator_min_required_stake_1 * default_min_stake.to_u64() / 1_000_000 - ); - - // Check this nomination is cleared - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &staker_coldkey, - netuid - ), - 0.into() - ); - }); -} - -// cargo test --package pallet-admin-utils --lib -- tests::test_sudo_set_ema_halving --exact --show-output -#[test] -fn test_sudo_set_ema_halving() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: u64 = 10; - add_network(netuid, 10); - - let value_before: u64 = pallet_subtensor::EMAPriceHalvingBlocks::::get(netuid); - assert_eq!( - AdminUtils::sudo_set_ema_price_halving_period( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - let value_after_0: u64 = pallet_subtensor::EMAPriceHalvingBlocks::::get(netuid); - assert_eq!(value_after_0, value_before); - - let owner = U256::from(10); - pallet_subtensor::SubnetOwner::::insert(netuid, owner); - assert_eq!( - AdminUtils::sudo_set_ema_price_halving_period( - <::RuntimeOrigin>::signed(owner), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - let value_after_1: u64 = pallet_subtensor::EMAPriceHalvingBlocks::::get(netuid); - assert_eq!(value_after_1, value_before); - assert_ok!(AdminUtils::sudo_set_ema_price_halving_period( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - let value_after_2: u64 = pallet_subtensor::EMAPriceHalvingBlocks::::get(netuid); - assert_eq!(value_after_2, to_be_set); - }); -} - -// cargo test --package pallet-admin-utils --lib -- tests::test_set_sn_owner_hotkey --exact --show-output -#[test] -fn test_set_sn_owner_hotkey_owner() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let hotkey: U256 = U256::from(3); - let bad_origin_coldkey: U256 = U256::from(4); - add_network(netuid, 10); - - let owner = U256::from(10); - pallet_subtensor::SubnetOwner::::insert(netuid, owner); - - // Non-owner and non-root cannot set the sn owner hotkey - assert_eq!( - AdminUtils::sudo_set_sn_owner_hotkey( - <::RuntimeOrigin>::signed(bad_origin_coldkey), - netuid, - hotkey - ), - Err(DispatchError::BadOrigin) - ); - - // SN owner can set the hotkey - assert_ok!(AdminUtils::sudo_set_sn_owner_hotkey( - <::RuntimeOrigin>::signed(owner), - netuid, - hotkey - )); - - // Check the value - let actual_hotkey = pallet_subtensor::SubnetOwnerHotkey::::get(netuid); - assert_eq!(actual_hotkey, hotkey); - - // Cannot set again (rate limited) - assert_err!( - AdminUtils::sudo_set_sn_owner_hotkey( - <::RuntimeOrigin>::signed(owner), - netuid, - hotkey - ), - pallet_subtensor::Error::::TxRateLimitExceeded - ); - }); -} - -// cargo test --package pallet-admin-utils --lib -- tests::test_set_sn_owner_hotkey_root --exact --show-output -#[test] -fn test_set_sn_owner_hotkey_root() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let hotkey: U256 = U256::from(3); - add_network(netuid, 10); - - let owner = U256::from(10); - pallet_subtensor::SubnetOwner::::insert(netuid, owner); - - // Root can set the hotkey - assert_ok!(AdminUtils::sudo_set_sn_owner_hotkey( - <::RuntimeOrigin>::root(), - netuid, - hotkey - )); - - // Check the value - let actual_hotkey = pallet_subtensor::SubnetOwnerHotkey::::get(netuid); - assert_eq!(actual_hotkey, hotkey); - }); -} - -#[test] -fn test_sudo_set_bonds_reset_enabled() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: bool = true; - let sn_owner = U256::from(1); - add_network(netuid, 10); - let init_value: bool = SubtensorModule::get_bonds_reset(netuid); - - assert_eq!( - AdminUtils::sudo_set_bonds_reset_enabled( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - - assert_ok!(AdminUtils::sudo_set_bonds_reset_enabled( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!(SubtensorModule::get_bonds_reset(netuid), to_be_set); - assert_ne!(SubtensorModule::get_bonds_reset(netuid), init_value); - - pallet_subtensor::SubnetOwner::::insert(netuid, sn_owner); - - assert_ok!(AdminUtils::sudo_set_bonds_reset_enabled( - <::RuntimeOrigin>::signed(sn_owner), - netuid, - !to_be_set - )); - assert_eq!(SubtensorModule::get_bonds_reset(netuid), !to_be_set); - }); -} - -#[test] -fn test_sudo_set_yuma3_enabled() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: bool = false; - let sn_owner = U256::from(1); - add_network(netuid, 10); - let init_value: bool = SubtensorModule::get_yuma3_enabled(netuid); - - assert_eq!( - AdminUtils::sudo_set_yuma3_enabled( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - to_be_set - ), - Err(DispatchError::BadOrigin) - ); - - assert_ok!(AdminUtils::sudo_set_yuma3_enabled( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!(SubtensorModule::get_yuma3_enabled(netuid), to_be_set); - assert_ne!(SubtensorModule::get_yuma3_enabled(netuid), init_value); - - pallet_subtensor::SubnetOwner::::insert(netuid, sn_owner); - - assert_ok!(AdminUtils::sudo_set_yuma3_enabled( - <::RuntimeOrigin>::signed(sn_owner), - netuid, - !to_be_set - )); - assert_eq!(SubtensorModule::get_yuma3_enabled(netuid), !to_be_set); - }); -} - -#[test] -fn test_sudo_set_commit_reveal_version() { - new_test_ext().execute_with(|| { - add_network(NetUid::from(1), 10); - - let to_be_set: u16 = 5; - let init_value: u16 = SubtensorModule::get_commit_reveal_weights_version(); - - assert_ok!(AdminUtils::sudo_set_commit_reveal_version( - <::RuntimeOrigin>::root(), - to_be_set - )); - - assert!(init_value != to_be_set); - assert_eq!( - SubtensorModule::get_commit_reveal_weights_version(), - to_be_set - ); - }); -} - -#[test] -fn test_sudo_set_admin_freeze_window_and_rate() { - new_test_ext().execute_with(|| { - // Non-root fails - assert_eq!( - AdminUtils::sudo_set_admin_freeze_window( - <::RuntimeOrigin>::signed(U256::from(1)), - 7 - ), - Err(DispatchError::BadOrigin) - ); - // Root succeeds - assert_ok!(AdminUtils::sudo_set_admin_freeze_window( - <::RuntimeOrigin>::root(), - 7 - )); - assert_eq!(pallet_subtensor::AdminFreezeWindow::::get(), 7); - - // Owner hyperparam tempos setter - assert_eq!( - AdminUtils::sudo_set_owner_hparam_rate_limit( - <::RuntimeOrigin>::signed(U256::from(1)), - 5 - ), - Err(DispatchError::BadOrigin) - ); - assert_ok!(AdminUtils::sudo_set_owner_hparam_rate_limit( - <::RuntimeOrigin>::root(), - 5 - )); - assert_eq!(pallet_subtensor::OwnerHyperparamRateLimit::::get(), 5); - }); -} - -#[test] -fn test_freeze_window_blocks_root_and_owner() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let tempo: u16 = 10; - // Create subnet with tempo 10 - add_network(netuid, tempo); - // Set freeze window to 3 blocks - assert_ok!(AdminUtils::sudo_set_admin_freeze_window( - <::RuntimeOrigin>::root(), - 3 - )); - // Pin the state-based scheduler so the next auto-epoch lands at - // `LastEpochBlock + tempo`. Freeze window covers blocks (next_auto - 3, next_auto]. - pallet_subtensor::LastEpochBlock::::insert(netuid, 0); - let next_auto = tempo as u64; - // Advance to a block inside the freeze window (remaining < 3). - run_to_block(next_auto - 2); - - // Root should be blocked during freeze window - assert_noop!( - AdminUtils::sudo_set_min_burn( - <::RuntimeOrigin>::root(), - netuid, - 123.into() - ), - SubtensorError::::AdminActionProhibitedDuringWeightsWindow - ); - - // Owner should be blocked during freeze window as well - // Set owner - let owner: U256 = U256::from(9); - SubnetOwner::::insert(netuid, owner); - assert_noop!( - AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::signed(owner), - netuid, - 77 - ), - SubtensorError::::AdminActionProhibitedDuringWeightsWindow - ); - }); -} - -#[test] -fn test_sudo_set_min_burn() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set = TaoBalance::from(1_000_000); - add_network(netuid, 10); - let init_value = SubtensorModule::get_min_burn(netuid); - - // Simple case - assert_ok!(AdminUtils::sudo_set_min_burn( - <::RuntimeOrigin>::root(), - netuid, - TaoBalance::from(to_be_set) - )); - assert_ne!(SubtensorModule::get_min_burn(netuid), init_value); - assert_eq!(SubtensorModule::get_min_burn(netuid), to_be_set); - - // Unknown subnet - assert_err!( - AdminUtils::sudo_set_min_burn( - <::RuntimeOrigin>::root(), - NetUid::from(42), - TaoBalance::from(to_be_set) - ), - Error::::SubnetDoesNotExist - ); - - // Non subnet owner - assert_err!( - AdminUtils::sudo_set_min_burn( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - TaoBalance::from(to_be_set) - ), - DispatchError::BadOrigin - ); - - // Above upper bound - assert_err!( - AdminUtils::sudo_set_min_burn( - <::RuntimeOrigin>::root(), - netuid, - ::MinBurnUpperBound::get() + 1.into() - ), - Error::::ValueNotInBounds - ); - - // Above max burn - assert_err!( - AdminUtils::sudo_set_min_burn( - <::RuntimeOrigin>::root(), - netuid, - SubtensorModule::get_max_burn(netuid) + 1.into() - ), - Error::::ValueNotInBounds - ); - }); -} - -#[test] -fn test_owner_hyperparam_update_rate_limit_enforced() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 10); - // Set owner - let owner: U256 = U256::from(5); - SubnetOwner::::insert(netuid, owner); - - // Set tempo to 1 so owner hyperparam RL = 2 tempos = 2 blocks - SubtensorModule::set_tempo_unchecked(netuid, 1); - // Disable admin freeze window to avoid blocking on small tempo - assert_ok!(AdminUtils::sudo_set_admin_freeze_window( - <::RuntimeOrigin>::root(), - 0 - )); - - // First update succeeds - assert_ok!(AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::signed(owner), - netuid, - 11 - )); - // Immediate second update fails due to TxRateLimitExceeded - assert_noop!( - AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::signed(owner), - netuid, - 12 - ), - SubtensorError::::TxRateLimitExceeded - ); - - // Advance less than limit still fails - run_to_block(SubtensorModule::get_current_block_as_u64() + 1); - assert_noop!( - AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::signed(owner), - netuid, - 13 - ), - SubtensorError::::TxRateLimitExceeded - ); - - // Advance one more block to pass the limit; should succeed - run_to_block(SubtensorModule::get_current_block_as_u64() + 1); - assert_ok!(AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::signed(owner), - netuid, - 14 - )); - }); -} - -// Verifies that owner hyperparameter rate limit is enforced based on tempo (2 tempos). -#[test] -fn test_hyperparam_rate_limit_enforced_by_tempo() { - new_test_ext().execute_with(|| { - // Setup subnet and owner - let netuid = NetUid::from(42); - add_network(netuid, 10); - let owner: U256 = U256::from(77); - SubnetOwner::::insert(netuid, owner); - - // Set tempo to 1 so RL = 2 blocks - SubtensorModule::set_tempo_unchecked(netuid, 1); - // Disable admin freeze window to avoid blocking on small tempo - assert_ok!(AdminUtils::sudo_set_admin_freeze_window( - <::RuntimeOrigin>::root(), - 0 - )); - - // First owner update should succeed - assert_ok!(AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::signed(owner), - netuid, - 1 - )); - - // Immediate second update should fail due to tempo-based RL - assert_noop!( - AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::signed(owner), - netuid, - 2 - ), - SubtensorError::::TxRateLimitExceeded - ); - - // Advance 2 blocks (2 tempos with tempo=1) then succeed - run_to_block(SubtensorModule::get_current_block_as_u64() + 2); - assert_ok!(AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::signed(owner), - netuid, - 3 - )); - }); -} - -// Verifies owner hyperparameters are rate-limited independently per parameter. -// Setting one hyperparameter should not block setting a different hyperparameter -// during the same rate-limit window, but it should still block itself. -#[test] -fn test_owner_hyperparam_rate_limit_independent_per_param() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(7); - add_network(netuid, 10); - - // Set subnet owner - let owner: U256 = U256::from(123); - SubnetOwner::::insert(netuid, owner); - - // Use small tempo to make RL short and deterministic (2 blocks when tempo=1) - SubtensorModule::set_tempo_unchecked(netuid, 1); - // Disable admin freeze window so it doesn't interfere with small tempo - assert_ok!(AdminUtils::sudo_set_admin_freeze_window( - <::RuntimeOrigin>::root(), - 0 - )); - - // First update to kappa should succeed - assert_ok!(AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::signed(owner), - netuid, - 10 - )); - - // Immediate second update to the SAME param (kappa) should be blocked by RL - assert_noop!( - AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::signed(owner), - netuid, - 11 - ), - SubtensorError::::TxRateLimitExceeded - ); - - // Updating a DIFFERENT param (rho) should pass immediately — independent RL key - assert_ok!(AdminUtils::sudo_set_rho( - <::RuntimeOrigin>::signed(owner), - netuid, - 5 - )); - - // kappa should still be blocked until its own RL window passes - assert_noop!( - AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::signed(owner), - netuid, - 12 - ), - SubtensorError::::TxRateLimitExceeded - ); - - // rho should also be blocked for itself immediately after being set - assert_noop!( - AdminUtils::sudo_set_rho(<::RuntimeOrigin>::signed(owner), netuid, 6), - SubtensorError::::TxRateLimitExceeded - ); - - // Advance enough blocks to pass the RL window (2 blocks when tempo=1 and default epochs=2) - run_to_block(SubtensorModule::get_current_block_as_u64() + 2); - - // Now both hyperparameters can be updated again - assert_ok!(AdminUtils::sudo_set_commit_reveal_weights_interval( - <::RuntimeOrigin>::signed(owner), - netuid, - 13 - )); - assert_ok!(AdminUtils::sudo_set_rho( - <::RuntimeOrigin>::signed(owner), - netuid, - 7 - )); - }); -} - -#[test] -fn test_sudo_set_max_burn() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set = TaoBalance::from(100_000_001); - add_network(netuid, 10); - let init_value = SubtensorModule::get_max_burn(netuid); - - // Simple case - assert_ok!(AdminUtils::sudo_set_max_burn( - <::RuntimeOrigin>::root(), - netuid, - TaoBalance::from(to_be_set) - )); - assert_ne!(SubtensorModule::get_max_burn(netuid), init_value); - assert_eq!(SubtensorModule::get_max_burn(netuid), to_be_set); - - // Unknown subnet - assert_err!( - AdminUtils::sudo_set_max_burn( - <::RuntimeOrigin>::root(), - NetUid::from(42), - TaoBalance::from(to_be_set) - ), - Error::::SubnetDoesNotExist - ); - - // Non subnet owner - assert_err!( - AdminUtils::sudo_set_max_burn( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - TaoBalance::from(to_be_set) - ), - DispatchError::BadOrigin - ); - - // Below lower bound - assert_err!( - AdminUtils::sudo_set_max_burn( - <::RuntimeOrigin>::root(), - netuid, - ::MaxBurnLowerBound::get() - 1.into() - ), - Error::::ValueNotInBounds - ); - - // Below min burn - assert_err!( - AdminUtils::sudo_set_max_burn( - <::RuntimeOrigin>::root(), - netuid, - SubtensorModule::get_min_burn(netuid) - 1.into() - ), - Error::::ValueNotInBounds - ); - }); -} - -#[test] -fn test_sudo_set_mechanism_count() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let ss_count_ok = MaxMechanismCount::::get(); - let ss_count_bad = MechId::from(u8::from(ss_count_ok) + 1); - - let sn_owner = U256::from(1324); - add_network(netuid, 10); - // Set the Subnet Owner - SubnetOwner::::insert(netuid, sn_owner); - MaxAllowedUids::::insert(netuid, 256_u16); - - assert_eq!( - AdminUtils::sudo_set_mechanism_count( - <::RuntimeOrigin>::signed(U256::from(1)), - netuid, - ss_count_ok - ), - Err(DispatchError::BadOrigin) - ); - assert_noop!( - AdminUtils::sudo_set_mechanism_count(RuntimeOrigin::root(), netuid, ss_count_bad), - pallet_subtensor::Error::::InvalidValue - ); - assert_noop!( - AdminUtils::sudo_set_mechanism_count(RuntimeOrigin::root(), netuid, ss_count_ok), - pallet_subtensor::Error::::TooManyUIDsPerMechanism - ); - - // Reduce max UIDs to 128 - MaxAllowedUids::::insert(netuid, 128_u16); - assert_ok!(AdminUtils::sudo_set_mechanism_count( - <::RuntimeOrigin>::root(), - netuid, - ss_count_ok - )); - - assert_ok!(AdminUtils::sudo_set_mechanism_count( - <::RuntimeOrigin>::signed(sn_owner), - netuid, - ss_count_ok - )); - }); -} - -#[test] -fn test_sudo_set_owner_cut_enabled() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(11); - let owner = U256::from(1234); - let call = RuntimeCall::AdminUtils(crate::Call::sudo_set_owner_cut_enabled { - netuid, - enabled: false, - }); - - add_network(netuid, 10); - SubnetOwner::::insert(netuid, owner); - - assert_ok!(AdminUtils::sudo_set_admin_freeze_window( - <::RuntimeOrigin>::root(), - 0 - )); - - let dispatch_info = call.get_dispatch_info(); - assert_eq!(dispatch_info.pays_fee, Pays::Yes); - - assert!(SubtensorModule::get_owner_cut_enabled(netuid)); - assert_ok!(AdminUtils::sudo_set_owner_cut_enabled( - <::RuntimeOrigin>::signed(owner), - netuid, - false - )); - assert!(!SubtensorModule::get_owner_cut_enabled(netuid)); - - assert_ok!(AdminUtils::sudo_set_owner_cut_enabled( - <::RuntimeOrigin>::root(), - netuid, - true - )); - assert!(SubtensorModule::get_owner_cut_enabled(netuid)); - }); -} - -#[test] -fn test_sudo_set_owner_cut_auto_lock_enabled() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(11); - let owner = U256::from(1234); - let non_owner = U256::from(4321); - let call = RuntimeCall::AdminUtils(crate::Call::sudo_set_owner_cut_auto_lock_enabled { - netuid, - enabled: true, - }); - - add_network(netuid, 10); - SubnetOwner::::insert(netuid, owner); - - assert_ok!(AdminUtils::sudo_set_admin_freeze_window( - <::RuntimeOrigin>::root(), - 0 - )); - - let dispatch_info = call.get_dispatch_info(); - assert_eq!(dispatch_info.pays_fee, Pays::Yes); - - assert!(!SubtensorModule::get_owner_cut_auto_lock_enabled(netuid)); - assert_noop!( - AdminUtils::sudo_set_owner_cut_auto_lock_enabled( - <::RuntimeOrigin>::signed(non_owner), - netuid, - true - ), - DispatchError::BadOrigin - ); - - assert_ok!(AdminUtils::sudo_set_owner_cut_auto_lock_enabled( - <::RuntimeOrigin>::signed(owner), - netuid, - false - )); - assert!(!SubtensorModule::get_owner_cut_auto_lock_enabled(netuid)); - - assert_ok!(AdminUtils::sudo_set_owner_cut_auto_lock_enabled( - <::RuntimeOrigin>::root(), - netuid, - true - )); - assert!(SubtensorModule::get_owner_cut_auto_lock_enabled(netuid)); - }); -} - -// cargo test --package pallet-admin-utils --lib -- tests::test_sudo_set_mechanism_count_and_emissions --exact --show-output -#[test] -fn test_sudo_set_mechanism_count_and_emissions() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let ss_count_ok = MechId::from(2); - - let sn_owner = U256::from(1324); - add_network(netuid, 10); - // Set the Subnet Owner - SubnetOwner::::insert(netuid, sn_owner); - MaxMechanismCount::::set(MechId::from(2)); - MaxAllowedUids::::set(netuid, 128_u16); - - assert_ok!(AdminUtils::sudo_set_mechanism_count( - <::RuntimeOrigin>::signed(sn_owner), - netuid, - ss_count_ok - )); - - // Cannot set emission split with wrong number of entries - // With two mechanisms the size of the split vector should be 2, not 3 - assert_noop!( - AdminUtils::sudo_set_mechanism_emission_split( - <::RuntimeOrigin>::signed(sn_owner), - netuid, - Some(vec![0xFFFF / 5 * 2, 0xFFFF / 5 * 2, 0xFFFF / 5]) - ), - pallet_subtensor::Error::::InvalidValue - ); - - // Cannot set emission split with wrong total of entries - // Split vector entries should sum up to exactly 0xFFFF - assert_noop!( - AdminUtils::sudo_set_mechanism_emission_split( - <::RuntimeOrigin>::signed(sn_owner), - netuid, - Some(vec![0xFFFF / 5 * 4, 0xFFFF / 5 - 1]) - ), - pallet_subtensor::Error::::InvalidValue - ); - - // Can set good split ok - // We also verify here that it can happen in the same block as setting mechanism counts - // or soon, without rate limiting - assert_ok!(AdminUtils::sudo_set_mechanism_emission_split( - <::RuntimeOrigin>::signed(sn_owner), - netuid, - Some(vec![0xFFFF / 5, 0xFFFF / 5 * 4]) - )); - - // Cannot set it again due to rate limits - assert_noop!( - AdminUtils::sudo_set_mechanism_emission_split( - <::RuntimeOrigin>::signed(sn_owner), - netuid, - Some(vec![0xFFFF / 5 * 4, 0xFFFF / 5]) - ), - pallet_subtensor::Error::::TxRateLimitExceeded - ); - }); -} - -#[test] -fn test_trim_to_max_allowed_uids() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let sn_owner = U256::from(1); - let sn_owner_hotkey1 = U256::from(2); - - add_network(netuid, 10); - SubnetOwner::::insert(netuid, sn_owner); - SubnetOwnerHotkey::::insert(netuid, sn_owner_hotkey1); - - MaxRegistrationsPerBlock::::insert(netuid, 256); - TargetRegistrationsPerInterval::::insert(netuid, 256); - ImmuneOwnerUidsLimit::::insert(netuid, 2); - // We set a low value here to make testing easier - MinAllowedUids::::set(netuid, 4); - // We define 4 mechanisms - let mechanism_count = MechId::from(4); - MechanismCountCurrent::::insert(netuid, mechanism_count); - - // Add some neurons (fund accounts + step blocks between regs). - let max_n: u16 = 16; - for i in 1..=max_n { - let n: u64 = (i as u64) * 1000; - let hotkey = U256::from(n); - let coldkey = U256::from(n + i as u64); - - let funds: u64 = 1_000_000_000_000_000; // 1,000,000 TAO (in RAO) - let _ = Balances::deposit_creating(&coldkey, Balance::from(funds)); - let _ = Balances::deposit_creating(&hotkey, Balance::from(funds)); // defensive - - register_ok_neuron(netuid, hotkey, coldkey, 0); - step_block(1); - } - - // Run some blocks to ensure stake weights are set and that we are past the immunity period - // for all neurons - let immunity_period: u64 = ImmunityPeriod::::get(netuid).into(); - let current_block: u64 = frame_system::Pallet::::block_number().into(); - run_to_block(current_block + immunity_period + 1); - - // Set some randomized values that we can keep track of - let values = vec![ - 17u16, 42u16, 8u16, 56u16, 23u16, 91u16, - 34u16, // uid 6 (34) will be forced-immune below - 77u16, 12u16, 65u16, 3u16, 88u16, 29u16, 51u16, 74u16, 39u16, - ]; - let bool_values = vec![ - false, false, false, true, false, true, true, true, false, true, false, true, false, - true, true, false, - ]; - let alpha_values = values.iter().map(|&v| (v as u64).into()).collect(); - let u64_values: Vec = values.iter().map(|&v| v as u64).collect(); - let per_values: Vec = values.iter().map(|&v| PerU16::from_parts(v)).collect(); - - Emission::::set(netuid, alpha_values); - Consensus::::insert(netuid, per_values.clone()); - Dividends::::insert(netuid, per_values.clone()); - ValidatorTrust::::insert(netuid, per_values.clone()); - StakeWeight::::insert(netuid, values.clone()); - ValidatorPermit::::insert(netuid, bool_values.clone()); - Active::::insert(netuid, bool_values); - - for mecid in 0..mechanism_count.into() { - let netuid_index = - SubtensorModule::get_mechanism_storage_index(netuid, MechId::from(mecid)); - Incentive::::insert(netuid_index, per_values.clone()); - LastUpdate::::insert(netuid_index, u64_values.clone()); - } - - // Make UID 6 temporally immune so it cannot be trimmed even though it's not a top-8 emitter. - let now = frame_system::Pallet::::block_number(); - BlockAtRegistration::::set(netuid, 6, now); - - // Set some evm addresses (include both kept + trimmed uids). Go through the normal - // setter so both the forward map and the reverse index are populated, exactly as the - // association extrinsic does in production. - let evm_addr_uid6 = sp_core::H160::from_slice(b"12345678901234567891"); - let evm_addr_uid10 = sp_core::H160::from_slice(b"12345678901234567892"); - let evm_addr_uid12 = sp_core::H160::from_slice(b"12345678901234567893"); - let evm_addr_uid14 = sp_core::H160::from_slice(b"12345678901234567894"); - SubtensorModule::set_associated_evm_address(netuid, 6, evm_addr_uid6, now); - SubtensorModule::set_associated_evm_address(netuid, 10, evm_addr_uid10, now); - SubtensorModule::set_associated_evm_address(netuid, 12, evm_addr_uid12, now); - SubtensorModule::set_associated_evm_address(netuid, 14, evm_addr_uid14, now); - - // Populate Weights and Bonds storage items to test trimming - for uid in 0..max_n { - let mut weights = Vec::new(); - let mut bonds = Vec::new(); - - // Add connections to all other uids, including those that will be trimmed - for target_uid in 0..max_n { - if target_uid != uid { - let weight_value = (uid + target_uid) % 1000; - let bond_value = (uid * target_uid) % 1000; - weights.push((target_uid, weight_value)); - bonds.push((target_uid, bond_value)); - } - } - - for mecid in 0..mechanism_count.into() { - let netuid_index = - SubtensorModule::get_mechanism_storage_index(netuid, MechId::from(mecid)); - Weights::::insert(netuid_index, uid, weights.clone()); - Bonds::::insert(netuid_index, uid, bonds.clone()); - } - } - - // Normal case - let new_max_n = 8; - assert_ok!(AdminUtils::sudo_trim_to_max_allowed_uids( - <::RuntimeOrigin>::root(), - netuid, - new_max_n - )); - - // Ensure the max allowed uids has been set correctly - assert_eq!(MaxAllowedUids::::get(netuid), new_max_n); - - // Ensure the emission has been trimmed correctly and compressed to the left - assert_eq!( - Emission::::get(netuid), - vec![ - 56.into(), - 91.into(), - 34.into(), - 77.into(), - 65.into(), - 88.into(), - 51.into(), - 74.into() - ] - ); - - // Ensure rest of (active) storage has been trimmed correctly - let expected_values: Vec = vec![56, 91, 34, 77, 65, 88, 51, 74]; - let expected_per_values: Vec = expected_values - .iter() - .map(|&v| PerU16::from_parts(v)) - .collect(); - let expected_bools = vec![true, true, true, true, true, true, true, true]; - let expected_u64_values = vec![56, 91, 34, 77, 65, 88, 51, 74]; - - assert_eq!(Active::::get(netuid), expected_bools); - assert_eq!(Consensus::::get(netuid), expected_per_values); - assert_eq!(Dividends::::get(netuid), expected_per_values); - assert_eq!(ValidatorTrust::::get(netuid), expected_per_values); - assert_eq!(ValidatorPermit::::get(netuid), expected_bools); - assert_eq!(StakeWeight::::get(netuid), expected_values); - - for mecid in 0..mechanism_count.into() { - let netuid_index = - SubtensorModule::get_mechanism_storage_index(netuid, MechId::from(mecid)); - assert_eq!(Incentive::::get(netuid_index), expected_per_values); - assert_eq!(LastUpdate::::get(netuid_index), expected_u64_values); - } - - // Ensure trimmed uids related storage has been cleared - for uid in new_max_n..max_n { - assert!(!Keys::::contains_key(netuid, uid)); - assert!(!BlockAtRegistration::::contains_key(netuid, uid)); - assert!(!AssociatedEvmAddress::::contains_key(netuid, uid)); - for mecid in 0..mechanism_count.into() { - let netuid_index = - SubtensorModule::get_mechanism_storage_index(netuid, MechId::from(mecid)); - assert!(!Weights::::contains_key(netuid_index, uid)); - assert!(!Bonds::::contains_key(netuid_index, uid)); - } - } - - // Ensure trimmed uids hotkey related storage has been cleared - let trimmed_hotkeys = vec![ - U256::from(1000), - U256::from(2000), - U256::from(3000), - U256::from(5000), - U256::from(9000), - U256::from(11000), - U256::from(13000), - U256::from(16000), - ]; - for hotkey in trimmed_hotkeys { - assert!(!Uids::::contains_key(netuid, hotkey)); - assert!(!IsNetworkMember::::contains_key(hotkey, netuid)); - assert!(!LastHotkeyEmissionOnNetuid::::contains_key( - hotkey, netuid - )); - assert!(!AlphaDividendsPerSubnet::::contains_key( - netuid, hotkey - )); - assert!(!Axons::::contains_key(netuid, hotkey)); - assert!(!NeuronCertificates::::contains_key(netuid, hotkey)); - assert!(!Prometheus::::contains_key(netuid, hotkey)); - } - - // Ensure trimmed uids weights and bonds connections have been trimmed correctly - for uid in 0..new_max_n { - for mecid in 0..mechanism_count.into() { - let netuid_index = - SubtensorModule::get_mechanism_storage_index(netuid, MechId::from(mecid)); - assert!( - Weights::::get(netuid_index, uid) - .iter() - .all(|(target_uid, _)| *target_uid < new_max_n), - "Found a weight with target_uid >= new_max_n" - ); - assert!( - Bonds::::get(netuid_index, uid) - .iter() - .all(|(target_uid, _)| *target_uid < new_max_n), - "Found a bond with target_uid >= new_max_n" - ); - } - } - - // Actual number of neurons on the network updated after trimming - assert_eq!(SubnetworkN::::get(netuid), new_max_n); - - // Uids match enumeration order - for i in 0..new_max_n.into() { - let hotkey = Keys::::get(netuid, i); - let uid = Uids::::get(netuid, hotkey); - assert_eq!(uid, Some(i)); - } - - // EVM association have been remapped correctly (uids: 6 -> 2, 14 -> 7) - assert_eq!( - AssociatedEvmAddress::::get(netuid, 2), - Some((evm_addr_uid6, now)) - ); - assert_eq!( - AssociatedEvmAddress::::get(netuid, 7), - Some((evm_addr_uid14, now)) - ); - - // The reverse index has been remapped in place to the new UIDs (6 -> 2, 14 -> 7), - // without rebuilding it from scratch. - assert_eq!( - AssociatedUidsByEvmAddress::::get(netuid, evm_addr_uid6).into_inner(), - vec![(2u16, now)] - ); - assert_eq!( - AssociatedUidsByEvmAddress::::get(netuid, evm_addr_uid14).into_inner(), - vec![(7u16, now)] - ); - // Trimmed UIDs (10, 12) were dropped from the reverse index entirely. - assert!(AssociatedUidsByEvmAddress::::get(netuid, evm_addr_uid10).is_empty()); - assert!(AssociatedUidsByEvmAddress::::get(netuid, evm_addr_uid12).is_empty()); - // uid_lookup resolves the remapped UID. - assert_eq!( - SubtensorModule::uid_lookup(netuid, evm_addr_uid6, u16::MAX), - vec![(2u16, now)] - ); - - // Non existent subnet - assert_err!( - AdminUtils::sudo_trim_to_max_allowed_uids( - <::RuntimeOrigin>::root(), - NetUid::from(42), - new_max_n - ), - pallet_subtensor::Error::::SubnetNotExists - ); - - // New max n less than lower bound - assert_err!( - AdminUtils::sudo_trim_to_max_allowed_uids( - <::RuntimeOrigin>::root(), - netuid, - 2 - ), - pallet_subtensor::Error::::InvalidValue - ); - - // New max n greater than upper bound - assert_err!( - AdminUtils::sudo_trim_to_max_allowed_uids( - <::RuntimeOrigin>::root(), - netuid, - SubtensorModule::get_max_allowed_uids(netuid) + 1 - ), - pallet_subtensor::Error::::InvalidValue - ); - }); -} - -#[test] -fn test_trim_to_max_allowed_uids_too_many_immune() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let sn_owner = U256::from(1); - add_network(netuid, 10); - SubnetOwner::::insert(netuid, sn_owner); - MaxRegistrationsPerBlock::::insert(netuid, 256); - TargetRegistrationsPerInterval::::insert(netuid, 256); - ImmuneOwnerUidsLimit::::insert(netuid, 2); - MinAllowedUids::::set(netuid, 2); - - // Add 5 neurons (fund + step blocks between regs) - let max_n = 5; - for i in 1..=max_n { - let n = i * 1000; - let hotkey = U256::from(n); - let coldkey = U256::from(n + i); - - let funds: u64 = 1_000_000_000_000_000; // 1,000,000 TAO (in RAO) - let _ = Balances::deposit_creating(&coldkey, Balance::from(funds)); - let _ = Balances::deposit_creating(&hotkey, Balance::from(funds)); // defensive - - register_ok_neuron(netuid, hotkey, coldkey, 0); - step_block(1); - } - - // Run some blocks to ensure stake weights are set - run_to_block((ImmunityPeriod::::get(netuid) + 1).into()); - - // Set owner immune uids (2 UIDs) by adding them to OwnedHotkeys - let owner_hotkey1 = U256::from(1000); - let owner_hotkey2 = U256::from(2000); - OwnedHotkeys::::insert(sn_owner, vec![owner_hotkey1, owner_hotkey2]); - Keys::::insert(netuid, 0, owner_hotkey1); - Uids::::insert(netuid, owner_hotkey1, 0); - Keys::::insert(netuid, 1, owner_hotkey2); - Uids::::insert(netuid, owner_hotkey2, 1); - - // Set temporally immune uids (2 UIDs) to make total immune count 4 out of 5 (80%) - // Set their registration block to current block to make them temporally immune - let current_block = frame_system::Pallet::::block_number(); - for uid in 2..4 { - let hotkey = U256::from(uid * 1000 + 1000); - Keys::::insert(netuid, uid, hotkey); - Uids::::insert(netuid, hotkey, uid); - BlockAtRegistration::::insert(netuid, uid, current_block); - } - - // Try to trim to 4 UIDs - this should fail because 4/4 = 100% immune (>= 80%) - assert_err!( - AdminUtils::sudo_trim_to_max_allowed_uids( - <::RuntimeOrigin>::root(), - netuid, - 4 - ), - pallet_subtensor::Error::::TrimmingWouldExceedMaxImmunePercentage - ); - - // Try to trim to 3 UIDs - this should also fail because 4/3 > 80% immune (>= 80%) - assert_err!( - AdminUtils::sudo_trim_to_max_allowed_uids( - <::RuntimeOrigin>::root(), - netuid, - 3 - ), - pallet_subtensor::Error::::TrimmingWouldExceedMaxImmunePercentage - ); - - // Now test a scenario where trimming should succeed - // Remove one immune UID to make it 3 immune out of 4 total - let uid_to_remove = 3; - let hotkey_to_remove = U256::from(uid_to_remove * 1000 + 1000); - #[allow(unknown_lints)] - Keys::::remove(netuid, uid_to_remove); - Uids::::remove(netuid, hotkey_to_remove); - BlockAtRegistration::::remove(netuid, uid_to_remove); - - // Remove another immune UID to make it 2 immune out of 3 total - let uid_to_remove2 = 2; - let hotkey_to_remove2 = U256::from(uid_to_remove2 * 1000 + 1000); - #[allow(unknown_lints)] - Keys::::remove(netuid, uid_to_remove2); - Uids::::remove(netuid, hotkey_to_remove2); - BlockAtRegistration::::remove(netuid, uid_to_remove2); - - // Now we have 2 immune out of 2 total UIDs - // Try to trim to 1 UID - this should fail because 2/1 is impossible, but the check prevents it - assert_err!( - AdminUtils::sudo_trim_to_max_allowed_uids( - <::RuntimeOrigin>::root(), - netuid, - 1 - ), - pallet_subtensor::Error::::InvalidValue - ); - }); -} - -#[test] -fn test_sudo_set_min_allowed_uids() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let to_be_set: u16 = 8; - add_network(netuid, 10); - MaxRegistrationsPerBlock::::insert(netuid, 256); - TargetRegistrationsPerInterval::::insert(netuid, 256); - - for i in 0..=16 { - let hotkey = U256::from(i * 1000); - let coldkey = U256::from(i * 1000 + i); - - let funds: u64 = 1_000_000_000_000_000; // 1,000,000 TAO (in RAO) - let _ = Balances::deposit_creating(&coldkey, Balance::from(funds)); - let _ = Balances::deposit_creating(&hotkey, Balance::from(funds)); // defensive - - register_ok_neuron(netuid, hotkey, coldkey, 0); - step_block(1); - } - - // Normal case - assert_ok!(AdminUtils::sudo_set_min_allowed_uids( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - assert_eq!(SubtensorModule::get_min_allowed_uids(netuid), to_be_set); - - // Non root - assert_err!( - AdminUtils::sudo_set_min_allowed_uids( - <::RuntimeOrigin>::signed(U256::from(0)), - netuid, - to_be_set - ), - DispatchError::BadOrigin - ); - - // Non existent subnet - assert_err!( - AdminUtils::sudo_set_min_allowed_uids( - <::RuntimeOrigin>::root(), - NetUid::from(42), - to_be_set - ), - Error::::SubnetDoesNotExist - ); - - // Min allowed uids greater than max allowed uids - assert_err!( - AdminUtils::sudo_set_min_allowed_uids( - <::RuntimeOrigin>::root(), - netuid, - SubtensorModule::get_max_allowed_uids(netuid) + 1 - ), - Error::::MinAllowedUidsGreaterThanMaxAllowedUids - ); - - // Min allowed uids greater than current uids - assert_err!( - AdminUtils::sudo_set_min_allowed_uids( - <::RuntimeOrigin>::root(), - netuid, - SubtensorModule::get_subnetwork_n(netuid) + 1 - ), - Error::::MinAllowedUidsGreaterThanCurrentUids - ); - }); -} - -#[test] -fn test_sudo_set_max_mechanism_count() { - new_test_ext().execute_with(|| { - // Normal case - assert_ok!(AdminUtils::sudo_set_max_mechanism_count( - <::RuntimeOrigin>::root(), - MechId::from(10) - )); - - // Zero fails - assert_noop!( - AdminUtils::sudo_set_max_mechanism_count( - <::RuntimeOrigin>::root(), - MechId::from(0) - ), - pallet_subtensor::Error::::InvalidValue - ); - - // Over max bound fails - assert_noop!( - AdminUtils::sudo_set_max_mechanism_count( - <::RuntimeOrigin>::root(), - MechId::from(MAX_MECHANISM_COUNT_PER_SUBNET + 1) - ), - pallet_subtensor::Error::::InvalidValue - ); - }); -} - -#[test] -fn test_sudo_set_min_non_immune_uids() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 10); - - let to_be_set: u16 = 12; - let init_value: u16 = SubtensorModule::get_min_non_immune_uids(netuid); - - assert_ok!(AdminUtils::sudo_set_min_non_immune_uids( - <::RuntimeOrigin>::root(), - netuid, - to_be_set - )); - - assert!(init_value != to_be_set); - assert_eq!(SubtensorModule::get_min_non_immune_uids(netuid), to_be_set); - }); -} - -#[test] -fn test_sudo_set_start_call_delay_permissions_and_zero_delay() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let tempo: u16 = 13; - let coldkey_account_id = U256::from(0); - let non_root_account = U256::from(1); - - // Get initial delay value (should be non-zero) - let initial_delay = pallet_subtensor::StartCallDelay::::get(); - assert_eq!(initial_delay, 0); - - // Test 1: Non-root account should fail to set delay - assert_noop!( - AdminUtils::sudo_set_start_call_delay( - <::RuntimeOrigin>::signed(non_root_account), - 0 - ), - DispatchError::BadOrigin - ); - - // Test 2: Create a subnet - add_network(netuid, tempo); - - if pallet_subtensor::FirstEmissionBlockNumber::::get(netuid).is_some() { - pallet_subtensor::FirstEmissionBlockNumber::::remove(netuid); - } - - assert_eq!( - pallet_subtensor::FirstEmissionBlockNumber::::get(netuid), - None, - "Emission block should not be set yet" - ); - assert_eq!( - pallet_subtensor::SubnetOwner::::get(netuid), - coldkey_account_id, - "Default owner should be account 0" - ); - - // Test 3: Can successfully start the subnet immediately - assert_ok!(pallet_subtensor::Pallet::::start_call( - <::RuntimeOrigin>::signed(coldkey_account_id), - netuid - )); - - // Verify emission has been set - assert!( - pallet_subtensor::FirstEmissionBlockNumber::::get(netuid).is_some(), - "Emission should be set" - ); - - // Test 4: Root sets delay to zero - assert_ok!(AdminUtils::sudo_set_start_call_delay( - <::RuntimeOrigin>::root(), - 0 - )); - assert_eq!( - pallet_subtensor::StartCallDelay::::get(), - 0, - "Delay should now be zero" - ); - - // Verify event was emitted - frame_system::Pallet::::assert_last_event(RuntimeEvent::SubtensorModule( - pallet_subtensor::Event::StartCallDelaySet(0), - )); - - // Test 5: Try to start the subnet again - should be FAILED (first emission block already set) - assert_err!( - pallet_subtensor::Pallet::::start_call( - <::RuntimeOrigin>::signed(coldkey_account_id), - netuid - ), - pallet_subtensor::Error::::FirstEmissionBlockNumberAlreadySet - ); - - assert_eq!( - pallet_subtensor::FirstEmissionBlockNumber::::get(netuid), - Some(frame_system::Pallet::::block_number() + 1), - "Emission should start at next block" - ); +/// Test runtime / helpers shared by admin-utils unit tests and benchmarks. +pub mod mock; - // Test 6: Try to start it a third time - should FAIL (already started) - assert_err!( - pallet_subtensor::Pallet::::start_call( - <::RuntimeOrigin>::signed(coldkey_account_id), - netuid - ), - pallet_subtensor::Error::::FirstEmissionBlockNumberAlreadySet - ); - }); +mod admin_windows_rate_limits; +mod alpha_commit_reveal; +mod consensus_hyperparams; +mod evm_grandpa_precompile; +mod mechanisms; +mod registration_burn; +mod stake_delegate_take; +mod subnet_owner_misc; +mod uids_validators; +mod weights_difficulty; + +#[allow(unused_imports)] // used by `impl_benchmark_test_suite!` in benchmarking.rs +pub use mock::{Test, new_test_ext}; + +/// Shared imports for concept test modules under this directory. +pub(crate) mod prelude { + pub(crate) use crate::{Error, pallet::PrecompileEnable}; + pub(crate) use frame_support::{ + assert_err, assert_noop, assert_ok, + dispatch::{DispatchClass, GetDispatchInfo, Pays}, + sp_runtime::DispatchError, + traits::{Currency as _, Hooks}, + }; + pub(crate) use frame_system::Config; + pub(crate) use pallet_subtensor::{ + Error as SubtensorError, Event, MaxRegistrationsPerBlock, SubnetOwner, + TargetRegistrationsPerInterval, Tempo, WeightsVersionKeyRateLimit, + subnets::mechanism::MAX_MECHANISM_COUNT_PER_SUBNET, utils::rate_limiting::TransactionType, + *, + }; + pub(crate) use sp_consensus_grandpa::AuthorityId as GrandpaId; + pub(crate) use sp_core::{Get, Pair, U256, ed25519}; + pub(crate) use sp_runtime::PerU16; + pub(crate) use substrate_fixed::types::I96F32; + pub(crate) use subtensor_runtime_common::{MechId, NetUid, TaoBalance, Token}; + + pub(crate) use super::mock::*; } diff --git a/pallets/admin-utils/src/tests/registration_burn.rs b/pallets/admin-utils/src/tests/registration_burn.rs new file mode 100644 index 0000000000..a4b2bee337 --- /dev/null +++ b/pallets/admin-utils/src/tests/registration_burn.rs @@ -0,0 +1,270 @@ +//! Registration targets, POW toggle, burn bounds, recycled RAO, and lock-reduction interval. +#![allow( + clippy::unwrap_used, + clippy::expect_used, + clippy::indexing_slicing, + unused_imports +)] + +use super::prelude::*; + +#[test] +fn test_sudo_set_target_registrations_per_interval() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: u16 = 10; + add_network(netuid, 10); + let init_value: u16 = SubtensorModule::get_target_registrations_per_interval(netuid); + assert_eq!( + AdminUtils::sudo_set_target_registrations_per_interval( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!( + AdminUtils::sudo_set_target_registrations_per_interval( + <::RuntimeOrigin>::root(), + netuid.next(), + to_be_set + ), + Err(Error::::SubnetDoesNotExist.into()) + ); + assert_eq!( + SubtensorModule::get_target_registrations_per_interval(netuid), + init_value + ); + assert_ok!(AdminUtils::sudo_set_target_registrations_per_interval( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!( + SubtensorModule::get_target_registrations_per_interval(netuid), + to_be_set + ); + }); +} + +#[test] +fn test_sudo_set_rao_recycled() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set = TaoBalance::from(10); + add_network(netuid, 10); + let init_value = SubtensorModule::get_rao_recycled(netuid); + + // Need to run from genesis block + run_to_block(1); + + assert_eq!( + AdminUtils::sudo_set_rao_recycled( + <::RuntimeOrigin>::signed(U256::from(0)), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!( + AdminUtils::sudo_set_rao_recycled( + <::RuntimeOrigin>::root(), + netuid.next(), + to_be_set + ), + Err(Error::::SubnetDoesNotExist.into()) + ); + assert_eq!(SubtensorModule::get_rao_recycled(netuid), init_value); + + // Verify no events emitted matching the expected event + assert_eq!( + System::events() + .iter() + .filter(|r| r.event + == RuntimeEvent::SubtensorModule(Event::RAORecycledForRegistrationSet( + netuid, to_be_set + ))) + .count(), + 0 + ); + + assert_ok!(AdminUtils::sudo_set_rao_recycled( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!(SubtensorModule::get_rao_recycled(netuid), to_be_set); + + // Verify event emitted with correct values + assert_eq!( + System::events() + .last() + .unwrap_or_else(|| panic!( + "Expected there to be events: {:?}", + System::events().to_vec() + )) + .event, + RuntimeEvent::SubtensorModule(Event::RAORecycledForRegistrationSet(netuid, to_be_set)) + ); + }); +} + +#[test] +fn test_sudo_set_network_lock_reduction_interval() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: u64 = 7200; + add_network(netuid, 10); + + let init_value: u64 = SubtensorModule::get_lock_reduction_interval(); + assert_eq!( + AdminUtils::sudo_set_lock_reduction_interval( + <::RuntimeOrigin>::signed(U256::from(1)), + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!(SubtensorModule::get_lock_reduction_interval(), init_value); + assert_ok!(AdminUtils::sudo_set_lock_reduction_interval( + <::RuntimeOrigin>::root(), + to_be_set + )); + assert_eq!(SubtensorModule::get_lock_reduction_interval(), to_be_set); + }); +} + +#[test] +fn test_sudo_set_network_pow_registration_allowed() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: bool = true; + add_network(netuid, 10); + + assert_eq!( + AdminUtils::sudo_set_network_pow_registration_allowed( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + to_be_set + ), + Err(Error::::POWRegistrationDisabled.into()) + ); + }); +} + +#[test] +fn test_sudo_set_min_burn() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set = TaoBalance::from(1_000_000); + add_network(netuid, 10); + let init_value = SubtensorModule::get_min_burn(netuid); + + // Simple case + assert_ok!(AdminUtils::sudo_set_min_burn( + <::RuntimeOrigin>::root(), + netuid, + TaoBalance::from(to_be_set) + )); + assert_ne!(SubtensorModule::get_min_burn(netuid), init_value); + assert_eq!(SubtensorModule::get_min_burn(netuid), to_be_set); + + // Unknown subnet + assert_err!( + AdminUtils::sudo_set_min_burn( + <::RuntimeOrigin>::root(), + NetUid::from(42), + TaoBalance::from(to_be_set) + ), + Error::::SubnetDoesNotExist + ); + + // Non subnet owner + assert_err!( + AdminUtils::sudo_set_min_burn( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + TaoBalance::from(to_be_set) + ), + DispatchError::BadOrigin + ); + + // Above upper bound + assert_err!( + AdminUtils::sudo_set_min_burn( + <::RuntimeOrigin>::root(), + netuid, + ::MinBurnUpperBound::get() + 1.into() + ), + Error::::ValueNotInBounds + ); + + // Above max burn + assert_err!( + AdminUtils::sudo_set_min_burn( + <::RuntimeOrigin>::root(), + netuid, + SubtensorModule::get_max_burn(netuid) + 1.into() + ), + Error::::ValueNotInBounds + ); + }); +} + +#[test] +fn test_sudo_set_max_burn() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set = TaoBalance::from(100_000_001); + add_network(netuid, 10); + let init_value = SubtensorModule::get_max_burn(netuid); + + // Simple case + assert_ok!(AdminUtils::sudo_set_max_burn( + <::RuntimeOrigin>::root(), + netuid, + TaoBalance::from(to_be_set) + )); + assert_ne!(SubtensorModule::get_max_burn(netuid), init_value); + assert_eq!(SubtensorModule::get_max_burn(netuid), to_be_set); + + // Unknown subnet + assert_err!( + AdminUtils::sudo_set_max_burn( + <::RuntimeOrigin>::root(), + NetUid::from(42), + TaoBalance::from(to_be_set) + ), + Error::::SubnetDoesNotExist + ); + + // Non subnet owner + assert_err!( + AdminUtils::sudo_set_max_burn( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + TaoBalance::from(to_be_set) + ), + DispatchError::BadOrigin + ); + + // Below lower bound + assert_err!( + AdminUtils::sudo_set_max_burn( + <::RuntimeOrigin>::root(), + netuid, + ::MaxBurnLowerBound::get() - 1.into() + ), + Error::::ValueNotInBounds + ); + + // Below min burn + assert_err!( + AdminUtils::sudo_set_max_burn( + <::RuntimeOrigin>::root(), + netuid, + SubtensorModule::get_min_burn(netuid) - 1.into() + ), + Error::::ValueNotInBounds + ); + }); +} diff --git a/pallets/admin-utils/src/tests/stake_delegate_take.rs b/pallets/admin-utils/src/tests/stake_delegate_take.rs new file mode 100644 index 0000000000..3d568646b0 --- /dev/null +++ b/pallets/admin-utils/src/tests/stake_delegate_take.rs @@ -0,0 +1,422 @@ +//! Delegate/default take, stake thresholds, nominator min stake, childkey take, and owner-cut toggles. +#![allow( + clippy::unwrap_used, + clippy::expect_used, + clippy::indexing_slicing, + unused_imports +)] + +use super::prelude::*; + +#[test] +fn test_sudo_set_default_take() { + new_test_ext().execute_with(|| { + let to_be_set = PerU16::from_parts(10); + let init_value: u16 = SubtensorModule::get_default_delegate_take(); + assert_eq!( + AdminUtils::sudo_set_default_take( + <::RuntimeOrigin>::signed(U256::from(0)), + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!(SubtensorModule::get_default_delegate_take(), init_value); + assert_ok!(AdminUtils::sudo_set_default_take( + <::RuntimeOrigin>::root(), + to_be_set + )); + assert_eq!( + SubtensorModule::get_default_delegate_take(), + to_be_set.deconstruct() + ); + }); +} + +#[test] +fn test_sudo_subnet_owner_cut() { + new_test_ext().execute_with(|| { + let to_be_set: u16 = 10; + let init_value: u16 = SubtensorModule::get_subnet_owner_cut(); + assert_eq!( + AdminUtils::sudo_set_subnet_owner_cut( + <::RuntimeOrigin>::signed(U256::from(0)), + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!(SubtensorModule::get_subnet_owner_cut(), init_value); + assert_ok!(AdminUtils::sudo_set_subnet_owner_cut( + <::RuntimeOrigin>::root(), + to_be_set + )); + assert_eq!(SubtensorModule::get_subnet_owner_cut(), to_be_set); + }); +} + +#[test] +fn test_sudo_set_stake_threshold() { + new_test_ext().execute_with(|| { + let to_be_set: u64 = 10; + let init_value: u64 = SubtensorModule::get_stake_threshold(); + assert_eq!( + AdminUtils::sudo_set_stake_threshold( + <::RuntimeOrigin>::signed(U256::from(1)), + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!(SubtensorModule::get_stake_threshold(), init_value); + assert_ok!(AdminUtils::sudo_set_stake_threshold( + <::RuntimeOrigin>::root(), + to_be_set + )); + assert_eq!(SubtensorModule::get_stake_threshold(), to_be_set); + }); +} + +mod sudo_set_nominator_min_required_stake { + use super::*; + + #[test] + fn can_only_be_called_by_admin() { + new_test_ext().execute_with(|| { + let to_be_set = SubtensorModule::get_nominator_min_required_stake() + 5; + assert_eq!( + AdminUtils::sudo_set_nominator_min_required_stake( + <::RuntimeOrigin>::signed(U256::from(0)), + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + }); + } + + #[test] + fn sets_a_lower_value() { + new_test_ext().execute_with(|| { + assert_ok!(AdminUtils::sudo_set_nominator_min_required_stake( + <::RuntimeOrigin>::root(), + 10 + )); + let default_min_stake = pallet_subtensor::DefaultMinStake::::get(); + assert_eq!( + SubtensorModule::get_nominator_min_required_stake(), + 10 * default_min_stake.to_u64() / 1_000_000 + ); + + assert_ok!(AdminUtils::sudo_set_nominator_min_required_stake( + <::RuntimeOrigin>::root(), + 5 + )); + assert_eq!( + SubtensorModule::get_nominator_min_required_stake(), + 5 * default_min_stake.to_u64() / 1_000_000 + ); + }); + } + + #[test] + fn sets_a_higher_value() { + new_test_ext().execute_with(|| { + let to_be_set = SubtensorModule::get_nominator_min_required_stake() + 5; + let default_min_stake = pallet_subtensor::DefaultMinStake::::get(); + assert_ok!(AdminUtils::sudo_set_nominator_min_required_stake( + <::RuntimeOrigin>::root(), + to_be_set + )); + assert_eq!( + SubtensorModule::get_nominator_min_required_stake(), + to_be_set * default_min_stake.to_u64() / 1_000_000 + ); + }); + } +} + +#[test] +fn test_sudo_set_tx_delegate_take_rate_limit() { + new_test_ext().execute_with(|| { + let to_be_set: u64 = 10; + let init_value: u64 = SubtensorModule::get_tx_delegate_take_rate_limit(); + assert_eq!( + AdminUtils::sudo_set_tx_delegate_take_rate_limit( + <::RuntimeOrigin>::signed(U256::from(1)), + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!( + SubtensorModule::get_tx_delegate_take_rate_limit(), + init_value + ); + assert_ok!(AdminUtils::sudo_set_tx_delegate_take_rate_limit( + <::RuntimeOrigin>::root(), + to_be_set + )); + assert_eq!( + SubtensorModule::get_tx_delegate_take_rate_limit(), + to_be_set + ); + }); +} + +#[test] +fn test_sudo_set_min_delegate_take() { + new_test_ext().execute_with(|| { + let to_be_set = PerU16::from_parts(u16::MAX / 100); + let init_value = SubtensorModule::get_min_delegate_take(); + assert_eq!( + AdminUtils::sudo_set_min_delegate_take( + <::RuntimeOrigin>::signed(U256::from(1)), + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!(SubtensorModule::get_min_delegate_take(), init_value); + assert_ok!(AdminUtils::sudo_set_min_delegate_take( + <::RuntimeOrigin>::root(), + to_be_set + )); + assert_eq!( + SubtensorModule::get_min_delegate_take(), + to_be_set.deconstruct() + ); + }); +} + +#[test] +fn test_sudo_set_min_childkey_take_per_subnet() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let owner = U256::from(10); + let non_owner = U256::from(11); + let take = PerU16::from_parts(SubtensorModule::get_max_childkey_take() / 2); + + add_network(netuid, 10); + SubnetOwner::::insert(netuid, owner); + + assert_eq!( + AdminUtils::sudo_set_min_childkey_take_per_subnet( + <::RuntimeOrigin>::signed(non_owner), + netuid, + take + ), + Err(DispatchError::BadOrigin) + ); + + assert_ok!(AdminUtils::sudo_set_min_childkey_take_per_subnet( + <::RuntimeOrigin>::signed(owner), + netuid, + take + )); + assert_eq!( + SubtensorModule::get_min_childkey_take_for_subnet(netuid), + take.deconstruct() + ); + assert_eq!( + SubtensorModule::get_effective_min_childkey_take(netuid), + take.deconstruct() + ); + }); +} + +#[test] +fn test_sudo_set_min_childkey_take_per_subnet_rejects_below_global() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let global_min: u16 = 100; + + add_network(netuid, 10); + SubtensorModule::set_min_childkey_take(PerU16::from_parts(global_min)); + + assert_noop!( + AdminUtils::sudo_set_min_childkey_take_per_subnet( + <::RuntimeOrigin>::root(), + netuid, + PerU16::from_parts(global_min - 1) + ), + Error::::InvalidValue + ); + assert_ok!(AdminUtils::sudo_set_min_childkey_take_per_subnet( + <::RuntimeOrigin>::root(), + netuid, + PerU16::from_parts(global_min) + )); + }); +} + +#[test] +fn test_sets_a_lower_value_clears_small_nominations() { + new_test_ext().execute_with(|| { + let hotkey: U256 = U256::from(3); + let owner_coldkey: U256 = U256::from(1); + let staker_coldkey: U256 = U256::from(2); + + let initial_nominator_min_required_stake = 10; + let nominator_min_required_stake_0 = 5; + let nominator_min_required_stake_1 = 20; + + assert!(nominator_min_required_stake_0 < nominator_min_required_stake_1); + assert!(nominator_min_required_stake_0 < initial_nominator_min_required_stake); + + let to_stake = initial_nominator_min_required_stake + 1; + + assert!(to_stake > initial_nominator_min_required_stake); + assert!(to_stake > nominator_min_required_stake_0); // Should stay when set + assert!(to_stake < nominator_min_required_stake_1); // Should be removed when set + + // ---- FIX: fund accounts so burn-based registration + staking doesn't fail. + let funds: u64 = 1_000_000_000_000_000; // 1,000,000 TAO (in RAO) + let _ = Balances::deposit_creating(&owner_coldkey, Balance::from(funds)); + let _ = Balances::deposit_creating(&staker_coldkey, Balance::from(funds)); + let _ = Balances::deposit_creating(&hotkey, Balance::from(funds)); // defensive + + // Create network + let netuid = NetUid::from(2); + add_network(netuid, 10); + + // Register a neuron + register_ok_neuron(netuid, hotkey, owner_coldkey, 0); + + let default_min_stake = pallet_subtensor::DefaultMinStake::::get(); + assert_ok!(AdminUtils::sudo_set_nominator_min_required_stake( + RuntimeOrigin::root(), + initial_nominator_min_required_stake + )); + assert_eq!( + SubtensorModule::get_nominator_min_required_stake(), + initial_nominator_min_required_stake * default_min_stake.to_u64() / 1_000_000 + ); + + // Stake to the hotkey as staker_coldkey + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &staker_coldkey, + netuid, + to_stake.into(), + ); + + let default_min_stake = pallet_subtensor::DefaultMinStake::::get(); + assert_ok!(AdminUtils::sudo_set_nominator_min_required_stake( + RuntimeOrigin::root(), + nominator_min_required_stake_0 + )); + assert_eq!( + SubtensorModule::get_nominator_min_required_stake(), + nominator_min_required_stake_0 * default_min_stake.to_u64() / 1_000_000 + ); + + // Check this nomination is not cleared + assert!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &staker_coldkey, + netuid + ) > 0.into() + ); + + assert_ok!(AdminUtils::sudo_set_nominator_min_required_stake( + RuntimeOrigin::root(), + nominator_min_required_stake_1 + )); + assert_eq!( + SubtensorModule::get_nominator_min_required_stake(), + nominator_min_required_stake_1 * default_min_stake.to_u64() / 1_000_000 + ); + + // Check this nomination is cleared + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &staker_coldkey, + netuid + ), + 0.into() + ); + }); +} + +#[test] +fn test_sudo_set_owner_cut_enabled() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(11); + let owner = U256::from(1234); + let call = RuntimeCall::AdminUtils(crate::Call::sudo_set_owner_cut_enabled { + netuid, + enabled: false, + }); + + add_network(netuid, 10); + SubnetOwner::::insert(netuid, owner); + + assert_ok!(AdminUtils::sudo_set_admin_freeze_window( + <::RuntimeOrigin>::root(), + 0 + )); + + let dispatch_info = call.get_dispatch_info(); + assert_eq!(dispatch_info.pays_fee, Pays::Yes); + + assert!(SubtensorModule::get_owner_cut_enabled(netuid)); + assert_ok!(AdminUtils::sudo_set_owner_cut_enabled( + <::RuntimeOrigin>::signed(owner), + netuid, + false + )); + assert!(!SubtensorModule::get_owner_cut_enabled(netuid)); + + assert_ok!(AdminUtils::sudo_set_owner_cut_enabled( + <::RuntimeOrigin>::root(), + netuid, + true + )); + assert!(SubtensorModule::get_owner_cut_enabled(netuid)); + }); +} + +#[test] +fn test_sudo_set_owner_cut_auto_lock_enabled() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(11); + let owner = U256::from(1234); + let non_owner = U256::from(4321); + let call = RuntimeCall::AdminUtils(crate::Call::sudo_set_owner_cut_auto_lock_enabled { + netuid, + enabled: true, + }); + + add_network(netuid, 10); + SubnetOwner::::insert(netuid, owner); + + assert_ok!(AdminUtils::sudo_set_admin_freeze_window( + <::RuntimeOrigin>::root(), + 0 + )); + + let dispatch_info = call.get_dispatch_info(); + assert_eq!(dispatch_info.pays_fee, Pays::Yes); + + assert!(!SubtensorModule::get_owner_cut_auto_lock_enabled(netuid)); + assert_noop!( + AdminUtils::sudo_set_owner_cut_auto_lock_enabled( + <::RuntimeOrigin>::signed(non_owner), + netuid, + true + ), + DispatchError::BadOrigin + ); + + assert_ok!(AdminUtils::sudo_set_owner_cut_auto_lock_enabled( + <::RuntimeOrigin>::signed(owner), + netuid, + false + )); + assert!(!SubtensorModule::get_owner_cut_auto_lock_enabled(netuid)); + + assert_ok!(AdminUtils::sudo_set_owner_cut_auto_lock_enabled( + <::RuntimeOrigin>::root(), + netuid, + true + )); + assert!(SubtensorModule::get_owner_cut_auto_lock_enabled(netuid)); + }); +} diff --git a/pallets/admin-utils/src/tests/subnet_owner_misc.rs b/pallets/admin-utils/src/tests/subnet_owner_misc.rs new file mode 100644 index 0000000000..e029cf4572 --- /dev/null +++ b/pallets/admin-utils/src/tests/subnet_owner_misc.rs @@ -0,0 +1,318 @@ +//! Subnet owner hotkey, moving alpha, EMA halving, dissolve schedule, coldkey-swap delays, max epochs. +#![allow( + clippy::unwrap_used, + clippy::expect_used, + clippy::indexing_slicing, + unused_imports +)] + +use super::prelude::*; + +#[test] +fn test_sudo_set_coldkey_swap_announcement_delay() { + new_test_ext().execute_with(|| { + // Arrange + let root = RuntimeOrigin::root(); + let non_root = RuntimeOrigin::signed(U256::from(1)); + let new_delay = 100u32.into(); + + // Act & Assert: Non-root account should fail + assert_noop!( + AdminUtils::sudo_set_coldkey_swap_announcement_delay(non_root, new_delay), + DispatchError::BadOrigin + ); + + // Act: Root account should succeed + assert_ok!(AdminUtils::sudo_set_coldkey_swap_announcement_delay( + root.clone(), + new_delay + )); + + // Assert: Check if the delay was actually set + assert_eq!( + pallet_subtensor::ColdkeySwapAnnouncementDelay::::get(), + new_delay + ); + + // Act & Assert: Setting the same value again should succeed (idempotent operation) + assert_ok!(AdminUtils::sudo_set_coldkey_swap_announcement_delay( + root, new_delay + )); + + // You might want to check for events here if your pallet emits them + System::assert_last_event(Event::ColdkeySwapAnnouncementDelaySet(new_delay).into()); + }); +} + +#[test] +fn test_sudo_set_coldkey_swap_reannouncement_delay() { + new_test_ext().execute_with(|| { + // Arrange + let root = RuntimeOrigin::root(); + let non_root = RuntimeOrigin::signed(U256::from(1)); + let new_delay = 100u32.into(); + + // Act & Assert: Non-root account should fail + assert_noop!( + AdminUtils::sudo_set_coldkey_swap_reannouncement_delay(non_root, new_delay), + DispatchError::BadOrigin + ); + + // Act: Root account should succeed + assert_ok!(AdminUtils::sudo_set_coldkey_swap_reannouncement_delay( + root.clone(), + new_delay + )); + + // Assert: Check if the delay was actually set + assert_eq!( + pallet_subtensor::ColdkeySwapReannouncementDelay::::get(), + new_delay + ); + + // Act & Assert: Setting the same value again should succeed (idempotent operation) + assert_ok!(AdminUtils::sudo_set_coldkey_swap_reannouncement_delay( + root, new_delay + )); + + // You might want to check for events here if your pallet emits them + System::assert_last_event(Event::ColdkeySwapReannouncementDelaySet(new_delay).into()); + }); +} + +#[test] +fn test_sudo_set_max_epochs_per_block() { + new_test_ext().execute_with(|| { + let root = RuntimeOrigin::root(); + let non_root = RuntimeOrigin::signed(U256::from(1)); + let init_value = SubtensorModule::get_max_epochs_per_block(); + let to_be_set: u8 = init_value.saturating_add(3); + + // Non-root is rejected and leaves the value untouched. + assert_noop!( + AdminUtils::sudo_set_max_epochs_per_block(non_root, to_be_set), + DispatchError::BadOrigin + ); + assert_eq!(SubtensorModule::get_max_epochs_per_block(), init_value); + + // Zero is rejected by the `>= 1` guard (a zero cap would halt all subnet epochs). + assert_noop!( + AdminUtils::sudo_set_max_epochs_per_block(root.clone(), 0u8), + Error::::ValueNotInBounds + ); + assert_eq!(SubtensorModule::get_max_epochs_per_block(), init_value); + + // Root succeeds: storage is updated and the event is emitted. + assert_ok!(AdminUtils::sudo_set_max_epochs_per_block(root, to_be_set)); + assert_eq!(SubtensorModule::get_max_epochs_per_block(), to_be_set); + System::assert_last_event(Event::MaxEpochsPerBlockSet(to_be_set).into()); + }); +} + +#[test] +fn test_sudo_set_max_epochs_per_block_changes_deferrals() { + new_test_ext().execute_with(|| { + let root = RuntimeOrigin::root(); + + // Create several subnets and force each to be "due this block". + let created: u16 = 4; + for i in 0..created { + let netuid = NetUid::from(i + 1); + add_network(netuid, 100 /*tempo*/); + pallet_subtensor::PendingEpochAt::::insert(netuid, 1); + } + + let block = SubtensorModule::get_current_block_as_u64(); + let subnets: Vec = SubtensorModule::get_all_subnet_netuids() + .into_iter() + .filter(|x| *x != NetUid::ROOT) + .collect(); + let due = subnets + .iter() + .filter(|n| SubtensorModule::should_run_epoch(**n, block)) + .count(); + assert!(due >= created as usize); + + // Tight cap (1): every due subnet beyond the first is deferred. + assert_ok!(AdminUtils::sudo_set_max_epochs_per_block(root.clone(), 1u8)); + let deferred_tight = SubtensorModule::epochs_deferred_this_block(&subnets, block).len(); + assert_eq!(deferred_tight, due.saturating_sub(1)); + + // Raising the cap above the due count clears all deferrals — proving the + // admin-set cap directly drives which epochs are deferred. + assert_ok!(AdminUtils::sudo_set_max_epochs_per_block( + root, + (due as u8).saturating_add(2) + )); + let deferred_loose = SubtensorModule::epochs_deferred_this_block(&subnets, block).len(); + assert_eq!(deferred_loose, 0); + assert!( + deferred_loose < deferred_tight, + "raising MaxEpochsPerBlock must defer fewer epochs" + ); + }); +} + +#[test] +fn test_sudo_set_dissolve_network_schedule_duration() { + new_test_ext().execute_with(|| { + // Arrange + let root = RuntimeOrigin::root(); + let non_root = RuntimeOrigin::signed(U256::from(1)); + let new_duration = 200u32.into(); + + // Act & Assert: Non-root account should fail + assert_noop!( + AdminUtils::sudo_set_dissolve_network_schedule_duration(non_root, new_duration), + DispatchError::BadOrigin + ); + + // Act: Root account should succeed + assert_ok!(AdminUtils::sudo_set_dissolve_network_schedule_duration( + root.clone(), + new_duration + )); + + // Assert: Check if the duration was actually set + assert_eq!( + pallet_subtensor::DissolveNetworkScheduleDuration::::get(), + new_duration + ); + + // Act & Assert: Setting the same value again should succeed (idempotent operation) + assert_ok!(AdminUtils::sudo_set_dissolve_network_schedule_duration( + root, + new_duration + )); + + // You might want to check for events here if your pallet emits them + System::assert_last_event(Event::DissolveNetworkScheduleDurationSet(new_duration).into()); + }); +} + +#[test] +fn test_sudo_root_sets_subnet_moving_alpha() { + new_test_ext().execute_with(|| { + let alpha: I96F32 = I96F32::saturating_from_num(0.5); + let initial = pallet_subtensor::SubnetMovingAlpha::::get(); + assert!(initial != alpha); + + assert_ok!(AdminUtils::sudo_set_subnet_moving_alpha( + <::RuntimeOrigin>::root(), + alpha + )); + + assert_eq!(pallet_subtensor::SubnetMovingAlpha::::get(), alpha); + }); +} + +// cargo test --package pallet-admin-utils --lib -- tests::test_sudo_set_ema_halving --exact --show-output +#[test] +fn test_sudo_set_ema_halving() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: u64 = 10; + add_network(netuid, 10); + + let value_before: u64 = pallet_subtensor::EMAPriceHalvingBlocks::::get(netuid); + assert_eq!( + AdminUtils::sudo_set_ema_price_halving_period( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + let value_after_0: u64 = pallet_subtensor::EMAPriceHalvingBlocks::::get(netuid); + assert_eq!(value_after_0, value_before); + + let owner = U256::from(10); + pallet_subtensor::SubnetOwner::::insert(netuid, owner); + assert_eq!( + AdminUtils::sudo_set_ema_price_halving_period( + <::RuntimeOrigin>::signed(owner), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + let value_after_1: u64 = pallet_subtensor::EMAPriceHalvingBlocks::::get(netuid); + assert_eq!(value_after_1, value_before); + assert_ok!(AdminUtils::sudo_set_ema_price_halving_period( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + let value_after_2: u64 = pallet_subtensor::EMAPriceHalvingBlocks::::get(netuid); + assert_eq!(value_after_2, to_be_set); + }); +} + +// cargo test --package pallet-admin-utils --lib -- tests::test_set_sn_owner_hotkey --exact --show-output +#[test] +fn test_set_sn_owner_hotkey_owner() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let hotkey: U256 = U256::from(3); + let bad_origin_coldkey: U256 = U256::from(4); + add_network(netuid, 10); + + let owner = U256::from(10); + pallet_subtensor::SubnetOwner::::insert(netuid, owner); + + // Non-owner and non-root cannot set the sn owner hotkey + assert_eq!( + AdminUtils::sudo_set_sn_owner_hotkey( + <::RuntimeOrigin>::signed(bad_origin_coldkey), + netuid, + hotkey + ), + Err(DispatchError::BadOrigin) + ); + + // SN owner can set the hotkey + assert_ok!(AdminUtils::sudo_set_sn_owner_hotkey( + <::RuntimeOrigin>::signed(owner), + netuid, + hotkey + )); + + // Check the value + let actual_hotkey = pallet_subtensor::SubnetOwnerHotkey::::get(netuid); + assert_eq!(actual_hotkey, hotkey); + + // Cannot set again (rate limited) + assert_err!( + AdminUtils::sudo_set_sn_owner_hotkey( + <::RuntimeOrigin>::signed(owner), + netuid, + hotkey + ), + pallet_subtensor::Error::::TxRateLimitExceeded + ); + }); +} + +// cargo test --package pallet-admin-utils --lib -- tests::test_set_sn_owner_hotkey_root --exact --show-output +#[test] +fn test_set_sn_owner_hotkey_root() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let hotkey: U256 = U256::from(3); + add_network(netuid, 10); + + let owner = U256::from(10); + pallet_subtensor::SubnetOwner::::insert(netuid, owner); + + // Root can set the hotkey + assert_ok!(AdminUtils::sudo_set_sn_owner_hotkey( + <::RuntimeOrigin>::root(), + netuid, + hotkey + )); + + // Check the value + let actual_hotkey = pallet_subtensor::SubnetOwnerHotkey::::get(netuid); + assert_eq!(actual_hotkey, hotkey); + }); +} diff --git a/pallets/admin-utils/src/tests/uids_validators.rs b/pallets/admin-utils/src/tests/uids_validators.rs new file mode 100644 index 0000000000..10368067e5 --- /dev/null +++ b/pallets/admin-utils/src/tests/uids_validators.rs @@ -0,0 +1,654 @@ +//! Max/min allowed UIDs, validators, trim-to-max, and min non-immune UID settings. +#![allow( + clippy::unwrap_used, + clippy::expect_used, + clippy::indexing_slicing, + unused_imports +)] + +use super::prelude::*; + +#[test] +fn test_sudo_set_max_allowed_uids() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: u16 = 12; + add_network(netuid, 10); + MaxRegistrationsPerBlock::::insert(netuid, 256); + TargetRegistrationsPerInterval::::insert(netuid, 256); + + for i in 0..=8 { + let hotkey = U256::from(i * 1000); + let coldkey = U256::from(i * 1000 + i); + + let funds: u64 = 1_000_000_000_000_000; // 1,000,000 TAO (in RAO) + let _ = Balances::deposit_creating(&coldkey, Balance::from(funds)); + let _ = Balances::deposit_creating(&hotkey, Balance::from(funds)); // defensive + + register_ok_neuron(netuid, hotkey, coldkey, 0); + step_block(1); + } + + // Bad origin that is not root or subnet owner + assert_noop!( + AdminUtils::sudo_set_max_allowed_uids( + <::RuntimeOrigin>::signed(U256::from(42)), + netuid, + to_be_set + ), + DispatchError::BadOrigin + ); + + // Random netuid that doesn't exist + assert_noop!( + AdminUtils::sudo_set_max_allowed_uids( + <::RuntimeOrigin>::root(), + NetUid::from(42), + to_be_set + ), + Error::::SubnetDoesNotExist + ); + + // Trying to set max allowed uids less than min allowed uids + assert_noop!( + AdminUtils::sudo_set_max_allowed_uids( + <::RuntimeOrigin>::root(), + netuid, + SubtensorModule::get_min_allowed_uids(netuid) - 1 + ), + Error::::MaxAllowedUidsLessThanMinAllowedUids + ); + + // Trying to set max allowed uids less than current uids + assert_noop!( + AdminUtils::sudo_set_max_allowed_uids( + <::RuntimeOrigin>::root(), + netuid, + SubtensorModule::get_subnetwork_n(netuid) - 1 + ), + Error::::MaxAllowedUIdsLessThanCurrentUIds + ); + + // Trying to set max allowed uids greater than default max allowed uids + assert_noop!( + AdminUtils::sudo_set_max_allowed_uids( + <::RuntimeOrigin>::root(), + netuid, + DefaultMaxAllowedUids::::get() + 1 + ), + Error::::MaxAllowedUidsGreaterThanDefaultMaxAllowedUids + ); + + // Trying to set max allowed uids that would cause max_allowed_uids * mechanism_count > 256 + MaxAllowedUids::::insert(netuid, 8); + MechanismCountCurrent::::insert(netuid, MechId::from(32)); + let large_max_uids = 16; + assert_noop!( + AdminUtils::sudo_set_max_allowed_uids( + <::RuntimeOrigin>::root(), + netuid, + large_max_uids + ), + SubtensorError::::TooManyUIDsPerMechanism + ); + MechanismCountCurrent::::insert(netuid, MechId::from(1)); + + // Normal case + assert_ok!(AdminUtils::sudo_set_max_allowed_uids( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!(SubtensorModule::get_max_allowed_uids(netuid), to_be_set); + + // Exact current case + assert_ok!(AdminUtils::sudo_set_max_allowed_uids( + <::RuntimeOrigin>::root(), + netuid, + SubtensorModule::get_subnetwork_n(netuid) + )); + assert_eq!( + SubtensorModule::get_max_allowed_uids(netuid), + SubtensorModule::get_subnetwork_n(netuid) + ); + + // Lower bound case + SubtensorModule::set_min_allowed_uids(netuid, SubtensorModule::get_subnetwork_n(netuid)); + assert_ok!(AdminUtils::sudo_set_max_allowed_uids( + <::RuntimeOrigin>::root(), + netuid, + SubtensorModule::get_min_allowed_uids(netuid) + )); + assert_eq!( + SubtensorModule::get_max_allowed_uids(netuid), + SubtensorModule::get_min_allowed_uids(netuid) + ); + + // Upper bound case + assert_ok!(AdminUtils::sudo_set_max_allowed_uids( + <::RuntimeOrigin>::root(), + netuid, + DefaultMaxAllowedUids::::get(), + )); + assert_eq!( + SubtensorModule::get_max_allowed_uids(netuid), + DefaultMaxAllowedUids::::get() + ); + }); +} + +#[test] +fn test_sudo_set_max_allowed_validators() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: u16 = 10; + add_network(netuid, 10); + let init_value: u16 = SubtensorModule::get_max_allowed_validators(netuid); + assert_eq!( + AdminUtils::sudo_set_max_allowed_validators( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!( + AdminUtils::sudo_set_max_allowed_validators( + <::RuntimeOrigin>::root(), + netuid.next(), + to_be_set + ), + Err(Error::::SubnetDoesNotExist.into()) + ); + assert_eq!( + SubtensorModule::get_max_allowed_validators(netuid), + init_value + ); + assert_ok!(AdminUtils::sudo_set_max_allowed_validators( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!( + SubtensorModule::get_max_allowed_validators(netuid), + to_be_set + ); + }); +} + +#[test] +fn test_trim_to_max_allowed_uids() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let sn_owner = U256::from(1); + let sn_owner_hotkey1 = U256::from(2); + + add_network(netuid, 10); + SubnetOwner::::insert(netuid, sn_owner); + SubnetOwnerHotkey::::insert(netuid, sn_owner_hotkey1); + + MaxRegistrationsPerBlock::::insert(netuid, 256); + TargetRegistrationsPerInterval::::insert(netuid, 256); + ImmuneOwnerUidsLimit::::insert(netuid, 2); + // We set a low value here to make testing easier + MinAllowedUids::::set(netuid, 4); + // We define 4 mechanisms + let mechanism_count = MechId::from(4); + MechanismCountCurrent::::insert(netuid, mechanism_count); + + // Add some neurons (fund accounts + step blocks between regs). + let max_n: u16 = 16; + for i in 1..=max_n { + let n: u64 = (i as u64) * 1000; + let hotkey = U256::from(n); + let coldkey = U256::from(n + i as u64); + + let funds: u64 = 1_000_000_000_000_000; // 1,000,000 TAO (in RAO) + let _ = Balances::deposit_creating(&coldkey, Balance::from(funds)); + let _ = Balances::deposit_creating(&hotkey, Balance::from(funds)); // defensive + + register_ok_neuron(netuid, hotkey, coldkey, 0); + step_block(1); + } + + // Run some blocks to ensure stake weights are set and that we are past the immunity period + // for all neurons + let immunity_period: u64 = ImmunityPeriod::::get(netuid).into(); + let current_block: u64 = frame_system::Pallet::::block_number().into(); + run_to_block(current_block + immunity_period + 1); + + // Set some randomized values that we can keep track of + let values = vec![ + 17u16, 42u16, 8u16, 56u16, 23u16, 91u16, + 34u16, // uid 6 (34) will be forced-immune below + 77u16, 12u16, 65u16, 3u16, 88u16, 29u16, 51u16, 74u16, 39u16, + ]; + let bool_values = vec![ + false, false, false, true, false, true, true, true, false, true, false, true, false, + true, true, false, + ]; + let alpha_values = values.iter().map(|&v| (v as u64).into()).collect(); + let u64_values: Vec = values.iter().map(|&v| v as u64).collect(); + let per_values: Vec = values.iter().map(|&v| PerU16::from_parts(v)).collect(); + + Emission::::set(netuid, alpha_values); + Consensus::::insert(netuid, per_values.clone()); + Dividends::::insert(netuid, per_values.clone()); + ValidatorTrust::::insert(netuid, per_values.clone()); + StakeWeight::::insert(netuid, values.clone()); + ValidatorPermit::::insert(netuid, bool_values.clone()); + Active::::insert(netuid, bool_values); + + for mecid in 0..mechanism_count.into() { + let netuid_index = + SubtensorModule::get_mechanism_storage_index(netuid, MechId::from(mecid)); + Incentive::::insert(netuid_index, per_values.clone()); + LastUpdate::::insert(netuid_index, u64_values.clone()); + } + + // Make UID 6 temporally immune so it cannot be trimmed even though it's not a top-8 emitter. + let now = frame_system::Pallet::::block_number(); + BlockAtRegistration::::set(netuid, 6, now); + + // Set some evm addresses (include both kept + trimmed uids). Go through the normal + // setter so both the forward map and the reverse index are populated, exactly as the + // association extrinsic does in production. + let evm_addr_uid6 = sp_core::H160::from_slice(b"12345678901234567891"); + let evm_addr_uid10 = sp_core::H160::from_slice(b"12345678901234567892"); + let evm_addr_uid12 = sp_core::H160::from_slice(b"12345678901234567893"); + let evm_addr_uid14 = sp_core::H160::from_slice(b"12345678901234567894"); + SubtensorModule::set_associated_evm_address(netuid, 6, evm_addr_uid6, now); + SubtensorModule::set_associated_evm_address(netuid, 10, evm_addr_uid10, now); + SubtensorModule::set_associated_evm_address(netuid, 12, evm_addr_uid12, now); + SubtensorModule::set_associated_evm_address(netuid, 14, evm_addr_uid14, now); + + // Populate Weights and Bonds storage items to test trimming + for uid in 0..max_n { + let mut weights = Vec::new(); + let mut bonds = Vec::new(); + + // Add connections to all other uids, including those that will be trimmed + for target_uid in 0..max_n { + if target_uid != uid { + let weight_value = (uid + target_uid) % 1000; + let bond_value = (uid * target_uid) % 1000; + weights.push((target_uid, weight_value)); + bonds.push((target_uid, bond_value)); + } + } + + for mecid in 0..mechanism_count.into() { + let netuid_index = + SubtensorModule::get_mechanism_storage_index(netuid, MechId::from(mecid)); + Weights::::insert(netuid_index, uid, weights.clone()); + Bonds::::insert(netuid_index, uid, bonds.clone()); + } + } + + // Normal case + let new_max_n = 8; + assert_ok!(AdminUtils::sudo_trim_to_max_allowed_uids( + <::RuntimeOrigin>::root(), + netuid, + new_max_n + )); + + // Ensure the max allowed uids has been set correctly + assert_eq!(MaxAllowedUids::::get(netuid), new_max_n); + + // Ensure the emission has been trimmed correctly and compressed to the left + assert_eq!( + Emission::::get(netuid), + vec![ + 56.into(), + 91.into(), + 34.into(), + 77.into(), + 65.into(), + 88.into(), + 51.into(), + 74.into() + ] + ); + + // Ensure rest of (active) storage has been trimmed correctly + let expected_values: Vec = vec![56, 91, 34, 77, 65, 88, 51, 74]; + let expected_per_values: Vec = expected_values + .iter() + .map(|&v| PerU16::from_parts(v)) + .collect(); + let expected_bools = vec![true, true, true, true, true, true, true, true]; + let expected_u64_values = vec![56, 91, 34, 77, 65, 88, 51, 74]; + + assert_eq!(Active::::get(netuid), expected_bools); + assert_eq!(Consensus::::get(netuid), expected_per_values); + assert_eq!(Dividends::::get(netuid), expected_per_values); + assert_eq!(ValidatorTrust::::get(netuid), expected_per_values); + assert_eq!(ValidatorPermit::::get(netuid), expected_bools); + assert_eq!(StakeWeight::::get(netuid), expected_values); + + for mecid in 0..mechanism_count.into() { + let netuid_index = + SubtensorModule::get_mechanism_storage_index(netuid, MechId::from(mecid)); + assert_eq!(Incentive::::get(netuid_index), expected_per_values); + assert_eq!(LastUpdate::::get(netuid_index), expected_u64_values); + } + + // Ensure trimmed uids related storage has been cleared + for uid in new_max_n..max_n { + assert!(!Keys::::contains_key(netuid, uid)); + assert!(!BlockAtRegistration::::contains_key(netuid, uid)); + assert!(!AssociatedEvmAddress::::contains_key(netuid, uid)); + for mecid in 0..mechanism_count.into() { + let netuid_index = + SubtensorModule::get_mechanism_storage_index(netuid, MechId::from(mecid)); + assert!(!Weights::::contains_key(netuid_index, uid)); + assert!(!Bonds::::contains_key(netuid_index, uid)); + } + } + + // Ensure trimmed uids hotkey related storage has been cleared + let trimmed_hotkeys = vec![ + U256::from(1000), + U256::from(2000), + U256::from(3000), + U256::from(5000), + U256::from(9000), + U256::from(11000), + U256::from(13000), + U256::from(16000), + ]; + for hotkey in trimmed_hotkeys { + assert!(!Uids::::contains_key(netuid, hotkey)); + assert!(!IsNetworkMember::::contains_key(hotkey, netuid)); + assert!(!LastHotkeyEmissionOnNetuid::::contains_key( + hotkey, netuid + )); + assert!(!AlphaDividendsPerSubnet::::contains_key( + netuid, hotkey + )); + assert!(!Axons::::contains_key(netuid, hotkey)); + assert!(!NeuronCertificates::::contains_key(netuid, hotkey)); + assert!(!Prometheus::::contains_key(netuid, hotkey)); + } + + // Ensure trimmed uids weights and bonds connections have been trimmed correctly + for uid in 0..new_max_n { + for mecid in 0..mechanism_count.into() { + let netuid_index = + SubtensorModule::get_mechanism_storage_index(netuid, MechId::from(mecid)); + assert!( + Weights::::get(netuid_index, uid) + .iter() + .all(|(target_uid, _)| *target_uid < new_max_n), + "Found a weight with target_uid >= new_max_n" + ); + assert!( + Bonds::::get(netuid_index, uid) + .iter() + .all(|(target_uid, _)| *target_uid < new_max_n), + "Found a bond with target_uid >= new_max_n" + ); + } + } + + // Actual number of neurons on the network updated after trimming + assert_eq!(SubnetworkN::::get(netuid), new_max_n); + + // Uids match enumeration order + for i in 0..new_max_n.into() { + let hotkey = Keys::::get(netuid, i); + let uid = Uids::::get(netuid, hotkey); + assert_eq!(uid, Some(i)); + } + + // EVM association have been remapped correctly (uids: 6 -> 2, 14 -> 7) + assert_eq!( + AssociatedEvmAddress::::get(netuid, 2), + Some((evm_addr_uid6, now)) + ); + assert_eq!( + AssociatedEvmAddress::::get(netuid, 7), + Some((evm_addr_uid14, now)) + ); + + // The reverse index has been remapped in place to the new UIDs (6 -> 2, 14 -> 7), + // without rebuilding it from scratch. + assert_eq!( + AssociatedUidsByEvmAddress::::get(netuid, evm_addr_uid6).into_inner(), + vec![(2u16, now)] + ); + assert_eq!( + AssociatedUidsByEvmAddress::::get(netuid, evm_addr_uid14).into_inner(), + vec![(7u16, now)] + ); + // Trimmed UIDs (10, 12) were dropped from the reverse index entirely. + assert!(AssociatedUidsByEvmAddress::::get(netuid, evm_addr_uid10).is_empty()); + assert!(AssociatedUidsByEvmAddress::::get(netuid, evm_addr_uid12).is_empty()); + // uid_lookup resolves the remapped UID. + assert_eq!( + SubtensorModule::uid_lookup(netuid, evm_addr_uid6, u16::MAX), + vec![(2u16, now)] + ); + + // Non existent subnet + assert_err!( + AdminUtils::sudo_trim_to_max_allowed_uids( + <::RuntimeOrigin>::root(), + NetUid::from(42), + new_max_n + ), + pallet_subtensor::Error::::SubnetNotExists + ); + + // New max n less than lower bound + assert_err!( + AdminUtils::sudo_trim_to_max_allowed_uids( + <::RuntimeOrigin>::root(), + netuid, + 2 + ), + pallet_subtensor::Error::::InvalidValue + ); + + // New max n greater than upper bound + assert_err!( + AdminUtils::sudo_trim_to_max_allowed_uids( + <::RuntimeOrigin>::root(), + netuid, + SubtensorModule::get_max_allowed_uids(netuid) + 1 + ), + pallet_subtensor::Error::::InvalidValue + ); + }); +} + +#[test] +fn test_trim_to_max_allowed_uids_too_many_immune() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let sn_owner = U256::from(1); + add_network(netuid, 10); + SubnetOwner::::insert(netuid, sn_owner); + MaxRegistrationsPerBlock::::insert(netuid, 256); + TargetRegistrationsPerInterval::::insert(netuid, 256); + ImmuneOwnerUidsLimit::::insert(netuid, 2); + MinAllowedUids::::set(netuid, 2); + + // Add 5 neurons (fund + step blocks between regs) + let max_n = 5; + for i in 1..=max_n { + let n = i * 1000; + let hotkey = U256::from(n); + let coldkey = U256::from(n + i); + + let funds: u64 = 1_000_000_000_000_000; // 1,000,000 TAO (in RAO) + let _ = Balances::deposit_creating(&coldkey, Balance::from(funds)); + let _ = Balances::deposit_creating(&hotkey, Balance::from(funds)); // defensive + + register_ok_neuron(netuid, hotkey, coldkey, 0); + step_block(1); + } + + // Run some blocks to ensure stake weights are set + run_to_block((ImmunityPeriod::::get(netuid) + 1).into()); + + // Set owner immune uids (2 UIDs) by adding them to OwnedHotkeys + let owner_hotkey1 = U256::from(1000); + let owner_hotkey2 = U256::from(2000); + OwnedHotkeys::::insert(sn_owner, vec![owner_hotkey1, owner_hotkey2]); + Keys::::insert(netuid, 0, owner_hotkey1); + Uids::::insert(netuid, owner_hotkey1, 0); + Keys::::insert(netuid, 1, owner_hotkey2); + Uids::::insert(netuid, owner_hotkey2, 1); + + // Set temporally immune uids (2 UIDs) to make total immune count 4 out of 5 (80%) + // Set their registration block to current block to make them temporally immune + let current_block = frame_system::Pallet::::block_number(); + for uid in 2..4 { + let hotkey = U256::from(uid * 1000 + 1000); + Keys::::insert(netuid, uid, hotkey); + Uids::::insert(netuid, hotkey, uid); + BlockAtRegistration::::insert(netuid, uid, current_block); + } + + // Try to trim to 4 UIDs - this should fail because 4/4 = 100% immune (>= 80%) + assert_err!( + AdminUtils::sudo_trim_to_max_allowed_uids( + <::RuntimeOrigin>::root(), + netuid, + 4 + ), + pallet_subtensor::Error::::TrimmingWouldExceedMaxImmunePercentage + ); + + // Try to trim to 3 UIDs - this should also fail because 4/3 > 80% immune (>= 80%) + assert_err!( + AdminUtils::sudo_trim_to_max_allowed_uids( + <::RuntimeOrigin>::root(), + netuid, + 3 + ), + pallet_subtensor::Error::::TrimmingWouldExceedMaxImmunePercentage + ); + + // Now test a scenario where trimming should succeed + // Remove one immune UID to make it 3 immune out of 4 total + let uid_to_remove = 3; + let hotkey_to_remove = U256::from(uid_to_remove * 1000 + 1000); + #[allow(unknown_lints)] + Keys::::remove(netuid, uid_to_remove); + Uids::::remove(netuid, hotkey_to_remove); + BlockAtRegistration::::remove(netuid, uid_to_remove); + + // Remove another immune UID to make it 2 immune out of 3 total + let uid_to_remove2 = 2; + let hotkey_to_remove2 = U256::from(uid_to_remove2 * 1000 + 1000); + #[allow(unknown_lints)] + Keys::::remove(netuid, uid_to_remove2); + Uids::::remove(netuid, hotkey_to_remove2); + BlockAtRegistration::::remove(netuid, uid_to_remove2); + + // Now we have 2 immune out of 2 total UIDs + // Try to trim to 1 UID - this should fail because 2/1 is impossible, but the check prevents it + assert_err!( + AdminUtils::sudo_trim_to_max_allowed_uids( + <::RuntimeOrigin>::root(), + netuid, + 1 + ), + pallet_subtensor::Error::::InvalidValue + ); + }); +} + +#[test] +fn test_sudo_set_min_allowed_uids() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: u16 = 8; + add_network(netuid, 10); + MaxRegistrationsPerBlock::::insert(netuid, 256); + TargetRegistrationsPerInterval::::insert(netuid, 256); + + for i in 0..=16 { + let hotkey = U256::from(i * 1000); + let coldkey = U256::from(i * 1000 + i); + + let funds: u64 = 1_000_000_000_000_000; // 1,000,000 TAO (in RAO) + let _ = Balances::deposit_creating(&coldkey, Balance::from(funds)); + let _ = Balances::deposit_creating(&hotkey, Balance::from(funds)); // defensive + + register_ok_neuron(netuid, hotkey, coldkey, 0); + step_block(1); + } + + // Normal case + assert_ok!(AdminUtils::sudo_set_min_allowed_uids( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!(SubtensorModule::get_min_allowed_uids(netuid), to_be_set); + + // Non root + assert_err!( + AdminUtils::sudo_set_min_allowed_uids( + <::RuntimeOrigin>::signed(U256::from(0)), + netuid, + to_be_set + ), + DispatchError::BadOrigin + ); + + // Non existent subnet + assert_err!( + AdminUtils::sudo_set_min_allowed_uids( + <::RuntimeOrigin>::root(), + NetUid::from(42), + to_be_set + ), + Error::::SubnetDoesNotExist + ); + + // Min allowed uids greater than max allowed uids + assert_err!( + AdminUtils::sudo_set_min_allowed_uids( + <::RuntimeOrigin>::root(), + netuid, + SubtensorModule::get_max_allowed_uids(netuid) + 1 + ), + Error::::MinAllowedUidsGreaterThanMaxAllowedUids + ); + + // Min allowed uids greater than current uids + assert_err!( + AdminUtils::sudo_set_min_allowed_uids( + <::RuntimeOrigin>::root(), + netuid, + SubtensorModule::get_subnetwork_n(netuid) + 1 + ), + Error::::MinAllowedUidsGreaterThanCurrentUids + ); + }); +} + +#[test] +fn test_sudo_set_min_non_immune_uids() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 10); + + let to_be_set: u16 = 12; + let init_value: u16 = SubtensorModule::get_min_non_immune_uids(netuid); + + assert_ok!(AdminUtils::sudo_set_min_non_immune_uids( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + + assert!(init_value != to_be_set); + assert_eq!(SubtensorModule::get_min_non_immune_uids(netuid), to_be_set); + }); +} diff --git a/pallets/admin-utils/src/tests/weights_difficulty.rs b/pallets/admin-utils/src/tests/weights_difficulty.rs new file mode 100644 index 0000000000..5265004615 --- /dev/null +++ b/pallets/admin-utils/src/tests/weights_difficulty.rs @@ -0,0 +1,381 @@ +//! Weights version/rate-limit, serving rate limit, difficulty, and adjustment hyperparams. +#![allow( + clippy::unwrap_used, + clippy::expect_used, + clippy::indexing_slicing, + unused_imports +)] + +use super::prelude::*; + +#[test] +fn test_sudo_set_serving_rate_limit() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(3); + let to_be_set: u64 = 10; + let init_value: u64 = SubtensorModule::get_serving_rate_limit(netuid); + assert_eq!( + AdminUtils::sudo_set_serving_rate_limit( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!(SubtensorModule::get_serving_rate_limit(netuid), init_value); + assert_ok!(AdminUtils::sudo_set_serving_rate_limit( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!(SubtensorModule::get_serving_rate_limit(netuid), to_be_set); + }); +} + +#[test] +fn test_sudo_set_min_difficulty() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: u64 = 10; + add_network(netuid, 10); + let init_value: u64 = SubtensorModule::get_min_difficulty(netuid); + assert_eq!( + AdminUtils::sudo_set_min_difficulty( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!( + AdminUtils::sudo_set_min_difficulty( + <::RuntimeOrigin>::root(), + netuid.next(), + to_be_set + ), + Err(Error::::SubnetDoesNotExist.into()) + ); + assert_eq!(SubtensorModule::get_min_difficulty(netuid), init_value); + assert_ok!(AdminUtils::sudo_set_min_difficulty( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!(SubtensorModule::get_min_difficulty(netuid), to_be_set); + }); +} + +#[test] +fn test_sudo_set_max_difficulty() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: u64 = 10; + add_network(netuid, 10); + let init_value: u64 = SubtensorModule::get_max_difficulty(netuid); + assert_eq!( + AdminUtils::sudo_set_max_difficulty( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!( + AdminUtils::sudo_set_max_difficulty( + <::RuntimeOrigin>::root(), + netuid.next(), + to_be_set + ), + Err(Error::::SubnetDoesNotExist.into()) + ); + assert_eq!(SubtensorModule::get_max_difficulty(netuid), init_value); + assert_ok!(AdminUtils::sudo_set_max_difficulty( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!(SubtensorModule::get_max_difficulty(netuid), to_be_set); + }); +} + +#[test] +fn test_sudo_set_weights_version_key() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: u64 = 10; + add_network(netuid, 10); + let init_value: u64 = SubtensorModule::get_weights_version_key(netuid); + assert_eq!( + AdminUtils::sudo_set_weights_version_key( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!( + AdminUtils::sudo_set_weights_version_key( + <::RuntimeOrigin>::root(), + netuid.next(), + to_be_set + ), + Err(Error::::SubnetDoesNotExist.into()) + ); + assert_eq!(SubtensorModule::get_weights_version_key(netuid), init_value); + assert_ok!(AdminUtils::sudo_set_weights_version_key( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!(SubtensorModule::get_weights_version_key(netuid), to_be_set); + }); +} + +#[test] +fn test_sudo_set_weights_version_key_rate_limit() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: u64 = 10; + + let sn_owner = U256::from(1); + add_network(netuid, 10); + // Set the Subnet Owner + SubnetOwner::::insert(netuid, sn_owner); + + let rate_limit = WeightsVersionKeyRateLimit::::get(); + let tempo = Tempo::::get(netuid); + + let rate_limit_period = rate_limit * (tempo as u64); + + assert_ok!(AdminUtils::sudo_set_weights_version_key( + <::RuntimeOrigin>::signed(sn_owner), + netuid, + to_be_set + )); + assert_eq!(SubtensorModule::get_weights_version_key(netuid), to_be_set); + + // Try to set again with + // Assert rate limit not passed + assert!( + !TransactionType::SetWeightsVersionKey + .passes_rate_limit_on_subnet::(&sn_owner, netuid) + ); + + // Try transaction + assert_noop!( + AdminUtils::sudo_set_weights_version_key( + <::RuntimeOrigin>::signed(sn_owner), + netuid, + to_be_set + 1 + ), + pallet_subtensor::Error::::TxRateLimitExceeded + ); + + // Wait for rate limit to pass + run_to_block(rate_limit_period + 1); + assert!( + TransactionType::SetWeightsVersionKey + .passes_rate_limit_on_subnet::(&sn_owner, netuid) + ); + + // Try transaction + assert_ok!(AdminUtils::sudo_set_weights_version_key( + <::RuntimeOrigin>::signed(sn_owner), + netuid, + to_be_set + 1 + )); + assert_eq!( + SubtensorModule::get_weights_version_key(netuid), + to_be_set + 1 + ); + }); +} + +#[test] +fn test_sudo_set_weights_version_key_rate_limit_root() { + // root should not be effected by rate limit + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: u64 = 10; + + let sn_owner = U256::from(1); + add_network(netuid, 10); + // Set the Subnet Owner + SubnetOwner::::insert(netuid, sn_owner); + + let rate_limit = WeightsVersionKeyRateLimit::::get(); + let tempo: u16 = Tempo::::get(netuid); + + let rate_limit_period = rate_limit * (tempo as u64); + // Verify the rate limit is more than 0 blocks + assert!(rate_limit_period > 0); + + assert_ok!(AdminUtils::sudo_set_weights_version_key( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!(SubtensorModule::get_weights_version_key(netuid), to_be_set); + + // Try transaction + assert_ok!(AdminUtils::sudo_set_weights_version_key( + <::RuntimeOrigin>::signed(sn_owner), + netuid, + to_be_set + 1 + )); + assert_eq!( + SubtensorModule::get_weights_version_key(netuid), + to_be_set + 1 + ); + }); +} + +#[test] +fn test_sudo_set_weights_set_rate_limit() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: u64 = 10; + add_network(netuid, 10); + let init_value: u64 = SubtensorModule::get_weights_set_rate_limit(netuid); + assert_eq!( + AdminUtils::sudo_set_weights_set_rate_limit( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!( + AdminUtils::sudo_set_weights_set_rate_limit( + <::RuntimeOrigin>::root(), + netuid.next(), + to_be_set + ), + Err(Error::::SubnetDoesNotExist.into()) + ); + assert_eq!( + SubtensorModule::get_weights_set_rate_limit(netuid), + init_value + ); + assert_ok!(AdminUtils::sudo_set_weights_set_rate_limit( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!( + SubtensorModule::get_weights_set_rate_limit(netuid), + to_be_set + ); + }); +} + +#[test] +fn test_sudo_set_adjustment_interval() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: u16 = 10; + add_network(netuid, 10); + let init_value: u16 = SubtensorModule::get_adjustment_interval(netuid); + assert_eq!( + AdminUtils::sudo_set_adjustment_interval( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!( + AdminUtils::sudo_set_adjustment_interval( + <::RuntimeOrigin>::root(), + netuid.next(), + to_be_set + ), + Err(Error::::SubnetDoesNotExist.into()) + ); + assert_eq!(SubtensorModule::get_adjustment_interval(netuid), init_value); + assert_ok!(AdminUtils::sudo_set_adjustment_interval( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!(SubtensorModule::get_adjustment_interval(netuid), to_be_set); + }); +} + +#[test] +fn test_sudo_set_adjustment_alpha() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: u64 = 10; + add_network(netuid, 10); + let init_value: u64 = SubtensorModule::get_adjustment_alpha(netuid); + assert_eq!( + AdminUtils::sudo_set_adjustment_alpha( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!( + AdminUtils::sudo_set_adjustment_alpha( + <::RuntimeOrigin>::root(), + netuid.next(), + to_be_set + ), + Err(Error::::SubnetDoesNotExist.into()) + ); + assert_eq!(SubtensorModule::get_adjustment_alpha(netuid), init_value); + assert_ok!(AdminUtils::sudo_set_adjustment_alpha( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!(SubtensorModule::get_adjustment_alpha(netuid), to_be_set); + }); +} + +#[test] +fn test_sudo_set_difficulty() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let to_be_set: u64 = 10; + add_network(netuid, 10); + let init_value: u64 = SubtensorModule::get_difficulty_as_u64(netuid); + assert_eq!( + AdminUtils::sudo_set_difficulty( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!( + AdminUtils::sudo_set_difficulty( + <::RuntimeOrigin>::root(), + netuid.next(), + to_be_set + ), + Err(Error::::SubnetDoesNotExist.into()) + ); + assert_eq!(SubtensorModule::get_difficulty_as_u64(netuid), init_value); + assert_ok!(AdminUtils::sudo_set_difficulty( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + assert_eq!(SubtensorModule::get_difficulty_as_u64(netuid), to_be_set); + + // Test that SN owner can't set difficulty + pallet_subtensor::SubnetOwner::::insert(netuid, U256::from(1)); + assert_eq!( + AdminUtils::sudo_set_difficulty( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + init_value + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!(SubtensorModule::get_difficulty_as_u64(netuid), to_be_set); // no change + }); +} diff --git a/pallets/alpha-assets/src/alpha_imbalance.rs b/pallets/alpha-assets/src/alpha_imbalance.rs new file mode 100644 index 0000000000..9a92d63fdc --- /dev/null +++ b/pallets/alpha-assets/src/alpha_imbalance.rs @@ -0,0 +1,249 @@ +//! Netuid-scoped alpha mint/burn imbalances for FRAME `Imbalance` accounting. +//! +//! A non-zero drop does **not** auto-credit or debit subnet pools; coinbase / staking +//! code must resolve a [`PositiveAlphaImbalance`] (e.g. into alpha-in or alpha-out). + +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::traits::{Imbalance, SameOrOther, TryDrop, tokens::imbalance::TryMerge}; +use scale_info::TypeInfo; +use sp_runtime::traits::Zero; +use subtensor_macros::freeze_struct; +use subtensor_runtime_common::{AlphaBalance, NetUid, Token}; + +/// Pending alpha mint for one subnet; callers resolve it into alpha-in or alpha-out. +/// +/// Amounts are in rao of alpha. Merge / offset across different `netuid`s is rejected +/// (logs and keeps the left-hand side) so subnet ledgers never mix. +#[freeze_struct("10d20e374f3d3dc0")] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] +pub struct PositiveAlphaImbalance { + netuid: NetUid, + amount: AlphaBalance, +} + +/// Opposite of [`PositiveAlphaImbalance`]: pending alpha debit for one subnet. +/// +/// Produced by [`Imbalance::offset`] when a negative imbalance exceeds a positive one. +/// Same netuid-isolation rules as the positive side. +#[freeze_struct("ff6feb7c6031d9d6")] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] +pub struct NegativeAlphaImbalance { + netuid: NetUid, + amount: AlphaBalance, +} + +impl PositiveAlphaImbalance { + /// Builds a mint imbalance for `netuid` with the given alpha `amount` (rao). + pub fn new(netuid: NetUid, amount: AlphaBalance) -> Self { + Self { netuid, amount } + } + + /// Subnet this mint is scoped to. + pub fn netuid(&self) -> NetUid { + self.netuid + } + + /// Alpha amount (rao) still carried by this imbalance. + pub fn amount(&self) -> AlphaBalance { + self.amount + } +} + +impl NegativeAlphaImbalance { + /// Builds a debit imbalance for `netuid` with the given alpha `amount` (rao). + pub fn new(netuid: NetUid, amount: AlphaBalance) -> Self { + Self { netuid, amount } + } +} + +/// Logs when imbalance ops attempt to combine different subnet ledgers. +fn log_cross_netuid_alpha_imbalance(context: &'static str, left: NetUid, right: NetUid) { + log::error!( + target: "runtime::alpha-assets", + "{context}: attempted to combine alpha imbalances from different netuids: left={left}, right={right}" + ); +} + +impl TryDrop for PositiveAlphaImbalance { + fn try_drop(self) -> Result<(), Self> { + if self.amount.is_zero() { + Ok(()) + } else { + Err(self) + } + } +} + +impl TryDrop for NegativeAlphaImbalance { + fn try_drop(self) -> Result<(), Self> { + if self.amount.is_zero() { + Ok(()) + } else { + Err(self) + } + } +} + +impl TryMerge for PositiveAlphaImbalance { + fn try_merge(self, other: Self) -> Result { + if self.netuid == other.netuid { + Ok(Self::new( + self.netuid, + self.amount.saturating_add(other.amount), + )) + } else { + Err((self, other)) + } + } +} + +impl TryMerge for NegativeAlphaImbalance { + fn try_merge(self, other: Self) -> Result { + if self.netuid == other.netuid { + Ok(Self::new( + self.netuid, + self.amount.saturating_add(other.amount), + )) + } else { + Err((self, other)) + } + } +} + +impl Imbalance for PositiveAlphaImbalance { + type Opposite = NegativeAlphaImbalance; + + fn zero() -> Self { + Self::default() + } + + fn drop_zero(self) -> Result<(), Self> { + self.try_drop() + } + + fn split(self, amount: AlphaBalance) -> (Self, Self) { + let first = self.amount.min(amount); + let second = self.amount.saturating_sub(first); + ( + Self::new(self.netuid, first), + Self::new(self.netuid, second), + ) + } + + fn extract(&mut self, amount: AlphaBalance) -> Self { + let extracted = self.amount.min(amount); + self.amount = self.amount.saturating_sub(extracted); + Self::new(self.netuid, extracted) + } + + fn merge(self, other: Self) -> Self { + match self.try_merge(other) { + Ok(merged) => merged, + Err((left, right)) => { + log_cross_netuid_alpha_imbalance("merge(positive)", left.netuid, right.netuid); + left + } + } + } + + fn subsume(&mut self, other: Self) { + if self.netuid != other.netuid { + log_cross_netuid_alpha_imbalance("subsume(positive)", self.netuid, other.netuid); + return; + } + self.amount = self.amount.saturating_add(other.amount); + } + + fn offset(self, other: Self::Opposite) -> SameOrOther { + if self.netuid != other.netuid { + log_cross_netuid_alpha_imbalance("offset(positive)", self.netuid, other.netuid); + return SameOrOther::Same(self); + } + if self.amount > other.amount { + SameOrOther::Same(Self::new( + self.netuid, + self.amount.saturating_sub(other.amount), + )) + } else if other.amount > self.amount { + SameOrOther::Other(NegativeAlphaImbalance::new( + self.netuid, + other.amount.saturating_sub(self.amount), + )) + } else { + SameOrOther::None + } + } + + fn peek(&self) -> AlphaBalance { + self.amount + } +} + +impl Imbalance for NegativeAlphaImbalance { + type Opposite = PositiveAlphaImbalance; + + fn zero() -> Self { + Self::default() + } + + fn drop_zero(self) -> Result<(), Self> { + self.try_drop() + } + + fn split(self, amount: AlphaBalance) -> (Self, Self) { + let first = self.amount.min(amount); + let second = self.amount.saturating_sub(first); + ( + Self::new(self.netuid, first), + Self::new(self.netuid, second), + ) + } + + fn extract(&mut self, amount: AlphaBalance) -> Self { + let extracted = self.amount.min(amount); + self.amount = self.amount.saturating_sub(extracted); + Self::new(self.netuid, extracted) + } + + fn merge(self, other: Self) -> Self { + match self.try_merge(other) { + Ok(merged) => merged, + Err((left, right)) => { + log_cross_netuid_alpha_imbalance("merge(negative)", left.netuid, right.netuid); + left + } + } + } + + fn subsume(&mut self, other: Self) { + if self.netuid != other.netuid { + log_cross_netuid_alpha_imbalance("subsume(negative)", self.netuid, other.netuid); + return; + } + self.amount = self.amount.saturating_add(other.amount); + } + + fn offset(self, other: Self::Opposite) -> SameOrOther { + if self.netuid != other.netuid { + log_cross_netuid_alpha_imbalance("offset(negative)", self.netuid, other.netuid); + return SameOrOther::Same(self); + } + if self.amount > other.amount { + SameOrOther::Same(Self::new( + self.netuid, + self.amount.saturating_sub(other.amount), + )) + } else if other.amount > self.amount { + SameOrOther::Other(PositiveAlphaImbalance::new( + self.netuid, + other.amount.saturating_sub(self.amount), + )) + } else { + SameOrOther::None + } + } + + fn peek(&self) -> AlphaBalance { + self.amount + } +} diff --git a/pallets/alpha-assets/src/lib.rs b/pallets/alpha-assets/src/lib.rs index 6e856975aa..83507fdaf7 100644 --- a/pallets/alpha-assets/src/lib.rs +++ b/pallets/alpha-assets/src/lib.rs @@ -1,3 +1,12 @@ +//! # Alpha Assets +//! +//! Tracks per-subnet alpha issuance, burns, and recycles, and exposes mint/burn/recycle +//! through [`AlphaAssetsInterface`] so coinbase and staking can stay loosely coupled. +//! +//! This pallet has no extrinsics: all mutations go through the interface / `Pallet` helpers. +//! Issued alpha is represented as a [`PositiveAlphaImbalance`] that must be resolved by the +//! caller (it does not auto-apply on drop). + #![cfg_attr(not(feature = "std"), no_std)] #[cfg(test)] @@ -5,250 +14,35 @@ mod mock; #[cfg(test)] mod tests; -use codec::{Decode, Encode, MaxEncodedLen}; -use frame_support::pallet_prelude::*; -use frame_support::traits::{Imbalance, SameOrOther, TryDrop, tokens::imbalance::TryMerge}; -use scale_info::TypeInfo; -use sp_runtime::traits::Zero; -use subtensor_macros::freeze_struct; -use subtensor_runtime_common::{AlphaBalance, NetUid, Token}; +mod alpha_imbalance; +pub use alpha_imbalance::{NegativeAlphaImbalance, PositiveAlphaImbalance}; pub use pallet::*; -/// Lightweight mint record that can later be resolved to a subnet or user alpha balance. -#[freeze_struct("2da64a64e80a7880")] -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] -pub struct PositiveAlphaImbalance { - netuid: NetUid, - amount: AlphaBalance, -} - -#[freeze_struct("1f16c8937e05cf36")] -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] -pub struct NegativeAlphaImbalance { - netuid: NetUid, - amount: AlphaBalance, -} - -impl PositiveAlphaImbalance { - pub fn new(netuid: NetUid, amount: AlphaBalance) -> Self { - Self { netuid, amount } - } - - pub fn netuid(&self) -> NetUid { - self.netuid - } - - pub fn amount(&self) -> AlphaBalance { - self.amount - } -} - -impl NegativeAlphaImbalance { - pub fn new(netuid: NetUid, amount: AlphaBalance) -> Self { - Self { netuid, amount } - } -} - -fn log_netuid_mismatch(context: &'static str, left: NetUid, right: NetUid) { - log::error!( - target: "runtime::alpha-assets", - "{context}: attempted to combine alpha imbalances from different netuids: left={left}, right={right}" - ); -} - -impl TryDrop for PositiveAlphaImbalance { - fn try_drop(self) -> Result<(), Self> { - if self.amount.is_zero() { - Ok(()) - } else { - Err(self) - } - } -} - -impl TryDrop for NegativeAlphaImbalance { - fn try_drop(self) -> Result<(), Self> { - if self.amount.is_zero() { - Ok(()) - } else { - Err(self) - } - } -} - -impl TryMerge for PositiveAlphaImbalance { - fn try_merge(self, other: Self) -> Result { - if self.netuid == other.netuid { - Ok(Self::new( - self.netuid, - self.amount.saturating_add(other.amount), - )) - } else { - Err((self, other)) - } - } -} - -impl TryMerge for NegativeAlphaImbalance { - fn try_merge(self, other: Self) -> Result { - if self.netuid == other.netuid { - Ok(Self::new( - self.netuid, - self.amount.saturating_add(other.amount), - )) - } else { - Err((self, other)) - } - } -} - -impl Imbalance for PositiveAlphaImbalance { - type Opposite = NegativeAlphaImbalance; - - fn zero() -> Self { - Self::default() - } - - fn drop_zero(self) -> Result<(), Self> { - self.try_drop() - } - - fn split(self, amount: AlphaBalance) -> (Self, Self) { - let first = self.amount.min(amount); - let second = self.amount.saturating_sub(first); - ( - Self::new(self.netuid, first), - Self::new(self.netuid, second), - ) - } - - fn extract(&mut self, amount: AlphaBalance) -> Self { - let extracted = self.amount.min(amount); - self.amount = self.amount.saturating_sub(extracted); - Self::new(self.netuid, extracted) - } - - fn merge(self, other: Self) -> Self { - match self.try_merge(other) { - Ok(merged) => merged, - Err((left, right)) => { - log_netuid_mismatch("merge(positive)", left.netuid, right.netuid); - left - } - } - } - - fn subsume(&mut self, other: Self) { - if self.netuid != other.netuid { - log_netuid_mismatch("subsume(positive)", self.netuid, other.netuid); - return; - } - self.amount = self.amount.saturating_add(other.amount); - } - - fn offset(self, other: Self::Opposite) -> SameOrOther { - if self.netuid != other.netuid { - log_netuid_mismatch("offset(positive)", self.netuid, other.netuid); - return SameOrOther::Same(self); - } - if self.amount > other.amount { - SameOrOther::Same(Self::new( - self.netuid, - self.amount.saturating_sub(other.amount), - )) - } else if other.amount > self.amount { - SameOrOther::Other(NegativeAlphaImbalance::new( - self.netuid, - other.amount.saturating_sub(self.amount), - )) - } else { - SameOrOther::None - } - } - - fn peek(&self) -> AlphaBalance { - self.amount - } -} - -impl Imbalance for NegativeAlphaImbalance { - type Opposite = PositiveAlphaImbalance; - - fn zero() -> Self { - Self::default() - } - - fn drop_zero(self) -> Result<(), Self> { - self.try_drop() - } - - fn split(self, amount: AlphaBalance) -> (Self, Self) { - let first = self.amount.min(amount); - let second = self.amount.saturating_sub(first); - ( - Self::new(self.netuid, first), - Self::new(self.netuid, second), - ) - } - - fn extract(&mut self, amount: AlphaBalance) -> Self { - let extracted = self.amount.min(amount); - self.amount = self.amount.saturating_sub(extracted); - Self::new(self.netuid, extracted) - } - - fn merge(self, other: Self) -> Self { - match self.try_merge(other) { - Ok(merged) => merged, - Err((left, right)) => { - log_netuid_mismatch("merge(negative)", left.netuid, right.netuid); - left - } - } - } - - fn subsume(&mut self, other: Self) { - if self.netuid != other.netuid { - log_netuid_mismatch("subsume(negative)", self.netuid, other.netuid); - return; - } - self.amount = self.amount.saturating_add(other.amount); - } - - fn offset(self, other: Self::Opposite) -> SameOrOther { - if self.netuid != other.netuid { - log_netuid_mismatch("offset(negative)", self.netuid, other.netuid); - return SameOrOther::Same(self); - } - if self.amount > other.amount { - SameOrOther::Same(Self::new( - self.netuid, - self.amount.saturating_sub(other.amount), - )) - } else if other.amount > self.amount { - SameOrOther::Other(PositiveAlphaImbalance::new( - self.netuid, - other.amount.saturating_sub(self.amount), - )) - } else { - SameOrOther::None - } - } - - fn peek(&self) -> AlphaBalance { - self.amount - } -} +use frame_support::pallet_prelude::*; +use sp_runtime::traits::Zero; +use subtensor_runtime_common::{AlphaBalance, NetUid, Token}; -/// Loose-coupling interface for alpha issuance operations. +/// Loose-coupling interface for alpha issuance, burn, and recycle operations. +/// +/// Runtime wiring typically binds this to [`Pallet`]; `()` is a no-op stub for tests that +/// do not need ledger side effects. pub trait AlphaAssetsInterface { + /// Current total alpha issued for `netuid` (rao), as tracked by this pallet. fn total_alpha_issuance(netuid: NetUid) -> AlphaBalance; + /// Increases [`TotalAlphaIssuance`] and returns a mint imbalance for the caller to resolve. fn mint_alpha(netuid: NetUid, amount: AlphaBalance) -> PositiveAlphaImbalance; + /// Records a burn against [`AlphaBurned`] without reducing [`TotalAlphaIssuance`]. + /// + /// Returns `amount` unchanged. Destroying circulating stake is the caller's job; this + /// only updates the burn counter. fn burn_alpha(netuid: NetUid, amount: AlphaBalance) -> AlphaBalance; + /// Records a recycle against [`AlphaRecycled`] and saturating-subtracts from issuance. + /// + /// Returns `amount` unchanged. Unlike burn, recycle shrinks [`TotalAlphaIssuance`]. fn recycle_alpha(netuid: NetUid, amount: AlphaBalance) -> AlphaBalance; } @@ -276,30 +70,36 @@ impl AlphaAssetsInterface for () { pub mod pallet { use super::*; + /// Pallet that stores per-subnet alpha issuance / burn / recycle totals. #[pallet::pallet] #[pallet::without_storage_info] pub struct Pallet(_); + /// Runtime configuration for alpha-assets (no extra associated types today). #[pallet::config] pub trait Config: frame_system::Config {} - /// Total alpha issuance tracked by the pallet. + /// Cumulative alpha minted per subnet (rao); increased by [`Pallet::mint_alpha`], + /// decreased by [`Pallet::recycle_alpha`]. #[pallet::storage] #[pallet::getter(fn total_alpha_issuance)] pub type TotalAlphaIssuance = StorageMap<_, Twox64Concat, NetUid, AlphaBalance, ValueQuery>; - /// Total alpha burned per subnet through this pallet. + /// Cumulative alpha burned per subnet (rao) via [`Pallet::burn_alpha`]. + /// + /// Burn does not decrease [`TotalAlphaIssuance`]; it only accumulates this counter. #[pallet::storage] #[pallet::getter(fn alpha_burned)] pub type AlphaBurned = StorageMap<_, Twox64Concat, NetUid, AlphaBalance, ValueQuery>; - /// Total alpha recycled per subnet through this pallet. + /// Cumulative alpha recycled per subnet (rao) via [`Pallet::recycle_alpha`]. #[pallet::storage] #[pallet::getter(fn alpha_recycled)] pub type AlphaRecycled = StorageMap<_, Twox64Concat, NetUid, AlphaBalance, ValueQuery>; } impl Pallet { + /// Mints `amount` of alpha for `netuid`, bumps issuance, and returns a resolveable imbalance. pub fn mint_alpha(netuid: NetUid, amount: AlphaBalance) -> PositiveAlphaImbalance { if !amount.is_zero() { TotalAlphaIssuance::::mutate(netuid, |issuance| { @@ -310,6 +110,7 @@ impl Pallet { PositiveAlphaImbalance::new(netuid, amount) } + /// Records `amount` as burned for `netuid` without changing total issuance. pub fn burn_alpha(netuid: NetUid, amount: AlphaBalance) -> AlphaBalance { if !amount.is_zero() { AlphaBurned::::mutate(netuid, |burned| { @@ -320,6 +121,7 @@ impl Pallet { amount } + /// Records `amount` as recycled and saturating-subtracts it from total issuance. pub fn recycle_alpha(netuid: NetUid, amount: AlphaBalance) -> AlphaBalance { if !amount.is_zero() { AlphaRecycled::::mutate(netuid, |recycled| { diff --git a/pallets/alpha-assets/src/mock.rs b/pallets/alpha-assets/src/mock.rs index e118ace555..58582ee9e1 100644 --- a/pallets/alpha-assets/src/mock.rs +++ b/pallets/alpha-assets/src/mock.rs @@ -1,3 +1,5 @@ +//! Test runtime wiring for `pallet-alpha-assets`. + #![allow(clippy::arithmetic_side_effects, clippy::expect_used)] use frame_support::derive_impl; @@ -45,6 +47,7 @@ impl system::Config for Test { impl crate::pallet::Config for Test {} +/// Fresh externalities with default `frame_system` genesis for unit tests. pub fn new_test_ext() -> sp_io::TestExternalities { let storage = frame_system::GenesisConfig::::default() .build_storage() diff --git a/pallets/alpha-assets/src/tests.rs b/pallets/alpha-assets/src/tests.rs index 48608f15a4..cf301d1dbe 100644 --- a/pallets/alpha-assets/src/tests.rs +++ b/pallets/alpha-assets/src/tests.rs @@ -1,8 +1,9 @@ +//! Unit tests for mint / burn / recycle ledger updates and imbalance merge rules. + #![allow(clippy::unwrap_used)] use frame_support::traits::{Imbalance, tokens::imbalance::TryMerge}; -use subtensor_runtime_common::Token; -use subtensor_runtime_common::{AlphaBalance, NetUid}; +use subtensor_runtime_common::{AlphaBalance, NetUid, Token}; use crate::{ AlphaAssetsInterface, AlphaBurned, AlphaRecycled, PositiveAlphaImbalance, TotalAlphaIssuance, diff --git a/pallets/commitments/src/benchmarking.rs b/pallets/commitments/src/benchmarking.rs index 9d14953f7e..8550d82735 100644 --- a/pallets/commitments/src/benchmarking.rs +++ b/pallets/commitments/src/benchmarking.rs @@ -1,4 +1,4 @@ -//! Benchmarking setup +//! Runtime benchmarks for commitment extrinsics and the timelock reveal hook. #![cfg(feature = "runtime-benchmarks")] #![allow(clippy::arithmetic_side_effects, clippy::expect_used)] use super::*; @@ -16,6 +16,7 @@ use tle::{ibe::fullident::Identity as TleIdentity, tlock::tle}; use sp_runtime::traits::Bounded; +/// Asserts the most recent system event equals `generic_event`. fn assert_last_event( generic_event: ::RuntimeEvent, ) { @@ -36,9 +37,8 @@ const DRAND_QUICKNET_SIGNATURE_BYTES: [u8; 48] = [ 126, 75, 107, 99, 237, 94, 57, ]; -// This creates an `IdentityInfo` object with `num_fields` extra fields. -// All data is pre-populated with some arbitrary bytes. -fn create_identity_info(_num_fields: u32) -> CommitmentInfo { +/// Builds an empty [`CommitmentInfo`] for the `set_commitment` extrinsic benchmark. +fn create_commitment_info(_num_fields: u32) -> CommitmentInfo { let _data = Data::Raw( vec![0; 32] .try_into() @@ -50,6 +50,7 @@ fn create_identity_info(_num_fields: u32) -> CommitmentInfo(round: u64) { let randomness: BoundedVec> = vec![0_u8; 32] .try_into() @@ -102,6 +104,7 @@ fn insert_benchmark_pulse(round: u64) { ); } +/// Commitment whose single field is a real TLE ciphertext for [`BENCHMARK_REVEAL_ROUND`]. fn timelocked_commitment_info() -> CommitmentInfo { let raw = Data::Raw( b"timelock benchmark" @@ -139,7 +142,7 @@ mod benchmarks { _( RawOrigin::Signed(caller.clone()), netuid, - Box::new(create_identity_info::(0)), + Box::new(create_commitment_info::(0)), ); assert_last_event::( diff --git a/pallets/commitments/src/lib.rs b/pallets/commitments/src/lib.rs index 5ed05744ed..798d26e649 100644 --- a/pallets/commitments/src/lib.rs +++ b/pallets/commitments/src/lib.rs @@ -1,3 +1,12 @@ +//! # Commitments pallet +//! +//! Stores per-(`netuid`, account) metadata commitments, optionally timelock-encrypted via +//! drand (TLE). Plain and hash fields are written by [`Call::set_commitment`]; +//! [`Pallet::reveal_timelocked_commitments`] (from `on_initialize`) decrypts matured +//! `Data::TimelockEncrypted` fields into [`RevealedCommitments`]. +//! +//! Rate limiting uses a per-epoch byte budget ([`UsedSpaceOf`] / [`MaxSpace`]) keyed by +//! subnet tempo via [`GetTempoInterface`]. #![cfg_attr(not(feature = "std"), no_std)] mod benchmarking; @@ -48,90 +57,93 @@ pub mod pallet { #[pallet::without_storage_info] pub struct Pallet(_); - // Configure the pallet by specifying the parameters and types on which it depends. + /// Runtime configuration for commitment deposits, rate limits, and cross-pallet hooks. #[pallet::config] pub trait Config: frame_system::Config + pallet_drand::Config { - ///Currency type that will be used to reserve deposits for commitments + /// Currency used to reserve/unreserve commitment deposits. type Currency: ReservableCurrency + Send + Sync; /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; - /// Interface to access-limit metadata commitments + /// Who may call [`Call::set_commitment`] on a given netuid. type CanCommit: CanCommit; - /// Interface to trigger other pallets when metadata is committed + /// Notified when a commitment includes [`Data::ResetBondsFlag`]. type OnMetadataCommitment: OnMetadataCommitment; - /// The maximum number of additional fields that can be added to a commitment + /// Max number of [`Data`] fields allowed in one [`CommitmentInfo`]. #[pallet::constant] type MaxFields: Get + TypeInfo + 'static; - /// The amount held on deposit for a registered identity + /// Base deposit reserved for any non-empty commitment registration. #[pallet::constant] type InitialDeposit: Get>; - /// The amount held on deposit per additional field for a registered identity. + /// Extra deposit reserved per additional field beyond the base. #[pallet::constant] type FieldDeposit: Get>; - /// Used to retrieve the given subnet's tempo - type TempoInterface: GetTempoInterface; + /// Supplies subnet epoch indices for the [`UsedSpaceOf`] rate-limit window. + type SubtensorTempoBridge: GetTempoInterface; } - /// Used to retrieve the given subnet's tempo + /// Resolves a subnet's current epoch index for commitment rate-limit windows. pub trait GetTempoInterface { - /// Used to retreive the epoch index for the given subnet. + /// Returns the epoch index for `netuid` at `cur_block` (used to reset [`UsedSpaceOf`]). fn get_epoch_index(netuid: NetUid, cur_block: u64) -> u64; } #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { - /// A commitment was set + /// A non-timelocked commitment was written via [`Call::set_commitment`]. Commitment { - /// The netuid of the commitment + /// Subnet the commitment belongs to. netuid: NetUid, - /// The account + /// Account that set the commitment. who: T::AccountId, }, - /// A timelock-encrypted commitment was set + /// A commitment containing at least one [`Data::TimelockEncrypted`] field was set. TimelockCommitment { - /// The netuid of the commitment + /// Subnet the commitment belongs to. netuid: NetUid, - /// The account + /// Account that set the commitment. who: T::AccountId, - /// The drand round to reveal + /// Drand round at/after which auto-reveal may decrypt the ciphertext. reveal_round: u64, }, - /// A timelock-encrypted commitment was auto-revealed + /// A timelock-encrypted field was decrypted and appended to [`RevealedCommitments`]. CommitmentRevealed { - /// The netuid of the commitment + /// Subnet of the revealed commitment. netuid: NetUid, - /// The account + /// Account whose ciphertext was revealed. who: T::AccountId, }, } #[pallet::error] pub enum Error { - /// Account passed too many additional fields to their commitment + /// `info.fields` length exceeds [`Config::MaxFields`]. TooManyFieldsInCommitmentInfo, - /// Account is not allowed to make commitments to the chain + /// [`CanCommit::can_commit`] rejected this account for the target netuid. AccountNotAllowedCommit, - /// Space Limit Exceeded for the current interval + /// Epoch byte budget ([`UsedSpaceOf`] vs [`MaxSpace`]) would be exceeded. SpaceLimitExceeded, - /// Indicates that unreserve returned a leftover, which is unexpected. + /// Currency unreserve returned a leftover balance; deposit accounting is inconsistent. UnexpectedUnreserveLeftover, } - /// Tracks all CommitmentOf that have at least one timelocked field. + /// Index of `(netuid, who)` pairs whose [`CommitmentOf`] still has a timelocked field. + /// + /// Scanned each block by [`Pallet::reveal_timelocked_commitments`]; entries are removed when + /// no `TimelockEncrypted` fields remain (or the commitment is gone). #[pallet::storage] #[pallet::getter(fn timelocked_index)] pub type TimelockedIndex = StorageValue<_, BTreeSet<(NetUid, T::AccountId)>, ValueQuery>; - /// Identity data by account + /// Current commitment registration for `(netuid, who)`, including reserved deposit. #[pallet::storage] #[pallet::getter(fn commitment_of)] pub(super) type CommitmentOf = StorageDoubleMap< @@ -144,6 +156,7 @@ pub mod pallet { OptionQuery, >; + /// Block number of the most recent successful [`Call::set_commitment`] for `(netuid, who)`. #[pallet::storage] #[pallet::getter(fn last_commitment)] pub(super) type LastCommitment = StorageDoubleMap< @@ -156,6 +169,7 @@ pub mod pallet { OptionQuery, >; + /// Block when a `ResetBondsFlag` field last triggered [`OnMetadataCommitment`]. #[pallet::storage] #[pallet::getter(fn last_bonds_reset)] pub(super) type LastBondsReset = StorageDoubleMap< @@ -168,6 +182,10 @@ pub mod pallet { OptionQuery, >; + /// Decrypted timelock payloads for `(netuid, who)` as `(plaintext_bytes, reveal_block)`. + /// + /// Capped at the 10 most recent reveals (oldest dropped). Populated by the reveal hook, not + /// by extrinsics. #[pallet::storage] #[pallet::getter(fn revealed_commitments)] pub(super) type RevealedCommitments = StorageDoubleMap< @@ -180,8 +198,9 @@ pub mod pallet { OptionQuery, >; - /// Maps (netuid, who) -> usage (how many “bytes” they've committed) - /// in the RateLimit window + /// Per-(netuid, who) rate-limit usage for the current tempo epoch ([`UsageTracker`]). + /// + /// Resets when [`GetTempoInterface::get_epoch_index`] advances; compared against [`MaxSpace`]. #[pallet::storage] #[pallet::getter(fn used_space_of)] pub type UsedSpaceOf = StorageDoubleMap< @@ -195,11 +214,12 @@ pub mod pallet { >; #[pallet::type_value] - /// The default Maximum Space + /// Default [`MaxSpace`] (bytes per user per tempo epoch) when unset. pub fn DefaultMaxSpace() -> u32 { 3100 } + /// Maximum rate-limit “space” (bytes) a user may consume per netuid per tempo epoch. #[pallet::storage] #[pallet::getter(fn max_space_per_user_per_rate_limit)] pub type MaxSpace = StorageValue<_, u32, ValueQuery, DefaultMaxSpace>; @@ -208,7 +228,12 @@ pub mod pallet { impl Pallet { #![deny(clippy::expect_used)] - /// Set the commitment for a given netuid + /// Replace the caller's commitment on `netuid`, reserving deposit and updating rate-limit usage. + /// + /// Emits [`Event::TimelockCommitment`] if any field is timelock-encrypted (and indexes the + /// account in [`TimelockedIndex`]); otherwise emits [`Event::Commitment`]. A + /// [`Data::ResetBondsFlag`] field records [`LastBondsReset`] and invokes + /// [`OnMetadataCommitment`]. Empty commitments still count at least 100 rate-limit bytes. #[pallet::call_index(0)] #[pallet::weight(( ::WeightInfo::set_commitment(), @@ -244,7 +269,7 @@ pub mod pallet { let mut usage = UsedSpaceOf::::get(netuid, &who).unwrap_or_default(); let cur_block_u64 = cur_block.saturated_into::(); - let current_epoch = T::TempoInterface::get_epoch_index(netuid, cur_block_u64); + let current_epoch = T::SubtensorTempoBridge::get_epoch_index(netuid, cur_block_u64); if usage.last_epoch != current_epoch { usage.last_epoch = current_epoch; @@ -329,7 +354,7 @@ pub mod pallet { Ok(()) } - /// Sudo-set MaxSpace + /// Root-only update of the per-user per-epoch commitment space budget ([`MaxSpace`]). #[pallet::call_index(2)] #[pallet::weight(::WeightInfo::set_max_space())] pub fn set_max_space(origin: OriginFor, new_limit: u32) -> DispatchResult { @@ -354,8 +379,11 @@ pub mod pallet { } } -// Interfaces to interact with other pallets +/// Gate for whether `who` may call [`Call::set_commitment`] on `netuid`. +/// +/// Runtime typically wires this to subnet registration / validator checks; `()` denies all. pub trait CanCommit { + /// Returns true if `who` is allowed to write a commitment on `netuid`. fn can_commit(netuid: NetUid, who: &AccountId) -> bool; } @@ -365,7 +393,9 @@ impl CanCommit for () { } } +/// Hook invoked when a commitment includes [`Data::ResetBondsFlag`] (bonds-reset signal). pub trait OnMetadataCommitment { + /// Called once per `set_commitment` that contains a bonds-reset flag for `(netuid, account)`. fn on_metadata_commitment(netuid: NetUid, account: &AccountId); } @@ -373,12 +403,12 @@ impl OnMetadataCommitment for () { fn on_metadata_commitment(_: NetUid, _: &A) {} } -/************************************************************ - CallType definition -************************************************************/ +/// Transaction-extension / fee path classification for commitment extrinsics. #[derive(Debug, PartialEq, Default)] pub enum CallType { + /// [`Call::set_commitment`] was dispatched. SetCommitment, + /// Any other call type. #[default] Other, } @@ -386,6 +416,11 @@ pub enum CallType { use frame_support::{dispatch::DispatchResult, pallet_prelude::TypeInfo}; impl Pallet { + /// Decrypt matured [`Data::TimelockEncrypted`] fields using drand pulses; append plaintexts to + /// [`RevealedCommitments`] and prune exhausted commitments from [`TimelockedIndex`]. + /// + /// Skips rewrite of [`CommitmentOf`] when no pulse is available yet for the reveal round. + /// Returns accumulated DB weight for the scan (does not abort the block on decrypt failures). pub fn reveal_timelocked_commitments() -> Result { let mut total_weight = Weight::from_parts(0, 0); @@ -571,6 +606,8 @@ impl Pallet { Ok(total_weight) } + + /// SCALE-encodes every [`CommitmentOf`] entry on `netuid` as `(account, registration_bytes)`. pub fn get_commitments(netuid: NetUid) -> Vec<(T::AccountId, Vec)> { let commitments: Vec<(T::AccountId, Vec)> = as IterableStorageDoubleMap< @@ -586,6 +623,9 @@ impl Pallet { commitments } + /// Clears all per-netuid commitment maps for `netuid` and drops matching [`TimelockedIndex`] rows. + /// + /// Returns `false` if the weight meter cannot finish (maps may be partially cleared). pub fn purge_netuid(netuid: NetUid, weight_meter: &mut WeightMeter) -> bool { let write_weight = T::DbWeight::get().writes(1); @@ -617,7 +657,9 @@ impl Pallet { } } +/// Runtime API-facing adapter for listing SCALE-encoded commitments on a netuid. pub trait GetCommitments { + /// See [`Pallet::get_commitments`]. fn get_commitments(netuid: NetUid) -> Vec<(AccountId, Vec)>; } diff --git a/pallets/commitments/src/mock.rs b/pallets/commitments/src/mock.rs index 58ed8cd863..827ada5c2d 100644 --- a/pallets/commitments/src/mock.rs +++ b/pallets/commitments/src/mock.rs @@ -1,3 +1,4 @@ +//! Test runtime wiring for the commitments pallet (System, Balances, Drand, Commitments). #![allow(clippy::expect_used)] use crate as pallet_commitments; use frame_support::{ @@ -12,7 +13,7 @@ use sp_runtime::{ testing::Header, traits::{BlakeTwo256, ConstU16, IdentityLookup}, }; -use subtensor_runtime_common::{ConstTao, TaoBalance}; +use subtensor_runtime_common::{ConstTao, NetUid, TaoBalance}; pub type Block = sp_runtime::generic::Block; pub type UncheckedExtrinsic = @@ -73,6 +74,7 @@ impl pallet_balances::Config for Test { } #[derive(Clone, Copy, Debug, Eq, PartialEq)] +/// Test [`crate::Config::MaxFields`] — allows up to 16 commitment fields. pub struct TestMaxFields; impl Get for TestMaxFields { fn get() -> u32 { @@ -88,6 +90,7 @@ impl TypeInfo for TestMaxFields { } } +/// Test [`crate::CanCommit`] that permits every account on every netuid. pub struct TestCanCommit; impl pallet_commitments::CanCommit for TestCanCommit { fn can_commit(_netuid: NetUid, _who: &u64) -> bool { @@ -95,6 +98,7 @@ impl pallet_commitments::CanCommit for TestCanCommit { } } +/// Zero-weight stub implementing [`crate::WeightInfo`] for unit tests. pub struct TestWeightInfo; impl pallet_commitments::WeightInfo for TestWeightInfo { fn set_commitment() -> Weight { @@ -117,14 +121,16 @@ impl pallet_commitments::Config for Test { type CanCommit = TestCanCommit; type FieldDeposit = ConstTao<0>; type InitialDeposit = ConstTao<0>; - type TempoInterface = MockTempoInterface; + type SubtensorTempoBridge = MockTempoInterface; type OnMetadataCommitment = (); } +/// Fixed-tempo (360) epoch indexer mirroring subtensor's netuid-offset epoch formula. pub struct MockTempoInterface; impl pallet_commitments::GetTempoInterface for MockTempoInterface { fn get_epoch_index(netuid: NetUid, cur_block: u64) -> u64 { - let tempo = 360; // TODO: configure SubtensorModule in this mock + // Deliberately does not pull SubtensorModule: unit tests only need a stable epoch clock. + let tempo = 360; let tempo_plus_one: u64 = tempo.saturating_add(1); let netuid_plus_one: u64 = (u16::from(netuid) as u64).saturating_add(1); let block_with_offset: u64 = cur_block.saturating_add(netuid_plus_one); @@ -193,6 +199,7 @@ where } } +/// Fresh test externalities with genesis defaults and block number set to 1. pub fn new_test_ext() -> sp_io::TestExternalities { let t = frame_system::GenesisConfig::::default() .build_storage() diff --git a/pallets/commitments/src/tests.rs b/pallets/commitments/src/tests.rs deleted file mode 100644 index fb001f99ae..0000000000 --- a/pallets/commitments/src/tests.rs +++ /dev/null @@ -1,2375 +0,0 @@ -#![allow(clippy::expect_used, clippy::indexing_slicing)] - -use codec::Encode; -use sp_std::prelude::*; -use subtensor_runtime_common::{NetUid, TaoBalance}; - -#[cfg(test)] -use crate::{ - BalanceOf, CommitmentInfo, CommitmentOf, Config, Data, Error, Event, LastBondsReset, - LastCommitment, MaxSpace, Pallet, Registration, RevealedCommitments, TimelockedIndex, - UsageTracker, UsedSpaceOf, WeightInfo, - mock::{ - Balances, DRAND_QUICKNET_SIG_2000_HEX, DRAND_QUICKNET_SIG_HEX, RuntimeEvent, RuntimeOrigin, - Test, TestMaxFields, insert_drand_pulse, new_test_ext, produce_ciphertext, - }, -}; -use frame_support::pallet_prelude::Hooks; -use frame_support::{ - BoundedVec, assert_noop, assert_ok, - traits::{Currency, Get, ReservableCurrency}, - weights::{Weight, constants::RocksDbWeight}, -}; -use frame_system::{Pallet as System, RawOrigin}; - -fn purge_netuid_with_meter(netuid: NetUid, limit: Weight) -> bool { - let mut weight_meter = frame_support::weights::WeightMeter::with_limit(limit); - Pallet::::purge_netuid(netuid, &mut weight_meter) -} - -#[test] -fn manual_data_type_info() { - let mut registry = scale_info::Registry::new(); - let type_id = registry.register_type(&scale_info::meta_type::()); - let registry: scale_info::PortableRegistry = registry.into(); - let type_info = registry.resolve(type_id.id).expect("Expected not to panic"); - - let check_type_info = |data: &Data| { - let variant_name = match data { - Data::None => "None".to_string(), - Data::BlakeTwo256(_) => "BlakeTwo256".to_string(), - Data::Sha256(_) => "Sha256".to_string(), - Data::Keccak256(_) => "Keccak256".to_string(), - Data::ShaThree256(_) => "ShaThree256".to_string(), - Data::Raw(bytes) => format!("Raw{}", bytes.len()), - Data::TimelockEncrypted { .. } => "TimelockEncrypted".to_string(), - Data::ResetBondsFlag => "ResetBondsFlag".to_string(), - Data::BigRaw(_) => "BigRaw".to_string(), - }; - if let scale_info::TypeDef::Variant(variant) = &type_info.type_def { - let variant = variant - .variants - .iter() - .find(|v| v.name == variant_name) - .unwrap_or_else(|| panic!("Expected to find variant {variant_name}")); - - let encoded = data.encode(); - assert_eq!(encoded[0], variant.index); - - // For variants with fields, check the encoded length matches expected field lengths - if !variant.fields.is_empty() { - let expected_len = match data { - Data::None => 0, - Data::Raw(bytes) => bytes.len() as u32, - Data::BigRaw(bytes) => bytes.len() as u32, - Data::BlakeTwo256(_) - | Data::Sha256(_) - | Data::Keccak256(_) - | Data::ShaThree256(_) => 32, - Data::TimelockEncrypted { - encrypted, - reveal_round, - } => { - // Calculate length: encrypted (length prefixed) + reveal_round (u64) - let encrypted_len = encrypted.encode().len() as u32; // Includes length prefix - let reveal_round_len = reveal_round.encode().len() as u32; // Typically 8 bytes - encrypted_len + reveal_round_len - } - Data::ResetBondsFlag => 0, - }; - assert_eq!( - encoded.len() as u32 - 1, // Subtract variant byte - expected_len, - "Encoded length mismatch for variant {variant_name}" - ); - } else { - assert_eq!( - encoded.len() as u32 - 1, - 0, - "Expected no fields for {variant_name}" - ); - } - } else { - panic!("Should be a variant type"); - } - }; - - let mut data = vec![ - Data::None, - Data::BlakeTwo256(Default::default()), - Data::Sha256(Default::default()), - Data::Keccak256(Default::default()), - Data::ShaThree256(Default::default()), - Data::ResetBondsFlag, - ]; - - // Add Raw instances for all possible sizes - for n in 0..128 { - data.push(Data::Raw( - vec![0u8; n as usize] - .try_into() - .expect("Expected not to panic"), - )); - } - - // Add a TimelockEncrypted instance - data.push(Data::TimelockEncrypted { - encrypted: vec![0u8; 64].try_into().expect("Expected not to panic"), - reveal_round: 12345, - }); - - for d in data.iter() { - check_type_info(d); - } -} - -#[test] -fn set_commitment_works() { - new_test_ext().execute_with(|| { - System::::set_block_number(1); - let info = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![]).expect("Expected not to panic"), - }); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(1), - 1.into(), - info.clone() - )); - - let commitment = - Pallet::::commitment_of(NetUid::from(1), 1).expect("Expected not to panic"); - let initial_deposit = ::InitialDeposit::get(); - assert_eq!(commitment.deposit, initial_deposit); - assert_eq!(commitment.block, 1); - assert_eq!(Pallet::::last_commitment(NetUid::from(1), 1), Some(1)); - }); -} - -#[test] -#[should_panic(expected = "BoundedVec::try_from failed")] -fn set_commitment_too_many_fields_panics() { - new_test_ext().execute_with(|| { - let max_fields: u32 = ::MaxFields::get(); - let fields = vec![Data::None; (max_fields + 1) as usize]; - - // This line will panic when 'BoundedVec::try_from(...)' sees too many items. - let info = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(fields).expect("BoundedVec::try_from failed"), - }); - - // We never get here, because the constructor panics above. - let _ = Pallet::::set_commitment( - frame_system::RawOrigin::Signed(1).into(), - 1.into(), - info, - ); - }); -} - -#[test] -fn set_commitment_updates_deposit() { - new_test_ext().execute_with(|| { - System::::set_block_number(1); - let info1 = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![Default::default(); 2]) - .expect("Expected not to panic"), - }); - let info2 = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![Default::default(); 3]) - .expect("Expected not to panic"), - }); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(1), - 1.into(), - info1 - )); - let initial_deposit = ::InitialDeposit::get(); - let field_deposit = ::FieldDeposit::get(); - let expected_deposit1 = initial_deposit + field_deposit * 2.into(); - assert_eq!( - Pallet::::commitment_of(NetUid::from(1), 1) - .expect("Expected not to panic") - .deposit, - expected_deposit1 - ); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(1), - 1.into(), - info2 - )); - let expected_deposit2 = initial_deposit + field_deposit * 3.into(); - assert_eq!( - Pallet::::commitment_of(NetUid::from(1), 1) - .expect("Expected not to panic") - .deposit, - expected_deposit2 - ); - }); -} - -#[test] -fn event_emission_works() { - new_test_ext().execute_with(|| { - System::::set_block_number(1); - let info = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![]).expect("Expected not to panic"), - }); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(1), - 1.into(), - info - )); - - let events = System::::events(); - let expected_event = RuntimeEvent::Commitments(Event::Commitment { - netuid: 1.into(), - who: 1, - }); - assert!(events.iter().any(|e| e.event == expected_event)); - }); -} - -#[allow(clippy::indexing_slicing)] -#[test] -fn happy_path_timelock_commitments() { - new_test_ext().execute_with(|| { - let message_text = b"Hello timelock only!"; - let data_raw = Data::Raw( - message_text - .to_vec() - .try_into() - .expect("<= 128 bytes for Raw variant"), - ); - let fields_vec = vec![data_raw]; - let fields_bounded: BoundedVec::MaxFields> = - BoundedVec::try_from(fields_vec).expect("Too many fields"); - - let inner_info: CommitmentInfo<::MaxFields> = CommitmentInfo { - fields: fields_bounded, - }; - - let plaintext = inner_info.encode(); - - let reveal_round = 1000; - let encrypted = produce_ciphertext(&plaintext, reveal_round); - - let data = Data::TimelockEncrypted { - encrypted: encrypted.clone(), - reveal_round, - }; - - let fields_outer: BoundedVec::MaxFields> = - BoundedVec::try_from(vec![data]).expect("Too many fields"); - let info_outer = CommitmentInfo { - fields: fields_outer, - }; - - let who = 123; - let netuid = NetUid::from(42); - System::::set_block_number(1); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid, - Box::new(info_outer) - )); - - let drand_signature_bytes = - hex::decode(DRAND_QUICKNET_SIG_HEX).expect("Expected not to panic"); - insert_drand_pulse(reveal_round, &drand_signature_bytes); - - System::::set_block_number(9999); - assert_ok!(Pallet::::reveal_timelocked_commitments()); - - let revealed = - RevealedCommitments::::get(netuid, who).expect("Should have revealed data"); - - let (revealed_bytes, _reveal_block) = revealed[0].clone(); - - let revealed_str = sp_std::str::from_utf8(&revealed_bytes) - .expect("Expected valid UTF-8 in the revealed bytes for this test"); - - let original_str = - sp_std::str::from_utf8(message_text).expect("`message_text` is valid UTF-8"); - assert!( - revealed_str.contains(original_str), - "Revealed data must contain the original message text." - ); - }); -} - -#[test] -fn reveal_timelocked_commitment_missing_round_does_nothing() { - new_test_ext().execute_with(|| { - let who = 1; - let netuid = NetUid::from(2); - System::::set_block_number(5); - let ciphertext = produce_ciphertext(b"My plaintext", 1000); - let data = Data::TimelockEncrypted { - encrypted: ciphertext, - reveal_round: 1000, - }; - let fields: BoundedVec<_, ::MaxFields> = - BoundedVec::try_from(vec![data]).expect("Expected not to panic"); - let info = CommitmentInfo { fields }; - let origin = RuntimeOrigin::signed(who); - assert_ok!(Pallet::::set_commitment( - origin, - netuid, - Box::new(info) - )); - System::::set_block_number(100_000); - assert_ok!(Pallet::::reveal_timelocked_commitments()); - assert!(RevealedCommitments::::get(netuid, who).is_none()); - }); -} - -#[allow(clippy::indexing_slicing)] -#[test] -fn reveal_timelocked_commitment_cant_deserialize_ciphertext() { - new_test_ext().execute_with(|| { - let who = 42; - let netuid = NetUid::from(9); - System::::set_block_number(10); - let good_ct = produce_ciphertext(b"Some data", 1000); - let mut corrupted = good_ct.into_inner(); - if !corrupted.is_empty() { - corrupted[0] = 0xFF; - } - let corrupted_ct = BoundedVec::try_from(corrupted).expect("Expected not to panic"); - let data = Data::TimelockEncrypted { - encrypted: corrupted_ct, - reveal_round: 1000, - }; - let fields = BoundedVec::try_from(vec![data]).expect("Expected not to panic"); - let info = CommitmentInfo { fields }; - let origin = RuntimeOrigin::signed(who); - assert_ok!(Pallet::::set_commitment( - origin, - netuid, - Box::new(info) - )); - let sig_bytes = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("Expected not to panic"); - insert_drand_pulse(1000, &sig_bytes); - System::::set_block_number(99999); - assert_ok!(Pallet::::reveal_timelocked_commitments()); - assert!(RevealedCommitments::::get(netuid, who).is_none()); - }); -} - -#[test] -fn reveal_timelocked_commitment_bad_signature_skips_decryption() { - new_test_ext().execute_with(|| { - let who = 10; - let netuid = NetUid::from(11); - System::::set_block_number(15); - let real_ct = produce_ciphertext(b"A valid plaintext", 1000); - let data = Data::TimelockEncrypted { - encrypted: real_ct, - reveal_round: 1000, - }; - let fields: BoundedVec<_, ::MaxFields> = - BoundedVec::try_from(vec![data]).expect("Expected not to panic"); - let info = CommitmentInfo { fields }; - let origin = RuntimeOrigin::signed(who); - assert_ok!(Pallet::::set_commitment( - origin, - netuid, - Box::new(info) - )); - let bad_signature = [0x33u8; 10]; - insert_drand_pulse(1000, &bad_signature); - System::::set_block_number(10_000); - assert_ok!(Pallet::::reveal_timelocked_commitments()); - assert!(RevealedCommitments::::get(netuid, who).is_none()); - }); -} - -#[test] -fn reveal_timelocked_commitment_empty_decrypted_data_is_skipped() { - new_test_ext().execute_with(|| { - let who = 2; - let netuid = NetUid::from(3); - let commit_block = 100u64; - System::::set_block_number(commit_block); - let reveal_round = 1000; - let empty_ct = produce_ciphertext(&[], reveal_round); - let data = Data::TimelockEncrypted { - encrypted: empty_ct, - reveal_round, - }; - let fields = BoundedVec::try_from(vec![data]).expect("Expected not to panic"); - let info = CommitmentInfo { fields }; - let origin = RuntimeOrigin::signed(who); - assert_ok!(Pallet::::set_commitment( - origin, - netuid, - Box::new(info) - )); - let sig_bytes = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("Expected not to panic"); - insert_drand_pulse(reveal_round, &sig_bytes); - System::::set_block_number(10_000); - assert_ok!(Pallet::::reveal_timelocked_commitments()); - assert!(RevealedCommitments::::get(netuid, who).is_none()); - }); -} - -#[allow(clippy::indexing_slicing)] -#[test] -fn reveal_timelocked_commitment_single_field_entry_is_removed_after_reveal() { - new_test_ext().execute_with(|| { - let message_text = b"Single field timelock test!"; - let data_raw = Data::Raw( - message_text - .to_vec() - .try_into() - .expect("Message must be <=128 bytes for Raw variant"), - ); - - let fields_bounded: BoundedVec::MaxFields> = - BoundedVec::try_from(vec![data_raw]).expect("BoundedVec creation must not fail"); - - let inner_info: CommitmentInfo<::MaxFields> = CommitmentInfo { - fields: fields_bounded, - }; - - let plaintext = inner_info.encode(); - let reveal_round = 1000; - let encrypted = produce_ciphertext(&plaintext, reveal_round); - - let timelock_data = Data::TimelockEncrypted { - encrypted, - reveal_round, - }; - let fields_outer: BoundedVec::MaxFields> = - BoundedVec::try_from(vec![timelock_data]).expect("Too many fields"); - let info_outer: CommitmentInfo<::MaxFields> = CommitmentInfo { - fields: fields_outer, - }; - - let who = 555; - let netuid = NetUid::from(777); - System::::set_block_number(1); - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid, - Box::new(info_outer) - )); - - let drand_signature_bytes = hex::decode(DRAND_QUICKNET_SIG_HEX) - .expect("Must decode DRAND_QUICKNET_SIG_HEX successfully"); - insert_drand_pulse(reveal_round, &drand_signature_bytes); - - System::::set_block_number(9999); - assert_ok!(Pallet::::reveal_timelocked_commitments()); - - let revealed = - RevealedCommitments::::get(netuid, who).expect("Expected to find revealed data"); - let (revealed_bytes, _reveal_block) = revealed[0].clone(); - - // The decrypted bytes have some extra SCALE metadata in front: - // we slice off the first two bytes before checking the string. - let offset = 2; - let truncated = &revealed_bytes[offset..]; - let revealed_str = sp_std::str::from_utf8(truncated) - .expect("Truncated bytes should be valid UTF-8 in this test"); - - let original_str = - sp_std::str::from_utf8(message_text).expect("`message_text` should be valid UTF-8"); - assert_eq!( - revealed_str, original_str, - "Expected the revealed data (minus prefix) to match the original message" - ); - assert!( - crate::CommitmentOf::::get(netuid, who).is_none(), - "Expected CommitmentOf entry to be removed after reveal" - ); - }); -} - -#[allow(clippy::indexing_slicing)] -#[test] -fn reveal_timelocked_multiple_fields_only_correct_ones_removed() { - new_test_ext().execute_with(|| { - let round_1000 = 1000; - - // 2) Build two CommitmentInfos, one for each timelock - let msg_1 = b"Hello from TLE #1"; - let inner_1_fields: BoundedVec::MaxFields> = - BoundedVec::try_from(vec![Data::Raw( - msg_1.to_vec().try_into().expect("expected not to panic"), - )]) - .expect("BoundedVec of size 1"); - let inner_info_1 = CommitmentInfo { - fields: inner_1_fields, - }; - let encoded_1 = inner_info_1.encode(); - let ciphertext_1 = produce_ciphertext(&encoded_1, round_1000); - let timelock_1 = Data::TimelockEncrypted { - encrypted: ciphertext_1, - reveal_round: round_1000, - }; - - let msg_2 = b"Hello from TLE #2"; - let inner_2_fields: BoundedVec::MaxFields> = - BoundedVec::try_from(vec![Data::Raw( - msg_2.to_vec().try_into().expect("expected not to panic"), - )]) - .expect("BoundedVec of size 1"); - let inner_info_2 = CommitmentInfo { - fields: inner_2_fields, - }; - let encoded_2 = inner_info_2.encode(); - let ciphertext_2 = produce_ciphertext(&encoded_2, round_1000); - let timelock_2 = Data::TimelockEncrypted { - encrypted: ciphertext_2, - reveal_round: round_1000, - }; - - // 3) One plain Data::Raw field (non-timelocked) - let raw_bytes = b"Plain non-timelocked data"; - let data_raw = Data::Raw( - raw_bytes - .to_vec() - .try_into() - .expect("expected not to panic"), - ); - - // 4) Outer commitment: 3 fields total => [Raw, TLE #1, TLE #2] - let outer_fields = BoundedVec::try_from(vec![ - data_raw.clone(), - timelock_1.clone(), - timelock_2.clone(), - ]) - .expect("T::MaxFields >= 3 in the test config, or at least 3 here"); - let outer_info = CommitmentInfo { - fields: outer_fields, - }; - - // 5) Insert the commitment - let who = 123; - let netuid = NetUid::from(999); - System::::set_block_number(1); - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid, - Box::new(outer_info) - )); - let initial = Pallet::::commitment_of(netuid, who).expect("Must exist"); - assert_eq!(initial.info.fields.len(), 3, "3 fields inserted"); - - // 6) Insert Drand signature for round=1000 - let drand_sig_1000 = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("decode DRAND sig"); - insert_drand_pulse(round_1000, &drand_sig_1000); - - // 7) Reveal once - System::::set_block_number(50); - assert_ok!(Pallet::::reveal_timelocked_commitments()); - - // => The pallet code has removed *both* TLE #1 and TLE #2 in this single call! - let after_reveal = Pallet::::commitment_of(netuid, who) - .expect("Should still exist with leftover fields"); - // Only the raw, non-timelocked field remains - assert_eq!( - after_reveal.info.fields.len(), - 1, - "Both timelocks referencing round=1000 got removed at once" - ); - assert_eq!( - after_reveal.info.fields[0], data_raw, - "Only the raw field is left" - ); - - // 8) Check revealed data - let revealed_data = RevealedCommitments::::get(netuid, who) - .expect("Expected revealed data for TLE #1 and #2"); - - let (revealed_bytes1, reveal_block1) = revealed_data[0].clone(); - let (revealed_bytes2, reveal_block2) = revealed_data[1].clone(); - - let truncated1 = &revealed_bytes1[2..]; - let truncated2 = &revealed_bytes2[2..]; - - assert_eq!(truncated1, msg_1); - assert_eq!(reveal_block1, 50); - assert_eq!(truncated2, msg_2); - assert_eq!(reveal_block2, 50); - - // 9) A second reveal call now does nothing, because no timelocks remain - System::::set_block_number(51); - assert_ok!(Pallet::::reveal_timelocked_commitments()); - - let after_second = Pallet::::commitment_of(netuid, who).expect("Still must exist"); - assert_eq!( - after_second.info.fields.len(), - 1, - "No new fields were removed, because no timelocks remain" - ); - }); -} - -#[test] -fn test_index_lifecycle_no_timelocks_updates_in_out() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(100); - let who = 999; - - // - // A) Create a commitment with **no** timelocks => shouldn't be in index - // - let no_tl_fields: BoundedVec::MaxFields> = - BoundedVec::try_from(vec![]).expect("Empty is ok"); - let info_no_tl = CommitmentInfo { - fields: no_tl_fields, - }; - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid, - Box::new(info_no_tl) - )); - assert!( - !TimelockedIndex::::get().contains(&(netuid, who)), - "User with no timelocks must not appear in index" - ); - - // - // B) Update the commitment to have a timelock => enters index - // - let tl_fields: BoundedVec<_, ::MaxFields> = - BoundedVec::try_from(vec![Data::TimelockEncrypted { - encrypted: Default::default(), - reveal_round: 1234, - }]) - .expect("Expected success"); - let info_with_tl = CommitmentInfo { fields: tl_fields }; - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid, - Box::new(info_with_tl) - )); - assert!( - TimelockedIndex::::get().contains(&(netuid, who)), - "User must appear in index after adding a timelock" - ); - - // - // C) Remove the timelock => leaves index - // - let back_to_no_tl: BoundedVec<_, ::MaxFields> = - BoundedVec::try_from(vec![]).expect("Expected success"); - let info_remove_tl = CommitmentInfo { - fields: back_to_no_tl, - }; - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid, - Box::new(info_remove_tl) - )); - - assert!( - !TimelockedIndex::::get().contains(&(netuid, who)), - "User must be removed from index after losing all timelocks" - ); - }); -} - -#[test] -fn two_timelocks_partial_then_full_reveal() { - new_test_ext().execute_with(|| { - let netuid_a = NetUid::from(1); - let who_a = 10; - let round_1000 = 1000; - let round_2000 = 2000; - - let drand_sig_1000 = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("Expected success"); - insert_drand_pulse(round_1000, &drand_sig_1000); - - let drand_sig_2000_hex = - "b6cb8f482a0b15d45936a4c4ea08e98a087e71787caee3f4d07a8a9843b1bc5423c6b3c22f446488b3137eaca799c77e"; - - // - // First Timelock => round=1000 - // - let msg_a1 = b"UserA timelock #1 (round=1000)"; - let inner_1_fields: BoundedVec::MaxFields> = BoundedVec::try_from( - vec![Data::Raw(msg_a1.to_vec().try_into().expect("Expected success"))], - ) - .expect("MaxFields >= 1"); - let inner_info_1: CommitmentInfo<::MaxFields> = CommitmentInfo { - fields: inner_1_fields, - }; - let encoded_1 = inner_info_1.encode(); - let ciphertext_1 = produce_ciphertext(&encoded_1, round_1000); - let tle_a1 = Data::TimelockEncrypted { - encrypted: ciphertext_1, - reveal_round: round_1000, - }; - - // - // Second Timelock => round=2000 - // - let msg_a2 = b"UserA timelock #2 (round=2000)"; - let inner_2_fields: BoundedVec::MaxFields> = BoundedVec::try_from( - vec![Data::Raw(msg_a2.to_vec().try_into().expect("Expected success"))], - ) - .expect("MaxFields >= 1"); - let inner_info_2: CommitmentInfo<::MaxFields> = CommitmentInfo { - fields: inner_2_fields, - }; - let encoded_2 = inner_info_2.encode(); - let ciphertext_2 = produce_ciphertext(&encoded_2, round_2000); - let tle_a2 = Data::TimelockEncrypted { - encrypted: ciphertext_2, - reveal_round: round_2000, - }; - - // - // Insert outer commitment with both timelocks - // - let fields_a: BoundedVec::MaxFields> = - BoundedVec::try_from(vec![tle_a1, tle_a2]).expect("2 fields, must be <= MaxFields"); - let info_a: CommitmentInfo<::MaxFields> = CommitmentInfo { fields: fields_a }; - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who_a), - netuid_a, - Box::new(info_a) - )); - assert!( - TimelockedIndex::::get().contains(&(netuid_a, who_a)), - "User A must be in index with 2 timelocks" - ); - - System::::set_block_number(10); - assert_ok!(Pallet::::reveal_timelocked_commitments()); - - let leftover_a1 = CommitmentOf::::get(netuid_a, who_a).expect("still there"); - assert_eq!( - leftover_a1.info.fields.len(), - 1, - "Only the round=1000 timelock removed; round=2000 remains" - ); - assert!( - TimelockedIndex::::get().contains(&(netuid_a, who_a)), - "Still in index with leftover timelock" - ); - - // - // Insert signature for round=2000 => final reveal => leftover=none => removed - // - let drand_sig_2000 = hex::decode(drand_sig_2000_hex).expect("Expected success"); - insert_drand_pulse(round_2000, &drand_sig_2000); - - System::::set_block_number(11); - assert_ok!(Pallet::::reveal_timelocked_commitments()); - - let leftover_a2 = CommitmentOf::::get(netuid_a, who_a); - assert!( - leftover_a2.is_none(), - "All timelocks removed => none leftover" - ); - assert!( - !TimelockedIndex::::get().contains(&(netuid_a, who_a)), - "User A removed from index after final reveal" - ); - }); -} - -#[test] -fn single_timelock_reveal_later_round() { - new_test_ext().execute_with(|| { - let netuid_b = NetUid::from(2); - let who_b = 20; - let round_2000 = 2000; - - let drand_sig_2000_hex = - "b6cb8f482a0b15d45936a4c4ea08e98a087e71787caee3f4d07a8a9843b1bc5423c6b3c22f446488b3137eaca799c77e"; - let drand_sig_2000 = hex::decode(drand_sig_2000_hex).expect("Expected success"); - insert_drand_pulse(round_2000, &drand_sig_2000); - - let msg_b = b"UserB single timelock (round=2000)"; - - let inner_b_fields: BoundedVec::MaxFields> = - BoundedVec::try_from(vec![Data::Raw(msg_b.to_vec().try_into().expect("Expected success"))]) - .expect("MaxFields >= 1"); - let inner_info_b: CommitmentInfo<::MaxFields> = CommitmentInfo { - fields: inner_b_fields, - }; - let encoded_b = inner_info_b.encode(); - let ciphertext_b = produce_ciphertext(&encoded_b, round_2000); - let tle_b = Data::TimelockEncrypted { - encrypted: ciphertext_b, - reveal_round: round_2000, - }; - - let fields_b: BoundedVec::MaxFields> = - BoundedVec::try_from(vec![tle_b]).expect("1 field"); - let info_b: CommitmentInfo<::MaxFields> = CommitmentInfo { fields: fields_b }; - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who_b), - netuid_b, - Box::new(info_b) - )); - assert!( - TimelockedIndex::::get().contains(&(netuid_b, who_b)), - "User B in index" - ); - - // Remove the round=2000 signature so first reveal does nothing - pallet_drand::Pulses::::remove(round_2000); - - System::::set_block_number(20); - assert_ok!(Pallet::::reveal_timelocked_commitments()); - - let leftover_b1 = CommitmentOf::::get(netuid_b, who_b).expect("still there"); - assert_eq!( - leftover_b1.info.fields.len(), - 1, - "No signature => timelock remains" - ); - assert!( - TimelockedIndex::::get().contains(&(netuid_b, who_b)), - "Still in index with leftover timelock" - ); - - insert_drand_pulse(round_2000, &drand_sig_2000); - - System::::set_block_number(21); - assert_ok!(Pallet::::reveal_timelocked_commitments()); - - let leftover_b2 = CommitmentOf::::get(netuid_b, who_b); - assert!(leftover_b2.is_none(), "Timelock removed => leftover=none"); - assert!( - !TimelockedIndex::::get().contains(&(netuid_b, who_b)), - "User B removed from index after final reveal" - ); - }); -} - -#[test] -fn tempo_based_space_limit_accumulates_in_same_window() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let who = 100; - let space_limit = 150; - MaxSpace::::set(space_limit); - System::::set_block_number(0); - - // A single commitment that uses some space, e.g. 30 bytes: - let data = vec![0u8; 30]; - let info = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![Data::Raw( - data.try_into().expect("Data up to 128 bytes OK"), - )]) - .expect("1 field is <= MaxFields"), - }); - - // 2) First call => usage=0 => usage=30 after. OK. - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid, - info.clone(), - )); - - // 3) Second call => tries another 30 bytes in the SAME block => total=60 => exceeds 50 => should fail. - assert_noop!( - Pallet::::set_commitment(RuntimeOrigin::signed(who), netuid, info.clone()), - Error::::SpaceLimitExceeded - ); - }); -} - -#[test] -fn tempo_based_space_limit_resets_after_tempo() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(2); - let who = 101; - - MaxSpace::::set(250); - System::::set_block_number(1); - - let commit_small = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![Data::Raw( - vec![0u8; 20].try_into().expect("expected ok"), - )]) - .expect("expected ok"), - }); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid, - commit_small.clone() - )); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid, - commit_small.clone() - )); - - assert_noop!( - Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid, - commit_small.clone() - ), - Error::::SpaceLimitExceeded - ); - - System::::set_block_number(200); - - assert_noop!( - Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid, - commit_small.clone() - ), - Error::::SpaceLimitExceeded - ); - - System::::set_block_number(360); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid, - commit_small - )); - }); -} - -#[test] -fn tempo_based_space_limit_does_not_affect_different_netuid() { - new_test_ext().execute_with(|| { - let netuid_a = NetUid::from(10); - let netuid_b = NetUid::from(20); - let who = 111; - let space_limit = 199; - MaxSpace::::set(space_limit); - - let commit_large = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![Data::Raw( - vec![0u8; 40].try_into().expect("expected ok"), - )]) - .expect("expected ok"), - }); - let commit_small = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![Data::Raw( - vec![0u8; 20].try_into().expect("expected ok"), - )]) - .expect("expected ok"), - }); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid_a, - commit_large.clone() - )); - - assert_noop!( - Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid_a, - commit_small.clone() - ), - Error::::SpaceLimitExceeded - ); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid_b, - commit_large - )); - - assert_noop!( - Pallet::::set_commitment(RuntimeOrigin::signed(who), netuid_b, commit_small), - Error::::SpaceLimitExceeded - ); - }); -} - -#[test] -fn tempo_based_space_limit_does_not_affect_different_user() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(10); - let user1 = 123; - let user2 = 456; - let space_limit = 199; - MaxSpace::::set(space_limit); - - let commit_large = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![Data::Raw( - vec![0u8; 40].try_into().expect("expected ok"), - )]) - .expect("expected ok"), - }); - let commit_small = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![Data::Raw( - vec![0u8; 20].try_into().expect("expected ok"), - )]) - .expect("expected ok"), - }); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(user1), - netuid, - commit_large.clone() - )); - - assert_noop!( - Pallet::::set_commitment( - RuntimeOrigin::signed(user1), - netuid, - commit_small.clone() - ), - Error::::SpaceLimitExceeded - ); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(user2), - netuid, - commit_large - )); - - assert_noop!( - Pallet::::set_commitment(RuntimeOrigin::signed(user2), netuid, commit_small), - Error::::SpaceLimitExceeded - ); - }); -} - -#[test] -fn tempo_based_space_limit_sudo_set_max_space() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(3); - let who = 15; - MaxSpace::::set(100); - - System::::set_block_number(1); - let commit_25 = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![Data::Raw( - vec![0u8; 25].try_into().expect("expected ok"), - )]) - .expect("expected ok"), - }); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid, - commit_25.clone() - )); - assert_noop!( - Pallet::::set_commitment(RuntimeOrigin::signed(who), netuid, commit_25.clone()), - Error::::SpaceLimitExceeded - ); - - assert_ok!(Pallet::::set_max_space(RuntimeOrigin::root(), 300)); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid, - commit_25 - )); - }); -} - -#[allow(clippy::indexing_slicing)] -#[test] -fn on_initialize_reveals_matured_timelocks() { - new_test_ext().execute_with(|| { - let who = 42; - let netuid = NetUid::from(7); - let reveal_round = 1000; - - let message_text = b"Timelock test via on_initialize"; - - let inner_fields: BoundedVec::MaxFields> = - BoundedVec::try_from(vec![Data::Raw( - message_text - .to_vec() - .try_into() - .expect("<= 128 bytes is OK for Data::Raw"), - )]) - .expect("Should not exceed MaxFields"); - - let inner_info: CommitmentInfo<::MaxFields> = CommitmentInfo { - fields: inner_fields, - }; - - let plaintext = inner_info.encode(); - let encrypted = produce_ciphertext(&plaintext, reveal_round); - - let outer_fields = BoundedVec::try_from(vec![Data::TimelockEncrypted { - encrypted, - reveal_round, - }]) - .expect("One field is well under MaxFields"); - let info_outer = CommitmentInfo { - fields: outer_fields, - }; - - System::::set_block_number(1); - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid, - Box::new(info_outer) - )); - - assert!(CommitmentOf::::get(netuid, who).is_some()); - assert!( - TimelockedIndex::::get().contains(&(netuid, who)), - "Should appear in TimelockedIndex since it contains a timelock" - ); - - let drand_sig_hex = hex::decode(DRAND_QUICKNET_SIG_HEX) - .expect("Decoding DRAND_QUICKNET_SIG_HEX must not fail"); - insert_drand_pulse(reveal_round, &drand_sig_hex); - - assert!(RevealedCommitments::::get(netuid, who).is_none()); - - System::::set_block_number(2); - let weight = as Hooks>::on_initialize(2); - let expected_weight = ::WeightInfo::reveal_timelocked_commitments() - .saturating_add(RocksDbWeight::get().reads(5)) - .saturating_add(RocksDbWeight::get().writes(3)); - assert_eq!(weight, expected_weight); - - let revealed_opt = RevealedCommitments::::get(netuid, who); - assert!( - revealed_opt.is_some(), - "Expected that the timelock got revealed at block #2" - ); - - let leftover = CommitmentOf::::get(netuid, who); - assert!( - leftover.is_none(), - "After revealing the only timelock, the entire commitment is removed." - ); - - assert!( - !TimelockedIndex::::get().contains(&(netuid, who)), - "No longer in TimelockedIndex after reveal." - ); - - let (revealed_bytes, reveal_block) = - revealed_opt.expect("expected to not panic")[0].clone(); - assert_eq!(reveal_block, 2, "Should have revealed at block #2"); - - let revealed_str = sp_std::str::from_utf8(&revealed_bytes) - .expect("Expected valid UTF-8 in the revealed bytes for this test"); - - let original_str = - sp_std::str::from_utf8(message_text).expect("`message_text` is valid UTF-8"); - assert!( - revealed_str.contains(original_str), - "Revealed data must contain the original message text." - ); - }); -} - -#[test] -fn set_commitment_unreserve_leftover_fails() { - new_test_ext().execute_with(|| { - use frame_system::RawOrigin; - - let netuid = NetUid::from(999); - let who = 99; - - Balances::make_free_balance_be(&who, 10_000.into()); - - let fake_deposit: TaoBalance = 100.into(); - let dummy_info = CommitmentInfo:: { - fields: BoundedVec::try_from(vec![]).expect("empty fields is fine"), - }; - let registration = Registration:: { - deposit: fake_deposit, - info: dummy_info, - block: 0u64.into(), - }; - - CommitmentOf::::insert(netuid, who, registration); - - assert_ok!(Balances::reserve(&who, fake_deposit)); - assert_eq!(Balances::reserved_balance(who), 100.into()); - - Balances::unreserve(&who, 10_000.into()); - assert_eq!(Balances::reserved_balance(who), 0.into()); - - let commit_small = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![]).expect("no fields is fine"), - }); - - assert_noop!( - Pallet::::set_commitment(RawOrigin::Signed(who).into(), netuid, commit_small), - Error::::UnexpectedUnreserveLeftover - ); - }); -} - -#[test] -fn timelocked_index_complex_scenario_works() { - new_test_ext().execute_with(|| { - System::::set_block_number(1); - - let netuid = NetUid::from(42); - let user_a = 1000; - let user_b = 2000; - let user_c = 3000; - - let make_timelock_data = |plaintext: &[u8], round: u64| { - let inner = CommitmentInfo:: { - fields: BoundedVec::try_from(vec![Data::Raw( - plaintext.to_vec().try_into().expect("<=128 bytes"), - )]) - .expect("1 field is fine"), - }; - let ct = produce_ciphertext(&inner.encode(), round); - Data::TimelockEncrypted { - encrypted: ct, - reveal_round: round, - } - }; - - let make_raw_data = - |payload: &[u8]| Data::Raw(payload.to_vec().try_into().expect("expected to not panic")); - - // ---------------------------------------------------- - // (1) USER A => no timelocks => NOT in index - // ---------------------------------------------------- - let info_a1 = CommitmentInfo:: { - fields: BoundedVec::try_from(vec![make_raw_data(b"A-regular")]) - .expect("1 field is fine"), - }; - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(user_a), - netuid, - Box::new(info_a1), - )); - assert!( - !TimelockedIndex::::get().contains(&(netuid, user_a)), - "A has no timelocks => not in TimelockedIndex" - ); - - // ---------------------------------------------------- - // (2) USER B => Single TLE => BUT USE round=2000! - // => B is in index - // ---------------------------------------------------- - let b_timelock_1 = make_timelock_data(b"B first TLE", 2000); - let info_b1 = CommitmentInfo:: { - fields: BoundedVec::try_from(vec![b_timelock_1]).expect("Single TLE is fine"), - }; - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(user_b), - netuid, - Box::new(info_b1), - )); - let idx = TimelockedIndex::::get(); - assert!(!idx.contains(&(netuid, user_a)), "A not in index"); - assert!(idx.contains(&(netuid, user_b)), "B in index (has TLE)"); - - // ---------------------------------------------------- - // (3) USER A => 2 timelocks: round=1000 & round=2000 - // => A is in index - // ---------------------------------------------------- - let a_timelock_1 = make_timelock_data(b"A TLE #1", 1000); - let a_timelock_2 = make_timelock_data(b"A TLE #2", 2000); - let info_a2 = CommitmentInfo:: { - fields: BoundedVec::try_from(vec![a_timelock_1, a_timelock_2]) - .expect("2 TLE fields OK"), - }; - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(user_a), - netuid, - Box::new(info_a2), - )); - - let idx = TimelockedIndex::::get(); - assert!(idx.contains(&(netuid, user_a)), "A in index"); - assert!(idx.contains(&(netuid, user_b)), "B still in index"); - - // ---------------------------------------------------- - // (4) USER B => remove all timelocks => B out of index - // ---------------------------------------------------- - let info_b2 = CommitmentInfo:: { - fields: BoundedVec::try_from(vec![make_raw_data(b"B back to raw")]) - .expect("no TLE => B out"), - }; - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(user_b), - netuid, - Box::new(info_b2), - )); - let idx = TimelockedIndex::::get(); - assert!(idx.contains(&(netuid, user_a)), "A remains"); - assert!( - !idx.contains(&(netuid, user_b)), - "B removed after losing TLEs" - ); - - // ---------------------------------------------------- - // (5) USER B => re-add TLE => round=2000 => back in index - // ---------------------------------------------------- - let b_timelock_2 = make_timelock_data(b"B TLE #2", 2000); - let info_b3 = CommitmentInfo:: { - fields: BoundedVec::try_from(vec![b_timelock_2]).expect("expected to not panic"), - }; - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(user_b), - netuid, - Box::new(info_b3), - )); - let idx = TimelockedIndex::::get(); - assert!(idx.contains(&(netuid, user_a)), "A in index"); - assert!(idx.contains(&(netuid, user_b)), "B back in index"); - - // ---------------------------------------------------- - // (6) USER C => sets 1 TLE => round=2000 => in index - // ---------------------------------------------------- - let c_timelock_1 = make_timelock_data(b"C TLE #1", 2000); - let info_c1 = CommitmentInfo:: { - fields: BoundedVec::try_from(vec![c_timelock_1]).expect("expected to not panic"), - }; - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(user_c), - netuid, - Box::new(info_c1), - )); - let idx = TimelockedIndex::::get(); - assert!(idx.contains(&(netuid, user_a)), "A"); - assert!(idx.contains(&(netuid, user_b)), "B"); - assert!(idx.contains(&(netuid, user_c)), "C"); - - // ---------------------------------------------------- - // (7) Partial reveal for round=1000 => affects only A - // because B & C have round=2000 - // ---------------------------------------------------- - let drand_sig_1000 = - hex::decode(DRAND_QUICKNET_SIG_HEX).expect("decode signature for round=1000"); - insert_drand_pulse(1000, &drand_sig_1000); - - System::::set_block_number(10); - assert_ok!(Pallet::::reveal_timelocked_commitments()); - - // After revealing round=1000: - // - A: Loses TLE #1 (1000), still has TLE #2 (2000) => remains in index - // - B: referencing 2000 => unaffected => remains - // - C: referencing 2000 => remains - let idx = TimelockedIndex::::get(); - assert!( - idx.contains(&(netuid, user_a)), - "A has leftover round=2000 => remains in index" - ); - assert!(idx.contains(&(netuid, user_b)), "B unaffected"); - assert!(idx.contains(&(netuid, user_c)), "C unaffected"); - - // ---------------------------------------------------- - // (8) Reveal round=2000 => fully remove A, B, and C - // ---------------------------------------------------- - let drand_sig_2000 = - hex::decode(DRAND_QUICKNET_SIG_2000_HEX).expect("decode signature for round=2000"); - insert_drand_pulse(2000, &drand_sig_2000); - - System::::set_block_number(11); - assert_ok!(Pallet::::reveal_timelocked_commitments()); - - // Now: - // - A's final TLE (#2 at 2000) is removed => A out - // - B had 2000 => out - // - C had 2000 => out - let idx = TimelockedIndex::::get(); - assert!( - !idx.contains(&(netuid, user_a)), - "A removed after 2000 reveal" - ); - assert!( - !idx.contains(&(netuid, user_b)), - "B removed after 2000 reveal" - ); - assert!( - !idx.contains(&(netuid, user_c)), - "C removed after 2000 reveal" - ); - - assert_eq!(idx.len(), 0, "All users revealed => index is empty"); - }); -} - -#[allow(clippy::indexing_slicing)] -#[test] -fn reveal_timelocked_bad_timelocks_are_removed() { - new_test_ext().execute_with(|| { - // - // 1) Prepare multiple Data::TimelockEncrypted fields with different “badness” scenarios + one good field - // - // Round used for valid Drand signature - let valid_round = 1000; - // Round used for intentionally invalid Drand signature - let invalid_sig_round = 999; - // Round that has *no* Drand pulse => timelock remains stored, not revealed yet - let no_pulse_round = 2001; - - // (a) TLE #1: Round=999 => Drand pulse *exists* but signature is invalid => skip/deleted - let plaintext_1 = b"BadSignature"; - let ciphertext_1 = produce_ciphertext(plaintext_1, invalid_sig_round); - let tle_bad_sig = Data::TimelockEncrypted { - encrypted: ciphertext_1, - reveal_round: invalid_sig_round, - }; - - // (b) TLE #2: Round=1000 => Drand signature is valid, but ciphertext is corrupted => skip/deleted - let plaintext_2 = b"CorruptedCiphertext"; - let good_ct_2 = produce_ciphertext(plaintext_2, valid_round); - let mut corrupted_ct_2 = good_ct_2.into_inner(); - if !corrupted_ct_2.is_empty() { - corrupted_ct_2[0] ^= 0xFF; // flip a byte - } - let tle_corrupted = Data::TimelockEncrypted { - encrypted: corrupted_ct_2.try_into().expect("Expected not to panic"), - reveal_round: valid_round, - }; - - // (c) TLE #3: Round=1000 => Drand signature valid, ciphertext good, *but* plaintext is empty => skip/deleted - let empty_good_ct = produce_ciphertext(&[], valid_round); - let tle_empty_plaintext = Data::TimelockEncrypted { - encrypted: empty_good_ct, - reveal_round: valid_round, - }; - - // (d) TLE #4: Round=1000 => Drand signature valid, ciphertext valid, nonempty plaintext => should be revealed - let plaintext_4 = b"Hello, I decrypt fine!"; - let good_ct_4 = produce_ciphertext(plaintext_4, valid_round); - let tle_good = Data::TimelockEncrypted { - encrypted: good_ct_4, - reveal_round: valid_round, - }; - - // (e) TLE #5: Round=2001 => no Drand pulse => remains in storage - let plaintext_5 = b"Still waiting for next round!"; - let good_ct_5 = produce_ciphertext(plaintext_5, no_pulse_round); - let tle_no_pulse = Data::TimelockEncrypted { - encrypted: good_ct_5, - reveal_round: no_pulse_round, - }; - - // - // 2) Assemble them all in one CommitmentInfo - // - let fields = vec![ - tle_bad_sig, // #1 - tle_corrupted, // #2 - tle_empty_plaintext, // #3 - tle_good, // #4 - tle_no_pulse, // #5 - ]; - let fields_bounded = BoundedVec::try_from(fields).expect("Should not exceed MaxFields"); - let info = CommitmentInfo { - fields: fields_bounded, - }; - - // - // 3) Insert the commitment - // - let who = 123; - let netuid = NetUid::from(777); - System::::set_block_number(1); - assert_ok!(Pallet::::set_commitment( - RawOrigin::Signed(who).into(), - netuid, - Box::new(info) - )); - - // - // 4) Insert pulses: - // - Round=999 => invalid signature => attempts to parse => fails => remove TLE #1 - // - Round=1000 => valid signature => TLE #2 is corrupted => remove; #3 empty => remove; #4 reveals successfully - // - Round=2001 => no signature => TLE #5 remains - // - let bad_sig = [0x33u8; 10]; // obviously invalid for TinyBLS - insert_drand_pulse(invalid_sig_round, &bad_sig); - - let drand_sig_1000 = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("Expected not to panic"); - insert_drand_pulse(valid_round, &drand_sig_1000); - - // - // 5) Call reveal => “bad” items are removed, “good” is revealed, “not ready” remains - // - System::::set_block_number(2); - assert_ok!(Pallet::::reveal_timelocked_commitments()); - - // - // 6) Check final storage - // - // (a) TLE #5 => still in fields => same user remains in CommitmentOf => TimelockedIndex includes them - let registration_after = - CommitmentOf::::get(netuid, who).expect("Should still exist"); - assert_eq!( - registration_after.info.fields.len(), - 1, - "Only the unrevealed TLE #5 should remain" - ); - let leftover = ®istration_after.info.fields[0]; - match leftover { - Data::TimelockEncrypted { reveal_round, .. } => { - assert_eq!(*reveal_round, no_pulse_round, "Should be TLE #5 leftover"); - } - _ => panic!("Expected the leftover field to be TLE #5"), - }; - assert!( - TimelockedIndex::::get().contains(&(netuid, who)), - "Still in index because there's one remaining timelock (#5)." - ); - - // (b) TLE #4 => revealed => check that the plaintext matches - let revealed = RevealedCommitments::::get(netuid, who) - .expect("Should have at least one revealed item for TLE #4"); - let (revealed_bytes, reveal_block) = &revealed[0]; - assert_eq!(*reveal_block, 2, "Revealed at block #2"); - - let revealed_str = sp_std::str::from_utf8(revealed_bytes) - .expect("Truncated bytes should be valid UTF-8 in this test"); - - let original_str = - sp_std::str::from_utf8(plaintext_4).expect("plaintext_4 should be valid UTF-8"); - - assert_eq!( - revealed_str, original_str, - "Expected revealed data to match the original plaintext" - ); - - // (c) TLE #1 / #2 / #3 => removed => do NOT appear in leftover fields, nor in revealed (they were invalid) - assert_eq!(revealed.len(), 1, "Only TLE #4 ended up in revealed list"); - }); -} - -#[test] -fn revealed_commitments_keeps_only_10_items() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let who = 2; - let reveal_round = 1000; - - let drand_sig_bytes = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("Should decode DRAND sig"); - insert_drand_pulse(reveal_round, &drand_sig_bytes); - - // --- 1) Build 12 TimelockEncrypted fields --- - // Each one has a unique plaintext "TLE #i" - const TOTAL_TLES: usize = 12; - let mut fields = Vec::with_capacity(TOTAL_TLES); - - for i in 0..TOTAL_TLES { - let plaintext = format!("TLE #{i}").into_bytes(); - let ciphertext = produce_ciphertext(&plaintext, reveal_round); - let timelock = Data::TimelockEncrypted { - encrypted: ciphertext, - reveal_round, - }; - fields.push(timelock); - } - let fields_bounded = BoundedVec::try_from(fields).expect("Should not exceed MaxFields"); - let info = CommitmentInfo { - fields: fields_bounded, - }; - - // --- 2) Set the commitment => 12 timelocks in storage --- - System::::set_block_number(1); - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid, - Box::new(info) - )); - - // --- 3) Reveal => all 12 are decrypted in one shot --- - System::::set_block_number(2); - assert_ok!(Pallet::::reveal_timelocked_commitments()); - - // --- 4) Check we only keep 10 in `RevealedCommitments` --- - let revealed = RevealedCommitments::::get(netuid, who) - .expect("Should have at least some revealed data"); - assert_eq!( - revealed.len(), - 10, - "We must only keep the newest 10, out of 12 total" - ); - - // The oldest 2 ("TLE #0" and "TLE #1") must be dropped. - // The items in `revealed` now correspond to "TLE #2" .. "TLE #11". - for (idx, (revealed_bytes, reveal_block)) in revealed.iter().enumerate() { - // Convert to UTF-8 - let revealed_str = sp_std::str::from_utf8(revealed_bytes) - .expect("Decrypted data should be valid UTF-8 for this test case"); - - // We expect them to be TLE #2..TLE #11 - let expected_index = idx + 2; // since we dropped #0 and #1 - let expected_str = format!("TLE #{expected_index}"); - assert_eq!(revealed_str, expected_str, "Check which TLE is kept"); - - // Also check it was revealed at block 2 - assert_eq!(*reveal_block, 2, "All reveal in the same block #2"); - } - }); -} - -#[test] -fn revealed_commitments_keeps_only_10_newest_with_individual_single_field_commits() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let who = 2; - let reveal_round = 1000; - - let drand_sig_bytes = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("decode DRAND sig"); - insert_drand_pulse(reveal_round, &drand_sig_bytes); - - // We will add 12 separate timelocks, one per iteration, each in its own set_commitment call. - // After each insertion, we call reveal + increment the block by 1. - - for i in 0..12 { - System::::set_block_number(i as u64 + 1); - - let plaintext = format!("TLE #{i}").into_bytes(); - let ciphertext = produce_ciphertext(&plaintext, reveal_round); - - let new_timelock = Data::TimelockEncrypted { - encrypted: ciphertext, - reveal_round, - }; - - let fields = BoundedVec::try_from(vec![new_timelock]) - .expect("Single field is well within MaxFields"); - let info = CommitmentInfo { fields }; - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid, - Box::new(info) - )); - - assert_ok!(Pallet::::reveal_timelocked_commitments()); - - let revealed = RevealedCommitments::::get(netuid, who).unwrap_or_default(); - let expected_count = (i + 1).min(10); - assert_eq!( - revealed.len(), - expected_count, - "At iteration {i}, we keep at most 10 reveals" - ); - } - - let revealed = - RevealedCommitments::::get(netuid, who).expect("expected to not panic"); - assert_eq!( - revealed.len(), - 10, - "After 12 total commits, only 10 remain revealed" - ); - - // Check that TLE #0 and TLE #1 are dropped; TLE #2..#11 remain in ascending order. - for (idx, (revealed_bytes, reveal_block)) in revealed.iter().enumerate() { - let revealed_str = - sp_std::str::from_utf8(revealed_bytes).expect("Should be valid UTF-8"); - let expected_i = idx + 2; // i=0 => "TLE #2", i=1 => "TLE #3", etc. - let expected_str = format!("TLE #{expected_i}"); - - assert_eq!( - revealed_str, expected_str, - "Revealed data #{idx} should match the truncated TLE #{expected_i}" - ); - - let expected_reveal_block = expected_i as u64 + 1; - assert_eq!( - *reveal_block, expected_reveal_block, - "Check which block TLE #{expected_i} was revealed in" - ); - } - }); -} - -#[test] -fn usage_respects_minimum_of_100_bytes() { - new_test_ext().execute_with(|| { - MaxSpace::::set(1000); - - let netuid = NetUid::from(1); - let who = 99; - - System::::set_block_number(1); - - let small_data = Data::Raw(vec![0u8; 50].try_into().expect("<=128 bytes for Raw")); - let info_small = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![small_data]).expect("Must not exceed MaxFields"), - }); - - let usage_before = UsedSpaceOf::::get(netuid, who).unwrap_or_default(); - assert_eq!(usage_before.used_space, 0); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid, - info_small - )); - - let usage_after_small = - UsedSpaceOf::::get(netuid, who).expect("expected to not panic"); - assert_eq!( - usage_after_small.used_space, 100, - "Usage must jump to 100 even though we only used 50 bytes" - ); - - let big_data = Data::Raw(vec![0u8; 110].try_into().expect("<=128 bytes for Raw")); - let info_big = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![big_data]).expect("Must not exceed MaxFields"), - }); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid, - info_big - )); - - let usage_after_big = UsedSpaceOf::::get(netuid, who).expect("expected to not panic"); - assert_eq!( - usage_after_big.used_space, 210, - "Usage should be 100 + 110 = 210 in this epoch" - ); - - UsedSpaceOf::::remove(netuid, who); - let usage_after_wipe = UsedSpaceOf::::get(netuid, who); - assert!( - usage_after_wipe.is_none(), - "Expected `UsedSpaceOf` entry to be removed" - ); - - let bigger_data = Data::Raw(vec![0u8; 120].try_into().expect("<=128 bytes for Raw")); - let info_bigger = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![bigger_data]).expect("Must not exceed MaxFields"), - }); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid, - info_bigger - )); - - let usage_after_reset = - UsedSpaceOf::::get(netuid, who).expect("expected to not panic"); - assert_eq!( - usage_after_reset.used_space, 120, - "After wiping old usage, the new usage should be exactly 120" - ); - }); -} - -#[test] -fn set_commitment_works_with_multiple_raw_fields() { - new_test_ext().execute_with(|| { - let cur_block = 10u64.into(); - System::::set_block_number(cur_block); - let initial_deposit: BalanceOf = ::InitialDeposit::get(); - let field_deposit: BalanceOf = ::FieldDeposit::get(); - - let field1 = Data::Raw(vec![0u8; 10].try_into().expect("<=128 bytes is OK")); - let field2 = Data::Raw(vec![1u8; 20].try_into().expect("<=128 bytes is OK")); - let field3 = Data::Raw(vec![2u8; 50].try_into().expect("<=128 bytes is OK")); - - let info_multiple = CommitmentInfo { - fields: BoundedVec::try_from(vec![field1.clone(), field2.clone(), field3.clone()]) - .expect("<= MaxFields"), - }; - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(12345), - 99.into(), - Box::new(info_multiple) - )); - - let expected_deposit: BalanceOf = initial_deposit + field_deposit * 3u64.into(); - let stored = CommitmentOf::::get(NetUid::from(99), 12345).expect("Should be stored"); - assert_eq!( - stored.deposit, expected_deposit, - "Deposit must equal initial + 3 * field_deposit" - ); - - assert_eq!(stored.block, cur_block, "Stored block must match cur_block"); - - let usage = - UsedSpaceOf::::get(NetUid::from(99), 12345).expect("Expected to not panic"); - assert_eq!( - usage.used_space, 100, - "Usage is clamped to 100 when sum of fields is < 100" - ); - - let next_block = 11u64.into(); - System::::set_block_number(next_block); - - let info_two_fields = CommitmentInfo { - fields: BoundedVec::try_from(vec![field1.clone(), field2.clone()]) - .expect("<= MaxFields"), - }; - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(12345), - 99.into(), - Box::new(info_two_fields) - )); - - let expected_deposit2: BalanceOf = initial_deposit + field_deposit * 2u64.into(); - let stored2 = CommitmentOf::::get(NetUid::from(99), 12345).expect("Should be stored"); - assert_eq!( - stored2.deposit, expected_deposit2, - "Deposit must have decreased after removing one field" - ); - - let usage2 = - UsedSpaceOf::::get(NetUid::from(99), 12345).expect("Expected to not panic"); - let expected_usage2 = 200u64; - assert_eq!( - usage2.used_space, expected_usage2, - "Usage accumulates in the same epoch, respecting the min usage of 100 each time" - ); - - let events = System::::events(); - let expected_event = RuntimeEvent::Commitments(Event::Commitment { - netuid: 99.into(), - who: 12345, - }); - let found_commitment_event = events.iter().any(|e| e.event == expected_event); - assert!( - found_commitment_event, - "Expected at least one Event::Commitment to be emitted" - ); - }); -} - -#[allow(clippy::indexing_slicing)] -#[test] -fn multiple_timelocked_commitments_reveal_works() { - new_test_ext().execute_with(|| { - // ------------------------------------------- - // 1) Set up initial block number and user - // ------------------------------------------- - let cur_block = 5u64.into(); - System::::set_block_number(cur_block); - - let who = 123; - let netuid = NetUid::from(999); - - // ------------------------------------------- - // 2) Create multiple TLE fields referencing - // two known valid Drand rounds: 1000, 2000 - // ------------------------------------------- - - let round_1000 = 1000; - let round_2000 = 2000; - - // 2.a) TLE #1 => round=1000 - let tle_1_plaintext = b"Timelock #1 => round=1000"; - let ciphertext_1 = produce_ciphertext(tle_1_plaintext, round_1000); - let tle_1 = Data::TimelockEncrypted { - encrypted: ciphertext_1, - reveal_round: round_1000, - }; - - // 2.b) TLE #2 => round=1000 - let tle_2_plaintext = b"Timelock #2 => round=1000"; - let ciphertext_2 = produce_ciphertext(tle_2_plaintext, round_1000); - let tle_2 = Data::TimelockEncrypted { - encrypted: ciphertext_2, - reveal_round: round_1000, - }; - - // 2.c) TLE #3 => round=2000 - let tle_3_plaintext = b"Timelock #3 => round=2000"; - let ciphertext_3 = produce_ciphertext(tle_3_plaintext, round_2000); - let tle_3 = Data::TimelockEncrypted { - encrypted: ciphertext_3, - reveal_round: round_2000, - }; - - // 2.d) TLE #4 => round=2000 - let tle_4_plaintext = b"Timelock #4 => round=2000"; - let ciphertext_4 = produce_ciphertext(tle_4_plaintext, round_2000); - let tle_4 = Data::TimelockEncrypted { - encrypted: ciphertext_4, - reveal_round: round_2000, - }; - - // ------------------------------------------- - // 3) Insert all TLEs in a single CommitmentInfo - // ------------------------------------------- - let fields = vec![tle_1, tle_2, tle_3, tle_4]; - let fields_bounded = BoundedVec::try_from(fields).expect("Must not exceed MaxFields"); - let info = CommitmentInfo { - fields: fields_bounded, - }; - - // ------------------------------------------- - // 4) set_commitment => user is now in TimelockedIndex - // ------------------------------------------- - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid, - Box::new(info) - )); - assert!( - TimelockedIndex::::get().contains(&(netuid, who)), - "User must appear in TimelockedIndex since they have TLE fields" - ); - - // Confirm the stored fields are as expected - let stored = CommitmentOf::::get(netuid, who).expect("Should be stored"); - assert_eq!( - stored.info.fields.len(), - 4, - "All 4 timelock fields must be stored" - ); - - // ------------------------------------------- - // 5) Insert valid Drand pulse => round=1000 - // ------------------------------------------- - let drand_sig_1000 = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("decode signature"); - insert_drand_pulse(round_1000, &drand_sig_1000); - - // Reveal at block=6 => should remove TLE #1 and TLE #2, leaving TLE #3, #4 - System::::set_block_number(6u64.into()); - assert_ok!(Pallet::::reveal_timelocked_commitments()); - - // Check leftover => TLE #3, TLE #4 remain - let leftover_after_1000 = CommitmentOf::::get(netuid, who).expect("Must exist"); - assert_eq!( - leftover_after_1000.info.fields.len(), - 2, - "After revealing round=1000, 2 timelocks remain (#3, #4)" - ); - - // Check partial reveals => TLE #1 & #2 in revealed storage - let revealed_1000 = RevealedCommitments::::get(netuid, who) - .expect("Should have partial reveals"); - assert_eq!( - revealed_1000.len(), - 2, - "We revealed exactly 2 items at round=1000" - ); - { - let (bytes_a, _) = &revealed_1000[0]; - let (bytes_b, _) = &revealed_1000[1]; - let txt_a = sp_std::str::from_utf8(bytes_a).expect("utf-8 expected"); - let txt_b = sp_std::str::from_utf8(bytes_b).expect("utf-8 expected"); - assert!( - txt_a.contains("Timelock #1") || txt_a.contains("Timelock #2"), - "Revealed #1 or #2" - ); - assert!( - txt_b.contains("Timelock #1") || txt_b.contains("Timelock #2"), - "Revealed #1 or #2" - ); - } - - assert!( - TimelockedIndex::::get().contains(&(netuid, who)), - "TLE left" - ); - - // ------------------------------------------- - // 6) Insert valid Drand pulse => round=2000 - // ------------------------------------------- - let drand_sig_2000_hex = - "b6cb8f482a0b15d45936a4c4ea08e98a087e71787caee3f4d07a8a9843b1bc5423c6b3c22f446488b3137eaca799c77e"; - let drand_sig_2000 = hex::decode(drand_sig_2000_hex).expect("decode signature"); - insert_drand_pulse(round_2000, &drand_sig_2000); - - // Reveal at block=7 => should remove TLE #3 and TLE #4 - System::::set_block_number(7u64.into()); - assert_ok!(Pallet::::reveal_timelocked_commitments()); - - // After revealing these last two timelocks => leftover is none - let leftover_after_2000 = CommitmentOf::::get(netuid, who); - assert!( - leftover_after_2000.is_none(), - "All timelocks revealed => leftover none => entry removed" - ); - - // Because the user has no timelocks left => removed from TimelockedIndex - assert!( - !TimelockedIndex::::get().contains(&(netuid, who)), - "No TLE left => user removed from index" - ); - - // Check TLE #3 and #4 were appended to revealed - let revealed_final = RevealedCommitments::::get(netuid, who) - .expect("Should exist with final reveals"); - assert_eq!( - revealed_final.len(), - 4, - "We should have all 4 TLE items revealed in total" - ); - - // The final two items in `revealed_final` must be #3, #4 - let (third_bytes, _) = &revealed_final[2]; - let (fourth_bytes, _) = &revealed_final[3]; - let third_txt = sp_std::str::from_utf8(third_bytes).expect("utf-8 expected"); - let fourth_txt = sp_std::str::from_utf8(fourth_bytes).expect("utf-8 expected"); - - assert!( - third_txt.contains("Timelock #3"), - "Expected TLE #3 among final reveals" - ); - assert!( - fourth_txt.contains("Timelock #4"), - "Expected TLE #4 among final reveals" - ); - }); -} - -#[allow(clippy::indexing_slicing)] -#[test] -fn mixed_timelocked_and_raw_fields_works() { - new_test_ext().execute_with(|| { - // ------------------------------------------- - // 1) Setup initial block number and user - // ------------------------------------------- - let cur_block = 3u64.into(); - System::::set_block_number(cur_block); - - let who = 77; - let netuid = NetUid::from(501); - - // ------------------------------------------- - // 2) Create raw fields and timelocked fields - // ------------------------------------------- - // We'll use 2 raw fields, and 2 timelocked fields referencing - // 2 Drand rounds (1000 and 2000) that we know have valid signatures. - - // Round constants: - let round_1000 = 1000; - let round_2000 = 2000; - - // (a) Timelock #1 => round=1000 - let tle_1_plaintext = b"TLE #1 => round=1000"; - let ciphertext_1 = produce_ciphertext(tle_1_plaintext, round_1000); - let tle_1 = Data::TimelockEncrypted { - encrypted: ciphertext_1, - reveal_round: round_1000, - }; - - // (b) Timelock #2 => round=2000 - let tle_2_plaintext = b"TLE #2 => round=2000"; - let ciphertext_2 = produce_ciphertext(tle_2_plaintext, round_2000); - let tle_2 = Data::TimelockEncrypted { - encrypted: ciphertext_2, - reveal_round: round_2000, - }; - - // (c) Two Raw fields - let raw_1 = Data::Raw(b"Raw field #1".to_vec().try_into().expect("<= 128 bytes")); - let raw_2 = Data::Raw(b"Raw field #2".to_vec().try_into().expect("<= 128 bytes")); - - // We'll put them in a single vector: [TLE #1, raw_1, TLE #2, raw_2] - let all_fields = vec![tle_1, raw_1.clone(), tle_2, raw_2.clone()]; - let fields_bounded = BoundedVec::try_from(all_fields).expect("<= MaxFields"); - - // ------------------------------------------- - // 3) Submit the single commitment - // ------------------------------------------- - let info = CommitmentInfo { fields: fields_bounded }; - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid, - Box::new(info) - )); - - // The user should appear in TimelockedIndex because they have timelocked fields. - assert!( - TimelockedIndex::::get().contains(&(netuid, who)), - "User must be in TimelockedIndex with TLE fields" - ); - - // Check the stored data - let stored = CommitmentOf::::get(netuid, who).expect("Should exist in storage"); - assert_eq!( - stored.info.fields.len(), - 4, - "We have 2 raw + 2 TLE fields in total" - ); - - // ------------------------------------------- - // 4) Insert Drand signature for round=1000 => partial reveal - // ------------------------------------------- - let drand_sig_1000 = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("decode signature"); - insert_drand_pulse(round_1000, &drand_sig_1000); - - System::::set_block_number(4u64.into()); - assert_ok!(Pallet::::reveal_timelocked_commitments()); - - // => TLE #1 (round=1000) is revealed. TLE #2 (round=2000) remains locked. - // => The two raw fields remain untouched. - let leftover_after_1000 = CommitmentOf::::get(netuid, who).expect("Must still exist"); - assert_eq!( - leftover_after_1000.info.fields.len(), - 3, - "One TLE removed => leftover=3 fields: TLE #2 + raw_1 + raw_2" - ); - - // Make sure user is still in TimelockedIndex (they still have TLE #2) - assert!( - TimelockedIndex::::get().contains(&(netuid, who)), - "Still has leftover TLE #2 => remains in index" - ); - - // Check partial reveal - let revealed_1000 = RevealedCommitments::::get(netuid, who) - .expect("Should have partial reveals"); - assert_eq!( - revealed_1000.len(), - 1, - "We revealed exactly 1 item at round=1000" - ); - let (revealed_bytes_1, _block_1) = &revealed_1000[0]; - let revealed_str_1 = - sp_std::str::from_utf8(revealed_bytes_1).expect("Should parse as UTF-8"); - assert!( - revealed_str_1.contains("TLE #1 => round=1000"), - "Check that TLE #1 was revealed" - ); - - // ------------------------------------------- - // 5) Insert Drand signature for round=2000 => final TLE reveal - // ------------------------------------------- - let drand_sig_2000_hex = - "b6cb8f482a0b15d45936a4c4ea08e98a087e71787caee3f4d07a8a9843b1bc5423c6b3c22f446488b3137eaca799c77e"; - let drand_sig_2000 = hex::decode(drand_sig_2000_hex).expect("decode signature"); - insert_drand_pulse(round_2000, &drand_sig_2000); - - System::::set_block_number(5u64.into()); - assert_ok!(Pallet::::reveal_timelocked_commitments()); - - // => TLE #2 is now revealed. The two raw fields remain. - let leftover_after_2000 = CommitmentOf::::get(netuid, who).expect("Still exists"); - let leftover_fields = &leftover_after_2000.info.fields; - assert_eq!( - leftover_fields.len(), - 2, - "Only the 2 raw fields remain after TLE #2 is revealed" - ); - - assert_eq!( - leftover_fields[0], - raw_1, - "Leftover field[0] must match raw_1" - ); - assert_eq!( - leftover_fields[1], - raw_2, - "Leftover field[1] must match raw_2" - ); - - // The user has no leftover timelocks => removed from TimelockedIndex - assert!( - !TimelockedIndex::::get().contains(&(netuid, who)), - "No more TLE => user removed from index" - ); - - // But the record is still present in storage (because raw fields remain) - // => leftover_fields must match our original raw fields. - let [f1, f2] = &leftover_fields[..] else { - panic!("Expected exactly 2 fields leftover"); - }; - assert_eq!(f1, &raw_1, "Raw field #1 remains unaltered"); - assert_eq!(f2, &raw_2, "Raw field #2 remains unaltered"); - - // Check that TLE #2 was appended to revealed data - let revealed_final = RevealedCommitments::::get(netuid, who) - .expect("Should have final reveals"); - assert_eq!( - revealed_final.len(), - 2, - "Now we have 2 revealed TLE items total (TLE #1 and TLE #2)." - ); - let (revealed_bytes_2, _block_2) = &revealed_final[1]; - let revealed_str_2 = - sp_std::str::from_utf8(revealed_bytes_2).expect("Should parse as UTF-8"); - assert!( - revealed_str_2.contains("TLE #2 => round=2000"), - "Check that TLE #2 was revealed" - ); - }); -} - -#[test] -fn purge_netuid_clears_only_that_netuid() { - new_test_ext().execute_with(|| { - // Setup - System::::set_block_number(1); - - let net_a = NetUid::from(42); - let net_b = NetUid::from(43); - let who_a1: u64 = 1001; - let who_a2: u64 = 1002; - let who_b: u64 = 2001; - - // Minimal commitment payload - let empty_fields: BoundedVec::MaxFields> = BoundedVec::default(); - let info_empty: CommitmentInfo<::MaxFields> = CommitmentInfo { - fields: empty_fields, - }; - let bn = System::::block_number(); - - // Seed NET A with two accounts across all tracked storages - let reg_a1 = Registration { - deposit: Default::default(), - block: bn, - info: info_empty.clone(), - }; - let reg_a2 = Registration { - deposit: Default::default(), - block: bn, - info: info_empty.clone(), - }; - CommitmentOf::::insert(net_a, who_a1, reg_a1); - CommitmentOf::::insert(net_a, who_a2, reg_a2); - LastCommitment::::insert(net_a, who_a1, bn); - LastCommitment::::insert(net_a, who_a2, bn); - LastBondsReset::::insert(net_a, who_a1, bn); - RevealedCommitments::::insert(net_a, who_a1, vec![(b"a".to_vec(), 7u64)]); - UsedSpaceOf::::insert( - net_a, - who_a1, - UsageTracker { - last_epoch: 1, - used_space: 123, - }, - ); - - // Seed NET B with one account that must remain intact - let reg_b = Registration { - deposit: Default::default(), - block: bn, - info: info_empty, - }; - CommitmentOf::::insert(net_b, who_b, reg_b); - LastCommitment::::insert(net_b, who_b, bn); - LastBondsReset::::insert(net_b, who_b, bn); - RevealedCommitments::::insert(net_b, who_b, vec![(b"b".to_vec(), 8u64)]); - UsedSpaceOf::::insert( - net_b, - who_b, - UsageTracker { - last_epoch: 9, - used_space: 999, - }, - ); - - // Timelocked index contains both nets - TimelockedIndex::::mutate(|idx| { - idx.insert((net_a, who_a1)); - idx.insert((net_a, who_a2)); - idx.insert((net_b, who_b)); - }); - - // Sanity pre-checks - assert!(CommitmentOf::::get(net_a, who_a1).is_some()); - assert!(CommitmentOf::::get(net_b, who_b).is_some()); - assert!(TimelockedIndex::::get().contains(&(net_a, who_a1))); - - // Act - purge_netuid_with_meter(net_a, Weight::from_parts(u64::MAX, u64::MAX)); - - // NET A: everything cleared - assert_eq!(CommitmentOf::::iter_prefix(net_a).count(), 0); - assert!(CommitmentOf::::get(net_a, who_a1).is_none()); - assert!(CommitmentOf::::get(net_a, who_a2).is_none()); - - assert_eq!(LastCommitment::::iter_prefix(net_a).count(), 0); - assert!(LastCommitment::::get(net_a, who_a1).is_none()); - assert!(LastCommitment::::get(net_a, who_a2).is_none()); - - assert_eq!(LastBondsReset::::iter_prefix(net_a).count(), 0); - assert!(LastBondsReset::::get(net_a, who_a1).is_none()); - - assert_eq!(RevealedCommitments::::iter_prefix(net_a).count(), 0); - assert!(RevealedCommitments::::get(net_a, who_a1).is_none()); - - assert_eq!(UsedSpaceOf::::iter_prefix(net_a).count(), 0); - assert!(UsedSpaceOf::::get(net_a, who_a1).is_none()); - - let idx_after = TimelockedIndex::::get(); - assert!(!idx_after.contains(&(net_a, who_a1))); - assert!(!idx_after.contains(&(net_a, who_a2))); - - // NET B: untouched - assert!(CommitmentOf::::get(net_b, who_b).is_some()); - assert!(LastCommitment::::get(net_b, who_b).is_some()); - assert!(LastBondsReset::::get(net_b, who_b).is_some()); - assert!(RevealedCommitments::::get(net_b, who_b).is_some()); - assert!(UsedSpaceOf::::get(net_b, who_b).is_some()); - assert!(idx_after.contains(&(net_b, who_b))); - - // Idempotency - purge_netuid_with_meter(net_a, Weight::from_parts(u64::MAX, u64::MAX)); - assert_eq!(CommitmentOf::::iter_prefix(net_a).count(), 0); - assert!(!TimelockedIndex::::get().contains(&(net_a, who_a1))); - }); -} - -/// `purge_netuid` runs weighted prefix clears **before** the timelock-index update. The macro batch -/// sizing uses the meter's **limit** (not accumulated consumption), so maps may already be empty -/// when the weight budget runs out; `done == false` must still mean the timelock index -/// row for this netuid survives until a later call with enough budget. -#[test] -fn purge_netuid_under_budget_may_skip_timelock_update_while_clearing_maps() { - new_test_ext().execute_with(|| { - System::::set_block_number(1); - let net_a = NetUid::from(77); - let who_a: u64 = 4001; - - let empty_fields: BoundedVec::MaxFields> = BoundedVec::default(); - let info_empty: CommitmentInfo<::MaxFields> = CommitmentInfo { - fields: empty_fields, - }; - let bn = System::::block_number(); - let reg = Registration { - deposit: Default::default(), - block: bn, - info: info_empty, - }; - CommitmentOf::::insert(net_a, who_a, reg); - LastCommitment::::insert(net_a, who_a, bn); - LastBondsReset::::insert(net_a, who_a, bn); - RevealedCommitments::::insert(net_a, who_a, vec![(b"x".to_vec(), 1u64)]); - UsedSpaceOf::::insert( - net_a, - who_a, - UsageTracker { - last_epoch: 1, - used_space: 1, - }, - ); - TimelockedIndex::::mutate(|idx| { - idx.insert((net_a, who_a)); - }); - - let write1 = ::DbWeight::get().writes(1); - // Budget is strictly below one DB write, so the weighted prefix clears inside - // `purge_netuid` reliably run out of budget and report `done == false`. - let budget = write1.saturating_sub(Weight::from_parts(1, 1)); - - let done = purge_netuid_with_meter(net_a, budget); - assert!( - !done, - "purge_netuid must report not-done when under-budget" - ); - assert!( - TimelockedIndex::::get().contains(&(net_a, who_a)), - "timelock index is only trimmed after a successful final pass; stale index entries are expected if that write is skipped" - ); - - // Full budget finishes (including timelock index), even if prior pass already cleared maps. - let done = purge_netuid_with_meter(net_a, Weight::from_parts(u64::MAX, u64::MAX)); - assert!(done); - assert!(CommitmentOf::::get(net_a, who_a).is_none()); - assert!(!TimelockedIndex::::get().contains(&(net_a, who_a))); - }); -} diff --git a/pallets/commitments/src/tests/data_type_info.rs b/pallets/commitments/src/tests/data_type_info.rs new file mode 100644 index 0000000000..ceb862ccd1 --- /dev/null +++ b/pallets/commitments/src/tests/data_type_info.rs @@ -0,0 +1,99 @@ +//! Tests for commitments pallet: data type info. + +use super::*; + +#[test] +fn manual_data_type_info() { + let mut registry = scale_info::Registry::new(); + let type_id = registry.register_type(&scale_info::meta_type::()); + let registry: scale_info::PortableRegistry = registry.into(); + let type_info = registry.resolve(type_id.id).expect("Expected not to panic"); + + let check_type_info = |data: &Data| { + let variant_name = match data { + Data::None => "None".to_string(), + Data::BlakeTwo256(_) => "BlakeTwo256".to_string(), + Data::Sha256(_) => "Sha256".to_string(), + Data::Keccak256(_) => "Keccak256".to_string(), + Data::ShaThree256(_) => "ShaThree256".to_string(), + Data::Raw(bytes) => format!("Raw{}", bytes.len()), + Data::TimelockEncrypted { .. } => "TimelockEncrypted".to_string(), + Data::ResetBondsFlag => "ResetBondsFlag".to_string(), + Data::BigRaw(_) => "BigRaw".to_string(), + }; + if let scale_info::TypeDef::Variant(variant) = &type_info.type_def { + let variant = variant + .variants + .iter() + .find(|v| v.name == variant_name) + .unwrap_or_else(|| panic!("Expected to find variant {variant_name}")); + + let encoded = data.encode(); + assert_eq!(encoded[0], variant.index); + + // For variants with fields, check the encoded length matches expected field lengths + if !variant.fields.is_empty() { + let expected_len = match data { + Data::None => 0, + Data::Raw(bytes) => bytes.len() as u32, + Data::BigRaw(bytes) => bytes.len() as u32, + Data::BlakeTwo256(_) + | Data::Sha256(_) + | Data::Keccak256(_) + | Data::ShaThree256(_) => 32, + Data::TimelockEncrypted { + encrypted, + reveal_round, + } => { + // Calculate length: encrypted (length prefixed) + reveal_round (u64) + let encrypted_len = encrypted.encode().len() as u32; // Includes length prefix + let reveal_round_len = reveal_round.encode().len() as u32; // Typically 8 bytes + encrypted_len + reveal_round_len + } + Data::ResetBondsFlag => 0, + }; + assert_eq!( + encoded.len() as u32 - 1, // Subtract variant byte + expected_len, + "Encoded length mismatch for variant {variant_name}" + ); + } else { + assert_eq!( + encoded.len() as u32 - 1, + 0, + "Expected no fields for {variant_name}" + ); + } + } else { + panic!("Should be a variant type"); + } + }; + + let mut data = vec![ + Data::None, + Data::BlakeTwo256(Default::default()), + Data::Sha256(Default::default()), + Data::Keccak256(Default::default()), + Data::ShaThree256(Default::default()), + Data::ResetBondsFlag, + ]; + + // Add Raw instances for all possible sizes + for n in 0..128 { + data.push(Data::Raw( + vec![0u8; n as usize] + .try_into() + .expect("Expected not to panic"), + )); + } + + // Add a TimelockEncrypted instance + data.push(Data::TimelockEncrypted { + encrypted: vec![0u8; 64].try_into().expect("Expected not to panic"), + reveal_round: 12345, + }); + + for d in data.iter() { + check_type_info(d); + } +} diff --git a/pallets/commitments/src/tests/mod.rs b/pallets/commitments/src/tests/mod.rs new file mode 100644 index 0000000000..5061dc9d81 --- /dev/null +++ b/pallets/commitments/src/tests/mod.rs @@ -0,0 +1,40 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing)] + +//! Unit tests for the commitments pallet. + +use codec::Encode; +use sp_std::prelude::*; +use subtensor_runtime_common::{NetUid, TaoBalance}; + +#[cfg(test)] +use crate::{ + BalanceOf, CommitmentInfo, CommitmentOf, Config, Data, Error, Event, LastBondsReset, + LastCommitment, MaxSpace, Pallet, Registration, RevealedCommitments, TimelockedIndex, + UsageTracker, UsedSpaceOf, WeightInfo, + mock::{ + Balances, DRAND_QUICKNET_SIG_2000_HEX, DRAND_QUICKNET_SIG_HEX, RuntimeEvent, RuntimeOrigin, + Test, TestMaxFields, insert_drand_pulse, new_test_ext, produce_ciphertext, + }, +}; +use frame_support::pallet_prelude::Hooks; +use frame_support::{ + BoundedVec, assert_noop, assert_ok, + traits::{Currency, Get, ReservableCurrency}, + weights::{Weight, constants::RocksDbWeight}, +}; +use frame_system::{Pallet as System, RawOrigin}; + +mod data_type_info; +mod purge_netuid; +mod revealed_commitments; +mod set_commitment; +mod space_limit; +mod timelock_mixed_fields; +mod timelock_reveal; +mod timelocked_index; + +/// Runs [`Pallet::purge_netuid`] under a weight meter capped at `limit`. +fn purge_netuid_with_meter(netuid: NetUid, limit: Weight) -> bool { + let mut weight_meter = frame_support::weights::WeightMeter::with_limit(limit); + Pallet::::purge_netuid(netuid, &mut weight_meter) +} diff --git a/pallets/commitments/src/tests/purge_netuid.rs b/pallets/commitments/src/tests/purge_netuid.rs new file mode 100644 index 0000000000..d812519ca7 --- /dev/null +++ b/pallets/commitments/src/tests/purge_netuid.rs @@ -0,0 +1,179 @@ +//! Tests for commitments pallet: purge netuid. + +use super::*; + +#[test] +fn purge_netuid_clears_only_that_netuid() { + new_test_ext().execute_with(|| { + // Setup + System::::set_block_number(1); + + let net_a = NetUid::from(42); + let net_b = NetUid::from(43); + let who_a1: u64 = 1001; + let who_a2: u64 = 1002; + let who_b: u64 = 2001; + + // Minimal commitment payload + let empty_fields: BoundedVec::MaxFields> = BoundedVec::default(); + let info_empty: CommitmentInfo<::MaxFields> = CommitmentInfo { + fields: empty_fields, + }; + let bn = System::::block_number(); + + // Seed NET A with two accounts across all tracked storages + let reg_a1 = Registration { + deposit: Default::default(), + block: bn, + info: info_empty.clone(), + }; + let reg_a2 = Registration { + deposit: Default::default(), + block: bn, + info: info_empty.clone(), + }; + CommitmentOf::::insert(net_a, who_a1, reg_a1); + CommitmentOf::::insert(net_a, who_a2, reg_a2); + LastCommitment::::insert(net_a, who_a1, bn); + LastCommitment::::insert(net_a, who_a2, bn); + LastBondsReset::::insert(net_a, who_a1, bn); + RevealedCommitments::::insert(net_a, who_a1, vec![(b"a".to_vec(), 7u64)]); + UsedSpaceOf::::insert( + net_a, + who_a1, + UsageTracker { + last_epoch: 1, + used_space: 123, + }, + ); + + // Seed NET B with one account that must remain intact + let reg_b = Registration { + deposit: Default::default(), + block: bn, + info: info_empty, + }; + CommitmentOf::::insert(net_b, who_b, reg_b); + LastCommitment::::insert(net_b, who_b, bn); + LastBondsReset::::insert(net_b, who_b, bn); + RevealedCommitments::::insert(net_b, who_b, vec![(b"b".to_vec(), 8u64)]); + UsedSpaceOf::::insert( + net_b, + who_b, + UsageTracker { + last_epoch: 9, + used_space: 999, + }, + ); + + // Timelocked index contains both nets + TimelockedIndex::::mutate(|idx| { + idx.insert((net_a, who_a1)); + idx.insert((net_a, who_a2)); + idx.insert((net_b, who_b)); + }); + + // Sanity pre-checks + assert!(CommitmentOf::::get(net_a, who_a1).is_some()); + assert!(CommitmentOf::::get(net_b, who_b).is_some()); + assert!(TimelockedIndex::::get().contains(&(net_a, who_a1))); + + // Act + purge_netuid_with_meter(net_a, Weight::from_parts(u64::MAX, u64::MAX)); + + // NET A: everything cleared + assert_eq!(CommitmentOf::::iter_prefix(net_a).count(), 0); + assert!(CommitmentOf::::get(net_a, who_a1).is_none()); + assert!(CommitmentOf::::get(net_a, who_a2).is_none()); + + assert_eq!(LastCommitment::::iter_prefix(net_a).count(), 0); + assert!(LastCommitment::::get(net_a, who_a1).is_none()); + assert!(LastCommitment::::get(net_a, who_a2).is_none()); + + assert_eq!(LastBondsReset::::iter_prefix(net_a).count(), 0); + assert!(LastBondsReset::::get(net_a, who_a1).is_none()); + + assert_eq!(RevealedCommitments::::iter_prefix(net_a).count(), 0); + assert!(RevealedCommitments::::get(net_a, who_a1).is_none()); + + assert_eq!(UsedSpaceOf::::iter_prefix(net_a).count(), 0); + assert!(UsedSpaceOf::::get(net_a, who_a1).is_none()); + + let idx_after = TimelockedIndex::::get(); + assert!(!idx_after.contains(&(net_a, who_a1))); + assert!(!idx_after.contains(&(net_a, who_a2))); + + // NET B: untouched + assert!(CommitmentOf::::get(net_b, who_b).is_some()); + assert!(LastCommitment::::get(net_b, who_b).is_some()); + assert!(LastBondsReset::::get(net_b, who_b).is_some()); + assert!(RevealedCommitments::::get(net_b, who_b).is_some()); + assert!(UsedSpaceOf::::get(net_b, who_b).is_some()); + assert!(idx_after.contains(&(net_b, who_b))); + + // Idempotency + purge_netuid_with_meter(net_a, Weight::from_parts(u64::MAX, u64::MAX)); + assert_eq!(CommitmentOf::::iter_prefix(net_a).count(), 0); + assert!(!TimelockedIndex::::get().contains(&(net_a, who_a1))); + }); +} + +/// `purge_netuid` runs weighted prefix clears **before** the timelock-index update. The macro batch +/// sizing uses the meter's **limit** (not accumulated consumption), so maps may already be empty +/// when the weight budget runs out; `done == false` must still mean the timelock index +/// row for this netuid survives until a later call with enough budget. +#[test] +fn purge_netuid_under_budget_may_skip_timelock_update_while_clearing_maps() { + new_test_ext().execute_with(|| { + System::::set_block_number(1); + let net_a = NetUid::from(77); + let who_a: u64 = 4001; + + let empty_fields: BoundedVec::MaxFields> = BoundedVec::default(); + let info_empty: CommitmentInfo<::MaxFields> = CommitmentInfo { + fields: empty_fields, + }; + let bn = System::::block_number(); + let reg = Registration { + deposit: Default::default(), + block: bn, + info: info_empty, + }; + CommitmentOf::::insert(net_a, who_a, reg); + LastCommitment::::insert(net_a, who_a, bn); + LastBondsReset::::insert(net_a, who_a, bn); + RevealedCommitments::::insert(net_a, who_a, vec![(b"x".to_vec(), 1u64)]); + UsedSpaceOf::::insert( + net_a, + who_a, + UsageTracker { + last_epoch: 1, + used_space: 1, + }, + ); + TimelockedIndex::::mutate(|idx| { + idx.insert((net_a, who_a)); + }); + + let write1 = ::DbWeight::get().writes(1); + // Budget is strictly below one DB write, so the weighted prefix clears inside + // `purge_netuid` reliably run out of budget and report `done == false`. + let budget = write1.saturating_sub(Weight::from_parts(1, 1)); + + let done = purge_netuid_with_meter(net_a, budget); + assert!( + !done, + "purge_netuid must report not-done when under-budget" + ); + assert!( + TimelockedIndex::::get().contains(&(net_a, who_a)), + "timelock index is only trimmed after a successful final pass; stale index entries are expected if that write is skipped" + ); + + // Full budget finishes (including timelock index), even if prior pass already cleared maps. + let done = purge_netuid_with_meter(net_a, Weight::from_parts(u64::MAX, u64::MAX)); + assert!(done); + assert!(CommitmentOf::::get(net_a, who_a).is_none()); + assert!(!TimelockedIndex::::get().contains(&(net_a, who_a))); + }); +} diff --git a/pallets/commitments/src/tests/revealed_commitments.rs b/pallets/commitments/src/tests/revealed_commitments.rs new file mode 100644 index 0000000000..5768d3973d --- /dev/null +++ b/pallets/commitments/src/tests/revealed_commitments.rs @@ -0,0 +1,386 @@ +//! RevealedCommitments retention and on_initialize auto-reveal hook. + +use super::*; + +#[allow(clippy::indexing_slicing)] +#[test] +fn on_initialize_reveals_matured_timelocks() { + new_test_ext().execute_with(|| { + let who = 42; + let netuid = NetUid::from(7); + let reveal_round = 1000; + + let message_text = b"Timelock test via on_initialize"; + + let inner_fields: BoundedVec::MaxFields> = + BoundedVec::try_from(vec![Data::Raw( + message_text + .to_vec() + .try_into() + .expect("<= 128 bytes is OK for Data::Raw"), + )]) + .expect("Should not exceed MaxFields"); + + let inner_info: CommitmentInfo<::MaxFields> = CommitmentInfo { + fields: inner_fields, + }; + + let plaintext = inner_info.encode(); + let encrypted = produce_ciphertext(&plaintext, reveal_round); + + let outer_fields = BoundedVec::try_from(vec![Data::TimelockEncrypted { + encrypted, + reveal_round, + }]) + .expect("One field is well under MaxFields"); + let info_outer = CommitmentInfo { + fields: outer_fields, + }; + + System::::set_block_number(1); + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + Box::new(info_outer) + )); + + assert!(CommitmentOf::::get(netuid, who).is_some()); + assert!( + TimelockedIndex::::get().contains(&(netuid, who)), + "Should appear in TimelockedIndex since it contains a timelock" + ); + + let drand_sig_hex = hex::decode(DRAND_QUICKNET_SIG_HEX) + .expect("Decoding DRAND_QUICKNET_SIG_HEX must not fail"); + insert_drand_pulse(reveal_round, &drand_sig_hex); + + assert!(RevealedCommitments::::get(netuid, who).is_none()); + + System::::set_block_number(2); + let weight = as Hooks>::on_initialize(2); + let expected_weight = ::WeightInfo::reveal_timelocked_commitments() + .saturating_add(RocksDbWeight::get().reads(5)) + .saturating_add(RocksDbWeight::get().writes(3)); + assert_eq!(weight, expected_weight); + + let revealed_opt = RevealedCommitments::::get(netuid, who); + assert!( + revealed_opt.is_some(), + "Expected that the timelock got revealed at block #2" + ); + + let leftover = CommitmentOf::::get(netuid, who); + assert!( + leftover.is_none(), + "After revealing the only timelock, the entire commitment is removed." + ); + + assert!( + !TimelockedIndex::::get().contains(&(netuid, who)), + "No longer in TimelockedIndex after reveal." + ); + + let (revealed_bytes, reveal_block) = + revealed_opt.expect("expected to not panic")[0].clone(); + assert_eq!(reveal_block, 2, "Should have revealed at block #2"); + + let revealed_str = sp_std::str::from_utf8(&revealed_bytes) + .expect("Expected valid UTF-8 in the revealed bytes for this test"); + + let original_str = + sp_std::str::from_utf8(message_text).expect("`message_text` is valid UTF-8"); + assert!( + revealed_str.contains(original_str), + "Revealed data must contain the original message text." + ); + }); +} + +#[allow(clippy::indexing_slicing)] +#[test] +fn reveal_timelocked_bad_timelocks_are_removed() { + new_test_ext().execute_with(|| { + // + // 1) Prepare multiple Data::TimelockEncrypted fields with different “badness” scenarios + one good field + // + // Round used for valid Drand signature + let valid_round = 1000; + // Round used for intentionally invalid Drand signature + let invalid_sig_round = 999; + // Round that has *no* Drand pulse => timelock remains stored, not revealed yet + let no_pulse_round = 2001; + + // (a) TLE #1: Round=999 => Drand pulse *exists* but signature is invalid => skip/deleted + let plaintext_1 = b"BadSignature"; + let ciphertext_1 = produce_ciphertext(plaintext_1, invalid_sig_round); + let tle_bad_sig = Data::TimelockEncrypted { + encrypted: ciphertext_1, + reveal_round: invalid_sig_round, + }; + + // (b) TLE #2: Round=1000 => Drand signature is valid, but ciphertext is corrupted => skip/deleted + let plaintext_2 = b"CorruptedCiphertext"; + let good_ct_2 = produce_ciphertext(plaintext_2, valid_round); + let mut corrupted_ct_2 = good_ct_2.into_inner(); + if !corrupted_ct_2.is_empty() { + corrupted_ct_2[0] ^= 0xFF; // flip a byte + } + let tle_corrupted = Data::TimelockEncrypted { + encrypted: corrupted_ct_2.try_into().expect("Expected not to panic"), + reveal_round: valid_round, + }; + + // (c) TLE #3: Round=1000 => Drand signature valid, ciphertext good, *but* plaintext is empty => skip/deleted + let empty_good_ct = produce_ciphertext(&[], valid_round); + let tle_empty_plaintext = Data::TimelockEncrypted { + encrypted: empty_good_ct, + reveal_round: valid_round, + }; + + // (d) TLE #4: Round=1000 => Drand signature valid, ciphertext valid, nonempty plaintext => should be revealed + let plaintext_4 = b"Hello, I decrypt fine!"; + let good_ct_4 = produce_ciphertext(plaintext_4, valid_round); + let tle_good = Data::TimelockEncrypted { + encrypted: good_ct_4, + reveal_round: valid_round, + }; + + // (e) TLE #5: Round=2001 => no Drand pulse => remains in storage + let plaintext_5 = b"Still waiting for next round!"; + let good_ct_5 = produce_ciphertext(plaintext_5, no_pulse_round); + let tle_no_pulse = Data::TimelockEncrypted { + encrypted: good_ct_5, + reveal_round: no_pulse_round, + }; + + // + // 2) Assemble them all in one CommitmentInfo + // + let fields = vec![ + tle_bad_sig, // #1 + tle_corrupted, // #2 + tle_empty_plaintext, // #3 + tle_good, // #4 + tle_no_pulse, // #5 + ]; + let fields_bounded = BoundedVec::try_from(fields).expect("Should not exceed MaxFields"); + let info = CommitmentInfo { + fields: fields_bounded, + }; + + // + // 3) Insert the commitment + // + let who = 123; + let netuid = NetUid::from(777); + System::::set_block_number(1); + assert_ok!(Pallet::::set_commitment( + RawOrigin::Signed(who).into(), + netuid, + Box::new(info) + )); + + // + // 4) Insert pulses: + // - Round=999 => invalid signature => attempts to parse => fails => remove TLE #1 + // - Round=1000 => valid signature => TLE #2 is corrupted => remove; #3 empty => remove; #4 reveals successfully + // - Round=2001 => no signature => TLE #5 remains + // + let bad_sig = [0x33u8; 10]; // obviously invalid for TinyBLS + insert_drand_pulse(invalid_sig_round, &bad_sig); + + let drand_sig_1000 = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("Expected not to panic"); + insert_drand_pulse(valid_round, &drand_sig_1000); + + // + // 5) Call reveal => “bad” items are removed, “good” is revealed, “not ready” remains + // + System::::set_block_number(2); + assert_ok!(Pallet::::reveal_timelocked_commitments()); + + // + // 6) Check final storage + // + // (a) TLE #5 => still in fields => same user remains in CommitmentOf => TimelockedIndex includes them + let registration_after = + CommitmentOf::::get(netuid, who).expect("Should still exist"); + assert_eq!( + registration_after.info.fields.len(), + 1, + "Only the unrevealed TLE #5 should remain" + ); + let leftover = ®istration_after.info.fields[0]; + match leftover { + Data::TimelockEncrypted { reveal_round, .. } => { + assert_eq!(*reveal_round, no_pulse_round, "Should be TLE #5 leftover"); + } + _ => panic!("Expected the leftover field to be TLE #5"), + }; + assert!( + TimelockedIndex::::get().contains(&(netuid, who)), + "Still in index because there's one remaining timelock (#5)." + ); + + // (b) TLE #4 => revealed => check that the plaintext matches + let revealed = RevealedCommitments::::get(netuid, who) + .expect("Should have at least one revealed item for TLE #4"); + let (revealed_bytes, reveal_block) = &revealed[0]; + assert_eq!(*reveal_block, 2, "Revealed at block #2"); + + let revealed_str = sp_std::str::from_utf8(revealed_bytes) + .expect("Truncated bytes should be valid UTF-8 in this test"); + + let original_str = + sp_std::str::from_utf8(plaintext_4).expect("plaintext_4 should be valid UTF-8"); + + assert_eq!( + revealed_str, original_str, + "Expected revealed data to match the original plaintext" + ); + + // (c) TLE #1 / #2 / #3 => removed => do NOT appear in leftover fields, nor in revealed (they were invalid) + assert_eq!(revealed.len(), 1, "Only TLE #4 ended up in revealed list"); + }); +} + +#[test] +fn revealed_commitments_keeps_only_10_items() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let who = 2; + let reveal_round = 1000; + + let drand_sig_bytes = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("Should decode DRAND sig"); + insert_drand_pulse(reveal_round, &drand_sig_bytes); + + // --- 1) Build 12 TimelockEncrypted fields --- + // Each one has a unique plaintext "TLE #i" + const TOTAL_TLES: usize = 12; + let mut fields = Vec::with_capacity(TOTAL_TLES); + + for i in 0..TOTAL_TLES { + let plaintext = format!("TLE #{i}").into_bytes(); + let ciphertext = produce_ciphertext(&plaintext, reveal_round); + let timelock = Data::TimelockEncrypted { + encrypted: ciphertext, + reveal_round, + }; + fields.push(timelock); + } + let fields_bounded = BoundedVec::try_from(fields).expect("Should not exceed MaxFields"); + let info = CommitmentInfo { + fields: fields_bounded, + }; + + // --- 2) Set the commitment => 12 timelocks in storage --- + System::::set_block_number(1); + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + Box::new(info) + )); + + // --- 3) Reveal => all 12 are decrypted in one shot --- + System::::set_block_number(2); + assert_ok!(Pallet::::reveal_timelocked_commitments()); + + // --- 4) Check we only keep 10 in `RevealedCommitments` --- + let revealed = RevealedCommitments::::get(netuid, who) + .expect("Should have at least some revealed data"); + assert_eq!( + revealed.len(), + 10, + "We must only keep the newest 10, out of 12 total" + ); + + // The oldest 2 ("TLE #0" and "TLE #1") must be dropped. + // The items in `revealed` now correspond to "TLE #2" .. "TLE #11". + for (idx, (revealed_bytes, reveal_block)) in revealed.iter().enumerate() { + // Convert to UTF-8 + let revealed_str = sp_std::str::from_utf8(revealed_bytes) + .expect("Decrypted data should be valid UTF-8 for this test case"); + + // We expect them to be TLE #2..TLE #11 + let expected_index = idx + 2; // since we dropped #0 and #1 + let expected_str = format!("TLE #{expected_index}"); + assert_eq!(revealed_str, expected_str, "Check which TLE is kept"); + + // Also check it was revealed at block 2 + assert_eq!(*reveal_block, 2, "All reveal in the same block #2"); + } + }); +} + +#[test] +fn revealed_commitments_keeps_only_10_newest_with_individual_single_field_commits() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let who = 2; + let reveal_round = 1000; + + let drand_sig_bytes = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("decode DRAND sig"); + insert_drand_pulse(reveal_round, &drand_sig_bytes); + + // We will add 12 separate timelocks, one per iteration, each in its own set_commitment call. + // After each insertion, we call reveal + increment the block by 1. + + for i in 0..12 { + System::::set_block_number(i as u64 + 1); + + let plaintext = format!("TLE #{i}").into_bytes(); + let ciphertext = produce_ciphertext(&plaintext, reveal_round); + + let new_timelock = Data::TimelockEncrypted { + encrypted: ciphertext, + reveal_round, + }; + + let fields = BoundedVec::try_from(vec![new_timelock]) + .expect("Single field is well within MaxFields"); + let info = CommitmentInfo { fields }; + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + Box::new(info) + )); + + assert_ok!(Pallet::::reveal_timelocked_commitments()); + + let revealed = RevealedCommitments::::get(netuid, who).unwrap_or_default(); + let expected_count = (i + 1).min(10); + assert_eq!( + revealed.len(), + expected_count, + "At iteration {i}, we keep at most 10 reveals" + ); + } + + let revealed = + RevealedCommitments::::get(netuid, who).expect("expected to not panic"); + assert_eq!( + revealed.len(), + 10, + "After 12 total commits, only 10 remain revealed" + ); + + // Check that TLE #0 and TLE #1 are dropped; TLE #2..#11 remain in ascending order. + for (idx, (revealed_bytes, reveal_block)) in revealed.iter().enumerate() { + let revealed_str = + sp_std::str::from_utf8(revealed_bytes).expect("Should be valid UTF-8"); + let expected_i = idx + 2; // i=0 => "TLE #2", i=1 => "TLE #3", etc. + let expected_str = format!("TLE #{expected_i}"); + + assert_eq!( + revealed_str, expected_str, + "Revealed data #{idx} should match the truncated TLE #{expected_i}" + ); + + let expected_reveal_block = expected_i as u64 + 1; + assert_eq!( + *reveal_block, expected_reveal_block, + "Check which block TLE #{expected_i} was revealed in" + ); + } + }); +} diff --git a/pallets/commitments/src/tests/set_commitment.rs b/pallets/commitments/src/tests/set_commitment.rs new file mode 100644 index 0000000000..f6c5a26c70 --- /dev/null +++ b/pallets/commitments/src/tests/set_commitment.rs @@ -0,0 +1,308 @@ +//! Tests for commitments pallet: set commitment. + +use super::*; + +#[test] +fn set_commitment_works() { + new_test_ext().execute_with(|| { + System::::set_block_number(1); + let info = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![]).expect("Expected not to panic"), + }); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(1), + 1.into(), + info.clone() + )); + + let commitment = + Pallet::::commitment_of(NetUid::from(1), 1).expect("Expected not to panic"); + let initial_deposit = ::InitialDeposit::get(); + assert_eq!(commitment.deposit, initial_deposit); + assert_eq!(commitment.block, 1); + assert_eq!(Pallet::::last_commitment(NetUid::from(1), 1), Some(1)); + }); +} + +#[test] +#[should_panic(expected = "BoundedVec::try_from failed")] +fn set_commitment_too_many_fields_panics() { + new_test_ext().execute_with(|| { + let max_fields: u32 = ::MaxFields::get(); + let fields = vec![Data::None; (max_fields + 1) as usize]; + + // This line will panic when 'BoundedVec::try_from(...)' sees too many items. + let info = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(fields).expect("BoundedVec::try_from failed"), + }); + + // We never get here, because the constructor panics above. + let _ = Pallet::::set_commitment( + frame_system::RawOrigin::Signed(1).into(), + 1.into(), + info, + ); + }); +} + +#[test] +fn set_commitment_updates_deposit() { + new_test_ext().execute_with(|| { + System::::set_block_number(1); + let info1 = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![Default::default(); 2]) + .expect("Expected not to panic"), + }); + let info2 = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![Default::default(); 3]) + .expect("Expected not to panic"), + }); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(1), + 1.into(), + info1 + )); + let initial_deposit = ::InitialDeposit::get(); + let field_deposit = ::FieldDeposit::get(); + let expected_deposit1 = initial_deposit + field_deposit * 2.into(); + assert_eq!( + Pallet::::commitment_of(NetUid::from(1), 1) + .expect("Expected not to panic") + .deposit, + expected_deposit1 + ); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(1), + 1.into(), + info2 + )); + let expected_deposit2 = initial_deposit + field_deposit * 3.into(); + assert_eq!( + Pallet::::commitment_of(NetUid::from(1), 1) + .expect("Expected not to panic") + .deposit, + expected_deposit2 + ); + }); +} + +#[test] +fn event_emission_works() { + new_test_ext().execute_with(|| { + System::::set_block_number(1); + let info = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![]).expect("Expected not to panic"), + }); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(1), + 1.into(), + info + )); + + let events = System::::events(); + let expected_event = RuntimeEvent::Commitments(Event::Commitment { + netuid: 1.into(), + who: 1, + }); + assert!(events.iter().any(|e| e.event == expected_event)); + }); +} + +#[test] +fn set_commitment_unreserve_leftover_fails() { + new_test_ext().execute_with(|| { + use frame_system::RawOrigin; + + let netuid = NetUid::from(999); + let who = 99; + + Balances::make_free_balance_be(&who, 10_000.into()); + + let fake_deposit: TaoBalance = 100.into(); + let dummy_info = CommitmentInfo:: { + fields: BoundedVec::try_from(vec![]).expect("empty fields is fine"), + }; + let registration = Registration:: { + deposit: fake_deposit, + info: dummy_info, + block: 0u64.into(), + }; + + CommitmentOf::::insert(netuid, who, registration); + + assert_ok!(Balances::reserve(&who, fake_deposit)); + assert_eq!(Balances::reserved_balance(who), 100.into()); + + Balances::unreserve(&who, 10_000.into()); + assert_eq!(Balances::reserved_balance(who), 0.into()); + + let commit_small = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![]).expect("no fields is fine"), + }); + + assert_noop!( + Pallet::::set_commitment(RawOrigin::Signed(who).into(), netuid, commit_small), + Error::::UnexpectedUnreserveLeftover + ); + }); +} + +#[test] +fn usage_respects_minimum_of_100_bytes() { + new_test_ext().execute_with(|| { + MaxSpace::::set(1000); + + let netuid = NetUid::from(1); + let who = 99; + + System::::set_block_number(1); + + let small_data = Data::Raw(vec![0u8; 50].try_into().expect("<=128 bytes for Raw")); + let info_small = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![small_data]).expect("Must not exceed MaxFields"), + }); + + let usage_before = UsedSpaceOf::::get(netuid, who).unwrap_or_default(); + assert_eq!(usage_before.used_space, 0); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + info_small + )); + + let usage_after_small = + UsedSpaceOf::::get(netuid, who).expect("expected to not panic"); + assert_eq!( + usage_after_small.used_space, 100, + "Usage must jump to 100 even though we only used 50 bytes" + ); + + let big_data = Data::Raw(vec![0u8; 110].try_into().expect("<=128 bytes for Raw")); + let info_big = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![big_data]).expect("Must not exceed MaxFields"), + }); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + info_big + )); + + let usage_after_big = UsedSpaceOf::::get(netuid, who).expect("expected to not panic"); + assert_eq!( + usage_after_big.used_space, 210, + "Usage should be 100 + 110 = 210 in this epoch" + ); + + UsedSpaceOf::::remove(netuid, who); + let usage_after_wipe = UsedSpaceOf::::get(netuid, who); + assert!( + usage_after_wipe.is_none(), + "Expected `UsedSpaceOf` entry to be removed" + ); + + let bigger_data = Data::Raw(vec![0u8; 120].try_into().expect("<=128 bytes for Raw")); + let info_bigger = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![bigger_data]).expect("Must not exceed MaxFields"), + }); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + info_bigger + )); + + let usage_after_reset = + UsedSpaceOf::::get(netuid, who).expect("expected to not panic"); + assert_eq!( + usage_after_reset.used_space, 120, + "After wiping old usage, the new usage should be exactly 120" + ); + }); +} + +#[test] +fn set_commitment_works_with_multiple_raw_fields() { + new_test_ext().execute_with(|| { + let cur_block = 10u64.into(); + System::::set_block_number(cur_block); + let initial_deposit: BalanceOf = ::InitialDeposit::get(); + let field_deposit: BalanceOf = ::FieldDeposit::get(); + + let field1 = Data::Raw(vec![0u8; 10].try_into().expect("<=128 bytes is OK")); + let field2 = Data::Raw(vec![1u8; 20].try_into().expect("<=128 bytes is OK")); + let field3 = Data::Raw(vec![2u8; 50].try_into().expect("<=128 bytes is OK")); + + let info_multiple = CommitmentInfo { + fields: BoundedVec::try_from(vec![field1.clone(), field2.clone(), field3.clone()]) + .expect("<= MaxFields"), + }; + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(12345), + 99.into(), + Box::new(info_multiple) + )); + + let expected_deposit: BalanceOf = initial_deposit + field_deposit * 3u64.into(); + let stored = CommitmentOf::::get(NetUid::from(99), 12345).expect("Should be stored"); + assert_eq!( + stored.deposit, expected_deposit, + "Deposit must equal initial + 3 * field_deposit" + ); + + assert_eq!(stored.block, cur_block, "Stored block must match cur_block"); + + let usage = + UsedSpaceOf::::get(NetUid::from(99), 12345).expect("Expected to not panic"); + assert_eq!( + usage.used_space, 100, + "Usage is clamped to 100 when sum of fields is < 100" + ); + + let next_block = 11u64.into(); + System::::set_block_number(next_block); + + let info_two_fields = CommitmentInfo { + fields: BoundedVec::try_from(vec![field1.clone(), field2.clone()]) + .expect("<= MaxFields"), + }; + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(12345), + 99.into(), + Box::new(info_two_fields) + )); + + let expected_deposit2: BalanceOf = initial_deposit + field_deposit * 2u64.into(); + let stored2 = CommitmentOf::::get(NetUid::from(99), 12345).expect("Should be stored"); + assert_eq!( + stored2.deposit, expected_deposit2, + "Deposit must have decreased after removing one field" + ); + + let usage2 = + UsedSpaceOf::::get(NetUid::from(99), 12345).expect("Expected to not panic"); + let expected_usage2 = 200u64; + assert_eq!( + usage2.used_space, expected_usage2, + "Usage accumulates in the same epoch, respecting the min usage of 100 each time" + ); + + let events = System::::events(); + let expected_event = RuntimeEvent::Commitments(Event::Commitment { + netuid: 99.into(), + who: 12345, + }); + let found_commitment_event = events.iter().any(|e| e.event == expected_event); + assert!( + found_commitment_event, + "Expected at least one Event::Commitment to be emitted" + ); + }); +} diff --git a/pallets/commitments/src/tests/space_limit.rs b/pallets/commitments/src/tests/space_limit.rs new file mode 100644 index 0000000000..827edcfbf4 --- /dev/null +++ b/pallets/commitments/src/tests/space_limit.rs @@ -0,0 +1,229 @@ +//! Tests for commitments pallet: space limit. + +use super::*; + +#[test] +fn tempo_based_space_limit_accumulates_in_same_window() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let who = 100; + let space_limit = 150; + MaxSpace::::set(space_limit); + System::::set_block_number(0); + + // A single commitment that uses some space, e.g. 30 bytes: + let data = vec![0u8; 30]; + let info = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![Data::Raw( + data.try_into().expect("Data up to 128 bytes OK"), + )]) + .expect("1 field is <= MaxFields"), + }); + + // 2) First call => usage=0 => usage=30 after. OK. + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + info.clone(), + )); + + // 3) Second call => tries another 30 bytes in the SAME block => total=60 => exceeds 50 => should fail. + assert_noop!( + Pallet::::set_commitment(RuntimeOrigin::signed(who), netuid, info.clone()), + Error::::SpaceLimitExceeded + ); + }); +} + +#[test] +fn tempo_based_space_limit_resets_after_tempo() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(2); + let who = 101; + + MaxSpace::::set(250); + System::::set_block_number(1); + + let commit_small = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![Data::Raw( + vec![0u8; 20].try_into().expect("expected ok"), + )]) + .expect("expected ok"), + }); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + commit_small.clone() + )); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + commit_small.clone() + )); + + assert_noop!( + Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + commit_small.clone() + ), + Error::::SpaceLimitExceeded + ); + + System::::set_block_number(200); + + assert_noop!( + Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + commit_small.clone() + ), + Error::::SpaceLimitExceeded + ); + + System::::set_block_number(360); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + commit_small + )); + }); +} + +#[test] +fn tempo_based_space_limit_does_not_affect_different_netuid() { + new_test_ext().execute_with(|| { + let netuid_a = NetUid::from(10); + let netuid_b = NetUid::from(20); + let who = 111; + let space_limit = 199; + MaxSpace::::set(space_limit); + + let commit_large = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![Data::Raw( + vec![0u8; 40].try_into().expect("expected ok"), + )]) + .expect("expected ok"), + }); + let commit_small = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![Data::Raw( + vec![0u8; 20].try_into().expect("expected ok"), + )]) + .expect("expected ok"), + }); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid_a, + commit_large.clone() + )); + + assert_noop!( + Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid_a, + commit_small.clone() + ), + Error::::SpaceLimitExceeded + ); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid_b, + commit_large + )); + + assert_noop!( + Pallet::::set_commitment(RuntimeOrigin::signed(who), netuid_b, commit_small), + Error::::SpaceLimitExceeded + ); + }); +} + +#[test] +fn tempo_based_space_limit_does_not_affect_different_user() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(10); + let user1 = 123; + let user2 = 456; + let space_limit = 199; + MaxSpace::::set(space_limit); + + let commit_large = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![Data::Raw( + vec![0u8; 40].try_into().expect("expected ok"), + )]) + .expect("expected ok"), + }); + let commit_small = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![Data::Raw( + vec![0u8; 20].try_into().expect("expected ok"), + )]) + .expect("expected ok"), + }); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(user1), + netuid, + commit_large.clone() + )); + + assert_noop!( + Pallet::::set_commitment( + RuntimeOrigin::signed(user1), + netuid, + commit_small.clone() + ), + Error::::SpaceLimitExceeded + ); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(user2), + netuid, + commit_large + )); + + assert_noop!( + Pallet::::set_commitment(RuntimeOrigin::signed(user2), netuid, commit_small), + Error::::SpaceLimitExceeded + ); + }); +} + +#[test] +fn tempo_based_space_limit_sudo_set_max_space() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(3); + let who = 15; + MaxSpace::::set(100); + + System::::set_block_number(1); + let commit_25 = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![Data::Raw( + vec![0u8; 25].try_into().expect("expected ok"), + )]) + .expect("expected ok"), + }); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + commit_25.clone() + )); + assert_noop!( + Pallet::::set_commitment(RuntimeOrigin::signed(who), netuid, commit_25.clone()), + Error::::SpaceLimitExceeded + ); + + assert_ok!(Pallet::::set_max_space(RuntimeOrigin::root(), 300)); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + commit_25 + )); + }); +} diff --git a/pallets/commitments/src/tests/timelock_mixed_fields.rs b/pallets/commitments/src/tests/timelock_mixed_fields.rs new file mode 100644 index 0000000000..b647efdddc --- /dev/null +++ b/pallets/commitments/src/tests/timelock_mixed_fields.rs @@ -0,0 +1,358 @@ +//! Commitments that mix TimelockEncrypted fields with Raw / other Data variants. + +use super::*; + +#[allow(clippy::indexing_slicing)] +#[test] +fn multiple_timelocked_commitments_reveal_works() { + new_test_ext().execute_with(|| { + // ------------------------------------------- + // 1) Set up initial block number and user + // ------------------------------------------- + let cur_block = 5u64.into(); + System::::set_block_number(cur_block); + + let who = 123; + let netuid = NetUid::from(999); + + // ------------------------------------------- + // 2) Create multiple TLE fields referencing + // two known valid Drand rounds: 1000, 2000 + // ------------------------------------------- + + let round_1000 = 1000; + let round_2000 = 2000; + + // 2.a) TLE #1 => round=1000 + let tle_1_plaintext = b"Timelock #1 => round=1000"; + let ciphertext_1 = produce_ciphertext(tle_1_plaintext, round_1000); + let tle_1 = Data::TimelockEncrypted { + encrypted: ciphertext_1, + reveal_round: round_1000, + }; + + // 2.b) TLE #2 => round=1000 + let tle_2_plaintext = b"Timelock #2 => round=1000"; + let ciphertext_2 = produce_ciphertext(tle_2_plaintext, round_1000); + let tle_2 = Data::TimelockEncrypted { + encrypted: ciphertext_2, + reveal_round: round_1000, + }; + + // 2.c) TLE #3 => round=2000 + let tle_3_plaintext = b"Timelock #3 => round=2000"; + let ciphertext_3 = produce_ciphertext(tle_3_plaintext, round_2000); + let tle_3 = Data::TimelockEncrypted { + encrypted: ciphertext_3, + reveal_round: round_2000, + }; + + // 2.d) TLE #4 => round=2000 + let tle_4_plaintext = b"Timelock #4 => round=2000"; + let ciphertext_4 = produce_ciphertext(tle_4_plaintext, round_2000); + let tle_4 = Data::TimelockEncrypted { + encrypted: ciphertext_4, + reveal_round: round_2000, + }; + + // ------------------------------------------- + // 3) Insert all TLEs in a single CommitmentInfo + // ------------------------------------------- + let fields = vec![tle_1, tle_2, tle_3, tle_4]; + let fields_bounded = BoundedVec::try_from(fields).expect("Must not exceed MaxFields"); + let info = CommitmentInfo { + fields: fields_bounded, + }; + + // ------------------------------------------- + // 4) set_commitment => user is now in TimelockedIndex + // ------------------------------------------- + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + Box::new(info) + )); + assert!( + TimelockedIndex::::get().contains(&(netuid, who)), + "User must appear in TimelockedIndex since they have TLE fields" + ); + + // Confirm the stored fields are as expected + let stored = CommitmentOf::::get(netuid, who).expect("Should be stored"); + assert_eq!( + stored.info.fields.len(), + 4, + "All 4 timelock fields must be stored" + ); + + // ------------------------------------------- + // 5) Insert valid Drand pulse => round=1000 + // ------------------------------------------- + let drand_sig_1000 = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("decode signature"); + insert_drand_pulse(round_1000, &drand_sig_1000); + + // Reveal at block=6 => should remove TLE #1 and TLE #2, leaving TLE #3, #4 + System::::set_block_number(6u64.into()); + assert_ok!(Pallet::::reveal_timelocked_commitments()); + + // Check leftover => TLE #3, TLE #4 remain + let leftover_after_1000 = CommitmentOf::::get(netuid, who).expect("Must exist"); + assert_eq!( + leftover_after_1000.info.fields.len(), + 2, + "After revealing round=1000, 2 timelocks remain (#3, #4)" + ); + + // Check partial reveals => TLE #1 & #2 in revealed storage + let revealed_1000 = RevealedCommitments::::get(netuid, who) + .expect("Should have partial reveals"); + assert_eq!( + revealed_1000.len(), + 2, + "We revealed exactly 2 items at round=1000" + ); + { + let (bytes_a, _) = &revealed_1000[0]; + let (bytes_b, _) = &revealed_1000[1]; + let txt_a = sp_std::str::from_utf8(bytes_a).expect("utf-8 expected"); + let txt_b = sp_std::str::from_utf8(bytes_b).expect("utf-8 expected"); + assert!( + txt_a.contains("Timelock #1") || txt_a.contains("Timelock #2"), + "Revealed #1 or #2" + ); + assert!( + txt_b.contains("Timelock #1") || txt_b.contains("Timelock #2"), + "Revealed #1 or #2" + ); + } + + assert!( + TimelockedIndex::::get().contains(&(netuid, who)), + "TLE left" + ); + + // ------------------------------------------- + // 6) Insert valid Drand pulse => round=2000 + // ------------------------------------------- + let drand_sig_2000_hex = + "b6cb8f482a0b15d45936a4c4ea08e98a087e71787caee3f4d07a8a9843b1bc5423c6b3c22f446488b3137eaca799c77e"; + let drand_sig_2000 = hex::decode(drand_sig_2000_hex).expect("decode signature"); + insert_drand_pulse(round_2000, &drand_sig_2000); + + // Reveal at block=7 => should remove TLE #3 and TLE #4 + System::::set_block_number(7u64.into()); + assert_ok!(Pallet::::reveal_timelocked_commitments()); + + // After revealing these last two timelocks => leftover is none + let leftover_after_2000 = CommitmentOf::::get(netuid, who); + assert!( + leftover_after_2000.is_none(), + "All timelocks revealed => leftover none => entry removed" + ); + + // Because the user has no timelocks left => removed from TimelockedIndex + assert!( + !TimelockedIndex::::get().contains(&(netuid, who)), + "No TLE left => user removed from index" + ); + + // Check TLE #3 and #4 were appended to revealed + let revealed_final = RevealedCommitments::::get(netuid, who) + .expect("Should exist with final reveals"); + assert_eq!( + revealed_final.len(), + 4, + "We should have all 4 TLE items revealed in total" + ); + + // The final two items in `revealed_final` must be #3, #4 + let (third_bytes, _) = &revealed_final[2]; + let (fourth_bytes, _) = &revealed_final[3]; + let third_txt = sp_std::str::from_utf8(third_bytes).expect("utf-8 expected"); + let fourth_txt = sp_std::str::from_utf8(fourth_bytes).expect("utf-8 expected"); + + assert!( + third_txt.contains("Timelock #3"), + "Expected TLE #3 among final reveals" + ); + assert!( + fourth_txt.contains("Timelock #4"), + "Expected TLE #4 among final reveals" + ); + }); +} + +#[allow(clippy::indexing_slicing)] +#[test] +fn mixed_timelocked_and_raw_fields_works() { + new_test_ext().execute_with(|| { + // ------------------------------------------- + // 1) Setup initial block number and user + // ------------------------------------------- + let cur_block = 3u64.into(); + System::::set_block_number(cur_block); + + let who = 77; + let netuid = NetUid::from(501); + + // ------------------------------------------- + // 2) Create raw fields and timelocked fields + // ------------------------------------------- + // We'll use 2 raw fields, and 2 timelocked fields referencing + // 2 Drand rounds (1000 and 2000) that we know have valid signatures. + + // Round constants: + let round_1000 = 1000; + let round_2000 = 2000; + + // (a) Timelock #1 => round=1000 + let tle_1_plaintext = b"TLE #1 => round=1000"; + let ciphertext_1 = produce_ciphertext(tle_1_plaintext, round_1000); + let tle_1 = Data::TimelockEncrypted { + encrypted: ciphertext_1, + reveal_round: round_1000, + }; + + // (b) Timelock #2 => round=2000 + let tle_2_plaintext = b"TLE #2 => round=2000"; + let ciphertext_2 = produce_ciphertext(tle_2_plaintext, round_2000); + let tle_2 = Data::TimelockEncrypted { + encrypted: ciphertext_2, + reveal_round: round_2000, + }; + + // (c) Two Raw fields + let raw_1 = Data::Raw(b"Raw field #1".to_vec().try_into().expect("<= 128 bytes")); + let raw_2 = Data::Raw(b"Raw field #2".to_vec().try_into().expect("<= 128 bytes")); + + // We'll put them in a single vector: [TLE #1, raw_1, TLE #2, raw_2] + let all_fields = vec![tle_1, raw_1.clone(), tle_2, raw_2.clone()]; + let fields_bounded = BoundedVec::try_from(all_fields).expect("<= MaxFields"); + + // ------------------------------------------- + // 3) Submit the single commitment + // ------------------------------------------- + let info = CommitmentInfo { fields: fields_bounded }; + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + Box::new(info) + )); + + // The user should appear in TimelockedIndex because they have timelocked fields. + assert!( + TimelockedIndex::::get().contains(&(netuid, who)), + "User must be in TimelockedIndex with TLE fields" + ); + + // Check the stored data + let stored = CommitmentOf::::get(netuid, who).expect("Should exist in storage"); + assert_eq!( + stored.info.fields.len(), + 4, + "We have 2 raw + 2 TLE fields in total" + ); + + // ------------------------------------------- + // 4) Insert Drand signature for round=1000 => partial reveal + // ------------------------------------------- + let drand_sig_1000 = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("decode signature"); + insert_drand_pulse(round_1000, &drand_sig_1000); + + System::::set_block_number(4u64.into()); + assert_ok!(Pallet::::reveal_timelocked_commitments()); + + // => TLE #1 (round=1000) is revealed. TLE #2 (round=2000) remains locked. + // => The two raw fields remain untouched. + let leftover_after_1000 = CommitmentOf::::get(netuid, who).expect("Must still exist"); + assert_eq!( + leftover_after_1000.info.fields.len(), + 3, + "One TLE removed => leftover=3 fields: TLE #2 + raw_1 + raw_2" + ); + + // Make sure user is still in TimelockedIndex (they still have TLE #2) + assert!( + TimelockedIndex::::get().contains(&(netuid, who)), + "Still has leftover TLE #2 => remains in index" + ); + + // Check partial reveal + let revealed_1000 = RevealedCommitments::::get(netuid, who) + .expect("Should have partial reveals"); + assert_eq!( + revealed_1000.len(), + 1, + "We revealed exactly 1 item at round=1000" + ); + let (revealed_bytes_1, _block_1) = &revealed_1000[0]; + let revealed_str_1 = + sp_std::str::from_utf8(revealed_bytes_1).expect("Should parse as UTF-8"); + assert!( + revealed_str_1.contains("TLE #1 => round=1000"), + "Check that TLE #1 was revealed" + ); + + // ------------------------------------------- + // 5) Insert Drand signature for round=2000 => final TLE reveal + // ------------------------------------------- + let drand_sig_2000_hex = + "b6cb8f482a0b15d45936a4c4ea08e98a087e71787caee3f4d07a8a9843b1bc5423c6b3c22f446488b3137eaca799c77e"; + let drand_sig_2000 = hex::decode(drand_sig_2000_hex).expect("decode signature"); + insert_drand_pulse(round_2000, &drand_sig_2000); + + System::::set_block_number(5u64.into()); + assert_ok!(Pallet::::reveal_timelocked_commitments()); + + // => TLE #2 is now revealed. The two raw fields remain. + let leftover_after_2000 = CommitmentOf::::get(netuid, who).expect("Still exists"); + let leftover_fields = &leftover_after_2000.info.fields; + assert_eq!( + leftover_fields.len(), + 2, + "Only the 2 raw fields remain after TLE #2 is revealed" + ); + + assert_eq!( + leftover_fields[0], + raw_1, + "Leftover field[0] must match raw_1" + ); + assert_eq!( + leftover_fields[1], + raw_2, + "Leftover field[1] must match raw_2" + ); + + // The user has no leftover timelocks => removed from TimelockedIndex + assert!( + !TimelockedIndex::::get().contains(&(netuid, who)), + "No more TLE => user removed from index" + ); + + // But the record is still present in storage (because raw fields remain) + // => leftover_fields must match our original raw fields. + let [f1, f2] = &leftover_fields[..] else { + panic!("Expected exactly 2 fields leftover"); + }; + assert_eq!(f1, &raw_1, "Raw field #1 remains unaltered"); + assert_eq!(f2, &raw_2, "Raw field #2 remains unaltered"); + + // Check that TLE #2 was appended to revealed data + let revealed_final = RevealedCommitments::::get(netuid, who) + .expect("Should have final reveals"); + assert_eq!( + revealed_final.len(), + 2, + "Now we have 2 revealed TLE items total (TLE #1 and TLE #2)." + ); + let (revealed_bytes_2, _block_2) = &revealed_final[1]; + let revealed_str_2 = + sp_std::str::from_utf8(revealed_bytes_2).expect("Should parse as UTF-8"); + assert!( + revealed_str_2.contains("TLE #2 => round=2000"), + "Check that TLE #2 was revealed" + ); + }); +} diff --git a/pallets/commitments/src/tests/timelock_reveal.rs b/pallets/commitments/src/tests/timelock_reveal.rs new file mode 100644 index 0000000000..cf2e6a23c5 --- /dev/null +++ b/pallets/commitments/src/tests/timelock_reveal.rs @@ -0,0 +1,556 @@ +//! Timelock reveal path: decrypt when drand pulse is available, leave immature entries untouched. + +use super::*; + +#[allow(clippy::indexing_slicing)] +#[test] +fn happy_path_timelock_commitments() { + new_test_ext().execute_with(|| { + let message_text = b"Hello timelock only!"; + let data_raw = Data::Raw( + message_text + .to_vec() + .try_into() + .expect("<= 128 bytes for Raw variant"), + ); + let fields_vec = vec![data_raw]; + let fields_bounded: BoundedVec::MaxFields> = + BoundedVec::try_from(fields_vec).expect("Too many fields"); + + let inner_info: CommitmentInfo<::MaxFields> = CommitmentInfo { + fields: fields_bounded, + }; + + let plaintext = inner_info.encode(); + + let reveal_round = 1000; + let encrypted = produce_ciphertext(&plaintext, reveal_round); + + let data = Data::TimelockEncrypted { + encrypted: encrypted.clone(), + reveal_round, + }; + + let fields_outer: BoundedVec::MaxFields> = + BoundedVec::try_from(vec![data]).expect("Too many fields"); + let info_outer = CommitmentInfo { + fields: fields_outer, + }; + + let who = 123; + let netuid = NetUid::from(42); + System::::set_block_number(1); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + Box::new(info_outer) + )); + + let drand_signature_bytes = + hex::decode(DRAND_QUICKNET_SIG_HEX).expect("Expected not to panic"); + insert_drand_pulse(reveal_round, &drand_signature_bytes); + + System::::set_block_number(9999); + assert_ok!(Pallet::::reveal_timelocked_commitments()); + + let revealed = + RevealedCommitments::::get(netuid, who).expect("Should have revealed data"); + + let (revealed_bytes, _reveal_block) = revealed[0].clone(); + + let revealed_str = sp_std::str::from_utf8(&revealed_bytes) + .expect("Expected valid UTF-8 in the revealed bytes for this test"); + + let original_str = + sp_std::str::from_utf8(message_text).expect("`message_text` is valid UTF-8"); + assert!( + revealed_str.contains(original_str), + "Revealed data must contain the original message text." + ); + }); +} + +#[test] +fn reveal_timelocked_commitment_missing_round_does_nothing() { + new_test_ext().execute_with(|| { + let who = 1; + let netuid = NetUid::from(2); + System::::set_block_number(5); + let ciphertext = produce_ciphertext(b"My plaintext", 1000); + let data = Data::TimelockEncrypted { + encrypted: ciphertext, + reveal_round: 1000, + }; + let fields: BoundedVec<_, ::MaxFields> = + BoundedVec::try_from(vec![data]).expect("Expected not to panic"); + let info = CommitmentInfo { fields }; + let origin = RuntimeOrigin::signed(who); + assert_ok!(Pallet::::set_commitment( + origin, + netuid, + Box::new(info) + )); + System::::set_block_number(100_000); + assert_ok!(Pallet::::reveal_timelocked_commitments()); + assert!(RevealedCommitments::::get(netuid, who).is_none()); + }); +} + +#[allow(clippy::indexing_slicing)] +#[test] +fn reveal_timelocked_commitment_cant_deserialize_ciphertext() { + new_test_ext().execute_with(|| { + let who = 42; + let netuid = NetUid::from(9); + System::::set_block_number(10); + let good_ct = produce_ciphertext(b"Some data", 1000); + let mut corrupted = good_ct.into_inner(); + if !corrupted.is_empty() { + corrupted[0] = 0xFF; + } + let corrupted_ct = BoundedVec::try_from(corrupted).expect("Expected not to panic"); + let data = Data::TimelockEncrypted { + encrypted: corrupted_ct, + reveal_round: 1000, + }; + let fields = BoundedVec::try_from(vec![data]).expect("Expected not to panic"); + let info = CommitmentInfo { fields }; + let origin = RuntimeOrigin::signed(who); + assert_ok!(Pallet::::set_commitment( + origin, + netuid, + Box::new(info) + )); + let sig_bytes = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("Expected not to panic"); + insert_drand_pulse(1000, &sig_bytes); + System::::set_block_number(99999); + assert_ok!(Pallet::::reveal_timelocked_commitments()); + assert!(RevealedCommitments::::get(netuid, who).is_none()); + }); +} + +#[test] +fn reveal_timelocked_commitment_bad_signature_skips_decryption() { + new_test_ext().execute_with(|| { + let who = 10; + let netuid = NetUid::from(11); + System::::set_block_number(15); + let real_ct = produce_ciphertext(b"A valid plaintext", 1000); + let data = Data::TimelockEncrypted { + encrypted: real_ct, + reveal_round: 1000, + }; + let fields: BoundedVec<_, ::MaxFields> = + BoundedVec::try_from(vec![data]).expect("Expected not to panic"); + let info = CommitmentInfo { fields }; + let origin = RuntimeOrigin::signed(who); + assert_ok!(Pallet::::set_commitment( + origin, + netuid, + Box::new(info) + )); + let bad_signature = [0x33u8; 10]; + insert_drand_pulse(1000, &bad_signature); + System::::set_block_number(10_000); + assert_ok!(Pallet::::reveal_timelocked_commitments()); + assert!(RevealedCommitments::::get(netuid, who).is_none()); + }); +} + +#[test] +fn reveal_timelocked_commitment_empty_decrypted_data_is_skipped() { + new_test_ext().execute_with(|| { + let who = 2; + let netuid = NetUid::from(3); + let commit_block = 100u64; + System::::set_block_number(commit_block); + let reveal_round = 1000; + let empty_ct = produce_ciphertext(&[], reveal_round); + let data = Data::TimelockEncrypted { + encrypted: empty_ct, + reveal_round, + }; + let fields = BoundedVec::try_from(vec![data]).expect("Expected not to panic"); + let info = CommitmentInfo { fields }; + let origin = RuntimeOrigin::signed(who); + assert_ok!(Pallet::::set_commitment( + origin, + netuid, + Box::new(info) + )); + let sig_bytes = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("Expected not to panic"); + insert_drand_pulse(reveal_round, &sig_bytes); + System::::set_block_number(10_000); + assert_ok!(Pallet::::reveal_timelocked_commitments()); + assert!(RevealedCommitments::::get(netuid, who).is_none()); + }); +} + +#[allow(clippy::indexing_slicing)] +#[test] +fn reveal_timelocked_commitment_single_field_entry_is_removed_after_reveal() { + new_test_ext().execute_with(|| { + let message_text = b"Single field timelock test!"; + let data_raw = Data::Raw( + message_text + .to_vec() + .try_into() + .expect("Message must be <=128 bytes for Raw variant"), + ); + + let fields_bounded: BoundedVec::MaxFields> = + BoundedVec::try_from(vec![data_raw]).expect("BoundedVec creation must not fail"); + + let inner_info: CommitmentInfo<::MaxFields> = CommitmentInfo { + fields: fields_bounded, + }; + + let plaintext = inner_info.encode(); + let reveal_round = 1000; + let encrypted = produce_ciphertext(&plaintext, reveal_round); + + let timelock_data = Data::TimelockEncrypted { + encrypted, + reveal_round, + }; + let fields_outer: BoundedVec::MaxFields> = + BoundedVec::try_from(vec![timelock_data]).expect("Too many fields"); + let info_outer: CommitmentInfo<::MaxFields> = CommitmentInfo { + fields: fields_outer, + }; + + let who = 555; + let netuid = NetUid::from(777); + System::::set_block_number(1); + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + Box::new(info_outer) + )); + + let drand_signature_bytes = hex::decode(DRAND_QUICKNET_SIG_HEX) + .expect("Must decode DRAND_QUICKNET_SIG_HEX successfully"); + insert_drand_pulse(reveal_round, &drand_signature_bytes); + + System::::set_block_number(9999); + assert_ok!(Pallet::::reveal_timelocked_commitments()); + + let revealed = + RevealedCommitments::::get(netuid, who).expect("Expected to find revealed data"); + let (revealed_bytes, _reveal_block) = revealed[0].clone(); + + // The decrypted bytes have some extra SCALE metadata in front: + // we slice off the first two bytes before checking the string. + let offset = 2; + let truncated = &revealed_bytes[offset..]; + let revealed_str = sp_std::str::from_utf8(truncated) + .expect("Truncated bytes should be valid UTF-8 in this test"); + + let original_str = + sp_std::str::from_utf8(message_text).expect("`message_text` should be valid UTF-8"); + assert_eq!( + revealed_str, original_str, + "Expected the revealed data (minus prefix) to match the original message" + ); + assert!( + crate::CommitmentOf::::get(netuid, who).is_none(), + "Expected CommitmentOf entry to be removed after reveal" + ); + }); +} + +#[allow(clippy::indexing_slicing)] +#[test] +fn reveal_timelocked_multiple_fields_only_correct_ones_removed() { + new_test_ext().execute_with(|| { + let round_1000 = 1000; + + // 2) Build two CommitmentInfos, one for each timelock + let msg_1 = b"Hello from TLE #1"; + let inner_1_fields: BoundedVec::MaxFields> = + BoundedVec::try_from(vec![Data::Raw( + msg_1.to_vec().try_into().expect("expected not to panic"), + )]) + .expect("BoundedVec of size 1"); + let inner_info_1 = CommitmentInfo { + fields: inner_1_fields, + }; + let encoded_1 = inner_info_1.encode(); + let ciphertext_1 = produce_ciphertext(&encoded_1, round_1000); + let timelock_1 = Data::TimelockEncrypted { + encrypted: ciphertext_1, + reveal_round: round_1000, + }; + + let msg_2 = b"Hello from TLE #2"; + let inner_2_fields: BoundedVec::MaxFields> = + BoundedVec::try_from(vec![Data::Raw( + msg_2.to_vec().try_into().expect("expected not to panic"), + )]) + .expect("BoundedVec of size 1"); + let inner_info_2 = CommitmentInfo { + fields: inner_2_fields, + }; + let encoded_2 = inner_info_2.encode(); + let ciphertext_2 = produce_ciphertext(&encoded_2, round_1000); + let timelock_2 = Data::TimelockEncrypted { + encrypted: ciphertext_2, + reveal_round: round_1000, + }; + + // 3) One plain Data::Raw field (non-timelocked) + let raw_bytes = b"Plain non-timelocked data"; + let data_raw = Data::Raw( + raw_bytes + .to_vec() + .try_into() + .expect("expected not to panic"), + ); + + // 4) Outer commitment: 3 fields total => [Raw, TLE #1, TLE #2] + let outer_fields = BoundedVec::try_from(vec![ + data_raw.clone(), + timelock_1.clone(), + timelock_2.clone(), + ]) + .expect("T::MaxFields >= 3 in the test config, or at least 3 here"); + let outer_info = CommitmentInfo { + fields: outer_fields, + }; + + // 5) Insert the commitment + let who = 123; + let netuid = NetUid::from(999); + System::::set_block_number(1); + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + Box::new(outer_info) + )); + let initial = Pallet::::commitment_of(netuid, who).expect("Must exist"); + assert_eq!(initial.info.fields.len(), 3, "3 fields inserted"); + + // 6) Insert Drand signature for round=1000 + let drand_sig_1000 = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("decode DRAND sig"); + insert_drand_pulse(round_1000, &drand_sig_1000); + + // 7) Reveal once + System::::set_block_number(50); + assert_ok!(Pallet::::reveal_timelocked_commitments()); + + // => The pallet code has removed *both* TLE #1 and TLE #2 in this single call! + let after_reveal = Pallet::::commitment_of(netuid, who) + .expect("Should still exist with leftover fields"); + // Only the raw, non-timelocked field remains + assert_eq!( + after_reveal.info.fields.len(), + 1, + "Both timelocks referencing round=1000 got removed at once" + ); + assert_eq!( + after_reveal.info.fields[0], data_raw, + "Only the raw field is left" + ); + + // 8) Check revealed data + let revealed_data = RevealedCommitments::::get(netuid, who) + .expect("Expected revealed data for TLE #1 and #2"); + + let (revealed_bytes1, reveal_block1) = revealed_data[0].clone(); + let (revealed_bytes2, reveal_block2) = revealed_data[1].clone(); + + let truncated1 = &revealed_bytes1[2..]; + let truncated2 = &revealed_bytes2[2..]; + + assert_eq!(truncated1, msg_1); + assert_eq!(reveal_block1, 50); + assert_eq!(truncated2, msg_2); + assert_eq!(reveal_block2, 50); + + // 9) A second reveal call now does nothing, because no timelocks remain + System::::set_block_number(51); + assert_ok!(Pallet::::reveal_timelocked_commitments()); + + let after_second = Pallet::::commitment_of(netuid, who).expect("Still must exist"); + assert_eq!( + after_second.info.fields.len(), + 1, + "No new fields were removed, because no timelocks remain" + ); + }); +} + +#[test] +fn two_timelocks_partial_then_full_reveal() { + new_test_ext().execute_with(|| { + let netuid_a = NetUid::from(1); + let who_a = 10; + let round_1000 = 1000; + let round_2000 = 2000; + + let drand_sig_1000 = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("Expected success"); + insert_drand_pulse(round_1000, &drand_sig_1000); + + let drand_sig_2000_hex = + "b6cb8f482a0b15d45936a4c4ea08e98a087e71787caee3f4d07a8a9843b1bc5423c6b3c22f446488b3137eaca799c77e"; + + // + // First Timelock => round=1000 + // + let msg_a1 = b"UserA timelock #1 (round=1000)"; + let inner_1_fields: BoundedVec::MaxFields> = BoundedVec::try_from( + vec![Data::Raw(msg_a1.to_vec().try_into().expect("Expected success"))], + ) + .expect("MaxFields >= 1"); + let inner_info_1: CommitmentInfo<::MaxFields> = CommitmentInfo { + fields: inner_1_fields, + }; + let encoded_1 = inner_info_1.encode(); + let ciphertext_1 = produce_ciphertext(&encoded_1, round_1000); + let tle_a1 = Data::TimelockEncrypted { + encrypted: ciphertext_1, + reveal_round: round_1000, + }; + + // + // Second Timelock => round=2000 + // + let msg_a2 = b"UserA timelock #2 (round=2000)"; + let inner_2_fields: BoundedVec::MaxFields> = BoundedVec::try_from( + vec![Data::Raw(msg_a2.to_vec().try_into().expect("Expected success"))], + ) + .expect("MaxFields >= 1"); + let inner_info_2: CommitmentInfo<::MaxFields> = CommitmentInfo { + fields: inner_2_fields, + }; + let encoded_2 = inner_info_2.encode(); + let ciphertext_2 = produce_ciphertext(&encoded_2, round_2000); + let tle_a2 = Data::TimelockEncrypted { + encrypted: ciphertext_2, + reveal_round: round_2000, + }; + + // + // Insert outer commitment with both timelocks + // + let fields_a: BoundedVec::MaxFields> = + BoundedVec::try_from(vec![tle_a1, tle_a2]).expect("2 fields, must be <= MaxFields"); + let info_a: CommitmentInfo<::MaxFields> = CommitmentInfo { fields: fields_a }; + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who_a), + netuid_a, + Box::new(info_a) + )); + assert!( + TimelockedIndex::::get().contains(&(netuid_a, who_a)), + "User A must be in index with 2 timelocks" + ); + + System::::set_block_number(10); + assert_ok!(Pallet::::reveal_timelocked_commitments()); + + let leftover_a1 = CommitmentOf::::get(netuid_a, who_a).expect("still there"); + assert_eq!( + leftover_a1.info.fields.len(), + 1, + "Only the round=1000 timelock removed; round=2000 remains" + ); + assert!( + TimelockedIndex::::get().contains(&(netuid_a, who_a)), + "Still in index with leftover timelock" + ); + + // + // Insert signature for round=2000 => final reveal => leftover=none => removed + // + let drand_sig_2000 = hex::decode(drand_sig_2000_hex).expect("Expected success"); + insert_drand_pulse(round_2000, &drand_sig_2000); + + System::::set_block_number(11); + assert_ok!(Pallet::::reveal_timelocked_commitments()); + + let leftover_a2 = CommitmentOf::::get(netuid_a, who_a); + assert!( + leftover_a2.is_none(), + "All timelocks removed => none leftover" + ); + assert!( + !TimelockedIndex::::get().contains(&(netuid_a, who_a)), + "User A removed from index after final reveal" + ); + }); +} + +#[test] +fn single_timelock_reveal_later_round() { + new_test_ext().execute_with(|| { + let netuid_b = NetUid::from(2); + let who_b = 20; + let round_2000 = 2000; + + let drand_sig_2000_hex = + "b6cb8f482a0b15d45936a4c4ea08e98a087e71787caee3f4d07a8a9843b1bc5423c6b3c22f446488b3137eaca799c77e"; + let drand_sig_2000 = hex::decode(drand_sig_2000_hex).expect("Expected success"); + insert_drand_pulse(round_2000, &drand_sig_2000); + + let msg_b = b"UserB single timelock (round=2000)"; + + let inner_b_fields: BoundedVec::MaxFields> = + BoundedVec::try_from(vec![Data::Raw(msg_b.to_vec().try_into().expect("Expected success"))]) + .expect("MaxFields >= 1"); + let inner_info_b: CommitmentInfo<::MaxFields> = CommitmentInfo { + fields: inner_b_fields, + }; + let encoded_b = inner_info_b.encode(); + let ciphertext_b = produce_ciphertext(&encoded_b, round_2000); + let tle_b = Data::TimelockEncrypted { + encrypted: ciphertext_b, + reveal_round: round_2000, + }; + + let fields_b: BoundedVec::MaxFields> = + BoundedVec::try_from(vec![tle_b]).expect("1 field"); + let info_b: CommitmentInfo<::MaxFields> = CommitmentInfo { fields: fields_b }; + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who_b), + netuid_b, + Box::new(info_b) + )); + assert!( + TimelockedIndex::::get().contains(&(netuid_b, who_b)), + "User B in index" + ); + + // Remove the round=2000 signature so first reveal does nothing + pallet_drand::Pulses::::remove(round_2000); + + System::::set_block_number(20); + assert_ok!(Pallet::::reveal_timelocked_commitments()); + + let leftover_b1 = CommitmentOf::::get(netuid_b, who_b).expect("still there"); + assert_eq!( + leftover_b1.info.fields.len(), + 1, + "No signature => timelock remains" + ); + assert!( + TimelockedIndex::::get().contains(&(netuid_b, who_b)), + "Still in index with leftover timelock" + ); + + insert_drand_pulse(round_2000, &drand_sig_2000); + + System::::set_block_number(21); + assert_ok!(Pallet::::reveal_timelocked_commitments()); + + let leftover_b2 = CommitmentOf::::get(netuid_b, who_b); + assert!(leftover_b2.is_none(), "Timelock removed => leftover=none"); + assert!( + !TimelockedIndex::::get().contains(&(netuid_b, who_b)), + "User B removed from index after final reveal" + ); + }); +} diff --git a/pallets/commitments/src/tests/timelocked_index.rs b/pallets/commitments/src/tests/timelocked_index.rs new file mode 100644 index 0000000000..f899eca24c --- /dev/null +++ b/pallets/commitments/src/tests/timelocked_index.rs @@ -0,0 +1,256 @@ +//! Tests for commitments pallet: timelocked index. + +use super::*; + +#[test] +fn test_index_lifecycle_no_timelocks_updates_in_out() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(100); + let who = 999; + + // + // A) Create a commitment with **no** timelocks => shouldn't be in index + // + let no_tl_fields: BoundedVec::MaxFields> = + BoundedVec::try_from(vec![]).expect("Empty is ok"); + let info_no_tl = CommitmentInfo { + fields: no_tl_fields, + }; + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + Box::new(info_no_tl) + )); + assert!( + !TimelockedIndex::::get().contains(&(netuid, who)), + "User with no timelocks must not appear in index" + ); + + // + // B) Update the commitment to have a timelock => enters index + // + let tl_fields: BoundedVec<_, ::MaxFields> = + BoundedVec::try_from(vec![Data::TimelockEncrypted { + encrypted: Default::default(), + reveal_round: 1234, + }]) + .expect("Expected success"); + let info_with_tl = CommitmentInfo { fields: tl_fields }; + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + Box::new(info_with_tl) + )); + assert!( + TimelockedIndex::::get().contains(&(netuid, who)), + "User must appear in index after adding a timelock" + ); + + // + // C) Remove the timelock => leaves index + // + let back_to_no_tl: BoundedVec<_, ::MaxFields> = + BoundedVec::try_from(vec![]).expect("Expected success"); + let info_remove_tl = CommitmentInfo { + fields: back_to_no_tl, + }; + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + Box::new(info_remove_tl) + )); + + assert!( + !TimelockedIndex::::get().contains(&(netuid, who)), + "User must be removed from index after losing all timelocks" + ); + }); +} + +#[test] +fn timelocked_index_complex_scenario_works() { + new_test_ext().execute_with(|| { + System::::set_block_number(1); + + let netuid = NetUid::from(42); + let user_a = 1000; + let user_b = 2000; + let user_c = 3000; + + let make_timelock_data = |plaintext: &[u8], round: u64| { + let inner = CommitmentInfo:: { + fields: BoundedVec::try_from(vec![Data::Raw( + plaintext.to_vec().try_into().expect("<=128 bytes"), + )]) + .expect("1 field is fine"), + }; + let ct = produce_ciphertext(&inner.encode(), round); + Data::TimelockEncrypted { + encrypted: ct, + reveal_round: round, + } + }; + + let make_raw_data = + |payload: &[u8]| Data::Raw(payload.to_vec().try_into().expect("expected to not panic")); + + // ---------------------------------------------------- + // (1) USER A => no timelocks => NOT in index + // ---------------------------------------------------- + let info_a1 = CommitmentInfo:: { + fields: BoundedVec::try_from(vec![make_raw_data(b"A-regular")]) + .expect("1 field is fine"), + }; + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(user_a), + netuid, + Box::new(info_a1), + )); + assert!( + !TimelockedIndex::::get().contains(&(netuid, user_a)), + "A has no timelocks => not in TimelockedIndex" + ); + + // ---------------------------------------------------- + // (2) USER B => Single TLE => BUT USE round=2000! + // => B is in index + // ---------------------------------------------------- + let b_timelock_1 = make_timelock_data(b"B first TLE", 2000); + let info_b1 = CommitmentInfo:: { + fields: BoundedVec::try_from(vec![b_timelock_1]).expect("Single TLE is fine"), + }; + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(user_b), + netuid, + Box::new(info_b1), + )); + let idx = TimelockedIndex::::get(); + assert!(!idx.contains(&(netuid, user_a)), "A not in index"); + assert!(idx.contains(&(netuid, user_b)), "B in index (has TLE)"); + + // ---------------------------------------------------- + // (3) USER A => 2 timelocks: round=1000 & round=2000 + // => A is in index + // ---------------------------------------------------- + let a_timelock_1 = make_timelock_data(b"A TLE #1", 1000); + let a_timelock_2 = make_timelock_data(b"A TLE #2", 2000); + let info_a2 = CommitmentInfo:: { + fields: BoundedVec::try_from(vec![a_timelock_1, a_timelock_2]) + .expect("2 TLE fields OK"), + }; + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(user_a), + netuid, + Box::new(info_a2), + )); + + let idx = TimelockedIndex::::get(); + assert!(idx.contains(&(netuid, user_a)), "A in index"); + assert!(idx.contains(&(netuid, user_b)), "B still in index"); + + // ---------------------------------------------------- + // (4) USER B => remove all timelocks => B out of index + // ---------------------------------------------------- + let info_b2 = CommitmentInfo:: { + fields: BoundedVec::try_from(vec![make_raw_data(b"B back to raw")]) + .expect("no TLE => B out"), + }; + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(user_b), + netuid, + Box::new(info_b2), + )); + let idx = TimelockedIndex::::get(); + assert!(idx.contains(&(netuid, user_a)), "A remains"); + assert!( + !idx.contains(&(netuid, user_b)), + "B removed after losing TLEs" + ); + + // ---------------------------------------------------- + // (5) USER B => re-add TLE => round=2000 => back in index + // ---------------------------------------------------- + let b_timelock_2 = make_timelock_data(b"B TLE #2", 2000); + let info_b3 = CommitmentInfo:: { + fields: BoundedVec::try_from(vec![b_timelock_2]).expect("expected to not panic"), + }; + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(user_b), + netuid, + Box::new(info_b3), + )); + let idx = TimelockedIndex::::get(); + assert!(idx.contains(&(netuid, user_a)), "A in index"); + assert!(idx.contains(&(netuid, user_b)), "B back in index"); + + // ---------------------------------------------------- + // (6) USER C => sets 1 TLE => round=2000 => in index + // ---------------------------------------------------- + let c_timelock_1 = make_timelock_data(b"C TLE #1", 2000); + let info_c1 = CommitmentInfo:: { + fields: BoundedVec::try_from(vec![c_timelock_1]).expect("expected to not panic"), + }; + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(user_c), + netuid, + Box::new(info_c1), + )); + let idx = TimelockedIndex::::get(); + assert!(idx.contains(&(netuid, user_a)), "A"); + assert!(idx.contains(&(netuid, user_b)), "B"); + assert!(idx.contains(&(netuid, user_c)), "C"); + + // ---------------------------------------------------- + // (7) Partial reveal for round=1000 => affects only A + // because B & C have round=2000 + // ---------------------------------------------------- + let drand_sig_1000 = + hex::decode(DRAND_QUICKNET_SIG_HEX).expect("decode signature for round=1000"); + insert_drand_pulse(1000, &drand_sig_1000); + + System::::set_block_number(10); + assert_ok!(Pallet::::reveal_timelocked_commitments()); + + // After revealing round=1000: + // - A: Loses TLE #1 (1000), still has TLE #2 (2000) => remains in index + // - B: referencing 2000 => unaffected => remains + // - C: referencing 2000 => remains + let idx = TimelockedIndex::::get(); + assert!( + idx.contains(&(netuid, user_a)), + "A has leftover round=2000 => remains in index" + ); + assert!(idx.contains(&(netuid, user_b)), "B unaffected"); + assert!(idx.contains(&(netuid, user_c)), "C unaffected"); + + // ---------------------------------------------------- + // (8) Reveal round=2000 => fully remove A, B, and C + // ---------------------------------------------------- + let drand_sig_2000 = + hex::decode(DRAND_QUICKNET_SIG_2000_HEX).expect("decode signature for round=2000"); + insert_drand_pulse(2000, &drand_sig_2000); + + System::::set_block_number(11); + assert_ok!(Pallet::::reveal_timelocked_commitments()); + + // Now: + // - A's final TLE (#2 at 2000) is removed => A out + // - B had 2000 => out + // - C had 2000 => out + let idx = TimelockedIndex::::get(); + assert!( + !idx.contains(&(netuid, user_a)), + "A removed after 2000 reveal" + ); + assert!( + !idx.contains(&(netuid, user_b)), + "B removed after 2000 reveal" + ); + assert!( + !idx.contains(&(netuid, user_c)), + "C removed after 2000 reveal" + ); + + assert_eq!(idx.len(), 0, "All users revealed => index is empty"); + }); +} diff --git a/pallets/commitments/src/types.rs b/pallets/commitments/src/types.rs index cdca47922b..d21f31c037 100644 --- a/pallets/commitments/src/types.rs +++ b/pallets/commitments/src/types.rs @@ -58,7 +58,9 @@ pub enum Data { ShaThree256([u8; 32]), /// A timelock-encrypted commitment with a reveal round. TimelockEncrypted { + /// TLE ciphertext bytes (max [`MAX_TIMELOCK_COMMITMENT_SIZE_BYTES`]). encrypted: BoundedVec>, + /// Drand Quicknet round at/after which the ciphertext may be decrypted on-chain. reveal_round: u64, }, /// Flag to trigger bonds reset for subnet @@ -68,15 +70,20 @@ pub enum Data { } impl Data { + /// Returns true when this is the empty [`Data::None`] variant. pub fn is_none(&self) -> bool { self == &Data::None } - /// Check if this is a timelock-encrypted commitment. + /// Returns true when this field is a [`Data::TimelockEncrypted`] ciphertext. pub fn is_timelock_encrypted(&self) -> bool { matches!(self, Data::TimelockEncrypted { .. }) } + /// Bytes counted against the per-epoch [`crate::UsedSpaceOf`] budget for this field. + /// + /// Hash variants count as 32; `ResetBondsFlag` and `None` count as 0; raw/timelock count + /// their payload length (not SCALE overhead). pub fn len_for_rate_limit(&self) -> u64 { match self { Data::None => 0, @@ -362,7 +369,8 @@ impl Default for Data { } } -#[freeze_struct("5ca4adbb4d2a2b20")] +/// Payload of a commitment extrinsic: bounded list of [`Data`] fields. +#[freeze_struct("13181415752e92ef")] #[derive( CloneNoBound, Encode, @@ -378,37 +386,42 @@ impl Default for Data { #[derive(frame_support::DefaultNoBound)] #[scale_info(skip_type_params(FieldLimit))] pub struct CommitmentInfo> { + /// Ordered fields (raw blobs, hashes, timelock ciphertext, or bonds-reset flag). pub fields: BoundedVec, } -/// Maximum size of the serialized timelock commitment in bytes +/// Max SCALE size (bytes) allowed inside a [`Data::TimelockEncrypted`] ciphertext. pub const MAX_TIMELOCK_COMMITMENT_SIZE_BYTES: u32 = 1024; +/// Max payload size (bytes) for the [`Data::BigRaw`] variant. pub const MAX_BIGRAW_COMMITMENT_SIZE_BYTES: u32 = 512; -/// Contains the decrypted data of a revealed commitment. -#[freeze_struct("bf575857b57f9bef")] +/// Historical record of a revealed (decrypted) commitment and its deposit/block metadata. +#[freeze_struct("e1fd4df85cac545e")] #[derive(Clone, Eq, PartialEq, Encode, Decode, TypeInfo, Debug)] pub struct RevealedData, BlockNumber> { + /// Commitment fields as they existed at reveal time. pub info: CommitmentInfo, + /// Block number when the reveal occurred. pub revealed_block: BlockNumber, + /// Deposit that was associated with the commitment. pub deposit: Balance, } -/// Tracks how much “space” each (netuid, who) has used within the current RateLimit block-window. +/// Per-(netuid, who) rate-limit counters for the current tempo epoch window. #[freeze_struct("1f23fb50f96326e4")] #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, TypeInfo)] pub struct UsageTracker { - /// Last epoch block + /// Epoch index from [`crate::GetTempoInterface`] when `used_space` was last accumulated. pub last_epoch: u64, - /// Space used + /// Bytes consumed in `last_epoch` toward [`crate::MaxSpace`]. pub used_space: u64, } -/// Information concerning the identity of the controller of an account. +/// On-chain commitment registration stored in [`crate::CommitmentOf`]. /// -/// NOTE: This is stored separately primarily to facilitate the addition of extra fields in a -/// backwards compatible way through a specialized `Decode` impl. -#[freeze_struct("632f12850e51c420")] +/// Stored separately (with a specialized `Decode`) so extra fields can be appended in a +/// backwards-compatible way via trailing zeros. +#[freeze_struct("6585afd993baff29")] #[derive( CloneNoBound, Encode, Eq, MaxEncodedLen, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo, )] @@ -419,12 +432,13 @@ pub struct Registration< MaxFields: Get, BlockNumber: Codec + Clone + Ord + Eq + AtLeast32BitUnsigned + MaxEncodedLen + Debug, > { - /// Amount held on deposit for this information. + /// Amount held on deposit for this commitment. pub deposit: Balance, + /// Block at which this registration was last written by `set_commitment`. pub block: BlockNumber, - /// Information on the identity. + /// Current commitment payload. pub info: CommitmentInfo, } diff --git a/pallets/crowdloan/src/benchmarking.rs b/pallets/crowdloan/src/benchmarking.rs index f7cbc6eb31..822bd861d3 100644 --- a/pallets/crowdloan/src/benchmarking.rs +++ b/pallets/crowdloan/src/benchmarking.rs @@ -1,4 +1,8 @@ -//! Benchmarks for Crowdloan Pallet +//! Runtime benchmarks for crowdloan extrinsics (`create`, `contribute`, `withdraw`, …). +//! +//! Setup helpers seed a funded crowdloan via storage so weight functions exercise +//! realistic contribution / refund / finalize paths without depending on prior extrinsics +//! beyond what each benchmark needs. #![cfg(feature = "runtime-benchmarks")] #![allow( clippy::arithmetic_side_effects, @@ -59,7 +63,7 @@ mod benchmarks { // ensure the crowdloan is stored correctly let crowdloan_id = 0; - let funds_account = Pallet::::funds_account(crowdloan_id); + let funds_account = Pallet::::crowdloan_funds_account(crowdloan_id); assert_eq!( Crowdloans::::get(crowdloan_id), Some(CrowdloanInfo { @@ -143,7 +147,7 @@ mod benchmarks { assert!(Crowdloans::::get(crowdloan_id).is_some_and(|c| c.raised == deposit + amount)); // ensure the contribution is present in the crowdloan account assert_eq!( - CurrencyOf::::balance(&Pallet::::funds_account(crowdloan_id)), + CurrencyOf::::balance(&Pallet::::crowdloan_funds_account(crowdloan_id)), deposit + amount ); // ensure the event is emitted @@ -202,7 +206,7 @@ mod benchmarks { assert_eq!(CurrencyOf::::balance(&contributor), amount); // ensure the crowdloan account has been deducted the contribution assert_eq!( - CurrencyOf::::balance(&Pallet::::funds_account(crowdloan_id)), + CurrencyOf::::balance(&Pallet::::crowdloan_funds_account(crowdloan_id)), deposit ); // ensure the crowdloan raised amount is updated correctly @@ -334,7 +338,7 @@ mod benchmarks { } // ensure the crowdloan account has been deducted the contributions assert_eq!( - CurrencyOf::::balance(&Pallet::::funds_account(crowdloan_id)), + CurrencyOf::::balance(&Pallet::::crowdloan_funds_account(crowdloan_id)), deposit ); // ensure the raised amount is updated correctly diff --git a/pallets/crowdloan/src/lib.rs b/pallets/crowdloan/src/lib.rs index f7d771f4a7..07087c73ad 100644 --- a/pallets/crowdloan/src/lib.rs +++ b/pallets/crowdloan/src/lib.rs @@ -1,9 +1,16 @@ //! # Crowdloan Pallet //! -//! A pallet allowing users to create generic crowdloans and contribute to them, -//! then finalize them through exactly one configured route: transfer the raised -//! funds to a target address or dispatch an extrinsic, making it reusable for any -//! crowdloan type. +//! Generic crowdloan raise-and-finalize flow used by Bittensor (e.g. subnet leasing). +//! +//! Lifecycle: +//! 1. [`Pallet::create`] — creator posts a deposit and configures **exactly one** +//! finalization route (`call` **xor** `target_address`). +//! 2. [`Pallet::contribute`] / [`Pallet::withdraw`] — raise funds until `cap` or `end`. +//! 3. Success path: [`Pallet::finalize`] (requires `raised == cap`). +//! 4. Failure path: [`Pallet::refund`] (batched) then [`Pallet::dissolve`]. +//! +//! During call-based finalization, [`CurrentCrowdloanId`] is briefly set so the +//! dispatched call can read which crowdloan is being finalized. #![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; @@ -33,6 +40,7 @@ use weights::WeightInfo; pub use pallet::*; use subtensor_macros::freeze_struct; +/// Incrementing identifier for a crowdloan; keys [`Crowdloans`] and related maps. pub type CrowdloanId = u32; mod benchmarking; @@ -41,44 +49,48 @@ mod mock; mod tests; pub mod weights; +/// Alias for the pallet's configured currency type. pub type CurrencyOf = ::Currency; +/// Balance type of [`CurrencyOf`], in rao (TAO smallest unit) for this runtime. pub type BalanceOf = as fungible::Inspect<::AccountId>>::Balance; -// Define a maximum length for the migration key +/// Max length of a `HasMigrationRun` key (`BoundedVec`). type MigrationKeyMaxLen = ConstU32<128>; +/// Preimage-bounded runtime call stored on a crowdloan for call-based finalization. pub type BoundedCallOf = Bounded<::RuntimeCall, ::Hashing>; -/// A struct containing the information about a crowdloan. -#[freeze_struct("5db9538284491545")] +/// On-chain record for one crowdloan (cap, timing, finalization route, raised total). +/// +/// Invariant: exactly one of `call` or `target_address` is `Some` for a valid +/// creatable/finalizable crowdloan; both or neither yields [`Error::InvalidFinalizationConfig`]. +#[freeze_struct("8a6ddd055c5a5c0b")] #[derive(Encode, Decode, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub struct CrowdloanInfo { - /// The creator of the crowdloan. + /// Coldkey / account that created the crowdloan and may finalize, refund, or dissolve it. pub creator: AccountId, - /// The initial deposit of the crowdloan from the creator. + /// Creator's locked deposit (rao); counted in `raised` and not withdrawable until dissolve. pub deposit: Balance, - /// Minimum contribution to the crowdloan. + /// Per-contribution floor (rao); also bounded by [`Config::AbsoluteMinimumContribution`]. pub min_contribution: Balance, - /// The end block of the crowdloan. + /// First block at which contributions are rejected (`now < end` required to contribute). pub end: BlockNumber, - /// The cap to raise. + /// Maximum `raised` (rao); finalization requires `raised == cap`. pub cap: Balance, - /// The account holding the funds for this crowdloan. Derived on chain but put here for ease of use. + /// Pallet-derived account holding contributed TAO for this crowdloan id. pub funds_account: AccountId, - /// The amount raised so far. + /// Total TAO held toward the cap (includes creator deposit), in rao. pub raised: Balance, - /// The optional target address to transfer the raised funds to, if not - /// provided, it means the funds will be transferred from on chain logic - /// inside the provided call to dispatch. + /// If set (and `call` is `None`), finalize transfers `raised` here. pub target_address: Option, - /// The optional call to dispatch when the crowdloan is finalized. + /// If set (and `target_address` is `None`), finalize dispatches this preimage-bounded call. pub call: Option, - /// Whether the crowdloan has been finalized. + /// Set true when [`Pallet::finalize`] succeeds; blocks further withdraw/refund/dissolve. pub finalized: bool, - /// The number of contributors to the crowdloan. + /// Distinct contributors with a nonzero [`Contributions`] entry (includes creator). pub contributors_count: u32, } @@ -97,10 +109,10 @@ pub mod pallet { #[pallet::pallet] pub struct Pallet(_); - /// Configuration trait. + /// Runtime configuration for the crowdloan pallet. #[pallet::config] pub trait Config: frame_system::Config { - /// The overarching call type. + /// Runtime call type; must be dispatchable and subtype-checkable for nested crowdloan calls. type RuntimeCall: Parameter + Dispatchable + GetDispatchInfo @@ -108,55 +120,55 @@ pub mod pallet { + IsSubType> + IsType<::RuntimeCall>; - /// The currency mechanism. + /// Fungible used for deposits and contributions (TAO / rao in production). type Currency: fungible::Balanced + fungible::Mutate; - /// The weight information for the pallet. + /// Extrinsic weight benchmarks for this pallet. type WeightInfo: WeightInfo; - /// The preimage provider which will be used to store the call to dispatch. + /// Stores / peeks the optional finalize `call` preimage. type Preimages: QueryPreimage + StorePreimage; - /// The pallet id that will be used to derive crowdloan account ids. + /// Seed for deriving per-crowdloan [`CrowdloanInfo::funds_account`] sub-accounts. #[pallet::constant] type PalletId: Get; - /// The minimum deposit required to create a crowdloan. + /// Floor on creator deposit at [`Pallet::create`] (rao). #[pallet::constant] type MinimumDeposit: Get>; - /// The absolute minimum contribution required to contribute to a crowdloan. + /// Global floor on `min_contribution` at create and update (rao). #[pallet::constant] type AbsoluteMinimumContribution: Get>; - /// The minimum block duration for a crowdloan. + /// Minimum `end - now` block span allowed for a crowdloan window. #[pallet::constant] type MinimumBlockDuration: Get>; - /// The maximum block duration for a crowdloan. + /// Maximum `end - now` block span allowed for a crowdloan window. #[pallet::constant] type MaximumBlockDuration: Get>; - /// The maximum number of contributors that can be refunded in a single refund. + /// Max non-creator contributors refunded per [`Pallet::refund`] extrinsic. #[pallet::constant] type RefundContributorsLimit: Get; - // The maximum number of contributors that can contribute to a crowdloan. + /// Hard cap on [`CrowdloanInfo::contributors_count`] (includes creator). #[pallet::constant] type MaxContributors: Get; } - /// A map of crowdloan ids to their information. + /// Crowdloan id → [`CrowdloanInfo`] for every live (not yet dissolved) crowdloan. #[pallet::storage] pub type Crowdloans = StorageMap<_, Twox64Concat, CrowdloanId, CrowdloanInfoOf, OptionQuery>; - /// The next incrementing crowdloan id. + /// Next unused [`CrowdloanId`]; starts at 0 and increments on each successful create. #[pallet::storage] pub type NextCrowdloanId = StorageValue<_, CrowdloanId, ValueQuery, ConstU32<0>>; - /// A map of crowdloan ids to their contributors and their contributions. + /// Per-(crowdloan id, contributor) cumulative contribution balance in rao. #[pallet::storage] pub type Contributions = StorageDoubleMap< _, @@ -168,17 +180,20 @@ pub mod pallet { OptionQuery, >; - /// A map of crowdloan ids to their optional maximum cumulative contribution per contributor. + /// Optional per-contributor cumulative contribution ceiling (rao) for a crowdloan. + /// + /// Absent means no per-account max beyond the crowdloan `cap`. #[pallet::storage] pub type MaxContributions = StorageMap<_, Twox64Concat, CrowdloanId, BalanceOf, OptionQuery>; - /// The current crowdloan id that will be set during the finalize call, making it - /// temporarily accessible to the dispatched call. + /// Crowdloan id being finalized while a call-route finalize dispatches; otherwise `None`. + /// + /// Nested crowdloan extrinsics see [`Error::AlreadyFinalizing`] while this is set. #[pallet::storage] pub type CurrentCrowdloanId = StorageValue<_, CrowdloanId, OptionQuery>; - /// Storage for the migration run status. + /// Idempotency flags for named storage migrations (`true` once that migration has run). #[pallet::storage] pub type HasMigrationRun = StorageMap<_, Identity, BoundedVec, bool, ValueQuery>; @@ -186,49 +201,49 @@ pub mod pallet { #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { - /// A crowdloan was created. + /// Emitted after [`Pallet::create`] stores a new crowdloan and takes the deposit. Created { crowdloan_id: CrowdloanId, creator: T::AccountId, end: BlockNumberFor, cap: BalanceOf, }, - /// A contribution was made to an active crowdloan. + /// Emitted after a successful [`Pallet::contribute`] (accepted amount may be clipped to room). Contributed { crowdloan_id: CrowdloanId, contributor: T::AccountId, amount: BalanceOf, }, - /// A contribution was withdrawn from a failed crowdloan. + /// Emitted after [`Pallet::withdraw`] returns contribution TAO to the contributor. Withdrew { crowdloan_id: CrowdloanId, contributor: T::AccountId, amount: BalanceOf, }, - /// A refund was partially processed for a failed crowdloan. + /// [`Pallet::refund`] hit [`Config::RefundContributorsLimit`] before clearing all non-creators. PartiallyRefunded { crowdloan_id: CrowdloanId }, - /// A refund was fully processed for a failed crowdloan. + /// [`Pallet::refund`] returned every non-creator contribution in this call. AllRefunded { crowdloan_id: CrowdloanId }, - /// A crowdloan was finalized, funds were transferred and the call was dispatched. + /// Cap reached and finalization route (transfer or call dispatch) completed. Finalized { crowdloan_id: CrowdloanId }, - /// A crowdloan was dissolved. + /// Crowdloan storage, contributions, and funds account provider ref cleared after dissolve. Dissolved { crowdloan_id: CrowdloanId }, - /// The minimum contribution was updated. + /// Creator changed `CrowdloanInfo::min_contribution` via [`Pallet::update_min_contribution`]. MinContributionUpdated { crowdloan_id: CrowdloanId, new_min_contribution: BalanceOf, }, - /// The end was updated. + /// Creator changed `CrowdloanInfo::end` via [`Pallet::update_end`]. EndUpdated { crowdloan_id: CrowdloanId, new_end: BlockNumberFor, }, - /// The cap was updated. + /// Creator changed `CrowdloanInfo::cap` via [`Pallet::update_cap`]. CapUpdated { crowdloan_id: CrowdloanId, new_cap: BalanceOf, }, - /// The maximum contribution was updated. + /// Creator set or cleared [`MaxContributions`] via [`Pallet::set_max_contribution`]. MaxContributionUpdated { crowdloan_id: CrowdloanId, new_max_contribution: Option>, @@ -237,59 +252,59 @@ pub mod pallet { #[pallet::error] pub enum Error { - /// The crowdloan initial deposit is too low. + /// Creator deposit below [`Config::MinimumDeposit`]. DepositTooLow, - /// The crowdloan cap is too low. + /// Cap not strictly above deposit (create) or below current `raised` (update). CapTooLow, - /// The minimum contribution is too low. + /// `min_contribution` below [`Config::AbsoluteMinimumContribution`]. MinimumContributionTooLow, - /// The crowdloan cannot end in the past. + /// Proposed `end` is not strictly after the current block. CannotEndInPast, - /// The crowdloan block duration is too short. + /// `end - now` shorter than [`Config::MinimumBlockDuration`]. BlockDurationTooShort, - /// The block duration is too long. + /// `end - now` longer than [`Config::MaximumBlockDuration`]. BlockDurationTooLong, - /// The account does not have enough balance to pay for the initial deposit/contribution. + /// Signer lacks free balance for the deposit or contribution transfer. InsufficientBalance, - /// An overflow occurred. + /// Checked arithmetic overflow (ids, raised totals, or contributor counts). Overflow, - /// The crowdloan id is invalid. + /// No [`Crowdloans`] entry for the given id. InvalidCrowdloanId, - /// The crowdloan cap has been fully raised. + /// Contributions rejected because `raised` already equals `cap`. CapRaised, - /// The contribution period has ended. + /// Contributions rejected because `now >= end`. ContributionPeriodEnded, - /// The contribution is too low. + /// Requested contribution below the crowdloan's `min_contribution`. ContributionTooLow, - /// The origin of this call is invalid. + /// Signed origin is not the crowdloan creator where creator-only is required. InvalidOrigin, - /// The crowdloan has already been finalized. + /// Operation blocked because `CrowdloanInfo::finalized` is already true. AlreadyFinalized, - /// A crowdloan finalization is already in progress. + /// Nested finalize attempted while [`CurrentCrowdloanId`] is set. AlreadyFinalizing, - /// The crowdloan contribution period has not ended yet. + /// Reserved for contribution-period gating (not currently returned by extrinsics). ContributionPeriodNotEnded, - /// The contributor has no contribution for this crowdloan. + /// Contributor has no [`Contributions`] row (or creator missing deposit row on dissolve). NoContribution, - /// The crowdloan cap has not been raised. + /// Finalize requires `raised == cap`. CapNotRaised, - /// An underflow occurred. + /// Checked arithmetic underflow. Underflow, - /// Call to dispatch was not found in the preimage storage. + /// Finalize call preimage missing from [`Config::Preimages`]. CallUnavailable, - /// The crowdloan is not ready to be dissolved, it still has contributions. + /// Dissolve requires `raised` equal only to the creator's remaining contribution. NotReadyToDissolve, - /// The deposit cannot be withdrawn from the crowdloan. + /// Creator tried to withdraw when only the locked deposit remains. DepositCannotBeWithdrawn, - /// The maximum number of contributors has been reached. + /// New contributor would exceed [`Config::MaxContributors`]. MaxContributorsReached, - /// Exactly one of call or target address must be provided. + /// Create/finalize config is not exactly one of `call` or `target_address`. InvalidFinalizationConfig, - /// The contributor has already reached the maximum contribution. + /// Contributor already at [`MaxContributions`] for this crowdloan. MaxContributionReached, - /// The maximum contribution is too low. + /// New max contribution below `min_contribution` or creator's current contribution. MaximumContributionTooLow, - /// The minimum contribution is too high. + /// New min contribution above the configured [`MaxContributions`] ceiling. MinimumContributionTooHigh, } @@ -366,7 +381,7 @@ pub mod pallet { Error::::InvalidFinalizationConfig ); - Self::ensure_valid_end(now, end)?; + Self::ensure_crowdloan_end_in_window(now, end)?; // Ensure the creator has enough balance to pay the initial deposit ensure!( @@ -379,7 +394,7 @@ pub mod pallet { NextCrowdloanId::::put(next_crowdloan_id); // Derive the funds account and keep track of it - let funds_account = Self::funds_account(crowdloan_id); + let funds_account = Self::crowdloan_funds_account(crowdloan_id); frame_system::Pallet::::inc_providers(&funds_account); // If the call is provided, bound it and store it in the preimage storage @@ -445,7 +460,7 @@ pub mod pallet { let contributor = ensure_signed(origin)?; let now = frame_system::Pallet::::block_number(); - let mut crowdloan = Self::ensure_crowdloan_exists(crowdloan_id)?; + let mut crowdloan = Self::require_crowdloan(crowdloan_id)?; // Ensure crowdloan has not ended and has not raised cap ensure!(now < crowdloan.end, Error::::ContributionPeriodEnded); @@ -551,7 +566,7 @@ pub mod pallet { ) -> DispatchResult { let who = ensure_signed(origin)?; - let mut crowdloan = Self::ensure_crowdloan_exists(crowdloan_id)?; + let mut crowdloan = Self::require_crowdloan(crowdloan_id)?; ensure!(!crowdloan.finalized, Error::::AlreadyFinalized); // Ensure contributor has balance left in the crowdloan account @@ -614,7 +629,7 @@ pub mod pallet { ) -> DispatchResult { let who = ensure_signed(origin)?; - let mut crowdloan = Self::ensure_crowdloan_exists(crowdloan_id)?; + let mut crowdloan = Self::require_crowdloan(crowdloan_id)?; // Ensure the origin is the creator of the crowdloan and the crowdloan has raised the cap // and is not finalized. @@ -691,7 +706,7 @@ pub mod pallet { ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; - let mut crowdloan = Self::ensure_crowdloan_exists(crowdloan_id)?; + let mut crowdloan = Self::require_crowdloan(crowdloan_id)?; // Ensure the crowdloan is not finalized ensure!(!crowdloan.finalized, Error::::AlreadyFinalized); @@ -766,7 +781,7 @@ pub mod pallet { ) -> DispatchResult { let who = ensure_signed(origin)?; - let crowdloan = Self::ensure_crowdloan_exists(crowdloan_id)?; + let crowdloan = Self::require_crowdloan(crowdloan_id)?; ensure!(!crowdloan.finalized, Error::::AlreadyFinalized); // Only the creator can dissolve the crowdloan @@ -823,7 +838,7 @@ pub mod pallet { ) -> DispatchResult { let who = ensure_signed(origin)?; - let mut crowdloan = Self::ensure_crowdloan_exists(crowdloan_id)?; + let mut crowdloan = Self::require_crowdloan(crowdloan_id)?; ensure!(!crowdloan.finalized, Error::::AlreadyFinalized); // Only the creator can update the min contribution. @@ -868,13 +883,13 @@ pub mod pallet { let who = ensure_signed(origin)?; let now = frame_system::Pallet::::block_number(); - let mut crowdloan = Self::ensure_crowdloan_exists(crowdloan_id)?; + let mut crowdloan = Self::require_crowdloan(crowdloan_id)?; ensure!(!crowdloan.finalized, Error::::AlreadyFinalized); // Only the creator can update the min contribution. ensure!(who == crowdloan.creator, Error::::InvalidOrigin); - Self::ensure_valid_end(now, new_end)?; + Self::ensure_crowdloan_end_in_window(now, new_end)?; crowdloan.end = new_end; Crowdloans::::insert(crowdloan_id, &crowdloan); @@ -903,7 +918,7 @@ pub mod pallet { let who = ensure_signed(origin)?; // The cap can only be updated if the crowdloan has not been finalized. - let mut crowdloan = Self::ensure_crowdloan_exists(crowdloan_id)?; + let mut crowdloan = Self::require_crowdloan(crowdloan_id)?; ensure!(!crowdloan.finalized, Error::::AlreadyFinalized); // Only the creator can update the cap. @@ -939,7 +954,7 @@ pub mod pallet { ) -> DispatchResult { let who = ensure_signed(origin)?; - let crowdloan = Self::ensure_crowdloan_exists(crowdloan_id)?; + let crowdloan = Self::require_crowdloan(crowdloan_id)?; ensure!(!crowdloan.finalized, Error::::AlreadyFinalized); // Only the creator can update the max contribution. @@ -969,17 +984,22 @@ pub mod pallet { } impl Pallet { - fn funds_account(id: CrowdloanId) -> T::AccountId { - T::PalletId::get().into_sub_account_truncating(id) + /// Derive the custodial account that holds TAO for `crowdloan_id` from [`Config::PalletId`]. + pub(crate) fn crowdloan_funds_account(crowdloan_id: CrowdloanId) -> T::AccountId { + T::PalletId::get().into_sub_account_truncating(crowdloan_id) } - fn ensure_crowdloan_exists(crowdloan_id: CrowdloanId) -> Result, Error> { + /// Load [`Crowdloans`] entry or [`Error::InvalidCrowdloanId`]. + fn require_crowdloan(crowdloan_id: CrowdloanId) -> Result, Error> { Crowdloans::::get(crowdloan_id).ok_or(Error::::InvalidCrowdloanId) } - // Ensure the provided end block is after the current block and the duration is - // between the minimum and maximum block duration - fn ensure_valid_end(now: BlockNumberFor, end: BlockNumberFor) -> Result<(), Error> { + /// Reject `end` in the past or outside [`Config::MinimumBlockDuration`] / + /// [`Config::MaximumBlockDuration`] relative to `now`. + fn ensure_crowdloan_end_in_window( + now: BlockNumberFor, + end: BlockNumberFor, + ) -> Result<(), Error> { ensure!(now < end, Error::::CannotEndInPast); let block_duration = end.checked_sub(&now).ok_or(Error::::Underflow)?; ensure!( diff --git a/pallets/crowdloan/src/migrations/migrate_add_contributors_count.rs b/pallets/crowdloan/src/migrations/migrate_add_contributors_count.rs index 604e99d4e3..211869bbbd 100644 --- a/pallets/crowdloan/src/migrations/migrate_add_contributors_count.rs +++ b/pallets/crowdloan/src/migrations/migrate_add_contributors_count.rs @@ -1,9 +1,16 @@ +//! Backfill [`crate::CrowdloanInfo::contributors_count`] onto pre-existing crowdloans. +//! +//! Reads the previous layout ([`old_storage::OldCrowdloanInfo`], without the count +//! field), counts [`crate::Contributions`] keys per id, and rewrites +//! [`crate::Crowdloans`]. Idempotent via migration name `migrate_add_contributors_count`. + use alloc::string::String; use frame_support::{BoundedVec, migration::storage_key_iter, traits::Get, weights::Weight}; use subtensor_macros::freeze_struct; use crate::*; +/// Pre-migration SCALE layout of crowdloan info (no `contributors_count` field). mod old_storage { use super::*; @@ -23,6 +30,7 @@ mod old_storage { } } +/// Populate `contributors_count` from contribution map cardinality; skip if already run. pub fn migrate_add_contributors_count() -> Weight { let migration_name = BoundedVec::truncate_from(b"migrate_add_contributors_count".to_vec()); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/crowdloan/src/migrations/mod.rs b/pallets/crowdloan/src/migrations/mod.rs index f6701fb83a..de2f324b28 100644 --- a/pallets/crowdloan/src/migrations/mod.rs +++ b/pallets/crowdloan/src/migrations/mod.rs @@ -1,2 +1,7 @@ +//! Storage migrations for `pallet-crowdloan`. +//! +//! Each migration records completion in [`crate::HasMigrationRun`] under a fixed +//! byte-string name (must stay stable for idempotency). + mod migrate_add_contributors_count; pub use migrate_add_contributors_count::*; diff --git a/pallets/crowdloan/src/mock.rs b/pallets/crowdloan/src/mock.rs index 486bb4e71c..ce3cebfcaf 100644 --- a/pallets/crowdloan/src/mock.rs +++ b/pallets/crowdloan/src/mock.rs @@ -1,3 +1,8 @@ +//! Test runtime and helpers for `pallet-crowdloan`. +//! +//! Provides [`Test`] (system + balances + crowdloan + preimage + [`pallet_test`]), +//! [`TestState`] for genesis balances/block number, and helpers such as +//! [`noop_call`] / [`run_to_block`] used by extrinsic unit tests. #![cfg(test)] #![allow( clippy::arithmetic_side_effects, @@ -143,7 +148,10 @@ impl pallet_crowdloan::Config for Test { type MaxContributors = MaxContributors; } -// A test pallet used to test some behavior of the crowdloan pallet +/// Companion pallet whose calls are used as crowdloan finalize payloads in tests. +/// +/// Extrinsics read [`pallet_crowdloan::CurrentCrowdloanId`] to assert finalize wiring +/// (transfer raised funds, record id, or fail on purpose). #[allow(unused)] #[frame_support::pallet(dev_mode)] pub(crate) mod pallet_test { @@ -221,6 +229,7 @@ impl pallet_test::Config for Test { type Currency = Balances; } +/// Builder for a [`Test`] externalities with optional balances and starting block. pub(crate) struct TestState { block_number: BlockNumberFor, balances: Vec<(AccountOf, BalanceOf)>, diff --git a/pallets/crowdloan/src/tests.rs b/pallets/crowdloan/src/tests.rs deleted file mode 100644 index 863ca9ae60..0000000000 --- a/pallets/crowdloan/src/tests.rs +++ /dev/null @@ -1,3202 +0,0 @@ -#![cfg(test)] -#![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] - -use frame_support::{StorageDoubleMap, assert_err, assert_ok, traits::StorePreimage}; -use frame_system::pallet_prelude::BlockNumberFor; -use sp_core::U256; -use sp_runtime::DispatchError; -use subtensor_runtime_common::TaoBalance; - -use crate::{BalanceOf, CrowdloanId, CrowdloanInfo, mock::*, pallet as pallet_crowdloan}; - -#[test] -fn test_create_succeeds() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - let crowdloan_id = 0; - let funds_account = pallet_crowdloan::Pallet::::funds_account(crowdloan_id); - // ensure the crowdloan is stored correctly - let call = pallet_preimage::Pallet::::bound(*noop_call()).unwrap(); - assert_eq!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id), - Some(CrowdloanInfo { - creator, - deposit, - min_contribution, - cap, - end, - funds_account, - raised: deposit, - target_address: None, - call: Some(call), - finalized: false, - contributors_count: 1, - }) - ); - // ensure the crowdloan account has the deposit - assert_eq!(Balances::free_balance(funds_account), deposit); - // ensure the creator has been deducted the deposit - assert_eq!( - Balances::free_balance(creator), - TaoBalance::from(100) - deposit - ); - // ensure the contributions have been updated - assert_eq!( - pallet_crowdloan::Contributions::::iter_prefix(crowdloan_id) - .collect::>(), - vec![(creator, deposit)] - ); - // ensure the raised amount is updated correctly - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.raised == deposit) - ); - // ensure the event is emitted - assert_eq!( - last_event(), - pallet_crowdloan::Event::::Created { - crowdloan_id, - creator, - end, - cap, - } - .into() - ); - // ensure next crowdloan id is incremented - assert_eq!( - pallet_crowdloan::NextCrowdloanId::::get(), - crowdloan_id + 1 - ); - }); -} - -#[test] -fn test_create_fails_if_bad_origin() { - TestState::default().build_and_execute(|| { - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 50; - - assert_err!( - Crowdloan::create( - RuntimeOrigin::none(), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - ), - DispatchError::BadOrigin - ); - - assert_err!( - Crowdloan::create( - RuntimeOrigin::root(), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - ), - DispatchError::BadOrigin - ); - }); -} - -#[test] -fn test_create_fails_if_deposit_is_too_low() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 20.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 50; - - assert_err!( - Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - ), - pallet_crowdloan::Error::::DepositTooLow - ); - }); -} - -#[test] -fn test_create_fails_if_cap_is_not_greater_than_deposit() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 40.into(); - let end: BlockNumberFor = 50; - - assert_err!( - Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - ), - pallet_crowdloan::Error::::CapTooLow - ); - }); -} - -#[test] -fn test_create_fails_if_min_contribution_is_too_low() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 5.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 50; - - assert_err!( - Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - ), - pallet_crowdloan::Error::::MinimumContributionTooLow - ); - }); -} - -#[test] -fn test_create_fails_if_call_and_target_address_are_provided() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 50; - let target_address: AccountOf = U256::from(42); - - assert_err!( - Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - Some(target_address), - ), - pallet_crowdloan::Error::::InvalidFinalizationConfig - ); - }); -} - -#[test] -fn test_create_fails_if_call_and_target_address_are_missing() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 50; - - assert_err!( - Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - None, - None, - ), - pallet_crowdloan::Error::::InvalidFinalizationConfig - ); - }); -} - -#[test] -fn test_set_max_contribution_fails_if_max_contribution_is_too_low() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let max_contribution: BalanceOf = 40.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - )); - - assert_err!( - Crowdloan::set_max_contribution( - RuntimeOrigin::signed(creator), - 0, - Some(max_contribution) - ), - pallet_crowdloan::Error::::MaximumContributionTooLow - ); - }); -} - -#[test] -fn test_create_fails_if_end_is_in_the_past() { - let current_block_number: BlockNumberFor = 10; - - TestState::default() - .with_block_number(current_block_number) - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = current_block_number - 5; - - assert_err!( - Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - ), - pallet_crowdloan::Error::::CannotEndInPast - ); - }); -} - -#[test] -fn test_create_fails_if_block_duration_is_too_short() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 11; - - assert_err!( - Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - ), - pallet_crowdloan::Error::::BlockDurationTooShort - ); - }); -} - -#[test] -fn test_create_fails_if_block_duration_is_too_long() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 1000; - - assert_err!( - Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - ), - pallet_crowdloan::Error::::BlockDurationTooLong - ); - }); -} - -#[test] -fn test_create_fails_if_creator_has_insufficient_balance() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 200.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 50; - - assert_err!( - Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - ), - pallet_crowdloan::Error::::InsufficientBalance - ); - }); -} - -#[test] -fn test_contribute_succeeds() { - TestState::default() - .with_balance(U256::from(1), 200.into()) - .with_balance(U256::from(2), 500.into()) - .with_balance(U256::from(3), 200.into()) - .build_and_execute(|| { - // create a crowdloan - let creator: AccountOf = U256::from(1); - let initial_deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - initial_deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - )); - - // run some blocks - run_to_block(10); - - let crowdloan_id: CrowdloanId = 0; - - // only the creator has contributed so far - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.contributors_count == 1) - ); - - // first contribution to the crowdloan from creator - let amount: BalanceOf = 50.into(); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(creator), - crowdloan_id, - amount - )); - assert_eq!( - last_event(), - pallet_crowdloan::Event::::Contributed { - crowdloan_id, - contributor: creator, - amount, - } - .into() - ); - assert_eq!( - pallet_crowdloan::Contributions::::get(crowdloan_id, creator), - Some(100.into()) - ); - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.contributors_count == 1) - ); - assert_eq!( - Balances::free_balance(creator), - TaoBalance::from(200) - amount - initial_deposit - ); - - // second contribution to the crowdloan - let contributor1: AccountOf = U256::from(2); - let amount: BalanceOf = 100.into(); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor1), - crowdloan_id, - amount - )); - assert_eq!( - last_event(), - pallet_crowdloan::Event::::Contributed { - crowdloan_id, - contributor: contributor1, - amount, - } - .into() - ); - assert_eq!( - pallet_crowdloan::Contributions::::get(crowdloan_id, contributor1), - Some(100.into()) - ); - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.contributors_count == 2) - ); - assert_eq!( - Balances::free_balance(contributor1), - TaoBalance::from(500) - amount - ); - - // third contribution to the crowdloan - let contributor2: AccountOf = U256::from(3); - let amount: BalanceOf = 50.into(); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor2), - crowdloan_id, - amount - )); - assert_eq!( - last_event(), - pallet_crowdloan::Event::::Contributed { - crowdloan_id, - contributor: contributor2, - amount, - } - .into() - ); - assert_eq!( - pallet_crowdloan::Contributions::::get(crowdloan_id, contributor2), - Some(50.into()) - ); - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.contributors_count == 3) - ); - assert_eq!( - Balances::free_balance(contributor2), - TaoBalance::from(200) - amount - ); - - // ensure the contributions are present in the funds account - let funds_account = pallet_crowdloan::Pallet::::funds_account(crowdloan_id); - assert_eq!(Balances::free_balance(funds_account), 250.into()); - - // ensure the crowdloan raised amount is updated correctly - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.raised == 250.into()) - ); - }); -} - -#[test] -fn test_contribute_succeeds_if_contribution_will_make_the_raised_amount_exceed_the_cap() { - TestState::default() - .with_balance(U256::from(1), 200.into()) - .with_balance(U256::from(2), 500.into()) - .build_and_execute(|| { - // create a crowdloan - let creator: AccountOf = U256::from(1); - let initial_deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - initial_deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - )); - - // run some blocks - run_to_block(10); - - // first contribution to the crowdloan from creator - let crowdloan_id: CrowdloanId = 0; - let amount: BalanceOf = 50.into(); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(creator), - crowdloan_id, - amount - )); - assert_eq!( - last_event(), - pallet_crowdloan::Event::::Contributed { - crowdloan_id, - contributor: creator, - amount, - } - .into() - ); - assert_eq!( - pallet_crowdloan::Contributions::::get(crowdloan_id, creator), - Some(100.into()) - ); - assert_eq!( - Balances::free_balance(creator), - TaoBalance::from(200) - amount - initial_deposit - ); - - // second contribution to the crowdloan above the cap - let contributor1: AccountOf = U256::from(2); - let amount: BalanceOf = 300.into(); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor1), - crowdloan_id, - amount - )); - assert_eq!( - last_event(), - pallet_crowdloan::Event::::Contributed { - crowdloan_id, - contributor: contributor1, - amount: 200.into(), // the amount is capped at the cap - } - .into() - ); - assert_eq!( - pallet_crowdloan::Contributions::::get(crowdloan_id, contributor1), - Some(200.into()) - ); - assert_eq!(Balances::free_balance(contributor1), (500 - 200).into()); - - // ensure the contributions are present in the crowdloan account up to the cap - let funds_account = pallet_crowdloan::Pallet::::funds_account(crowdloan_id); - assert_eq!(Balances::free_balance(funds_account), 300.into()); - - // ensure the crowdloan raised amount is updated correctly - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.raised == 300.into()) - ); - }); -} - -#[test] -fn test_contribute_caps_amount_at_max_contribution() { - TestState::default() - .with_balance(U256::from(1), 200.into()) - .with_balance(U256::from(2), 500.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let initial_deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let max_contribution: BalanceOf = 120.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - initial_deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - )); - - run_to_block(10); - - let crowdloan_id: CrowdloanId = 0; - assert_ok!(Crowdloan::set_max_contribution( - RuntimeOrigin::signed(creator), - crowdloan_id, - Some(max_contribution) - )); - - let contributor: AccountOf = U256::from(2); - let amount: BalanceOf = 200.into(); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - crowdloan_id, - amount - )); - assert_eq!( - last_event(), - pallet_crowdloan::Event::::Contributed { - crowdloan_id, - contributor, - amount: max_contribution, - } - .into() - ); - assert_eq!( - pallet_crowdloan::Contributions::::get(crowdloan_id, contributor), - Some(max_contribution) - ); - assert_eq!( - Balances::free_balance(contributor), - TaoBalance::from(500) - max_contribution - ); - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.raised == initial_deposit + max_contribution) - ); - - assert_err!( - Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - crowdloan_id, - min_contribution - ), - pallet_crowdloan::Error::::MaxContributionReached - ); - }); -} - -#[test] -fn test_contribute_can_be_capped_below_minimum_when_filling_cap() { - TestState::default() - .with_balance(U256::from(1), 200.into()) - .with_balance(U256::from(2), 100.into()) - .with_balance(U256::from(3), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let initial_deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 115.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - initial_deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - )); - - run_to_block(10); - - let crowdloan_id: CrowdloanId = 0; - let first_contributor: AccountOf = U256::from(2); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(first_contributor), - crowdloan_id, - 60.into() - )); - - let final_contributor: AccountOf = U256::from(3); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(final_contributor), - crowdloan_id, - min_contribution - )); - - assert_eq!( - last_event(), - pallet_crowdloan::Event::::Contributed { - crowdloan_id, - contributor: final_contributor, - amount: 5.into(), - } - .into() - ); - assert_eq!( - pallet_crowdloan::Contributions::::get(crowdloan_id, final_contributor), - Some(5.into()) - ); - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.raised == cap) - ); - }); -} - -#[test] -fn test_contribute_can_be_capped_below_minimum_when_reaching_max_contribution() { - TestState::default() - .with_balance(U256::from(1), 200.into()) - .with_balance(U256::from(2), 500.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let initial_deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let max_contribution: BalanceOf = 105.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - initial_deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - )); - - run_to_block(10); - - let crowdloan_id: CrowdloanId = 0; - assert_ok!(Crowdloan::set_max_contribution( - RuntimeOrigin::signed(creator), - crowdloan_id, - Some(max_contribution) - )); - - let contributor: AccountOf = U256::from(2); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - crowdloan_id, - 100.into() - )); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - crowdloan_id, - min_contribution - )); - - assert_eq!( - last_event(), - pallet_crowdloan::Event::::Contributed { - crowdloan_id, - contributor, - amount: 5.into(), - } - .into() - ); - assert_eq!( - pallet_crowdloan::Contributions::::get(crowdloan_id, contributor), - Some(max_contribution) - ); - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.raised == initial_deposit + max_contribution) - ); - }); -} - -#[test] -fn test_contribute_fails_if_bad_origin() { - TestState::default().build_and_execute(|| { - let crowdloan_id: CrowdloanId = 0; - let amount: BalanceOf = 100.into(); - - assert_err!( - Crowdloan::contribute(RuntimeOrigin::none(), crowdloan_id, amount), - DispatchError::BadOrigin - ); - - assert_err!( - Crowdloan::contribute(RuntimeOrigin::root(), crowdloan_id, amount), - DispatchError::BadOrigin - ); - }); -} - -#[test] -fn test_contribute_fails_if_crowdloan_does_not_exist() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - let contributor: AccountOf = U256::from(1); - let crowdloan_id: CrowdloanId = 0; - let amount: BalanceOf = 20.into(); - - assert_err!( - Crowdloan::contribute(RuntimeOrigin::signed(contributor), crowdloan_id, amount), - pallet_crowdloan::Error::::InvalidCrowdloanId - ); - }); -} - -#[test] -fn test_contribute_fails_if_contribution_period_ended() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 100.into()) - .build_and_execute(|| { - // create a crowdloan - let creator: AccountOf = U256::from(1); - let initial_deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - initial_deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - )); - - // run past the end of the crowdloan - run_to_block(60); - - // contribute to the crowdloan - let contributor: AccountOf = U256::from(2); - let crowdloan_id: CrowdloanId = 0; - let amount: BalanceOf = 20.into(); - assert_err!( - Crowdloan::contribute(RuntimeOrigin::signed(contributor), crowdloan_id, amount), - pallet_crowdloan::Error::::ContributionPeriodEnded - ); - }); -} - -#[test] -fn test_contribute_fails_if_cap_has_been_raised() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 1000.into()) - .with_balance(U256::from(3), 100.into()) - .build_and_execute(|| { - // create a crowdloan - let creator: AccountOf = U256::from(1); - let initial_deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - initial_deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - )); - - // run some blocks - run_to_block(10); - - // first contribution to the crowdloan fully raise the cap - let crowdloan_id: CrowdloanId = 0; - let contributor1: AccountOf = U256::from(2); - let amount: BalanceOf = cap - initial_deposit; - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor1), - crowdloan_id, - amount - )); - - // second contribution to the crowdloan - let contributor2: AccountOf = U256::from(3); - let amount: BalanceOf = 10.into(); - assert_err!( - Crowdloan::contribute(RuntimeOrigin::signed(contributor2), crowdloan_id, amount), - pallet_crowdloan::Error::::CapRaised - ); - }); -} - -#[test] -fn test_contribute_fails_if_contribution_is_below_minimum_contribution() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 100.into()) - .build_and_execute(|| { - // create a crowdloan - let creator: AccountOf = U256::from(1); - let initial_deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - initial_deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - )); - - // run some blocks - run_to_block(10); - - // contribute to the crowdloan - let contributor: AccountOf = U256::from(2); - let crowdloan_id: CrowdloanId = 0; - let amount: BalanceOf = 5.into(); - assert_err!( - Crowdloan::contribute(RuntimeOrigin::signed(contributor), crowdloan_id, amount), - pallet_crowdloan::Error::::ContributionTooLow - ) - }); -} - -#[test] -fn test_contribute_fails_if_max_contributors_has_been_reached() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 100.into()) - .with_balance(U256::from(3), 100.into()) - .with_balance(U256::from(4), 100.into()) - .with_balance(U256::from(5), 100.into()) - .with_balance(U256::from(6), 100.into()) - .with_balance(U256::from(7), 100.into()) - .with_balance(U256::from(8), 100.into()) - .with_balance(U256::from(9), 100.into()) - .with_balance(U256::from(10), 100.into()) - .with_balance(U256::from(11), 100.into()) - .build_and_execute(|| { - // create a crowdloan - let creator: AccountOf = U256::from(1); - let initial_deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 1000.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - initial_deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - )); - - // run some blocks - run_to_block(10); - - // contribute to the crowdloan - let crowdloan_id: CrowdloanId = 0; - let amount: BalanceOf = 20.into(); - for i in 2..=10 { - let contributor: AccountOf = U256::from(i); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - crowdloan_id, - amount - )); - } - - // try to contribute - let contributor: AccountOf = U256::from(10); - assert_err!( - Crowdloan::contribute(RuntimeOrigin::signed(contributor), crowdloan_id, amount), - pallet_crowdloan::Error::::MaxContributorsReached - ); - }); -} - -#[test] -fn test_contribute_fails_if_contributor_has_insufficient_balance() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 50.into()) - .build_and_execute(|| { - // create a crowdloan - let creator: AccountOf = U256::from(1); - let initial_deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - initial_deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - )); - - // run some blocks - run_to_block(10); - - // contribute to the crowdloan - let crowdloan_id: CrowdloanId = 0; - let contributor: AccountOf = U256::from(2); - let amount: BalanceOf = 100.into(); - - assert_err!( - Crowdloan::contribute(RuntimeOrigin::signed(contributor), crowdloan_id, amount), - pallet_crowdloan::Error::::InsufficientBalance - ); - }); -} - -#[test] -fn test_withdraw_from_contributor_succeeds() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 100.into()) - .with_balance(U256::from(3), 100.into()) - .build_and_execute(|| { - // create a crowdloan - let creator: AccountOf = U256::from(1); - let initial_deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - initial_deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - )); - - // run some blocks - run_to_block(10); - - // contribute to the crowdloan - let crowdloan_id: CrowdloanId = 0; - - let contributor1: AccountOf = U256::from(2); - let amount1: BalanceOf = 100.into(); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor1), - crowdloan_id, - amount1 - )); - - let contributor2: AccountOf = U256::from(3); - let amount2: BalanceOf = 100.into(); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor2), - crowdloan_id, - amount2 - )); - - // run some more blocks past the end of the contribution period - run_to_block(60); - - // ensure the contributor count is correct - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.contributors_count == 3) - ); - - // withdraw from contributor1 - assert_ok!(Crowdloan::withdraw( - RuntimeOrigin::signed(contributor1), - crowdloan_id - )); - // ensure the contributor1 contribution has been removed - assert_eq!( - pallet_crowdloan::Contributions::::get(crowdloan_id, contributor1), - None, - ); - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.contributors_count == 2) - ); - // ensure the contributor1 has the correct amount - assert_eq!( - pallet_balances::Pallet::::free_balance(contributor1), - 100.into() - ); - - // withdraw from contributor2 - assert_ok!(Crowdloan::withdraw( - RuntimeOrigin::signed(contributor2), - crowdloan_id - )); - // ensure the contributor2 contribution has been removed - assert_eq!( - pallet_crowdloan::Contributions::::get(crowdloan_id, contributor2), - None, - ); - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.contributors_count == 1) - ); - // ensure the contributor2 has the correct amount - assert_eq!( - pallet_balances::Pallet::::free_balance(contributor2), - 100.into() - ); - - // ensure the crowdloan account has the correct amount - let funds_account = pallet_crowdloan::Pallet::::funds_account(crowdloan_id); - assert_eq!(Balances::free_balance(funds_account), initial_deposit); - // ensure the crowdloan raised amount is updated correctly - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.raised == initial_deposit) - ); - }); -} - -#[test] -fn test_withdraw_from_creator_with_contribution_over_deposit_succeeds() { - TestState::default() - .with_balance(U256::from(1), 200.into()) - .build_and_execute(|| { - // create a crowdloan - let creator: AccountOf = U256::from(1); - let initial_deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - initial_deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - )); - - // contribute to the crowdloan as the creator - let crowdloan_id: CrowdloanId = 0; - - let amount: BalanceOf = 100.into(); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(creator), - crowdloan_id, - amount - )); - - // ensure the contributor count is correct - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.contributors_count == 1) - ); - - // withdraw - let crowdloan_id: CrowdloanId = 0; - assert_ok!(Crowdloan::withdraw( - RuntimeOrigin::signed(creator), - crowdloan_id - )); - - // ensure the creator has the correct amount - assert_eq!( - pallet_balances::Pallet::::free_balance(creator), - TaoBalance::from(200) - initial_deposit - ); - // ensure the creator contribution has been removed - assert_eq!( - pallet_crowdloan::Contributions::::get(crowdloan_id, creator), - Some(initial_deposit), - ); - // ensure the contributor count hasn't changed because deposit is kept - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.contributors_count == 1) - ); - - // ensure the crowdloan account has the correct amount - let funds_account = pallet_crowdloan::Pallet::::funds_account(crowdloan_id); - assert_eq!(Balances::free_balance(funds_account), initial_deposit); - // ensure the crowdloan raised amount is updated correctly - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.raised == initial_deposit) - ); - }); -} -#[test] -fn test_withdraw_fails_from_creator_with_no_contribution_over_deposit() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 200.into()) - .build_and_execute(|| { - // create a crowdloan - let creator: AccountOf = U256::from(1); - let initial_deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - initial_deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - )); - - // try to withdraw - let crowdloan_id: CrowdloanId = 0; - assert_err!( - Crowdloan::withdraw(RuntimeOrigin::signed(creator), crowdloan_id), - pallet_crowdloan::Error::::DepositCannotBeWithdrawn - ); - - // ensure the crowdloan account has the correct amount - let funds_account = pallet_crowdloan::Pallet::::funds_account(crowdloan_id); - assert_eq!(Balances::free_balance(funds_account), initial_deposit); - // ensure the crowdloan raised amount is updated correctly - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.raised == initial_deposit) - ); - }); -} - -#[test] -fn test_withdraw_fails_if_bad_origin() { - TestState::default().build_and_execute(|| { - let crowdloan_id: CrowdloanId = 0; - - assert_err!( - Crowdloan::withdraw(RuntimeOrigin::none(), crowdloan_id), - DispatchError::BadOrigin - ); - - assert_err!( - Crowdloan::withdraw(RuntimeOrigin::root(), crowdloan_id), - DispatchError::BadOrigin - ); - }); -} - -#[test] -fn test_withdraw_fails_if_crowdloan_does_not_exists() { - TestState::default().build_and_execute(|| { - let contributor: AccountOf = U256::from(1); - let crowdloan_id: CrowdloanId = 0; - - assert_err!( - Crowdloan::withdraw(RuntimeOrigin::signed(contributor), crowdloan_id), - pallet_crowdloan::Error::::InvalidCrowdloanId - ); - }); -} - -#[test] -fn test_withdraw_fails_if_crowdloan_has_already_been_finalized() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 200.into()) - .build_and_execute(|| { - // create a crowdloan - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - // some contribution - let crowdloan_id: CrowdloanId = 0; - let contributor: AccountOf = U256::from(2); - let amount: BalanceOf = 50.into(); - - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - crowdloan_id, - amount - )); - - // run some more blocks past the end of the contribution period - run_to_block(60); - - // finalize the crowdloan - assert_ok!(Crowdloan::finalize( - RuntimeOrigin::signed(creator), - crowdloan_id - )); - - // try to withdraw - assert_err!( - Crowdloan::withdraw(RuntimeOrigin::signed(creator), crowdloan_id), - pallet_crowdloan::Error::::AlreadyFinalized - ); - }); -} - -#[test] -fn test_withdraw_fails_if_no_contribution_exists() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 200.into()) - .build_and_execute(|| { - // create a crowdloan - let creator: AccountOf = U256::from(1); - let initial_deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - initial_deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - )); - - // run some more blocks past the end of the contribution period - run_to_block(60); - - // try to withdraw - let crowdloan_id: CrowdloanId = 0; - let contributor: AccountOf = U256::from(2); - assert_err!( - Crowdloan::withdraw(RuntimeOrigin::signed(contributor), crowdloan_id), - pallet_crowdloan::Error::::NoContribution - ); - }); -} - -#[test] -fn test_finalize_succeeds() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 100.into()) - .build_and_execute(|| { - // create a crowdloan - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - let call = Box::new(RuntimeCall::TestPallet( - pallet_test::Call::::transfer_funds { - dest: U256::from(42), - }, - )); - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(call), - None - )); - - // run some blocks - run_to_block(10); - - // some contribution - let crowdloan_id: CrowdloanId = 0; - let contributor: AccountOf = U256::from(2); - let amount: BalanceOf = 50.into(); - - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - crowdloan_id, - amount - )); - - // finalize the crowdloan - assert_ok!(Crowdloan::finalize( - RuntimeOrigin::signed(creator), - crowdloan_id - )); - - // ensure the transfer was a success from the dispatched call - assert_eq!( - pallet_balances::Pallet::::free_balance(U256::from(42)), - 100.into() - ); - - // ensure the crowdloan is marked as finalized - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.finalized) - ); - - // ensure the event is emitted - assert_eq!( - last_event(), - pallet_crowdloan::Event::::Finalized { crowdloan_id }.into() - ); - - // ensure the current crowdloan id was accessible from the dispatched call - assert_eq!( - pallet_test::PassedCrowdloanId::::get(), - Some(crowdloan_id) - ); - }); -} - -#[test] -fn test_finalize_succeeds_with_target_address() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 100.into()) - .build_and_execute(|| { - // create a crowdloan - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - let target_address: AccountOf = U256::from(42); - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - None, - Some(target_address), - )); - - // run some blocks - run_to_block(10); - - // some contribution - let crowdloan_id: CrowdloanId = 0; - let contributor: AccountOf = U256::from(2); - let amount: BalanceOf = 50.into(); - - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - crowdloan_id, - amount - )); - - // run some more blocks past the end of the contribution period - run_to_block(60); - - // finalize the crowdloan - assert_ok!(Crowdloan::finalize( - RuntimeOrigin::signed(creator), - crowdloan_id - )); - - // ensure the target address has received the funds - assert_eq!( - pallet_balances::Pallet::::free_balance(target_address), - 100.into() - ); - - // ensure the crowdloan is marked as finalized - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.finalized) - ); - - // ensure the event is emitted - assert_eq!( - last_event(), - pallet_crowdloan::Event::::Finalized { crowdloan_id }.into() - ); - }) -} - -#[test] -fn test_finalize_fails_if_call_and_target_address_are_provided() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - run_to_block(10); - - let crowdloan_id: CrowdloanId = 0; - let contributor: AccountOf = U256::from(2); - let amount: BalanceOf = 50.into(); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - crowdloan_id, - amount - )); - - let target_address: AccountOf = U256::from(42); - pallet_crowdloan::Crowdloans::::mutate(crowdloan_id, |crowdloan| { - crowdloan.as_mut().unwrap().target_address = Some(target_address); - }); - - run_to_block(60); - - assert_err!( - Crowdloan::finalize(RuntimeOrigin::signed(creator), crowdloan_id), - pallet_crowdloan::Error::::InvalidFinalizationConfig - ); - - assert_eq!( - pallet_balances::Pallet::::free_balance(target_address), - 0.into() - ); - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| !c.finalized) - ); - }); -} - -#[test] -fn test_finalize_fails_if_call_and_target_address_are_missing() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - run_to_block(10); - - let crowdloan_id: CrowdloanId = 0; - let contributor: AccountOf = U256::from(2); - let amount: BalanceOf = 50.into(); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - crowdloan_id, - amount - )); - - pallet_crowdloan::Crowdloans::::mutate(crowdloan_id, |crowdloan| { - crowdloan.as_mut().unwrap().call = None; - }); - - run_to_block(60); - - assert_err!( - Crowdloan::finalize(RuntimeOrigin::signed(creator), crowdloan_id), - pallet_crowdloan::Error::::InvalidFinalizationConfig - ); - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| !c.finalized) - ); - }); -} - -#[test] -fn test_finalize_fails_if_bad_origin() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - let crowdloan_id: CrowdloanId = 0; - - assert_err!( - Crowdloan::finalize(RuntimeOrigin::none(), crowdloan_id), - DispatchError::BadOrigin - ); - - assert_err!( - Crowdloan::finalize(RuntimeOrigin::root(), crowdloan_id), - DispatchError::BadOrigin - ); - }); -} - -#[test] -fn test_finalize_fails_if_crowdloan_does_not_exist() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let crowdloan_id: CrowdloanId = 0; - - // try to finalize - assert_err!( - Crowdloan::finalize(RuntimeOrigin::signed(creator), crowdloan_id), - pallet_crowdloan::Error::::InvalidCrowdloanId - ); - }); -} - -#[test] -fn test_finalize_fails_if_not_creator_origin() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 100.into()) - .build_and_execute(|| { - // create a crowdloan - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None - )); - - // run some blocks - run_to_block(10); - - // some contribution - let crowdloan_id: CrowdloanId = 0; - let contributor: AccountOf = U256::from(2); - let amount: BalanceOf = 50.into(); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - crowdloan_id, - amount - )); - - // run some more blocks past the end of the contribution period - run_to_block(60); - - // try finalize the crowdloan - assert_err!( - Crowdloan::finalize(RuntimeOrigin::signed(contributor), crowdloan_id), - pallet_crowdloan::Error::::InvalidOrigin - ); - }); -} - -#[test] -fn test_finalize_fails_if_crowdloan_cap_is_not_raised() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 100.into()) - .build_and_execute(|| { - // create a crowdloan - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - // run some blocks - run_to_block(10); - - // some contribution - let crowdloan_id: CrowdloanId = 0; - let contributor: AccountOf = U256::from(2); - let amount: BalanceOf = 49.into(); // below cap - - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - crowdloan_id, - amount - )); - - // run some more blocks past the end of the contribution period - run_to_block(60); - - // try finalize the crowdloan - assert_err!( - Crowdloan::finalize(RuntimeOrigin::signed(creator), crowdloan_id), - pallet_crowdloan::Error::::CapNotRaised - ); - }); -} - -#[test] -fn test_finalize_fails_if_crowdloan_has_already_been_finalized() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 100.into()) - .build_and_execute(|| { - // create a crowdloan - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - // some contribution - let crowdloan_id: CrowdloanId = 0; - let contributor: AccountOf = U256::from(2); - let amount: BalanceOf = 50.into(); - - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - crowdloan_id, - amount - )); - - // run some more blocks past the end of the contribution period - run_to_block(60); - - // finalize the crowdloan - assert_ok!(Crowdloan::finalize( - RuntimeOrigin::signed(creator), - crowdloan_id - )); - - // try finalize the crowdloan a second time - assert_err!( - Crowdloan::finalize(RuntimeOrigin::signed(creator), crowdloan_id), - pallet_crowdloan::Error::::AlreadyFinalized - ); - }); -} - -#[test] -fn test_finalize_fails_if_call_fails() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 100.into()) - .build_and_execute(|| { - // create a crowdloan - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - let call = Box::new(RuntimeCall::TestPallet( - pallet_test::Call::::failing_extrinsic {}, - )); - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(call), - None, - )); - - // run some blocks - run_to_block(10); - - // some contribution - let crowdloan_id: CrowdloanId = 0; - let contributor: AccountOf = U256::from(2); - let amount: BalanceOf = 50.into(); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - crowdloan_id, - amount - )); - - // run some more blocks past the end of the contribution period - run_to_block(60); - - // try finalize the crowdloan - assert_err!( - Crowdloan::finalize(RuntimeOrigin::signed(creator), crowdloan_id), - pallet_test::Error::::ShouldFail - ); - }); -} - -#[test] -fn test_finalize_fails_if_another_finalize_is_in_progress() { - TestState::default() - .with_balance(U256::from(1), 300.into()) - .with_balance(U256::from(2), 300.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let contributor: AccountOf = U256::from(2); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - let first_crowdloan_id: CrowdloanId = 0; - let second_crowdloan_id: CrowdloanId = 1; - - let nested_finalize_call = Box::new(RuntimeCall::Crowdloan(pallet_crowdloan::Call::< - Test, - >::finalize { - crowdloan_id: second_crowdloan_id, - })); - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(nested_finalize_call), - None, - )); - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - run_to_block(10); - - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - first_crowdloan_id, - 50.into() - )); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - second_crowdloan_id, - 50.into() - )); - - run_to_block(60); - - assert_err!( - Crowdloan::finalize(RuntimeOrigin::signed(creator), first_crowdloan_id), - pallet_crowdloan::Error::::AlreadyFinalizing - ); - - assert_eq!(pallet_crowdloan::CurrentCrowdloanId::::get(), None); - assert!( - pallet_crowdloan::Crowdloans::::get(first_crowdloan_id) - .is_some_and(|c| !c.finalized) - ); - assert!( - pallet_crowdloan::Crowdloans::::get(second_crowdloan_id) - .is_some_and(|c| !c.finalized) - ); - }); -} - -// The finalize `call` cannot re-enter `withdraw` on the same crowdloan: it is rejected and -// the extrinsic reverts, so no funds move and `raised` stays consistent with the real balance. -#[test] -fn test_finalize_blocks_reentrant_withdraw() { - TestState::default() - .with_balance(U256::from(1), 200.into()) // creator - .with_balance(U256::from(2), 200.into()) // contributor - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let contributor: AccountOf = U256::from(2); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - let crowdloan_id: CrowdloanId = 0; - - // The finalize call re-enters `withdraw` on the same crowdloan. - let reentrant_call = Box::new(RuntimeCall::Crowdloan( - pallet_crowdloan::Call::::withdraw { crowdloan_id }, - )); - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(reentrant_call), - None, - )); - run_to_block(10); - - // Creator contributes 30 over the deposit (total 80); contributor fills the cap. - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(creator), - crowdloan_id, - 30.into() - )); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - crowdloan_id, - 20.into() - )); - - let funds_account = pallet_crowdloan::Pallet::::funds_account(crowdloan_id); - assert_eq!(Balances::free_balance(funds_account), cap); - let creator_balance_before = Balances::free_balance(creator); - - run_to_block(60); - - // Finalize dispatches the re-entrant withdraw, which is rejected with - // `AlreadyFinalized`. Wrap in a storage layer to model the per-extrinsic - // transaction the runtime applies in production, so the revert is observable. - let outcome = frame_support::storage::with_storage_layer(|| { - Crowdloan::finalize(RuntimeOrigin::signed(creator), crowdloan_id) - }); - assert_err!(outcome, pallet_crowdloan::Error::::AlreadyFinalized); - - // No funds were extracted and accounting is intact. - assert_eq!(Balances::free_balance(creator), creator_balance_before); - assert_eq!(Balances::free_balance(funds_account), cap); - assert_eq!(pallet_crowdloan::CurrentCrowdloanId::::get(), None); - let crowdloan = pallet_crowdloan::Crowdloans::::get(crowdloan_id).unwrap(); - assert!(!crowdloan.finalized); - assert_eq!(crowdloan.raised, cap); - - // Contributor funds are not frozen: the contributor can still withdraw. - assert_ok!(Crowdloan::withdraw( - RuntimeOrigin::signed(contributor), - crowdloan_id - )); - assert_eq!(Balances::free_balance(contributor), 200.into()); - }); -} - -// A re-entrant `refund` embedded as the finalize call is likewise rejected before moving funds. -#[test] -fn test_finalize_blocks_reentrant_refund() { - TestState::default() - .with_balance(U256::from(1), 200.into()) // creator - .with_balance(U256::from(2), 200.into()) // contributor - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let contributor: AccountOf = U256::from(2); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - let crowdloan_id: CrowdloanId = 0; - - let reentrant_call = Box::new(RuntimeCall::Crowdloan( - pallet_crowdloan::Call::::refund { crowdloan_id }, - )); - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(reentrant_call), - None, - )); - run_to_block(10); - - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(creator), - crowdloan_id, - 30.into() - )); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - crowdloan_id, - 20.into() - )); - - let funds_account = pallet_crowdloan::Pallet::::funds_account(crowdloan_id); - run_to_block(60); - - // The re-entrant refund hits the `finalized` guard before transferring anything. - assert_err!( - Crowdloan::finalize(RuntimeOrigin::signed(creator), crowdloan_id), - pallet_crowdloan::Error::::AlreadyFinalized - ); - assert_eq!(Balances::free_balance(funds_account), cap); - }); -} - -#[test] -fn test_refund_succeeds() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 100.into()) - .with_balance(U256::from(3), 100.into()) - .with_balance(U256::from(4), 100.into()) - .with_balance(U256::from(5), 100.into()) - .with_balance(U256::from(6), 100.into()) - .with_balance(U256::from(7), 100.into()) - .build_and_execute(|| { - // create a crowdloan - let creator: AccountOf = U256::from(1); - let initial_deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 400.into(); - let end: BlockNumberFor = 50; - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - initial_deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - // run some blocks - run_to_block(10); - - // make 6 contributions to reach 350 raised amount (initial deposit + contributions) - let crowdloan_id: CrowdloanId = 0; - let amount: BalanceOf = 50.into(); - for i in 2..8 { - let contributor: AccountOf = U256::from(i); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - crowdloan_id, - amount - )); - } - - // ensure the contributor count is correct - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.contributors_count == 7) - ); - - // run some more blocks before the end of the contribution period - run_to_block(20); - - // first round of refund - assert_ok!(Crowdloan::refund( - RuntimeOrigin::signed(creator), - crowdloan_id - )); - - // ensure the contributor count is correct, we processed 5 refunds - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.contributors_count == 2) - ); - - // ensure the crowdloan account has the correct amount - let funds_account = pallet_crowdloan::Pallet::::funds_account(crowdloan_id); - assert_eq!( - Balances::free_balance(funds_account), - TaoBalance::from(350) - TaoBalance::from(5) * amount - ); - // ensure raised amount is updated correctly - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id).is_some_and( - |c| c.raised == TaoBalance::from(350) - TaoBalance::from(5) * amount - ) - ); - // ensure the event is emitted - assert_eq!( - last_event(), - pallet_crowdloan::Event::::PartiallyRefunded { crowdloan_id }.into() - ); - - // run some more blocks past the end of the contribution period - run_to_block(70); - - // second round of refund - assert_ok!(Crowdloan::refund( - RuntimeOrigin::signed(creator), - crowdloan_id - )); - - // ensure the contributor count is correct, we processed 1 more refund - // keeping deposit - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.contributors_count == 1) - ); - - // ensure the crowdloan account has the correct amount - assert_eq!( - pallet_balances::Pallet::::free_balance(funds_account), - initial_deposit - ); - // ensure the raised amount is updated correctly - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.raised == initial_deposit) - ); - - // ensure creator has the correct amount - assert_eq!( - pallet_balances::Pallet::::free_balance(creator), - initial_deposit - ); - - // ensure each contributor has been refunded and removed from the crowdloan - for i in 2..8 { - let contributor: AccountOf = U256::from(i); - assert_eq!( - pallet_balances::Pallet::::free_balance(contributor), - 100.into() - ); - assert_eq!( - pallet_crowdloan::Contributions::::get(crowdloan_id, contributor), - None, - ); - } - - // ensure the event is emitted - assert_eq!( - last_event(), - pallet_crowdloan::Event::::AllRefunded { crowdloan_id }.into() - ); - }) -} - -#[test] -fn test_refund_fails_if_bad_or_invalid_origin() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - // create a crowdloan - let crowdloan_id: CrowdloanId = 0; - let creator: AccountOf = U256::from(1); - let initial_deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 50; - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - initial_deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - assert_err!( - Crowdloan::refund(RuntimeOrigin::none(), crowdloan_id), - DispatchError::BadOrigin - ); - - assert_err!( - Crowdloan::refund(RuntimeOrigin::root(), crowdloan_id), - DispatchError::BadOrigin - ); - - // run some blocks - run_to_block(60); - - // try to refund - let unknown_contributor: AccountOf = U256::from(2); - assert_err!( - Crowdloan::refund(RuntimeOrigin::signed(unknown_contributor), crowdloan_id), - pallet_crowdloan::Error::::InvalidOrigin, - ); - }); -} - -#[test] -fn test_refund_fails_if_crowdloan_does_not_exist() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let crowdloan_id: CrowdloanId = 0; - - assert_err!( - Crowdloan::refund(RuntimeOrigin::signed(creator), crowdloan_id), - pallet_crowdloan::Error::::InvalidCrowdloanId - ); - }); -} - -#[test] -fn test_dissolve_succeeds() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - // create a crowdloan - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - let crowdloan_id: CrowdloanId = 0; - assert_ok!(Crowdloan::set_max_contribution( - RuntimeOrigin::signed(creator), - crowdloan_id, - Some(cap) - )); - - // run some blocks past end - run_to_block(60); - - // ensure the contributor count is correct - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.contributors_count == 1) - ); - - // dissolve the crowdloan - assert_ok!(Crowdloan::dissolve( - RuntimeOrigin::signed(creator), - crowdloan_id - )); - - // ensure the crowdloan is removed from the crowdloans map - assert!(pallet_crowdloan::Crowdloans::::get(crowdloan_id).is_none()); - - // ensure the contributions are removed - assert!(!pallet_crowdloan::Contributions::::contains_prefix( - crowdloan_id - )); - - // ensure the maximum contribution is removed - assert!(pallet_crowdloan::MaxContributions::::get(crowdloan_id).is_none()); - - // ensure the event is emitted - assert_eq!( - last_event(), - pallet_crowdloan::Event::::Dissolved { crowdloan_id }.into() - ) - }); -} - -#[test] -fn test_dissolve_fails_if_bad_origin() { - TestState::default().build_and_execute(|| { - let crowdloan_id: CrowdloanId = 0; - - assert_err!( - Crowdloan::dissolve(RuntimeOrigin::none(), crowdloan_id), - DispatchError::BadOrigin - ); - - assert_err!( - Crowdloan::dissolve(RuntimeOrigin::root(), crowdloan_id), - DispatchError::BadOrigin - ); - }); -} - -#[test] -fn test_dissolve_fails_if_crowdloan_does_not_exist() { - TestState::default().build_and_execute(|| { - let crowdloan_id: CrowdloanId = 0; - assert_err!( - Crowdloan::dissolve(RuntimeOrigin::signed(U256::from(1)), crowdloan_id), - pallet_crowdloan::Error::::InvalidCrowdloanId - ); - }); -} - -#[test] -fn test_dissolve_fails_if_crowdloan_has_been_finalized() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - // run some blocks - run_to_block(10); - - // some contribution - let crowdloan_id: CrowdloanId = 0; - let contributor: AccountOf = U256::from(2); - let amount: BalanceOf = 50.into(); - - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - crowdloan_id, - amount - )); - - // run some more blocks past the end of the contribution period - run_to_block(60); - - // finalize the crowdloan - assert_ok!(Crowdloan::finalize( - RuntimeOrigin::signed(creator), - crowdloan_id - )); - - // try dissolve the crowdloan - assert_err!( - Crowdloan::dissolve(RuntimeOrigin::signed(creator), crowdloan_id), - pallet_crowdloan::Error::::AlreadyFinalized - ); - }); -} - -#[test] -fn test_dissolve_fails_if_origin_is_not_creator() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - // run some blocks - run_to_block(10); - - // some contribution - let crowdloan_id: CrowdloanId = 0; - - // try dissolve the crowdloan - assert_err!( - Crowdloan::dissolve(RuntimeOrigin::signed(U256::from(2)), crowdloan_id), - pallet_crowdloan::Error::::InvalidOrigin - ); - }); -} - -#[test] -fn test_dissolve_fails_if_not_everyone_has_been_refunded() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 100.into()) - .build_and_execute(|| { - // create a crowdloan - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - // run some blocks - run_to_block(10); - - // some contribution - let crowdloan_id: CrowdloanId = 0; - let contributor: AccountOf = U256::from(2); - let amount: BalanceOf = 50.into(); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - crowdloan_id, - amount - )); - - // run some blocks - run_to_block(10); - - // try to dissolve the crowdloan - let crowdloan_id = 0; - assert_err!( - Crowdloan::dissolve(RuntimeOrigin::signed(creator), crowdloan_id), - pallet_crowdloan::Error::::NotReadyToDissolve - ); - }); -} - -#[test] -fn test_update_min_contribution_succeeds() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - // create a crowdloan - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - let crowdloan_id: CrowdloanId = 0; - let new_min_contribution: BalanceOf = 20.into(); - - // update the min contribution - assert_ok!(Crowdloan::update_min_contribution( - RuntimeOrigin::signed(creator), - crowdloan_id, - new_min_contribution - )); - - // ensure the min contribution is updated - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.min_contribution == new_min_contribution) - ); - // ensure the event is emitted - assert_eq!( - last_event(), - pallet_crowdloan::Event::::MinContributionUpdated { - crowdloan_id, - new_min_contribution - } - .into() - ); - }); -} - -#[test] -fn test_update_min_contribution_fails_if_bad_origin() { - TestState::default().build_and_execute(|| { - let crowdloan_id: CrowdloanId = 0; - - assert_err!( - Crowdloan::update_min_contribution(RuntimeOrigin::none(), crowdloan_id, 20.into()), - DispatchError::BadOrigin - ); - - assert_err!( - Crowdloan::update_min_contribution(RuntimeOrigin::root(), crowdloan_id, 20.into()), - DispatchError::BadOrigin - ); - }); -} - -#[test] -fn test_update_min_contribution_fails_if_crowdloan_does_not_exist() { - TestState::default().build_and_execute(|| { - let crowdloan_id: CrowdloanId = 0; - - assert_err!( - Crowdloan::update_min_contribution( - RuntimeOrigin::signed(U256::from(1)), - crowdloan_id, - 20.into() - ), - pallet_crowdloan::Error::::InvalidCrowdloanId - ); - }); -} - -#[test] -fn test_update_min_contribution_fails_if_crowdloan_has_been_finalized() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - // some contribution - let crowdloan_id: CrowdloanId = 0; - let contributor: AccountOf = U256::from(2); - let amount: BalanceOf = 50.into(); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - crowdloan_id, - amount - )); - - // run some blocks - run_to_block(50); - - // finalize the crowdloan - let crowdloan_id: CrowdloanId = 0; - assert_ok!(Crowdloan::finalize( - RuntimeOrigin::signed(creator), - crowdloan_id - )); - - // try update the min contribution - let new_min_contribution: BalanceOf = 20.into(); - assert_err!( - Crowdloan::update_min_contribution( - RuntimeOrigin::signed(creator), - crowdloan_id, - new_min_contribution - ), - pallet_crowdloan::Error::::AlreadyFinalized - ); - }); -} - -#[test] -fn test_update_min_contribution_fails_if_not_creator() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - let crowdloan_id: CrowdloanId = 0; - let new_min_contribution: BalanceOf = 20.into(); - - // try update the min contribution - assert_err!( - Crowdloan::update_min_contribution( - RuntimeOrigin::signed(U256::from(2)), - crowdloan_id, - new_min_contribution - ), - pallet_crowdloan::Error::::InvalidOrigin - ); - }); -} - -#[test] -fn test_update_min_contribution_fails_if_new_min_contribution_is_too_low() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - let crowdloan_id: CrowdloanId = 0; - let new_min_contribution: BalanceOf = 9.into(); - - // try update the min contribution - assert_err!( - Crowdloan::update_min_contribution( - RuntimeOrigin::signed(creator), - crowdloan_id, - new_min_contribution - ), - pallet_crowdloan::Error::::MinimumContributionTooLow - ); - }); -} - -#[test] -fn test_update_min_contribution_fails_if_new_min_contribution_exceeds_max_contribution() { - TestState::default() - .with_balance(U256::from(1), 200.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let max_contribution: BalanceOf = 60.into(); - let cap: BalanceOf = 300.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - let crowdloan_id: CrowdloanId = 0; - assert_ok!(Crowdloan::set_max_contribution( - RuntimeOrigin::signed(creator), - crowdloan_id, - Some(max_contribution) - )); - - assert_err!( - Crowdloan::update_min_contribution( - RuntimeOrigin::signed(creator), - crowdloan_id, - 70.into() - ), - pallet_crowdloan::Error::::MinimumContributionTooHigh - ); - }); -} - -#[test] -fn test_update_end_succeeds() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - let crowdloan_id: CrowdloanId = 0; - let new_end: BlockNumberFor = 60; - - // update the end - assert_ok!(Crowdloan::update_end( - RuntimeOrigin::signed(creator), - crowdloan_id, - new_end - )); - - // ensure the end is updated - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.end == new_end) - ); - // ensure the event is emitted - assert_eq!( - last_event(), - pallet_crowdloan::Event::::EndUpdated { - crowdloan_id, - new_end - } - .into() - ); - }); -} - -#[test] -fn test_update_end_fails_if_bad_origin() { - TestState::default().build_and_execute(|| { - let crowdloan_id: CrowdloanId = 0; - - assert_err!( - Crowdloan::update_end(RuntimeOrigin::none(), crowdloan_id, 60), - DispatchError::BadOrigin - ); - - assert_err!( - Crowdloan::update_end(RuntimeOrigin::root(), crowdloan_id, 60), - DispatchError::BadOrigin - ); - }); -} - -#[test] -fn test_update_end_fails_if_crowdloan_does_not_exist() { - TestState::default().build_and_execute(|| { - let crowdloan_id: CrowdloanId = 0; - - assert_err!( - Crowdloan::update_end(RuntimeOrigin::signed(U256::from(1)), crowdloan_id, 60), - pallet_crowdloan::Error::::InvalidCrowdloanId - ); - }); -} - -#[test] -fn test_update_end_fails_if_crowdloan_has_been_finalized() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - let crowdloan_id: CrowdloanId = 0; - - // some contribution - let contributor: AccountOf = U256::from(2); - let amount: BalanceOf = 50.into(); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - crowdloan_id, - amount - )); - - // run some blocks - run_to_block(60); - - // finalize the crowdloan - assert_ok!(Crowdloan::finalize( - RuntimeOrigin::signed(creator), - crowdloan_id - )); - - // try update the end - let new_end: BlockNumberFor = 60; - assert_err!( - Crowdloan::update_end(RuntimeOrigin::signed(creator), crowdloan_id, new_end), - pallet_crowdloan::Error::::AlreadyFinalized - ); - }); -} - -#[test] -fn test_update_end_fails_if_not_creator() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - let crowdloan_id: CrowdloanId = 0; - let new_end: BlockNumberFor = 60; - - // try update the end - assert_err!( - Crowdloan::update_end(RuntimeOrigin::signed(U256::from(2)), crowdloan_id, new_end), - pallet_crowdloan::Error::::InvalidOrigin - ); - }); -} - -#[test] -fn test_update_end_fails_if_new_end_is_in_past() { - TestState::default() - .with_block_number(50) - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 100; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - let crowdloan_id: CrowdloanId = 0; - let new_end: BlockNumberFor = 40; - - // try update the end to a past block number - assert_err!( - Crowdloan::update_end(RuntimeOrigin::signed(creator), crowdloan_id, new_end), - pallet_crowdloan::Error::::CannotEndInPast - ); - }); -} - -#[test] -fn test_update_end_fails_if_block_duration_is_too_short() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - // run some blocks - run_to_block(50); - - let crowdloan_id: CrowdloanId = 0; - let new_end: BlockNumberFor = 51; - - // try update the end to a block number that is too long - assert_err!( - Crowdloan::update_end(RuntimeOrigin::signed(creator), crowdloan_id, new_end), - pallet_crowdloan::Error::::BlockDurationTooShort - ); - }); -} - -#[test] -fn test_update_end_fails_if_block_duration_is_too_long() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - let crowdloan_id: CrowdloanId = 0; - let new_end: BlockNumberFor = 1000; - - // try update the end to a block number that is too long - assert_err!( - Crowdloan::update_end(RuntimeOrigin::signed(creator), crowdloan_id, new_end), - pallet_crowdloan::Error::::BlockDurationTooLong - ); - }); -} - -#[test] -fn test_update_cap_succeeds() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - // try update the cap - let crowdloan_id: CrowdloanId = 0; - let new_cap: BalanceOf = 200.into(); - assert_ok!(Crowdloan::update_cap( - RuntimeOrigin::signed(creator), - crowdloan_id, - new_cap - )); - - // ensure the cap is updated - assert!( - pallet_crowdloan::Crowdloans::::get(crowdloan_id) - .is_some_and(|c| c.cap == new_cap) - ); - // ensure the event is emitted - assert_eq!( - last_event(), - pallet_crowdloan::Event::::CapUpdated { - crowdloan_id, - new_cap - } - .into() - ); - }); -} - -#[test] -fn test_update_cap_fails_if_bad_origin() { - TestState::default().build_and_execute(|| { - let crowdloan_id: CrowdloanId = 0; - - assert_err!( - Crowdloan::update_cap(RuntimeOrigin::none(), crowdloan_id, 200.into()), - DispatchError::BadOrigin - ); - - assert_err!( - Crowdloan::update_cap(RuntimeOrigin::root(), crowdloan_id, 200.into()), - DispatchError::BadOrigin - ); - }); -} - -#[test] -fn test_update_cap_fails_if_crowdloan_does_not_exist() { - TestState::default().build_and_execute(|| { - let crowdloan_id: CrowdloanId = 0; - - assert_err!( - Crowdloan::update_cap( - RuntimeOrigin::signed(U256::from(1)), - crowdloan_id, - 200.into() - ), - pallet_crowdloan::Error::::InvalidCrowdloanId - ); - }); -} - -#[test] -fn test_update_cap_fails_if_crowdloan_has_been_finalized() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - // some contribution - let crowdloan_id: CrowdloanId = 0; - let contributor: AccountOf = U256::from(2); - let amount: BalanceOf = 50.into(); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(contributor), - crowdloan_id, - amount - )); - - // run some blocks - run_to_block(60); - - // finalize the crowdloan - let crowdloan_id: CrowdloanId = 0; - assert_ok!(Crowdloan::finalize( - RuntimeOrigin::signed(creator), - crowdloan_id - )); - - // try update the cap - let new_cap: BalanceOf = 200.into(); - assert_err!( - Crowdloan::update_cap(RuntimeOrigin::signed(creator), crowdloan_id, new_cap), - pallet_crowdloan::Error::::AlreadyFinalized - ); - }); -} - -#[test] -fn test_update_cap_fails_if_not_creator() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .with_balance(U256::from(2), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - // try update the cap - let crowdloan_id: CrowdloanId = 0; - let new_cap: BalanceOf = 200.into(); - assert_err!( - Crowdloan::update_cap(RuntimeOrigin::signed(U256::from(2)), crowdloan_id, new_cap), - pallet_crowdloan::Error::::InvalidOrigin - ); - }); -} - -#[test] -fn test_update_cap_fails_if_new_cap_is_too_low() { - TestState::default() - .with_balance(U256::from(1), 100.into()) - .build_and_execute(|| { - let creator: AccountOf = U256::from(1); - let deposit: BalanceOf = 50.into(); - let min_contribution: BalanceOf = 10.into(); - let cap: BalanceOf = 100.into(); - let end: BlockNumberFor = 50; - - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(creator), - deposit, - min_contribution, - cap, - end, - Some(noop_call()), - None, - )); - - // try update the cap - let crowdloan_id: CrowdloanId = 0; - let new_cap: BalanceOf = 49.into(); - assert_err!( - Crowdloan::update_cap(RuntimeOrigin::signed(creator), crowdloan_id, new_cap), - pallet_crowdloan::Error::::CapTooLow - ); - }); -} diff --git a/pallets/crowdloan/src/tests/contribute.rs b/pallets/crowdloan/src/tests/contribute.rs new file mode 100644 index 0000000000..cf5eb7ef08 --- /dev/null +++ b/pallets/crowdloan/src/tests/contribute.rs @@ -0,0 +1,679 @@ +#![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] + +use frame_support::{assert_err, assert_ok}; +use frame_system::pallet_prelude::BlockNumberFor; +use sp_core::U256; +use sp_runtime::DispatchError; +use subtensor_runtime_common::TaoBalance; + +use crate::{BalanceOf, CrowdloanId, mock::*, pallet as pallet_crowdloan}; + +#[test] +fn test_contribute_succeeds() { + TestState::default() + .with_balance(U256::from(1), 200.into()) + .with_balance(U256::from(2), 500.into()) + .with_balance(U256::from(3), 200.into()) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + // run some blocks + run_to_block(10); + + let crowdloan_id: CrowdloanId = 0; + + // only the creator has contributed so far + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.contributors_count == 1) + ); + + // first contribution to the crowdloan from creator + let amount: BalanceOf = 50.into(); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(creator), + crowdloan_id, + amount + )); + assert_eq!( + last_event(), + pallet_crowdloan::Event::::Contributed { + crowdloan_id, + contributor: creator, + amount, + } + .into() + ); + assert_eq!( + pallet_crowdloan::Contributions::::get(crowdloan_id, creator), + Some(100.into()) + ); + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.contributors_count == 1) + ); + assert_eq!( + Balances::free_balance(creator), + TaoBalance::from(200) - amount - initial_deposit + ); + + // second contribution to the crowdloan + let contributor1: AccountOf = U256::from(2); + let amount: BalanceOf = 100.into(); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor1), + crowdloan_id, + amount + )); + assert_eq!( + last_event(), + pallet_crowdloan::Event::::Contributed { + crowdloan_id, + contributor: contributor1, + amount, + } + .into() + ); + assert_eq!( + pallet_crowdloan::Contributions::::get(crowdloan_id, contributor1), + Some(100.into()) + ); + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.contributors_count == 2) + ); + assert_eq!( + Balances::free_balance(contributor1), + TaoBalance::from(500) - amount + ); + + // third contribution to the crowdloan + let contributor2: AccountOf = U256::from(3); + let amount: BalanceOf = 50.into(); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor2), + crowdloan_id, + amount + )); + assert_eq!( + last_event(), + pallet_crowdloan::Event::::Contributed { + crowdloan_id, + contributor: contributor2, + amount, + } + .into() + ); + assert_eq!( + pallet_crowdloan::Contributions::::get(crowdloan_id, contributor2), + Some(50.into()) + ); + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.contributors_count == 3) + ); + assert_eq!( + Balances::free_balance(contributor2), + TaoBalance::from(200) - amount + ); + + // ensure the contributions are present in the funds account + let funds_account = + pallet_crowdloan::Pallet::::crowdloan_funds_account(crowdloan_id); + assert_eq!(Balances::free_balance(funds_account), 250.into()); + + // ensure the crowdloan raised amount is updated correctly + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.raised == 250.into()) + ); + }); +} + +#[test] +fn test_contribute_succeeds_if_contribution_will_make_the_raised_amount_exceed_the_cap() { + TestState::default() + .with_balance(U256::from(1), 200.into()) + .with_balance(U256::from(2), 500.into()) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + // run some blocks + run_to_block(10); + + // first contribution to the crowdloan from creator + let crowdloan_id: CrowdloanId = 0; + let amount: BalanceOf = 50.into(); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(creator), + crowdloan_id, + amount + )); + assert_eq!( + last_event(), + pallet_crowdloan::Event::::Contributed { + crowdloan_id, + contributor: creator, + amount, + } + .into() + ); + assert_eq!( + pallet_crowdloan::Contributions::::get(crowdloan_id, creator), + Some(100.into()) + ); + assert_eq!( + Balances::free_balance(creator), + TaoBalance::from(200) - amount - initial_deposit + ); + + // second contribution to the crowdloan above the cap + let contributor1: AccountOf = U256::from(2); + let amount: BalanceOf = 300.into(); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor1), + crowdloan_id, + amount + )); + assert_eq!( + last_event(), + pallet_crowdloan::Event::::Contributed { + crowdloan_id, + contributor: contributor1, + amount: 200.into(), // the amount is capped at the cap + } + .into() + ); + assert_eq!( + pallet_crowdloan::Contributions::::get(crowdloan_id, contributor1), + Some(200.into()) + ); + assert_eq!(Balances::free_balance(contributor1), (500 - 200).into()); + + // ensure the contributions are present in the crowdloan account up to the cap + let funds_account = + pallet_crowdloan::Pallet::::crowdloan_funds_account(crowdloan_id); + assert_eq!(Balances::free_balance(funds_account), 300.into()); + + // ensure the crowdloan raised amount is updated correctly + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.raised == 300.into()) + ); + }); +} + +#[test] +fn test_contribute_caps_amount_at_max_contribution() { + TestState::default() + .with_balance(U256::from(1), 200.into()) + .with_balance(U256::from(2), 500.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let max_contribution: BalanceOf = 120.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + run_to_block(10); + + let crowdloan_id: CrowdloanId = 0; + assert_ok!(Crowdloan::set_max_contribution( + RuntimeOrigin::signed(creator), + crowdloan_id, + Some(max_contribution) + )); + + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 200.into(); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + assert_eq!( + last_event(), + pallet_crowdloan::Event::::Contributed { + crowdloan_id, + contributor, + amount: max_contribution, + } + .into() + ); + assert_eq!( + pallet_crowdloan::Contributions::::get(crowdloan_id, contributor), + Some(max_contribution) + ); + assert_eq!( + Balances::free_balance(contributor), + TaoBalance::from(500) - max_contribution + ); + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.raised == initial_deposit + max_contribution) + ); + + assert_err!( + Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + min_contribution + ), + pallet_crowdloan::Error::::MaxContributionReached + ); + }); +} + +#[test] +fn test_contribute_can_be_capped_below_minimum_when_filling_cap() { + TestState::default() + .with_balance(U256::from(1), 200.into()) + .with_balance(U256::from(2), 100.into()) + .with_balance(U256::from(3), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 115.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + run_to_block(10); + + let crowdloan_id: CrowdloanId = 0; + let first_contributor: AccountOf = U256::from(2); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(first_contributor), + crowdloan_id, + 60.into() + )); + + let final_contributor: AccountOf = U256::from(3); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(final_contributor), + crowdloan_id, + min_contribution + )); + + assert_eq!( + last_event(), + pallet_crowdloan::Event::::Contributed { + crowdloan_id, + contributor: final_contributor, + amount: 5.into(), + } + .into() + ); + assert_eq!( + pallet_crowdloan::Contributions::::get(crowdloan_id, final_contributor), + Some(5.into()) + ); + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.raised == cap) + ); + }); +} + +#[test] +fn test_contribute_can_be_capped_below_minimum_when_reaching_max_contribution() { + TestState::default() + .with_balance(U256::from(1), 200.into()) + .with_balance(U256::from(2), 500.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let max_contribution: BalanceOf = 105.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + run_to_block(10); + + let crowdloan_id: CrowdloanId = 0; + assert_ok!(Crowdloan::set_max_contribution( + RuntimeOrigin::signed(creator), + crowdloan_id, + Some(max_contribution) + )); + + let contributor: AccountOf = U256::from(2); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + 100.into() + )); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + min_contribution + )); + + assert_eq!( + last_event(), + pallet_crowdloan::Event::::Contributed { + crowdloan_id, + contributor, + amount: 5.into(), + } + .into() + ); + assert_eq!( + pallet_crowdloan::Contributions::::get(crowdloan_id, contributor), + Some(max_contribution) + ); + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.raised == initial_deposit + max_contribution) + ); + }); +} + +#[test] +fn test_contribute_fails_if_bad_origin() { + TestState::default().build_and_execute(|| { + let crowdloan_id: CrowdloanId = 0; + let amount: BalanceOf = 100.into(); + + assert_err!( + Crowdloan::contribute(RuntimeOrigin::none(), crowdloan_id, amount), + DispatchError::BadOrigin + ); + + assert_err!( + Crowdloan::contribute(RuntimeOrigin::root(), crowdloan_id, amount), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn test_contribute_fails_if_crowdloan_does_not_exist() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + let contributor: AccountOf = U256::from(1); + let crowdloan_id: CrowdloanId = 0; + let amount: BalanceOf = 20.into(); + + assert_err!( + Crowdloan::contribute(RuntimeOrigin::signed(contributor), crowdloan_id, amount), + pallet_crowdloan::Error::::InvalidCrowdloanId + ); + }); +} + +#[test] +fn test_contribute_fails_if_contribution_period_ended() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 100.into()) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + // run past the end of the crowdloan + run_to_block(60); + + // contribute to the crowdloan + let contributor: AccountOf = U256::from(2); + let crowdloan_id: CrowdloanId = 0; + let amount: BalanceOf = 20.into(); + assert_err!( + Crowdloan::contribute(RuntimeOrigin::signed(contributor), crowdloan_id, amount), + pallet_crowdloan::Error::::ContributionPeriodEnded + ); + }); +} + +#[test] +fn test_contribute_fails_if_cap_has_been_raised() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 1000.into()) + .with_balance(U256::from(3), 100.into()) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + // run some blocks + run_to_block(10); + + // first contribution to the crowdloan fully raise the cap + let crowdloan_id: CrowdloanId = 0; + let contributor1: AccountOf = U256::from(2); + let amount: BalanceOf = cap - initial_deposit; + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor1), + crowdloan_id, + amount + )); + + // second contribution to the crowdloan + let contributor2: AccountOf = U256::from(3); + let amount: BalanceOf = 10.into(); + assert_err!( + Crowdloan::contribute(RuntimeOrigin::signed(contributor2), crowdloan_id, amount), + pallet_crowdloan::Error::::CapRaised + ); + }); +} + +#[test] +fn test_contribute_fails_if_contribution_is_below_minimum_contribution() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 100.into()) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + // run some blocks + run_to_block(10); + + // contribute to the crowdloan + let contributor: AccountOf = U256::from(2); + let crowdloan_id: CrowdloanId = 0; + let amount: BalanceOf = 5.into(); + assert_err!( + Crowdloan::contribute(RuntimeOrigin::signed(contributor), crowdloan_id, amount), + pallet_crowdloan::Error::::ContributionTooLow + ) + }); +} + +#[test] +fn test_contribute_fails_if_max_contributors_has_been_reached() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 100.into()) + .with_balance(U256::from(3), 100.into()) + .with_balance(U256::from(4), 100.into()) + .with_balance(U256::from(5), 100.into()) + .with_balance(U256::from(6), 100.into()) + .with_balance(U256::from(7), 100.into()) + .with_balance(U256::from(8), 100.into()) + .with_balance(U256::from(9), 100.into()) + .with_balance(U256::from(10), 100.into()) + .with_balance(U256::from(11), 100.into()) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 1000.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + // run some blocks + run_to_block(10); + + // contribute to the crowdloan + let crowdloan_id: CrowdloanId = 0; + let amount: BalanceOf = 20.into(); + for i in 2..=10 { + let contributor: AccountOf = U256::from(i); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + } + + // try to contribute + let contributor: AccountOf = U256::from(10); + assert_err!( + Crowdloan::contribute(RuntimeOrigin::signed(contributor), crowdloan_id, amount), + pallet_crowdloan::Error::::MaxContributorsReached + ); + }); +} + +#[test] +fn test_contribute_fails_if_contributor_has_insufficient_balance() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 50.into()) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + // run some blocks + run_to_block(10); + + // contribute to the crowdloan + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 100.into(); + + assert_err!( + Crowdloan::contribute(RuntimeOrigin::signed(contributor), crowdloan_id, amount), + pallet_crowdloan::Error::::InsufficientBalance + ); + }); +} diff --git a/pallets/crowdloan/src/tests/create.rs b/pallets/crowdloan/src/tests/create.rs new file mode 100644 index 0000000000..7134edb787 --- /dev/null +++ b/pallets/crowdloan/src/tests/create.rs @@ -0,0 +1,362 @@ +#![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] + +use frame_support::{assert_err, assert_ok, traits::StorePreimage}; +use frame_system::pallet_prelude::BlockNumberFor; +use sp_core::U256; +use sp_runtime::DispatchError; +use subtensor_runtime_common::TaoBalance; + +use crate::{BalanceOf, CrowdloanInfo, mock::*, pallet as pallet_crowdloan}; + +#[test] +fn test_create_succeeds() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + let crowdloan_id = 0; + let funds_account = + pallet_crowdloan::Pallet::::crowdloan_funds_account(crowdloan_id); + // ensure the crowdloan is stored correctly + let call = pallet_preimage::Pallet::::bound(*noop_call()).unwrap(); + assert_eq!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id), + Some(CrowdloanInfo { + creator, + deposit, + min_contribution, + cap, + end, + funds_account, + raised: deposit, + target_address: None, + call: Some(call), + finalized: false, + contributors_count: 1, + }) + ); + // ensure the crowdloan account has the deposit + assert_eq!(Balances::free_balance(funds_account), deposit); + // ensure the creator has been deducted the deposit + assert_eq!( + Balances::free_balance(creator), + TaoBalance::from(100) - deposit + ); + // ensure the contributions have been updated + assert_eq!( + pallet_crowdloan::Contributions::::iter_prefix(crowdloan_id) + .collect::>(), + vec![(creator, deposit)] + ); + // ensure the raised amount is updated correctly + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.raised == deposit) + ); + // ensure the event is emitted + assert_eq!( + last_event(), + pallet_crowdloan::Event::::Created { + crowdloan_id, + creator, + end, + cap, + } + .into() + ); + // ensure next crowdloan id is incremented + assert_eq!( + pallet_crowdloan::NextCrowdloanId::::get(), + crowdloan_id + 1 + ); + }); +} + +#[test] +fn test_create_fails_if_bad_origin() { + TestState::default().build_and_execute(|| { + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 50; + + assert_err!( + Crowdloan::create( + RuntimeOrigin::none(), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + ), + DispatchError::BadOrigin + ); + + assert_err!( + Crowdloan::create( + RuntimeOrigin::root(), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + ), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn test_create_fails_if_deposit_is_too_low() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 20.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 50; + + assert_err!( + Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + ), + pallet_crowdloan::Error::::DepositTooLow + ); + }); +} + +#[test] +fn test_create_fails_if_cap_is_not_greater_than_deposit() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 40.into(); + let end: BlockNumberFor = 50; + + assert_err!( + Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + ), + pallet_crowdloan::Error::::CapTooLow + ); + }); +} + +#[test] +fn test_create_fails_if_min_contribution_is_too_low() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 5.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 50; + + assert_err!( + Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + ), + pallet_crowdloan::Error::::MinimumContributionTooLow + ); + }); +} + +#[test] +fn test_create_fails_if_call_and_target_address_are_provided() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 50; + let target_address: AccountOf = U256::from(42); + + assert_err!( + Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + Some(target_address), + ), + pallet_crowdloan::Error::::InvalidFinalizationConfig + ); + }); +} + +#[test] +fn test_create_fails_if_call_and_target_address_are_missing() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 50; + + assert_err!( + Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + None, + None, + ), + pallet_crowdloan::Error::::InvalidFinalizationConfig + ); + }); +} + +#[test] +fn test_create_fails_if_end_is_in_the_past() { + let current_block_number: BlockNumberFor = 10; + + TestState::default() + .with_block_number(current_block_number) + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = current_block_number - 5; + + assert_err!( + Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + ), + pallet_crowdloan::Error::::CannotEndInPast + ); + }); +} + +#[test] +fn test_create_fails_if_block_duration_is_too_short() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 11; + + assert_err!( + Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + ), + pallet_crowdloan::Error::::BlockDurationTooShort + ); + }); +} + +#[test] +fn test_create_fails_if_block_duration_is_too_long() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 1000; + + assert_err!( + Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + ), + pallet_crowdloan::Error::::BlockDurationTooLong + ); + }); +} + +#[test] +fn test_create_fails_if_creator_has_insufficient_balance() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 200.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 50; + + assert_err!( + Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + ), + pallet_crowdloan::Error::::InsufficientBalance + ); + }); +} diff --git a/pallets/crowdloan/src/tests/dissolve.rs b/pallets/crowdloan/src/tests/dissolve.rs new file mode 100644 index 0000000000..7d9076ffdd --- /dev/null +++ b/pallets/crowdloan/src/tests/dissolve.rs @@ -0,0 +1,235 @@ +#![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] + +use frame_support::{StorageDoubleMap, assert_err, assert_ok}; +use frame_system::pallet_prelude::BlockNumberFor; +use sp_core::U256; +use sp_runtime::DispatchError; + +use crate::{BalanceOf, CrowdloanId, mock::*, pallet as pallet_crowdloan}; + +#[test] +fn test_dissolve_succeeds() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + let crowdloan_id: CrowdloanId = 0; + assert_ok!(Crowdloan::set_max_contribution( + RuntimeOrigin::signed(creator), + crowdloan_id, + Some(cap) + )); + + // run some blocks past end + run_to_block(60); + + // ensure the contributor count is correct + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.contributors_count == 1) + ); + + // dissolve the crowdloan + assert_ok!(Crowdloan::dissolve( + RuntimeOrigin::signed(creator), + crowdloan_id + )); + + // ensure the crowdloan is removed from the crowdloans map + assert!(pallet_crowdloan::Crowdloans::::get(crowdloan_id).is_none()); + + // ensure the contributions are removed + assert!(!pallet_crowdloan::Contributions::::contains_prefix( + crowdloan_id + )); + + // ensure the maximum contribution is removed + assert!(pallet_crowdloan::MaxContributions::::get(crowdloan_id).is_none()); + + // ensure the event is emitted + assert_eq!( + last_event(), + pallet_crowdloan::Event::::Dissolved { crowdloan_id }.into() + ) + }); +} + +#[test] +fn test_dissolve_fails_if_bad_origin() { + TestState::default().build_and_execute(|| { + let crowdloan_id: CrowdloanId = 0; + + assert_err!( + Crowdloan::dissolve(RuntimeOrigin::none(), crowdloan_id), + DispatchError::BadOrigin + ); + + assert_err!( + Crowdloan::dissolve(RuntimeOrigin::root(), crowdloan_id), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn test_dissolve_fails_if_crowdloan_does_not_exist() { + TestState::default().build_and_execute(|| { + let crowdloan_id: CrowdloanId = 0; + assert_err!( + Crowdloan::dissolve(RuntimeOrigin::signed(U256::from(1)), crowdloan_id), + pallet_crowdloan::Error::::InvalidCrowdloanId + ); + }); +} + +#[test] +fn test_dissolve_fails_if_crowdloan_has_been_finalized() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // run some blocks + run_to_block(10); + + // some contribution + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 50.into(); + + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some more blocks past the end of the contribution period + run_to_block(60); + + // finalize the crowdloan + assert_ok!(Crowdloan::finalize( + RuntimeOrigin::signed(creator), + crowdloan_id + )); + + // try dissolve the crowdloan + assert_err!( + Crowdloan::dissolve(RuntimeOrigin::signed(creator), crowdloan_id), + pallet_crowdloan::Error::::AlreadyFinalized + ); + }); +} + +#[test] +fn test_dissolve_fails_if_origin_is_not_creator() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // run some blocks + run_to_block(10); + + // some contribution + let crowdloan_id: CrowdloanId = 0; + + // try dissolve the crowdloan + assert_err!( + Crowdloan::dissolve(RuntimeOrigin::signed(U256::from(2)), crowdloan_id), + pallet_crowdloan::Error::::InvalidOrigin + ); + }); +} + +#[test] +fn test_dissolve_fails_if_not_everyone_has_been_refunded() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 100.into()) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // run some blocks + run_to_block(10); + + // some contribution + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 50.into(); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some blocks + run_to_block(10); + + // try to dissolve the crowdloan + let crowdloan_id = 0; + assert_err!( + Crowdloan::dissolve(RuntimeOrigin::signed(creator), crowdloan_id), + pallet_crowdloan::Error::::NotReadyToDissolve + ); + }); +} diff --git a/pallets/crowdloan/src/tests/finalize.rs b/pallets/crowdloan/src/tests/finalize.rs new file mode 100644 index 0000000000..3077eefa00 --- /dev/null +++ b/pallets/crowdloan/src/tests/finalize.rs @@ -0,0 +1,690 @@ +#![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] + +use frame_support::{assert_err, assert_ok}; +use frame_system::pallet_prelude::BlockNumberFor; +use sp_core::U256; +use sp_runtime::DispatchError; + +use crate::{BalanceOf, CrowdloanId, mock::*, pallet as pallet_crowdloan}; + +#[test] +fn test_finalize_succeeds() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 100.into()) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + let call = Box::new(RuntimeCall::TestPallet( + pallet_test::Call::::transfer_funds { + dest: U256::from(42), + }, + )); + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(call), + None + )); + + // run some blocks + run_to_block(10); + + // some contribution + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 50.into(); + + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // finalize the crowdloan + assert_ok!(Crowdloan::finalize( + RuntimeOrigin::signed(creator), + crowdloan_id + )); + + // ensure the transfer was a success from the dispatched call + assert_eq!( + pallet_balances::Pallet::::free_balance(U256::from(42)), + 100.into() + ); + + // ensure the crowdloan is marked as finalized + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.finalized) + ); + + // ensure the event is emitted + assert_eq!( + last_event(), + pallet_crowdloan::Event::::Finalized { crowdloan_id }.into() + ); + + // ensure the current crowdloan id was accessible from the dispatched call + assert_eq!( + pallet_test::PassedCrowdloanId::::get(), + Some(crowdloan_id) + ); + }); +} + +#[test] +fn test_finalize_succeeds_with_target_address() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 100.into()) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + let target_address: AccountOf = U256::from(42); + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + None, + Some(target_address), + )); + + // run some blocks + run_to_block(10); + + // some contribution + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 50.into(); + + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some more blocks past the end of the contribution period + run_to_block(60); + + // finalize the crowdloan + assert_ok!(Crowdloan::finalize( + RuntimeOrigin::signed(creator), + crowdloan_id + )); + + // ensure the target address has received the funds + assert_eq!( + pallet_balances::Pallet::::free_balance(target_address), + 100.into() + ); + + // ensure the crowdloan is marked as finalized + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.finalized) + ); + + // ensure the event is emitted + assert_eq!( + last_event(), + pallet_crowdloan::Event::::Finalized { crowdloan_id }.into() + ); + }) +} + +#[test] +fn test_finalize_fails_if_call_and_target_address_are_provided() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + run_to_block(10); + + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 50.into(); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + let target_address: AccountOf = U256::from(42); + pallet_crowdloan::Crowdloans::::mutate(crowdloan_id, |crowdloan| { + crowdloan.as_mut().unwrap().target_address = Some(target_address); + }); + + run_to_block(60); + + assert_err!( + Crowdloan::finalize(RuntimeOrigin::signed(creator), crowdloan_id), + pallet_crowdloan::Error::::InvalidFinalizationConfig + ); + + assert_eq!( + pallet_balances::Pallet::::free_balance(target_address), + 0.into() + ); + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| !c.finalized) + ); + }); +} + +#[test] +fn test_finalize_fails_if_call_and_target_address_are_missing() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + run_to_block(10); + + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 50.into(); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + pallet_crowdloan::Crowdloans::::mutate(crowdloan_id, |crowdloan| { + crowdloan.as_mut().unwrap().call = None; + }); + + run_to_block(60); + + assert_err!( + Crowdloan::finalize(RuntimeOrigin::signed(creator), crowdloan_id), + pallet_crowdloan::Error::::InvalidFinalizationConfig + ); + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| !c.finalized) + ); + }); +} + +#[test] +fn test_finalize_fails_if_bad_origin() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + let crowdloan_id: CrowdloanId = 0; + + assert_err!( + Crowdloan::finalize(RuntimeOrigin::none(), crowdloan_id), + DispatchError::BadOrigin + ); + + assert_err!( + Crowdloan::finalize(RuntimeOrigin::root(), crowdloan_id), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn test_finalize_fails_if_crowdloan_does_not_exist() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let crowdloan_id: CrowdloanId = 0; + + // try to finalize + assert_err!( + Crowdloan::finalize(RuntimeOrigin::signed(creator), crowdloan_id), + pallet_crowdloan::Error::::InvalidCrowdloanId + ); + }); +} + +#[test] +fn test_finalize_fails_if_not_creator_origin() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 100.into()) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + // run some blocks + run_to_block(10); + + // some contribution + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 50.into(); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some more blocks past the end of the contribution period + run_to_block(60); + + // try finalize the crowdloan + assert_err!( + Crowdloan::finalize(RuntimeOrigin::signed(contributor), crowdloan_id), + pallet_crowdloan::Error::::InvalidOrigin + ); + }); +} + +#[test] +fn test_finalize_fails_if_crowdloan_cap_is_not_raised() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 100.into()) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // run some blocks + run_to_block(10); + + // some contribution + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 49.into(); // below cap + + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some more blocks past the end of the contribution period + run_to_block(60); + + // try finalize the crowdloan + assert_err!( + Crowdloan::finalize(RuntimeOrigin::signed(creator), crowdloan_id), + pallet_crowdloan::Error::::CapNotRaised + ); + }); +} + +#[test] +fn test_finalize_fails_if_crowdloan_has_already_been_finalized() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 100.into()) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // some contribution + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 50.into(); + + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some more blocks past the end of the contribution period + run_to_block(60); + + // finalize the crowdloan + assert_ok!(Crowdloan::finalize( + RuntimeOrigin::signed(creator), + crowdloan_id + )); + + // try finalize the crowdloan a second time + assert_err!( + Crowdloan::finalize(RuntimeOrigin::signed(creator), crowdloan_id), + pallet_crowdloan::Error::::AlreadyFinalized + ); + }); +} + +#[test] +fn test_finalize_fails_if_call_fails() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 100.into()) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + let call = Box::new(RuntimeCall::TestPallet( + pallet_test::Call::::failing_extrinsic {}, + )); + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(call), + None, + )); + + // run some blocks + run_to_block(10); + + // some contribution + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 50.into(); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some more blocks past the end of the contribution period + run_to_block(60); + + // try finalize the crowdloan + assert_err!( + Crowdloan::finalize(RuntimeOrigin::signed(creator), crowdloan_id), + pallet_test::Error::::ShouldFail + ); + }); +} + +#[test] +fn test_finalize_fails_if_another_finalize_is_in_progress() { + TestState::default() + .with_balance(U256::from(1), 300.into()) + .with_balance(U256::from(2), 300.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let contributor: AccountOf = U256::from(2); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + let first_crowdloan_id: CrowdloanId = 0; + let second_crowdloan_id: CrowdloanId = 1; + + let nested_finalize_call = Box::new(RuntimeCall::Crowdloan(pallet_crowdloan::Call::< + Test, + >::finalize { + crowdloan_id: second_crowdloan_id, + })); + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(nested_finalize_call), + None, + )); + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + run_to_block(10); + + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + first_crowdloan_id, + 50.into() + )); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + second_crowdloan_id, + 50.into() + )); + + run_to_block(60); + + assert_err!( + Crowdloan::finalize(RuntimeOrigin::signed(creator), first_crowdloan_id), + pallet_crowdloan::Error::::AlreadyFinalizing + ); + + assert_eq!(pallet_crowdloan::CurrentCrowdloanId::::get(), None); + assert!( + pallet_crowdloan::Crowdloans::::get(first_crowdloan_id) + .is_some_and(|c| !c.finalized) + ); + assert!( + pallet_crowdloan::Crowdloans::::get(second_crowdloan_id) + .is_some_and(|c| !c.finalized) + ); + }); +} + +// The finalize `call` cannot re-enter `withdraw` on the same crowdloan: it is rejected and +// the extrinsic reverts, so no funds move and `raised` stays consistent with the real balance. + +#[test] +fn test_finalize_blocks_reentrant_withdraw() { + TestState::default() + .with_balance(U256::from(1), 200.into()) // creator + .with_balance(U256::from(2), 200.into()) // contributor + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let contributor: AccountOf = U256::from(2); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + let crowdloan_id: CrowdloanId = 0; + + // The finalize call re-enters `withdraw` on the same crowdloan. + let reentrant_call = Box::new(RuntimeCall::Crowdloan( + pallet_crowdloan::Call::::withdraw { crowdloan_id }, + )); + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(reentrant_call), + None, + )); + run_to_block(10); + + // Creator contributes 30 over the deposit (total 80); contributor fills the cap. + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(creator), + crowdloan_id, + 30.into() + )); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + 20.into() + )); + + let funds_account = + pallet_crowdloan::Pallet::::crowdloan_funds_account(crowdloan_id); + assert_eq!(Balances::free_balance(funds_account), cap); + let creator_balance_before = Balances::free_balance(creator); + + run_to_block(60); + + // Finalize dispatches the re-entrant withdraw, which is rejected with + // `AlreadyFinalized`. Wrap in a storage layer to model the per-extrinsic + // transaction the runtime applies in production, so the revert is observable. + let outcome = frame_support::storage::with_storage_layer(|| { + Crowdloan::finalize(RuntimeOrigin::signed(creator), crowdloan_id) + }); + assert_err!(outcome, pallet_crowdloan::Error::::AlreadyFinalized); + + // No funds were extracted and accounting is intact. + assert_eq!(Balances::free_balance(creator), creator_balance_before); + assert_eq!(Balances::free_balance(funds_account), cap); + assert_eq!(pallet_crowdloan::CurrentCrowdloanId::::get(), None); + let crowdloan = pallet_crowdloan::Crowdloans::::get(crowdloan_id).unwrap(); + assert!(!crowdloan.finalized); + assert_eq!(crowdloan.raised, cap); + + // Contributor funds are not frozen: the contributor can still withdraw. + assert_ok!(Crowdloan::withdraw( + RuntimeOrigin::signed(contributor), + crowdloan_id + )); + assert_eq!(Balances::free_balance(contributor), 200.into()); + }); +} + +// A re-entrant `refund` embedded as the finalize call is likewise rejected before moving funds. + +#[test] +fn test_finalize_blocks_reentrant_refund() { + TestState::default() + .with_balance(U256::from(1), 200.into()) // creator + .with_balance(U256::from(2), 200.into()) // contributor + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let contributor: AccountOf = U256::from(2); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + let crowdloan_id: CrowdloanId = 0; + + let reentrant_call = Box::new(RuntimeCall::Crowdloan( + pallet_crowdloan::Call::::refund { crowdloan_id }, + )); + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(reentrant_call), + None, + )); + run_to_block(10); + + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(creator), + crowdloan_id, + 30.into() + )); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + 20.into() + )); + + let funds_account = + pallet_crowdloan::Pallet::::crowdloan_funds_account(crowdloan_id); + run_to_block(60); + + // The re-entrant refund hits the `finalized` guard before transferring anything. + assert_err!( + Crowdloan::finalize(RuntimeOrigin::signed(creator), crowdloan_id), + pallet_crowdloan::Error::::AlreadyFinalized + ); + assert_eq!(Balances::free_balance(funds_account), cap); + }); +} diff --git a/pallets/crowdloan/src/tests/mod.rs b/pallets/crowdloan/src/tests/mod.rs new file mode 100644 index 0000000000..644b6dbd14 --- /dev/null +++ b/pallets/crowdloan/src/tests/mod.rs @@ -0,0 +1,13 @@ +//! Unit tests for `pallet-crowdloan`, split by extrinsic / concept. +#![cfg(test)] + +mod contribute; +mod create; +mod dissolve; +mod finalize; +mod refund; +mod set_max_contribution; +mod update_cap; +mod update_end; +mod update_min_contribution; +mod withdraw; diff --git a/pallets/crowdloan/src/tests/refund.rs b/pallets/crowdloan/src/tests/refund.rs new file mode 100644 index 0000000000..215cc1e06f --- /dev/null +++ b/pallets/crowdloan/src/tests/refund.rs @@ -0,0 +1,204 @@ +#![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] + +use frame_support::{assert_err, assert_ok}; +use frame_system::pallet_prelude::BlockNumberFor; +use sp_core::U256; +use sp_runtime::DispatchError; +use subtensor_runtime_common::TaoBalance; + +use crate::{BalanceOf, CrowdloanId, mock::*, pallet as pallet_crowdloan}; + +#[test] +fn test_refund_succeeds() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 100.into()) + .with_balance(U256::from(3), 100.into()) + .with_balance(U256::from(4), 100.into()) + .with_balance(U256::from(5), 100.into()) + .with_balance(U256::from(6), 100.into()) + .with_balance(U256::from(7), 100.into()) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 400.into(); + let end: BlockNumberFor = 50; + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // run some blocks + run_to_block(10); + + // make 6 contributions to reach 350 raised amount (initial deposit + contributions) + let crowdloan_id: CrowdloanId = 0; + let amount: BalanceOf = 50.into(); + for i in 2..8 { + let contributor: AccountOf = U256::from(i); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + } + + // ensure the contributor count is correct + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.contributors_count == 7) + ); + + // run some more blocks before the end of the contribution period + run_to_block(20); + + // first round of refund + assert_ok!(Crowdloan::refund( + RuntimeOrigin::signed(creator), + crowdloan_id + )); + + // ensure the contributor count is correct, we processed 5 refunds + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.contributors_count == 2) + ); + + // ensure the crowdloan account has the correct amount + let funds_account = + pallet_crowdloan::Pallet::::crowdloan_funds_account(crowdloan_id); + assert_eq!( + Balances::free_balance(funds_account), + TaoBalance::from(350) - TaoBalance::from(5) * amount + ); + // ensure raised amount is updated correctly + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id).is_some_and( + |c| c.raised == TaoBalance::from(350) - TaoBalance::from(5) * amount + ) + ); + // ensure the event is emitted + assert_eq!( + last_event(), + pallet_crowdloan::Event::::PartiallyRefunded { crowdloan_id }.into() + ); + + // run some more blocks past the end of the contribution period + run_to_block(70); + + // second round of refund + assert_ok!(Crowdloan::refund( + RuntimeOrigin::signed(creator), + crowdloan_id + )); + + // ensure the contributor count is correct, we processed 1 more refund + // keeping deposit + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.contributors_count == 1) + ); + + // ensure the crowdloan account has the correct amount + assert_eq!( + pallet_balances::Pallet::::free_balance(funds_account), + initial_deposit + ); + // ensure the raised amount is updated correctly + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.raised == initial_deposit) + ); + + // ensure creator has the correct amount + assert_eq!( + pallet_balances::Pallet::::free_balance(creator), + initial_deposit + ); + + // ensure each contributor has been refunded and removed from the crowdloan + for i in 2..8 { + let contributor: AccountOf = U256::from(i); + assert_eq!( + pallet_balances::Pallet::::free_balance(contributor), + 100.into() + ); + assert_eq!( + pallet_crowdloan::Contributions::::get(crowdloan_id, contributor), + None, + ); + } + + // ensure the event is emitted + assert_eq!( + last_event(), + pallet_crowdloan::Event::::AllRefunded { crowdloan_id }.into() + ); + }) +} + +#[test] +fn test_refund_fails_if_bad_or_invalid_origin() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + // create a crowdloan + let crowdloan_id: CrowdloanId = 0; + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 50; + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + assert_err!( + Crowdloan::refund(RuntimeOrigin::none(), crowdloan_id), + DispatchError::BadOrigin + ); + + assert_err!( + Crowdloan::refund(RuntimeOrigin::root(), crowdloan_id), + DispatchError::BadOrigin + ); + + // run some blocks + run_to_block(60); + + // try to refund + let unknown_contributor: AccountOf = U256::from(2); + assert_err!( + Crowdloan::refund(RuntimeOrigin::signed(unknown_contributor), crowdloan_id), + pallet_crowdloan::Error::::InvalidOrigin, + ); + }); +} + +#[test] +fn test_refund_fails_if_crowdloan_does_not_exist() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let crowdloan_id: CrowdloanId = 0; + + assert_err!( + Crowdloan::refund(RuntimeOrigin::signed(creator), crowdloan_id), + pallet_crowdloan::Error::::InvalidCrowdloanId + ); + }); +} diff --git a/pallets/crowdloan/src/tests/set_max_contribution.rs b/pallets/crowdloan/src/tests/set_max_contribution.rs new file mode 100644 index 0000000000..a82484239d --- /dev/null +++ b/pallets/crowdloan/src/tests/set_max_contribution.rs @@ -0,0 +1,40 @@ +#![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] + +use frame_support::{assert_err, assert_ok}; +use frame_system::pallet_prelude::BlockNumberFor; +use sp_core::U256; + +use crate::{BalanceOf, mock::*, pallet as pallet_crowdloan}; + +#[test] +fn test_set_max_contribution_fails_if_max_contribution_is_too_low() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let max_contribution: BalanceOf = 40.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + assert_err!( + Crowdloan::set_max_contribution( + RuntimeOrigin::signed(creator), + 0, + Some(max_contribution) + ), + pallet_crowdloan::Error::::MaximumContributionTooLow + ); + }); +} diff --git a/pallets/crowdloan/src/tests/update_cap.rs b/pallets/crowdloan/src/tests/update_cap.rs new file mode 100644 index 0000000000..be446f5207 --- /dev/null +++ b/pallets/crowdloan/src/tests/update_cap.rs @@ -0,0 +1,202 @@ +#![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] + +use frame_support::{assert_err, assert_ok}; +use frame_system::pallet_prelude::BlockNumberFor; +use sp_core::U256; +use sp_runtime::DispatchError; + +use crate::{BalanceOf, CrowdloanId, mock::*, pallet as pallet_crowdloan}; + +#[test] +fn test_update_cap_succeeds() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // try update the cap + let crowdloan_id: CrowdloanId = 0; + let new_cap: BalanceOf = 200.into(); + assert_ok!(Crowdloan::update_cap( + RuntimeOrigin::signed(creator), + crowdloan_id, + new_cap + )); + + // ensure the cap is updated + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.cap == new_cap) + ); + // ensure the event is emitted + assert_eq!( + last_event(), + pallet_crowdloan::Event::::CapUpdated { + crowdloan_id, + new_cap + } + .into() + ); + }); +} + +#[test] +fn test_update_cap_fails_if_bad_origin() { + TestState::default().build_and_execute(|| { + let crowdloan_id: CrowdloanId = 0; + + assert_err!( + Crowdloan::update_cap(RuntimeOrigin::none(), crowdloan_id, 200.into()), + DispatchError::BadOrigin + ); + + assert_err!( + Crowdloan::update_cap(RuntimeOrigin::root(), crowdloan_id, 200.into()), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn test_update_cap_fails_if_crowdloan_does_not_exist() { + TestState::default().build_and_execute(|| { + let crowdloan_id: CrowdloanId = 0; + + assert_err!( + Crowdloan::update_cap( + RuntimeOrigin::signed(U256::from(1)), + crowdloan_id, + 200.into() + ), + pallet_crowdloan::Error::::InvalidCrowdloanId + ); + }); +} + +#[test] +fn test_update_cap_fails_if_crowdloan_has_been_finalized() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // some contribution + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 50.into(); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some blocks + run_to_block(60); + + // finalize the crowdloan + let crowdloan_id: CrowdloanId = 0; + assert_ok!(Crowdloan::finalize( + RuntimeOrigin::signed(creator), + crowdloan_id + )); + + // try update the cap + let new_cap: BalanceOf = 200.into(); + assert_err!( + Crowdloan::update_cap(RuntimeOrigin::signed(creator), crowdloan_id, new_cap), + pallet_crowdloan::Error::::AlreadyFinalized + ); + }); +} + +#[test] +fn test_update_cap_fails_if_not_creator() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // try update the cap + let crowdloan_id: CrowdloanId = 0; + let new_cap: BalanceOf = 200.into(); + assert_err!( + Crowdloan::update_cap(RuntimeOrigin::signed(U256::from(2)), crowdloan_id, new_cap), + pallet_crowdloan::Error::::InvalidOrigin + ); + }); +} + +#[test] +fn test_update_cap_fails_if_new_cap_is_too_low() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // try update the cap + let crowdloan_id: CrowdloanId = 0; + let new_cap: BalanceOf = 49.into(); + assert_err!( + Crowdloan::update_cap(RuntimeOrigin::signed(creator), crowdloan_id, new_cap), + pallet_crowdloan::Error::::CapTooLow + ); + }); +} diff --git a/pallets/crowdloan/src/tests/update_end.rs b/pallets/crowdloan/src/tests/update_end.rs new file mode 100644 index 0000000000..9a9ade71ff --- /dev/null +++ b/pallets/crowdloan/src/tests/update_end.rs @@ -0,0 +1,269 @@ +#![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] + +use frame_support::{assert_err, assert_ok}; +use frame_system::pallet_prelude::BlockNumberFor; +use sp_core::U256; +use sp_runtime::DispatchError; + +use crate::{BalanceOf, CrowdloanId, mock::*, pallet as pallet_crowdloan}; + +#[test] +fn test_update_end_succeeds() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + let crowdloan_id: CrowdloanId = 0; + let new_end: BlockNumberFor = 60; + + // update the end + assert_ok!(Crowdloan::update_end( + RuntimeOrigin::signed(creator), + crowdloan_id, + new_end + )); + + // ensure the end is updated + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.end == new_end) + ); + // ensure the event is emitted + assert_eq!( + last_event(), + pallet_crowdloan::Event::::EndUpdated { + crowdloan_id, + new_end + } + .into() + ); + }); +} + +#[test] +fn test_update_end_fails_if_bad_origin() { + TestState::default().build_and_execute(|| { + let crowdloan_id: CrowdloanId = 0; + + assert_err!( + Crowdloan::update_end(RuntimeOrigin::none(), crowdloan_id, 60), + DispatchError::BadOrigin + ); + + assert_err!( + Crowdloan::update_end(RuntimeOrigin::root(), crowdloan_id, 60), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn test_update_end_fails_if_crowdloan_does_not_exist() { + TestState::default().build_and_execute(|| { + let crowdloan_id: CrowdloanId = 0; + + assert_err!( + Crowdloan::update_end(RuntimeOrigin::signed(U256::from(1)), crowdloan_id, 60), + pallet_crowdloan::Error::::InvalidCrowdloanId + ); + }); +} + +#[test] +fn test_update_end_fails_if_crowdloan_has_been_finalized() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + let crowdloan_id: CrowdloanId = 0; + + // some contribution + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 50.into(); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some blocks + run_to_block(60); + + // finalize the crowdloan + assert_ok!(Crowdloan::finalize( + RuntimeOrigin::signed(creator), + crowdloan_id + )); + + // try update the end + let new_end: BlockNumberFor = 60; + assert_err!( + Crowdloan::update_end(RuntimeOrigin::signed(creator), crowdloan_id, new_end), + pallet_crowdloan::Error::::AlreadyFinalized + ); + }); +} + +#[test] +fn test_update_end_fails_if_not_creator() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + let crowdloan_id: CrowdloanId = 0; + let new_end: BlockNumberFor = 60; + + // try update the end + assert_err!( + Crowdloan::update_end(RuntimeOrigin::signed(U256::from(2)), crowdloan_id, new_end), + pallet_crowdloan::Error::::InvalidOrigin + ); + }); +} + +#[test] +fn test_update_end_fails_if_new_end_is_in_past() { + TestState::default() + .with_block_number(50) + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 100; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + let crowdloan_id: CrowdloanId = 0; + let new_end: BlockNumberFor = 40; + + // try update the end to a past block number + assert_err!( + Crowdloan::update_end(RuntimeOrigin::signed(creator), crowdloan_id, new_end), + pallet_crowdloan::Error::::CannotEndInPast + ); + }); +} + +#[test] +fn test_update_end_fails_if_block_duration_is_too_short() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // run some blocks + run_to_block(50); + + let crowdloan_id: CrowdloanId = 0; + let new_end: BlockNumberFor = 51; + + // try update the end to a block number that is too long + assert_err!( + Crowdloan::update_end(RuntimeOrigin::signed(creator), crowdloan_id, new_end), + pallet_crowdloan::Error::::BlockDurationTooShort + ); + }); +} + +#[test] +fn test_update_end_fails_if_block_duration_is_too_long() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + let crowdloan_id: CrowdloanId = 0; + let new_end: BlockNumberFor = 1000; + + // try update the end to a block number that is too long + assert_err!( + Crowdloan::update_end(RuntimeOrigin::signed(creator), crowdloan_id, new_end), + pallet_crowdloan::Error::::BlockDurationTooLong + ); + }); +} diff --git a/pallets/crowdloan/src/tests/update_min_contribution.rs b/pallets/crowdloan/src/tests/update_min_contribution.rs new file mode 100644 index 0000000000..5a73a69885 --- /dev/null +++ b/pallets/crowdloan/src/tests/update_min_contribution.rs @@ -0,0 +1,258 @@ +#![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] + +use frame_support::{assert_err, assert_ok}; +use frame_system::pallet_prelude::BlockNumberFor; +use sp_core::U256; +use sp_runtime::DispatchError; + +use crate::{BalanceOf, CrowdloanId, mock::*, pallet as pallet_crowdloan}; + +#[test] +fn test_update_min_contribution_succeeds() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + let crowdloan_id: CrowdloanId = 0; + let new_min_contribution: BalanceOf = 20.into(); + + // update the min contribution + assert_ok!(Crowdloan::update_min_contribution( + RuntimeOrigin::signed(creator), + crowdloan_id, + new_min_contribution + )); + + // ensure the min contribution is updated + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.min_contribution == new_min_contribution) + ); + // ensure the event is emitted + assert_eq!( + last_event(), + pallet_crowdloan::Event::::MinContributionUpdated { + crowdloan_id, + new_min_contribution + } + .into() + ); + }); +} + +#[test] +fn test_update_min_contribution_fails_if_bad_origin() { + TestState::default().build_and_execute(|| { + let crowdloan_id: CrowdloanId = 0; + + assert_err!( + Crowdloan::update_min_contribution(RuntimeOrigin::none(), crowdloan_id, 20.into()), + DispatchError::BadOrigin + ); + + assert_err!( + Crowdloan::update_min_contribution(RuntimeOrigin::root(), crowdloan_id, 20.into()), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn test_update_min_contribution_fails_if_crowdloan_does_not_exist() { + TestState::default().build_and_execute(|| { + let crowdloan_id: CrowdloanId = 0; + + assert_err!( + Crowdloan::update_min_contribution( + RuntimeOrigin::signed(U256::from(1)), + crowdloan_id, + 20.into() + ), + pallet_crowdloan::Error::::InvalidCrowdloanId + ); + }); +} + +#[test] +fn test_update_min_contribution_fails_if_crowdloan_has_been_finalized() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // some contribution + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 50.into(); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some blocks + run_to_block(50); + + // finalize the crowdloan + let crowdloan_id: CrowdloanId = 0; + assert_ok!(Crowdloan::finalize( + RuntimeOrigin::signed(creator), + crowdloan_id + )); + + // try update the min contribution + let new_min_contribution: BalanceOf = 20.into(); + assert_err!( + Crowdloan::update_min_contribution( + RuntimeOrigin::signed(creator), + crowdloan_id, + new_min_contribution + ), + pallet_crowdloan::Error::::AlreadyFinalized + ); + }); +} + +#[test] +fn test_update_min_contribution_fails_if_not_creator() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + let crowdloan_id: CrowdloanId = 0; + let new_min_contribution: BalanceOf = 20.into(); + + // try update the min contribution + assert_err!( + Crowdloan::update_min_contribution( + RuntimeOrigin::signed(U256::from(2)), + crowdloan_id, + new_min_contribution + ), + pallet_crowdloan::Error::::InvalidOrigin + ); + }); +} + +#[test] +fn test_update_min_contribution_fails_if_new_min_contribution_is_too_low() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + let crowdloan_id: CrowdloanId = 0; + let new_min_contribution: BalanceOf = 9.into(); + + // try update the min contribution + assert_err!( + Crowdloan::update_min_contribution( + RuntimeOrigin::signed(creator), + crowdloan_id, + new_min_contribution + ), + pallet_crowdloan::Error::::MinimumContributionTooLow + ); + }); +} + +#[test] +fn test_update_min_contribution_fails_if_new_min_contribution_exceeds_max_contribution() { + TestState::default() + .with_balance(U256::from(1), 200.into()) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let max_contribution: BalanceOf = 60.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + let crowdloan_id: CrowdloanId = 0; + assert_ok!(Crowdloan::set_max_contribution( + RuntimeOrigin::signed(creator), + crowdloan_id, + Some(max_contribution) + )); + + assert_err!( + Crowdloan::update_min_contribution( + RuntimeOrigin::signed(creator), + crowdloan_id, + 70.into() + ), + pallet_crowdloan::Error::::MinimumContributionTooHigh + ); + }); +} diff --git a/pallets/crowdloan/src/tests/withdraw.rs b/pallets/crowdloan/src/tests/withdraw.rs new file mode 100644 index 0000000000..91e0d45c3e --- /dev/null +++ b/pallets/crowdloan/src/tests/withdraw.rs @@ -0,0 +1,348 @@ +#![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] + +use frame_support::{assert_err, assert_ok}; +use frame_system::pallet_prelude::BlockNumberFor; +use sp_core::U256; +use sp_runtime::DispatchError; +use subtensor_runtime_common::TaoBalance; + +use crate::{BalanceOf, CrowdloanId, mock::*, pallet as pallet_crowdloan}; + +#[test] +fn test_withdraw_from_contributor_succeeds() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 100.into()) + .with_balance(U256::from(3), 100.into()) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + // run some blocks + run_to_block(10); + + // contribute to the crowdloan + let crowdloan_id: CrowdloanId = 0; + + let contributor1: AccountOf = U256::from(2); + let amount1: BalanceOf = 100.into(); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor1), + crowdloan_id, + amount1 + )); + + let contributor2: AccountOf = U256::from(3); + let amount2: BalanceOf = 100.into(); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor2), + crowdloan_id, + amount2 + )); + + // run some more blocks past the end of the contribution period + run_to_block(60); + + // ensure the contributor count is correct + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.contributors_count == 3) + ); + + // withdraw from contributor1 + assert_ok!(Crowdloan::withdraw( + RuntimeOrigin::signed(contributor1), + crowdloan_id + )); + // ensure the contributor1 contribution has been removed + assert_eq!( + pallet_crowdloan::Contributions::::get(crowdloan_id, contributor1), + None, + ); + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.contributors_count == 2) + ); + // ensure the contributor1 has the correct amount + assert_eq!( + pallet_balances::Pallet::::free_balance(contributor1), + 100.into() + ); + + // withdraw from contributor2 + assert_ok!(Crowdloan::withdraw( + RuntimeOrigin::signed(contributor2), + crowdloan_id + )); + // ensure the contributor2 contribution has been removed + assert_eq!( + pallet_crowdloan::Contributions::::get(crowdloan_id, contributor2), + None, + ); + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.contributors_count == 1) + ); + // ensure the contributor2 has the correct amount + assert_eq!( + pallet_balances::Pallet::::free_balance(contributor2), + 100.into() + ); + + // ensure the crowdloan account has the correct amount + let funds_account = + pallet_crowdloan::Pallet::::crowdloan_funds_account(crowdloan_id); + assert_eq!(Balances::free_balance(funds_account), initial_deposit); + // ensure the crowdloan raised amount is updated correctly + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.raised == initial_deposit) + ); + }); +} + +#[test] +fn test_withdraw_from_creator_with_contribution_over_deposit_succeeds() { + TestState::default() + .with_balance(U256::from(1), 200.into()) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + // contribute to the crowdloan as the creator + let crowdloan_id: CrowdloanId = 0; + + let amount: BalanceOf = 100.into(); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(creator), + crowdloan_id, + amount + )); + + // ensure the contributor count is correct + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.contributors_count == 1) + ); + + // withdraw + let crowdloan_id: CrowdloanId = 0; + assert_ok!(Crowdloan::withdraw( + RuntimeOrigin::signed(creator), + crowdloan_id + )); + + // ensure the creator has the correct amount + assert_eq!( + pallet_balances::Pallet::::free_balance(creator), + TaoBalance::from(200) - initial_deposit + ); + // ensure the creator contribution has been removed + assert_eq!( + pallet_crowdloan::Contributions::::get(crowdloan_id, creator), + Some(initial_deposit), + ); + // ensure the contributor count hasn't changed because deposit is kept + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.contributors_count == 1) + ); + + // ensure the crowdloan account has the correct amount + let funds_account = + pallet_crowdloan::Pallet::::crowdloan_funds_account(crowdloan_id); + assert_eq!(Balances::free_balance(funds_account), initial_deposit); + // ensure the crowdloan raised amount is updated correctly + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.raised == initial_deposit) + ); + }); +} + +#[test] +fn test_withdraw_fails_from_creator_with_no_contribution_over_deposit() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 200.into()) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + // try to withdraw + let crowdloan_id: CrowdloanId = 0; + assert_err!( + Crowdloan::withdraw(RuntimeOrigin::signed(creator), crowdloan_id), + pallet_crowdloan::Error::::DepositCannotBeWithdrawn + ); + + // ensure the crowdloan account has the correct amount + let funds_account = + pallet_crowdloan::Pallet::::crowdloan_funds_account(crowdloan_id); + assert_eq!(Balances::free_balance(funds_account), initial_deposit); + // ensure the crowdloan raised amount is updated correctly + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.raised == initial_deposit) + ); + }); +} + +#[test] +fn test_withdraw_fails_if_bad_origin() { + TestState::default().build_and_execute(|| { + let crowdloan_id: CrowdloanId = 0; + + assert_err!( + Crowdloan::withdraw(RuntimeOrigin::none(), crowdloan_id), + DispatchError::BadOrigin + ); + + assert_err!( + Crowdloan::withdraw(RuntimeOrigin::root(), crowdloan_id), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn test_withdraw_fails_if_crowdloan_does_not_exists() { + TestState::default().build_and_execute(|| { + let contributor: AccountOf = U256::from(1); + let crowdloan_id: CrowdloanId = 0; + + assert_err!( + Crowdloan::withdraw(RuntimeOrigin::signed(contributor), crowdloan_id), + pallet_crowdloan::Error::::InvalidCrowdloanId + ); + }); +} + +#[test] +fn test_withdraw_fails_if_crowdloan_has_already_been_finalized() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 200.into()) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 100.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // some contribution + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 50.into(); + + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some more blocks past the end of the contribution period + run_to_block(60); + + // finalize the crowdloan + assert_ok!(Crowdloan::finalize( + RuntimeOrigin::signed(creator), + crowdloan_id + )); + + // try to withdraw + assert_err!( + Crowdloan::withdraw(RuntimeOrigin::signed(creator), crowdloan_id), + pallet_crowdloan::Error::::AlreadyFinalized + ); + }); +} + +#[test] +fn test_withdraw_fails_if_no_contribution_exists() { + TestState::default() + .with_balance(U256::from(1), 100.into()) + .with_balance(U256::from(2), 200.into()) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50.into(); + let min_contribution: BalanceOf = 10.into(); + let cap: BalanceOf = 300.into(); + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + // run some more blocks past the end of the contribution period + run_to_block(60); + + // try to withdraw + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + assert_err!( + Crowdloan::withdraw(RuntimeOrigin::signed(contributor), crowdloan_id), + pallet_crowdloan::Error::::NoContribution + ); + }); +} diff --git a/pallets/drand/src/benchmarking.rs b/pallets/drand/src/benchmarking.rs index 799b9d16b2..2459d281e4 100644 --- a/pallets/drand/src/benchmarking.rs +++ b/pallets/drand/src/benchmarking.rs @@ -14,7 +14,7 @@ * limitations under the License. */ -//! Benchmarking setup for pallet-drand +//! Runtime benchmarks for `set_beacon_config`, `write_pulse`, and `set_oldest_stored_round`. use super::*; #[allow(unused)] diff --git a/pallets/drand/src/bls12_381.rs b/pallets/drand/src/bls12_381.rs index e31118a3e4..ef5668f261 100644 --- a/pallets/drand/src/bls12_381.rs +++ b/pallets/drand/src/bls12_381.rs @@ -14,22 +14,21 @@ * limitations under the License. */ +//! Optimized BLS12-381 pairing check used by [`crate::verifier::QuicknetVerifier`]. + use ark_ec::pairing::Pairing; use ark_std::{Zero, ops::Neg}; use sp_crypto_ec_utils::bls12_381::{ Bls12_381 as Bls12_381Opt, G1Affine as G1AffineOpt, G2Affine as G2AffineOpt, }; -/// An optimized way to verify Drand pulses from quicket -/// Instead of computing two pairings and comparing them, we instead compute a multi miller loop, -/// and then take the final exponentiation, saving a lot of computational cost. -/// -/// This function is also inlined as a way to optimize performance. +/// Return true iff `$e(signature, q) == e(r, s)$` via one multi-Miller loop + final exp. /// -/// * `signature`: -/// * `q`: -/// * `msg_on_curve`: The message signed by Drand, hashed to G1 -/// * `p_pub`: The beacon public key +/// Cheaper than two separate pairings. Arguments for Quicknet verification: +/// * `signature` — pulse signature in G1 +/// * `q` — G2 generator +/// * `r` — hash-to-curve of the round message in G1 +/// * `s` — beacon public key in G2 #[inline] pub fn fast_pairing_opt( signature: G1AffineOpt, diff --git a/pallets/drand/src/drand_priority.rs b/pallets/drand/src/drand_priority.rs index c63ffa5803..5069dcf493 100644 --- a/pallets/drand/src/drand_priority.rs +++ b/pallets/drand/src/drand_priority.rs @@ -1,3 +1,5 @@ +//! Transaction extension that raises mempool priority for unsigned [`Call::write_pulse`]. + use crate::{Call, Config}; use codec::{Decode, DecodeWithMemTracking, Encode}; use frame_support::dispatch::{DispatchInfo, PostDispatchInfo}; @@ -14,9 +16,13 @@ use sp_runtime::transaction_validity::{ use sp_std::marker::PhantomData; use subtensor_macros::freeze_struct; +/// Runtime call type alias used by [`DrandPriority`]. pub type RuntimeCallFor = ::RuntimeCall; -#[freeze_struct("d0d094192bd6390e")] +/// Signed-extension marker that bumps priority when the call is [`Call::write_pulse`]. +/// +/// Does not alter other calls; weight is currently zero pending a dedicated benchmark. +#[freeze_struct("fafde7074128b670")] #[derive(Default, Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo)] pub struct DrandPriority(pub PhantomData); @@ -27,11 +33,13 @@ impl sp_std::fmt::Debug for DrandPriority } impl DrandPriority { + /// Construct the extension (stateless; `PhantomData` only). pub fn new() -> Self { Self(PhantomData) } - fn get_drand_priority() -> TransactionPriority { + /// Fixed priority applied to `write_pulse` in `validate`. + fn unsigned_write_pulse_priority() -> TransactionPriority { 10_000u64 } } @@ -66,7 +74,7 @@ where match call.is_sub_type() { Some(Call::write_pulse { .. }) => { let validity = ValidTransaction { - priority: Self::get_drand_priority(), + priority: Self::unsigned_write_pulse_priority(), ..Default::default() }; diff --git a/pallets/drand/src/lib.rs b/pallets/drand/src/lib.rs index f92cc09236..036fde7fd8 100644 --- a/pallets/drand/src/lib.rs +++ b/pallets/drand/src/lib.rs @@ -77,7 +77,7 @@ mod benchmarking; pub mod weights; -/// the main drand api endpoint +/// HTTP hosts tried in parallel by the offchain worker when fetching Quicknet pulses. const ENDPOINTS: [&str; 5] = [ "https://api.drand.sh", "https://api2.drand.sh", @@ -86,15 +86,20 @@ const ENDPOINTS: [&str; 5] = [ "https://api.drand.secureweb3.com:6875", ]; -/// the drand quicknet chain hash -/// quicknet uses 'Tiny' BLS381, with small 48-byte sigs in G1 and 96-byte pubkeys in G2 +/// Hex hash of the drand Quicknet chain (path segment in API URLs). +/// +/// Quicknet uses Tiny BLS381: 48-byte signatures in G1 and 96-byte public keys in G2. pub const QUICKNET_CHAIN_HASH: &str = "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"; +/// Active chain hash embedded in offchain HTTP paths; always Quicknet today. const CHAIN_HASH: &str = QUICKNET_CHAIN_HASH; +/// Max consecutive rounds the offchain worker pulls in one catch-up batch. pub const MAX_PULSES_TO_FETCH: u64 = 50; -pub const MAX_KEPT_PULSES: u64 = 216_000; // 1 week +/// Max pulses retained on-chain (~1 week of 3s Quicknet rounds). +pub const MAX_KEPT_PULSES: u64 = 216_000; +/// Max pulses removed in a single `prune_old_pulses` call (weight bound). pub const MAX_REMOVED_PULSES: u64 = 100; /// Defines application identifier for crypto keys of this module. @@ -162,29 +167,30 @@ pub mod pallet { #[pallet::config] pub trait Config: CreateBare> + SigningTypes + frame_system::Config { - /// The identifier type for an offchain worker. + /// Offchain-worker key type used to sign unsigned pulse / config payloads. type AuthorityId: AppCrypto; - /// something that knows how to verify beacon pulses + /// BLS verifier for Quicknet pulses (pairing check against [`BeaconConfig`]). type Verifier: Verifier; - /// A configuration for base priority of unsigned transactions. + /// Base mempool priority for this pallet's unsigned transactions. /// - /// This is exposed so that it can be tuned for particular runtime, when - /// multiple pallets send unsigned transactions. + /// Tunable per runtime when several pallets compete for unsigned inclusion. #[pallet::constant] type UnsignedPriority: Get; - /// The maximum number of milliseconds we are willing to wait for the HTTP request to - /// complete. + /// HTTP deadline for offchain drand fetches, in milliseconds. #[pallet::constant] type HttpFetchTimeout: Get; - /// Weight information for extrinsics in this pallet. + /// Extrinsic weight benchmarks for this pallet. type WeightInfo: crate::weights::WeightInfo; } - /// the drand beacon configuration + /// Quicknet beacon parameters (public key, period, genesis time, chain hashes). + /// + /// Defaults to the live Quicknet info; root may overwrite via [`Call::set_beacon_config`]. #[pallet::storage] pub type BeaconConfig = StorageValue<_, BeaconConfiguration, ValueQuery, DefaultBeaconConfig>; + /// Genesis / empty-storage default: canonical Quicknet beacon info. #[pallet::type_value] pub fn DefaultBeaconConfig() -> BeaconConfiguration { BeaconConfiguration { @@ -216,57 +222,62 @@ pub mod pallet { } } - /// Define a maximum length for the migration key + /// Max bytes for a migration name key in [`HasMigrationRun`]. type MigrationKeyMaxLen = ConstU32<128>; - /// Storage for migration run status + /// Idempotency flags for named storage migrations (`migrate_*` string keys). #[pallet::storage] pub type HasMigrationRun = StorageMap<_, Identity, BoundedVec, bool, ValueQuery>; - /// map round number to pulse + /// Verified Quicknet pulses keyed by round number. + /// + /// Pruned from the low end when the window exceeds [`MAX_KEPT_PULSES`]. #[pallet::storage] pub type Pulses = StorageMap<_, Blake2_128Concat, RoundNumber, Pulse, OptionQuery>; + /// Highest round successfully written to [`Pulses`]; `0` means no pulse yet. + /// + /// After the first pulse anchors storage, new rounds must be exactly `last + 1`. #[pallet::storage] pub type LastStoredRound = StorageValue<_, RoundNumber, ValueQuery>; - /// oldest stored round + /// Lowest round still present in [`Pulses`]; advances as old pulses are pruned. #[pallet::storage] pub type OldestStoredRound = StorageValue<_, RoundNumber, ValueQuery>; - /// Defines the block when next unsigned transaction will be accepted. + /// Earliest block at which another unsigned extrinsic from this pallet is accepted. /// - /// To prevent spam of unsigned (and unpaid!) transactions on the network, - /// we only allow one transaction per block. - /// This storage entry defines when new transaction is going to be accepted. + /// Spam control for unpaid unsigned txs: after a successful write, further unsigned + /// submissions are stale until this block (same-block multi-tx still allowed for + /// ascending rounds — see `validate_transaction_parameters`). #[pallet::storage] pub(super) type NextUnsignedAt = StorageValue<_, BlockNumberFor, ValueQuery>; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { - /// Beacon Configuration has changed. + /// Root replaced [`BeaconConfig`] via [`Call::set_beacon_config`]. BeaconConfigChanged, - /// Successfully set a new pulse(s). + /// One or more pulses were verified and inserted; `rounds` are the new keys in [`Pulses`]. NewPulse { rounds: Vec }, - /// Oldest Stored Round has been set. + /// Root set [`OldestStoredRound`] via [`Call::set_oldest_stored_round`]. SetOldestStoredRound(u64), } #[pallet::error] pub enum Error { - /// The value retrieved was `None` as no value was previously set. + /// Placeholder: a required storage value was missing (unused by current paths). NoneValue, - /// There was an attempt to increment the value in storage over `u32::MAX`. + /// Placeholder: a counter would overflow `u32::MAX` (unused by current paths). StorageOverflow, - /// failed to connect to the + /// Offchain path could not reach any drand HTTP endpoint. DrandConnectionFailure, - /// the pulse is invalid + /// Pulse failed the configured [`Config::Verifier`] check (legacy alias). UnverifiedPulse, - /// the round number did not increment + /// Round is not the required next value after [`LastStoredRound`] (or `> 0` when empty). InvalidRoundNumber, - /// the pulse could not be verified + /// Verifier returned an error while checking the pulse signature / pairing. PulseVerificationError, } @@ -274,7 +285,7 @@ pub mod pallet { impl Hooks> for Pallet { fn offchain_worker(block_number: BlockNumberFor) { log::debug!("Drand OCW working on block: {block_number:?}"); - if let Err(e) = Self::fetch_drand_pulse_and_send_unsigned(block_number) { + if let Err(e) = Self::fetch_and_submit_pulses(block_number) { log::debug!("Drand: Failed to fetch pulse from drand. {e:?}"); } } @@ -333,7 +344,11 @@ pub mod pallet { #[pallet::call] impl Pallet { - /// Verify and write a pulse from the beacon into the runtime + /// Verify each pulse in `pulses_payload` and append consecutive rounds to [`Pulses`]. + /// + /// Unsigned only (`ensure_none`). First successful write anchors both round markers; + /// afterward every pulse must be exactly `LastStoredRound + 1` so gaps cannot wedge + /// timelock / reveal consumers. Prunes old rounds after the write. #[pallet::call_index(0)] #[pallet::weight(T::WeightInfo::write_pulse())] pub fn write_pulse( @@ -415,12 +430,9 @@ pub mod pallet { Ok(()) } - /// allows the root user to set the beacon configuration - /// generally this would be called from an offchain worker context. - /// there is no verification of configurations, so be careful with this. + /// Root-only: replace [`BeaconConfig`] with `config_payload.config` (no BLS check). /// - /// * `origin`: the root user - /// * `config`: the beacon configuration + /// Intended for governance / bootstrap; a wrong key makes all later pulses fail verification. #[pallet::call_index(1)] #[pallet::weight(T::WeightInfo::set_beacon_config())] pub fn set_beacon_config( @@ -439,7 +451,9 @@ pub mod pallet { Ok(()) } - /// allows the root user to set the oldest stored round + /// Root-only: set [`OldestStoredRound`] without pruning [`Pulses`] entries. + /// + /// Does not delete pulse data; use when repairing the prune watermark after a migration. #[pallet::call_index(2)] #[pallet::weight(T::WeightInfo::set_oldest_stored_round())] pub fn set_oldest_stored_round(origin: OriginFor, oldest_round: u64) -> DispatchResult { @@ -452,11 +466,9 @@ pub mod pallet { } impl Pallet { - /// fetch the latest public pulse from the configured drand beacon - /// then send a signed transaction to include it on-chain - fn fetch_drand_pulse_and_send_unsigned( - block_number: BlockNumberFor, - ) -> Result<(), &'static str> { + /// Offchain: pull missing Quicknet rounds (up to [`MAX_PULSES_TO_FETCH`]) and submit + /// one unsigned [`Call::write_pulse`] per round in ascending order. + fn fetch_and_submit_pulses(block_number: BlockNumberFor) -> Result<(), &'static str> { // Ensure we can send an unsigned transaction let next_unsigned_at = NextUnsignedAt::::get(); if next_unsigned_at > block_number { @@ -530,20 +542,21 @@ impl Pallet { Ok(()) } + /// GET `/{chain}/public/{round}` from the first healthy [`ENDPOINTS`] host. fn fetch_drand_by_round(round: RoundNumber) -> Result { let relative_path = format!("/{CHAIN_HASH}/public/{round}"); - Self::fetch_and_decode_from_any_endpoint(&relative_path) + Self::fetch_drand_json_response(&relative_path) } + /// GET `/{chain}/public/latest` from the first healthy [`ENDPOINTS`] host. fn fetch_drand_latest() -> Result { let relative_path = format!("/{CHAIN_HASH}/public/latest"); - Self::fetch_and_decode_from_any_endpoint(&relative_path) + Self::fetch_drand_json_response(&relative_path) } - /// Try to fetch from multiple endpoints simultaneously and return the first successfully decoded JSON response. - fn fetch_and_decode_from_any_endpoint( - relative_path: &str, - ) -> Result { + /// Race HTTP GETs across [`ENDPOINTS`] and return the first JSON body that decodes as + /// [`DrandResponseBody`], or an error if every host fails / times out. + fn fetch_drand_json_response(relative_path: &str) -> Result { let uris: Vec = ENDPOINTS .iter() .map(|e| format!("{e}{relative_path}")) @@ -633,8 +646,7 @@ impl Pallet { Err("Drand: No valid response from any endpoint") } - /// get the randomness at a specific block height - /// returns [0u8;32] if it does not exist + /// Return the 32-byte Quicknet randomness for `round`, or `[0u8; 32]` if missing / malformed. pub fn random_at(round: RoundNumber) -> [u8; 32] { let pulse = Pulses::::get(round).unwrap_or_default(); let rand = pulse.randomness.clone(); @@ -643,6 +655,7 @@ impl Pallet { bounded_rand } + /// Verify the offchain authority signature, then apply unsigned-tx freshness rules. fn validate_signature_and_parameters( payload: &impl SignedPayload, signature: &T::Signature, @@ -658,6 +671,8 @@ impl Pallet { Self::validate_transaction_parameters(block_number, public, rounds) } + /// Mempool rules for unsigned calls: block number freshness, single-round payloads, + /// no stale / far-ahead rounds, and priority favoring lower rounds first. fn validate_transaction_parameters( block_number: &BlockNumberFor, public: &T::Public, @@ -725,6 +740,8 @@ impl Pallet { } } + /// Delete pulses from [`OldestStoredRound`] upward until the kept window is + /// ≤ [`MAX_KEPT_PULSES`], removing at most [`MAX_REMOVED_PULSES`] entries per call. fn prune_old_pulses(last_stored_round: RoundNumber) { let mut oldest = OldestStoredRound::::get(); if oldest == 0 { @@ -744,7 +761,10 @@ impl Pallet { } } -/// construct a message (e.g. signed by drand) +/// SHA-256(`prev_sig || round_be_bytes`) — message bytes hashed-to-curve for chained beacons. +/// +/// Quicknet is unchained and uses an empty `prev_sig`; verification uses +/// [`verifier::hash_unchained_round_message`] instead. pub fn message(current_round: RoundNumber, prev_sig: &[u8]) -> Vec { let mut hasher = Sha256::default(); hasher.update(prev_sig); @@ -753,7 +773,7 @@ pub fn message(current_round: RoundNumber, prev_sig: &[u8]) -> Vec { } impl Randomness> for Pallet { - // this function hashes together the subject with the latest known randomness from quicknet + /// Mix `subject` with the latest stored Quicknet randomness and `block_number - 1`. fn random(subject: &[u8]) -> (T::Hash, BlockNumberFor) { let block_number_minus_one = >::block_number().saturating_sub(One::one()); diff --git a/pallets/drand/src/migrations/migrate_prune_old_pulses.rs b/pallets/drand/src/migrations/migrate_prune_old_pulses.rs index 0a6697f6fd..42aa34086b 100644 --- a/pallets/drand/src/migrations/migrate_prune_old_pulses.rs +++ b/pallets/drand/src/migrations/migrate_prune_old_pulses.rs @@ -1,7 +1,11 @@ +//! One-shot migration: trim [`Pulses`](crate::Pulses) to [`MAX_KEPT_PULSES`](crate::MAX_KEPT_PULSES) +//! and set [`OldestStoredRound`](crate::OldestStoredRound) / [`LastStoredRound`](crate::LastStoredRound). + use crate::*; use frame_support::{traits::Get, weights::Weight}; use log; +/// Prune excess historical pulses and refresh round watermarks; idempotent via [`HasMigrationRun`]. pub fn migrate_prune_old_pulses() -> Weight { let migration_name = BoundedVec::truncate_from(b"migrate_prune_old_pulses".to_vec()); diff --git a/pallets/drand/src/migrations/migrate_set_oldest_round.rs b/pallets/drand/src/migrations/migrate_set_oldest_round.rs index c1ef6b9e04..219f8a9842 100644 --- a/pallets/drand/src/migrations/migrate_set_oldest_round.rs +++ b/pallets/drand/src/migrations/migrate_set_oldest_round.rs @@ -1,8 +1,12 @@ +//! One-shot migration: set [`OldestStoredRound`](crate::OldestStoredRound) to the minimum +//! key present in [`Pulses`](crate::Pulses) (or `0` if empty). + use crate::*; use frame_support::weights::Weight; use log; -/// Migration to set `OldestStoredRound` to the oldest round in storage. +/// Scan `Pulses` keys once and write the minimum as `OldestStoredRound`; idempotent via +/// [`HasMigrationRun`](crate::HasMigrationRun). Does not modify `LastStoredRound` or pulse data. pub fn migrate_set_oldest_round() -> Weight { use frame_support::traits::Get; diff --git a/pallets/drand/src/migrations/mod.rs b/pallets/drand/src/migrations/mod.rs index 7518996fc9..7de5b61884 100644 --- a/pallets/drand/src/migrations/mod.rs +++ b/pallets/drand/src/migrations/mod.rs @@ -1,3 +1,5 @@ +//! Storage migrations for `pallet-drand` (pulse window repair / prune watermarks). + pub mod migrate_prune_old_pulses; pub use migrate_prune_old_pulses::*; pub mod migrate_set_oldest_round; diff --git a/pallets/drand/src/mock.rs b/pallets/drand/src/mock.rs index aa370292b1..9ec783d383 100644 --- a/pallets/drand/src/mock.rs +++ b/pallets/drand/src/mock.rs @@ -1,3 +1,5 @@ +//! Minimal runtime + keystore fixture for `pallet-drand` unit tests. + use crate as pallet_drand_bridge; use crate::verifier::*; use crate::*; @@ -87,7 +89,7 @@ impl pallet_drand_bridge::Config for Test { type WeightInfo = (); } -// Build genesis storage according to the mock runtime. +/// Genesis storage plus an in-memory keystore with Alice's `drnd` sr25519 key. pub fn new_test_ext() -> sp_io::TestExternalities { let t = frame_system::GenesisConfig::::default() .build_storage() diff --git a/pallets/drand/src/tests.rs b/pallets/drand/src/tests.rs deleted file mode 100644 index ae6cb25ea3..0000000000 --- a/pallets/drand/src/tests.rs +++ /dev/null @@ -1,912 +0,0 @@ -/* - * Copyright 2024 by Ideal Labs, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -use crate::{ - BeaconConfig, BeaconConfigurationPayload, BeaconInfoResponse, Call, DrandResponseBody, - ENDPOINTS, Error, HasMigrationRun, LastStoredRound, MAX_KEPT_PULSES, OldestStoredRound, Pulse, - Pulses, PulsesPayload, QUICKNET_CHAIN_HASH, migrations::migrate_prune_old_pulses, - migrations::migrate_set_oldest_round, mock::*, -}; -use codec::Encode; -use frame_support::{ - BoundedVec, assert_noop, assert_ok, - pallet_prelude::{InvalidTransaction, TransactionSource}, - weights::RuntimeDbWeight, -}; -use frame_system::RawOrigin; -use sp_core::Get; -use sp_runtime::{ - offchain::{ - OffchainWorkerExt, - testing::{PendingRequest, TestOffchainExt}, - }, - traits::ValidateUnsigned, -}; - -// The round number used to collect drand pulses -pub const ROUND_NUMBER: u64 = 1000; - -// Quicknet parameters -pub const DRAND_PULSE: &str = "{\"round\":1000,\"randomness\":\"fe290beca10872ef2fb164d2aa4442de4566183ec51c56ff3cd603d930e54fdd\",\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}"; -pub const DRAND_INFO_RESPONSE: &str = "{\"public_key\":\"83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a\",\"period\":3,\"genesis_time\":1692803367,\"hash\":\"52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971\",\"groupHash\":\"f477d5c89f21a17c863a7f937c6a6d15859414d2be09cd448d4279af331c5d3e\",\"schemeID\":\"bls-unchained-g1-rfc9380\",\"metadata\":{\"beaconID\":\"quicknet\"}}"; -const INVALID_JSON: &str = r#"{"round":1000,"randomness":"not base64??","signature":}"#; - -#[test] -fn it_can_submit_valid_pulse_when_beacon_config_exists() { - new_test_ext().execute_with(|| { - let u_p: DrandResponseBody = serde_json::from_str(DRAND_PULSE).unwrap(); - let p: Pulse = u_p.try_into_pulse().unwrap(); - - let alice = sp_keyring::Sr25519Keyring::Alice; - let block_number = 100_000_000; - System::set_block_number(block_number); - - // Set the beacon config - let info: BeaconInfoResponse = serde_json::from_str(DRAND_INFO_RESPONSE).unwrap(); - let config_payload = BeaconConfigurationPayload { - block_number, - config: info.clone().try_into_beacon_config().unwrap(), - public: alice.public(), - }; - - // The signature doesn't really matter here because the signature is validated in the - // transaction validation phase not in the dispatchable itself. - let signature = None; - assert_ok!(Drand::set_beacon_config( - RuntimeOrigin::root(), - config_payload, - signature - )); - - let pulses_payload = PulsesPayload { - pulses: vec![p.clone()], - block_number, - public: alice.public(), - }; - - // Dispatch an unsigned extrinsic. - assert_ok!(Drand::write_pulse( - RuntimeOrigin::none(), - pulses_payload, - signature - )); - - // Read pallet storage and assert an expected result. - let pulse = Pulses::::get(ROUND_NUMBER); - assert!(pulse.is_some()); - assert_eq!(pulse, Some(p)); - }); -} - -#[test] -fn it_rejects_invalid_pulse_due_to_bad_signature() { - new_test_ext().execute_with(|| { - let alice = sp_keyring::Sr25519Keyring::Alice; - let block_number = 100_000_000; - System::set_block_number(block_number); - - // Set the beacon config using Root origin - let info: BeaconInfoResponse = serde_json::from_str(DRAND_INFO_RESPONSE).unwrap(); - let config_payload = BeaconConfigurationPayload { - block_number, - config: info.try_into_beacon_config().unwrap(), - public: alice.public(), - }; - // Signature is not required for Root origin - let config_signature = None; - assert_ok!(Drand::set_beacon_config( - RuntimeOrigin::root(), - config_payload.clone(), - config_signature - )); - - // Get a bad pulse (invalid signature within the pulse data) - let bad_http_response = "{\"round\":1000,\"randomness\":\"87f03ef5f62885390defedf60d5b8132b4dc2115b1efc6e99d166a37ab2f3a02\",\"signature\":\"b0a8b04e009cf72534321aca0f50048da596a3feec1172a0244d9a4a623a3123d0402da79854d4c705e94bc73224c341\"}"; - let u_p: DrandResponseBody = serde_json::from_str(bad_http_response).unwrap(); - let p: Pulse = u_p.try_into_pulse().unwrap(); - - // Prepare the pulses payload - let pulses_payload = PulsesPayload { - pulses: vec![p.clone()], - block_number, - public: alice.public(), - }; - let pulses_signature = alice.sign(&pulses_payload.encode()); - - assert_noop!( - Drand::write_pulse( - RawOrigin::None.into(), - pulses_payload.clone(), - Some(pulses_signature) - ), - Error::::PulseVerificationError - ); - - let pulse = Pulses::::get(ROUND_NUMBER); - assert!(pulse.is_none()); - }); -} - -#[test] -fn it_rejects_pulses_with_non_incremental_round_numbers() { - new_test_ext().execute_with(|| { - let block_number = 100_000_000; - let alice = sp_keyring::Sr25519Keyring::Alice; - System::set_block_number(block_number); - - // Set the beacon config - let info: BeaconInfoResponse = serde_json::from_str(DRAND_INFO_RESPONSE).unwrap(); - let config_payload = BeaconConfigurationPayload { - block_number, - config: info.clone().try_into_beacon_config().unwrap(), - public: alice.public(), - }; - // The signature doesn't really matter here because the signature is validated in the - // transaction validation phase not in the dispatchable itself. - let signature = None; - assert_ok!(Drand::set_beacon_config( - RuntimeOrigin::root(), - config_payload, - signature - )); - - let u_p: DrandResponseBody = serde_json::from_str(DRAND_PULSE).unwrap(); - let p: Pulse = u_p.try_into_pulse().unwrap(); - let pulses_payload = PulsesPayload { - pulses: vec![p.clone()], - block_number, - public: alice.public(), - }; - - // Dispatch an unsigned extrinsic. - assert_ok!(Drand::write_pulse( - RuntimeOrigin::none(), - pulses_payload.clone(), - signature - )); - let pulse = Pulses::::get(ROUND_NUMBER); - assert!(pulse.is_some()); - - System::set_block_number(2); - - // Attempt to submit the same pulse again, which should fail - assert_noop!( - Drand::write_pulse(RuntimeOrigin::none(), pulses_payload, signature), - Error::::InvalidRoundNumber, - ); - }); -} - -#[test] -fn write_pulse_rejects_round_skip() { - // A single pulse must not be allowed to leap LastStoredRound past the rounds - // in between, or those rounds can never be stored and any reveal/timelock that - // references them is wedged (#2794). Here round 1000 is a valid (BLS-verified) - // pulse, but LastStoredRound is seeded to 998 so round 1000 is a skip of two. - new_test_ext().execute_with(|| { - let block_number = 100_000_000; - let alice = sp_keyring::Sr25519Keyring::Alice; - System::set_block_number(block_number); - - let info: BeaconInfoResponse = serde_json::from_str(DRAND_INFO_RESPONSE).unwrap(); - let config_payload = BeaconConfigurationPayload { - block_number, - config: info.clone().try_into_beacon_config().unwrap(), - public: alice.public(), - }; - let signature = None; - assert_ok!(Drand::set_beacon_config( - RuntimeOrigin::root(), - config_payload, - signature - )); - - // Seed an existing baseline so this is not the anchor (first) storage. - LastStoredRound::::put(998); - OldestStoredRound::::put(998); - - let u_p: DrandResponseBody = serde_json::from_str(DRAND_PULSE).unwrap(); - let p: Pulse = u_p.try_into_pulse().unwrap(); - let pulses_payload = PulsesPayload { - pulses: vec![p.clone()], - block_number, - public: alice.public(), - }; - - // Round 1000 is NOT last(998) + 1, so it must be rejected. - assert_noop!( - Drand::write_pulse(RuntimeOrigin::none(), pulses_payload, signature), - Error::::InvalidRoundNumber, - ); - - // State is unchanged: no leap, nothing stored. - assert_eq!(LastStoredRound::::get(), 998); - assert!(Pulses::::get(ROUND_NUMBER).is_none()); - }); -} - -#[test] -fn write_pulse_accepts_consecutive_round() { - // The strict-advance rule must still accept the legitimate next round. - // Round 1000 == last(999) + 1, so it is stored. - new_test_ext().execute_with(|| { - let block_number = 100_000_000; - let alice = sp_keyring::Sr25519Keyring::Alice; - System::set_block_number(block_number); - - let info: BeaconInfoResponse = serde_json::from_str(DRAND_INFO_RESPONSE).unwrap(); - let config_payload = BeaconConfigurationPayload { - block_number, - config: info.clone().try_into_beacon_config().unwrap(), - public: alice.public(), - }; - let signature = None; - assert_ok!(Drand::set_beacon_config( - RuntimeOrigin::root(), - config_payload, - signature - )); - - LastStoredRound::::put(999); - OldestStoredRound::::put(999); - - let u_p: DrandResponseBody = serde_json::from_str(DRAND_PULSE).unwrap(); - let p: Pulse = u_p.try_into_pulse().unwrap(); - let pulses_payload = PulsesPayload { - pulses: vec![p.clone()], - block_number, - public: alice.public(), - }; - - assert_ok!(Drand::write_pulse( - RuntimeOrigin::none(), - pulses_payload, - signature - )); - assert_eq!(LastStoredRound::::get(), ROUND_NUMBER); - assert!(Pulses::::get(ROUND_NUMBER).is_some()); - }); -} - -#[test] -fn it_blocks_non_root_from_submit_beacon_info() { - new_test_ext().execute_with(|| { - let block_number = 100_000_000; - let alice = sp_keyring::Sr25519Keyring::Alice; - System::set_block_number(block_number); - - // Prepare the beacon configuration payload - let info: BeaconInfoResponse = serde_json::from_str(DRAND_INFO_RESPONSE).unwrap(); - let config_payload = BeaconConfigurationPayload { - block_number, - config: info.try_into_beacon_config().unwrap(), - public: alice.public(), - }; - - // Signature is not required when using Root origin, but we'll include it for completeness - let signature = None; - - // Attempt to set the beacon config with a non-root origin (signed by Alice) - // Expect it to fail with BadOrigin - assert_noop!( - Drand::set_beacon_config( - RuntimeOrigin::signed(alice.public()), - config_payload.clone(), - signature - ), - sp_runtime::DispatchError::BadOrigin - ); - - // Attempt to set the beacon config with an unsigned origin - // Expect it to fail with BadOrigin - assert_noop!( - Drand::set_beacon_config(RuntimeOrigin::none(), config_payload.clone(), signature), - sp_runtime::DispatchError::BadOrigin - ); - - // Now attempt to set the beacon config with Root origin - // Expect it to succeed - assert_ok!(Drand::set_beacon_config( - RuntimeOrigin::root(), - config_payload, - signature - )); - - // Verify that the BeaconConfig storage item has been updated - let stored_config = BeaconConfig::::get(); - assert_eq!(stored_config, info.try_into_beacon_config().unwrap()); - }); -} - -#[test] -fn signed_cannot_submit_beacon_info() { - new_test_ext().execute_with(|| { - let block_number = 100_000_000; - let alice = sp_keyring::Sr25519Keyring::Alice; - System::set_block_number(block_number); - - // Set the beacon config - let info: BeaconInfoResponse = serde_json::from_str(DRAND_INFO_RESPONSE).unwrap(); - let config_payload = BeaconConfigurationPayload { - block_number, - config: info.clone().try_into_beacon_config().unwrap(), - public: alice.public(), - }; - // The signature doesn't really matter here because the signature is validated in the - // transaction validation phase not in the dispatchable itself. - let signature = None; - // Dispatch a signed extrinsic - assert_noop!( - Drand::set_beacon_config( - RuntimeOrigin::signed(alice.public()), - config_payload, - signature - ), - sp_runtime::DispatchError::BadOrigin - ); - }); -} - -#[test] -fn test_validate_unsigned_write_pulse() { - new_test_ext().execute_with(|| { - let block_number = 100_000_000; - let alice = sp_keyring::Sr25519Keyring::Alice; - System::set_block_number(block_number); - - let pulse = Pulse { - round: 1, - randomness: frame_support::BoundedVec::truncate_from(vec![0u8; 32]), - signature: frame_support::BoundedVec::truncate_from(vec![1u8; 96]), - }; - - let pulses_payload = PulsesPayload { - block_number, - pulses: vec![pulse], - public: alice.public(), - }; - let signature = alice.sign(&pulses_payload.encode()); - - let call = Call::write_pulse { - pulses_payload: pulses_payload.clone(), - signature: Some(signature), - }; - - let source = TransactionSource::External; - let validity = Drand::validate_unsigned(source, &call); - - assert_ok!(validity); - }); -} - -#[test] -fn validate_unsigned_accepts_first_live_round_as_storage_anchor() { - // On a fresh chain the current drand round is far beyond the normal catch-up - // window. It must be admitted so `write_pulse` can anchor both round markers. - new_test_ext().execute_with(|| { - let block_number = 100_000_000; - let alice = sp_keyring::Sr25519Keyring::Alice; - System::set_block_number(block_number); - - assert_eq!(LastStoredRound::::get(), 0); - assert_eq!(OldestStoredRound::::get(), 0); - - let pulse = Pulse { - round: crate::MAX_PULSES_TO_FETCH + 1, - randomness: frame_support::BoundedVec::truncate_from(vec![0u8; 32]), - signature: frame_support::BoundedVec::truncate_from(vec![1u8; 96]), - }; - let pulses_payload = PulsesPayload { - block_number, - pulses: vec![pulse], - public: alice.public(), - }; - let signature = alice.sign(&pulses_payload.encode()); - let call = Call::write_pulse { - pulses_payload, - signature: Some(signature), - }; - - assert_ok!(Drand::validate_unsigned(TransactionSource::Local, &call)); - }); -} - -#[test] -fn validate_unsigned_rejects_round_too_far_ahead() { - // A round that would leap LastStoredRound by more than the offchain worker ever - // submits in one run is not a legitimate catch-up pulse. Drop it at the mempool - // before it can reach dispatch (#2794). - new_test_ext().execute_with(|| { - let block_number = 100_000_000; - let alice = sp_keyring::Sr25519Keyring::Alice; - System::set_block_number(block_number); - - LastStoredRound::::put(100); - - // 151 == last(100) + MAX_PULSES_TO_FETCH(50) + 1, i.e. one beyond the cap. - let pulse = Pulse { - round: 100 + crate::MAX_PULSES_TO_FETCH + 1, - randomness: frame_support::BoundedVec::truncate_from(vec![0u8; 32]), - signature: frame_support::BoundedVec::truncate_from(vec![1u8; 96]), - }; - let pulses_payload = PulsesPayload { - block_number, - pulses: vec![pulse], - public: alice.public(), - }; - let signature = alice.sign(&pulses_payload.encode()); - - let call = Call::write_pulse { - pulses_payload: pulses_payload.clone(), - signature: Some(signature), - }; - - let source = TransactionSource::External; - let validity = Drand::validate_unsigned(source, &call); - - assert_noop!(validity, InvalidTransaction::Stale); - }); -} - -#[test] -fn test_not_validate_unsigned_write_pulse_with_bad_proof() { - new_test_ext().execute_with(|| { - let block_number = 100_000_000; - let alice = sp_keyring::Sr25519Keyring::Alice; - System::set_block_number(block_number); - let pulses_payload = PulsesPayload { - block_number, - pulses: vec![], - public: alice.public(), - }; - - // Bad signature - let signature = ::Signature::default(); - let call = Call::write_pulse { - pulses_payload: pulses_payload.clone(), - signature: Some(signature), - }; - - let source = TransactionSource::External; - let validity = Drand::validate_unsigned(source, &call); - - assert_noop!(validity, InvalidTransaction::BadProof); - }); -} - -#[test] -fn test_not_validate_unsigned_write_pulse_with_no_payload_signature() { - new_test_ext().execute_with(|| { - let block_number = 100_000_000; - let alice = sp_keyring::Sr25519Keyring::Alice; - System::set_block_number(block_number); - let pulses_payload = PulsesPayload { - block_number, - pulses: vec![], - public: alice.public(), - }; - - // No signature - let signature = None; - let call = Call::write_pulse { - pulses_payload: pulses_payload.clone(), - signature, - }; - - let source = TransactionSource::External; - let validity = Drand::validate_unsigned(source, &call); - - assert_noop!(validity, InvalidTransaction::BadSigner); - }); -} - -#[test] -fn can_execute_and_handle_valid_http_responses() { - use serde_json; - - let expected_pulse: DrandResponseBody = serde_json::from_str(DRAND_PULSE).unwrap(); - - let (offchain, state) = TestOffchainExt::new(); - let mut t = sp_io::TestExternalities::default(); - t.register_extension(OffchainWorkerExt::new(offchain)); - - { - let mut state = state.write(); - - for endpoint in ENDPOINTS.iter() { - state.expect_request(PendingRequest { - method: "GET".into(), - uri: format!("{endpoint}/{QUICKNET_CHAIN_HASH}/public/1000"), - response: Some(DRAND_PULSE.as_bytes().to_vec()), - sent: true, - ..Default::default() - }); - } - - for endpoint in ENDPOINTS.iter() { - state.expect_request(PendingRequest { - method: "GET".into(), - uri: format!("{endpoint}/{QUICKNET_CHAIN_HASH}/public/latest"), - response: Some(DRAND_PULSE.as_bytes().to_vec()), - sent: true, - ..Default::default() - }); - } - } - - t.execute_with(|| { - let actual_specific = Drand::fetch_drand_by_round(1000u64).unwrap(); - assert_eq!(actual_specific, expected_pulse); - - let actual_pulse = Drand::fetch_drand_latest().unwrap(); - assert_eq!(actual_pulse, expected_pulse); - }); -} - -#[test] -fn validate_unsigned_rejects_future_block_number() { - new_test_ext().execute_with(|| { - let block_number = 100_000_000; - let future_block_number = 100_000_100; - let alice = sp_keyring::Sr25519Keyring::Alice; - System::set_block_number(block_number); - let pulses_payload = PulsesPayload { - block_number: future_block_number, - pulses: vec![], - public: alice.public(), - }; - let signature = alice.sign(&pulses_payload.encode()); - - let call = Call::write_pulse { - pulses_payload: pulses_payload.clone(), - signature: Some(signature), - }; - - let source = TransactionSource::External; - let validity = Drand::validate_unsigned(source, &call); - - assert_noop!(validity, InvalidTransaction::Future); - }); -} - -#[test] -fn test_all_endpoints_fail() { - let (offchain, state) = TestOffchainExt::new(); - let mut t = sp_io::TestExternalities::default(); - t.register_extension(OffchainWorkerExt::new(offchain)); - - { - let mut state = state.write(); - let endpoints = ENDPOINTS; - - for endpoint in endpoints.iter() { - state.expect_request(PendingRequest { - method: "GET".into(), - uri: format!("{endpoint}/{QUICKNET_CHAIN_HASH}/public/1000"), - response: Some(INVALID_JSON.as_bytes().to_vec()), - sent: true, - ..Default::default() - }); - } - } - - t.execute_with(|| { - let result = Drand::fetch_drand_by_round(1000u64); - assert!( - result.is_err(), - "All endpoints should fail due to invalid JSON responses" - ); - }); -} - -#[test] -fn test_eventual_success() { - let expected_pulse: DrandResponseBody = serde_json::from_str(DRAND_PULSE).unwrap(); - - let (offchain, state) = TestOffchainExt::new(); - let mut t = sp_io::TestExternalities::default(); - t.register_extension(OffchainWorkerExt::new(offchain)); - - { - let mut state = state.write(); - let endpoints = ENDPOINTS; - - // We'll make all endpoints except the last return invalid JSON. - // Since no meta is provided, these are "200 OK" but invalid JSON, causing decode failures. - // The last endpoint returns the valid DRAND_PULSE JSON, leading to success. - - // Endpoint 0: Invalid JSON (decode fail) - state.expect_request(PendingRequest { - method: "GET".into(), - uri: format!("{}/{}/public/1000", endpoints[0], QUICKNET_CHAIN_HASH), - response: Some(INVALID_JSON.as_bytes().to_vec()), - sent: true, - ..Default::default() - }); - - // Endpoint 1: Invalid JSON - state.expect_request(PendingRequest { - method: "GET".into(), - uri: format!("{}/{}/public/1000", endpoints[1], QUICKNET_CHAIN_HASH), - response: Some(Vec::new()), - sent: true, - ..Default::default() - }); - - // Endpoint 2: Invalid JSON - state.expect_request(PendingRequest { - method: "GET".into(), - uri: format!("{}/{}/public/1000", endpoints[2], QUICKNET_CHAIN_HASH), - response: Some(INVALID_JSON.as_bytes().to_vec()), - sent: true, - ..Default::default() - }); - - // Endpoint 3: Invalid JSON - state.expect_request(PendingRequest { - method: "GET".into(), - uri: format!("{}/{}/public/1000", endpoints[3], QUICKNET_CHAIN_HASH), - response: Some(INVALID_JSON.as_bytes().to_vec()), - sent: true, - ..Default::default() - }); - - // Endpoint 4: Valid JSON (success) - state.expect_request(PendingRequest { - method: "GET".into(), - uri: format!("{}/{}/public/1000", endpoints[4], QUICKNET_CHAIN_HASH), - response: Some(DRAND_PULSE.as_bytes().to_vec()), - sent: true, - ..Default::default() - }); - } - - t.execute_with(|| { - let actual = Drand::fetch_drand_by_round(1000u64).unwrap(); - assert_eq!( - actual, expected_pulse, - "Should succeed on the last endpoint after failing at the previous ones" - ); - }); -} - -#[test] -fn test_invalid_json_then_success() { - let expected_pulse: DrandResponseBody = serde_json::from_str(DRAND_PULSE).unwrap(); - - let (offchain, state) = TestOffchainExt::new(); - let mut t = sp_io::TestExternalities::default(); - t.register_extension(OffchainWorkerExt::new(offchain)); - - { - let mut state = state.write(); - - let endpoints = ENDPOINTS; - - // Endpoint 1: Invalid JSON - state.expect_request(PendingRequest { - method: "GET".into(), - uri: format!("{}/{}/public/1000", endpoints[0], QUICKNET_CHAIN_HASH), - response: Some(INVALID_JSON.as_bytes().to_vec()), - sent: true, - ..Default::default() - }); - - // Endpoint 2: Valid response - state.expect_request(PendingRequest { - method: "GET".into(), - uri: format!("{}/{}/public/1000", endpoints[1], QUICKNET_CHAIN_HASH), - response: Some(DRAND_PULSE.as_bytes().to_vec()), - sent: true, - ..Default::default() - }); - } - - t.execute_with(|| { - let actual = Drand::fetch_drand_by_round(1000u64).unwrap(); - assert_eq!(actual, expected_pulse); - }); -} - -#[test] -fn test_pulses_are_correctly_pruned() { - new_test_ext().execute_with(|| { - let pulse = Pulse::default(); - let last_round: u64 = MAX_KEPT_PULSES + 2; - let oldest_round: u64 = 1; - let prune_count: u64 = 2; - let new_oldest: u64 = oldest_round + prune_count; - let middle_round: u64 = MAX_KEPT_PULSES / 2; - - // Set storage bounds - OldestStoredRound::::put(oldest_round); - LastStoredRound::::put(last_round); - - // Insert pulses at boundaries - // These should be pruned - Pulses::::insert(1, pulse.clone()); - Pulses::::insert(2, pulse.clone()); - - // This should remain (new oldest) - Pulses::::insert(new_oldest, pulse.clone()); - - // Middle and last should remain - Pulses::::insert(middle_round, pulse.clone()); - Pulses::::insert(last_round, pulse.clone()); - - // Trigger prune - Drand::prune_old_pulses(last_round); - - // Assert new oldest - assert_eq!(OldestStoredRound::::get(), new_oldest); - - // Assert pruned correctly - assert!(!Pulses::::contains_key(1), "Round 1 should be pruned"); - assert!(!Pulses::::contains_key(2), "Round 2 should be pruned"); - - // Assert not pruned incorrectly - assert!( - Pulses::::contains_key(new_oldest), - "New oldest round should remain" - ); - assert!( - Pulses::::contains_key(middle_round), - "Middle round should remain" - ); - assert!( - Pulses::::contains_key(last_round), - "Last round should remain" - ); - }); -} - -#[test] -fn test_migrate_prune_old_pulses() { - new_test_ext().execute_with(|| { - let migration_name = BoundedVec::truncate_from(b"migrate_prune_old_pulses".to_vec()); - let pulse = Pulse::default(); - - assert_eq!(Pulses::::iter().count(), 0); - assert!(!HasMigrationRun::::get(&migration_name)); - assert_eq!(OldestStoredRound::::get(), 0); - assert_eq!(LastStoredRound::::get(), 0); - - // Test with more pulses than MAX_KEPT_PULSES - let excess: u64 = 9; - let total: u64 = MAX_KEPT_PULSES + excess; - for i in 1..=total { - Pulses::::insert(i, pulse.clone()); - } - - let weight_large = migrate_prune_old_pulses::(); - - let expected_oldest = excess + 1; - assert_eq!(OldestStoredRound::::get(), expected_oldest); - assert_eq!(LastStoredRound::::get(), total); - - for i in 1..=excess { - assert!(!Pulses::::contains_key(i)); - } - for i in expected_oldest..=total { - assert!(Pulses::::contains_key(i)); - } - - let db_weight: RuntimeDbWeight = ::DbWeight::get(); - let num_pulses = total; - let num_to_delete = num_pulses - MAX_KEPT_PULSES; - let expected_weight = db_weight.reads(1 + num_pulses) + db_weight.writes(num_to_delete + 3); - assert_eq!(weight_large, expected_weight); - }); -} - -#[test] -fn test_prune_maximum_of_100_pulses_per_call() { - new_test_ext().execute_with(|| { - // ------------------------------------------------------------ - // 1. Arrange – create a storage layout that exceeds MAX_KEPT_PULSES - // ------------------------------------------------------------ - const EXTRA: u64 = 250; - let oldest_round: u64 = 1; - let last_round: u64 = oldest_round + MAX_KEPT_PULSES + EXTRA; - - OldestStoredRound::::put(oldest_round); - LastStoredRound::::put(last_round); - let pulse = Pulse::default(); - - // Insert the first 150 rounds so we can check they disappear / stay - for r in oldest_round..=oldest_round + 150 { - Pulses::::insert(r, pulse.clone()); - } - let mid_round = oldest_round + 150; - Pulses::::insert(last_round, pulse.clone()); - - // ------------------------------------------------------------ - // 2. Act – run the pruning function once - // ------------------------------------------------------------ - Drand::prune_old_pulses(last_round); - - // ------------------------------------------------------------ - // 3. Assert – only the *first* 100 pulses were removed - // ------------------------------------------------------------ - let expected_new_oldest = oldest_round + 100; // 101 - - // ‣ Storage bound updated correctly - assert_eq!( - OldestStoredRound::::get(), - expected_new_oldest, - "OldestStoredRound should advance by exactly 100" - ); - - // ‣ Rounds 1‑100 are gone - for r in oldest_round..expected_new_oldest { - assert!( - !Pulses::::contains_key(r), - "Round {r} should have been pruned" - ); - } - - // ‣ Round 101 (new oldest) and later rounds remain - assert!( - Pulses::::contains_key(expected_new_oldest), - "Round {expected_new_oldest} should remain after pruning" - ); - assert!( - Pulses::::contains_key(mid_round), - "Mid-range round should remain after pruning" - ); - assert!( - Pulses::::contains_key(last_round), - "LastStoredRound should remain after pruning" - ); - }); -} - -#[test] -fn test_migrate_set_oldest_round() { - new_test_ext().execute_with(|| { - let migration_name = BoundedVec::truncate_from(b"migrate_set_oldest_round".to_vec()); - let db_weight: RuntimeDbWeight = ::DbWeight::get(); - let pulse = Pulse::default(); - - assert_eq!(Pulses::::iter().count(), 0); - assert!(!HasMigrationRun::::get(&migration_name)); - assert_eq!(OldestStoredRound::::get(), 0); - assert_eq!(LastStoredRound::::get(), 0); - - // Insert out-of-order rounds: oldest should be 5 - for r in [10u64, 7, 5].into_iter() { - Pulses::::insert(r, pulse.clone()); - } - let num_rounds = 3u64; - - // Run migration - let weight = migrate_set_oldest_round::(); - - assert_eq!(OldestStoredRound::::get(), 5); - // Migration does NOT touch LastStoredRound - assert_eq!(LastStoredRound::::get(), 0); - // Pulses untouched - assert!(Pulses::::contains_key(5)); - assert!(Pulses::::contains_key(7)); - assert!(Pulses::::contains_key(10)); - // Flag set - assert!(HasMigrationRun::::get(&migration_name)); - - // Weight: reads(1 + num_rounds) + writes(2) [Oldest + HasMigrationRun] - let expected = db_weight.reads(1 + num_rounds) + db_weight.writes(2); - assert_eq!(weight, expected); - }); -} diff --git a/pallets/drand/src/tests/beacon_config.rs b/pallets/drand/src/tests/beacon_config.rs new file mode 100644 index 0000000000..cafa3b8fd4 --- /dev/null +++ b/pallets/drand/src/tests/beacon_config.rs @@ -0,0 +1,80 @@ +use super::*; + +#[test] +fn it_blocks_non_root_from_submit_beacon_info() { + new_test_ext().execute_with(|| { + let block_number = 100_000_000; + let alice = sp_keyring::Sr25519Keyring::Alice; + System::set_block_number(block_number); + + // Prepare the beacon configuration payload + let info: BeaconInfoResponse = serde_json::from_str(DRAND_INFO_RESPONSE).unwrap(); + let config_payload = BeaconConfigurationPayload { + block_number, + config: info.try_into_beacon_config().unwrap(), + public: alice.public(), + }; + + // Signature is not required when using Root origin, but we'll include it for completeness + let signature = None; + + // Attempt to set the beacon config with a non-root origin (signed by Alice) + // Expect it to fail with BadOrigin + assert_noop!( + Drand::set_beacon_config( + RuntimeOrigin::signed(alice.public()), + config_payload.clone(), + signature + ), + sp_runtime::DispatchError::BadOrigin + ); + + // Attempt to set the beacon config with an unsigned origin + // Expect it to fail with BadOrigin + assert_noop!( + Drand::set_beacon_config(RuntimeOrigin::none(), config_payload.clone(), signature), + sp_runtime::DispatchError::BadOrigin + ); + + // Now attempt to set the beacon config with Root origin + // Expect it to succeed + assert_ok!(Drand::set_beacon_config( + RuntimeOrigin::root(), + config_payload, + signature + )); + + // Verify that the BeaconConfig storage item has been updated + let stored_config = BeaconConfig::::get(); + assert_eq!(stored_config, info.try_into_beacon_config().unwrap()); + }); +} + +#[test] +fn signed_cannot_submit_beacon_info() { + new_test_ext().execute_with(|| { + let block_number = 100_000_000; + let alice = sp_keyring::Sr25519Keyring::Alice; + System::set_block_number(block_number); + + // Set the beacon config + let info: BeaconInfoResponse = serde_json::from_str(DRAND_INFO_RESPONSE).unwrap(); + let config_payload = BeaconConfigurationPayload { + block_number, + config: info.clone().try_into_beacon_config().unwrap(), + public: alice.public(), + }; + // The signature doesn't really matter here because the signature is validated in the + // transaction validation phase not in the dispatchable itself. + let signature = None; + // Dispatch a signed extrinsic + assert_noop!( + Drand::set_beacon_config( + RuntimeOrigin::signed(alice.public()), + config_payload, + signature + ), + sp_runtime::DispatchError::BadOrigin + ); + }); +} diff --git a/pallets/drand/src/tests/http_fetch.rs b/pallets/drand/src/tests/http_fetch.rs new file mode 100644 index 0000000000..7f778661b8 --- /dev/null +++ b/pallets/drand/src/tests/http_fetch.rs @@ -0,0 +1,183 @@ +use super::*; + +#[test] +fn can_execute_and_handle_valid_http_responses() { + use serde_json; + + let expected_pulse: DrandResponseBody = serde_json::from_str(DRAND_PULSE).unwrap(); + + let (offchain, state) = TestOffchainExt::new(); + let mut t = sp_io::TestExternalities::default(); + t.register_extension(OffchainWorkerExt::new(offchain)); + + { + let mut state = state.write(); + + for endpoint in ENDPOINTS.iter() { + state.expect_request(PendingRequest { + method: "GET".into(), + uri: format!("{endpoint}/{QUICKNET_CHAIN_HASH}/public/1000"), + response: Some(DRAND_PULSE.as_bytes().to_vec()), + sent: true, + ..Default::default() + }); + } + + for endpoint in ENDPOINTS.iter() { + state.expect_request(PendingRequest { + method: "GET".into(), + uri: format!("{endpoint}/{QUICKNET_CHAIN_HASH}/public/latest"), + response: Some(DRAND_PULSE.as_bytes().to_vec()), + sent: true, + ..Default::default() + }); + } + } + + t.execute_with(|| { + let actual_specific = Drand::fetch_drand_by_round(1000u64).unwrap(); + assert_eq!(actual_specific, expected_pulse); + + let actual_pulse = Drand::fetch_drand_latest().unwrap(); + assert_eq!(actual_pulse, expected_pulse); + }); +} + +#[test] +fn test_all_endpoints_fail() { + let (offchain, state) = TestOffchainExt::new(); + let mut t = sp_io::TestExternalities::default(); + t.register_extension(OffchainWorkerExt::new(offchain)); + + { + let mut state = state.write(); + let endpoints = ENDPOINTS; + + for endpoint in endpoints.iter() { + state.expect_request(PendingRequest { + method: "GET".into(), + uri: format!("{endpoint}/{QUICKNET_CHAIN_HASH}/public/1000"), + response: Some(INVALID_JSON.as_bytes().to_vec()), + sent: true, + ..Default::default() + }); + } + } + + t.execute_with(|| { + let result = Drand::fetch_drand_by_round(1000u64); + assert!( + result.is_err(), + "All endpoints should fail due to invalid JSON responses" + ); + }); +} + +#[test] +fn test_eventual_success() { + let expected_pulse: DrandResponseBody = serde_json::from_str(DRAND_PULSE).unwrap(); + + let (offchain, state) = TestOffchainExt::new(); + let mut t = sp_io::TestExternalities::default(); + t.register_extension(OffchainWorkerExt::new(offchain)); + + { + let mut state = state.write(); + let endpoints = ENDPOINTS; + + // We'll make all endpoints except the last return invalid JSON. + // Since no meta is provided, these are "200 OK" but invalid JSON, causing decode failures. + // The last endpoint returns the valid DRAND_PULSE JSON, leading to success. + + // Endpoint 0: Invalid JSON (decode fail) + state.expect_request(PendingRequest { + method: "GET".into(), + uri: format!("{}/{}/public/1000", endpoints[0], QUICKNET_CHAIN_HASH), + response: Some(INVALID_JSON.as_bytes().to_vec()), + sent: true, + ..Default::default() + }); + + // Endpoint 1: Invalid JSON + state.expect_request(PendingRequest { + method: "GET".into(), + uri: format!("{}/{}/public/1000", endpoints[1], QUICKNET_CHAIN_HASH), + response: Some(Vec::new()), + sent: true, + ..Default::default() + }); + + // Endpoint 2: Invalid JSON + state.expect_request(PendingRequest { + method: "GET".into(), + uri: format!("{}/{}/public/1000", endpoints[2], QUICKNET_CHAIN_HASH), + response: Some(INVALID_JSON.as_bytes().to_vec()), + sent: true, + ..Default::default() + }); + + // Endpoint 3: Invalid JSON + state.expect_request(PendingRequest { + method: "GET".into(), + uri: format!("{}/{}/public/1000", endpoints[3], QUICKNET_CHAIN_HASH), + response: Some(INVALID_JSON.as_bytes().to_vec()), + sent: true, + ..Default::default() + }); + + // Endpoint 4: Valid JSON (success) + state.expect_request(PendingRequest { + method: "GET".into(), + uri: format!("{}/{}/public/1000", endpoints[4], QUICKNET_CHAIN_HASH), + response: Some(DRAND_PULSE.as_bytes().to_vec()), + sent: true, + ..Default::default() + }); + } + + t.execute_with(|| { + let actual = Drand::fetch_drand_by_round(1000u64).unwrap(); + assert_eq!( + actual, expected_pulse, + "Should succeed on the last endpoint after failing at the previous ones" + ); + }); +} + +#[test] +fn test_invalid_json_then_success() { + let expected_pulse: DrandResponseBody = serde_json::from_str(DRAND_PULSE).unwrap(); + + let (offchain, state) = TestOffchainExt::new(); + let mut t = sp_io::TestExternalities::default(); + t.register_extension(OffchainWorkerExt::new(offchain)); + + { + let mut state = state.write(); + + let endpoints = ENDPOINTS; + + // Endpoint 1: Invalid JSON + state.expect_request(PendingRequest { + method: "GET".into(), + uri: format!("{}/{}/public/1000", endpoints[0], QUICKNET_CHAIN_HASH), + response: Some(INVALID_JSON.as_bytes().to_vec()), + sent: true, + ..Default::default() + }); + + // Endpoint 2: Valid response + state.expect_request(PendingRequest { + method: "GET".into(), + uri: format!("{}/{}/public/1000", endpoints[1], QUICKNET_CHAIN_HASH), + response: Some(DRAND_PULSE.as_bytes().to_vec()), + sent: true, + ..Default::default() + }); + } + + t.execute_with(|| { + let actual = Drand::fetch_drand_by_round(1000u64).unwrap(); + assert_eq!(actual, expected_pulse); + }); +} diff --git a/pallets/drand/src/tests/migrations.rs b/pallets/drand/src/tests/migrations.rs new file mode 100644 index 0000000000..243ac6e754 --- /dev/null +++ b/pallets/drand/src/tests/migrations.rs @@ -0,0 +1,77 @@ +use super::*; + +#[test] +fn test_migrate_prune_old_pulses() { + new_test_ext().execute_with(|| { + let migration_name = BoundedVec::truncate_from(b"migrate_prune_old_pulses".to_vec()); + let pulse = Pulse::default(); + + assert_eq!(Pulses::::iter().count(), 0); + assert!(!HasMigrationRun::::get(&migration_name)); + assert_eq!(OldestStoredRound::::get(), 0); + assert_eq!(LastStoredRound::::get(), 0); + + // Test with more pulses than MAX_KEPT_PULSES + let excess: u64 = 9; + let total: u64 = MAX_KEPT_PULSES + excess; + for i in 1..=total { + Pulses::::insert(i, pulse.clone()); + } + + let weight_large = migrate_prune_old_pulses::(); + + let expected_oldest = excess + 1; + assert_eq!(OldestStoredRound::::get(), expected_oldest); + assert_eq!(LastStoredRound::::get(), total); + + for i in 1..=excess { + assert!(!Pulses::::contains_key(i)); + } + for i in expected_oldest..=total { + assert!(Pulses::::contains_key(i)); + } + + let db_weight: RuntimeDbWeight = ::DbWeight::get(); + let num_pulses = total; + let num_to_delete = num_pulses - MAX_KEPT_PULSES; + let expected_weight = db_weight.reads(1 + num_pulses) + db_weight.writes(num_to_delete + 3); + assert_eq!(weight_large, expected_weight); + }); +} + +#[test] +fn test_migrate_set_oldest_round() { + new_test_ext().execute_with(|| { + let migration_name = BoundedVec::truncate_from(b"migrate_set_oldest_round".to_vec()); + let db_weight: RuntimeDbWeight = ::DbWeight::get(); + let pulse = Pulse::default(); + + assert_eq!(Pulses::::iter().count(), 0); + assert!(!HasMigrationRun::::get(&migration_name)); + assert_eq!(OldestStoredRound::::get(), 0); + assert_eq!(LastStoredRound::::get(), 0); + + // Insert out-of-order rounds: oldest should be 5 + for r in [10u64, 7, 5].into_iter() { + Pulses::::insert(r, pulse.clone()); + } + let num_rounds = 3u64; + + // Run migration + let weight = migrate_set_oldest_round::(); + + assert_eq!(OldestStoredRound::::get(), 5); + // Migration does NOT touch LastStoredRound + assert_eq!(LastStoredRound::::get(), 0); + // Pulses untouched + assert!(Pulses::::contains_key(5)); + assert!(Pulses::::contains_key(7)); + assert!(Pulses::::contains_key(10)); + // Flag set + assert!(HasMigrationRun::::get(&migration_name)); + + // Weight: reads(1 + num_rounds) + writes(2) [Oldest + HasMigrationRun] + let expected = db_weight.reads(1 + num_rounds) + db_weight.writes(2); + assert_eq!(weight, expected); + }); +} diff --git a/pallets/drand/src/tests/mod.rs b/pallets/drand/src/tests/mod.rs new file mode 100644 index 0000000000..d19b5c538e --- /dev/null +++ b/pallets/drand/src/tests/mod.rs @@ -0,0 +1,40 @@ +//! Unit tests for `pallet-drand`, split by concept for discoverability. + +pub(crate) use crate::{ + BeaconConfig, BeaconConfigurationPayload, BeaconInfoResponse, Call, DrandResponseBody, + ENDPOINTS, Error, HasMigrationRun, LastStoredRound, MAX_KEPT_PULSES, OldestStoredRound, Pulse, + Pulses, PulsesPayload, QUICKNET_CHAIN_HASH, migrations::migrate_prune_old_pulses, + migrations::migrate_set_oldest_round, mock::*, +}; +pub(crate) use codec::Encode; +pub(crate) use frame_support::{ + BoundedVec, assert_noop, assert_ok, + pallet_prelude::{InvalidTransaction, TransactionSource}, + weights::RuntimeDbWeight, +}; +pub(crate) use frame_system::RawOrigin; +pub(crate) use sp_core::Get; +pub(crate) use sp_runtime::{ + offchain::{ + OffchainWorkerExt, + testing::{PendingRequest, TestOffchainExt}, + }, + traits::ValidateUnsigned, +}; + +/// Round number of the fixture pulse in [`DRAND_PULSE`]. +pub const ROUND_NUMBER: u64 = 1000; + +/// Canonical Quicknet pulse JSON used across write / fetch tests. +pub const DRAND_PULSE: &str = "{\"round\":1000,\"randomness\":\"fe290beca10872ef2fb164d2aa4442de4566183ec51c56ff3cd603d930e54fdd\",\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}"; +/// Canonical Quicknet `/info` JSON for beacon config fixtures. +pub const DRAND_INFO_RESPONSE: &str = "{\"public_key\":\"83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a\",\"period\":3,\"genesis_time\":1692803367,\"hash\":\"52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971\",\"groupHash\":\"f477d5c89f21a17c863a7f937c6a6d15859414d2be09cd448d4279af331c5d3e\",\"schemeID\":\"bls-unchained-g1-rfc9380\",\"metadata\":{\"beaconID\":\"quicknet\"}}"; +/// Malformed JSON used to force decode failures in HTTP endpoint tests. +pub(crate) const INVALID_JSON: &str = r#"{"round":1000,"randomness":"not base64??","signature":}"#; + +mod beacon_config; +mod http_fetch; +mod migrations; +mod prune_pulses; +mod validate_unsigned; +mod write_pulse; diff --git a/pallets/drand/src/tests/prune_pulses.rs b/pallets/drand/src/tests/prune_pulses.rs new file mode 100644 index 0000000000..a9cbcf1fe4 --- /dev/null +++ b/pallets/drand/src/tests/prune_pulses.rs @@ -0,0 +1,115 @@ +use super::*; + +#[test] +fn test_pulses_are_correctly_pruned() { + new_test_ext().execute_with(|| { + let pulse = Pulse::default(); + let last_round: u64 = MAX_KEPT_PULSES + 2; + let oldest_round: u64 = 1; + let prune_count: u64 = 2; + let new_oldest: u64 = oldest_round + prune_count; + let middle_round: u64 = MAX_KEPT_PULSES / 2; + + // Set storage bounds + OldestStoredRound::::put(oldest_round); + LastStoredRound::::put(last_round); + + // Insert pulses at boundaries + // These should be pruned + Pulses::::insert(1, pulse.clone()); + Pulses::::insert(2, pulse.clone()); + + // This should remain (new oldest) + Pulses::::insert(new_oldest, pulse.clone()); + + // Middle and last should remain + Pulses::::insert(middle_round, pulse.clone()); + Pulses::::insert(last_round, pulse.clone()); + + // Trigger prune + Drand::prune_old_pulses(last_round); + + // Assert new oldest + assert_eq!(OldestStoredRound::::get(), new_oldest); + + // Assert pruned correctly + assert!(!Pulses::::contains_key(1), "Round 1 should be pruned"); + assert!(!Pulses::::contains_key(2), "Round 2 should be pruned"); + + // Assert not pruned incorrectly + assert!( + Pulses::::contains_key(new_oldest), + "New oldest round should remain" + ); + assert!( + Pulses::::contains_key(middle_round), + "Middle round should remain" + ); + assert!( + Pulses::::contains_key(last_round), + "Last round should remain" + ); + }); +} + +#[test] +fn test_prune_maximum_of_100_pulses_per_call() { + new_test_ext().execute_with(|| { + // ------------------------------------------------------------ + // 1. Arrange – create a storage layout that exceeds MAX_KEPT_PULSES + // ------------------------------------------------------------ + const EXTRA: u64 = 250; + let oldest_round: u64 = 1; + let last_round: u64 = oldest_round + MAX_KEPT_PULSES + EXTRA; + + OldestStoredRound::::put(oldest_round); + LastStoredRound::::put(last_round); + let pulse = Pulse::default(); + + // Insert the first 150 rounds so we can check they disappear / stay + for r in oldest_round..=oldest_round + 150 { + Pulses::::insert(r, pulse.clone()); + } + let mid_round = oldest_round + 150; + Pulses::::insert(last_round, pulse.clone()); + + // ------------------------------------------------------------ + // 2. Act – run the pruning function once + // ------------------------------------------------------------ + Drand::prune_old_pulses(last_round); + + // ------------------------------------------------------------ + // 3. Assert – only the *first* 100 pulses were removed + // ------------------------------------------------------------ + let expected_new_oldest = oldest_round + 100; // 101 + + // ‣ Storage bound updated correctly + assert_eq!( + OldestStoredRound::::get(), + expected_new_oldest, + "OldestStoredRound should advance by exactly 100" + ); + + // ‣ Rounds 1‑100 are gone + for r in oldest_round..expected_new_oldest { + assert!( + !Pulses::::contains_key(r), + "Round {r} should have been pruned" + ); + } + + // ‣ Round 101 (new oldest) and later rounds remain + assert!( + Pulses::::contains_key(expected_new_oldest), + "Round {expected_new_oldest} should remain after pruning" + ); + assert!( + Pulses::::contains_key(mid_round), + "Mid-range round should remain after pruning" + ); + assert!( + Pulses::::contains_key(last_round), + "LastStoredRound should remain after pruning" + ); + }); +} diff --git a/pallets/drand/src/tests/validate_unsigned.rs b/pallets/drand/src/tests/validate_unsigned.rs new file mode 100644 index 0000000000..4d18d3a641 --- /dev/null +++ b/pallets/drand/src/tests/validate_unsigned.rs @@ -0,0 +1,180 @@ +use super::*; + +#[test] +fn test_validate_unsigned_write_pulse() { + new_test_ext().execute_with(|| { + let block_number = 100_000_000; + let alice = sp_keyring::Sr25519Keyring::Alice; + System::set_block_number(block_number); + + let pulse = Pulse { + round: 1, + randomness: frame_support::BoundedVec::truncate_from(vec![0u8; 32]), + signature: frame_support::BoundedVec::truncate_from(vec![1u8; 96]), + }; + + let pulses_payload = PulsesPayload { + block_number, + pulses: vec![pulse], + public: alice.public(), + }; + let signature = alice.sign(&pulses_payload.encode()); + + let call = Call::write_pulse { + pulses_payload: pulses_payload.clone(), + signature: Some(signature), + }; + + let source = TransactionSource::External; + let validity = Drand::validate_unsigned(source, &call); + + assert_ok!(validity); + }); +} + +#[test] +fn validate_unsigned_accepts_first_live_round_as_storage_anchor() { + // On a fresh chain the current drand round is far beyond the normal catch-up + // window. It must be admitted so `write_pulse` can anchor both round markers. + new_test_ext().execute_with(|| { + let block_number = 100_000_000; + let alice = sp_keyring::Sr25519Keyring::Alice; + System::set_block_number(block_number); + + assert_eq!(LastStoredRound::::get(), 0); + assert_eq!(OldestStoredRound::::get(), 0); + + let pulse = Pulse { + round: crate::MAX_PULSES_TO_FETCH + 1, + randomness: frame_support::BoundedVec::truncate_from(vec![0u8; 32]), + signature: frame_support::BoundedVec::truncate_from(vec![1u8; 96]), + }; + let pulses_payload = PulsesPayload { + block_number, + pulses: vec![pulse], + public: alice.public(), + }; + let signature = alice.sign(&pulses_payload.encode()); + let call = Call::write_pulse { + pulses_payload, + signature: Some(signature), + }; + + assert_ok!(Drand::validate_unsigned(TransactionSource::Local, &call)); + }); +} + +#[test] +fn validate_unsigned_rejects_round_too_far_ahead() { + // A round that would leap LastStoredRound by more than the offchain worker ever + // submits in one run is not a legitimate catch-up pulse. Drop it at the mempool + // before it can reach dispatch (#2794). + new_test_ext().execute_with(|| { + let block_number = 100_000_000; + let alice = sp_keyring::Sr25519Keyring::Alice; + System::set_block_number(block_number); + + LastStoredRound::::put(100); + + // 151 == last(100) + MAX_PULSES_TO_FETCH(50) + 1, i.e. one beyond the cap. + let pulse = Pulse { + round: 100 + crate::MAX_PULSES_TO_FETCH + 1, + randomness: frame_support::BoundedVec::truncate_from(vec![0u8; 32]), + signature: frame_support::BoundedVec::truncate_from(vec![1u8; 96]), + }; + let pulses_payload = PulsesPayload { + block_number, + pulses: vec![pulse], + public: alice.public(), + }; + let signature = alice.sign(&pulses_payload.encode()); + + let call = Call::write_pulse { + pulses_payload: pulses_payload.clone(), + signature: Some(signature), + }; + + let source = TransactionSource::External; + let validity = Drand::validate_unsigned(source, &call); + + assert_noop!(validity, InvalidTransaction::Stale); + }); +} + +#[test] +fn test_not_validate_unsigned_write_pulse_with_bad_proof() { + new_test_ext().execute_with(|| { + let block_number = 100_000_000; + let alice = sp_keyring::Sr25519Keyring::Alice; + System::set_block_number(block_number); + let pulses_payload = PulsesPayload { + block_number, + pulses: vec![], + public: alice.public(), + }; + + // Bad signature + let signature = ::Signature::default(); + let call = Call::write_pulse { + pulses_payload: pulses_payload.clone(), + signature: Some(signature), + }; + + let source = TransactionSource::External; + let validity = Drand::validate_unsigned(source, &call); + + assert_noop!(validity, InvalidTransaction::BadProof); + }); +} + +#[test] +fn test_not_validate_unsigned_write_pulse_with_no_payload_signature() { + new_test_ext().execute_with(|| { + let block_number = 100_000_000; + let alice = sp_keyring::Sr25519Keyring::Alice; + System::set_block_number(block_number); + let pulses_payload = PulsesPayload { + block_number, + pulses: vec![], + public: alice.public(), + }; + + // No signature + let signature = None; + let call = Call::write_pulse { + pulses_payload: pulses_payload.clone(), + signature, + }; + + let source = TransactionSource::External; + let validity = Drand::validate_unsigned(source, &call); + + assert_noop!(validity, InvalidTransaction::BadSigner); + }); +} + +#[test] +fn validate_unsigned_rejects_future_block_number() { + new_test_ext().execute_with(|| { + let block_number = 100_000_000; + let future_block_number = 100_000_100; + let alice = sp_keyring::Sr25519Keyring::Alice; + System::set_block_number(block_number); + let pulses_payload = PulsesPayload { + block_number: future_block_number, + pulses: vec![], + public: alice.public(), + }; + let signature = alice.sign(&pulses_payload.encode()); + + let call = Call::write_pulse { + pulses_payload: pulses_payload.clone(), + signature: Some(signature), + }; + + let source = TransactionSource::External; + let validity = Drand::validate_unsigned(source, &call); + + assert_noop!(validity, InvalidTransaction::Future); + }); +} diff --git a/pallets/drand/src/tests/write_pulse.rs b/pallets/drand/src/tests/write_pulse.rs new file mode 100644 index 0000000000..c0c31844e4 --- /dev/null +++ b/pallets/drand/src/tests/write_pulse.rs @@ -0,0 +1,238 @@ +use super::*; + +#[test] +fn it_can_submit_valid_pulse_when_beacon_config_exists() { + new_test_ext().execute_with(|| { + let u_p: DrandResponseBody = serde_json::from_str(DRAND_PULSE).unwrap(); + let p: Pulse = u_p.try_into_pulse().unwrap(); + + let alice = sp_keyring::Sr25519Keyring::Alice; + let block_number = 100_000_000; + System::set_block_number(block_number); + + // Set the beacon config + let info: BeaconInfoResponse = serde_json::from_str(DRAND_INFO_RESPONSE).unwrap(); + let config_payload = BeaconConfigurationPayload { + block_number, + config: info.clone().try_into_beacon_config().unwrap(), + public: alice.public(), + }; + + // The signature doesn't really matter here because the signature is validated in the + // transaction validation phase not in the dispatchable itself. + let signature = None; + assert_ok!(Drand::set_beacon_config( + RuntimeOrigin::root(), + config_payload, + signature + )); + + let pulses_payload = PulsesPayload { + pulses: vec![p.clone()], + block_number, + public: alice.public(), + }; + + // Dispatch an unsigned extrinsic. + assert_ok!(Drand::write_pulse( + RuntimeOrigin::none(), + pulses_payload, + signature + )); + + // Read pallet storage and assert an expected result. + let pulse = Pulses::::get(ROUND_NUMBER); + assert!(pulse.is_some()); + assert_eq!(pulse, Some(p)); + }); +} + +#[test] +fn it_rejects_invalid_pulse_due_to_bad_signature() { + new_test_ext().execute_with(|| { + let alice = sp_keyring::Sr25519Keyring::Alice; + let block_number = 100_000_000; + System::set_block_number(block_number); + + // Set the beacon config using Root origin + let info: BeaconInfoResponse = serde_json::from_str(DRAND_INFO_RESPONSE).unwrap(); + let config_payload = BeaconConfigurationPayload { + block_number, + config: info.try_into_beacon_config().unwrap(), + public: alice.public(), + }; + // Signature is not required for Root origin + let config_signature = None; + assert_ok!(Drand::set_beacon_config( + RuntimeOrigin::root(), + config_payload.clone(), + config_signature + )); + + // Get a bad pulse (invalid signature within the pulse data) + let bad_http_response = "{\"round\":1000,\"randomness\":\"87f03ef5f62885390defedf60d5b8132b4dc2115b1efc6e99d166a37ab2f3a02\",\"signature\":\"b0a8b04e009cf72534321aca0f50048da596a3feec1172a0244d9a4a623a3123d0402da79854d4c705e94bc73224c341\"}"; + let u_p: DrandResponseBody = serde_json::from_str(bad_http_response).unwrap(); + let p: Pulse = u_p.try_into_pulse().unwrap(); + + // Prepare the pulses payload + let pulses_payload = PulsesPayload { + pulses: vec![p.clone()], + block_number, + public: alice.public(), + }; + let pulses_signature = alice.sign(&pulses_payload.encode()); + + assert_noop!( + Drand::write_pulse( + RawOrigin::None.into(), + pulses_payload.clone(), + Some(pulses_signature) + ), + Error::::PulseVerificationError + ); + + let pulse = Pulses::::get(ROUND_NUMBER); + assert!(pulse.is_none()); + }); +} + +#[test] +fn it_rejects_pulses_with_non_incremental_round_numbers() { + new_test_ext().execute_with(|| { + let block_number = 100_000_000; + let alice = sp_keyring::Sr25519Keyring::Alice; + System::set_block_number(block_number); + + // Set the beacon config + let info: BeaconInfoResponse = serde_json::from_str(DRAND_INFO_RESPONSE).unwrap(); + let config_payload = BeaconConfigurationPayload { + block_number, + config: info.clone().try_into_beacon_config().unwrap(), + public: alice.public(), + }; + // The signature doesn't really matter here because the signature is validated in the + // transaction validation phase not in the dispatchable itself. + let signature = None; + assert_ok!(Drand::set_beacon_config( + RuntimeOrigin::root(), + config_payload, + signature + )); + + let u_p: DrandResponseBody = serde_json::from_str(DRAND_PULSE).unwrap(); + let p: Pulse = u_p.try_into_pulse().unwrap(); + let pulses_payload = PulsesPayload { + pulses: vec![p.clone()], + block_number, + public: alice.public(), + }; + + // Dispatch an unsigned extrinsic. + assert_ok!(Drand::write_pulse( + RuntimeOrigin::none(), + pulses_payload.clone(), + signature + )); + let pulse = Pulses::::get(ROUND_NUMBER); + assert!(pulse.is_some()); + + System::set_block_number(2); + + // Attempt to submit the same pulse again, which should fail + assert_noop!( + Drand::write_pulse(RuntimeOrigin::none(), pulses_payload, signature), + Error::::InvalidRoundNumber, + ); + }); +} + +#[test] +fn write_pulse_rejects_round_skip() { + // A single pulse must not be allowed to leap LastStoredRound past the rounds + // in between, or those rounds can never be stored and any reveal/timelock that + // references them is wedged (#2794). Here round 1000 is a valid (BLS-verified) + // pulse, but LastStoredRound is seeded to 998 so round 1000 is a skip of two. + new_test_ext().execute_with(|| { + let block_number = 100_000_000; + let alice = sp_keyring::Sr25519Keyring::Alice; + System::set_block_number(block_number); + + let info: BeaconInfoResponse = serde_json::from_str(DRAND_INFO_RESPONSE).unwrap(); + let config_payload = BeaconConfigurationPayload { + block_number, + config: info.clone().try_into_beacon_config().unwrap(), + public: alice.public(), + }; + let signature = None; + assert_ok!(Drand::set_beacon_config( + RuntimeOrigin::root(), + config_payload, + signature + )); + + // Seed an existing baseline so this is not the anchor (first) storage. + LastStoredRound::::put(998); + OldestStoredRound::::put(998); + + let u_p: DrandResponseBody = serde_json::from_str(DRAND_PULSE).unwrap(); + let p: Pulse = u_p.try_into_pulse().unwrap(); + let pulses_payload = PulsesPayload { + pulses: vec![p.clone()], + block_number, + public: alice.public(), + }; + + // Round 1000 is NOT last(998) + 1, so it must be rejected. + assert_noop!( + Drand::write_pulse(RuntimeOrigin::none(), pulses_payload, signature), + Error::::InvalidRoundNumber, + ); + + // State is unchanged: no leap, nothing stored. + assert_eq!(LastStoredRound::::get(), 998); + assert!(Pulses::::get(ROUND_NUMBER).is_none()); + }); +} + +#[test] +fn write_pulse_accepts_consecutive_round() { + // The strict-advance rule must still accept the legitimate next round. + // Round 1000 == last(999) + 1, so it is stored. + new_test_ext().execute_with(|| { + let block_number = 100_000_000; + let alice = sp_keyring::Sr25519Keyring::Alice; + System::set_block_number(block_number); + + let info: BeaconInfoResponse = serde_json::from_str(DRAND_INFO_RESPONSE).unwrap(); + let config_payload = BeaconConfigurationPayload { + block_number, + config: info.clone().try_into_beacon_config().unwrap(), + public: alice.public(), + }; + let signature = None; + assert_ok!(Drand::set_beacon_config( + RuntimeOrigin::root(), + config_payload, + signature + )); + + LastStoredRound::::put(999); + OldestStoredRound::::put(999); + + let u_p: DrandResponseBody = serde_json::from_str(DRAND_PULSE).unwrap(); + let p: Pulse = u_p.try_into_pulse().unwrap(); + let pulses_payload = PulsesPayload { + pulses: vec![p.clone()], + block_number, + public: alice.public(), + }; + + assert_ok!(Drand::write_pulse( + RuntimeOrigin::none(), + pulses_payload, + signature + )); + assert_eq!(LastStoredRound::::get(), ROUND_NUMBER); + assert!(Pulses::::get(ROUND_NUMBER).is_some()); + }); +} diff --git a/pallets/drand/src/types.rs b/pallets/drand/src/types.rs index 6763787935..ff329bf244 100644 --- a/pallets/drand/src/types.rs +++ b/pallets/drand/src/types.rs @@ -14,47 +14,57 @@ * limitations under the License. */ +//! SCALE / JSON types for drand Quicknet pulses and beacon configuration. + use alloc::{string::String, vec::Vec}; use codec::{Decode, DecodeWithMemTracking, Encode}; use frame_support::pallet_prelude::*; use serde::{Deserialize, Serialize}; use subtensor_macros::freeze_struct; -/// Represents an opaque public key used in drand's quicknet +/// Opaque Quicknet G2 public key bytes (96 bytes when Tiny BLS381). pub type OpaquePublicKey = BoundedVec>; -/// an opaque hash type +/// Opaque 32-byte hash (chain hash, group hash, scheme id encoding, etc.). pub type BoundedHash = BoundedVec>; -/// the round number to track rounds of the beacon +/// Drand beacon round index (increments every `period` seconds on Quicknet). pub type RoundNumber = u64; -/// the expected response body from the drand api endpoint `api.drand.sh/{chainId}/info` -#[freeze_struct("f9e09b3273fe00cd")] +/// JSON body from `GET …/{chainId}/info` before conversion to [`BeaconConfiguration`]. +#[freeze_struct("f9e2d735dd9fb3b3")] #[derive(Debug, Decode, Default, PartialEq, Encode, Serialize, Deserialize, TypeInfo, Clone)] pub struct BeaconInfoResponse { + /// Hex-encoded beacon public key from the HTTP API. #[serde(with = "hex::serde")] pub public_key: Vec, + /// Seconds between rounds on this chain. pub period: u32, + /// Unix timestamp of round 1 genesis. pub genesis_time: u32, + /// Hex-encoded chain hash. #[serde(with = "hex::serde")] pub hash: Vec, + /// Hex-encoded group hash. #[serde(with = "hex::serde", rename = "groupHash")] pub group_hash: Vec, + /// Scheme identifier string (e.g. `bls-unchained-g1-rfc9380`). #[serde(rename = "schemeID")] pub scheme_id: String, + /// Nested beacon metadata from the info response. pub metadata: MetadataInfoResponse, } -/// metadata associated with the drand info response -#[freeze_struct("91c762d05dbf1d21")] +/// Nested `metadata` object inside a drand `/info` JSON response. +#[freeze_struct("199c70163a6d97a8")] #[derive(Debug, Decode, Default, PartialEq, Encode, Serialize, Deserialize, TypeInfo, Clone)] pub struct MetadataInfoResponse { + /// Beacon id string (Quicknet uses `quicknet`). #[serde(rename = "beaconID")] beacon_id: String, } impl BeaconInfoResponse { - /// the default configuration fetches from quicknet + /// Convert unbounded HTTP `/info` fields into on-chain [`BeaconConfiguration`] bounds. pub fn try_into_beacon_config(&self) -> Result { let bounded_pubkey = OpaquePublicKey::try_from(self.public_key.clone()) .map_err(|_| "Failed to convert public_key")?; @@ -82,24 +92,24 @@ impl BeaconInfoResponse { } } -/// a pulse from the drand beacon -/// the expected response body from the drand api endpoint `api.drand.sh/{chainId}/public/latest` -#[freeze_struct("a3fed2c99a0638bf")] +/// JSON body from `GET …/{chainId}/public/{round|latest}` before conversion to [`Pulse`]. +#[freeze_struct("e4eceee3fd13178b")] #[derive(Debug, Decode, Default, PartialEq, Encode, Serialize, Deserialize)] pub struct DrandResponseBody { - /// the randomness round number + /// Round index for this pulse. pub round: RoundNumber, - /// the sha256 hash of the signature + /// Hex-encoded sha256 of the BLS signature (API `randomness` field). // TODO: use Hash (https://github.com/ideal-lab5/pallet-drand/issues/2) #[serde(with = "hex::serde")] pub randomness: Vec, - /// BLS sig for the current round + /// Hex-encoded BLS signature for this round. // TODO: use Signature (https://github.com/ideal-lab5/pallet-drand/issues/2) #[serde(with = "hex::serde")] pub signature: Vec, } impl DrandResponseBody { + /// Convert unbounded HTTP pulse fields into an on-chain [`Pulse`]. pub fn try_into_pulse(&self) -> Result { // TODO: update these bounded vecs let bounded_randomness = BoundedVec::>::try_from(self.randomness.clone()) @@ -115,8 +125,9 @@ impl DrandResponseBody { }) } } -/// A drand chain configuration -#[freeze_struct("e839cb287e55b4f5")] + +/// On-chain drand chain parameters stored in [`crate::BeaconConfig`]. +#[freeze_struct("cecf61bb24ece161")] #[derive( Clone, Debug, @@ -131,27 +142,36 @@ impl DrandResponseBody { TypeInfo, )] pub struct BeaconConfiguration { + /// BLS public key used to verify pulses. pub public_key: OpaquePublicKey, + /// Seconds between consecutive rounds. pub period: u32, + /// Unix genesis time of round 1. pub genesis_time: u32, + /// Chain hash identifying this beacon. pub hash: BoundedHash, + /// Group hash from the beacon info. pub group_hash: BoundedHash, + /// Scheme id bytes (e.g. unchained G1 RFC9380). pub scheme_id: BoundedHash, + /// Beacon metadata (id string as bytes). pub metadata: Metadata, } -/// Payload used by to hold the beacon -/// config required to submit a transaction. -#[freeze_struct("aa582bfb5fcb7d4f")] +/// Unsigned-tx payload carrying a new [`BeaconConfiguration`] plus signer metadata. +#[freeze_struct("381d5ee5cfb1db23")] #[derive(Encode, Decode, DecodeWithMemTracking, Debug, Clone, PartialEq, scale_info::TypeInfo)] pub struct BeaconConfigurationPayload { + /// Block number observed by the offchain worker when building the payload. pub block_number: BlockNumber, + /// Beacon parameters to write into storage. pub config: BeaconConfiguration, + /// Local authority public key that signed this payload. pub public: Public, } -/// metadata for the drand beacon configuration -#[freeze_struct("e4cfd191c043f56f")] +/// On-chain beacon metadata nested in [`BeaconConfiguration`]. +#[freeze_struct("52e3179192cb40fd")] #[derive( Clone, Debug, @@ -166,10 +186,11 @@ pub struct BeaconConfigurationPayload { TypeInfo, )] pub struct Metadata { + /// Beacon id bytes (Quicknet: ASCII `quicknet`). pub beacon_id: BoundedHash, } -/// A pulse from the drand beacon +/// One verified (or pending) Quicknet pulse stored under [`crate::Pulses`]. #[freeze_struct("3836b1f8846739fc")] #[derive( Clone, @@ -186,23 +207,25 @@ pub struct Metadata { Eq, )] pub struct Pulse { - /// the randomness round number + /// Round index for this pulse. pub round: RoundNumber, - /// the sha256 hash of the signature + /// Sha256 of the BLS signature (32 bytes). // TODO: use Hash (https://github.com/ideal-lab5/pallet-drand/issues/2) pub randomness: BoundedVec>, - /// BLS sig for the current round + /// BLS signature bytes for this round. // TODO: use Signature (https://github.com/ideal-lab5/pallet-drand/issues/2) // maybe add the sig size as a generic? pub signature: BoundedVec>, } -/// Payload used by to hold the pulse -/// data required to submit a transaction. -#[freeze_struct("d56228e0330b6598")] +/// Unsigned-tx payload of one or more [`Pulse`]s plus signer metadata. +#[freeze_struct("ce91cf9cce9f7d48")] #[derive(Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] pub struct PulsesPayload { + /// Block number observed by the offchain worker when building the payload. pub block_number: BlockNumber, + /// Pulses to verify and append (runtime currently submits one per extrinsic). pub pulses: Vec, + /// Local authority public key that signed this payload. pub public: Public, } diff --git a/pallets/drand/src/utils.rs b/pallets/drand/src/utils.rs index 809efddac9..58a60ebdf8 100644 --- a/pallets/drand/src/utils.rs +++ b/pallets/drand/src/utils.rs @@ -14,6 +14,8 @@ * limitations under the License. */ +//! Host-function / arkworks argument builders used by benchmarks and local crypto tests. + #![allow(dead_code)] use crate::verifier::ArkScale; @@ -21,11 +23,13 @@ use ark_ec::AffineRepr; use ark_scale::hazmat::ArkScaleProjective; use ark_serialize::{CanonicalSerialize, Compress}; use ark_std::{UniformRand, test_rng, vec, vec::Vec}; +/// Scalar field of an affine curve point group. pub type ScalarFieldFor = ::ScalarField; -// `words_count` is the scalar length in words, with 1 word assumed to be 64 bits. -// Most significant bit is set. -fn make_scalar(words_count: u32) -> Vec { +/// Random scalar as `words_count` little-endian `u64` limbs with MSB of the first limb set. +/// +/// Arkworks treats the limb vector as **big endian** for scalar encoding. +fn random_scalar_words(words_count: u32) -> Vec { let mut scalar: Vec<_> = (0..words_count as usize) .map(|_| u64::rand(&mut test_rng())) .collect(); @@ -34,31 +38,41 @@ fn make_scalar(words_count: u32) -> Vec { scalar } -fn make_base() -> Group { +/// Uniform random element of `Group` from the test RNG. +fn random_group_element() -> Group { Group::rand(&mut test_rng()) } -// `words_count` is the scalar length in words, with 1 word assumed to be 64 bits. -// Most significant bit is set. +/// Pair `(base, scalar_limbs)` for scalar-mul host-function inputs. pub fn make_scalar_args( words_count: u32, ) -> (ArkScale, ArkScale>) { - (make_base::().into(), make_scalar(words_count).into()) + ( + random_group_element::().into(), + random_scalar_words(words_count).into(), + ) } -// `words_count` is the scalar length in words, with 1 word assumed to be 64 bits. -// Most significant bit is set. +/// Projective variant of [`make_scalar_args`]. pub fn make_scalar_args_projective( words_count: u32, ) -> (ArkScaleProjective, ArkScale>) { - (make_base::().into(), make_scalar(words_count).into()) + ( + random_group_element::().into(), + random_scalar_words(words_count).into(), + ) } +/// Pair of random points for pairing host-function inputs. pub fn make_pairing_args() -> (ArkScale, ArkScale) { - (make_base::().into(), make_base::().into()) + ( + random_group_element::().into(), + random_group_element::().into(), + ) } +/// Random MSM bases and scalars of the given `size`. pub fn make_msm_args( size: u32, ) -> (ArkScale>, ArkScale>) { @@ -72,6 +86,7 @@ pub fn make_msm_args( (bases, scalars) } +/// Uncompressed canonical serialization of an arkworks argument. pub fn serialize_argument(argument: impl CanonicalSerialize) -> Vec { let mut buf = vec![0; argument.serialized_size(Compress::No)]; argument diff --git a/pallets/drand/src/verifier.rs b/pallets/drand/src/verifier.rs index 779f2e59d8..f106c62c1a 100644 --- a/pallets/drand/src/verifier.rs +++ b/pallets/drand/src/verifier.rs @@ -14,9 +14,7 @@ * limitations under the License. */ -//! A collection of verifiers -//! -//! +//! BLS verifiers for drand beacon pulses ([`QuicknetVerifier`], [`UnsafeSkipVerifier`]). use crate::{ bls12_381, @@ -32,35 +30,33 @@ use tle::curves::drand::TinyBLS381; use w3f_bls::engine::EngineBLS; const USAGE: ark_scale::Usage = ark_scale::WIRE; +/// Arkworks type SCALE wrapper used when decoding beacon keys / signatures from storage. pub type ArkScale = ark_scale::ArkScale; -/// construct a message (e.g. signed by drand) -fn message(current_round: RoundNumber, prev_sig: &[u8]) -> Vec { +/// SHA-256 of the round number alone (empty previous signature) — Quicknet unchained message. +pub fn hash_unchained_round_message(current_round: RoundNumber) -> Vec { let mut hasher = Sha256::default(); - hasher.update(prev_sig); + hasher.update([]); hasher.update(current_round.to_be_bytes()); hasher.finalize().to_vec() } -/// something to verify beacon pulses +/// Verifies that a [`Pulse`] is a valid signature under a [`BeaconConfiguration`]. pub trait Verifier { - /// verify the given pulse using beacon_config + /// Return `Ok(true)` if `pulse` verifies under `beacon_config`, `Ok(false)` if not, + /// or `Err` on decode / hash-to-curve failures. fn verify(beacon_config: BeaconConfiguration, pulse: Pulse) -> Result; } -/// A verifier to check values received from quicknet. It outputs true if valid, false otherwise +/// Verifier for [Quicknet](https://drand.love/blog/quicknet-is-live-on-the-league-of-entropy-mainnet). /// -/// [Quicknet](https://drand.love/blog/quicknet-is-live-on-the-league-of-entropy-mainnet) operates in an unchained mode, -/// so messages contain only the round number. in addition, public keys are in G2 and signatures are -/// in G1 +/// Quicknet is unchained: the signed message is only the round number. Public keys are in G2 +/// and signatures in G1. A pulse is valid when the pairing equality holds: /// -/// Values are valid if the pairing equality holds: -/// $e(sig, g_2) == e(msg_on_curve, pk)$ -/// where $sig \in \mathbb{G}_1$ is the signature -/// $g_2 \in \mathbb{G}_2$ is a generator -/// $msg_on_curve \in \mathbb{G}_1$ is a hash of the message that drand signed -/// (hash(round_number)) $pk \in \mathbb{G}_2$ is the public key, read from the input public -/// parameters +/// `$e(sig, g_2) == e(msg_on_curve, pk)$` +/// +/// where `$sig \in G_1$`, `$g_2$` is a G2 generator, `$msg_on_curve$` is hash-to-curve of the +/// round message, and `$pk \in G_2$` comes from [`BeaconConfiguration::public_key`]. pub struct QuicknetVerifier; impl Verifier for QuicknetVerifier { @@ -75,7 +71,7 @@ impl Verifier for QuicknetVerifier { .map_err(|e| format!("Failed to decode signature: {e}"))?; // m = sha256({} || {round}) - let message = message(pulse.round, &[]); + let message = hash_unchained_round_message(pulse.round); let hasher = ::hash_to_curve_map(); // H(m) \in G1 let message_hash = hasher @@ -101,7 +97,7 @@ impl Verifier for QuicknetVerifier { } } -/// The unsafe skip verifier is just a pass-through verification, always returns true +/// Test / benchmark verifier that accepts every pulse without a pairing check. pub struct UnsafeSkipVerifier; impl Verifier for UnsafeSkipVerifier { fn verify(_beacon_config: BeaconConfiguration, _pulse: Pulse) -> Result { diff --git a/pallets/limit-orders/src/lib.rs b/pallets/limit-orders/src/lib.rs index f7a7c2a07e..de9d2dad55 100644 --- a/pallets/limit-orders/src/lib.rs +++ b/pallets/limit-orders/src/lib.rs @@ -1,3 +1,17 @@ +//! # Limit Orders Pallet +//! +//! Lets users sign off-chain **limit / take-profit / stop-loss** orders that an +//! authorized relayer later submits for on-chain execution against a subnet's +//! TAO↔alpha pool. +//! +//! - [`Call::execute_orders`] — per-order pool swaps (best-effort or all-or-nothing) +//! - [`Call::execute_batched_orders`] — netted single-pool swap for one `netuid` +//! - [`Call::cancel_order`] — signer registers a terminal cancellation intent +//! - [`Call::set_pallet_status`] — root enable/disable switch +//! +//! Only the blake2-256 order hash is stored (`Orders`); the full signed payload +//! is supplied at execution or cancel time. + #![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; @@ -105,7 +119,7 @@ pub struct Order /// EVM-compatible chain ID that this order is bound to. /// Prevents replay of testnet-signed orders on mainnet and vice versa. pub chain_id: u64, - /// Wether partial fills are enabled + /// Whether partial fills are enabled for this order. pub partial_fills_enabled: bool, } @@ -136,15 +150,16 @@ impl VersionedOrd /// Signature verification is performed against `order.inner().signer` (the AccountId) /// directly. Sr25519 and ed25519 signatures over either the SCALE-encoded order or its /// ``-wrapped blake2-256 hash are accepted; ecdsa is rejected at validation time. -#[freeze_struct("9dd5a8ac812dc504")] +#[freeze_struct("dfa1bee7fec7fcc9")] #[derive( Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, Clone, PartialEq, Eq, Debug, )] pub struct SignedOrder { + /// Versioned order payload the signature covers (currently `V1` only). pub order: VersionedOrder, /// Sr25519 or ed25519 signature over the raw order or its wrapped hash. pub signature: MultiSignature, - /// Whether we want a partial fill for this order + /// When `Some(n)`, execute only `n` of the remaining amount (requires relayer + partial fills). pub partial_fill: Option, } @@ -248,19 +263,19 @@ pub mod pallet { // ── Storage ─────────────────────────────────────────────────────────────── - /// Tracks the on-chain status of a known `OrderId`. - /// Absent ⇒ never seen (still executable if valid). - /// Present ⇒ Fulfilled or Cancelled (both are terminal). + /// Status of a known order hash (`OrderId` = blake2-256 of the versioned payload). + /// + /// Absent ⇒ never seen (still executable if valid). Present ⇒ `Fulfilled`, + /// `PartiallyFilled`, or `Cancelled` (`Fulfilled` / `Cancelled` are terminal). #[pallet::storage] pub type Orders = StorageMap<_, Blake2_128Concat, H256, OrderStatus, OptionQuery>; - /// Switch to enable/disable the pallet. - /// Defaults to `false` so bare node deployments are safe; genesis sets it to `true`. + /// Master switch for all limit-order extrinsics. + /// Defaults to `false` (safe for bare upgrades); genesis / root sets `true`. #[pallet::storage] pub type LimitOrdersEnabled = StorageValue<_, bool, ValueQuery, ConstBool>; - /// Tracks which named migrations have already been applied. - /// Keyed by a short migration name; value is always `true`. + /// Idempotency flags for named on-runtime-upgrade migrations (value always `true` once run). #[pallet::storage] pub type HasMigrationRun = StorageMap<_, Identity, BoundedVec, bool, ValueQuery>; @@ -306,7 +321,7 @@ pub mod pallet { /// Number of orders that were successfully executed. executed_count: u32, }, - /// Root has either enabled(true) or disabled(false) the pallet + /// Root toggled `LimitOrdersEnabled` (`true` = enabled, `false` = disabled). LimitOrdersPalletStatusChanged { enabled: bool }, } @@ -318,7 +333,7 @@ pub mod pallet { InvalidSignature, /// The order has already been Fulfilled or Cancelled. OrderAlreadyProcessed, - /// Order has been cancelled + /// The order was cancelled and can never be executed. OrderCancelled, /// The order's expiry timestamp is in the past. OrderExpired, @@ -332,15 +347,15 @@ pub mod pallet { RootNetUidNotAllowed, /// An order in the batch targets a different netuid than the batch netuid parameter. OrderNetUidMismatch, - /// Limit orders are disabled + /// Pallet is disabled via `LimitOrdersEnabled` / `set_pallet_status`. LimitOrdersDisabled, - /// Relayer not the same as specified in the order + /// Caller is not in the order's authorized `relayer` list. RelayerMissMatch, - /// Partial fills not enabled for this order + /// Order payload has `partial_fills_enabled = false` but a partial fill was requested. PartialFillsNotEnabled, - /// Incorrect partial fill amount provided + /// Partial fill amount is zero, exceeds remaining, or `None` against an already partial order. IncorrectPartialFillAmount, - /// A relayer must be set on the order when using partial fills + /// Partial fills require a non-empty `relayer` allow-list on the order payload. RelayerRequiredForPartialFill, /// The order's chain_id does not match the current chain. ChainIdMismatch, @@ -477,7 +492,7 @@ pub mod pallet { Error::::LimitOrdersDisabled ); - Self::do_execute_batched_orders(netuid, orders, relayer) + Self::execute_netted_batch(netuid, orders, relayer) } /// Register a cancellation intent for an order. @@ -510,11 +525,10 @@ pub mod pallet { Ok(()) } - /// Set a status for the limit orders pallet + /// Enable or disable the pallet (`true` = on, `false` = off). Root-only. /// - /// Must be called by root - /// It allows disabling or enabling the pallet - /// true means enabling, false means disabling + /// Enabling requires the configured `PalletHotkey` to already be registered + /// to the pallet intermediary account; otherwise returns `PalletHotkeyNotRegistered`. #[pallet::call_index(3)] #[pallet::weight(T::WeightInfo::set_pallet_status())] pub fn set_pallet_status(origin: OriginFor, enabled: bool) -> DispatchResult { @@ -810,13 +824,13 @@ pub mod pallet { Ok(()) } - /// Thin orchestrator for `execute_batched_orders`. + /// Netted batch pipeline for `execute_batched_orders`: + /// classify → collect → single pool swap → pro-rata distribute → fees. /// - /// All-or-nothing: any `Err` returned here (e.g. a `ZeroShareInBatch` rejection - /// during distribution) rolls back the whole batch — including the up-front - /// `collect_assets` debits and the pool swap — via FRAME's default per-dispatch - /// storage layer, so no signer is left debited without receiving output. - fn do_execute_batched_orders( + /// All-or-nothing: any `Err` (e.g. `ZeroShareInBatch`) rolls back the whole + /// batch — including `collect_assets` and the pool swap — via FRAME's + /// per-dispatch storage layer, so no signer is debited without output. + fn execute_netted_batch( netuid: NetUid, orders: BoundedVec, T::MaxOrdersPerBatch>, relayer: T::AccountId, diff --git a/pallets/limit-orders/src/migrations/mod.rs b/pallets/limit-orders/src/migrations/mod.rs index 391730d481..441bf7a01d 100644 --- a/pallets/limit-orders/src/migrations/mod.rs +++ b/pallets/limit-orders/src/migrations/mod.rs @@ -1,2 +1,4 @@ +//! On-runtime-upgrade migrations for `pallet-limit-orders`. + mod migrate_register_pallet_hotkey; pub use migrate_register_pallet_hotkey::*; diff --git a/pallets/limit-orders/src/tests/auxiliary.rs b/pallets/limit-orders/src/tests/auxiliary.rs deleted file mode 100644 index 351859a291..0000000000 --- a/pallets/limit-orders/src/tests/auxiliary.rs +++ /dev/null @@ -1,1758 +0,0 @@ -#![allow(clippy::expect_used, clippy::unwrap_used, clippy::indexing_slicing)] -//! Unit tests for the auxiliary helper functions in `pallet-limit-orders`. -//! -//! Extrinsics are NOT tested here. Each section focuses on one helper. - -use frame_support::{BoundedVec, assert_noop, assert_ok, traits::ConstU32}; -use sp_core::H256; -use sp_keyring::Sr25519Keyring as AccountKeyring; -use substrate_fixed::types::U64F64; -use subtensor_runtime_common::NetUid; - -use sp_runtime::Perbill; - -use crate::pallet::Pallet as LimitOrders; -use crate::{OrderEntry, OrderSide, OrderStatus, OrderType, Orders}; - -use super::mock::*; - -// ───────────────────────────────────────────────────────────────────────────── -// net_amount_for_event -// ───────────────────────────────────────────────────────────────────────────── - -#[test] -fn net_amount_for_event_buy_dominant() { - new_test_ext().execute_with(|| { - // Buys = 1000 TAO net, sells TAO-equiv = 300 TAO → net 700 TAO buy-side - let price = U64F64::from_num(2u32); // 2 TAO/alpha - let net = LimitOrders::::net_amount_for_event( - &OrderSide::Buy, - 1_000u128, // total_buy_net (TAO) - 150u128, // total_sell_net (alpha) ← not used in Buy branch - 300u128, // total_sell_tao_equiv - price, - ) - .expect("conversion does not overflow"); - assert_eq!(net, 700u64); - }); -} - -#[test] -fn net_amount_for_event_sell_dominant() { - new_test_ext().execute_with(|| { - // Sells = 500 alpha net, buys TAO = 200 TAO at price 2 → buy_alpha_equiv = 100 - // net sell = 500 - 100 = 400 alpha - let price = U64F64::from_num(2u32); // 2 TAO/alpha → 1 alpha = 2 TAO - let net = LimitOrders::::net_amount_for_event( - &OrderSide::Sell, - 200u128, // total_buy_net (TAO) - 500u128, // total_sell_net (alpha) - 400u128, // total_sell_tao_equiv (not used in Sell branch directly) - price, - ) - .expect("conversion does not overflow"); - // buy_alpha_equiv = 200 / 2 = 100; net = 500 - 100 = 400 - assert_eq!(net, 400u64); - }); -} - -#[test] -fn net_amount_for_event_perfectly_offset() { - new_test_ext().execute_with(|| { - // Buys = 200 TAO, sells TAO-equiv = 200 → net = 0 (buy-side result = 0) - let price = U64F64::from_num(2u32); - let net = LimitOrders::::net_amount_for_event( - &OrderSide::Buy, - 200u128, - 100u128, - 200u128, - price, - ) - .expect("conversion does not overflow"); - assert_eq!(net, 0u64); - }); -} - -#[test] -fn net_amount_for_event_sell_overflow_returns_error() { - new_test_ext().execute_with(|| { - let tiny_price = U64F64::from_bits(1); - assert_eq!( - LimitOrders::::net_amount_for_event( - &OrderSide::Sell, - u128::MAX, - 500u128, - 0u128, - tiny_price, - ), - Err(Error::::ArithmeticOverflow.into()), - ); - }); -} - -// ───────────────────────────────────────────────────────────────────────────── -// validate_and_classify -// ───────────────────────────────────────────────────────────────────────────── - -#[test] -fn validate_and_classify_separates_buys_and_sells() { - new_test_ext().execute_with(|| { - // Current time = 1_000_000 ms; expiry = 2_000_000 ms (well in the future). - MockTime::set(1_000_000); - // Price = 1.0 TAO/alpha. - MockSwap::set_price(1.0); - - let buy_order = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000u64, // amount in TAO - 2_000_000_000u64, // limit_price: willing to pay up to 2 TAO/alpha in ×10⁹ scale (scaled=1_000_000_000 ≤ 2_000_000_000 ✓) - 2_000_000u64, // expiry ms - Perbill::zero(), - fee_recipient(), - None, - ); - let sell_order = make_signed_order( - AccountKeyring::Bob, - alice(), - netuid(), - OrderType::TakeProfit, - 500u64, // amount in alpha - 1_000_000_000u64, // limit_price: sell if price >= 1 TAO/alpha in ×10⁹ scale (scaled=1_000_000_000 >= 1_000_000_000 ✓) - 2_000_000u64, - Perbill::zero(), - fee_recipient(), - None, - ); - - let orders = bounded(vec![buy_order, sell_order]); - let (buys, sells) = LimitOrders::::validate_and_classify( - netuid(), - &orders, - 1_000_000u64, - U64F64::from_num(1u32), - bob(), - ) - .expect("validate_and_classify should succeed"); - - assert_eq!(buys.len(), 1, "expected 1 valid buy"); - assert_eq!(sells.len(), 1, "expected 1 valid sell"); - - // Buy entry: gross=1000, net=1000 (0% fee_rate) - let buy = &buys[0]; - assert_eq!(buy.signer, alice()); - assert_eq!(buy.gross, 1_000u64); - assert_eq!(buy.net, 1_000u64); - assert_eq!(buy.fee_rate, Perbill::zero()); - - // Sell entry: gross=500, net=500 (fee applied on TAO output, not alpha input) - let sell = &sells[0]; - assert_eq!(sell.signer, bob()); - assert_eq!(sell.gross, 500u64); - assert_eq!(sell.net, 500u64); - }); -} - -#[test] -fn validate_and_classify_fails_for_wrong_netuid() { - new_test_ext().execute_with(|| { - // An order whose netuid does not match the batch netuid must cause a hard failure. - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - - let wrong_netuid_order = make_signed_order( - AccountKeyring::Alice, - bob(), - NetUid::from(99u16), // different netuid - OrderType::LimitBuy, - 1_000u64, - 2_000_000_000u64, // 2.0 in ×10⁹ scale - 2_000_000u64, - Perbill::zero(), - fee_recipient(), - None, - ); - - let orders = bounded(vec![wrong_netuid_order]); - assert_noop!( - LimitOrders::::validate_and_classify( - netuid(), // batch is for netuid 1 - &orders, - 1_000_000u64, - U64F64::from_num(1u32), - bob() - ), - crate::Error::::OrderNetUidMismatch - ); - }); -} - -#[test] -fn validate_and_classify_fails_for_expired_order() { - new_test_ext().execute_with(|| { - // now_ms = 2_000_001, expiry = 2_000_000 → expired → hard failure. - MockTime::set(2_000_001); - MockSwap::set_price(1.0); - - let expired = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000u64, - 2_000_000_000u64, // 2.0 in ×10⁹ scale - 2_000_000u64, // expiry already past - Perbill::zero(), - fee_recipient(), - None, - ); - - let orders = bounded(vec![expired]); - assert_noop!( - LimitOrders::::validate_and_classify( - netuid(), - &orders, - 2_000_001u64, - U64F64::from_num(1u32), - bob() - ), - crate::Error::::OrderExpired - ); - }); -} - -#[test] -fn validate_and_classify_fails_for_price_condition_not_met_for_buy() { - new_test_ext().execute_with(|| { - // Price = 3.0 TAO/alpha, scaled = 3_000_000_000, buyer's limit = 2_000_000_000 (2.0 in ×10⁹) → scaled > limit → hard failure. - MockTime::set(1_000_000); - let order = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000u64, - 2_000_000_000u64, // 2.0 in ×10⁹ scale - 2_000_000u64, - Perbill::zero(), - fee_recipient(), - None, - ); - - let orders = bounded(vec![order]); - assert_noop!( - LimitOrders::::validate_and_classify( - netuid(), - &orders, - 1_000_000u64, - U64F64::from_num(3u32), // current price = 3 > limit 2 → fails - bob() - ), - crate::Error::::PriceConditionNotMet - ); - }); -} - -#[test] -fn validate_and_classify_fails_for_already_processed_order() { - new_test_ext().execute_with(|| { - // An order already marked Fulfilled must cause a hard failure. - MockTime::set(1_000_000); - let order = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000u64, - 2_000_000_000u64, // 2.0 in ×10⁹ scale - 2_000_000u64, - Perbill::zero(), - fee_recipient(), - None, - ); - - // Pre-mark as fulfilled on-chain. - let oid = LimitOrders::::derive_order_id(&order.order); - Orders::::insert(oid, OrderStatus::Fulfilled); - - let orders = bounded(vec![order]); - assert_noop!( - LimitOrders::::validate_and_classify( - netuid(), - &orders, - 1_000_000u64, - U64F64::from_num(1u32), - bob() - ), - crate::Error::::OrderAlreadyProcessed - ); - }); -} - -#[test] -fn validate_and_classify_applies_buy_fee_to_net() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - // 1_000_000 ppb = 0.1% - // amount = 1_000_000_000, fee = 1_000_000, net = 999_000_000 - - let order = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000_000_000u64, - u64::MAX, // limit price: accept any price - 2_000_000u64, - Perbill::from_parts(1_000_000), // 0.1% fee - fee_recipient(), - None, - ); - - let orders = bounded(vec![order]); - let (buys, _) = LimitOrders::::validate_and_classify( - netuid(), - &orders, - 1_000_000u64, - U64F64::from_num(1u32), - bob(), - ) - .expect("validate_and_classify should succeed"); - - assert_eq!(buys.len(), 1); - let entry = &buys[0]; - assert_eq!(entry.gross, 1_000_000_000u64); - assert_eq!(entry.fee_rate, Perbill::from_parts(1_000_000)); - assert_eq!(entry.net, 999_000_000u64); - }); -} - -// ───────────────────────────────────────────────────────────────────────────── -// compute_effective_swap_limit -// ───────────────────────────────────────────────────────────────────────────── - -#[test] -fn compute_effective_swap_limit_buy_no_slippage() { - new_test_ext().execute_with(|| { - // No slippage → u64::MAX (no ceiling). - let limit = LimitOrders::::compute_effective_swap_limit(true, 1_000, None); - assert_eq!(limit, u64::MAX); - }); -} - -#[test] -fn compute_effective_swap_limit_sell_no_slippage() { - new_test_ext().execute_with(|| { - // No slippage → 0 (no floor). - let limit = LimitOrders::::compute_effective_swap_limit(false, 1_000, None); - assert_eq!(limit, 0); - }); -} - -#[test] -fn compute_effective_swap_limit_buy_one_percent() { - new_test_ext().execute_with(|| { - // 1% slippage on a buy with limit_price=1000 → ceiling = 1010. - let limit = LimitOrders::::compute_effective_swap_limit( - true, - 1_000, - Some(Perbill::from_percent(1)), - ); - assert_eq!(limit, 1_010); - }); -} - -#[test] -fn compute_effective_swap_limit_sell_one_percent() { - new_test_ext().execute_with(|| { - // 1% slippage on a sell with limit_price=1000 → floor = 990. - let limit = LimitOrders::::compute_effective_swap_limit( - false, - 1_000, - Some(Perbill::from_percent(1)), - ); - assert_eq!(limit, 990); - }); -} - -#[test] -fn compute_effective_swap_limit_sell_saturates_at_zero() { - new_test_ext().execute_with(|| { - // 100% slippage on a sell with limit_price=500 → floor saturates at 0. - let limit = LimitOrders::::compute_effective_swap_limit( - false, - 500, - Some(Perbill::from_percent(100)), - ); - assert_eq!(limit, 0); - }); -} - -#[test] -fn compute_effective_swap_limit_buy_saturates_at_u64_max() { - new_test_ext().execute_with(|| { - // 100% slippage on a buy with limit_price=u64::MAX → ceiling saturates at u64::MAX. - let limit = LimitOrders::::compute_effective_swap_limit( - true, - u64::MAX, - Some(Perbill::from_percent(100)), - ); - assert_eq!(limit, u64::MAX); - }); -} - -// ───────────────────────────────────────────────────────────────────────────── -// validate_and_classify — effective_swap_limit propagation -// ───────────────────────────────────────────────────────────────────────────── - -#[test] -fn validate_and_classify_stores_effective_swap_limit_for_buy() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - - // 1% slippage on limit_price=2_000_000_000 (2.0 in ×10⁹) → ceiling = 2_020_000_000. - // price=1.0, scaled=1_000_000_000 <= 2_000_000_000 ✓. - let order = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 500u64, - 2_000_000_000u64, // 2.0 in ×10⁹ scale - 2_000_000u64, - Perbill::zero(), - fee_recipient(), - None, - ); - // Override max_slippage on the inner order after signing — we need to rebuild - // the signed order so the signature covers the updated payload. - let new_inner = { - let mut o = order.order.inner().clone(); - o.max_slippage = Some(Perbill::from_percent(1)); - o - }; - let versioned = crate::VersionedOrder::V1(new_inner.clone()); - let sig = AccountKeyring::Alice.pair().sign(&versioned.encode()); - let signed_with_slippage = crate::SignedOrder { - order: versioned, - signature: sp_runtime::MultiSignature::Sr25519(sig), - partial_fill: None, - }; - - let orders = bounded(vec![signed_with_slippage]); - let (buys, _) = LimitOrders::::validate_and_classify( - netuid(), - &orders, - 1_000_000u64, - U64F64::from_num(1u32), - bob(), - ) - .expect("should succeed"); - - assert_eq!(buys[0].effective_swap_limit, 2_020_000_000); - }); -} - -#[test] -fn validate_and_classify_stores_effective_swap_limit_for_sell() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - // Price must be >= limit_price (in ×10⁹ scale) for TakeProfit to trigger. - // limit_price=1_000_000_000 (1.0 in ×10⁹), 1% slippage → floor = 990_000_000. - let new_inner = crate::Order { - signer: AccountKeyring::Alice.to_account_id(), - hotkey: bob(), - netuid: netuid(), - order_type: OrderType::TakeProfit, - amount: 500u64, - limit_price: 1_000_000_000u64, // 1.0 in ×10⁹ scale - expiry: u64::MAX, - fee_rate: Perbill::zero(), - fee_recipient: fee_recipient(), - relayer: None, - max_slippage: Some(Perbill::from_percent(1)), - chain_id: 945, - partial_fills_enabled: false, - }; - let versioned = crate::VersionedOrder::V1(new_inner); - let sig = AccountKeyring::Alice.pair().sign(&versioned.encode()); - let signed = crate::SignedOrder { - order: versioned, - signature: sp_runtime::MultiSignature::Sr25519(sig), - partial_fill: None, - }; - - let orders = bounded(vec![signed]); - let (_, sells) = LimitOrders::::validate_and_classify( - netuid(), - &orders, - 1_000_000u64, - U64F64::from_num(2u32), // current_price=2.0, scaled=2_000_000_000 >= limit_price=1_000_000_000 ✓ - bob(), - ) - .expect("should succeed"); - - assert_eq!(sells[0].effective_swap_limit, 990_000_000); - }); -} - -// ───────────────────────────────────────────────────────────────────────────── -// validate_and_classify — relayer enforcement -// ───────────────────────────────────────────────────────────────────────────── - -#[test] -fn validate_and_classify_fails_for_wrong_relayer() { - new_test_ext().execute_with(|| { - // Order explicitly locks execution to charlie(); submitting as bob() must fail. - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - - let order = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000u64, - u64::MAX, - 2_000_000u64, - Perbill::zero(), - fee_recipient(), - Some(BoundedVec::try_from(vec![charlie()]).unwrap()), // only charlie may relay this order - ); - - let orders = bounded(vec![order]); - assert_noop!( - LimitOrders::::validate_and_classify( - netuid(), - &orders, - 1_000_000u64, - U64F64::from_num(1u32), - bob() // wrong relayer - ), - crate::Error::::RelayerMissMatch - ); - }); -} - -#[test] -fn validate_and_classify_succeeds_for_correct_relayer() { - new_test_ext().execute_with(|| { - // Same setup as above but now the correct relayer (charlie) is used. - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - - let order = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000u64, - u64::MAX, - 2_000_000u64, - Perbill::zero(), - fee_recipient(), - Some(BoundedVec::try_from(vec![charlie()]).unwrap()), // only charlie may relay this order - ); - - let orders = bounded(vec![order]); - let (buys, sells) = LimitOrders::::validate_and_classify( - netuid(), - &orders, - 1_000_000u64, - U64F64::from_num(1u32), - charlie(), // correct relayer - ) - .expect("validate_and_classify should succeed"); - - assert_eq!(buys.len(), 1, "expected 1 valid buy"); - assert_eq!(sells.len(), 0); - }); -} - -// ───────────────────────────────────────────────────────────────────────────── -// distribute_alpha_pro_rata -// ───────────────────────────────────────────────────────────────────────────── -// -// Scenario A – buy-dominant, pool rate = 1:1 -// ─────────────────────────────────────────── -// Both buyers and sellers are present, but buys exceed sells in TAO terms. -// Sellers are settled first (they receive TAO in distribute_tao_pro_rata). -// Their alpha (200 total) stays in the pallet account as passthrough for buyers. -// The residual buy TAO hits the pool and returns 800 alpha (at 1:1 rate). -// -// 3 buyers: Alice 300 TAO net, Bob 200 TAO net, Charlie 500 TAO net (total 1000) -// Sellers contributed 200 alpha (passthrough, no pool interaction). -// Net residual TAO to pool = 1000 - 200 = 800 TAO → pool returns 800 alpha (1:1). -// Total alpha available to buyers = 800 (pool) + 200 (seller passthrough) = 1000. -// -// Pro-rata shares (proportional to each buyer's net TAO): -// Alice: 1000 * 300 / 1000 = 300 alpha -// Bob: 1000 * 200 / 1000 = 200 alpha -// Charlie: 1000 * 500 / 1000 = 500 alpha -// -// Scenario B – sell-dominant -// ─────────────────────────── -// Both buyers and sellers are present, but sells exceed buys in TAO terms. -// Buyers are settled from the sellers' alpha directly (no pool for them). -// The residual sell alpha hits the pool; sellers receive TAO in distribute_tao_pro_rata. -// -// 2 buyers: Alice 400 TAO net, Bob 600 TAO net (total 1000) -// Price = 2.0 TAO/alpha → total alpha for buyers = 1000 / 2 = 500 alpha. -// -// Pro-rata shares: -// Alice: 500 * 400 / 1000 = 200 alpha -// Bob: 500 * 600 / 1000 = 300 alpha -// -// Scenario C – buy-dominant, pool rate != 1:1 -// ──────────────────────────────────────────────────────── -// Same structure as Scenario A but the pool returns fewer alpha than the TAO -// sent in, simulating realistic AMM. Pro-rata is computed over -// whatever the pool actually returned — the distribution logic is rate-agnostic. -// -// 3 buyers: Alice 300 TAO net, Bob 200 TAO net, Charlie 500 TAO net (total 1000) -// Sellers contributed 200 alpha (passthrough). -// Net residual TAO to pool = 800 TAO → pool returns 750 alpha (slippage). -// Total alpha available to buyers = 750 (pool) + 200 (seller passthrough) = 950. -// -// Pro-rata shares: -// Alice: 950 * 300 / 1000 = 285 alpha -// Bob: 950 * 200 / 1000 = 190 alpha -// Charlie: 950 * 500 / 1000 = 475 alpha -// -// Scenario D – buy-dominant, indivisible remainder (dust) -// ───────────────────────────────────────────────────────── -// Integer division floors every share. The sum of floors is strictly less than -// total_alpha when total_alpha is not divisible by total_buy_net. -// The leftover alpha stays in the pallet intermediary account (never transferred). -// -// 3 buyers: Alice 1 TAO net, Bob 1 TAO net, Charlie 1 TAO net (total 3) -// Pool returns 10 alpha; no sellers → total_alpha = 10. -// -// Pro-rata shares (floor): -// Alice: floor(10 * 1 / 3) = 3 alpha -// Bob: floor(10 * 1 / 3) = 3 alpha -// Charlie: floor(10 * 1 / 3) = 3 alpha -// Total distributed: 9 alpha -// Dust remaining in pallet account: 10 - 9 = 1 alpha (never transferred) - -fn make_buy_entry( - order_id: H256, - signer: AccountId, - hotkey: AccountId, - gross: u64, - net: u64, - fee_rate: Perbill, - fee_recipient: AccountId, -) -> OrderEntry { - OrderEntry { - order_id, - signer, - hotkey, - side: OrderType::LimitBuy, - gross, - order_amount: gross, - net, - fee_rate, - fee_recipient, - effective_swap_limit: u64::MAX, // no slippage constraint - partial_fill: None, - } -} - -fn bounded_buy_entries( - v: Vec>, -) -> BoundedVec, ConstU32<64>> { - BoundedVec::try_from(v).unwrap() -} - -fn bounded_sell_entries( - v: Vec>, -) -> BoundedVec, ConstU32<64>> { - BoundedVec::try_from(v).unwrap() -} - -#[test] -fn distribute_alpha_pro_rata_buy_dominant_scenario_a() { - new_test_ext().execute_with(|| { - // Pool returned 800 alpha; sell-side passthrough = 200 alpha. - // Total = 1000 alpha distributed across 3 buyers (300, 200, 500 TAO net). - // Expected shares: Alice 300, Bob 200, Charlie 500. - - let hotkey = AccountKeyring::Dave.to_account_id(); - let entries = bounded_buy_entries(vec![ - make_buy_entry( - H256::repeat_byte(1), - alice(), - hotkey.clone(), - 300, - 300, - Perbill::zero(), - fee_recipient(), - ), - make_buy_entry( - H256::repeat_byte(2), - bob(), - hotkey.clone(), - 200, - 200, - Perbill::zero(), - fee_recipient(), - ), - make_buy_entry( - H256::repeat_byte(3), - charlie(), - hotkey.clone(), - 500, - 500, - Perbill::zero(), - fee_recipient(), - ), - ]); - let pallet_acct = PalletHotkeyAccount::get(); // reuse as coldkey for brevity - let pallet_hk = PalletHotkeyAccount::get(); - - LimitOrders::::distribute_alpha_pro_rata( - &entries, - 800u128, // actual_out from pool (alpha) - 1_000u128, // total_buy_net (TAO) - 200u128, // total_sell_net (alpha passthrough) - &OrderSide::Buy, - U64F64::from_num(1u32), - &pallet_acct, - &pallet_hk, - netuid(), - ) - .unwrap(); - - let transfers = MockSwap::alpha_transfers(); - // 3 transfers expected (one per buyer) - assert_eq!(transfers.len(), 3); - - // Check each recipient's amount (signer is to_coldkey). - let alice_amt = transfers - .iter() - .find(|(_, _, to_ck, _, _, _)| to_ck == &alice()) - .unwrap() - .5; - let bob_amt = transfers - .iter() - .find(|(_, _, to_ck, _, _, _)| to_ck == &bob()) - .unwrap() - .5; - let charlie_amt = transfers - .iter() - .find(|(_, _, to_ck, _, _, _)| to_ck == &charlie()) - .unwrap() - .5; - - assert_eq!(alice_amt, 300u64, "Alice should receive 300 alpha"); - assert_eq!(bob_amt, 200u64, "Bob should receive 200 alpha"); - assert_eq!(charlie_amt, 500u64, "Charlie should receive 500 alpha"); - }); -} - -#[test] -fn distribute_alpha_pro_rata_sell_dominant_scenario_b() { - new_test_ext().execute_with(|| { - // Price = 2.0 TAO/alpha; buyers have 400 + 600 = 1000 TAO net. - // Total alpha = 1000 / 2 = 500. - // Expected: Alice 200 alpha, Bob 300 alpha. - - let hotkey = AccountKeyring::Dave.to_account_id(); - let entries = bounded_buy_entries(vec![ - make_buy_entry( - H256::repeat_byte(4), - alice(), - hotkey.clone(), - 400, - 400, - Perbill::zero(), - fee_recipient(), - ), - make_buy_entry( - H256::repeat_byte(5), - bob(), - hotkey.clone(), - 600, - 600, - Perbill::zero(), - fee_recipient(), - ), - ]); - let pallet_acct = PalletHotkeyAccount::get(); - let pallet_hk = PalletHotkeyAccount::get(); - - LimitOrders::::distribute_alpha_pro_rata( - &entries, - 0u128, // actual_out unused in sell-dominant branch - 1_000u128, // total_buy_net (TAO) - 999u128, // total_sell_net — doesn't matter for sell-dominant logic - &OrderSide::Sell, - U64F64::from_num(2u32), // price = 2 TAO/alpha - &pallet_acct, - &pallet_hk, - netuid(), - ) - .unwrap(); - - let transfers = MockSwap::alpha_transfers(); - assert_eq!(transfers.len(), 2); - - let alice_amt = transfers - .iter() - .find(|(_, _, to_ck, _, _, _)| to_ck == &alice()) - .unwrap() - .5; - let bob_amt = transfers - .iter() - .find(|(_, _, to_ck, _, _, _)| to_ck == &bob()) - .unwrap() - .5; - - assert_eq!(alice_amt, 200u64, "Alice should receive 200 alpha"); - assert_eq!(bob_amt, 300u64, "Bob should receive 300 alpha"); - }); -} - -#[test] -fn distribute_alpha_pro_rata_buy_dominant_scenario_c() { - new_test_ext().execute_with(|| { - // Scenario C: same buyer setup as A but pool returns 750 alpha (slippage) - // instead of 800. Proves pro-rata is computed over actual pool output and - // is therefore rate-agnostic — the distribution logic doesn't assume 1:1. - // - // Net residual TAO to pool = 800 TAO → pool returns 750 alpha (not 800). - // Total alpha = 750 (pool) + 200 (seller passthrough) = 950. - // - // Expected shares: - // Alice: 950 * 300 / 1000 = 285 alpha - // Bob: 950 * 200 / 1000 = 190 alpha - // Charlie: 950 * 500 / 1000 = 475 alpha - - let hotkey = AccountKeyring::Dave.to_account_id(); - let entries = bounded_buy_entries(vec![ - make_buy_entry( - H256::repeat_byte(6), - alice(), - hotkey.clone(), - 300, - 300, - Perbill::zero(), - fee_recipient(), - ), - make_buy_entry( - H256::repeat_byte(7), - bob(), - hotkey.clone(), - 200, - 200, - Perbill::zero(), - fee_recipient(), - ), - make_buy_entry( - H256::repeat_byte(8), - charlie(), - hotkey.clone(), - 500, - 500, - Perbill::zero(), - fee_recipient(), - ), - ]); - let pallet_acct = PalletHotkeyAccount::get(); - let pallet_hk = PalletHotkeyAccount::get(); - - LimitOrders::::distribute_alpha_pro_rata( - &entries, - 750u128, // actual_out from pool (750, not 800 — slippage) - 1_000u128, // total_buy_net (TAO) - 200u128, // total_sell_net (alpha passthrough) - &OrderSide::Buy, - U64F64::from_num(1u32), - &pallet_acct, - &pallet_hk, - netuid(), - ) - .unwrap(); - - let transfers = MockSwap::alpha_transfers(); - assert_eq!(transfers.len(), 3); - - let alice_amt = transfers - .iter() - .find(|(_, _, to_ck, _, _, _)| to_ck == &alice()) - .unwrap() - .5; - let bob_amt = transfers - .iter() - .find(|(_, _, to_ck, _, _, _)| to_ck == &bob()) - .unwrap() - .5; - let charlie_amt = transfers - .iter() - .find(|(_, _, to_ck, _, _, _)| to_ck == &charlie()) - .unwrap() - .5; - - assert_eq!( - alice_amt, 285u64, - "Alice receives 950 * 300/1000 = 285 alpha" - ); - assert_eq!(bob_amt, 190u64, "Bob receives 950 * 200/1000 = 190 alpha"); - assert_eq!( - charlie_amt, 475u64, - "Charlie receives 950 * 500/1000 = 475 alpha" - ); - }); -} - -#[test] -fn distribute_alpha_pro_rata_dust_remains_in_pallet_scenario_d() { - new_test_ext().execute_with(|| { - // Scenario D: total_alpha = 10, three equal buyers (total_buy_net = 3). - // floor(10 * 1/3) = 3 each → 9 distributed → 1 alpha dust stays in pallet. - - let hotkey = AccountKeyring::Dave.to_account_id(); - let pallet_acct = PalletHotkeyAccount::get(); - let pallet_hk = PalletHotkeyAccount::get(); - - // Seed the pallet account with the 10 alpha it would hold after collect_assets - // and the pool swap (actual_out=10, no sellers). - MockSwap::set_alpha_balance(pallet_acct.clone(), pallet_hk.clone(), netuid(), 10); - - let entries = bounded_buy_entries(vec![ - make_buy_entry( - H256::repeat_byte(9), - alice(), - hotkey.clone(), - 1, - 1, - Perbill::zero(), - fee_recipient(), - ), - make_buy_entry( - H256::repeat_byte(10), - bob(), - hotkey.clone(), - 1, - 1, - Perbill::zero(), - fee_recipient(), - ), - make_buy_entry( - H256::repeat_byte(11), - charlie(), - hotkey.clone(), - 1, - 1, - Perbill::zero(), - fee_recipient(), - ), - ]); - - LimitOrders::::distribute_alpha_pro_rata( - &entries, - 10u128, // actual_out from pool - 3u128, // total_buy_net (TAO) — not divisible into 10 evenly - 0u128, // total_sell_net — no sellers - &OrderSide::Buy, - U64F64::from_num(1u32), - &pallet_acct, - &pallet_hk, - netuid(), - ) - .unwrap(); - - let transfers = MockSwap::alpha_transfers(); - assert_eq!(transfers.len(), 3); - - let alice_amt = transfers - .iter() - .find(|(_, _, to_ck, _, _, _)| to_ck == &alice()) - .unwrap() - .5; - let bob_amt = transfers - .iter() - .find(|(_, _, to_ck, _, _, _)| to_ck == &bob()) - .unwrap() - .5; - let charlie_amt = transfers - .iter() - .find(|(_, _, to_ck, _, _, _)| to_ck == &charlie()) - .unwrap() - .5; - - assert_eq!(alice_amt, 3u64, "floor(10 * 1/3) = 3"); - assert_eq!(bob_amt, 3u64, "floor(10 * 1/3) = 3"); - assert_eq!(charlie_amt, 3u64, "floor(10 * 1/3) = 3"); - - // The pallet account started with 10 and sent out 9 — 1 alpha dust remains - // in the pallet account, not burnt, not distributed. - let pallet_remaining = MockSwap::alpha_balance(&pallet_acct, &pallet_hk, netuid()); - assert_eq!( - pallet_remaining, 1u64, - "1 alpha dust stays in pallet account, not burnt" - ); - }); -} - -// ───────────────────────────────────────────────────────────────────────────── -// distribute_tao_pro_rata -// ───────────────────────────────────────────────────────────────────────────── -// -// Scenario A – sell-dominant, fee = 0 -// ───────────────────────────────────── -// Both buyers and sellers are present, but sells exceed buys in TAO terms. -// Buyers are settled first (they receive alpha in distribute_alpha_pro_rata). -// The residual sell alpha hits the pool; pool returns TAO. -// Buy-side TAO also stays in pallet as passthrough for sellers. -// -// 2 sellers: Alice 400 alpha, Bob 600 alpha (total 1000 alpha) -// Price = 2.0 TAO/alpha → sell_tao_equiv: Alice 800, Bob 1200, total 2000. -// Pool returned 1200 TAO for the residual alpha; buy passthrough = 800 TAO. -// Total TAO available to sellers = 1200 (pool) + 800 (buy passthrough) = 2000. -// -// Pro-rata shares (proportional to each seller's TAO-equiv): -// Alice: 2000 * 800 / 2000 = 800 TAO -// Bob: 2000 * 1200 / 2000 = 1200 TAO -// -// Scenario B – sell-dominant, fee = 1% (10_000_000 ppb) -// ──────────────────────────────────────────────────────── -// Same structure as Scenario A. Fee is deducted from each seller's gross TAO -// payout; the withheld TAO stays in the pallet account for collect_fees. -// -// Alice gross=800, fee=8 (1% of 800), net=792 TAO -// Bob gross=1200, fee=12, net=1188 TAO -// Total sell fee returned: 20 TAO -// -// Scenario C – buy-dominant -// ────────────────────────── -// Both buyers and sellers are present, but buys exceed sells in TAO terms. -// Sellers receive their alpha valued at current_price — no pool interaction -// for them. The TAO they receive comes from the buyers' collected TAO directly. -// -// 2 sellers: Alice 300 alpha, Bob 200 alpha (total 500 alpha) -// Price = 2.0 TAO/alpha → sell_tao_equiv: Alice 600, Bob 400, total 1000. -// Buy-dominant branch: total_tao = total_sell_tao_equiv = 1000 TAO. -// -// Shares: -// Alice: 1000 * 600 / 1000 = 600 TAO -// Bob: 1000 * 400 / 1000 = 400 TAO -// -// Scenario D – sell-dominant, indivisible remainder (dust) -// ───────────────────────────────────────────────────────── -// Integer division floors every gross share. The leftover TAO stays in the -// pallet intermediary account (never transferred, not burnt). -// -// 3 sellers: Alice 1 alpha, Bob 1 alpha, Charlie 1 alpha (total 3 alpha) -// Price = 1.0 TAO/alpha → sell_tao_equiv = 1 each, total_sell_tao_equiv = 3. -// No buyers; actual_out from pool = 10 TAO, buy passthrough = 0. -// total_tao = 10 + 0 = 10. -// -// Pro-rata shares (floor): -// Alice: floor(10 * 1 / 3) = 3 TAO -// Bob: floor(10 * 1 / 3) = 3 TAO -// Charlie: floor(10 * 1 / 3) = 3 TAO -// Total distributed: 9 TAO -// Dust remaining in pallet account: 10 - 9 = 1 TAO (never transferred) - -#[test] -fn distribute_tao_pro_rata_sell_dominant_no_fee_scenario_a() { - new_test_ext().execute_with(|| { - // Price = 2, total_tao = 1200 (pool) + 800 (buy passthrough) = 2000 - // Alice alpha=400 → tao_equiv=800; Bob alpha=600 → tao_equiv=1200. - // total_sell_tao_equiv = 2000. - // Shares: Alice 800, Bob 1200. - - let hotkey = AccountKeyring::Dave.to_account_id(); - let entries = bounded_sell_entries(vec![ - make_buy_entry( - H256::repeat_byte(6), - alice(), - hotkey.clone(), - 400, - 400, - Perbill::zero(), - fee_recipient(), - ), - make_buy_entry( - H256::repeat_byte(7), - bob(), - hotkey.clone(), - 600, - 600, - Perbill::zero(), - fee_recipient(), - ), - ]); - let pallet_acct = PalletHotkeyAccount::get(); - - let sell_fees = LimitOrders::::distribute_tao_pro_rata( - &entries, - 1_200u128, // actual_out (pool TAO) - 800u128, // total_buy_net (buy passthrough TAO) - 2_000u128, // total_sell_tao_equiv (Alice 800 + Bob 1200) - &OrderSide::Sell, - U64F64::from_num(2u32), - &pallet_acct, - netuid(), - ) - .unwrap(); - - let transfers = MockSwap::tao_transfers(); - assert_eq!(transfers.len(), 2); - let alice_tao = transfers - .iter() - .find(|(_, to, _)| to == &alice()) - .unwrap() - .2; - let bob_tao = transfers.iter().find(|(_, to, _)| to == &bob()).unwrap().2; - - assert_eq!(alice_tao, 800u64, "Alice should receive 800 TAO"); - assert_eq!(bob_tao, 1_200u64, "Bob should receive 1200 TAO"); - assert_eq!( - sell_fees, - vec![] as Vec<(AccountId, u64)>, - "No fees at 0 ppb" - ); - }); -} - -#[test] -fn distribute_tao_pro_rata_sell_dominant_with_fee_scenario_b() { - new_test_ext().execute_with(|| { - // Same setup as above but fee = 10_000_000 ppb = 1%. - // Alice gross=800, fee=8, net=792; Bob gross=1200, fee=12, net=1188. - // Total sell fee = 20. - - let hotkey = AccountKeyring::Dave.to_account_id(); - let entries = bounded_sell_entries(vec![ - make_buy_entry( - H256::repeat_byte(8), - alice(), - hotkey.clone(), - 400, - 400, - Perbill::from_parts(10_000_000), - fee_recipient(), - ), - make_buy_entry( - H256::repeat_byte(9), - bob(), - hotkey.clone(), - 600, - 600, - Perbill::from_parts(10_000_000), - fee_recipient(), - ), - ]); - let pallet_acct = PalletHotkeyAccount::get(); - - let sell_fees = LimitOrders::::distribute_tao_pro_rata( - &entries, - 1_200u128, - 800u128, - 2_000u128, - &OrderSide::Sell, - U64F64::from_num(2u32), - &pallet_acct, - netuid(), - ) - .unwrap(); - - let transfers = MockSwap::tao_transfers(); - assert_eq!(transfers.len(), 2); - let alice_tao = transfers - .iter() - .find(|(_, to, _)| to == &alice()) - .unwrap() - .2; - let bob_tao = transfers.iter().find(|(_, to, _)| to == &bob()).unwrap().2; - - assert_eq!(alice_tao, 792u64, "Alice net after 1% fee on 800"); - assert_eq!(bob_tao, 1_188u64, "Bob net after 1% fee on 1200"); - assert_eq!( - sell_fees, - vec![(fee_recipient(), 20u64)], - "total sell fee = 8 + 12" - ); - }); -} - -#[test] -fn distribute_tao_pro_rata_buy_dominant_scenario_c() { - new_test_ext().execute_with(|| { - // Buy-dominant: total_tao = total_sell_tao_equiv = 1000. - // Alice alpha=300 → tao_equiv=600; Bob alpha=200 → tao_equiv=400. - // Shares: Alice 600, Bob 400. - - let hotkey = AccountKeyring::Dave.to_account_id(); - let entries = bounded_sell_entries(vec![ - make_buy_entry( - H256::repeat_byte(10), - alice(), - hotkey.clone(), - 300, - 300, - Perbill::zero(), - fee_recipient(), - ), - make_buy_entry( - H256::repeat_byte(11), - bob(), - hotkey.clone(), - 200, - 200, - Perbill::zero(), - fee_recipient(), - ), - ]); - let pallet_acct = PalletHotkeyAccount::get(); - - let sell_fees = LimitOrders::::distribute_tao_pro_rata( - &entries, - 0u128, // actual_out unused in Buy-dominant branch - 0u128, // total_buy_net unused in Buy-dominant branch - 1_000u128, // total_sell_tao_equiv (total_tao = this in Buy branch) - &OrderSide::Buy, - U64F64::from_num(2u32), - &pallet_acct, - netuid(), - ) - .unwrap(); - - let transfers = MockSwap::tao_transfers(); - assert_eq!(transfers.len(), 2); - let alice_tao = transfers - .iter() - .find(|(_, to, _)| to == &alice()) - .unwrap() - .2; - let bob_tao = transfers.iter().find(|(_, to, _)| to == &bob()).unwrap().2; - - assert_eq!(alice_tao, 600u64, "Alice should receive 600 TAO"); - assert_eq!(bob_tao, 400u64, "Bob should receive 400 TAO"); - assert_eq!(sell_fees, vec![] as Vec<(AccountId, u64)>); - }); -} - -#[test] -fn distribute_tao_pro_rata_dust_remains_in_pallet_scenario_d() { - new_test_ext().execute_with(|| { - // Scenario D: total_tao = 10, three equal sellers (total_sell_tao_equiv = 3). - // floor(10 * 1/3) = 3 each → 9 distributed → 1 TAO dust stays in pallet. - - let hotkey = AccountKeyring::Dave.to_account_id(); - let pallet_acct = PalletHotkeyAccount::get(); - - // Seed the pallet account with the 10 TAO it would hold after collect_assets - // and the pool swap (actual_out=10, no buyers). - MockSwap::set_tao_balance(pallet_acct.clone(), 10); - - let entries = bounded_sell_entries(vec![ - make_buy_entry( - H256::repeat_byte(12), - alice(), - hotkey.clone(), - 1, - 1, - Perbill::zero(), - fee_recipient(), - ), - make_buy_entry( - H256::repeat_byte(13), - bob(), - hotkey.clone(), - 1, - 1, - Perbill::zero(), - fee_recipient(), - ), - make_buy_entry( - H256::repeat_byte(14), - charlie(), - hotkey.clone(), - 1, - 1, - Perbill::zero(), - fee_recipient(), - ), - ]); - - let sell_fees = LimitOrders::::distribute_tao_pro_rata( - &entries, - 10u128, // actual_out from pool (TAO) - 0u128, // total_buy_net — no buyers - 3u128, // total_sell_tao_equiv — not divisible into 10 evenly - &OrderSide::Sell, - U64F64::from_num(1u32), - &pallet_acct, - netuid(), - ) - .unwrap(); - - let transfers = MockSwap::tao_transfers(); - assert_eq!(transfers.len(), 3); - - let alice_tao = transfers - .iter() - .find(|(_, to, _)| to == &alice()) - .unwrap() - .2; - let bob_tao = transfers.iter().find(|(_, to, _)| to == &bob()).unwrap().2; - let charlie_tao = transfers - .iter() - .find(|(_, to, _)| to == &charlie()) - .unwrap() - .2; - - assert_eq!(alice_tao, 3u64, "floor(10 * 1/3) = 3"); - assert_eq!(bob_tao, 3u64, "floor(10 * 1/3) = 3"); - assert_eq!(charlie_tao, 3u64, "floor(10 * 1/3) = 3"); - assert_eq!(sell_fees, vec![] as Vec<(AccountId, u64)>); - - // The pallet account started with 10 TAO and sent out 9 — 1 TAO dust remains, - // not burnt, not distributed. - let pallet_remaining = MockSwap::tao_balance(&pallet_acct); - assert_eq!( - pallet_remaining, 1u64, - "1 TAO dust stays in pallet account, not burnt" - ); - }); -} - -// ───────────────────────────────────────────────────────────────────────────── -// collect_fees -// ───────────────────────────────────────────────────────────────────────────── -// -// Scenario: -// 2 buy orders with fees 50 and 150 TAO → total_buy_fee = 200 TAO. -// sell_fee_tao passed in = 80 TAO. -// Total fee = 280 TAO forwarded to FeeCollector in one transfer. - -#[test] -fn collect_fees_forwards_combined_fees_to_collector() { - new_test_ext().execute_with(|| { - let hotkey = AccountKeyring::Dave.to_account_id(); - // Buy entries carry fee in field index 5. - let buys = bounded_buy_entries(vec![ - make_buy_entry( - H256::repeat_byte(20), - alice(), - hotkey.clone(), - 1_000, - 950, - Perbill::from_parts(50_000_000), // 5% of 1000 = 50 - fee_recipient(), - ), - make_buy_entry( - H256::repeat_byte(21), - bob(), - hotkey.clone(), - 1_500, - 1_350, - Perbill::from_parts(100_000_000), // 10% of 1500 = 150 - fee_recipient(), - ), - ]); - let pallet_acct = PalletHotkeyAccount::get(); - - assert_ok!(LimitOrders::::collect_fees( - &buys, - vec![(fee_recipient(), 80u64)], - &pallet_acct - )); - - let tao_transfers = MockSwap::tao_transfers(); - assert_eq!(tao_transfers.len(), 1, "single transfer to fee_recipient"); - let (from, to, amount) = &tao_transfers[0]; - assert_eq!(from, &pallet_acct, "fee comes from pallet account"); - assert_eq!(to, &fee_recipient(), "fee goes to fee_recipient"); - assert_eq!(*amount, 280u64, "total fee = 200 (buy) + 80 (sell)"); - }); -} - -#[test] -fn collect_fees_no_transfer_when_zero_fees() { - new_test_ext().execute_with(|| { - // No buy fees, no sell fee. - let hotkey = AccountKeyring::Dave.to_account_id(); - let buys = bounded_buy_entries(vec![make_buy_entry( - H256::repeat_byte(22), - alice(), - hotkey, - 1_000, - 1_000, - Perbill::zero(), - fee_recipient(), - )]); - let pallet_acct = PalletHotkeyAccount::get(); - - assert_ok!(LimitOrders::::collect_fees( - &buys, - vec![], - &pallet_acct - )); - - let tao_transfers = MockSwap::tao_transfers(); - assert_eq!(tao_transfers.len(), 0, "no transfer when total fee is zero"); - }); -} - -// ───────────────────────────────────────────────────────────────────────────── -// is_order_valid -// ───────────────────────────────────────────────────────────────────────────── - -use crate::Error; -use codec::Encode; -use sp_core::Pair; -use sp_runtime::{ - MultiSignature, MultiSigner, - traits::{IdentifyAccount, Verify}, -}; -use subtensor_swap_interface::OrderSwapInterface; - -fn make_valid_signed_order() -> (crate::SignedOrder, sp_core::H256) { - let keyring = AccountKeyring::Alice; - let order = crate::VersionedOrder::V1(crate::Order { - signer: keyring.to_account_id(), - hotkey: AccountKeyring::Bob.to_account_id(), - netuid: netuid(), - order_type: OrderType::LimitBuy, - amount: 1_000, - limit_price: u64::MAX, - expiry: u64::MAX, - fee_rate: Perbill::zero(), - fee_recipient: fee_recipient(), - relayer: None, - max_slippage: None, - chain_id: 945, - partial_fills_enabled: false, - }); - let id = H256(sp_io::hashing::blake2_256(&order.encode())); - let sig = keyring.pair().sign(&order.encode()); - let signed = crate::SignedOrder { - order, - signature: MultiSignature::Sr25519(sig), - partial_fill: None, - }; - (signed, id) -} - -#[test] -fn is_order_valid_returns_ok_for_well_formed_order() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - let (signed, id) = make_valid_signed_order(); - let price = MockSwap::current_alpha_price(netuid()); - assert_ok!(LimitOrders::::is_order_valid( - &signed, - id, - 1_000_000, - price, - &bob() - )); - }); -} - -#[test] -fn is_order_valid_invalid_signature_returns_error() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - let (mut signed, id) = make_valid_signed_order(); - // Replace with a signature from a different key. - let wrong_sig = AccountKeyring::Bob.pair().sign(&signed.order.encode()); - signed.signature = MultiSignature::Sr25519(wrong_sig); - let price = MockSwap::current_alpha_price(netuid()); - assert_noop!( - LimitOrders::::is_order_valid(&signed, id, 1_000_000, price, &bob()), - Error::::InvalidSignature - ); - }); -} - -#[test] -fn is_order_valid_accepts_raw_ed25519_signature() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - let (signed, _) = make_valid_signed_order(); - let ed_pair = sp_core::ed25519::Pair::from_legacy_string("//Alice", None); - let order = crate::VersionedOrder::V1(crate::Order { - signer: AccountId::from(ed_pair.public()), - ..signed.order.inner().clone() - }); - let id = H256(sp_io::hashing::blake2_256(&order.encode())); - let signature = ed_pair.sign(&order.encode()); - let signed = crate::SignedOrder { - order, - signature: MultiSignature::Ed25519(signature), - partial_fill: None, - }; - let price = MockSwap::current_alpha_price(netuid()); - assert_ok!(LimitOrders::::is_order_valid( - &signed, - id, - 1_000_000, - price, - &bob() - )); - }); -} - -#[test] -fn is_order_valid_accepts_wrapped_sr25519_signature() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - let (mut signed, id) = make_valid_signed_order(); - let payload = [b"".as_slice(), id.as_bytes(), b"".as_slice()].concat(); - signed.signature = MultiSignature::Sr25519(AccountKeyring::Alice.pair().sign(&payload)); - let price = MockSwap::current_alpha_price(netuid()); - assert_ok!(LimitOrders::::is_order_valid( - &signed, - id, - 1_000_000, - price, - &bob() - )); - }); -} - -#[test] -fn is_order_valid_accepts_wrapped_ed25519_signature() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - let (signed, _) = make_valid_signed_order(); - let ed_pair = sp_core::ed25519::Pair::from_legacy_string("//Alice", None); - let order = crate::VersionedOrder::V1(crate::Order { - signer: AccountId::from(ed_pair.public()), - ..signed.order.inner().clone() - }); - let id = H256(sp_io::hashing::blake2_256(&order.encode())); - let payload = [b"".as_slice(), id.as_bytes(), b"".as_slice()].concat(); - let signed = crate::SignedOrder { - order, - signature: MultiSignature::Ed25519(ed_pair.sign(&payload)), - partial_fill: None, - }; - let price = MockSwap::current_alpha_price(netuid()); - assert_ok!(LimitOrders::::is_order_valid( - &signed, - id, - 1_000_000, - price, - &bob() - )); - }); -} - -#[test] -fn is_order_valid_ecdsa_signature_returns_error() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - let (signed, _) = make_valid_signed_order(); - let pair = sp_core::ecdsa::Pair::from_legacy_string("//Alice", None); - let signer = MultiSigner::from(pair.public()).into_account(); - let order = crate::VersionedOrder::V1(crate::Order { - signer, - ..signed.order.inner().clone() - }); - let id = H256(sp_io::hashing::blake2_256(&order.encode())); - let signature = MultiSignature::Ecdsa(pair.sign(&order.encode())); - assert!(signature.verify(order.encode().as_slice(), &order.inner().signer)); - let signed = crate::SignedOrder { - order, - signature, - partial_fill: None, - }; - let price = MockSwap::current_alpha_price(netuid()); - assert_noop!( - LimitOrders::::is_order_valid(&signed, id, 1_000_000, price, &bob()), - Error::::InvalidSignature - ); - }); -} - -#[test] -fn is_order_valid_already_processed_returns_error() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - let (signed, id) = make_valid_signed_order(); - Orders::::insert(id, crate::OrderStatus::Fulfilled); - let price = MockSwap::current_alpha_price(netuid()); - assert_noop!( - LimitOrders::::is_order_valid(&signed, id, 1_000_000, price, &bob()), - Error::::OrderAlreadyProcessed - ); - }); -} - -#[test] -fn is_order_valid_expired_order_returns_error() { - new_test_ext().execute_with(|| { - MockSwap::set_price(1.0); - let (signed, _id) = make_valid_signed_order(); - // now_ms (2_000_001) > expiry (u64::MAX is fine, so use a low expiry order). - // Re-build a signed order with a past expiry. - let keyring = AccountKeyring::Alice; - let order = crate::VersionedOrder::V1(crate::Order { - expiry: 500_000, - ..signed.order.inner().clone() - }); - let id2 = H256(sp_io::hashing::blake2_256(&order.encode())); - let sig = keyring.pair().sign(&order.encode()); - let signed2 = crate::SignedOrder { - order, - signature: MultiSignature::Sr25519(sig), - partial_fill: None, - }; - let price = MockSwap::current_alpha_price(netuid()); - assert_noop!( - LimitOrders::::is_order_valid(&signed2, id2, 1_000_000, price, &bob()), - Error::::OrderExpired - ); - }); -} - -#[test] -fn is_order_valid_price_condition_not_met_returns_error() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - // Price 5.0, scaled = 5_000_000_000 > limit_price 2_000_000_000 (2.0 in ×10⁹) → LimitBuy condition (scaled ≤ limit) not met. - MockSwap::set_price(5.0); - let keyring = AccountKeyring::Alice; - let order = crate::VersionedOrder::V1(crate::Order { - signer: keyring.to_account_id(), - hotkey: AccountKeyring::Bob.to_account_id(), - netuid: netuid(), - order_type: OrderType::LimitBuy, - amount: 1_000, - limit_price: 2_000_000_000, // 2.0 in ×10⁹ scale - expiry: u64::MAX, - fee_rate: Perbill::zero(), - fee_recipient: fee_recipient(), - relayer: None, - max_slippage: None, - chain_id: 945, - partial_fills_enabled: false, - }); - let id = H256(sp_io::hashing::blake2_256(&order.encode())); - let sig = keyring.pair().sign(&order.encode()); - let signed = crate::SignedOrder { - order, - signature: MultiSignature::Sr25519(sig), - partial_fill: None, - }; - let price = MockSwap::current_alpha_price(netuid()); - assert_noop!( - LimitOrders::::is_order_valid(&signed, id, 1_000_000, price, &bob()), - Error::::PriceConditionNotMet - ); - }); -} - -#[test] -fn is_order_valid_wrong_chain_id_returns_error() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - let keyring = AccountKeyring::Alice; - // Build an order with a chain_id that doesn't match the mock config (945). - let order = crate::VersionedOrder::V1(crate::Order { - chain_id: 9999, - ..make_valid_signed_order().0.order.inner().clone() - }); - let id = H256(sp_io::hashing::blake2_256(&order.encode())); - let sig = keyring.pair().sign(&order.encode()); - let signed = crate::SignedOrder { - order, - signature: MultiSignature::Sr25519(sig), - partial_fill: None, - }; - let price = MockSwap::current_alpha_price(netuid()); - assert_noop!( - LimitOrders::::is_order_valid(&signed, id, 1_000_000, price, &bob()), - Error::::ChainIdMismatch - ); - }); -} - -// ───────────────────────────────────────────────────────────────────────────── -// compute_order_status -// ───────────────────────────────────────────────────────────────────────────── - -#[test] -fn compute_order_status_no_partial_fill_returns_fulfilled() { - new_test_ext().execute_with(|| { - let id = H256::repeat_byte(1); - // No existing state, no partial fill → Fulfilled immediately. - let status = LimitOrders::::compute_order_status(id, None, 1_000); - assert_eq!(status, OrderStatus::Fulfilled); - }); -} - -#[test] -fn compute_order_status_partial_fill_below_total_returns_partially_filled() { - new_test_ext().execute_with(|| { - let id = H256::repeat_byte(2); - // First partial fill of 400 on a 1000-unit order → PartiallyFilled(400). - let status = LimitOrders::::compute_order_status(id, Some(400), 1_000); - assert_eq!(status, OrderStatus::PartiallyFilled(400)); - }); -} - -#[test] -fn compute_order_status_partial_fill_exact_total_returns_fulfilled() { - new_test_ext().execute_with(|| { - let id = H256::repeat_byte(3); - // Single partial fill that equals the full order amount → Fulfilled. - let status = LimitOrders::::compute_order_status(id, Some(1_000), 1_000); - assert_eq!(status, OrderStatus::Fulfilled); - }); -} - -#[test] -fn compute_order_status_accumulates_previous_partial_fill() { - new_test_ext().execute_with(|| { - let id = H256::repeat_byte(4); - // Pre-seed storage as if a prior partial fill of 300 already happened. - Orders::::insert(id, OrderStatus::PartiallyFilled(300)); - - // Second fill of 400 → 300 + 400 = 700, still below 1000. - let status = LimitOrders::::compute_order_status(id, Some(400), 1_000); - assert_eq!(status, OrderStatus::PartiallyFilled(700)); - }); -} - -#[test] -fn compute_order_status_completes_order_when_accumulated_total_reaches_amount() { - new_test_ext().execute_with(|| { - let id = H256::repeat_byte(5); - Orders::::insert(id, OrderStatus::PartiallyFilled(600)); - - // Fill the remaining 400 → 600 + 400 = 1000 = order_amount → Fulfilled. - let status = LimitOrders::::compute_order_status(id, Some(400), 1_000); - assert_eq!(status, OrderStatus::Fulfilled); - }); -} - -#[test] -fn compute_order_status_ignores_fulfilled_storage_when_no_partial_fill() { - new_test_ext().execute_with(|| { - let id = H256::repeat_byte(6); - // If somehow called with no partial_fill regardless of what's in storage - // (should not happen in practice) it still returns Fulfilled. - Orders::::insert(id, OrderStatus::PartiallyFilled(500)); - let status = LimitOrders::::compute_order_status(id, None, 1_000); - assert_eq!(status, OrderStatus::Fulfilled); - }); -} diff --git a/pallets/limit-orders/src/tests/auxiliary/collect_fees.rs b/pallets/limit-orders/src/tests/auxiliary/collect_fees.rs new file mode 100644 index 0000000000..8a571f1904 --- /dev/null +++ b/pallets/limit-orders/src/tests/auxiliary/collect_fees.rs @@ -0,0 +1,81 @@ +//! Helper tests: `collect_fees`. + +use super::*; + +// ───────────────────────────────────────────────────────────────────────────── +// collect_fees +// ───────────────────────────────────────────────────────────────────────────── +// +// Scenario: +// 2 buy orders with fees 50 and 150 TAO → total_buy_fee = 200 TAO. +// sell_fee_tao passed in = 80 TAO. +// Total fee = 280 TAO forwarded to FeeCollector in one transfer. + +#[test] +fn collect_fees_forwards_combined_fees_to_collector() { + new_test_ext().execute_with(|| { + let hotkey = AccountKeyring::Dave.to_account_id(); + // Buy entries carry fee in field index 5. + let buys = bounded_buy_entries(vec![ + make_buy_entry( + H256::repeat_byte(20), + alice(), + hotkey.clone(), + 1_000, + 950, + Perbill::from_parts(50_000_000), // 5% of 1000 = 50 + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(21), + bob(), + hotkey.clone(), + 1_500, + 1_350, + Perbill::from_parts(100_000_000), // 10% of 1500 = 150 + fee_recipient(), + ), + ]); + let pallet_acct = PalletHotkeyAccount::get(); + + assert_ok!(LimitOrders::::collect_fees( + &buys, + vec![(fee_recipient(), 80u64)], + &pallet_acct + )); + + let tao_transfers = MockSwap::tao_transfers(); + assert_eq!(tao_transfers.len(), 1, "single transfer to fee_recipient"); + let (from, to, amount) = &tao_transfers[0]; + assert_eq!(from, &pallet_acct, "fee comes from pallet account"); + assert_eq!(to, &fee_recipient(), "fee goes to fee_recipient"); + assert_eq!(*amount, 280u64, "total fee = 200 (buy) + 80 (sell)"); + }); +} + +#[test] +fn collect_fees_no_transfer_when_zero_fees() { + new_test_ext().execute_with(|| { + // No buy fees, no sell fee. + let hotkey = AccountKeyring::Dave.to_account_id(); + let buys = bounded_buy_entries(vec![make_buy_entry( + H256::repeat_byte(22), + alice(), + hotkey, + 1_000, + 1_000, + Perbill::zero(), + fee_recipient(), + )]); + let pallet_acct = PalletHotkeyAccount::get(); + + assert_ok!(LimitOrders::::collect_fees( + &buys, + vec![], + &pallet_acct + )); + + let tao_transfers = MockSwap::tao_transfers(); + assert_eq!(tao_transfers.len(), 0, "no transfer when total fee is zero"); + }); +} diff --git a/pallets/limit-orders/src/tests/auxiliary/compute_effective_swap_limit.rs b/pallets/limit-orders/src/tests/auxiliary/compute_effective_swap_limit.rs new file mode 100644 index 0000000000..89284a4d70 --- /dev/null +++ b/pallets/limit-orders/src/tests/auxiliary/compute_effective_swap_limit.rs @@ -0,0 +1,77 @@ +//! Helper tests: `compute_effective_swap_limit`. + +use super::*; + +// ───────────────────────────────────────────────────────────────────────────── +// compute_effective_swap_limit +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn compute_effective_swap_limit_buy_no_slippage() { + new_test_ext().execute_with(|| { + // No slippage → u64::MAX (no ceiling). + let limit = LimitOrders::::compute_effective_swap_limit(true, 1_000, None); + assert_eq!(limit, u64::MAX); + }); +} + +#[test] +fn compute_effective_swap_limit_sell_no_slippage() { + new_test_ext().execute_with(|| { + // No slippage → 0 (no floor). + let limit = LimitOrders::::compute_effective_swap_limit(false, 1_000, None); + assert_eq!(limit, 0); + }); +} + +#[test] +fn compute_effective_swap_limit_buy_one_percent() { + new_test_ext().execute_with(|| { + // 1% slippage on a buy with limit_price=1000 → ceiling = 1010. + let limit = LimitOrders::::compute_effective_swap_limit( + true, + 1_000, + Some(Perbill::from_percent(1)), + ); + assert_eq!(limit, 1_010); + }); +} + +#[test] +fn compute_effective_swap_limit_sell_one_percent() { + new_test_ext().execute_with(|| { + // 1% slippage on a sell with limit_price=1000 → floor = 990. + let limit = LimitOrders::::compute_effective_swap_limit( + false, + 1_000, + Some(Perbill::from_percent(1)), + ); + assert_eq!(limit, 990); + }); +} + +#[test] +fn compute_effective_swap_limit_sell_saturates_at_zero() { + new_test_ext().execute_with(|| { + // 100% slippage on a sell with limit_price=500 → floor saturates at 0. + let limit = LimitOrders::::compute_effective_swap_limit( + false, + 500, + Some(Perbill::from_percent(100)), + ); + assert_eq!(limit, 0); + }); +} + +#[test] +fn compute_effective_swap_limit_buy_saturates_at_u64_max() { + new_test_ext().execute_with(|| { + // 100% slippage on a buy with limit_price=u64::MAX → ceiling saturates at u64::MAX. + let limit = LimitOrders::::compute_effective_swap_limit( + true, + u64::MAX, + Some(Perbill::from_percent(100)), + ); + assert_eq!(limit, u64::MAX); + }); +} diff --git a/pallets/limit-orders/src/tests/auxiliary/compute_order_status.rs b/pallets/limit-orders/src/tests/auxiliary/compute_order_status.rs new file mode 100644 index 0000000000..4f3e490e6a --- /dev/null +++ b/pallets/limit-orders/src/tests/auxiliary/compute_order_status.rs @@ -0,0 +1,74 @@ +//! Helper tests: `compute_order_status`. + +use super::*; + +// ───────────────────────────────────────────────────────────────────────────── +// compute_order_status +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn compute_order_status_no_partial_fill_returns_fulfilled() { + new_test_ext().execute_with(|| { + let id = H256::repeat_byte(1); + // No existing state, no partial fill → Fulfilled immediately. + let status = LimitOrders::::compute_order_status(id, None, 1_000); + assert_eq!(status, OrderStatus::Fulfilled); + }); +} + +#[test] +fn compute_order_status_partial_fill_below_total_returns_partially_filled() { + new_test_ext().execute_with(|| { + let id = H256::repeat_byte(2); + // First partial fill of 400 on a 1000-unit order → PartiallyFilled(400). + let status = LimitOrders::::compute_order_status(id, Some(400), 1_000); + assert_eq!(status, OrderStatus::PartiallyFilled(400)); + }); +} + +#[test] +fn compute_order_status_partial_fill_exact_total_returns_fulfilled() { + new_test_ext().execute_with(|| { + let id = H256::repeat_byte(3); + // Single partial fill that equals the full order amount → Fulfilled. + let status = LimitOrders::::compute_order_status(id, Some(1_000), 1_000); + assert_eq!(status, OrderStatus::Fulfilled); + }); +} + +#[test] +fn compute_order_status_accumulates_previous_partial_fill() { + new_test_ext().execute_with(|| { + let id = H256::repeat_byte(4); + // Pre-seed storage as if a prior partial fill of 300 already happened. + Orders::::insert(id, OrderStatus::PartiallyFilled(300)); + + // Second fill of 400 → 300 + 400 = 700, still below 1000. + let status = LimitOrders::::compute_order_status(id, Some(400), 1_000); + assert_eq!(status, OrderStatus::PartiallyFilled(700)); + }); +} + +#[test] +fn compute_order_status_completes_order_when_accumulated_total_reaches_amount() { + new_test_ext().execute_with(|| { + let id = H256::repeat_byte(5); + Orders::::insert(id, OrderStatus::PartiallyFilled(600)); + + // Fill the remaining 400 → 600 + 400 = 1000 = order_amount → Fulfilled. + let status = LimitOrders::::compute_order_status(id, Some(400), 1_000); + assert_eq!(status, OrderStatus::Fulfilled); + }); +} + +#[test] +fn compute_order_status_ignores_fulfilled_storage_when_no_partial_fill() { + new_test_ext().execute_with(|| { + let id = H256::repeat_byte(6); + // If somehow called with no partial_fill regardless of what's in storage + // (should not happen in practice) it still returns Fulfilled. + Orders::::insert(id, OrderStatus::PartiallyFilled(500)); + let status = LimitOrders::::compute_order_status(id, None, 1_000); + assert_eq!(status, OrderStatus::Fulfilled); + }); +} diff --git a/pallets/limit-orders/src/tests/auxiliary/distribute_alpha_pro_rata.rs b/pallets/limit-orders/src/tests/auxiliary/distribute_alpha_pro_rata.rs new file mode 100644 index 0000000000..4ae20665e2 --- /dev/null +++ b/pallets/limit-orders/src/tests/auxiliary/distribute_alpha_pro_rata.rs @@ -0,0 +1,394 @@ +//! Helper tests: `distribute_alpha_pro_rata`. + +use super::*; + +// ───────────────────────────────────────────────────────────────────────────── +// distribute_alpha_pro_rata +// ───────────────────────────────────────────────────────────────────────────── +// +// Scenario A – buy-dominant, pool rate = 1:1 +// ─────────────────────────────────────────── +// Both buyers and sellers are present, but buys exceed sells in TAO terms. +// Sellers are settled first (they receive TAO in distribute_tao_pro_rata). +// Their alpha (200 total) stays in the pallet account as passthrough for buyers. +// The residual buy TAO hits the pool and returns 800 alpha (at 1:1 rate). +// +// 3 buyers: Alice 300 TAO net, Bob 200 TAO net, Charlie 500 TAO net (total 1000) +// Sellers contributed 200 alpha (passthrough, no pool interaction). +// Net residual TAO to pool = 1000 - 200 = 800 TAO → pool returns 800 alpha (1:1). +// Total alpha available to buyers = 800 (pool) + 200 (seller passthrough) = 1000. +// +// Pro-rata shares (proportional to each buyer's net TAO): +// Alice: 1000 * 300 / 1000 = 300 alpha +// Bob: 1000 * 200 / 1000 = 200 alpha +// Charlie: 1000 * 500 / 1000 = 500 alpha +// +// Scenario B – sell-dominant +// ─────────────────────────── +// Both buyers and sellers are present, but sells exceed buys in TAO terms. +// Buyers are settled from the sellers' alpha directly (no pool for them). +// The residual sell alpha hits the pool; sellers receive TAO in distribute_tao_pro_rata. +// +// 2 buyers: Alice 400 TAO net, Bob 600 TAO net (total 1000) +// Price = 2.0 TAO/alpha → total alpha for buyers = 1000 / 2 = 500 alpha. +// +// Pro-rata shares: +// Alice: 500 * 400 / 1000 = 200 alpha +// Bob: 500 * 600 / 1000 = 300 alpha +// +// Scenario C – buy-dominant, pool rate != 1:1 +// ──────────────────────────────────────────────────────── +// Same structure as Scenario A but the pool returns fewer alpha than the TAO +// sent in, simulating realistic AMM. Pro-rata is computed over +// whatever the pool actually returned — the distribution logic is rate-agnostic. +// +// 3 buyers: Alice 300 TAO net, Bob 200 TAO net, Charlie 500 TAO net (total 1000) +// Sellers contributed 200 alpha (passthrough). +// Net residual TAO to pool = 800 TAO → pool returns 750 alpha (slippage). +// Total alpha available to buyers = 750 (pool) + 200 (seller passthrough) = 950. +// +// Pro-rata shares: +// Alice: 950 * 300 / 1000 = 285 alpha +// Bob: 950 * 200 / 1000 = 190 alpha +// Charlie: 950 * 500 / 1000 = 475 alpha +// +// Scenario D – buy-dominant, indivisible remainder (dust) +// ───────────────────────────────────────────────────────── +// Integer division floors every share. The sum of floors is strictly less than +// total_alpha when total_alpha is not divisible by total_buy_net. +// The leftover alpha stays in the pallet intermediary account (never transferred). +// +// 3 buyers: Alice 1 TAO net, Bob 1 TAO net, Charlie 1 TAO net (total 3) +// Pool returns 10 alpha; no sellers → total_alpha = 10. +// +// Pro-rata shares (floor): +// Alice: floor(10 * 1 / 3) = 3 alpha +// Bob: floor(10 * 1 / 3) = 3 alpha +// Charlie: floor(10 * 1 / 3) = 3 alpha +// Total distributed: 9 alpha +// Dust remaining in pallet account: 10 - 9 = 1 alpha (never transferred) + +#[test] +fn distribute_alpha_pro_rata_buy_dominant_scenario_a() { + new_test_ext().execute_with(|| { + // Pool returned 800 alpha; sell-side passthrough = 200 alpha. + // Total = 1000 alpha distributed across 3 buyers (300, 200, 500 TAO net). + // Expected shares: Alice 300, Bob 200, Charlie 500. + + let hotkey = AccountKeyring::Dave.to_account_id(); + let entries = bounded_buy_entries(vec![ + make_buy_entry( + H256::repeat_byte(1), + alice(), + hotkey.clone(), + 300, + 300, + Perbill::zero(), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(2), + bob(), + hotkey.clone(), + 200, + 200, + Perbill::zero(), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(3), + charlie(), + hotkey.clone(), + 500, + 500, + Perbill::zero(), + fee_recipient(), + ), + ]); + let pallet_acct = PalletHotkeyAccount::get(); // reuse as coldkey for brevity + let pallet_hk = PalletHotkeyAccount::get(); + + LimitOrders::::distribute_alpha_pro_rata( + &entries, + 800u128, // actual_out from pool (alpha) + 1_000u128, // total_buy_net (TAO) + 200u128, // total_sell_net (alpha passthrough) + &OrderSide::Buy, + U64F64::from_num(1u32), + &pallet_acct, + &pallet_hk, + netuid(), + ) + .unwrap(); + + let transfers = MockSwap::alpha_transfers(); + // 3 transfers expected (one per buyer) + assert_eq!(transfers.len(), 3); + + // Check each recipient's amount (signer is to_coldkey). + let alice_amt = transfers + .iter() + .find(|(_, _, to_ck, _, _, _)| to_ck == &alice()) + .unwrap() + .5; + let bob_amt = transfers + .iter() + .find(|(_, _, to_ck, _, _, _)| to_ck == &bob()) + .unwrap() + .5; + let charlie_amt = transfers + .iter() + .find(|(_, _, to_ck, _, _, _)| to_ck == &charlie()) + .unwrap() + .5; + + assert_eq!(alice_amt, 300u64, "Alice should receive 300 alpha"); + assert_eq!(bob_amt, 200u64, "Bob should receive 200 alpha"); + assert_eq!(charlie_amt, 500u64, "Charlie should receive 500 alpha"); + }); +} + +#[test] +fn distribute_alpha_pro_rata_sell_dominant_scenario_b() { + new_test_ext().execute_with(|| { + // Price = 2.0 TAO/alpha; buyers have 400 + 600 = 1000 TAO net. + // Total alpha = 1000 / 2 = 500. + // Expected: Alice 200 alpha, Bob 300 alpha. + + let hotkey = AccountKeyring::Dave.to_account_id(); + let entries = bounded_buy_entries(vec![ + make_buy_entry( + H256::repeat_byte(4), + alice(), + hotkey.clone(), + 400, + 400, + Perbill::zero(), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(5), + bob(), + hotkey.clone(), + 600, + 600, + Perbill::zero(), + fee_recipient(), + ), + ]); + let pallet_acct = PalletHotkeyAccount::get(); + let pallet_hk = PalletHotkeyAccount::get(); + + LimitOrders::::distribute_alpha_pro_rata( + &entries, + 0u128, // actual_out unused in sell-dominant branch + 1_000u128, // total_buy_net (TAO) + 999u128, // total_sell_net — doesn't matter for sell-dominant logic + &OrderSide::Sell, + U64F64::from_num(2u32), // price = 2 TAO/alpha + &pallet_acct, + &pallet_hk, + netuid(), + ) + .unwrap(); + + let transfers = MockSwap::alpha_transfers(); + assert_eq!(transfers.len(), 2); + + let alice_amt = transfers + .iter() + .find(|(_, _, to_ck, _, _, _)| to_ck == &alice()) + .unwrap() + .5; + let bob_amt = transfers + .iter() + .find(|(_, _, to_ck, _, _, _)| to_ck == &bob()) + .unwrap() + .5; + + assert_eq!(alice_amt, 200u64, "Alice should receive 200 alpha"); + assert_eq!(bob_amt, 300u64, "Bob should receive 300 alpha"); + }); +} + +#[test] +fn distribute_alpha_pro_rata_buy_dominant_scenario_c() { + new_test_ext().execute_with(|| { + // Scenario C: same buyer setup as A but pool returns 750 alpha (slippage) + // instead of 800. Proves pro-rata is computed over actual pool output and + // is therefore rate-agnostic — the distribution logic doesn't assume 1:1. + // + // Net residual TAO to pool = 800 TAO → pool returns 750 alpha (not 800). + // Total alpha = 750 (pool) + 200 (seller passthrough) = 950. + // + // Expected shares: + // Alice: 950 * 300 / 1000 = 285 alpha + // Bob: 950 * 200 / 1000 = 190 alpha + // Charlie: 950 * 500 / 1000 = 475 alpha + + let hotkey = AccountKeyring::Dave.to_account_id(); + let entries = bounded_buy_entries(vec![ + make_buy_entry( + H256::repeat_byte(6), + alice(), + hotkey.clone(), + 300, + 300, + Perbill::zero(), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(7), + bob(), + hotkey.clone(), + 200, + 200, + Perbill::zero(), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(8), + charlie(), + hotkey.clone(), + 500, + 500, + Perbill::zero(), + fee_recipient(), + ), + ]); + let pallet_acct = PalletHotkeyAccount::get(); + let pallet_hk = PalletHotkeyAccount::get(); + + LimitOrders::::distribute_alpha_pro_rata( + &entries, + 750u128, // actual_out from pool (750, not 800 — slippage) + 1_000u128, // total_buy_net (TAO) + 200u128, // total_sell_net (alpha passthrough) + &OrderSide::Buy, + U64F64::from_num(1u32), + &pallet_acct, + &pallet_hk, + netuid(), + ) + .unwrap(); + + let transfers = MockSwap::alpha_transfers(); + assert_eq!(transfers.len(), 3); + + let alice_amt = transfers + .iter() + .find(|(_, _, to_ck, _, _, _)| to_ck == &alice()) + .unwrap() + .5; + let bob_amt = transfers + .iter() + .find(|(_, _, to_ck, _, _, _)| to_ck == &bob()) + .unwrap() + .5; + let charlie_amt = transfers + .iter() + .find(|(_, _, to_ck, _, _, _)| to_ck == &charlie()) + .unwrap() + .5; + + assert_eq!( + alice_amt, 285u64, + "Alice receives 950 * 300/1000 = 285 alpha" + ); + assert_eq!(bob_amt, 190u64, "Bob receives 950 * 200/1000 = 190 alpha"); + assert_eq!( + charlie_amt, 475u64, + "Charlie receives 950 * 500/1000 = 475 alpha" + ); + }); +} + +#[test] +fn distribute_alpha_pro_rata_dust_remains_in_pallet_scenario_d() { + new_test_ext().execute_with(|| { + // Scenario D: total_alpha = 10, three equal buyers (total_buy_net = 3). + // floor(10 * 1/3) = 3 each → 9 distributed → 1 alpha dust stays in pallet. + + let hotkey = AccountKeyring::Dave.to_account_id(); + let pallet_acct = PalletHotkeyAccount::get(); + let pallet_hk = PalletHotkeyAccount::get(); + + // Seed the pallet account with the 10 alpha it would hold after collect_assets + // and the pool swap (actual_out=10, no sellers). + MockSwap::set_alpha_balance(pallet_acct.clone(), pallet_hk.clone(), netuid(), 10); + + let entries = bounded_buy_entries(vec![ + make_buy_entry( + H256::repeat_byte(9), + alice(), + hotkey.clone(), + 1, + 1, + Perbill::zero(), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(10), + bob(), + hotkey.clone(), + 1, + 1, + Perbill::zero(), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(11), + charlie(), + hotkey.clone(), + 1, + 1, + Perbill::zero(), + fee_recipient(), + ), + ]); + + LimitOrders::::distribute_alpha_pro_rata( + &entries, + 10u128, // actual_out from pool + 3u128, // total_buy_net (TAO) — not divisible into 10 evenly + 0u128, // total_sell_net — no sellers + &OrderSide::Buy, + U64F64::from_num(1u32), + &pallet_acct, + &pallet_hk, + netuid(), + ) + .unwrap(); + + let transfers = MockSwap::alpha_transfers(); + assert_eq!(transfers.len(), 3); + + let alice_amt = transfers + .iter() + .find(|(_, _, to_ck, _, _, _)| to_ck == &alice()) + .unwrap() + .5; + let bob_amt = transfers + .iter() + .find(|(_, _, to_ck, _, _, _)| to_ck == &bob()) + .unwrap() + .5; + let charlie_amt = transfers + .iter() + .find(|(_, _, to_ck, _, _, _)| to_ck == &charlie()) + .unwrap() + .5; + + assert_eq!(alice_amt, 3u64, "floor(10 * 1/3) = 3"); + assert_eq!(bob_amt, 3u64, "floor(10 * 1/3) = 3"); + assert_eq!(charlie_amt, 3u64, "floor(10 * 1/3) = 3"); + + // The pallet account started with 10 and sent out 9 — 1 alpha dust remains + // in the pallet account, not burnt, not distributed. + let pallet_remaining = MockSwap::alpha_balance(&pallet_acct, &pallet_hk, netuid()); + assert_eq!( + pallet_remaining, 1u64, + "1 alpha dust stays in pallet account, not burnt" + ); + }); +} diff --git a/pallets/limit-orders/src/tests/auxiliary/distribute_tao_pro_rata.rs b/pallets/limit-orders/src/tests/auxiliary/distribute_tao_pro_rata.rs new file mode 100644 index 0000000000..678e69c1ca --- /dev/null +++ b/pallets/limit-orders/src/tests/auxiliary/distribute_tao_pro_rata.rs @@ -0,0 +1,328 @@ +//! Helper tests: `distribute_tao_pro_rata`. + +use super::*; + +// ───────────────────────────────────────────────────────────────────────────── +// distribute_tao_pro_rata +// ───────────────────────────────────────────────────────────────────────────── +// +// Scenario A – sell-dominant, fee = 0 +// ───────────────────────────────────── +// Both buyers and sellers are present, but sells exceed buys in TAO terms. +// Buyers are settled first (they receive alpha in distribute_alpha_pro_rata). +// The residual sell alpha hits the pool; pool returns TAO. +// Buy-side TAO also stays in pallet as passthrough for sellers. +// +// 2 sellers: Alice 400 alpha, Bob 600 alpha (total 1000 alpha) +// Price = 2.0 TAO/alpha → sell_tao_equiv: Alice 800, Bob 1200, total 2000. +// Pool returned 1200 TAO for the residual alpha; buy passthrough = 800 TAO. +// Total TAO available to sellers = 1200 (pool) + 800 (buy passthrough) = 2000. +// +// Pro-rata shares (proportional to each seller's TAO-equiv): +// Alice: 2000 * 800 / 2000 = 800 TAO +// Bob: 2000 * 1200 / 2000 = 1200 TAO +// +// Scenario B – sell-dominant, fee = 1% (10_000_000 ppb) +// ──────────────────────────────────────────────────────── +// Same structure as Scenario A. Fee is deducted from each seller's gross TAO +// payout; the withheld TAO stays in the pallet account for collect_fees. +// +// Alice gross=800, fee=8 (1% of 800), net=792 TAO +// Bob gross=1200, fee=12, net=1188 TAO +// Total sell fee returned: 20 TAO +// +// Scenario C – buy-dominant +// ────────────────────────── +// Both buyers and sellers are present, but buys exceed sells in TAO terms. +// Sellers receive their alpha valued at current_price — no pool interaction +// for them. The TAO they receive comes from the buyers' collected TAO directly. +// +// 2 sellers: Alice 300 alpha, Bob 200 alpha (total 500 alpha) +// Price = 2.0 TAO/alpha → sell_tao_equiv: Alice 600, Bob 400, total 1000. +// Buy-dominant branch: total_tao = total_sell_tao_equiv = 1000 TAO. +// +// Shares: +// Alice: 1000 * 600 / 1000 = 600 TAO +// Bob: 1000 * 400 / 1000 = 400 TAO +// +// Scenario D – sell-dominant, indivisible remainder (dust) +// ───────────────────────────────────────────────────────── +// Integer division floors every gross share. The leftover TAO stays in the +// pallet intermediary account (never transferred, not burnt). +// +// 3 sellers: Alice 1 alpha, Bob 1 alpha, Charlie 1 alpha (total 3 alpha) +// Price = 1.0 TAO/alpha → sell_tao_equiv = 1 each, total_sell_tao_equiv = 3. +// No buyers; actual_out from pool = 10 TAO, buy passthrough = 0. +// total_tao = 10 + 0 = 10. +// +// Pro-rata shares (floor): +// Alice: floor(10 * 1 / 3) = 3 TAO +// Bob: floor(10 * 1 / 3) = 3 TAO +// Charlie: floor(10 * 1 / 3) = 3 TAO +// Total distributed: 9 TAO +// Dust remaining in pallet account: 10 - 9 = 1 TAO (never transferred) + +#[test] +fn distribute_tao_pro_rata_sell_dominant_no_fee_scenario_a() { + new_test_ext().execute_with(|| { + // Price = 2, total_tao = 1200 (pool) + 800 (buy passthrough) = 2000 + // Alice alpha=400 → tao_equiv=800; Bob alpha=600 → tao_equiv=1200. + // total_sell_tao_equiv = 2000. + // Shares: Alice 800, Bob 1200. + + let hotkey = AccountKeyring::Dave.to_account_id(); + let entries = bounded_sell_entries(vec![ + make_buy_entry( + H256::repeat_byte(6), + alice(), + hotkey.clone(), + 400, + 400, + Perbill::zero(), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(7), + bob(), + hotkey.clone(), + 600, + 600, + Perbill::zero(), + fee_recipient(), + ), + ]); + let pallet_acct = PalletHotkeyAccount::get(); + + let sell_fees = LimitOrders::::distribute_tao_pro_rata( + &entries, + 1_200u128, // actual_out (pool TAO) + 800u128, // total_buy_net (buy passthrough TAO) + 2_000u128, // total_sell_tao_equiv (Alice 800 + Bob 1200) + &OrderSide::Sell, + U64F64::from_num(2u32), + &pallet_acct, + netuid(), + ) + .unwrap(); + + let transfers = MockSwap::tao_transfers(); + assert_eq!(transfers.len(), 2); + let alice_tao = transfers + .iter() + .find(|(_, to, _)| to == &alice()) + .unwrap() + .2; + let bob_tao = transfers.iter().find(|(_, to, _)| to == &bob()).unwrap().2; + + assert_eq!(alice_tao, 800u64, "Alice should receive 800 TAO"); + assert_eq!(bob_tao, 1_200u64, "Bob should receive 1200 TAO"); + assert_eq!( + sell_fees, + vec![] as Vec<(AccountId, u64)>, + "No fees at 0 ppb" + ); + }); +} + +#[test] +fn distribute_tao_pro_rata_sell_dominant_with_fee_scenario_b() { + new_test_ext().execute_with(|| { + // Same setup as above but fee = 10_000_000 ppb = 1%. + // Alice gross=800, fee=8, net=792; Bob gross=1200, fee=12, net=1188. + // Total sell fee = 20. + + let hotkey = AccountKeyring::Dave.to_account_id(); + let entries = bounded_sell_entries(vec![ + make_buy_entry( + H256::repeat_byte(8), + alice(), + hotkey.clone(), + 400, + 400, + Perbill::from_parts(10_000_000), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(9), + bob(), + hotkey.clone(), + 600, + 600, + Perbill::from_parts(10_000_000), + fee_recipient(), + ), + ]); + let pallet_acct = PalletHotkeyAccount::get(); + + let sell_fees = LimitOrders::::distribute_tao_pro_rata( + &entries, + 1_200u128, + 800u128, + 2_000u128, + &OrderSide::Sell, + U64F64::from_num(2u32), + &pallet_acct, + netuid(), + ) + .unwrap(); + + let transfers = MockSwap::tao_transfers(); + assert_eq!(transfers.len(), 2); + let alice_tao = transfers + .iter() + .find(|(_, to, _)| to == &alice()) + .unwrap() + .2; + let bob_tao = transfers.iter().find(|(_, to, _)| to == &bob()).unwrap().2; + + assert_eq!(alice_tao, 792u64, "Alice net after 1% fee on 800"); + assert_eq!(bob_tao, 1_188u64, "Bob net after 1% fee on 1200"); + assert_eq!( + sell_fees, + vec![(fee_recipient(), 20u64)], + "total sell fee = 8 + 12" + ); + }); +} + +#[test] +fn distribute_tao_pro_rata_buy_dominant_scenario_c() { + new_test_ext().execute_with(|| { + // Buy-dominant: total_tao = total_sell_tao_equiv = 1000. + // Alice alpha=300 → tao_equiv=600; Bob alpha=200 → tao_equiv=400. + // Shares: Alice 600, Bob 400. + + let hotkey = AccountKeyring::Dave.to_account_id(); + let entries = bounded_sell_entries(vec![ + make_buy_entry( + H256::repeat_byte(10), + alice(), + hotkey.clone(), + 300, + 300, + Perbill::zero(), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(11), + bob(), + hotkey.clone(), + 200, + 200, + Perbill::zero(), + fee_recipient(), + ), + ]); + let pallet_acct = PalletHotkeyAccount::get(); + + let sell_fees = LimitOrders::::distribute_tao_pro_rata( + &entries, + 0u128, // actual_out unused in Buy-dominant branch + 0u128, // total_buy_net unused in Buy-dominant branch + 1_000u128, // total_sell_tao_equiv (total_tao = this in Buy branch) + &OrderSide::Buy, + U64F64::from_num(2u32), + &pallet_acct, + netuid(), + ) + .unwrap(); + + let transfers = MockSwap::tao_transfers(); + assert_eq!(transfers.len(), 2); + let alice_tao = transfers + .iter() + .find(|(_, to, _)| to == &alice()) + .unwrap() + .2; + let bob_tao = transfers.iter().find(|(_, to, _)| to == &bob()).unwrap().2; + + assert_eq!(alice_tao, 600u64, "Alice should receive 600 TAO"); + assert_eq!(bob_tao, 400u64, "Bob should receive 400 TAO"); + assert_eq!(sell_fees, vec![] as Vec<(AccountId, u64)>); + }); +} + +#[test] +fn distribute_tao_pro_rata_dust_remains_in_pallet_scenario_d() { + new_test_ext().execute_with(|| { + // Scenario D: total_tao = 10, three equal sellers (total_sell_tao_equiv = 3). + // floor(10 * 1/3) = 3 each → 9 distributed → 1 TAO dust stays in pallet. + + let hotkey = AccountKeyring::Dave.to_account_id(); + let pallet_acct = PalletHotkeyAccount::get(); + + // Seed the pallet account with the 10 TAO it would hold after collect_assets + // and the pool swap (actual_out=10, no buyers). + MockSwap::set_tao_balance(pallet_acct.clone(), 10); + + let entries = bounded_sell_entries(vec![ + make_buy_entry( + H256::repeat_byte(12), + alice(), + hotkey.clone(), + 1, + 1, + Perbill::zero(), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(13), + bob(), + hotkey.clone(), + 1, + 1, + Perbill::zero(), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(14), + charlie(), + hotkey.clone(), + 1, + 1, + Perbill::zero(), + fee_recipient(), + ), + ]); + + let sell_fees = LimitOrders::::distribute_tao_pro_rata( + &entries, + 10u128, // actual_out from pool (TAO) + 0u128, // total_buy_net — no buyers + 3u128, // total_sell_tao_equiv — not divisible into 10 evenly + &OrderSide::Sell, + U64F64::from_num(1u32), + &pallet_acct, + netuid(), + ) + .unwrap(); + + let transfers = MockSwap::tao_transfers(); + assert_eq!(transfers.len(), 3); + + let alice_tao = transfers + .iter() + .find(|(_, to, _)| to == &alice()) + .unwrap() + .2; + let bob_tao = transfers.iter().find(|(_, to, _)| to == &bob()).unwrap().2; + let charlie_tao = transfers + .iter() + .find(|(_, to, _)| to == &charlie()) + .unwrap() + .2; + + assert_eq!(alice_tao, 3u64, "floor(10 * 1/3) = 3"); + assert_eq!(bob_tao, 3u64, "floor(10 * 1/3) = 3"); + assert_eq!(charlie_tao, 3u64, "floor(10 * 1/3) = 3"); + assert_eq!(sell_fees, vec![] as Vec<(AccountId, u64)>); + + // The pallet account started with 10 TAO and sent out 9 — 1 TAO dust remains, + // not burnt, not distributed. + let pallet_remaining = MockSwap::tao_balance(&pallet_acct); + assert_eq!( + pallet_remaining, 1u64, + "1 TAO dust stays in pallet account, not burnt" + ); + }); +} diff --git a/pallets/limit-orders/src/tests/auxiliary/is_order_valid.rs b/pallets/limit-orders/src/tests/auxiliary/is_order_valid.rs new file mode 100644 index 0000000000..ea0251620f --- /dev/null +++ b/pallets/limit-orders/src/tests/auxiliary/is_order_valid.rs @@ -0,0 +1,287 @@ +//! Helper tests: `is_order_valid`. + +use super::*; + +// ───────────────────────────────────────────────────────────────────────────── +// is_order_valid +// ───────────────────────────────────────────────────────────────────────────── + +use crate::Error; +use codec::Encode; +use sp_core::Pair; +use sp_runtime::{ + MultiSignature, MultiSigner, + traits::{IdentifyAccount, Verify}, +}; +use subtensor_swap_interface::OrderSwapInterface; + +fn make_valid_signed_order() -> (crate::SignedOrder, sp_core::H256) { + let keyring = AccountKeyring::Alice; + let order = crate::VersionedOrder::V1(crate::Order { + signer: keyring.to_account_id(), + hotkey: AccountKeyring::Bob.to_account_id(), + netuid: netuid(), + order_type: OrderType::LimitBuy, + amount: 1_000, + limit_price: u64::MAX, + expiry: u64::MAX, + fee_rate: Perbill::zero(), + fee_recipient: fee_recipient(), + relayer: None, + max_slippage: None, + chain_id: 945, + partial_fills_enabled: false, + }); + let id = H256(sp_io::hashing::blake2_256(&order.encode())); + let sig = keyring.pair().sign(&order.encode()); + let signed = crate::SignedOrder { + order, + signature: MultiSignature::Sr25519(sig), + partial_fill: None, + }; + (signed, id) +} + +#[test] +fn is_order_valid_returns_ok_for_well_formed_order() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + let (signed, id) = make_valid_signed_order(); + let price = MockSwap::current_alpha_price(netuid()); + assert_ok!(LimitOrders::::is_order_valid( + &signed, + id, + 1_000_000, + price, + &bob() + )); + }); +} + +#[test] +fn is_order_valid_invalid_signature_returns_error() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + let (mut signed, id) = make_valid_signed_order(); + // Replace with a signature from a different key. + let wrong_sig = AccountKeyring::Bob.pair().sign(&signed.order.encode()); + signed.signature = MultiSignature::Sr25519(wrong_sig); + let price = MockSwap::current_alpha_price(netuid()); + assert_noop!( + LimitOrders::::is_order_valid(&signed, id, 1_000_000, price, &bob()), + Error::::InvalidSignature + ); + }); +} + +#[test] +fn is_order_valid_accepts_raw_ed25519_signature() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + let (signed, _) = make_valid_signed_order(); + let ed_pair = sp_core::ed25519::Pair::from_legacy_string("//Alice", None); + let order = crate::VersionedOrder::V1(crate::Order { + signer: AccountId::from(ed_pair.public()), + ..signed.order.inner().clone() + }); + let id = H256(sp_io::hashing::blake2_256(&order.encode())); + let signature = ed_pair.sign(&order.encode()); + let signed = crate::SignedOrder { + order, + signature: MultiSignature::Ed25519(signature), + partial_fill: None, + }; + let price = MockSwap::current_alpha_price(netuid()); + assert_ok!(LimitOrders::::is_order_valid( + &signed, + id, + 1_000_000, + price, + &bob() + )); + }); +} + +#[test] +fn is_order_valid_accepts_wrapped_sr25519_signature() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + let (mut signed, id) = make_valid_signed_order(); + let payload = [b"".as_slice(), id.as_bytes(), b"".as_slice()].concat(); + signed.signature = MultiSignature::Sr25519(AccountKeyring::Alice.pair().sign(&payload)); + let price = MockSwap::current_alpha_price(netuid()); + assert_ok!(LimitOrders::::is_order_valid( + &signed, + id, + 1_000_000, + price, + &bob() + )); + }); +} + +#[test] +fn is_order_valid_accepts_wrapped_ed25519_signature() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + let (signed, _) = make_valid_signed_order(); + let ed_pair = sp_core::ed25519::Pair::from_legacy_string("//Alice", None); + let order = crate::VersionedOrder::V1(crate::Order { + signer: AccountId::from(ed_pair.public()), + ..signed.order.inner().clone() + }); + let id = H256(sp_io::hashing::blake2_256(&order.encode())); + let payload = [b"".as_slice(), id.as_bytes(), b"".as_slice()].concat(); + let signed = crate::SignedOrder { + order, + signature: MultiSignature::Ed25519(ed_pair.sign(&payload)), + partial_fill: None, + }; + let price = MockSwap::current_alpha_price(netuid()); + assert_ok!(LimitOrders::::is_order_valid( + &signed, + id, + 1_000_000, + price, + &bob() + )); + }); +} + +#[test] +fn is_order_valid_ecdsa_signature_returns_error() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + let (signed, _) = make_valid_signed_order(); + let pair = sp_core::ecdsa::Pair::from_legacy_string("//Alice", None); + let signer = MultiSigner::from(pair.public()).into_account(); + let order = crate::VersionedOrder::V1(crate::Order { + signer, + ..signed.order.inner().clone() + }); + let id = H256(sp_io::hashing::blake2_256(&order.encode())); + let signature = MultiSignature::Ecdsa(pair.sign(&order.encode())); + assert!(signature.verify(order.encode().as_slice(), &order.inner().signer)); + let signed = crate::SignedOrder { + order, + signature, + partial_fill: None, + }; + let price = MockSwap::current_alpha_price(netuid()); + assert_noop!( + LimitOrders::::is_order_valid(&signed, id, 1_000_000, price, &bob()), + Error::::InvalidSignature + ); + }); +} + +#[test] +fn is_order_valid_already_processed_returns_error() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + let (signed, id) = make_valid_signed_order(); + Orders::::insert(id, crate::OrderStatus::Fulfilled); + let price = MockSwap::current_alpha_price(netuid()); + assert_noop!( + LimitOrders::::is_order_valid(&signed, id, 1_000_000, price, &bob()), + Error::::OrderAlreadyProcessed + ); + }); +} + +#[test] +fn is_order_valid_expired_order_returns_error() { + new_test_ext().execute_with(|| { + MockSwap::set_price(1.0); + let (signed, _id) = make_valid_signed_order(); + // now_ms (2_000_001) > expiry (u64::MAX is fine, so use a low expiry order). + // Re-build a signed order with a past expiry. + let keyring = AccountKeyring::Alice; + let order = crate::VersionedOrder::V1(crate::Order { + expiry: 500_000, + ..signed.order.inner().clone() + }); + let id2 = H256(sp_io::hashing::blake2_256(&order.encode())); + let sig = keyring.pair().sign(&order.encode()); + let signed2 = crate::SignedOrder { + order, + signature: MultiSignature::Sr25519(sig), + partial_fill: None, + }; + let price = MockSwap::current_alpha_price(netuid()); + assert_noop!( + LimitOrders::::is_order_valid(&signed2, id2, 1_000_000, price, &bob()), + Error::::OrderExpired + ); + }); +} + +#[test] +fn is_order_valid_price_condition_not_met_returns_error() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + // Price 5.0, scaled = 5_000_000_000 > limit_price 2_000_000_000 (2.0 in ×10⁹) → LimitBuy condition (scaled ≤ limit) not met. + MockSwap::set_price(5.0); + let keyring = AccountKeyring::Alice; + let order = crate::VersionedOrder::V1(crate::Order { + signer: keyring.to_account_id(), + hotkey: AccountKeyring::Bob.to_account_id(), + netuid: netuid(), + order_type: OrderType::LimitBuy, + amount: 1_000, + limit_price: 2_000_000_000, // 2.0 in ×10⁹ scale + expiry: u64::MAX, + fee_rate: Perbill::zero(), + fee_recipient: fee_recipient(), + relayer: None, + max_slippage: None, + chain_id: 945, + partial_fills_enabled: false, + }); + let id = H256(sp_io::hashing::blake2_256(&order.encode())); + let sig = keyring.pair().sign(&order.encode()); + let signed = crate::SignedOrder { + order, + signature: MultiSignature::Sr25519(sig), + partial_fill: None, + }; + let price = MockSwap::current_alpha_price(netuid()); + assert_noop!( + LimitOrders::::is_order_valid(&signed, id, 1_000_000, price, &bob()), + Error::::PriceConditionNotMet + ); + }); +} + +#[test] +fn is_order_valid_wrong_chain_id_returns_error() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + let keyring = AccountKeyring::Alice; + // Build an order with a chain_id that doesn't match the mock config (945). + let order = crate::VersionedOrder::V1(crate::Order { + chain_id: 9999, + ..make_valid_signed_order().0.order.inner().clone() + }); + let id = H256(sp_io::hashing::blake2_256(&order.encode())); + let sig = keyring.pair().sign(&order.encode()); + let signed = crate::SignedOrder { + order, + signature: MultiSignature::Sr25519(sig), + partial_fill: None, + }; + let price = MockSwap::current_alpha_price(netuid()); + assert_noop!( + LimitOrders::::is_order_valid(&signed, id, 1_000_000, price, &bob()), + Error::::ChainIdMismatch + ); + }); +} diff --git a/pallets/limit-orders/src/tests/auxiliary/mod.rs b/pallets/limit-orders/src/tests/auxiliary/mod.rs new file mode 100644 index 0000000000..895e8f8066 --- /dev/null +++ b/pallets/limit-orders/src/tests/auxiliary/mod.rs @@ -0,0 +1,68 @@ +#![allow( + clippy::expect_used, + clippy::unwrap_used, + clippy::indexing_slicing, + unused_imports +)] +//! Unit tests for auxiliary helpers in `pallet-limit-orders`, split by concept. + +pub(crate) use codec::Encode; +pub(crate) use frame_support::{BoundedVec, assert_noop, assert_ok, traits::ConstU32}; +pub(crate) use sp_core::H256; +pub(crate) use sp_core::Pair; +pub(crate) use sp_keyring::Sr25519Keyring as AccountKeyring; +pub(crate) use sp_runtime::Perbill; +pub(crate) use substrate_fixed::types::U64F64; +pub(crate) use subtensor_runtime_common::NetUid; + +pub(crate) use crate::pallet::Pallet as LimitOrders; +pub(crate) use crate::{Error, OrderEntry, OrderSide, OrderStatus, OrderType, Orders}; + +pub(crate) use super::mock::*; + +// Shared OrderEntry builders used by distribute_* / collect_fees tests. +pub(crate) fn make_buy_entry( + order_id: H256, + signer: AccountId, + hotkey: AccountId, + gross: u64, + net: u64, + fee_rate: Perbill, + fee_recipient: AccountId, +) -> OrderEntry { + OrderEntry { + order_id, + signer, + hotkey, + side: OrderType::LimitBuy, + gross, + order_amount: gross, + net, + fee_rate, + fee_recipient, + effective_swap_limit: u64::MAX, // no slippage constraint + partial_fill: None, + } +} + +pub(crate) fn bounded_buy_entries( + v: Vec>, +) -> BoundedVec, ConstU32<64>> { + BoundedVec::try_from(v).unwrap() +} + +pub(crate) fn bounded_sell_entries( + v: Vec>, +) -> BoundedVec, ConstU32<64>> { + BoundedVec::try_from(v).unwrap() +} + +mod collect_fees; +mod compute_effective_swap_limit; +mod compute_order_status; +mod distribute_alpha_pro_rata; +mod distribute_tao_pro_rata; +mod is_order_valid; +mod net_amount_for_event; +mod validate_and_classify; +mod validate_and_classify_slippage_relayer; diff --git a/pallets/limit-orders/src/tests/auxiliary/net_amount_for_event.rs b/pallets/limit-orders/src/tests/auxiliary/net_amount_for_event.rs new file mode 100644 index 0000000000..100fededd9 --- /dev/null +++ b/pallets/limit-orders/src/tests/auxiliary/net_amount_for_event.rs @@ -0,0 +1,77 @@ +//! Helper tests: `net_amount_for_event`. + +use super::*; + +// ───────────────────────────────────────────────────────────────────────────── +// net_amount_for_event +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn net_amount_for_event_buy_dominant() { + new_test_ext().execute_with(|| { + // Buys = 1000 TAO net, sells TAO-equiv = 300 TAO → net 700 TAO buy-side + let price = U64F64::from_num(2u32); // 2 TAO/alpha + let net = LimitOrders::::net_amount_for_event( + &OrderSide::Buy, + 1_000u128, // total_buy_net (TAO) + 150u128, // total_sell_net (alpha) ← not used in Buy branch + 300u128, // total_sell_tao_equiv + price, + ) + .expect("conversion does not overflow"); + assert_eq!(net, 700u64); + }); +} + +#[test] +fn net_amount_for_event_sell_dominant() { + new_test_ext().execute_with(|| { + // Sells = 500 alpha net, buys TAO = 200 TAO at price 2 → buy_alpha_equiv = 100 + // net sell = 500 - 100 = 400 alpha + let price = U64F64::from_num(2u32); // 2 TAO/alpha → 1 alpha = 2 TAO + let net = LimitOrders::::net_amount_for_event( + &OrderSide::Sell, + 200u128, // total_buy_net (TAO) + 500u128, // total_sell_net (alpha) + 400u128, // total_sell_tao_equiv (not used in Sell branch directly) + price, + ) + .expect("conversion does not overflow"); + // buy_alpha_equiv = 200 / 2 = 100; net = 500 - 100 = 400 + assert_eq!(net, 400u64); + }); +} + +#[test] +fn net_amount_for_event_perfectly_offset() { + new_test_ext().execute_with(|| { + // Buys = 200 TAO, sells TAO-equiv = 200 → net = 0 (buy-side result = 0) + let price = U64F64::from_num(2u32); + let net = LimitOrders::::net_amount_for_event( + &OrderSide::Buy, + 200u128, + 100u128, + 200u128, + price, + ) + .expect("conversion does not overflow"); + assert_eq!(net, 0u64); + }); +} + +#[test] +fn net_amount_for_event_sell_overflow_returns_error() { + new_test_ext().execute_with(|| { + let tiny_price = U64F64::from_bits(1); + assert_eq!( + LimitOrders::::net_amount_for_event( + &OrderSide::Sell, + u128::MAX, + 500u128, + 0u128, + tiny_price, + ), + Err(Error::::ArithmeticOverflow.into()), + ); + }); +} diff --git a/pallets/limit-orders/src/tests/auxiliary/validate_and_classify.rs b/pallets/limit-orders/src/tests/auxiliary/validate_and_classify.rs new file mode 100644 index 0000000000..d1ed8be58c --- /dev/null +++ b/pallets/limit-orders/src/tests/auxiliary/validate_and_classify.rs @@ -0,0 +1,242 @@ +//! Helper tests: `validate_and_classify`. + +use super::*; + +// ───────────────────────────────────────────────────────────────────────────── +// validate_and_classify +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn validate_and_classify_separates_buys_and_sells() { + new_test_ext().execute_with(|| { + // Current time = 1_000_000 ms; expiry = 2_000_000 ms (well in the future). + MockTime::set(1_000_000); + // Price = 1.0 TAO/alpha. + MockSwap::set_price(1.0); + + let buy_order = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000u64, // amount in TAO + 2_000_000_000u64, // limit_price: willing to pay up to 2 TAO/alpha in ×10⁹ scale (scaled=1_000_000_000 ≤ 2_000_000_000 ✓) + 2_000_000u64, // expiry ms + Perbill::zero(), + fee_recipient(), + None, + ); + let sell_order = make_signed_order( + AccountKeyring::Bob, + alice(), + netuid(), + OrderType::TakeProfit, + 500u64, // amount in alpha + 1_000_000_000u64, // limit_price: sell if price >= 1 TAO/alpha in ×10⁹ scale (scaled=1_000_000_000 >= 1_000_000_000 ✓) + 2_000_000u64, + Perbill::zero(), + fee_recipient(), + None, + ); + + let orders = bounded(vec![buy_order, sell_order]); + let (buys, sells) = LimitOrders::::validate_and_classify( + netuid(), + &orders, + 1_000_000u64, + U64F64::from_num(1u32), + bob(), + ) + .expect("validate_and_classify should succeed"); + + assert_eq!(buys.len(), 1, "expected 1 valid buy"); + assert_eq!(sells.len(), 1, "expected 1 valid sell"); + + // Buy entry: gross=1000, net=1000 (0% fee_rate) + let buy = &buys[0]; + assert_eq!(buy.signer, alice()); + assert_eq!(buy.gross, 1_000u64); + assert_eq!(buy.net, 1_000u64); + assert_eq!(buy.fee_rate, Perbill::zero()); + + // Sell entry: gross=500, net=500 (fee applied on TAO output, not alpha input) + let sell = &sells[0]; + assert_eq!(sell.signer, bob()); + assert_eq!(sell.gross, 500u64); + assert_eq!(sell.net, 500u64); + }); +} + +#[test] +fn validate_and_classify_fails_for_wrong_netuid() { + new_test_ext().execute_with(|| { + // An order whose netuid does not match the batch netuid must cause a hard failure. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + let wrong_netuid_order = make_signed_order( + AccountKeyring::Alice, + bob(), + NetUid::from(99u16), // different netuid + OrderType::LimitBuy, + 1_000u64, + 2_000_000_000u64, // 2.0 in ×10⁹ scale + 2_000_000u64, + Perbill::zero(), + fee_recipient(), + None, + ); + + let orders = bounded(vec![wrong_netuid_order]); + assert_noop!( + LimitOrders::::validate_and_classify( + netuid(), // batch is for netuid 1 + &orders, + 1_000_000u64, + U64F64::from_num(1u32), + bob() + ), + crate::Error::::OrderNetUidMismatch + ); + }); +} + +#[test] +fn validate_and_classify_fails_for_expired_order() { + new_test_ext().execute_with(|| { + // now_ms = 2_000_001, expiry = 2_000_000 → expired → hard failure. + MockTime::set(2_000_001); + MockSwap::set_price(1.0); + + let expired = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000u64, + 2_000_000_000u64, // 2.0 in ×10⁹ scale + 2_000_000u64, // expiry already past + Perbill::zero(), + fee_recipient(), + None, + ); + + let orders = bounded(vec![expired]); + assert_noop!( + LimitOrders::::validate_and_classify( + netuid(), + &orders, + 2_000_001u64, + U64F64::from_num(1u32), + bob() + ), + crate::Error::::OrderExpired + ); + }); +} + +#[test] +fn validate_and_classify_fails_for_price_condition_not_met_for_buy() { + new_test_ext().execute_with(|| { + // Price = 3.0 TAO/alpha, scaled = 3_000_000_000, buyer's limit = 2_000_000_000 (2.0 in ×10⁹) → scaled > limit → hard failure. + MockTime::set(1_000_000); + let order = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000u64, + 2_000_000_000u64, // 2.0 in ×10⁹ scale + 2_000_000u64, + Perbill::zero(), + fee_recipient(), + None, + ); + + let orders = bounded(vec![order]); + assert_noop!( + LimitOrders::::validate_and_classify( + netuid(), + &orders, + 1_000_000u64, + U64F64::from_num(3u32), // current price = 3 > limit 2 → fails + bob() + ), + crate::Error::::PriceConditionNotMet + ); + }); +} + +#[test] +fn validate_and_classify_fails_for_already_processed_order() { + new_test_ext().execute_with(|| { + // An order already marked Fulfilled must cause a hard failure. + MockTime::set(1_000_000); + let order = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000u64, + 2_000_000_000u64, // 2.0 in ×10⁹ scale + 2_000_000u64, + Perbill::zero(), + fee_recipient(), + None, + ); + + // Pre-mark as fulfilled on-chain. + let oid = LimitOrders::::derive_order_id(&order.order); + Orders::::insert(oid, OrderStatus::Fulfilled); + + let orders = bounded(vec![order]); + assert_noop!( + LimitOrders::::validate_and_classify( + netuid(), + &orders, + 1_000_000u64, + U64F64::from_num(1u32), + bob() + ), + crate::Error::::OrderAlreadyProcessed + ); + }); +} + +#[test] +fn validate_and_classify_applies_buy_fee_to_net() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + // 1_000_000 ppb = 0.1% + // amount = 1_000_000_000, fee = 1_000_000, net = 999_000_000 + + let order = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000_000_000u64, + u64::MAX, // limit price: accept any price + 2_000_000u64, + Perbill::from_parts(1_000_000), // 0.1% fee + fee_recipient(), + None, + ); + + let orders = bounded(vec![order]); + let (buys, _) = LimitOrders::::validate_and_classify( + netuid(), + &orders, + 1_000_000u64, + U64F64::from_num(1u32), + bob(), + ) + .expect("validate_and_classify should succeed"); + + assert_eq!(buys.len(), 1); + let entry = &buys[0]; + assert_eq!(entry.gross, 1_000_000_000u64); + assert_eq!(entry.fee_rate, Perbill::from_parts(1_000_000)); + assert_eq!(entry.net, 999_000_000u64); + }); +} diff --git a/pallets/limit-orders/src/tests/auxiliary/validate_and_classify_slippage_relayer.rs b/pallets/limit-orders/src/tests/auxiliary/validate_and_classify_slippage_relayer.rs new file mode 100644 index 0000000000..a288426b21 --- /dev/null +++ b/pallets/limit-orders/src/tests/auxiliary/validate_and_classify_slippage_relayer.rs @@ -0,0 +1,172 @@ +//! Helper tests: `validate_and_classify_slippage_relayer`. + +use super::*; + +// ───────────────────────────────────────────────────────────────────────────── +// validate_and_classify — effective_swap_limit propagation +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn validate_and_classify_stores_effective_swap_limit_for_buy() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + // 1% slippage on limit_price=2_000_000_000 (2.0 in ×10⁹) → ceiling = 2_020_000_000. + // price=1.0, scaled=1_000_000_000 <= 2_000_000_000 ✓. + let order = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 500u64, + 2_000_000_000u64, // 2.0 in ×10⁹ scale + 2_000_000u64, + Perbill::zero(), + fee_recipient(), + None, + ); + // Override max_slippage on the inner order after signing — we need to rebuild + // the signed order so the signature covers the updated payload. + let new_inner = { + let mut o = order.order.inner().clone(); + o.max_slippage = Some(Perbill::from_percent(1)); + o + }; + let versioned = crate::VersionedOrder::V1(new_inner.clone()); + let sig = AccountKeyring::Alice.pair().sign(&versioned.encode()); + let signed_with_slippage = crate::SignedOrder { + order: versioned, + signature: sp_runtime::MultiSignature::Sr25519(sig), + partial_fill: None, + }; + + let orders = bounded(vec![signed_with_slippage]); + let (buys, _) = LimitOrders::::validate_and_classify( + netuid(), + &orders, + 1_000_000u64, + U64F64::from_num(1u32), + bob(), + ) + .expect("should succeed"); + + assert_eq!(buys[0].effective_swap_limit, 2_020_000_000); + }); +} + +#[test] +fn validate_and_classify_stores_effective_swap_limit_for_sell() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + // Price must be >= limit_price (in ×10⁹ scale) for TakeProfit to trigger. + // limit_price=1_000_000_000 (1.0 in ×10⁹), 1% slippage → floor = 990_000_000. + let new_inner = crate::Order { + signer: AccountKeyring::Alice.to_account_id(), + hotkey: bob(), + netuid: netuid(), + order_type: OrderType::TakeProfit, + amount: 500u64, + limit_price: 1_000_000_000u64, // 1.0 in ×10⁹ scale + expiry: u64::MAX, + fee_rate: Perbill::zero(), + fee_recipient: fee_recipient(), + relayer: None, + max_slippage: Some(Perbill::from_percent(1)), + chain_id: 945, + partial_fills_enabled: false, + }; + let versioned = crate::VersionedOrder::V1(new_inner); + let sig = AccountKeyring::Alice.pair().sign(&versioned.encode()); + let signed = crate::SignedOrder { + order: versioned, + signature: sp_runtime::MultiSignature::Sr25519(sig), + partial_fill: None, + }; + + let orders = bounded(vec![signed]); + let (_, sells) = LimitOrders::::validate_and_classify( + netuid(), + &orders, + 1_000_000u64, + U64F64::from_num(2u32), // current_price=2.0, scaled=2_000_000_000 >= limit_price=1_000_000_000 ✓ + bob(), + ) + .expect("should succeed"); + + assert_eq!(sells[0].effective_swap_limit, 990_000_000); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// validate_and_classify — relayer enforcement +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn validate_and_classify_fails_for_wrong_relayer() { + new_test_ext().execute_with(|| { + // Order explicitly locks execution to charlie(); submitting as bob() must fail. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + let order = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000u64, + u64::MAX, + 2_000_000u64, + Perbill::zero(), + fee_recipient(), + Some(BoundedVec::try_from(vec![charlie()]).unwrap()), // only charlie may relay this order + ); + + let orders = bounded(vec![order]); + assert_noop!( + LimitOrders::::validate_and_classify( + netuid(), + &orders, + 1_000_000u64, + U64F64::from_num(1u32), + bob() // wrong relayer + ), + crate::Error::::RelayerMissMatch + ); + }); +} + +#[test] +fn validate_and_classify_succeeds_for_correct_relayer() { + new_test_ext().execute_with(|| { + // Same setup as above but now the correct relayer (charlie) is used. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + let order = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000u64, + u64::MAX, + 2_000_000u64, + Perbill::zero(), + fee_recipient(), + Some(BoundedVec::try_from(vec![charlie()]).unwrap()), // only charlie may relay this order + ); + + let orders = bounded(vec![order]); + let (buys, sells) = LimitOrders::::validate_and_classify( + netuid(), + &orders, + 1_000_000u64, + U64F64::from_num(1u32), + charlie(), // correct relayer + ) + .expect("validate_and_classify should succeed"); + + assert_eq!(buys.len(), 1, "expected 1 valid buy"); + assert_eq!(sells.len(), 0); + }); +} diff --git a/pallets/limit-orders/src/tests/extrinsics.rs b/pallets/limit-orders/src/tests/extrinsics.rs deleted file mode 100644 index 43a85a1db1..0000000000 --- a/pallets/limit-orders/src/tests/extrinsics.rs +++ /dev/null @@ -1,3470 +0,0 @@ -#![allow(clippy::indexing_slicing)] -//! Integration tests for `pallet-limit-orders` extrinsics. -//! -//! Tests go through the full dispatch path: origin enforcement, storage changes, -//! and event emission are all verified. SwapInterface calls are handled by -//! `MockSwap`, which records calls and maintains in-memory balance ledgers. - -use codec::Encode; -use frame_support::{BoundedVec, assert_noop, assert_ok}; -use sp_core::Pair; -use sp_keyring::Sr25519Keyring as AccountKeyring; -use sp_runtime::{DispatchError, Perbill}; -use subtensor_runtime_common::NetUid; - -use crate::{ - Error, Order, OrderSide, OrderStatus, OrderType, Orders, VersionedOrder, pallet::Event, -}; - -type LimitOrders = crate::pallet::Pallet; - -use super::mock::*; - -/// Check that a specific pallet event was emitted. -fn assert_event(event: Event) { - assert!( - System::events() - .iter() - .any(|r| r.event == RuntimeEvent::LimitOrders(event.clone())), - "expected event not found: {event:?}", - ); -} - -// ───────────────────────────────────────────────────────────────────────────── -// cancel_order -// ───────────────────────────────────────────────────────────────────────────── - -#[test] -fn cancel_order_signer_can_cancel() { - new_test_ext().execute_with(|| { - let order = VersionedOrder::V1(Order { - signer: alice(), - hotkey: bob(), - netuid: netuid(), - order_type: OrderType::LimitBuy, - amount: 1_000, - limit_price: u64::MAX, - expiry: FAR_FUTURE, - fee_rate: Perbill::zero(), - fee_recipient: fee_recipient(), - relayer: None, - max_slippage: None, - chain_id: 945, - partial_fills_enabled: false, - }); - let id = order_id(&order); - - assert_ok!(LimitOrders::cancel_order( - RuntimeOrigin::signed(alice()), - order - )); - assert_eq!(Orders::::get(id), Some(OrderStatus::Cancelled)); - assert_event(Event::OrderCancelled { - order_id: id, - signer: alice(), - }); - }); -} - -#[test] -fn cancel_order_non_signer_rejected() { - new_test_ext().execute_with(|| { - let order = VersionedOrder::V1(Order { - signer: alice(), - hotkey: bob(), - netuid: netuid(), - order_type: OrderType::LimitBuy, - amount: 1_000, - limit_price: u64::MAX, - expiry: FAR_FUTURE, - fee_rate: Perbill::zero(), - fee_recipient: fee_recipient(), - relayer: None, - max_slippage: None, - chain_id: 945, - partial_fills_enabled: false, - }); - // Bob tries to cancel Alice's order. - assert_noop!( - LimitOrders::cancel_order(RuntimeOrigin::signed(bob()), order), - Error::::Unauthorized - ); - }); -} - -#[test] -fn cancel_order_already_cancelled_rejected() { - new_test_ext().execute_with(|| { - let order = VersionedOrder::V1(Order { - signer: alice(), - hotkey: bob(), - netuid: netuid(), - order_type: OrderType::LimitBuy, - amount: 1_000, - limit_price: u64::MAX, - expiry: FAR_FUTURE, - fee_rate: Perbill::zero(), - fee_recipient: fee_recipient(), - relayer: None, - max_slippage: None, - chain_id: 945, - partial_fills_enabled: false, - }); - let id = order_id(&order); - Orders::::insert(id, OrderStatus::Cancelled); - - assert_noop!( - LimitOrders::cancel_order(RuntimeOrigin::signed(alice()), order), - Error::::OrderAlreadyProcessed - ); - }); -} - -#[test] -fn cancel_order_already_fulfilled_rejected() { - new_test_ext().execute_with(|| { - let order = VersionedOrder::V1(Order { - signer: alice(), - hotkey: bob(), - netuid: netuid(), - order_type: OrderType::LimitBuy, - amount: 1_000, - limit_price: u64::MAX, - expiry: FAR_FUTURE, - fee_rate: Perbill::zero(), - fee_recipient: fee_recipient(), - relayer: None, - max_slippage: None, - chain_id: 945, - partial_fills_enabled: false, - }); - let id = order_id(&order); - Orders::::insert(id, OrderStatus::Fulfilled); - - assert_noop!( - LimitOrders::cancel_order(RuntimeOrigin::signed(alice()), order), - Error::::OrderAlreadyProcessed - ); - }); -} - -#[test] -fn cancel_order_unsigned_rejected() { - new_test_ext().execute_with(|| { - let order = VersionedOrder::V1(Order { - signer: alice(), - hotkey: bob(), - netuid: netuid(), - order_type: OrderType::LimitBuy, - amount: 1_000, - limit_price: u64::MAX, - expiry: FAR_FUTURE, - fee_rate: Perbill::zero(), - fee_recipient: fee_recipient(), - relayer: None, - max_slippage: None, - chain_id: 945, - partial_fills_enabled: false, - }); - assert_noop!( - LimitOrders::cancel_order(RuntimeOrigin::none(), order), - DispatchError::BadOrigin - ); - }); -} - -// ───────────────────────────────────────────────────────────────────────────── -// execute_orders -// ───────────────────────────────────────────────────────────────────────────── - -#[test] -fn execute_orders_buy_order_fulfilled() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - // Price = 1.0 ≤ limit = 2.0 → condition met. - let signed = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - 2_000_000_000, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let id = order_id(&signed.order); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed]), - false, - )); - - assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); - assert_event(Event::OrderExecuted { - order_id: id, - signer: alice(), - netuid: netuid(), - order_type: OrderType::LimitBuy, - amount_in: 1_000, - amount_out: 0, - }); - }); -} - -#[test] -fn execute_orders_sell_order_fulfilled() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(2.0); - // Price = 2.0, scaled = 2_000_000_000 ≥ limit = 1_000_000_000 → condition met. - let signed = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::TakeProfit, - 500, - 1_000_000_000, // 1.0 in ×10⁹ scale - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let id = order_id(&signed.order); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed]), - false, - )); - - assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); - assert_event(Event::OrderExecuted { - order_id: id, - signer: alice(), - netuid: netuid(), - order_type: OrderType::TakeProfit, - amount_in: 500, - amount_out: 0, - }); - }); -} - -#[test] -fn execute_orders_stop_loss_order_fulfilled() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(0.5); - // Price = 0.5, scaled = 500_000_000 ≤ limit = 1_000_000_000 → condition met. - let signed = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::StopLoss, - 500, - 1_000_000_000, // 1.0 in ×10⁹ scale - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let id = order_id(&signed.order); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed]), - false, - )); - - assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); - assert_event(Event::OrderExecuted { - order_id: id, - signer: alice(), - netuid: netuid(), - order_type: OrderType::StopLoss, - amount_in: 500, - amount_out: 0, - }); - }); -} - -#[test] -fn execute_orders_stop_loss_price_not_met_skipped() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(2.0); // price 2.0, scaled=2_000_000_000 > limit 1_000_000_000 → stop loss condition not met - let signed = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::StopLoss, - 500, - 1_000_000_000, // 1.0 in ×10⁹ scale - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let id = order_id(&signed.order); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed]), - false, - )); - - assert!(Orders::::get(id).is_none()); - assert_event(Event::OrderSkipped { - order_id: id, - reason: Error::::PriceConditionNotMet.into(), - }); - }); -} - -#[test] -fn execute_orders_expired_order_skipped() { - new_test_ext().execute_with(|| { - MockTime::set(2_000_001); // now > expiry - MockSwap::set_price(1.0); - let signed = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - 2_000_000, // expiry in the past - Perbill::zero(), - fee_recipient(), - None, - ); - let id = order_id(&signed.order); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed]), - false, - )); - - // Skipped — storage untouched. - assert!(Orders::::get(id).is_none()); - assert_event(Event::OrderSkipped { - order_id: id, - reason: Error::::OrderExpired.into(), - }); - }); -} - -#[test] -fn execute_orders_price_not_met_skipped() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(5.0); // price 5.0, scaled=5_000_000_000 > limit 2_000_000_000 → buy condition not met - let signed = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - 2_000_000_000, // 2.0 in ×10⁹ scale - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let id = order_id(&signed.order); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed]), - false, - )); - - assert!(Orders::::get(id).is_none()); - assert_event(Event::OrderSkipped { - order_id: id, - reason: Error::::PriceConditionNotMet.into(), - }); - }); -} - -// Regression tests: with the ×10⁹ scale fix, sub-unity prices can be meaningfully -// expressed as limit_price values. A price of 0.5 TAO/alpha is represented as -// 500_000_000 in ×10⁹ scale, enabling fine-grained TakeProfit thresholds below 1.0. -#[test] -fn take_profit_sub_unity_price_executes_when_limit_met() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - // Market price = 0.5 TAO/alpha → scaled = 500_000_000. - MockSwap::set_price(0.5); - - // limit_price = 400_000_000 (0.4 in ×10⁹ scale). - // TakeProfit condition: scaled_price (500_000_000) >= limit_price (400_000_000) ✓ - let signed = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::TakeProfit, - 500, - 400_000_000, // 0.4 in ×10⁹ scale — below current price of 0.5 - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let id = order_id(&signed.order); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed]), - false, - )); - - // Executes: 500_000_000 >= 400_000_000 → condition met. - assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); - }); -} - -#[test] -fn take_profit_sub_unity_price_skipped_when_limit_not_met() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - // Market price = 0.5 TAO/alpha → scaled = 500_000_000. - MockSwap::set_price(0.5); - - // limit_price = 600_000_000 (0.6 in ×10⁹ scale). - // TakeProfit condition: scaled_price (500_000_000) >= limit_price (600_000_000) → FALSE. - let signed = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::TakeProfit, - 500, - 600_000_000, // 0.6 in ×10⁹ scale — above current price of 0.5 - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let id = order_id(&signed.order); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed]), - false, - )); - - // Skipped: 500_000_000 >= 600_000_000 is false. - assert!(Orders::::get(id).is_none()); - assert_event(Event::OrderSkipped { - order_id: id, - reason: Error::::PriceConditionNotMet.into(), - }); - }); -} - -#[test] -fn execute_orders_already_processed_skipped() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - let signed = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let id = order_id(&signed.order); - Orders::::insert(id, OrderStatus::Fulfilled); - - // Should succeed (batch-level) but skip this order silently. - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed]), - false, - )); - // Still Fulfilled (not changed). - assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); - assert_event(Event::OrderSkipped { - order_id: id, - reason: Error::::OrderAlreadyProcessed.into(), - }); - }); -} - -#[test] -fn execute_orders_mixed_batch_valid_and_skipped() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - - let valid = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let expired = make_signed_order( - AccountKeyring::Bob, - alice(), - netuid(), - OrderType::LimitBuy, - 500, - u64::MAX, - 500_000, // already expired - Perbill::zero(), - fee_recipient(), - None, - ); - let valid_id = order_id(&valid.order); - let expired_id = order_id(&expired.order); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![valid, expired]), - false, - )); - - assert_eq!(Orders::::get(valid_id), Some(OrderStatus::Fulfilled)); - assert_event(Event::OrderSkipped { - order_id: expired_id, - reason: Error::::OrderExpired.into(), - }); - }); -} - -#[test] -fn execute_orders_unsigned_rejected() { - new_test_ext().execute_with(|| { - assert_noop!( - LimitOrders::execute_orders(RuntimeOrigin::none(), bounded(vec![]), false), - DispatchError::BadOrigin - ); - }); -} - -#[test] -fn execute_orders_buy_with_fee_charges_fee() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - - // fee_rate = 1% (10_000_000 parts-per-billion), recipient = fee_recipient(). - let signed = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::from_parts(10_000_000), // 1% - fee_recipient(), - None, - ); - MockSwap::set_tao_balance(alice(), 1_000); - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed]), - false, - )); - - // One buy_alpha call for the net amount (990 TAO after 1% fee). - let buys: Vec<_> = MockSwap::log() - .into_iter() - .filter_map(|c| { - if let super::mock::SwapCall::BuyAlpha { tao, .. } = c { - Some(tao) - } else { - None - } - }) - .collect(); - assert_eq!(buys, vec![990], "main swap must use 990 TAO after 1% fee"); - - // Fee (10 TAO) forwarded directly to fee_recipient via transfer_tao. - assert_eq!(MockSwap::tao_balance(&fee_recipient()), 10); - }); -} - -#[test] -fn execute_orders_sell_with_fee_charges_fee() { - new_test_ext().execute_with(|| { - // fee = 1% (10_000_000 ppb). - // Alice sells 1_000 alpha; pool returns 800 TAO. - // fee_tao = 1% of 800 = 8 TAO, forwarded to fee_recipient via transfer_tao. - // Alice keeps 792 TAO. - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_sell_tao_return(800); - MockSwap::set_alpha_balance(alice(), bob(), netuid(), 1_000); - - let signed = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::TakeProfit, - 1_000, - 0, - FAR_FUTURE, - Perbill::from_parts(10_000_000), // 1% - fee_recipient(), - None, - ); - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed]), - false, - )); - - // Full 1_000 alpha sold (no alpha deducted for fee). - let sells: Vec<_> = MockSwap::log() - .into_iter() - .filter_map(|c| { - if let super::mock::SwapCall::SellAlpha { alpha, .. } = c { - Some(alpha) - } else { - None - } - }) - .collect(); - assert_eq!(sells, vec![1_000], "full alpha amount must be sold"); - - // fee_recipient received 8 TAO (1% of 800). - assert_eq!(MockSwap::tao_balance(&fee_recipient()), 8); - // Alice kept the remaining 792 TAO. - assert_eq!(MockSwap::tao_balance(&alice()), 792); - }); -} - -#[test] -fn execute_orders_empty_batch_returns_ok() { - new_test_ext().execute_with(|| { - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![]), - false, - )); - }); -} - -#[test] -fn execute_orders_fee_transfer_failure_skips_order() { - new_test_ext().execute_with(|| { - // When the fee transfer fails the entire order is rolled back and emits OrderSkipped. - // This prevents users from exploiting a tight balance to execute swaps fee-free. - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_buy_alpha_return(500); - MockSwap::set_tao_balance(alice(), 10_000); - - let signed = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::from_parts(10_000_000), // 1% - fee_recipient(), - None, - ); - - FAIL_FEE_TRANSFER.with(|f| *f.borrow_mut() = true); - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed.clone()]), - false, - )); - FAIL_FEE_TRANSFER.with(|f| *f.borrow_mut() = false); - - // Order was skipped — not stored as Fulfilled. - let id = crate::tests::mock::order_id(&signed.order); - assert!(Orders::::get(id).is_none()); - - // OrderSkipped was emitted with the fee-transfer error as the reason. - assert_event(Event::OrderSkipped { - order_id: id, - reason: DispatchError::CannotLookup, - }); - - // fee_recipient received nothing. - assert_eq!(MockSwap::tao_balance(&fee_recipient()), 0); - }); -} - -// ───────────────────────────────────────────────────────────────────────────── -// execute_orders — silent-skip behaviour -// ───────────────────────────────────────────────────────────────────────────── - -mod execute_orders_skip_invalid { - use super::*; - - /// A single expired order is silently skipped: the call returns `Ok` and - /// nothing is written to the `Orders` storage map. - #[test] - fn execute_orders_skips_expired_order() { - new_test_ext().execute_with(|| { - MockTime::set(2_000_001); // now > expiry - MockSwap::set_price(1.0); - - let signed = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - 2_000_000, // expiry in the past - Perbill::zero(), - fee_recipient(), - None, - ); - let id = order_id(&signed.order); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed]), - false, - )); - - // Skipped — storage untouched. - assert!(Orders::::get(id).is_none()); - assert_event(Event::OrderSkipped { - order_id: id, - reason: Error::::OrderExpired.into(), - }); - }); - } - - /// A LimitBuy with `limit_price = 0` (price ceiling below current price) - /// is silently skipped: the call returns `Ok` and nothing is written to - /// the `Orders` storage map. - #[test] - fn execute_orders_skips_price_condition_not_met() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(5.0); // price 5.0 > limit 0 → buy condition not met - - let signed = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - 0, // price ceiling of 0 — never satisfied at price 5.0 - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let id = order_id(&signed.order); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed]), - false, - )); - - // Skipped — storage untouched. - assert!(Orders::::get(id).is_none()); - assert_event(Event::OrderSkipped { - order_id: id, - reason: Error::::PriceConditionNotMet.into(), - }); - }); - } - - /// A batch containing one valid order and one expired order: the call - /// returns `Ok`, the valid order is stored as `Fulfilled`, and the expired - /// order is NOT written to storage. - #[test] - fn execute_orders_valid_and_invalid_mixed() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - - let valid = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let expired = make_signed_order( - AccountKeyring::Bob, - alice(), - netuid(), - OrderType::LimitBuy, - 500, - u64::MAX, - 500_000, // already expired - Perbill::zero(), - fee_recipient(), - None, - ); - let valid_id = order_id(&valid.order); - let expired_id = order_id(&expired.order); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![valid, expired]), - false, - )); - - // Valid order executed successfully. - assert_eq!(Orders::::get(valid_id), Some(OrderStatus::Fulfilled)); - // Expired order silently skipped — not written to storage. - assert!(Orders::::get(expired_id).is_none()); - assert_event(Event::OrderSkipped { - order_id: expired_id, - reason: Error::::OrderExpired.into(), - }); - }); - } - - /// With `should_fail = true` a single expired order is NOT silently skipped: - /// the whole call fails with `OrderExpired` and storage stays untouched. - #[test] - fn execute_orders_should_fail_expired_order_reverts() { - new_test_ext().execute_with(|| { - MockTime::set(2_000_001); // now > expiry - MockSwap::set_price(1.0); - - let signed = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - 2_000_000, // expiry in the past - Perbill::zero(), - fee_recipient(), - None, - ); - let id = order_id(&signed.order); - - // all-or-nothing: the failing order makes the whole call return Err - // and assert_noop! confirms storage is unchanged. - assert_noop!( - LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed]), - true, - ), - Error::::OrderExpired - ); - - assert!(Orders::::get(id).is_none()); - }); - } - - /// With `should_fail = true` a batch containing a VALID order followed by an - /// INVALID (expired) order reverts entirely: the valid order's effects are - /// rolled back, so it is NOT recorded as `Fulfilled` and the relayer's TAO - /// is not consumed. Contrast `execute_orders_valid_and_invalid_mixed`, where - /// the same batch with `should_fail = false` keeps the valid order. - #[test] - fn execute_orders_should_fail_valid_then_invalid_reverts_whole_batch() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - - let valid = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let expired = make_signed_order( - AccountKeyring::Bob, - alice(), - netuid(), - OrderType::LimitBuy, - 500, - u64::MAX, - 500_000, // already expired - Perbill::zero(), - fee_recipient(), - None, - ); - let valid_id = order_id(&valid.order); - let expired_id = order_id(&expired.order); - - // The expired order is the second in the batch; with should_fail = true - // its failure reverts the already-executed valid order too. - assert_noop!( - LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![valid, expired]), - true, - ), - Error::::OrderExpired - ); - - // Neither order survived: the valid order's Fulfilled status was rolled back. - assert!(Orders::::get(valid_id).is_none()); - assert!(Orders::::get(expired_id).is_none()); - }); - } - - /// With `should_fail = true` a price-condition-not-met order hard-fails the - /// whole call with `PriceConditionNotMet`, mirroring `execute_batched_orders` - /// rather than the best-effort skip path. - #[test] - fn execute_orders_should_fail_price_condition_not_met_reverts() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(5.0); // price 5.0 > limit 0 → buy condition not met - - let signed = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - 0, // price ceiling of 0 — never satisfied at price 5.0 - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let id = order_id(&signed.order); - - assert_noop!( - LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed]), - true, - ), - Error::::PriceConditionNotMet - ); - - assert!(Orders::::get(id).is_none()); - }); - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// execute_batched_orders -// ───────────────────────────────────────────────────────────────────────────── - -#[test] -fn execute_batched_orders_unsigned_rejected() { - new_test_ext().execute_with(|| { - assert_noop!( - LimitOrders::execute_batched_orders(RuntimeOrigin::none(), netuid(), bounded(vec![])), - DispatchError::BadOrigin - ); - }); -} - -#[test] -fn execute_batched_orders_all_invalid_fails() { - new_test_ext().execute_with(|| { - // An expired order causes the whole batch to fail. - MockTime::set(2_000_001); // all expired - let expired = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - 1_000_000, - Perbill::zero(), - fee_recipient(), - None, - ); - assert_noop!( - LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![expired]), - ), - Error::::OrderExpired - ); - }); -} - -#[test] -fn execute_batched_orders_fails_for_wrong_netuid() { - new_test_ext().execute_with(|| { - // An order whose netuid does not match the batch netuid must cause the batch to fail. - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_buy_alpha_return(100); - - let wrong_net = make_signed_order( - AccountKeyring::Alice, - bob(), - NetUid::from(99u16), // wrong netuid - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - - assert_noop!( - LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), // batch targets netuid 1 - bounded(vec![wrong_net]), - ), - Error::::OrderNetUidMismatch - ); - }); -} - -#[test] -fn execute_batched_orders_price_condition_not_met_fails_entire_batch() { - new_test_ext().execute_with(|| { - // Price condition not met is a hard-fail in execute_batched_orders — - // unlike execute_orders where it silently skips the order. - MockTime::set(1_000_000); - MockSwap::set_price(100.0); // current price = 100, scaled = 100_000_000_000 - - // LimitBuy requires scaled_price <= limit_price; with limit_price=1_000_000_000 (1.0) this fails. - let order = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - 1_000_000_000, // 1.0 in ×10⁹ scale, far below scaled price of 100_000_000_000 - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - - assert_noop!( - LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![order]) - ), - Error::::PriceConditionNotMet - ); - }); -} - -#[test] -fn execute_batched_orders_buy_only_fulfills_orders_and_distributes_alpha() { - new_test_ext().execute_with(|| { - // Setup: - // Alice buys 600 TAO, Bob buys 400 TAO (total 1000 TAO net, fee=0). - // Pool returns 500 alpha (MOCK_BUY_ALPHA_RETURN). - // No sellers → total_alpha = 500. - // Pro-rata: Alice 500*600/1000=300, Bob 500*400/1000=200. - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_buy_alpha_return(500); - MockSwap::set_tao_balance(alice(), 600); - MockSwap::set_tao_balance(bob(), 400); - - let alice_order = make_signed_order( - AccountKeyring::Alice, - dave(), - netuid(), - OrderType::LimitBuy, - 600, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let bob_order = make_signed_order( - AccountKeyring::Bob, - dave(), - netuid(), - OrderType::LimitBuy, - 400, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let alice_id = order_id(&alice_order.order); - let bob_id = order_id(&bob_order.order); - - assert_ok!(LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![alice_order, bob_order]), - )); - - // Both orders fulfilled. - assert_eq!(Orders::::get(alice_id), Some(OrderStatus::Fulfilled)); - assert_eq!(Orders::::get(bob_id), Some(OrderStatus::Fulfilled)); - - // Alpha distributed pro-rata. - assert_eq!(MockSwap::alpha_balance(&alice(), &dave(), netuid()), 300); - assert_eq!(MockSwap::alpha_balance(&bob(), &dave(), netuid()), 200); - - // Summary event. - assert_event(Event::GroupExecutionSummary { - netuid: netuid(), - net_side: OrderSide::Buy, - net_amount: 1_000, - actual_out: 500, - executed_count: 2, - }); - }); -} - -/// Regression test for the zero-share batch fund-loss bug. -/// -/// Bug (pre-fix): `collect_assets` debited every buyer's full TAO input up front, -/// then `distribute_alpha_pro_rata` floored each buyer's alpha share. When a -/// buyer's `share = floor(total_alpha * net / total_buy_net)` floored to 0, the -/// old code silently SKIPPED the alpha transfer (`if share > 0 { .. }`) yet STILL -/// marked the order `Fulfilled`. The victim therefore paid full TAO, received zero -/// alpha, and the order was permanently closed. -/// -/// Fix: `distribute_alpha_pro_rata` now `ensure!(share > 0, ZeroShareInBatch)`, -/// hard-failing the whole `execute_batched_orders` call. In production FRAME's -/// per-dispatch storage layer then rolls back `collect_assets` and the pool swap, -/// so no signer is debited and no order is stored. -/// -/// `assert_noop!` asserts both the error AND that no on-chain storage mutation -/// persisted — i.e. neither order is written, so neither is marked `Fulfilled`. -/// Against the old code this call returned `Ok` and wrote `Fulfilled`, so the -/// `assert_noop!` (storage-root-unchanged) would have FAILED. -/// -/// NOTE: we deliberately do NOT assert the victim's TAO balance was refunded. -/// `MockSwap` keeps balances in a `thread_local!` map that lives OUTSIDE the -/// substrate storage overlay, so `collect_assets`' debit is not transactional in -/// the mock and is not rolled back here. The balance refund is a property of the -/// real `frame_system` balances under the dispatch storage layer (exercised by the -/// L2/integration PoC), not something this mock can model. -#[test] -fn execute_batched_orders_zero_share_buyer_hard_fails() { - new_test_ext().execute_with(|| { - // Buy-only batch, price 1.0, pool alpha output pinned to 1000. - // big buyer net = 1_000_000 TAO - // victim buyer net = 1 TAO - // total_buy_net = 1_000_001 - // total_alpha = actual_out(1000) + total_sell_net(0) = 1000 - // victim share = floor(1000 * 1 / 1_000_001) = 0 → ZeroShareInBatch - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_buy_alpha_return(1000); - // Distinct signer coldkeys; each buyer must be able to cover its own input. - MockSwap::set_tao_balance(alice(), 1_000_000); // big buyer (Alice) - MockSwap::set_tao_balance(bob(), 1); // victim (Bob) - - let big_buyer = make_signed_order( - AccountKeyring::Alice, - dave(), - netuid(), - OrderType::LimitBuy, - 1_000_000, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let victim = make_signed_order( - AccountKeyring::Bob, - dave(), - netuid(), - OrderType::LimitBuy, - 1, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let big_id = order_id(&big_buyer.order); - let victim_id = order_id(&victim.order); - - // The whole batch must hard-fail with ZeroShareInBatch. assert_noop! also asserts - // the storage root is unchanged, so neither order was written/marked Fulfilled — - // the core of the fix. (Old code: returned Ok and wrote Fulfilled → this fails.) - assert_noop!( - LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![big_buyer, victim]), - ), - Error::::ZeroShareInBatch - ); - - // Explicit, redundant-with-assert_noop! statement of intent: no order is terminal. - assert_eq!(Orders::::get(victim_id), None); - assert_eq!(Orders::::get(big_id), None); - }); -} - -/// Guards against over-restriction: the `ZeroShareInBatch` fix must NOT reject a -/// legitimate multi-buyer batch where every buyer's floored share is at least 1. -#[test] -fn execute_batched_orders_all_nonzero_shares_still_succeeds() { - new_test_ext().execute_with(|| { - // Buy-only, price 1.0, pool alpha output = 1000, comparable buyer nets so - // neither share floors to zero: - // Alice net = 600, Bob net = 400, total_buy_net = 1000, total_alpha = 1000 - // Alice share = floor(1000 * 600 / 1000) = 600 - // Bob share = floor(1000 * 400 / 1000) = 400 - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_buy_alpha_return(1000); - MockSwap::set_tao_balance(alice(), 600); - MockSwap::set_tao_balance(bob(), 400); - - let alice_order = make_signed_order( - AccountKeyring::Alice, - dave(), - netuid(), - OrderType::LimitBuy, - 600, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let bob_order = make_signed_order( - AccountKeyring::Bob, - dave(), - netuid(), - OrderType::LimitBuy, - 400, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let alice_id = order_id(&alice_order.order); - let bob_id = order_id(&bob_order.order); - - assert_ok!(LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![alice_order, bob_order]), - )); - - // Both orders fulfilled and both buyers received non-zero alpha. - assert_eq!(Orders::::get(alice_id), Some(OrderStatus::Fulfilled)); - assert_eq!(Orders::::get(bob_id), Some(OrderStatus::Fulfilled)); - assert_eq!(MockSwap::alpha_balance(&alice(), &dave(), netuid()), 600); - assert_eq!(MockSwap::alpha_balance(&bob(), &dave(), netuid()), 400); - assert!(MockSwap::alpha_balance(&alice(), &dave(), netuid()) > 0); - assert!(MockSwap::alpha_balance(&bob(), &dave(), netuid()) > 0); - }); -} - -/// Sell-side analogue of the zero-share regression. A seller whose `net_share` -/// floors to 0 in `distribute_tao_pro_rata` must hard-fail the whole batch with -/// `ZeroShareInBatch`. `assert_noop!` proves no on-chain storage mutation persisted -/// (neither order is written/marked Fulfilled). As in the buy-side test, the -/// seller's collected alpha is not refunded *in the mock* (MockSwap balances are -/// thread_local, outside the storage overlay); the refund is a real-balance -/// property under the dispatch storage layer, not modelled here. -#[test] -fn execute_batched_orders_zero_share_seller_hard_fails() { - new_test_ext().execute_with(|| { - // Sell-only batch, price 1.0, pool TAO output pinned to 1000. - // big seller alpha = 1_000_000 → sell_tao_equiv 1_000_000 - // victim seller alpha = 1 → sell_tao_equiv 1 - // total_sell_tao_equiv = 1_000_001 - // total_tao = actual_out(1000) + total_buy_net(0) = 1000 - // victim gross_share = floor(1000 * 1 / 1_000_001) = 0 - // net_share = 0 → ZeroShareInBatch - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_sell_tao_return(1000); - MockSwap::set_alpha_balance(alice(), dave(), netuid(), 1_000_000); // big seller - MockSwap::set_alpha_balance(bob(), dave(), netuid(), 1); // victim seller - - let big_seller = make_signed_order( - AccountKeyring::Alice, - dave(), - netuid(), - OrderType::TakeProfit, - 1_000_000, - 0, // limit=0 → accept any price - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let victim = make_signed_order( - AccountKeyring::Bob, - dave(), - netuid(), - OrderType::TakeProfit, - 1, - 0, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let big_id = order_id(&big_seller.order); - let victim_id = order_id(&victim.order); - - // The whole batch must hard-fail with ZeroShareInBatch; assert_noop! also asserts - // the storage root is unchanged, so neither order was written/marked Fulfilled. - assert_noop!( - LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![big_seller, victim]), - ), - Error::::ZeroShareInBatch - ); - - // Explicit, redundant-with-assert_noop! statement of intent: no order is terminal. - assert_eq!(Orders::::get(victim_id), None); - assert_eq!(Orders::::get(big_id), None); - }); -} - -#[test] -fn execute_batched_orders_sell_only_fulfills_orders_and_distributes_tao() { - new_test_ext().execute_with(|| { - // Setup: - // Alice sells 300 alpha, Bob sells 200 alpha (total 500 alpha, fee=0). - // Price = 2.0 → sell_tao_equiv: Alice 600, Bob 400, total 1000. - // Pool returns 800 TAO (MOCK_SELL_TAO_RETURN) for the net 500 alpha. - // No buyers → total_tao = 800 + 0 = 800. - // Pro-rata: Alice 800*600/1000=480, Bob 800*400/1000=320. - MockTime::set(1_000_000); - MockSwap::set_price(2.0); - MockSwap::set_sell_tao_return(800); - MockSwap::set_alpha_balance(alice(), dave(), netuid(), 300); - MockSwap::set_alpha_balance(bob(), dave(), netuid(), 200); - - let alice_order = make_signed_order( - AccountKeyring::Alice, - dave(), - netuid(), - OrderType::TakeProfit, - 300, - 0, - FAR_FUTURE, // limit=0 → accept any price - Perbill::zero(), - fee_recipient(), - None, - ); - let bob_order = make_signed_order( - AccountKeyring::Bob, - dave(), - netuid(), - OrderType::TakeProfit, - 200, - 0, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let alice_id = order_id(&alice_order.order); - let bob_id = order_id(&bob_order.order); - - assert_ok!(LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![alice_order, bob_order]), - )); - - assert_eq!(Orders::::get(alice_id), Some(OrderStatus::Fulfilled)); - assert_eq!(Orders::::get(bob_id), Some(OrderStatus::Fulfilled)); - - // TAO distributed pro-rata. - assert_eq!(MockSwap::tao_balance(&alice()), 480); - assert_eq!(MockSwap::tao_balance(&bob()), 320); - - assert_event(Event::GroupExecutionSummary { - netuid: netuid(), - net_side: OrderSide::Sell, - net_amount: 500, - actual_out: 800, - executed_count: 2, - }); - }); -} - -#[test] -fn execute_batched_orders_buy_dominant_mixed() { - new_test_ext().execute_with(|| { - // Setup (fee=0, price=2.0 TAO/alpha): - // Buyers: Alice 1000 TAO, Bob 600 TAO → total_buy_net = 1600. - // Sellers: Charlie 200 alpha → sell_tao_equiv = 400 TAO. - // Net (buy-dominant): 1600 - 400 = 1200 TAO goes to pool. - // Pool returns 300 alpha (MOCK_BUY_ALPHA_RETURN). - // total_alpha for buyers = 300 (pool) + 200 (seller passthrough) = 500. - // Pro-rata buyers (by buy_net TAO): - // Alice: 500 * 1000/1600 = 312 alpha - // Bob: 500 * 600/1600 = 187 alpha - // (dust = 1 alpha stays in pallet) - // Sellers (buy-dominant branch): total_tao = total_sell_tao_equiv = 400. - // Charlie: 400 * 400/400 = 400 TAO. - MockTime::set(1_000_000); - MockSwap::set_price(2.0); - MockSwap::set_buy_alpha_return(300); - MockSwap::set_tao_balance(alice(), 1_000); - MockSwap::set_tao_balance(bob(), 600); - MockSwap::set_alpha_balance(charlie(), dave(), netuid(), 200); - - let alice_buy = make_signed_order( - AccountKeyring::Alice, - dave(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let bob_buy = make_signed_order( - AccountKeyring::Bob, - dave(), - netuid(), - OrderType::LimitBuy, - 600, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let charlie_sell = make_signed_order( - AccountKeyring::Charlie, - dave(), - netuid(), - OrderType::TakeProfit, - 200, - 0, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - - assert_ok!(LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(dave()), - netuid(), - bounded(vec![alice_buy, bob_buy, charlie_sell]), - )); - - assert_eq!(MockSwap::alpha_balance(&alice(), &dave(), netuid()), 312); - assert_eq!(MockSwap::alpha_balance(&bob(), &dave(), netuid()), 187); - assert_eq!(MockSwap::tao_balance(&charlie()), 400); - - assert_event(Event::GroupExecutionSummary { - netuid: netuid(), - net_side: OrderSide::Buy, - net_amount: 1_200, - actual_out: 300, - executed_count: 3, - }); - }); -} - -#[test] -fn execute_batched_orders_sell_dominant_mixed() { - new_test_ext().execute_with(|| { - // Setup (fee=0, price=2.0 TAO/alpha): - // Buyers: Alice 200 TAO → total_buy_net = 200. - // Sellers: Bob 300 alpha, Charlie 200 alpha → total_sell_net = 500. - // sell_tao_equiv: Bob 600, Charlie 400, total 1000. - // Net (sell-dominant): buy_alpha_equiv = 200/2 = 100 alpha; - // residual sell alpha = 500 - 100 = 400 alpha → pool returns 300 TAO. - // total_tao for sellers = 300 (pool) + 200 (buy passthrough) = 500 TAO. - // Pro-rata sellers (by sell_tao_equiv): - // Bob: 500 * 600/1000 = 300 TAO - // Charlie: 500 * 400/1000 = 200 TAO - // total_alpha for buyers = buy_net / price = 200/2 = 100 alpha. - // Alice: 100 * 200/200 = 100 alpha. - MockTime::set(1_000_000); - MockSwap::set_price(2.0); - MockSwap::set_sell_tao_return(300); - MockSwap::set_tao_balance(alice(), 200); - MockSwap::set_alpha_balance(bob(), dave(), netuid(), 300); - MockSwap::set_alpha_balance(charlie(), dave(), netuid(), 200); - - let alice_buy = make_signed_order( - AccountKeyring::Alice, - dave(), - netuid(), - OrderType::LimitBuy, - 200, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let bob_sell = make_signed_order( - AccountKeyring::Bob, - dave(), - netuid(), - OrderType::TakeProfit, - 300, - 0, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let charlie_sell = make_signed_order( - AccountKeyring::Charlie, - dave(), - netuid(), - OrderType::TakeProfit, - 200, - 0, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - - assert_ok!(LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(dave()), - netuid(), - bounded(vec![alice_buy, bob_sell, charlie_sell]), - )); - - assert_eq!(MockSwap::alpha_balance(&alice(), &dave(), netuid()), 100); - assert_eq!(MockSwap::tao_balance(&bob()), 300); - assert_eq!(MockSwap::tao_balance(&charlie()), 200); - - assert_event(Event::GroupExecutionSummary { - netuid: netuid(), - net_side: OrderSide::Sell, - net_amount: 400, - actual_out: 300, - executed_count: 3, - }); - }); -} - -#[test] -fn execute_batched_orders_fee_forwarded_to_collector() { - new_test_ext().execute_with(|| { - // fee = 1% (10_000_000 ppb). - // Alice buys 1000 TAO: fee = 10, net = 990. - // Pool returns 500 alpha for 990 TAO. - // collect_fees transfers 10 TAO (buy fee) to fee_recipient. - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_buy_alpha_return(500); - - let alice_buy = make_signed_order( - AccountKeyring::Alice, - dave(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::from_parts(10_000_000), // 1% - fee_recipient(), - None, - ); - - assert_ok!(LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![alice_buy]), - )); - - // Fee recipient received the buy-side fee. - assert_eq!(MockSwap::tao_balance(&fee_recipient()), 10); - }); -} - -#[test] -fn execute_batched_orders_fails_for_cancelled_order() { - new_test_ext().execute_with(|| { - // A cancelled order is already processed; including it in the batch must cause a hard failure. - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_buy_alpha_return(100); - - let signed = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let id = order_id(&signed.order); - Orders::::insert(id, OrderStatus::Cancelled); - - assert_noop!( - LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![signed]), - ), - Error::::OrderCancelled - ); - - // Still cancelled, not changed to Fulfilled. - assert_eq!(Orders::::get(id), Some(OrderStatus::Cancelled)); - }); -} - -#[test] -fn execute_batched_orders_fees_charged_on_both_sides_when_matched_internally() { - new_test_ext().execute_with(|| { - // fee = 1% (10_000_000 ppb), price = 1.0 TAO/alpha. - // - // Alice buys 1_000 TAO → buy fee = 10 TAO, net = 990 TAO. - // Bob sells 1_000 alpha → sell_tao_equiv = 1_000 TAO. - // - // sell-dominant: residual = 1_000 - 990 = 10 alpha sent to pool. - // Pool returns 9 TAO (mocked) for that residual. - // total_tao for sellers = 9 (pool) + 990 (buy passthrough) = 999. - // Bob gross_share = 999 * 1_000/1_000 = 999. - // Sell fee = mul_floor(1%, 999) = floor(9.99) = 9; Bob nets 990 TAO. - // fee_recipient total = buy_fee(10) + sell_fee(9) = 19 TAO. - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_sell_tao_return(9); - MockSwap::set_tao_balance(alice(), 1_000); - MockSwap::set_alpha_balance(bob(), dave(), netuid(), 1_000); - - let alice_buy = make_signed_order( - AccountKeyring::Alice, - dave(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::from_parts(10_000_000), // 1% - fee_recipient(), - None, - ); - let bob_sell = make_signed_order( - AccountKeyring::Bob, - dave(), - netuid(), - OrderType::TakeProfit, - 1_000, - 0, - FAR_FUTURE, - Perbill::from_parts(10_000_000), // 1% - fee_recipient(), - None, - ); - - assert_ok!(LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![alice_buy, bob_sell]), - )); - - // Both sides charged: fee_recipient gets buy fee (10) + sell fee (9) = 19. - assert_eq!(MockSwap::tao_balance(&fee_recipient()), 19); - // Bob receives 990 TAO after sell-side fee (999 gross - 9 fee). - assert_eq!(MockSwap::tao_balance(&bob()), 990); - }); -} - -// ───────────────────────────────────────────────────────────────────────────── -// net_pool_swap – SwapReturnedZero errors -// ───────────────────────────────────────────────────────────────────────────── - -#[test] -fn execute_batched_orders_buy_zero_alpha_returns_error() { - new_test_ext().execute_with(|| { - // buy_alpha returns 0 alpha for a non-zero TAO input → SwapReturnedZero. - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_buy_alpha_return(0); // pool gives back nothing - MockSwap::set_tao_balance(alice(), 1_000); - - let order = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - - assert_noop!( - LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![order]), - ), - Error::::SwapReturnedZero - ); - }); -} - -#[test] -fn execute_batched_orders_sell_zero_tao_returns_error() { - new_test_ext().execute_with(|| { - // sell_alpha returns 0 TAO for a non-zero alpha input → SwapReturnedZero. - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_sell_tao_return(0); // pool gives back nothing - MockSwap::set_alpha_balance(alice(), bob(), netuid(), 1_000); - - let order = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::TakeProfit, - 1_000, - 0, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - - assert_noop!( - LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![order]), - ), - Error::::SwapReturnedZero - ); - }); -} - -#[test] -fn execute_batched_orders_sell_alpha_respects_swap_fail() { - new_test_ext().execute_with(|| { - // sell_alpha should propagate DispatchError when MOCK_SWAP_FAIL is set. - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_swap_fail(true); - MockSwap::set_alpha_balance(alice(), bob(), netuid(), 1_000); - - let order = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::TakeProfit, - 1_000, - 0, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - - assert_noop!( - LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![order]), - ), - DispatchError::Other("pool error") - ); - }); -} - -// ───────────────────────────────────────────────────────────────────────────── -// fee routing – multiple recipients -// ───────────────────────────────────────────────────────────────────────────── - -#[test] -fn execute_batched_orders_fees_routed_to_different_recipients() { - new_test_ext().execute_with(|| { - // Alice and Bob both buy; Alice's fee goes to charlie(), Bob's to dave(). - // fee = 1% for both orders. - // Alice buys 1_000 TAO: fee = 10 → charlie(). - // Bob buys 1_000 TAO: fee = 10 → dave(). - // Pool returns 900 alpha total for 1_980 TAO net. - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_buy_alpha_return(900); - MockSwap::set_tao_balance(alice(), 1_000); - MockSwap::set_tao_balance(bob(), 1_000); - - let alice_buy = make_signed_order( - AccountKeyring::Alice, - dave(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::from_parts(10_000_000), // 1% - charlie(), - None, - ); - let bob_buy = make_signed_order( - AccountKeyring::Bob, - dave(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::from_parts(10_000_000), // 1% - dave(), - None, - ); - - assert_ok!(LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![alice_buy, bob_buy]), - )); - - // Each recipient gets exactly their order's fee. - assert_eq!( - MockSwap::tao_balance(&charlie()), - 10, - "charlie gets Alice's fee" - ); - assert_eq!(MockSwap::tao_balance(&dave()), 10, "dave gets Bob's fee"); - }); -} - -#[test] -fn execute_batched_orders_fees_batched_for_shared_recipient() { - new_test_ext().execute_with(|| { - // Both Alice and Bob's fees go to the same recipient (charlie()). - // Expect a single combined transfer of 20 TAO to charlie(). - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_buy_alpha_return(900); - MockSwap::set_tao_balance(alice(), 1_000); - MockSwap::set_tao_balance(bob(), 1_000); - - let alice_buy = make_signed_order( - AccountKeyring::Alice, - dave(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::from_parts(10_000_000), // 1% - charlie(), - None, - ); - let bob_buy = make_signed_order( - AccountKeyring::Bob, - dave(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::from_parts(10_000_000), // 1% - charlie(), - None, - ); - - assert_ok!(LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![alice_buy, bob_buy]), - )); - - // One combined transfer: charlie() receives 10 + 10 = 20 TAO. - let fee_transfers: Vec<_> = MockSwap::tao_transfers() - .into_iter() - .filter(|(_, to, _)| to == &charlie()) - .collect(); - assert_eq!( - fee_transfers.len(), - 1, - "single transfer to shared recipient" - ); - assert_eq!(fee_transfers[0].2, 20, "combined fee = 20 TAO"); - }); -} - -/// 4 orders split across 2 fee recipients. -/// -/// Orders: -/// Alice LimitBuy 1_000 TAO fee_recipient = ferdie (buy-fee collector) -/// Bob LimitBuy 1_000 TAO fee_recipient = ferdie (buy-fee collector) -/// Charlie TakeProfit 1_000 α fee_recipient = fee_recipient() (sell-fee collector) -/// Eve TakeProfit 1_000 α fee_recipient = fee_recipient() (sell-fee collector) -/// -/// Neither ferdie nor fee_recipient() are order signers, so every TAO transfer -/// to those accounts is exclusively a fee transfer — making the single-transfer -/// assertion unambiguous. -/// -/// At price 1.0 (1 TAO = 1 α), fee = 1%: -/// net buy TAO = (1_000 - 10) + (1_000 - 10) = 1_980 -/// sell α equiv = 2_000 TAO → sell-dominant, residual = 20 α → pool -/// pool returns 18 TAO for residual -/// total TAO for sellers = 18 + 1_980 = 1_998 -/// each seller gross_share = 1_998 * 1_000 / 2_000 = 999 -/// sell fee = mul_floor(1%, 999) = floor(9.99) = 9 TAO each -/// -/// Expected: -/// ferdie receives 10 (Alice) + 10 (Bob) = 20 TAO (1 transfer) -/// fee_recipient() receives 9 (Charlie) + 9 (Eve) = 18 TAO (1 transfer) -#[test] -fn execute_batched_orders_four_orders_two_fee_recipients() { - new_test_ext().execute_with(|| { - let ferdie = AccountKeyring::Ferdie.to_account_id(); - let eve = AccountKeyring::Eve.to_account_id(); - - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_sell_tao_return(18); - MockSwap::set_tao_balance(alice(), 1_000); - MockSwap::set_tao_balance(bob(), 1_000); - MockSwap::set_alpha_balance(charlie(), dave(), netuid(), 1_000); - MockSwap::set_alpha_balance(eve.clone(), dave(), netuid(), 1_000); - - let alice_buy = make_signed_order( - AccountKeyring::Alice, - dave(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::from_parts(10_000_000), // 1% - ferdie.clone(), - None, - ); - let bob_buy = make_signed_order( - AccountKeyring::Bob, - dave(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::from_parts(10_000_000), // 1% - ferdie.clone(), - None, - ); - let charlie_sell = make_signed_order( - AccountKeyring::Charlie, - dave(), - netuid(), - OrderType::TakeProfit, - 1_000, - 0, - FAR_FUTURE, - Perbill::from_parts(10_000_000), // 1% - fee_recipient(), - None, - ); - let eve_sell = make_signed_order( - AccountKeyring::Eve, - dave(), - netuid(), - OrderType::TakeProfit, - 1_000, - 0, - FAR_FUTURE, - Perbill::from_parts(10_000_000), // 1% - fee_recipient(), - None, - ); - - assert_ok!(LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(alice()), - netuid(), - bounded(vec![alice_buy, bob_buy, charlie_sell, eve_sell]), - )); - - // ferdie collects Alice's and Bob's buy fees: 10 + 10 = 20 TAO in one transfer. - let ferdie_transfers: Vec<_> = MockSwap::tao_transfers() - .into_iter() - .filter(|(_, to, _)| to == &ferdie) - .collect(); - assert_eq!(ferdie_transfers.len(), 1, "single transfer to ferdie"); - assert_eq!( - ferdie_transfers[0].2, 20, - "ferdie receives 20 TAO in buy fees" - ); - - // fee_recipient() collects Charlie's and Eve's sell fees: 10 + 10 = 20 TAO in one transfer. - let fp_transfers: Vec<_> = MockSwap::tao_transfers() - .into_iter() - .filter(|(_, to, _)| to == &fee_recipient()) - .collect(); - assert_eq!(fp_transfers.len(), 1, "single transfer to fee_recipient"); - assert_eq!( - fp_transfers[0].2, 18, - "fee_recipient receives 18 TAO in sell fees" - ); - }); -} - -/// A mixed batch (buy + sell) must not rate-limit the pallet intermediary -/// account during asset collection, which would otherwise block the -/// subsequent alpha distribution to buyers. -/// -/// Regression test: previously `transfer_staked_alpha` with a single -/// `apply_limits: true` flag set the rate-limit on `to_coldkey` (pallet) -/// during collection, then the distribution step checked `from_coldkey` -/// (pallet) and failed with `StakingOperationRateLimitExceeded`. -#[test] -fn execute_batched_orders_mixed_batch_does_not_rate_limit_pallet_intermediary() { - new_test_ext().execute_with(|| { - // Alice buys 1_000 TAO; Bob sells 500 alpha. - // Buy-dominant: residual 500 TAO goes to pool, pool returns 400 alpha. - // Total alpha = 400 (pool) + 500 (Bob passthrough) = 900 → all to Alice. - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_buy_alpha_return(400); - MockSwap::set_tao_balance(alice(), 1_000); - MockSwap::set_alpha_balance(bob(), dave(), netuid(), 500); - - let buy = make_signed_order( - AccountKeyring::Alice, - dave(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let sell = make_signed_order( - AccountKeyring::Bob, - dave(), - netuid(), - OrderType::TakeProfit, - 500, - 0, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - - // Must succeed: collecting Bob's alpha must not rate-limit the pallet - // intermediary, so distributing alpha to Alice is not blocked. - assert_ok!(LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![buy, sell]), - )); - - // Alice received staked alpha. - assert!( - MockSwap::alpha_balance(&alice(), &dave(), netuid()) > 0, - "alice should hold staked alpha after the buy" - ); - // Alice is rate-limited after receiving stake (set_receiver_limit=true). - assert!( - MockSwap::is_rate_limited(&dave(), &alice(), netuid()), - "alice should be rate-limited after receiving stake" - ); - // Bob's hotkey on the pallet side is NOT rate-limited (set_receiver_limit=false on collect). - assert!( - !MockSwap::is_rate_limited(&dave(), &bob(), netuid()), - "bob's rate-limit should not be set by the collection step" - ); - }); -} - -/// Root changes the pallet status, extrinsics are filtered -#[test] -fn root_disables_and_extrinsics_are_filtered() { - new_test_ext().execute_with(|| { - // Disable the pallet - assert_ok!(LimitOrders::set_pallet_status(RuntimeOrigin::root(), false)); - - let sell = make_signed_order( - AccountKeyring::Bob, - dave(), - netuid(), - OrderType::TakeProfit, - 500, - 0, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - - // Must succeed: collecting Bob's alpha must not rate-limit the pallet - // intermediary, so distributing alpha to Alice is not blocked. - assert_noop!( - LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![sell]) - ), - Error::::LimitOrdersDisabled - ); - }); -} - -// ───────────────────────────────────────────────────────────────────────────── -// max_slippage — execute_orders passes effective_swap_limit to pool -// ───────────────────────────────────────────────────────────────────────────── - -/// Build a signed order with a specific `max_slippage` value. -#[allow(clippy::too_many_arguments)] -fn make_signed_order_with_slippage( - keyring: AccountKeyring, - hotkey: AccountId, - netuid: subtensor_runtime_common::NetUid, - order_type: OrderType, - amount: u64, - limit_price: u64, - expiry: u64, - fee_rate: sp_runtime::Perbill, - fee_recipient: AccountId, - max_slippage: Option, -) -> crate::SignedOrder { - let order = crate::VersionedOrder::V1(crate::Order { - signer: keyring.to_account_id(), - hotkey, - netuid, - order_type, - amount, - limit_price, - expiry, - fee_rate, - fee_recipient, - relayer: None, - max_slippage, - chain_id: 945, - partial_fills_enabled: false, - }); - let sig = keyring.pair().sign(&order.encode()); - crate::SignedOrder { - order, - signature: sp_runtime::MultiSignature::Sr25519(sig), - partial_fill: None, - } -} - -#[test] -fn execute_orders_buy_no_slippage_passes_u64_max_to_pool() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - - let signed = make_signed_order_with_slippage( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, // no slippage → u64::MAX ceiling - ); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed]), - false, - )); - - // Pool must have been called with u64::MAX as price ceiling. - assert_eq!(MockSwap::buy_alpha_limit_prices(), vec![u64::MAX]); - }); -} - -#[test] -fn execute_orders_sell_no_slippage_passes_zero_to_pool() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(2.0); - - let signed = make_signed_order_with_slippage( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::TakeProfit, - 500, - 1_000_000_000, // 1.0 in ×10⁹ scale; price=2.0 (scaled=2_000_000_000) >= 1_000_000_000 ✓ - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, // no slippage → 0 floor - ); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed]), - false, - )); - - assert_eq!(MockSwap::sell_alpha_limit_prices(), vec![0]); - }); -} - -#[test] -fn execute_orders_buy_one_percent_slippage_passes_ceiling_to_pool() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - - // limit_price=1_000_000_000 (1.0 in ×10⁹), 1% slippage → ceiling = 1_010_000_000. - let signed = make_signed_order_with_slippage( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - 1_000_000_000, // 1.0 in ×10⁹ scale; price=1.0 (scaled=1_000_000_000) <= 1_000_000_000 ✓ - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - Some(Perbill::from_percent(1)), - ); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed]), - false, - )); - - assert_eq!(MockSwap::buy_alpha_limit_prices(), vec![1_010_000_000]); - }); -} - -#[test] -fn execute_orders_sell_one_percent_slippage_passes_floor_to_pool() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - // Price must be >= limit_price for TakeProfit to trigger. - MockSwap::set_price(2_000.0); - - // limit_price=1_000_000_000 (1.0 in ×10⁹), 1% slippage → floor = 990_000_000. - let signed = make_signed_order_with_slippage( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::TakeProfit, - 500, - 1_000_000_000, // 1.0 in ×10⁹ scale; price=2000.0 (scaled=2T) >= 1_000_000_000 ✓ - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - Some(Perbill::from_percent(1)), - ); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed]), - false, - )); - - assert_eq!(MockSwap::sell_alpha_limit_prices(), vec![990_000_000]); - }); -} - -// ───────────────────────────────────────────────────────────────────────────── -// max_slippage — execute_batched_orders aggregates tightest constraint -// ───────────────────────────────────────────────────────────────────────────── - -#[test] -fn execute_batched_orders_buy_dominant_uses_min_ceiling() { - new_test_ext().execute_with(|| { - // 3 buy orders with different slippage constraints. - // Alice: limit=1_000_000_000, 2% → ceiling=1_020_000_000 - // Bob: limit=1_000_000_000, 1% → ceiling=1_010_000_000 ← tightest - // Charlie (as signer, not relayer): limit=1_000_000_000, 3% → ceiling=1_030_000_000 - // Expected pool price_limit = min(1_020_000_000, 1_010_000_000, 1_030_000_000) = 1_010_000_000. - // price=1.0, scaled=1_000_000_000 <= 1_000_000_000 ✓ for all LimitBuy orders. - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_buy_alpha_return(500); - MockSwap::set_tao_balance(alice(), 600); - MockSwap::set_tao_balance(bob(), 200); - MockSwap::set_tao_balance(dave(), 200); - - let alice_order = make_signed_order_with_slippage( - AccountKeyring::Alice, - dave(), - netuid(), - OrderType::LimitBuy, - 600, - 1_000_000_000, // 1.0 in ×10⁹ scale - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - Some(Perbill::from_percent(2)), // ceiling = 1_020_000_000 - ); - let bob_order = make_signed_order_with_slippage( - AccountKeyring::Bob, - dave(), - netuid(), - OrderType::LimitBuy, - 200, - 1_000_000_000, // 1.0 in ×10⁹ scale - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - Some(Perbill::from_percent(1)), // ceiling = 1_010_000_000 ← tightest - ); - let dave_order = make_signed_order_with_slippage( - AccountKeyring::Dave, - dave(), - netuid(), - OrderType::LimitBuy, - 200, - 1_000_000_000, // 1.0 in ×10⁹ scale - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - Some(Perbill::from_percent(3)), // ceiling = 1_030_000_000 - ); - - assert_ok!(LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![alice_order, bob_order, dave_order]), - )); - - // Net pool swap must have been called with the tightest ceiling = 1_010_000_000. - assert_eq!(MockSwap::buy_alpha_limit_prices(), vec![1_010_000_000]); - }); -} - -#[test] -fn execute_batched_orders_sell_dominant_uses_max_floor() { - new_test_ext().execute_with(|| { - // 3 sell orders with different slippage constraints. - // Alice: limit=1_000_000_000, 3% → floor=970_000_000 - // Bob: limit=1_000_000_000, 1% → floor=990_000_000 ← tightest (highest floor) - // Dave: limit=1_000_000_000, 2% → floor=980_000_000 - // Expected pool price_limit = max(970_000_000, 990_000_000, 980_000_000) = 990_000_000. - // Price must be >= limit_price=1_000_000_000 (1.0 in ×10⁹) for TakeProfit to trigger. - // price=2000.0, scaled=2_000_000_000_000 >= 1_000_000_000 ✓. - MockTime::set(1_000_000); - MockSwap::set_price(2_000.0); - MockSwap::set_sell_tao_return(500); - MockSwap::set_alpha_balance(alice(), dave(), netuid(), 600); - MockSwap::set_alpha_balance(bob(), dave(), netuid(), 200); - MockSwap::set_alpha_balance(dave(), dave(), netuid(), 200); - - let alice_order = make_signed_order_with_slippage( - AccountKeyring::Alice, - dave(), - netuid(), - OrderType::TakeProfit, - 600, - 1_000_000_000, // 1.0 in ×10⁹ scale - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - Some(Perbill::from_percent(3)), // floor = 970_000_000 - ); - let bob_order = make_signed_order_with_slippage( - AccountKeyring::Bob, - dave(), - netuid(), - OrderType::TakeProfit, - 200, - 1_000_000_000, // 1.0 in ×10⁹ scale - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - Some(Perbill::from_percent(1)), // floor = 990_000_000 ← tightest - ); - let dave_order = make_signed_order_with_slippage( - AccountKeyring::Dave, - dave(), - netuid(), - OrderType::TakeProfit, - 200, - 1_000_000_000, // 1.0 in ×10⁹ scale - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - Some(Perbill::from_percent(2)), // floor = 980_000_000 - ); - - assert_ok!(LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![alice_order, bob_order, dave_order]), - )); - - // Net pool swap must have been called with the tightest floor = 990_000_000. - assert_eq!(MockSwap::sell_alpha_limit_prices(), vec![990_000_000]); - }); -} - -#[test] -fn execute_batched_orders_no_slippage_uses_unconstrained_limits() { - new_test_ext().execute_with(|| { - // Orders without max_slippage should pass u64::MAX (buy) or 0 (sell). - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_buy_alpha_return(500); - MockSwap::set_tao_balance(alice(), 1_000); - - let order = make_signed_order_with_slippage( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - - assert_ok!(LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![order]), - )); - - assert_eq!(MockSwap::buy_alpha_limit_prices(), vec![u64::MAX]); - }); -} - -// ───────────────────────────────────────────────────────────────────────────── -// max_slippage — mixed order type coexistence -// ───────────────────────────────────────────────────────────────────────────── - -/// Sell-dominant batch: TakeProfit orders (with slippage) + StopLoss (no slippage). -/// -/// TakeProfit orders set meaningful floors; StopLoss contributes 0 (no constraint). -/// pool_price_limit = max(take_floors..., 0s) = max(take_floors). -/// All three orders are fulfilled. -#[test] -fn execute_batched_orders_takeprofit_and_stoploss_coexist_sell_dominant() { - new_test_ext().execute_with(|| { - // Price = 2000 — scaled = 2_000_000_000_000. - // TakeProfit triggers when scaled_price >= limit_price (2T >= 1_000_000_000 ✓). - // StopLoss triggers when scaled_price <= limit_price (2T <= 5_000_000_000_000 ✓). - MockTime::set(1_000_000); - MockSwap::set_price(2_000.0); - MockSwap::set_sell_tao_return(500); - - // Alice TakeProfit: limit=1_000_000_000 (1.0), 3% → floor=970_000_000. - // Bob TakeProfit: limit=1_000_000_000 (1.0), 1% → floor=990_000_000. ← tightest - // Dave StopLoss: limit=5_000_000_000_000 (5000.0), None → floor=0. - MockSwap::set_alpha_balance(alice(), dave(), netuid(), 600); - MockSwap::set_alpha_balance(bob(), dave(), netuid(), 200); - MockSwap::set_alpha_balance(dave(), alice(), netuid(), 200); - - let alice_order = make_signed_order_with_slippage( - AccountKeyring::Alice, - dave(), - netuid(), - OrderType::TakeProfit, - 600, - 1_000_000_000, // 1.0 in ×10⁹ scale - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - Some(Perbill::from_percent(3)), - ); - let bob_order = make_signed_order_with_slippage( - AccountKeyring::Bob, - dave(), - netuid(), - OrderType::TakeProfit, - 200, - 1_000_000_000, // 1.0 in ×10⁹ scale - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - Some(Perbill::from_percent(1)), - ); - let dave_stoploss = make_signed_order_with_slippage( - AccountKeyring::Dave, - alice(), - netuid(), - OrderType::StopLoss, - 200, - 5_000_000_000_000, // 5000.0 in ×10⁹ scale; scaled_price 2T <= 5T ✓ - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, // StopLoss: no slippage → floor=0, does not constrain pool - ); - - let alice_id = order_id(&alice_order.order); - let bob_id = order_id(&bob_order.order); - let dave_id = order_id(&dave_stoploss.order); - - assert_ok!(LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![alice_order, bob_order, dave_stoploss]), - )); - - // All three fulfilled. - assert_eq!(Orders::::get(alice_id), Some(OrderStatus::Fulfilled)); - assert_eq!(Orders::::get(bob_id), Some(OrderStatus::Fulfilled)); - assert_eq!(Orders::::get(dave_id), Some(OrderStatus::Fulfilled)); - - // Pool called once with the tightest TakeProfit floor (990_000_000), not 0 from StopLoss. - assert_eq!(MockSwap::sell_alpha_limit_prices(), vec![990_000_000]); - }); -} - -/// Buy-dominant batch: LimitBuy orders (with slippage) dominant + StopLoss (no slippage) on offset side. -/// -/// The offset StopLoss is settled internally at spot price; it does not contribute -/// to the pool's price ceiling (which comes only from the dominant buy side). -/// pool_price_limit = min(buy_ceilings) = 1_010_000_000. -#[test] -fn execute_batched_orders_limitbuy_and_stoploss_offset_coexist_buy_dominant() { - new_test_ext().execute_with(|| { - // Price = 1.0, scaled = 1_000_000_000. - // LimitBuy triggers (scaled <= limit ✓). StopLoss triggers (scaled <= limit ✓). - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_buy_alpha_return(900); - - // Alice LimitBuy: limit=1_000_000_000 (1.0), 2% → ceiling=1_020_000_000. - // Bob LimitBuy: limit=1_000_000_000 (1.0), 1% → ceiling=1_010_000_000. ← tightest - // Dave StopLoss: limit=2_000_000_000 (2.0), None → floor=0 (offset side, not used for pool limit). - MockSwap::set_tao_balance(alice(), 600); - MockSwap::set_tao_balance(bob(), 400); - MockSwap::set_alpha_balance(dave(), alice(), netuid(), 100); - - let alice_order = make_signed_order_with_slippage( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 600, - 1_000_000_000, // 1.0 in ×10⁹ scale; scaled=1_000_000_000 <= 1_000_000_000 ✓ - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - Some(Perbill::from_percent(2)), - ); - let bob_order = make_signed_order_with_slippage( - AccountKeyring::Bob, - bob(), - netuid(), - OrderType::LimitBuy, - 400, - 1_000_000_000, // 1.0 in ×10⁹ scale; scaled=1_000_000_000 <= 1_000_000_000 ✓ - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - Some(Perbill::from_percent(1)), - ); - let dave_stoploss = make_signed_order_with_slippage( - AccountKeyring::Dave, - alice(), - netuid(), - OrderType::StopLoss, - 100, - 2_000_000_000, // 2.0 in ×10⁹ scale; scaled=1_000_000_000 <= 2_000_000_000 ✓ - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, // StopLoss: no slippage; settled at spot, never constrains pool ceiling - ); - - let alice_id = order_id(&alice_order.order); - let bob_id = order_id(&bob_order.order); - let dave_id = order_id(&dave_stoploss.order); - - assert_ok!(LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![alice_order, bob_order, dave_stoploss]), - )); - - // All three fulfilled. - assert_eq!(Orders::::get(alice_id), Some(OrderStatus::Fulfilled)); - assert_eq!(Orders::::get(bob_id), Some(OrderStatus::Fulfilled)); - assert_eq!(Orders::::get(dave_id), Some(OrderStatus::Fulfilled)); - - // Pool buy called with min(1_020_000_000, 1_010_000_000) = 1_010_000_000. StopLoss's floor (0) is ignored on buy side. - assert_eq!(MockSwap::buy_alpha_limit_prices(), vec![1_010_000_000]); - }); -} - -/// StopLoss with a narrow slippage sets an effective floor above the current market price, -/// making the pool swap impossible and failing the entire batch. -/// -/// This demonstrates Issue 1 from the design: relayers should not apply max_slippage to -/// StopLoss orders. StopLoss triggers when price has already fallen; a floor derived from -/// the (higher) trigger threshold will almost always exceed the actual market price. -#[test] -fn execute_batched_orders_stoploss_narrow_slippage_breaks_batch() { - new_test_ext().execute_with(|| { - // StopLoss: limit=100_000_000_000 (100.0 in ×10⁹), triggers at price=50 (scaled=50_000_000_000 ≤ 100_000_000_000 ✓). - // 1% slippage → floor=99_000_000_000. Market is at 50 → pool cannot deliver ≥99_000_000_000. - MockTime::set(1_000_000); - MockSwap::set_price(50.0); - MockSwap::set_sell_tao_return(100); // non-zero so SwapReturnedZero is not the cause - MockSwap::set_enforce_price_limit(true); - MockSwap::set_alpha_balance(dave(), alice(), netuid(), 200); - - let stoploss = make_signed_order_with_slippage( - AccountKeyring::Dave, - alice(), - netuid(), - OrderType::StopLoss, - 200, - 100_000_000_000, // 100.0 in ×10⁹ scale; scaled=50_000_000_000 <= 100_000_000_000 ✓ - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - Some(Perbill::from_percent(1)), // floor=99_000_000_000, but market=50 → pool rejects - ); - - assert_noop!( - LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![stoploss]), - ), - DispatchError::Other("price limit exceeded") - ); - }); -} - -/// Same StopLoss scenario through execute_orders (best-effort): the order is silently -/// skipped rather than failing the whole call. -/// -/// Note: `DispatchError::Other` has `#[codec(skip)]` on its string field, so the reason -/// string is lost when stored in the event log. We verify the skip via storage absence -/// and by asserting the floor (99_000_000_000 = 100_000_000_000 - 1%) was actually passed -/// to the pool — which is what caused the rejection. The `execute_batched_orders` variant -/// below uses `assert_noop!` (checks the return value directly, no storage round-trip) and -/// can verify the string. -#[test] -fn execute_orders_stoploss_narrow_slippage_skips_order() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(50.0); - MockSwap::set_sell_tao_return(100); - MockSwap::set_enforce_price_limit(true); - - let stoploss = make_signed_order_with_slippage( - AccountKeyring::Dave, - alice(), - netuid(), - OrderType::StopLoss, - 200, - 100_000_000_000, // 100.0 in ×10⁹ scale; scaled=50_000_000_000 <= 100_000_000_000 ✓ - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - Some(Perbill::from_percent(1)), // floor=99_000_000_000, but market=50 → pool rejects - ); - let id = order_id(&stoploss.order); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![stoploss]), - false, - )); - - // Order not stored — pool rejected the floor. - assert!(Orders::::get(id).is_none()); - - // An OrderSkipped event must have been emitted for this order. - assert!( - System::events().iter().any(|r| matches!( - &r.event, - RuntimeEvent::LimitOrders(Event::OrderSkipped { order_id, .. }) - if *order_id == id - )), - "expected OrderSkipped event for this order" - ); - - // The sell was attempted with the correct floor (99_000_000_000 = 100_000_000_000 - 1%). - // This is the value that exceeded the market price and caused the rejection. - assert_eq!(MockSwap::sell_alpha_limit_prices(), vec![99_000_000_000]); - }); -} - -// ───────────────────────────────────────────────────────────────────────────── -// relayer enforcement -// ───────────────────────────────────────────────────────────────────────────── - -#[test] -fn execute_orders_wrong_relayer_skipped() { - new_test_ext().execute_with(|| { - // Order locks execution to charlie(); submitting as bob() must be silently skipped. - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - - let signed = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - Some(BoundedVec::truncate_from(vec![charlie()])), // only charlie may relay this order - ); - let id = order_id(&signed.order); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(bob()), // wrong relayer - bounded(vec![signed]), - false, - )); - - // Order not stored — it was skipped. - assert!(Orders::::get(id).is_none()); - assert_event(Event::OrderSkipped { - order_id: id, - reason: Error::::RelayerMissMatch.into(), - }); - }); -} - -#[test] -fn execute_orders_correct_relayer_executed() { - new_test_ext().execute_with(|| { - // Same order submitted by the designated relayer (charlie) — must succeed. - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - - let signed = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - Some(BoundedVec::truncate_from(vec![charlie()])), // charlie is the designated relayer - ); - let id = order_id(&signed.order); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), // correct relayer - bounded(vec![signed]), - false, - )); - - assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); - assert_event(Event::OrderExecuted { - order_id: id, - signer: alice(), - netuid: netuid(), - order_type: OrderType::LimitBuy, - amount_in: 1_000, - amount_out: 0, - }); - }); -} - -#[test] -fn execute_batched_orders_wrong_relayer_fails_entire_batch() { - new_test_ext().execute_with(|| { - // In execute_batched_orders a relayer mismatch is a hard failure — the - // whole call is reverted, unlike the best-effort skip in execute_orders. - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - - let signed = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - Some(BoundedVec::truncate_from(vec![charlie()])), // only charlie may relay this order - ); - - assert_noop!( - LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(bob()), // wrong relayer - netuid(), - bounded(vec![signed]) - ), - Error::::RelayerMissMatch - ); - }); -} - -#[test] -fn execute_batched_orders_correct_relayer_succeeds() { - new_test_ext().execute_with(|| { - // Same order submitted by the designated relayer — must execute and - // distribute alpha to the buyer. - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_buy_alpha_return(1_000); - MockSwap::set_tao_balance(alice(), 1_000); - - let signed = make_signed_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - Some(BoundedVec::truncate_from(vec![charlie()])), // charlie is the designated relayer - ); - let id = order_id(&signed.order); - - assert_ok!(LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), // correct relayer - netuid(), - bounded(vec![signed]) - )); - - assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); - }); -} - -// ───────────────────────────────────────────────────────────────────────────── -// Partial fills — execute_orders -// ───────────────────────────────────────────────────────────────────────────── - -#[test] -fn execute_orders_partial_fill_sets_partially_filled_status() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_tao_balance(alice(), 1_000); - - // Order for 1000 TAO; relayer is charlie (required for partial fills). - let signed = make_partial_fill_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - charlie(), - 400, // fill 400 out of 1000 - ); - let id = order_id(&signed.order); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed]), - false, - )); - - assert_eq!( - Orders::::get(id), - Some(OrderStatus::PartiallyFilled(400)) - ); - }); -} - -#[test] -fn execute_orders_second_partial_fill_completes_order() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_tao_balance(alice(), 1_000); - - let signed_first = make_partial_fill_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - charlie(), - 600, - ); - let id = order_id(&signed_first.order); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed_first.clone()]), - false, - )); - assert_eq!( - Orders::::get(id), - Some(OrderStatus::PartiallyFilled(600)) - ); - - // Re-submit the same signed order payload with a different partial_fill amount. - let mut signed_second = signed_first.clone(); - signed_second.partial_fill = Some(400); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed_second]), - false, - )); - assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); - }); -} - -#[test] -fn execute_orders_partial_fill_without_relayer_skipped() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_tao_balance(alice(), 1_000); - - // Build an order with partial_fills_enabled but no relayer set. - let inner = crate::Order { - signer: alice(), - hotkey: bob(), - netuid: netuid(), - order_type: OrderType::LimitBuy, - amount: 1_000, - limit_price: u64::MAX, - expiry: FAR_FUTURE, - fee_rate: Perbill::zero(), - fee_recipient: fee_recipient(), - relayer: None, // <-- no relayer - max_slippage: None, - chain_id: 945, - partial_fills_enabled: true, - }; - let versioned = VersionedOrder::V1(inner); - let sig = AccountKeyring::Alice.pair().sign(&versioned.encode()); - let signed = crate::SignedOrder { - order: versioned, - signature: sp_runtime::MultiSignature::Sr25519(sig), - partial_fill: Some(400), - }; - let id = order_id(&signed.order); - - // The order is skipped (best-effort), not reverting the batch. - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed]), - false, - )); - - // Nothing written to storage. - assert_eq!(Orders::::get(id), None); - assert_event(Event::OrderSkipped { - order_id: id, - reason: Error::::RelayerRequiredForPartialFill.into(), - }); - }); -} - -#[test] -fn execute_orders_partial_fill_exceeding_remaining_is_skipped() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_tao_balance(alice(), 1_000); - - // Pre-fill 700 of 1000. - let signed = make_partial_fill_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - charlie(), - 700, - ); - let id = order_id(&signed.order); - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed.clone()]), - false, - )); - assert_eq!( - Orders::::get(id), - Some(OrderStatus::PartiallyFilled(700)) - ); - - // Try to fill 500 more, but only 300 remain → should be skipped. - let mut over_fill = signed.clone(); - over_fill.partial_fill = Some(500); - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![over_fill]), - false, - )); - - // Status unchanged. - assert_eq!( - Orders::::get(id), - Some(OrderStatus::PartiallyFilled(700)) - ); - assert_event(Event::OrderSkipped { - order_id: id, - reason: Error::::IncorrectPartialFillAmount.into(), - }); - }); -} - -#[test] -fn execute_orders_partial_fill_none_on_partially_filled_is_skipped() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_tao_balance(alice(), 1_000); - - // Pre-fill 700 of 1000. - let signed = make_partial_fill_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - charlie(), - 700, - ); - let id = order_id(&signed.order); - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![signed.clone()]), - false, - )); - assert_eq!( - Orders::::get(id), - Some(OrderStatus::PartiallyFilled(700)) - ); - - // Re-submit the same signed order with partial_fill = None against an - // order already PartiallyFilled. The one-shot full-execution path must - // not fire here: it would re-swap the full order.amount (over-debiting - // the signer) and mark the order Fulfilled, discarding the 700 already - // filled. The fix rejects this with IncorrectPartialFillAmount → skipped. - let mut none_fill = signed.clone(); - none_fill.partial_fill = None; - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![none_fill]), - false, - )); - - // Status unchanged — NOT over-filled and NOT marked Fulfilled. - assert_eq!( - Orders::::get(id), - Some(OrderStatus::PartiallyFilled(700)) - ); - assert_event(Event::OrderSkipped { - order_id: id, - reason: Error::::IncorrectPartialFillAmount.into(), - }); - }); -} - -// ───────────────────────────────────────────────────────────────────────────── -// Partial fills — execute_batched_orders -// ───────────────────────────────────────────────────────────────────────────── - -#[test] -fn execute_batched_orders_partial_fill_sets_partially_filled_status() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_buy_alpha_return(400); - MockSwap::set_tao_balance(alice(), 1_000); - - let signed = make_partial_fill_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - charlie(), - 400, - ); - let id = order_id(&signed.order); - - assert_ok!(LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![signed]), - )); - - assert_eq!( - Orders::::get(id), - Some(OrderStatus::PartiallyFilled(400)) - ); - }); -} - -#[test] -fn execute_batched_orders_second_partial_fill_completes_order() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_buy_alpha_return(600); - MockSwap::set_tao_balance(alice(), 1_000); - - let signed_first = make_partial_fill_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - charlie(), - 600, - ); - let id = order_id(&signed_first.order); - - assert_ok!(LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![signed_first.clone()]), - )); - assert_eq!( - Orders::::get(id), - Some(OrderStatus::PartiallyFilled(600)) - ); - - let mut signed_second = signed_first.clone(); - signed_second.partial_fill = Some(400); - - assert_ok!(LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![signed_second]), - )); - assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); - }); -} - -// ───────────────────────────────────────────────────────────────────────────── -// In-batch order_id deduplication — regression tests -// ───────────────────────────────────────────────────────────────────────────── - -/// Regression: the same fully-signed `LimitBuy` order appearing twice in one -/// batch must hard-fail with `DuplicateOrderInBatch` rather than debiting the -/// signer twice. Pre-fix, `validate_and_classify` validated each entry against -/// the same pre-batch `Orders::get(order_id)` snapshot with no in-batch tracking, -/// so the signer was charged N× their signed amount. -/// -/// `assert_noop!` also asserts the storage root is unchanged, proving the -/// all-or-nothing batch rolled back. (The mock's TAO/alpha ledgers are -/// thread-local RefCell maps, not substrate storage, so we do not assert on -/// them here — see `mock.rs`.) We additionally assert `Orders::get` was never -/// written. -#[test] -fn execute_batched_orders_full_fill_duplicate_rejected() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_buy_alpha_return(500); - MockSwap::set_tao_balance(alice(), 1_000); - - // Open-relay (relayer: None) fully-signed LimitBuy. - let order = make_signed_order( - AccountKeyring::Alice, - dave(), - netuid(), - OrderType::LimitBuy, - 600, - u64::MAX, - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - None, - ); - let id = order_id(&order.order); - - // The same order twice in one batch. - assert_noop!( - LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![order.clone(), order]), - ), - Error::::DuplicateOrderInBatch - ); - - // The batch rolled back: no order status was recorded. - assert!(Orders::::get(id).is_none()); - }); -} - -/// Regression: two `SignedOrder`s that share the same inner `VersionedOrder` -/// (so the same `order_id`, since `order_id` excludes `partial_fill` and the -/// signature) but carry *different* `partial_fill` values must still collide -/// and be caught by the in-batch dedup. This exercises the partial-fill path -/// (partial_fills_enabled = true, relayer set). -#[test] -fn execute_batched_orders_partial_fill_duplicate_rejected() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_buy_alpha_return(400); - MockSwap::set_tao_balance(alice(), 1_000); - - // Same inner VersionedOrder; only the envelope `partial_fill` differs. - let first = make_partial_fill_order( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, - FAR_FUTURE, - charlie(), - 600, - ); - let mut second = first.clone(); - second.partial_fill = Some(400); - - // Same inner order ⇒ same order_id ⇒ caught by the dedup set. - assert_eq!(order_id(&first.order), order_id(&second.order)); - let id = order_id(&first.order); - - assert_noop!( - LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![first, second]), - ), - Error::::DuplicateOrderInBatch - ); - - assert!(Orders::::get(id).is_none()); - }); -} - -/// Non-root origin cannot disable the pallet -#[test] -fn non_root_cannot_disable_the_pallet() { - new_test_ext().execute_with(|| { - // Try disabling the pallet with charlie - assert_noop!( - LimitOrders::set_pallet_status(RuntimeOrigin::signed(charlie()), false), - DispatchError::BadOrigin - ); - }); -} - -// ───────────────────────────────────────────────────────────────────────────── -// MOCK_SIMULATE_PARTIAL_FILL — sim-swap detects partial fill before funds move -// ───────────────────────────────────────────────────────────────────────────── - -/// `execute_batched_orders` hard-fails the whole batch when the sim-swap for a -/// `LimitBuy` order detects a partial fill (price limit would stop the AMM -/// before consuming the full input). -#[test] -fn execute_batched_orders_buy_partial_fill_fails_batch() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_simulate_partial_fill(true); - - let order = make_signed_order_with_slippage( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, // limit_price always passes for a buy - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - Some(Perbill::from_parts(1)), // slippage field set; mock ignores value - ); - - assert_noop!( - LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![order]), - ), - DispatchError::Other("slippage too high") - ); - }); -} - -/// `execute_orders` silently skips a `LimitBuy` order when the sim-swap detects -/// a partial fill: the order must not appear in storage and an `OrderSkipped` -/// event must be emitted. -#[test] -fn execute_orders_buy_partial_fill_skips_order() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_simulate_partial_fill(true); - - let order = make_signed_order_with_slippage( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::LimitBuy, - 1_000, - u64::MAX, // limit_price always passes for a buy - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - Some(Perbill::from_parts(1)), // slippage field set; mock ignores value - ); - let id = order_id(&order.order); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![order]), - false, - )); - - // Order must not be stored — it was skipped, not fulfilled. - assert!(Orders::::get(id).is_none()); - - // An OrderSkipped event must have been emitted for this order. - assert!( - System::events().iter().any(|r| matches!( - &r.event, - RuntimeEvent::LimitOrders(Event::OrderSkipped { order_id, .. }) - if *order_id == id - )), - "expected OrderSkipped event for this order" - ); - }); -} - -/// `execute_batched_orders` hard-fails the whole batch when the sim-swap for a -/// `TakeProfit` (sell) order detects a partial fill. -#[test] -fn execute_batched_orders_sell_partial_fill_fails_batch() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_simulate_partial_fill(true); - // Seed alpha so the order passes the balance check before reaching the swap. - MockSwap::set_alpha_balance(alice(), bob(), netuid(), 1_000); - - let order = make_signed_order_with_slippage( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::TakeProfit, - 1_000, - 0, // limit_price = 0 → floor always passes for a TakeProfit - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - Some(Perbill::from_parts(1)), // slippage field set; mock ignores value - ); - - assert_noop!( - LimitOrders::execute_batched_orders( - RuntimeOrigin::signed(charlie()), - netuid(), - bounded(vec![order]), - ), - DispatchError::Other("slippage too high") - ); - }); -} - -/// `execute_orders` silently skips a `TakeProfit` order when the sim-swap -/// detects a partial fill: the order must not appear in storage and an -/// `OrderSkipped` event must be emitted. -#[test] -fn execute_orders_sell_partial_fill_skips_order() { - new_test_ext().execute_with(|| { - MockTime::set(1_000_000); - MockSwap::set_price(1.0); - MockSwap::set_simulate_partial_fill(true); - // Seed alpha so the order passes the balance check before reaching the swap. - MockSwap::set_alpha_balance(alice(), bob(), netuid(), 1_000); - - let order = make_signed_order_with_slippage( - AccountKeyring::Alice, - bob(), - netuid(), - OrderType::TakeProfit, - 1_000, - 0, // limit_price = 0 → floor always passes for a TakeProfit - FAR_FUTURE, - Perbill::zero(), - fee_recipient(), - Some(Perbill::from_parts(1)), // slippage field set; mock ignores value - ); - let id = order_id(&order.order); - - assert_ok!(LimitOrders::execute_orders( - RuntimeOrigin::signed(charlie()), - bounded(vec![order]), - false, - )); - - // Order must not be stored — it was skipped, not fulfilled. - assert!(Orders::::get(id).is_none()); - - // An OrderSkipped event must have been emitted for this order. - assert!( - System::events().iter().any(|r| matches!( - &r.event, - RuntimeEvent::LimitOrders(Event::OrderSkipped { order_id, .. }) - if *order_id == id - )), - "expected OrderSkipped event for this order" - ); - }); -} diff --git a/pallets/limit-orders/src/tests/extrinsics/cancel_order.rs b/pallets/limit-orders/src/tests/extrinsics/cancel_order.rs new file mode 100644 index 0000000000..3031b1db0c --- /dev/null +++ b/pallets/limit-orders/src/tests/extrinsics/cancel_order.rs @@ -0,0 +1,146 @@ +//! Extrinsic tests: cancel order. + +use super::*; + +// ───────────────────────────────────────────────────────────────────────────── +// cancel_order +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn cancel_order_signer_can_cancel() { + new_test_ext().execute_with(|| { + let order = VersionedOrder::V1(Order { + signer: alice(), + hotkey: bob(), + netuid: netuid(), + order_type: OrderType::LimitBuy, + amount: 1_000, + limit_price: u64::MAX, + expiry: FAR_FUTURE, + fee_rate: Perbill::zero(), + fee_recipient: fee_recipient(), + relayer: None, + max_slippage: None, + chain_id: 945, + partial_fills_enabled: false, + }); + let id = order_id(&order); + + assert_ok!(LimitOrders::cancel_order( + RuntimeOrigin::signed(alice()), + order + )); + assert_eq!(Orders::::get(id), Some(OrderStatus::Cancelled)); + assert_event(Event::OrderCancelled { + order_id: id, + signer: alice(), + }); + }); +} + +#[test] +fn cancel_order_non_signer_rejected() { + new_test_ext().execute_with(|| { + let order = VersionedOrder::V1(Order { + signer: alice(), + hotkey: bob(), + netuid: netuid(), + order_type: OrderType::LimitBuy, + amount: 1_000, + limit_price: u64::MAX, + expiry: FAR_FUTURE, + fee_rate: Perbill::zero(), + fee_recipient: fee_recipient(), + relayer: None, + max_slippage: None, + chain_id: 945, + partial_fills_enabled: false, + }); + // Bob tries to cancel Alice's order. + assert_noop!( + LimitOrders::cancel_order(RuntimeOrigin::signed(bob()), order), + Error::::Unauthorized + ); + }); +} + +#[test] +fn cancel_order_already_cancelled_rejected() { + new_test_ext().execute_with(|| { + let order = VersionedOrder::V1(Order { + signer: alice(), + hotkey: bob(), + netuid: netuid(), + order_type: OrderType::LimitBuy, + amount: 1_000, + limit_price: u64::MAX, + expiry: FAR_FUTURE, + fee_rate: Perbill::zero(), + fee_recipient: fee_recipient(), + relayer: None, + max_slippage: None, + chain_id: 945, + partial_fills_enabled: false, + }); + let id = order_id(&order); + Orders::::insert(id, OrderStatus::Cancelled); + + assert_noop!( + LimitOrders::cancel_order(RuntimeOrigin::signed(alice()), order), + Error::::OrderAlreadyProcessed + ); + }); +} + +#[test] +fn cancel_order_already_fulfilled_rejected() { + new_test_ext().execute_with(|| { + let order = VersionedOrder::V1(Order { + signer: alice(), + hotkey: bob(), + netuid: netuid(), + order_type: OrderType::LimitBuy, + amount: 1_000, + limit_price: u64::MAX, + expiry: FAR_FUTURE, + fee_rate: Perbill::zero(), + fee_recipient: fee_recipient(), + relayer: None, + max_slippage: None, + chain_id: 945, + partial_fills_enabled: false, + }); + let id = order_id(&order); + Orders::::insert(id, OrderStatus::Fulfilled); + + assert_noop!( + LimitOrders::cancel_order(RuntimeOrigin::signed(alice()), order), + Error::::OrderAlreadyProcessed + ); + }); +} + +#[test] +fn cancel_order_unsigned_rejected() { + new_test_ext().execute_with(|| { + let order = VersionedOrder::V1(Order { + signer: alice(), + hotkey: bob(), + netuid: netuid(), + order_type: OrderType::LimitBuy, + amount: 1_000, + limit_price: u64::MAX, + expiry: FAR_FUTURE, + fee_rate: Perbill::zero(), + fee_recipient: fee_recipient(), + relayer: None, + max_slippage: None, + chain_id: 945, + partial_fills_enabled: false, + }); + assert_noop!( + LimitOrders::cancel_order(RuntimeOrigin::none(), order), + DispatchError::BadOrigin + ); + }); +} diff --git a/pallets/limit-orders/src/tests/extrinsics/execute_batched_orders.rs b/pallets/limit-orders/src/tests/extrinsics/execute_batched_orders.rs new file mode 100644 index 0000000000..da05bce275 --- /dev/null +++ b/pallets/limit-orders/src/tests/extrinsics/execute_batched_orders.rs @@ -0,0 +1,741 @@ +//! Extrinsic tests: execute batched orders. + +use super::*; + +// ───────────────────────────────────────────────────────────────────────────── +// execute_batched_orders +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn execute_batched_orders_unsigned_rejected() { + new_test_ext().execute_with(|| { + assert_noop!( + LimitOrders::execute_batched_orders(RuntimeOrigin::none(), netuid(), bounded(vec![])), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn execute_batched_orders_all_invalid_fails() { + new_test_ext().execute_with(|| { + // An expired order causes the whole batch to fail. + MockTime::set(2_000_001); // all expired + let expired = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + 1_000_000, + Perbill::zero(), + fee_recipient(), + None, + ); + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![expired]), + ), + Error::::OrderExpired + ); + }); +} + +#[test] +fn execute_batched_orders_fails_for_wrong_netuid() { + new_test_ext().execute_with(|| { + // An order whose netuid does not match the batch netuid must cause the batch to fail. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(100); + + let wrong_net = make_signed_order( + AccountKeyring::Alice, + bob(), + NetUid::from(99u16), // wrong netuid + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), // batch targets netuid 1 + bounded(vec![wrong_net]), + ), + Error::::OrderNetUidMismatch + ); + }); +} + +#[test] +fn execute_batched_orders_price_condition_not_met_fails_entire_batch() { + new_test_ext().execute_with(|| { + // Price condition not met is a hard-fail in execute_batched_orders — + // unlike execute_orders where it silently skips the order. + MockTime::set(1_000_000); + MockSwap::set_price(100.0); // current price = 100, scaled = 100_000_000_000 + + // LimitBuy requires scaled_price <= limit_price; with limit_price=1_000_000_000 (1.0) this fails. + let order = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + 1_000_000_000, // 1.0 in ×10⁹ scale, far below scaled price of 100_000_000_000 + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![order]) + ), + Error::::PriceConditionNotMet + ); + }); +} + +#[test] +fn execute_batched_orders_buy_only_fulfills_orders_and_distributes_alpha() { + new_test_ext().execute_with(|| { + // Setup: + // Alice buys 600 TAO, Bob buys 400 TAO (total 1000 TAO net, fee=0). + // Pool returns 500 alpha (MOCK_BUY_ALPHA_RETURN). + // No sellers → total_alpha = 500. + // Pro-rata: Alice 500*600/1000=300, Bob 500*400/1000=200. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(500); + MockSwap::set_tao_balance(alice(), 600); + MockSwap::set_tao_balance(bob(), 400); + + let alice_order = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 600, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let bob_order = make_signed_order( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::LimitBuy, + 400, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let alice_id = order_id(&alice_order.order); + let bob_id = order_id(&bob_order.order); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![alice_order, bob_order]), + )); + + // Both orders fulfilled. + assert_eq!(Orders::::get(alice_id), Some(OrderStatus::Fulfilled)); + assert_eq!(Orders::::get(bob_id), Some(OrderStatus::Fulfilled)); + + // Alpha distributed pro-rata. + assert_eq!(MockSwap::alpha_balance(&alice(), &dave(), netuid()), 300); + assert_eq!(MockSwap::alpha_balance(&bob(), &dave(), netuid()), 200); + + // Summary event. + assert_event(Event::GroupExecutionSummary { + netuid: netuid(), + net_side: OrderSide::Buy, + net_amount: 1_000, + actual_out: 500, + executed_count: 2, + }); + }); +} + +/// Regression test for the zero-share batch fund-loss bug. +/// +/// Bug (pre-fix): `collect_assets` debited every buyer's full TAO input up front, +/// then `distribute_alpha_pro_rata` floored each buyer's alpha share. When a +/// buyer's `share = floor(total_alpha * net / total_buy_net)` floored to 0, the +/// old code silently SKIPPED the alpha transfer (`if share > 0 { .. }`) yet STILL +/// marked the order `Fulfilled`. The victim therefore paid full TAO, received zero +/// alpha, and the order was permanently closed. +/// +/// Fix: `distribute_alpha_pro_rata` now `ensure!(share > 0, ZeroShareInBatch)`, +/// hard-failing the whole `execute_batched_orders` call. In production FRAME's +/// per-dispatch storage layer then rolls back `collect_assets` and the pool swap, +/// so no signer is debited and no order is stored. +/// +/// `assert_noop!` asserts both the error AND that no on-chain storage mutation +/// persisted — i.e. neither order is written, so neither is marked `Fulfilled`. +/// Against the old code this call returned `Ok` and wrote `Fulfilled`, so the +/// `assert_noop!` (storage-root-unchanged) would have FAILED. +/// +/// NOTE: we deliberately do NOT assert the victim's TAO balance was refunded. +/// `MockSwap` keeps balances in a `thread_local!` map that lives OUTSIDE the +/// substrate storage overlay, so `collect_assets`' debit is not transactional in +/// the mock and is not rolled back here. The balance refund is a property of the +/// real `frame_system` balances under the dispatch storage layer (exercised by the +/// L2/integration PoC), not something this mock can model. +#[test] +fn execute_batched_orders_zero_share_buyer_hard_fails() { + new_test_ext().execute_with(|| { + // Buy-only batch, price 1.0, pool alpha output pinned to 1000. + // big buyer net = 1_000_000 TAO + // victim buyer net = 1 TAO + // total_buy_net = 1_000_001 + // total_alpha = actual_out(1000) + total_sell_net(0) = 1000 + // victim share = floor(1000 * 1 / 1_000_001) = 0 → ZeroShareInBatch + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(1000); + // Distinct signer coldkeys; each buyer must be able to cover its own input. + MockSwap::set_tao_balance(alice(), 1_000_000); // big buyer (Alice) + MockSwap::set_tao_balance(bob(), 1); // victim (Bob) + + let big_buyer = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 1_000_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let victim = make_signed_order( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::LimitBuy, + 1, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let big_id = order_id(&big_buyer.order); + let victim_id = order_id(&victim.order); + + // The whole batch must hard-fail with ZeroShareInBatch. assert_noop! also asserts + // the storage root is unchanged, so neither order was written/marked Fulfilled — + // the core of the fix. (Old code: returned Ok and wrote Fulfilled → this fails.) + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![big_buyer, victim]), + ), + Error::::ZeroShareInBatch + ); + + // Explicit, redundant-with-assert_noop! statement of intent: no order is terminal. + assert_eq!(Orders::::get(victim_id), None); + assert_eq!(Orders::::get(big_id), None); + }); +} + +/// Guards against over-restriction: the `ZeroShareInBatch` fix must NOT reject a +/// legitimate multi-buyer batch where every buyer's floored share is at least 1. +#[test] +fn execute_batched_orders_all_nonzero_shares_still_succeeds() { + new_test_ext().execute_with(|| { + // Buy-only, price 1.0, pool alpha output = 1000, comparable buyer nets so + // neither share floors to zero: + // Alice net = 600, Bob net = 400, total_buy_net = 1000, total_alpha = 1000 + // Alice share = floor(1000 * 600 / 1000) = 600 + // Bob share = floor(1000 * 400 / 1000) = 400 + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(1000); + MockSwap::set_tao_balance(alice(), 600); + MockSwap::set_tao_balance(bob(), 400); + + let alice_order = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 600, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let bob_order = make_signed_order( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::LimitBuy, + 400, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let alice_id = order_id(&alice_order.order); + let bob_id = order_id(&bob_order.order); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![alice_order, bob_order]), + )); + + // Both orders fulfilled and both buyers received non-zero alpha. + assert_eq!(Orders::::get(alice_id), Some(OrderStatus::Fulfilled)); + assert_eq!(Orders::::get(bob_id), Some(OrderStatus::Fulfilled)); + assert_eq!(MockSwap::alpha_balance(&alice(), &dave(), netuid()), 600); + assert_eq!(MockSwap::alpha_balance(&bob(), &dave(), netuid()), 400); + assert!(MockSwap::alpha_balance(&alice(), &dave(), netuid()) > 0); + assert!(MockSwap::alpha_balance(&bob(), &dave(), netuid()) > 0); + }); +} + +/// Sell-side analogue of the zero-share regression. A seller whose `net_share` +/// floors to 0 in `distribute_tao_pro_rata` must hard-fail the whole batch with +/// `ZeroShareInBatch`. `assert_noop!` proves no on-chain storage mutation persisted +/// (neither order is written/marked Fulfilled). As in the buy-side test, the +/// seller's collected alpha is not refunded *in the mock* (MockSwap balances are +/// thread_local, outside the storage overlay); the refund is a real-balance +/// property under the dispatch storage layer, not modelled here. +#[test] +fn execute_batched_orders_zero_share_seller_hard_fails() { + new_test_ext().execute_with(|| { + // Sell-only batch, price 1.0, pool TAO output pinned to 1000. + // big seller alpha = 1_000_000 → sell_tao_equiv 1_000_000 + // victim seller alpha = 1 → sell_tao_equiv 1 + // total_sell_tao_equiv = 1_000_001 + // total_tao = actual_out(1000) + total_buy_net(0) = 1000 + // victim gross_share = floor(1000 * 1 / 1_000_001) = 0 + // net_share = 0 → ZeroShareInBatch + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_sell_tao_return(1000); + MockSwap::set_alpha_balance(alice(), dave(), netuid(), 1_000_000); // big seller + MockSwap::set_alpha_balance(bob(), dave(), netuid(), 1); // victim seller + + let big_seller = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::TakeProfit, + 1_000_000, + 0, // limit=0 → accept any price + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let victim = make_signed_order( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::TakeProfit, + 1, + 0, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let big_id = order_id(&big_seller.order); + let victim_id = order_id(&victim.order); + + // The whole batch must hard-fail with ZeroShareInBatch; assert_noop! also asserts + // the storage root is unchanged, so neither order was written/marked Fulfilled. + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![big_seller, victim]), + ), + Error::::ZeroShareInBatch + ); + + // Explicit, redundant-with-assert_noop! statement of intent: no order is terminal. + assert_eq!(Orders::::get(victim_id), None); + assert_eq!(Orders::::get(big_id), None); + }); +} + +#[test] +fn execute_batched_orders_sell_only_fulfills_orders_and_distributes_tao() { + new_test_ext().execute_with(|| { + // Setup: + // Alice sells 300 alpha, Bob sells 200 alpha (total 500 alpha, fee=0). + // Price = 2.0 → sell_tao_equiv: Alice 600, Bob 400, total 1000. + // Pool returns 800 TAO (MOCK_SELL_TAO_RETURN) for the net 500 alpha. + // No buyers → total_tao = 800 + 0 = 800. + // Pro-rata: Alice 800*600/1000=480, Bob 800*400/1000=320. + MockTime::set(1_000_000); + MockSwap::set_price(2.0); + MockSwap::set_sell_tao_return(800); + MockSwap::set_alpha_balance(alice(), dave(), netuid(), 300); + MockSwap::set_alpha_balance(bob(), dave(), netuid(), 200); + + let alice_order = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::TakeProfit, + 300, + 0, + FAR_FUTURE, // limit=0 → accept any price + Perbill::zero(), + fee_recipient(), + None, + ); + let bob_order = make_signed_order( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::TakeProfit, + 200, + 0, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let alice_id = order_id(&alice_order.order); + let bob_id = order_id(&bob_order.order); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![alice_order, bob_order]), + )); + + assert_eq!(Orders::::get(alice_id), Some(OrderStatus::Fulfilled)); + assert_eq!(Orders::::get(bob_id), Some(OrderStatus::Fulfilled)); + + // TAO distributed pro-rata. + assert_eq!(MockSwap::tao_balance(&alice()), 480); + assert_eq!(MockSwap::tao_balance(&bob()), 320); + + assert_event(Event::GroupExecutionSummary { + netuid: netuid(), + net_side: OrderSide::Sell, + net_amount: 500, + actual_out: 800, + executed_count: 2, + }); + }); +} + +#[test] +fn execute_batched_orders_buy_dominant_mixed() { + new_test_ext().execute_with(|| { + // Setup (fee=0, price=2.0 TAO/alpha): + // Buyers: Alice 1000 TAO, Bob 600 TAO → total_buy_net = 1600. + // Sellers: Charlie 200 alpha → sell_tao_equiv = 400 TAO. + // Net (buy-dominant): 1600 - 400 = 1200 TAO goes to pool. + // Pool returns 300 alpha (MOCK_BUY_ALPHA_RETURN). + // total_alpha for buyers = 300 (pool) + 200 (seller passthrough) = 500. + // Pro-rata buyers (by buy_net TAO): + // Alice: 500 * 1000/1600 = 312 alpha + // Bob: 500 * 600/1600 = 187 alpha + // (dust = 1 alpha stays in pallet) + // Sellers (buy-dominant branch): total_tao = total_sell_tao_equiv = 400. + // Charlie: 400 * 400/400 = 400 TAO. + MockTime::set(1_000_000); + MockSwap::set_price(2.0); + MockSwap::set_buy_alpha_return(300); + MockSwap::set_tao_balance(alice(), 1_000); + MockSwap::set_tao_balance(bob(), 600); + MockSwap::set_alpha_balance(charlie(), dave(), netuid(), 200); + + let alice_buy = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let bob_buy = make_signed_order( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::LimitBuy, + 600, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let charlie_sell = make_signed_order( + AccountKeyring::Charlie, + dave(), + netuid(), + OrderType::TakeProfit, + 200, + 0, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(dave()), + netuid(), + bounded(vec![alice_buy, bob_buy, charlie_sell]), + )); + + assert_eq!(MockSwap::alpha_balance(&alice(), &dave(), netuid()), 312); + assert_eq!(MockSwap::alpha_balance(&bob(), &dave(), netuid()), 187); + assert_eq!(MockSwap::tao_balance(&charlie()), 400); + + assert_event(Event::GroupExecutionSummary { + netuid: netuid(), + net_side: OrderSide::Buy, + net_amount: 1_200, + actual_out: 300, + executed_count: 3, + }); + }); +} + +#[test] +fn execute_batched_orders_sell_dominant_mixed() { + new_test_ext().execute_with(|| { + // Setup (fee=0, price=2.0 TAO/alpha): + // Buyers: Alice 200 TAO → total_buy_net = 200. + // Sellers: Bob 300 alpha, Charlie 200 alpha → total_sell_net = 500. + // sell_tao_equiv: Bob 600, Charlie 400, total 1000. + // Net (sell-dominant): buy_alpha_equiv = 200/2 = 100 alpha; + // residual sell alpha = 500 - 100 = 400 alpha → pool returns 300 TAO. + // total_tao for sellers = 300 (pool) + 200 (buy passthrough) = 500 TAO. + // Pro-rata sellers (by sell_tao_equiv): + // Bob: 500 * 600/1000 = 300 TAO + // Charlie: 500 * 400/1000 = 200 TAO + // total_alpha for buyers = buy_net / price = 200/2 = 100 alpha. + // Alice: 100 * 200/200 = 100 alpha. + MockTime::set(1_000_000); + MockSwap::set_price(2.0); + MockSwap::set_sell_tao_return(300); + MockSwap::set_tao_balance(alice(), 200); + MockSwap::set_alpha_balance(bob(), dave(), netuid(), 300); + MockSwap::set_alpha_balance(charlie(), dave(), netuid(), 200); + + let alice_buy = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 200, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let bob_sell = make_signed_order( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::TakeProfit, + 300, + 0, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let charlie_sell = make_signed_order( + AccountKeyring::Charlie, + dave(), + netuid(), + OrderType::TakeProfit, + 200, + 0, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(dave()), + netuid(), + bounded(vec![alice_buy, bob_sell, charlie_sell]), + )); + + assert_eq!(MockSwap::alpha_balance(&alice(), &dave(), netuid()), 100); + assert_eq!(MockSwap::tao_balance(&bob()), 300); + assert_eq!(MockSwap::tao_balance(&charlie()), 200); + + assert_event(Event::GroupExecutionSummary { + netuid: netuid(), + net_side: OrderSide::Sell, + net_amount: 400, + actual_out: 300, + executed_count: 3, + }); + }); +} + +#[test] +fn execute_batched_orders_fee_forwarded_to_collector() { + new_test_ext().execute_with(|| { + // fee = 1% (10_000_000 ppb). + // Alice buys 1000 TAO: fee = 10, net = 990. + // Pool returns 500 alpha for 990 TAO. + // collect_fees transfers 10 TAO (buy fee) to fee_recipient. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(500); + + let alice_buy = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + fee_recipient(), + None, + ); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![alice_buy]), + )); + + // Fee recipient received the buy-side fee. + assert_eq!(MockSwap::tao_balance(&fee_recipient()), 10); + }); +} + +#[test] +fn execute_batched_orders_fails_for_cancelled_order() { + new_test_ext().execute_with(|| { + // A cancelled order is already processed; including it in the batch must cause a hard failure. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(100); + + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + Orders::::insert(id, OrderStatus::Cancelled); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![signed]), + ), + Error::::OrderCancelled + ); + + // Still cancelled, not changed to Fulfilled. + assert_eq!(Orders::::get(id), Some(OrderStatus::Cancelled)); + }); +} + +#[test] +fn execute_batched_orders_fees_charged_on_both_sides_when_matched_internally() { + new_test_ext().execute_with(|| { + // fee = 1% (10_000_000 ppb), price = 1.0 TAO/alpha. + // + // Alice buys 1_000 TAO → buy fee = 10 TAO, net = 990 TAO. + // Bob sells 1_000 alpha → sell_tao_equiv = 1_000 TAO. + // + // sell-dominant: residual = 1_000 - 990 = 10 alpha sent to pool. + // Pool returns 9 TAO (mocked) for that residual. + // total_tao for sellers = 9 (pool) + 990 (buy passthrough) = 999. + // Bob gross_share = 999 * 1_000/1_000 = 999. + // Sell fee = mul_floor(1%, 999) = floor(9.99) = 9; Bob nets 990 TAO. + // fee_recipient total = buy_fee(10) + sell_fee(9) = 19 TAO. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_sell_tao_return(9); + MockSwap::set_tao_balance(alice(), 1_000); + MockSwap::set_alpha_balance(bob(), dave(), netuid(), 1_000); + + let alice_buy = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + fee_recipient(), + None, + ); + let bob_sell = make_signed_order( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::TakeProfit, + 1_000, + 0, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + fee_recipient(), + None, + ); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![alice_buy, bob_sell]), + )); + + // Both sides charged: fee_recipient gets buy fee (10) + sell fee (9) = 19. + assert_eq!(MockSwap::tao_balance(&fee_recipient()), 19); + // Bob receives 990 TAO after sell-side fee (999 gross - 9 fee). + assert_eq!(MockSwap::tao_balance(&bob()), 990); + }); +} diff --git a/pallets/limit-orders/src/tests/extrinsics/execute_batched_orders_fee_routing.rs b/pallets/limit-orders/src/tests/extrinsics/execute_batched_orders_fee_routing.rs new file mode 100644 index 0000000000..bfafe0b45b --- /dev/null +++ b/pallets/limit-orders/src/tests/extrinsics/execute_batched_orders_fee_routing.rs @@ -0,0 +1,305 @@ +//! Extrinsic tests: execute batched orders fee routing. + +use super::*; + +// ───────────────────────────────────────────────────────────────────────────── +// fee routing – multiple recipients +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn execute_batched_orders_fees_routed_to_different_recipients() { + new_test_ext().execute_with(|| { + // Alice and Bob both buy; Alice's fee goes to charlie(), Bob's to dave(). + // fee = 1% for both orders. + // Alice buys 1_000 TAO: fee = 10 → charlie(). + // Bob buys 1_000 TAO: fee = 10 → dave(). + // Pool returns 900 alpha total for 1_980 TAO net. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(900); + MockSwap::set_tao_balance(alice(), 1_000); + MockSwap::set_tao_balance(bob(), 1_000); + + let alice_buy = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + charlie(), + None, + ); + let bob_buy = make_signed_order( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + dave(), + None, + ); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![alice_buy, bob_buy]), + )); + + // Each recipient gets exactly their order's fee. + assert_eq!( + MockSwap::tao_balance(&charlie()), + 10, + "charlie gets Alice's fee" + ); + assert_eq!(MockSwap::tao_balance(&dave()), 10, "dave gets Bob's fee"); + }); +} + +#[test] +fn execute_batched_orders_fees_batched_for_shared_recipient() { + new_test_ext().execute_with(|| { + // Both Alice and Bob's fees go to the same recipient (charlie()). + // Expect a single combined transfer of 20 TAO to charlie(). + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(900); + MockSwap::set_tao_balance(alice(), 1_000); + MockSwap::set_tao_balance(bob(), 1_000); + + let alice_buy = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + charlie(), + None, + ); + let bob_buy = make_signed_order( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + charlie(), + None, + ); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![alice_buy, bob_buy]), + )); + + // One combined transfer: charlie() receives 10 + 10 = 20 TAO. + let fee_transfers: Vec<_> = MockSwap::tao_transfers() + .into_iter() + .filter(|(_, to, _)| to == &charlie()) + .collect(); + assert_eq!( + fee_transfers.len(), + 1, + "single transfer to shared recipient" + ); + assert_eq!(fee_transfers[0].2, 20, "combined fee = 20 TAO"); + }); +} + +/// 4 orders split across 2 fee recipients. +/// +/// Orders: +/// Alice LimitBuy 1_000 TAO fee_recipient = ferdie (buy-fee collector) +/// Bob LimitBuy 1_000 TAO fee_recipient = ferdie (buy-fee collector) +/// Charlie TakeProfit 1_000 α fee_recipient = fee_recipient() (sell-fee collector) +/// Eve TakeProfit 1_000 α fee_recipient = fee_recipient() (sell-fee collector) +/// +/// Neither ferdie nor fee_recipient() are order signers, so every TAO transfer +/// to those accounts is exclusively a fee transfer — making the single-transfer +/// assertion unambiguous. +/// +/// At price 1.0 (1 TAO = 1 α), fee = 1%: +/// net buy TAO = (1_000 - 10) + (1_000 - 10) = 1_980 +/// sell α equiv = 2_000 TAO → sell-dominant, residual = 20 α → pool +/// pool returns 18 TAO for residual +/// total TAO for sellers = 18 + 1_980 = 1_998 +/// each seller gross_share = 1_998 * 1_000 / 2_000 = 999 +/// sell fee = mul_floor(1%, 999) = floor(9.99) = 9 TAO each +/// +/// Expected: +/// ferdie receives 10 (Alice) + 10 (Bob) = 20 TAO (1 transfer) +/// fee_recipient() receives 9 (Charlie) + 9 (Eve) = 18 TAO (1 transfer) +#[test] +fn execute_batched_orders_four_orders_two_fee_recipients() { + new_test_ext().execute_with(|| { + let ferdie = AccountKeyring::Ferdie.to_account_id(); + let eve = AccountKeyring::Eve.to_account_id(); + + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_sell_tao_return(18); + MockSwap::set_tao_balance(alice(), 1_000); + MockSwap::set_tao_balance(bob(), 1_000); + MockSwap::set_alpha_balance(charlie(), dave(), netuid(), 1_000); + MockSwap::set_alpha_balance(eve.clone(), dave(), netuid(), 1_000); + + let alice_buy = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + ferdie.clone(), + None, + ); + let bob_buy = make_signed_order( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + ferdie.clone(), + None, + ); + let charlie_sell = make_signed_order( + AccountKeyring::Charlie, + dave(), + netuid(), + OrderType::TakeProfit, + 1_000, + 0, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + fee_recipient(), + None, + ); + let eve_sell = make_signed_order( + AccountKeyring::Eve, + dave(), + netuid(), + OrderType::TakeProfit, + 1_000, + 0, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + fee_recipient(), + None, + ); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(alice()), + netuid(), + bounded(vec![alice_buy, bob_buy, charlie_sell, eve_sell]), + )); + + // ferdie collects Alice's and Bob's buy fees: 10 + 10 = 20 TAO in one transfer. + let ferdie_transfers: Vec<_> = MockSwap::tao_transfers() + .into_iter() + .filter(|(_, to, _)| to == &ferdie) + .collect(); + assert_eq!(ferdie_transfers.len(), 1, "single transfer to ferdie"); + assert_eq!( + ferdie_transfers[0].2, 20, + "ferdie receives 20 TAO in buy fees" + ); + + // fee_recipient() collects Charlie's and Eve's sell fees: 10 + 10 = 20 TAO in one transfer. + let fp_transfers: Vec<_> = MockSwap::tao_transfers() + .into_iter() + .filter(|(_, to, _)| to == &fee_recipient()) + .collect(); + assert_eq!(fp_transfers.len(), 1, "single transfer to fee_recipient"); + assert_eq!( + fp_transfers[0].2, 18, + "fee_recipient receives 18 TAO in sell fees" + ); + }); +} + +/// A mixed batch (buy + sell) must not rate-limit the pallet intermediary +/// account during asset collection, which would otherwise block the +/// subsequent alpha distribution to buyers. +/// +/// Regression test: previously `transfer_staked_alpha` with a single +/// `apply_limits: true` flag set the rate-limit on `to_coldkey` (pallet) +/// during collection, then the distribution step checked `from_coldkey` +/// (pallet) and failed with `StakingOperationRateLimitExceeded`. +#[test] +fn execute_batched_orders_mixed_batch_does_not_rate_limit_pallet_intermediary() { + new_test_ext().execute_with(|| { + // Alice buys 1_000 TAO; Bob sells 500 alpha. + // Buy-dominant: residual 500 TAO goes to pool, pool returns 400 alpha. + // Total alpha = 400 (pool) + 500 (Bob passthrough) = 900 → all to Alice. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(400); + MockSwap::set_tao_balance(alice(), 1_000); + MockSwap::set_alpha_balance(bob(), dave(), netuid(), 500); + + let buy = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let sell = make_signed_order( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::TakeProfit, + 500, + 0, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + + // Must succeed: collecting Bob's alpha must not rate-limit the pallet + // intermediary, so distributing alpha to Alice is not blocked. + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![buy, sell]), + )); + + // Alice received staked alpha. + assert!( + MockSwap::alpha_balance(&alice(), &dave(), netuid()) > 0, + "alice should hold staked alpha after the buy" + ); + // Alice is rate-limited after receiving stake (set_receiver_limit=true). + assert!( + MockSwap::is_rate_limited(&dave(), &alice(), netuid()), + "alice should be rate-limited after receiving stake" + ); + // Bob's hotkey on the pallet side is NOT rate-limited (set_receiver_limit=false on collect). + assert!( + !MockSwap::is_rate_limited(&dave(), &bob(), netuid()), + "bob's rate-limit should not be set by the collection step" + ); + }); +} diff --git a/pallets/limit-orders/src/tests/extrinsics/execute_batched_orders_swap_errors.rs b/pallets/limit-orders/src/tests/extrinsics/execute_batched_orders_swap_errors.rs new file mode 100644 index 0000000000..ba00365dc8 --- /dev/null +++ b/pallets/limit-orders/src/tests/extrinsics/execute_batched_orders_swap_errors.rs @@ -0,0 +1,106 @@ +//! Extrinsic tests: execute batched orders swap errors. + +use super::*; + +// ───────────────────────────────────────────────────────────────────────────── +// net_pool_swap – SwapReturnedZero errors +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn execute_batched_orders_buy_zero_alpha_returns_error() { + new_test_ext().execute_with(|| { + // buy_alpha returns 0 alpha for a non-zero TAO input → SwapReturnedZero. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(0); // pool gives back nothing + MockSwap::set_tao_balance(alice(), 1_000); + + let order = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![order]), + ), + Error::::SwapReturnedZero + ); + }); +} + +#[test] +fn execute_batched_orders_sell_zero_tao_returns_error() { + new_test_ext().execute_with(|| { + // sell_alpha returns 0 TAO for a non-zero alpha input → SwapReturnedZero. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_sell_tao_return(0); // pool gives back nothing + MockSwap::set_alpha_balance(alice(), bob(), netuid(), 1_000); + + let order = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::TakeProfit, + 1_000, + 0, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![order]), + ), + Error::::SwapReturnedZero + ); + }); +} + +#[test] +fn execute_batched_orders_sell_alpha_respects_swap_fail() { + new_test_ext().execute_with(|| { + // sell_alpha should propagate DispatchError when MOCK_SWAP_FAIL is set. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_swap_fail(true); + MockSwap::set_alpha_balance(alice(), bob(), netuid(), 1_000); + + let order = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::TakeProfit, + 1_000, + 0, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![order]), + ), + DispatchError::Other("pool error") + ); + }); +} diff --git a/pallets/limit-orders/src/tests/extrinsics/execute_orders.rs b/pallets/limit-orders/src/tests/extrinsics/execute_orders.rs new file mode 100644 index 0000000000..46184f833b --- /dev/null +++ b/pallets/limit-orders/src/tests/extrinsics/execute_orders.rs @@ -0,0 +1,539 @@ +//! Extrinsic tests: execute orders. + +use super::*; + +// ───────────────────────────────────────────────────────────────────────────── +// execute_orders +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn execute_orders_buy_order_fulfilled() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + // Price = 1.0 ≤ limit = 2.0 → condition met. + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + 2_000_000_000, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); + assert_event(Event::OrderExecuted { + order_id: id, + signer: alice(), + netuid: netuid(), + order_type: OrderType::LimitBuy, + amount_in: 1_000, + amount_out: 0, + }); + }); +} + +#[test] +fn execute_orders_sell_order_fulfilled() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(2.0); + // Price = 2.0, scaled = 2_000_000_000 ≥ limit = 1_000_000_000 → condition met. + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::TakeProfit, + 500, + 1_000_000_000, // 1.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); + assert_event(Event::OrderExecuted { + order_id: id, + signer: alice(), + netuid: netuid(), + order_type: OrderType::TakeProfit, + amount_in: 500, + amount_out: 0, + }); + }); +} + +#[test] +fn execute_orders_stop_loss_order_fulfilled() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(0.5); + // Price = 0.5, scaled = 500_000_000 ≤ limit = 1_000_000_000 → condition met. + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::StopLoss, + 500, + 1_000_000_000, // 1.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); + assert_event(Event::OrderExecuted { + order_id: id, + signer: alice(), + netuid: netuid(), + order_type: OrderType::StopLoss, + amount_in: 500, + amount_out: 0, + }); + }); +} + +#[test] +fn execute_orders_stop_loss_price_not_met_skipped() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(2.0); // price 2.0, scaled=2_000_000_000 > limit 1_000_000_000 → stop loss condition not met + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::StopLoss, + 500, + 1_000_000_000, // 1.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + assert!(Orders::::get(id).is_none()); + assert_event(Event::OrderSkipped { + order_id: id, + reason: Error::::PriceConditionNotMet.into(), + }); + }); +} + +#[test] +fn execute_orders_expired_order_skipped() { + new_test_ext().execute_with(|| { + MockTime::set(2_000_001); // now > expiry + MockSwap::set_price(1.0); + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + 2_000_000, // expiry in the past + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + // Skipped — storage untouched. + assert!(Orders::::get(id).is_none()); + assert_event(Event::OrderSkipped { + order_id: id, + reason: Error::::OrderExpired.into(), + }); + }); +} + +#[test] +fn execute_orders_price_not_met_skipped() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(5.0); // price 5.0, scaled=5_000_000_000 > limit 2_000_000_000 → buy condition not met + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + 2_000_000_000, // 2.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + assert!(Orders::::get(id).is_none()); + assert_event(Event::OrderSkipped { + order_id: id, + reason: Error::::PriceConditionNotMet.into(), + }); + }); +} + +// Regression tests: with the ×10⁹ scale fix, sub-unity prices can be meaningfully +// expressed as limit_price values. A price of 0.5 TAO/alpha is represented as +// 500_000_000 in ×10⁹ scale, enabling fine-grained TakeProfit thresholds below 1.0. +#[test] +fn take_profit_sub_unity_price_executes_when_limit_met() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + // Market price = 0.5 TAO/alpha → scaled = 500_000_000. + MockSwap::set_price(0.5); + + // limit_price = 400_000_000 (0.4 in ×10⁹ scale). + // TakeProfit condition: scaled_price (500_000_000) >= limit_price (400_000_000) ✓ + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::TakeProfit, + 500, + 400_000_000, // 0.4 in ×10⁹ scale — below current price of 0.5 + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + // Executes: 500_000_000 >= 400_000_000 → condition met. + assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); + }); +} + +#[test] +fn take_profit_sub_unity_price_skipped_when_limit_not_met() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + // Market price = 0.5 TAO/alpha → scaled = 500_000_000. + MockSwap::set_price(0.5); + + // limit_price = 600_000_000 (0.6 in ×10⁹ scale). + // TakeProfit condition: scaled_price (500_000_000) >= limit_price (600_000_000) → FALSE. + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::TakeProfit, + 500, + 600_000_000, // 0.6 in ×10⁹ scale — above current price of 0.5 + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + // Skipped: 500_000_000 >= 600_000_000 is false. + assert!(Orders::::get(id).is_none()); + assert_event(Event::OrderSkipped { + order_id: id, + reason: Error::::PriceConditionNotMet.into(), + }); + }); +} + +#[test] +fn execute_orders_already_processed_skipped() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + Orders::::insert(id, OrderStatus::Fulfilled); + + // Should succeed (batch-level) but skip this order silently. + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + // Still Fulfilled (not changed). + assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); + assert_event(Event::OrderSkipped { + order_id: id, + reason: Error::::OrderAlreadyProcessed.into(), + }); + }); +} + +#[test] +fn execute_orders_mixed_batch_valid_and_skipped() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + let valid = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let expired = make_signed_order( + AccountKeyring::Bob, + alice(), + netuid(), + OrderType::LimitBuy, + 500, + u64::MAX, + 500_000, // already expired + Perbill::zero(), + fee_recipient(), + None, + ); + let valid_id = order_id(&valid.order); + let expired_id = order_id(&expired.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![valid, expired]), + false, + )); + + assert_eq!(Orders::::get(valid_id), Some(OrderStatus::Fulfilled)); + assert_event(Event::OrderSkipped { + order_id: expired_id, + reason: Error::::OrderExpired.into(), + }); + }); +} + +#[test] +fn execute_orders_unsigned_rejected() { + new_test_ext().execute_with(|| { + assert_noop!( + LimitOrders::execute_orders(RuntimeOrigin::none(), bounded(vec![]), false), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn execute_orders_buy_with_fee_charges_fee() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + // fee_rate = 1% (10_000_000 parts-per-billion), recipient = fee_recipient(). + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + fee_recipient(), + None, + ); + MockSwap::set_tao_balance(alice(), 1_000); + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + // One buy_alpha call for the net amount (990 TAO after 1% fee). + let buys: Vec<_> = MockSwap::log() + .into_iter() + .filter_map(|c| { + if let SwapCall::BuyAlpha { tao, .. } = c { + Some(tao) + } else { + None + } + }) + .collect(); + assert_eq!(buys, vec![990], "main swap must use 990 TAO after 1% fee"); + + // Fee (10 TAO) forwarded directly to fee_recipient via transfer_tao. + assert_eq!(MockSwap::tao_balance(&fee_recipient()), 10); + }); +} + +#[test] +fn execute_orders_sell_with_fee_charges_fee() { + new_test_ext().execute_with(|| { + // fee = 1% (10_000_000 ppb). + // Alice sells 1_000 alpha; pool returns 800 TAO. + // fee_tao = 1% of 800 = 8 TAO, forwarded to fee_recipient via transfer_tao. + // Alice keeps 792 TAO. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_sell_tao_return(800); + MockSwap::set_alpha_balance(alice(), bob(), netuid(), 1_000); + + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::TakeProfit, + 1_000, + 0, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + fee_recipient(), + None, + ); + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + // Full 1_000 alpha sold (no alpha deducted for fee). + let sells: Vec<_> = MockSwap::log() + .into_iter() + .filter_map(|c| { + if let SwapCall::SellAlpha { alpha, .. } = c { + Some(alpha) + } else { + None + } + }) + .collect(); + assert_eq!(sells, vec![1_000], "full alpha amount must be sold"); + + // fee_recipient received 8 TAO (1% of 800). + assert_eq!(MockSwap::tao_balance(&fee_recipient()), 8); + // Alice kept the remaining 792 TAO. + assert_eq!(MockSwap::tao_balance(&alice()), 792); + }); +} + +#[test] +fn execute_orders_empty_batch_returns_ok() { + new_test_ext().execute_with(|| { + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![]), + false, + )); + }); +} + +#[test] +fn execute_orders_fee_transfer_failure_skips_order() { + new_test_ext().execute_with(|| { + // When the fee transfer fails the entire order is rolled back and emits OrderSkipped. + // This prevents users from exploiting a tight balance to execute swaps fee-free. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(500); + MockSwap::set_tao_balance(alice(), 10_000); + + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + fee_recipient(), + None, + ); + + FAIL_FEE_TRANSFER.with(|f| *f.borrow_mut() = true); + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed.clone()]), + false, + )); + FAIL_FEE_TRANSFER.with(|f| *f.borrow_mut() = false); + + // Order was skipped — not stored as Fulfilled. + let id = crate::tests::mock::order_id(&signed.order); + assert!(Orders::::get(id).is_none()); + + // OrderSkipped was emitted with the fee-transfer error as the reason. + assert_event(Event::OrderSkipped { + order_id: id, + reason: DispatchError::CannotLookup, + }); + + // fee_recipient received nothing. + assert_eq!(MockSwap::tao_balance(&fee_recipient()), 0); + }); +} diff --git a/pallets/limit-orders/src/tests/extrinsics/execute_orders_skip_invalid.rs b/pallets/limit-orders/src/tests/extrinsics/execute_orders_skip_invalid.rs new file mode 100644 index 0000000000..ab8e2f2dcf --- /dev/null +++ b/pallets/limit-orders/src/tests/extrinsics/execute_orders_skip_invalid.rs @@ -0,0 +1,263 @@ +//! Silent-skip and should_fail behaviour for `execute_orders`. + +use super::*; + +// ───────────────────────────────────────────────────────────────────────────── +// execute_orders — silent-skip behaviour +// ───────────────────────────────────────────────────────────────────────────── + +/// A single expired order is silently skipped: the call returns `Ok` and +/// nothing is written to the `Orders` storage map. +#[test] +fn execute_orders_skips_expired_order() { + new_test_ext().execute_with(|| { + MockTime::set(2_000_001); // now > expiry + MockSwap::set_price(1.0); + + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + 2_000_000, // expiry in the past + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + // Skipped — storage untouched. + assert!(Orders::::get(id).is_none()); + assert_event(Event::OrderSkipped { + order_id: id, + reason: Error::::OrderExpired.into(), + }); + }); +} + +/// A LimitBuy with `limit_price = 0` (price ceiling below current price) +/// is silently skipped: the call returns `Ok` and nothing is written to +/// the `Orders` storage map. +#[test] +fn execute_orders_skips_price_condition_not_met() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(5.0); // price 5.0 > limit 0 → buy condition not met + + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + 0, // price ceiling of 0 — never satisfied at price 5.0 + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + // Skipped — storage untouched. + assert!(Orders::::get(id).is_none()); + assert_event(Event::OrderSkipped { + order_id: id, + reason: Error::::PriceConditionNotMet.into(), + }); + }); +} + +/// A batch containing one valid order and one expired order: the call +/// returns `Ok`, the valid order is stored as `Fulfilled`, and the expired +/// order is NOT written to storage. +#[test] +fn execute_orders_valid_and_invalid_mixed() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + let valid = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let expired = make_signed_order( + AccountKeyring::Bob, + alice(), + netuid(), + OrderType::LimitBuy, + 500, + u64::MAX, + 500_000, // already expired + Perbill::zero(), + fee_recipient(), + None, + ); + let valid_id = order_id(&valid.order); + let expired_id = order_id(&expired.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![valid, expired]), + false, + )); + + // Valid order executed successfully. + assert_eq!(Orders::::get(valid_id), Some(OrderStatus::Fulfilled)); + // Expired order silently skipped — not written to storage. + assert!(Orders::::get(expired_id).is_none()); + assert_event(Event::OrderSkipped { + order_id: expired_id, + reason: Error::::OrderExpired.into(), + }); + }); +} + +/// With `should_fail = true` a single expired order is NOT silently skipped: +/// the whole call fails with `OrderExpired` and storage stays untouched. +#[test] +fn execute_orders_should_fail_expired_order_reverts() { + new_test_ext().execute_with(|| { + MockTime::set(2_000_001); // now > expiry + MockSwap::set_price(1.0); + + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + 2_000_000, // expiry in the past + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + // all-or-nothing: the failing order makes the whole call return Err + // and assert_noop! confirms storage is unchanged. + assert_noop!( + LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + true, + ), + Error::::OrderExpired + ); + + assert!(Orders::::get(id).is_none()); + }); +} + +/// With `should_fail = true` a batch containing a VALID order followed by an +/// INVALID (expired) order reverts entirely: the valid order's effects are +/// rolled back, so it is NOT recorded as `Fulfilled` and the relayer's TAO +/// is not consumed. Contrast `execute_orders_valid_and_invalid_mixed`, where +/// the same batch with `should_fail = false` keeps the valid order. +#[test] +fn execute_orders_should_fail_valid_then_invalid_reverts_whole_batch() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + let valid = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let expired = make_signed_order( + AccountKeyring::Bob, + alice(), + netuid(), + OrderType::LimitBuy, + 500, + u64::MAX, + 500_000, // already expired + Perbill::zero(), + fee_recipient(), + None, + ); + let valid_id = order_id(&valid.order); + let expired_id = order_id(&expired.order); + + // The expired order is the second in the batch; with should_fail = true + // its failure reverts the already-executed valid order too. + assert_noop!( + LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![valid, expired]), + true, + ), + Error::::OrderExpired + ); + + // Neither order survived: the valid order's Fulfilled status was rolled back. + assert!(Orders::::get(valid_id).is_none()); + assert!(Orders::::get(expired_id).is_none()); + }); +} + +/// With `should_fail = true` a price-condition-not-met order hard-fails the +/// whole call with `PriceConditionNotMet`, mirroring `execute_batched_orders` +/// rather than the best-effort skip path. +#[test] +fn execute_orders_should_fail_price_condition_not_met_reverts() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(5.0); // price 5.0 > limit 0 → buy condition not met + + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + 0, // price ceiling of 0 — never satisfied at price 5.0 + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + assert_noop!( + LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + true, + ), + Error::::PriceConditionNotMet + ); + + assert!(Orders::::get(id).is_none()); + }); +} diff --git a/pallets/limit-orders/src/tests/extrinsics/max_slippage.rs b/pallets/limit-orders/src/tests/extrinsics/max_slippage.rs new file mode 100644 index 0000000000..76592e66dd --- /dev/null +++ b/pallets/limit-orders/src/tests/extrinsics/max_slippage.rs @@ -0,0 +1,550 @@ +//! Extrinsic tests: max slippage. + +use super::*; + +// ───────────────────────────────────────────────────────────────────────────── +// max_slippage — execute_orders passes effective_swap_limit to pool +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn execute_orders_buy_no_slippage_passes_u64_max_to_pool() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + let signed = make_signed_order_with_slippage( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, // no slippage → u64::MAX ceiling + ); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + // Pool must have been called with u64::MAX as price ceiling. + assert_eq!(MockSwap::buy_alpha_limit_prices(), vec![u64::MAX]); + }); +} + +#[test] +fn execute_orders_sell_no_slippage_passes_zero_to_pool() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(2.0); + + let signed = make_signed_order_with_slippage( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::TakeProfit, + 500, + 1_000_000_000, // 1.0 in ×10⁹ scale; price=2.0 (scaled=2_000_000_000) >= 1_000_000_000 ✓ + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, // no slippage → 0 floor + ); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + assert_eq!(MockSwap::sell_alpha_limit_prices(), vec![0]); + }); +} + +#[test] +fn execute_orders_buy_one_percent_slippage_passes_ceiling_to_pool() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + // limit_price=1_000_000_000 (1.0 in ×10⁹), 1% slippage → ceiling = 1_010_000_000. + let signed = make_signed_order_with_slippage( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + 1_000_000_000, // 1.0 in ×10⁹ scale; price=1.0 (scaled=1_000_000_000) <= 1_000_000_000 ✓ + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(1)), + ); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + assert_eq!(MockSwap::buy_alpha_limit_prices(), vec![1_010_000_000]); + }); +} + +#[test] +fn execute_orders_sell_one_percent_slippage_passes_floor_to_pool() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + // Price must be >= limit_price for TakeProfit to trigger. + MockSwap::set_price(2_000.0); + + // limit_price=1_000_000_000 (1.0 in ×10⁹), 1% slippage → floor = 990_000_000. + let signed = make_signed_order_with_slippage( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::TakeProfit, + 500, + 1_000_000_000, // 1.0 in ×10⁹ scale; price=2000.0 (scaled=2T) >= 1_000_000_000 ✓ + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(1)), + ); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + assert_eq!(MockSwap::sell_alpha_limit_prices(), vec![990_000_000]); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// max_slippage — execute_batched_orders aggregates tightest constraint +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn execute_batched_orders_buy_dominant_uses_min_ceiling() { + new_test_ext().execute_with(|| { + // 3 buy orders with different slippage constraints. + // Alice: limit=1_000_000_000, 2% → ceiling=1_020_000_000 + // Bob: limit=1_000_000_000, 1% → ceiling=1_010_000_000 ← tightest + // Charlie (as signer, not relayer): limit=1_000_000_000, 3% → ceiling=1_030_000_000 + // Expected pool price_limit = min(1_020_000_000, 1_010_000_000, 1_030_000_000) = 1_010_000_000. + // price=1.0, scaled=1_000_000_000 <= 1_000_000_000 ✓ for all LimitBuy orders. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(500); + MockSwap::set_tao_balance(alice(), 600); + MockSwap::set_tao_balance(bob(), 200); + MockSwap::set_tao_balance(dave(), 200); + + let alice_order = make_signed_order_with_slippage( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 600, + 1_000_000_000, // 1.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(2)), // ceiling = 1_020_000_000 + ); + let bob_order = make_signed_order_with_slippage( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::LimitBuy, + 200, + 1_000_000_000, // 1.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(1)), // ceiling = 1_010_000_000 ← tightest + ); + let dave_order = make_signed_order_with_slippage( + AccountKeyring::Dave, + dave(), + netuid(), + OrderType::LimitBuy, + 200, + 1_000_000_000, // 1.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(3)), // ceiling = 1_030_000_000 + ); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![alice_order, bob_order, dave_order]), + )); + + // Net pool swap must have been called with the tightest ceiling = 1_010_000_000. + assert_eq!(MockSwap::buy_alpha_limit_prices(), vec![1_010_000_000]); + }); +} + +#[test] +fn execute_batched_orders_sell_dominant_uses_max_floor() { + new_test_ext().execute_with(|| { + // 3 sell orders with different slippage constraints. + // Alice: limit=1_000_000_000, 3% → floor=970_000_000 + // Bob: limit=1_000_000_000, 1% → floor=990_000_000 ← tightest (highest floor) + // Dave: limit=1_000_000_000, 2% → floor=980_000_000 + // Expected pool price_limit = max(970_000_000, 990_000_000, 980_000_000) = 990_000_000. + // Price must be >= limit_price=1_000_000_000 (1.0 in ×10⁹) for TakeProfit to trigger. + // price=2000.0, scaled=2_000_000_000_000 >= 1_000_000_000 ✓. + MockTime::set(1_000_000); + MockSwap::set_price(2_000.0); + MockSwap::set_sell_tao_return(500); + MockSwap::set_alpha_balance(alice(), dave(), netuid(), 600); + MockSwap::set_alpha_balance(bob(), dave(), netuid(), 200); + MockSwap::set_alpha_balance(dave(), dave(), netuid(), 200); + + let alice_order = make_signed_order_with_slippage( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::TakeProfit, + 600, + 1_000_000_000, // 1.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(3)), // floor = 970_000_000 + ); + let bob_order = make_signed_order_with_slippage( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::TakeProfit, + 200, + 1_000_000_000, // 1.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(1)), // floor = 990_000_000 ← tightest + ); + let dave_order = make_signed_order_with_slippage( + AccountKeyring::Dave, + dave(), + netuid(), + OrderType::TakeProfit, + 200, + 1_000_000_000, // 1.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(2)), // floor = 980_000_000 + ); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![alice_order, bob_order, dave_order]), + )); + + // Net pool swap must have been called with the tightest floor = 990_000_000. + assert_eq!(MockSwap::sell_alpha_limit_prices(), vec![990_000_000]); + }); +} + +#[test] +fn execute_batched_orders_no_slippage_uses_unconstrained_limits() { + new_test_ext().execute_with(|| { + // Orders without max_slippage should pass u64::MAX (buy) or 0 (sell). + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(500); + MockSwap::set_tao_balance(alice(), 1_000); + + let order = make_signed_order_with_slippage( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![order]), + )); + + assert_eq!(MockSwap::buy_alpha_limit_prices(), vec![u64::MAX]); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// max_slippage — mixed order type coexistence +// ───────────────────────────────────────────────────────────────────────────── + +/// Sell-dominant batch: TakeProfit orders (with slippage) + StopLoss (no slippage). +/// +/// TakeProfit orders set meaningful floors; StopLoss contributes 0 (no constraint). +/// pool_price_limit = max(take_floors..., 0s) = max(take_floors). +/// All three orders are fulfilled. +#[test] +fn execute_batched_orders_takeprofit_and_stoploss_coexist_sell_dominant() { + new_test_ext().execute_with(|| { + // Price = 2000 — scaled = 2_000_000_000_000. + // TakeProfit triggers when scaled_price >= limit_price (2T >= 1_000_000_000 ✓). + // StopLoss triggers when scaled_price <= limit_price (2T <= 5_000_000_000_000 ✓). + MockTime::set(1_000_000); + MockSwap::set_price(2_000.0); + MockSwap::set_sell_tao_return(500); + + // Alice TakeProfit: limit=1_000_000_000 (1.0), 3% → floor=970_000_000. + // Bob TakeProfit: limit=1_000_000_000 (1.0), 1% → floor=990_000_000. ← tightest + // Dave StopLoss: limit=5_000_000_000_000 (5000.0), None → floor=0. + MockSwap::set_alpha_balance(alice(), dave(), netuid(), 600); + MockSwap::set_alpha_balance(bob(), dave(), netuid(), 200); + MockSwap::set_alpha_balance(dave(), alice(), netuid(), 200); + + let alice_order = make_signed_order_with_slippage( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::TakeProfit, + 600, + 1_000_000_000, // 1.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(3)), + ); + let bob_order = make_signed_order_with_slippage( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::TakeProfit, + 200, + 1_000_000_000, // 1.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(1)), + ); + let dave_stoploss = make_signed_order_with_slippage( + AccountKeyring::Dave, + alice(), + netuid(), + OrderType::StopLoss, + 200, + 5_000_000_000_000, // 5000.0 in ×10⁹ scale; scaled_price 2T <= 5T ✓ + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, // StopLoss: no slippage → floor=0, does not constrain pool + ); + + let alice_id = order_id(&alice_order.order); + let bob_id = order_id(&bob_order.order); + let dave_id = order_id(&dave_stoploss.order); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![alice_order, bob_order, dave_stoploss]), + )); + + // All three fulfilled. + assert_eq!(Orders::::get(alice_id), Some(OrderStatus::Fulfilled)); + assert_eq!(Orders::::get(bob_id), Some(OrderStatus::Fulfilled)); + assert_eq!(Orders::::get(dave_id), Some(OrderStatus::Fulfilled)); + + // Pool called once with the tightest TakeProfit floor (990_000_000), not 0 from StopLoss. + assert_eq!(MockSwap::sell_alpha_limit_prices(), vec![990_000_000]); + }); +} + +/// Buy-dominant batch: LimitBuy orders (with slippage) dominant + StopLoss (no slippage) on offset side. +/// +/// The offset StopLoss is settled internally at spot price; it does not contribute +/// to the pool's price ceiling (which comes only from the dominant buy side). +/// pool_price_limit = min(buy_ceilings) = 1_010_000_000. +#[test] +fn execute_batched_orders_limitbuy_and_stoploss_offset_coexist_buy_dominant() { + new_test_ext().execute_with(|| { + // Price = 1.0, scaled = 1_000_000_000. + // LimitBuy triggers (scaled <= limit ✓). StopLoss triggers (scaled <= limit ✓). + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(900); + + // Alice LimitBuy: limit=1_000_000_000 (1.0), 2% → ceiling=1_020_000_000. + // Bob LimitBuy: limit=1_000_000_000 (1.0), 1% → ceiling=1_010_000_000. ← tightest + // Dave StopLoss: limit=2_000_000_000 (2.0), None → floor=0 (offset side, not used for pool limit). + MockSwap::set_tao_balance(alice(), 600); + MockSwap::set_tao_balance(bob(), 400); + MockSwap::set_alpha_balance(dave(), alice(), netuid(), 100); + + let alice_order = make_signed_order_with_slippage( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 600, + 1_000_000_000, // 1.0 in ×10⁹ scale; scaled=1_000_000_000 <= 1_000_000_000 ✓ + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(2)), + ); + let bob_order = make_signed_order_with_slippage( + AccountKeyring::Bob, + bob(), + netuid(), + OrderType::LimitBuy, + 400, + 1_000_000_000, // 1.0 in ×10⁹ scale; scaled=1_000_000_000 <= 1_000_000_000 ✓ + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(1)), + ); + let dave_stoploss = make_signed_order_with_slippage( + AccountKeyring::Dave, + alice(), + netuid(), + OrderType::StopLoss, + 100, + 2_000_000_000, // 2.0 in ×10⁹ scale; scaled=1_000_000_000 <= 2_000_000_000 ✓ + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, // StopLoss: no slippage; settled at spot, never constrains pool ceiling + ); + + let alice_id = order_id(&alice_order.order); + let bob_id = order_id(&bob_order.order); + let dave_id = order_id(&dave_stoploss.order); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![alice_order, bob_order, dave_stoploss]), + )); + + // All three fulfilled. + assert_eq!(Orders::::get(alice_id), Some(OrderStatus::Fulfilled)); + assert_eq!(Orders::::get(bob_id), Some(OrderStatus::Fulfilled)); + assert_eq!(Orders::::get(dave_id), Some(OrderStatus::Fulfilled)); + + // Pool buy called with min(1_020_000_000, 1_010_000_000) = 1_010_000_000. StopLoss's floor (0) is ignored on buy side. + assert_eq!(MockSwap::buy_alpha_limit_prices(), vec![1_010_000_000]); + }); +} + +/// StopLoss with a narrow slippage sets an effective floor above the current market price, +/// making the pool swap impossible and failing the entire batch. +/// +/// This demonstrates Issue 1 from the design: relayers should not apply max_slippage to +/// StopLoss orders. StopLoss triggers when price has already fallen; a floor derived from +/// the (higher) trigger threshold will almost always exceed the actual market price. +#[test] +fn execute_batched_orders_stoploss_narrow_slippage_breaks_batch() { + new_test_ext().execute_with(|| { + // StopLoss: limit=100_000_000_000 (100.0 in ×10⁹), triggers at price=50 (scaled=50_000_000_000 ≤ 100_000_000_000 ✓). + // 1% slippage → floor=99_000_000_000. Market is at 50 → pool cannot deliver ≥99_000_000_000. + MockTime::set(1_000_000); + MockSwap::set_price(50.0); + MockSwap::set_sell_tao_return(100); // non-zero so SwapReturnedZero is not the cause + MockSwap::set_enforce_price_limit(true); + MockSwap::set_alpha_balance(dave(), alice(), netuid(), 200); + + let stoploss = make_signed_order_with_slippage( + AccountKeyring::Dave, + alice(), + netuid(), + OrderType::StopLoss, + 200, + 100_000_000_000, // 100.0 in ×10⁹ scale; scaled=50_000_000_000 <= 100_000_000_000 ✓ + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(1)), // floor=99_000_000_000, but market=50 → pool rejects + ); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![stoploss]), + ), + DispatchError::Other("price limit exceeded") + ); + }); +} + +/// Same StopLoss scenario through execute_orders (best-effort): the order is silently +/// skipped rather than failing the whole call. +/// +/// Note: `DispatchError::Other` has `#[codec(skip)]` on its string field, so the reason +/// string is lost when stored in the event log. We verify the skip via storage absence +/// and by asserting the floor (99_000_000_000 = 100_000_000_000 - 1%) was actually passed +/// to the pool — which is what caused the rejection. The `execute_batched_orders` variant +/// below uses `assert_noop!` (checks the return value directly, no storage round-trip) and +/// can verify the string. +#[test] +fn execute_orders_stoploss_narrow_slippage_skips_order() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(50.0); + MockSwap::set_sell_tao_return(100); + MockSwap::set_enforce_price_limit(true); + + let stoploss = make_signed_order_with_slippage( + AccountKeyring::Dave, + alice(), + netuid(), + OrderType::StopLoss, + 200, + 100_000_000_000, // 100.0 in ×10⁹ scale; scaled=50_000_000_000 <= 100_000_000_000 ✓ + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(1)), // floor=99_000_000_000, but market=50 → pool rejects + ); + let id = order_id(&stoploss.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![stoploss]), + false, + )); + + // Order not stored — pool rejected the floor. + assert!(Orders::::get(id).is_none()); + + // An OrderSkipped event must have been emitted for this order. + assert!( + System::events().iter().any(|r| matches!( + &r.event, + RuntimeEvent::LimitOrders(Event::OrderSkipped { order_id, .. }) + if *order_id == id + )), + "expected OrderSkipped event for this order" + ); + + // The sell was attempted with the correct floor (99_000_000_000 = 100_000_000_000 - 1%). + // This is the value that exceeded the market price and caused the rejection. + assert_eq!(MockSwap::sell_alpha_limit_prices(), vec![99_000_000_000]); + }); +} diff --git a/pallets/limit-orders/src/tests/extrinsics/mod.rs b/pallets/limit-orders/src/tests/extrinsics/mod.rs new file mode 100644 index 0000000000..c71b59829f --- /dev/null +++ b/pallets/limit-orders/src/tests/extrinsics/mod.rs @@ -0,0 +1,76 @@ +#![allow(clippy::indexing_slicing, unused_imports)] +//! Integration tests for `pallet-limit-orders` extrinsics, split by concept. + +pub(crate) use codec::Encode; +pub(crate) use frame_support::{BoundedVec, assert_noop, assert_ok}; +pub(crate) use sp_core::Pair; +pub(crate) use sp_keyring::Sr25519Keyring as AccountKeyring; +pub(crate) use sp_runtime::{DispatchError, Perbill}; +pub(crate) use subtensor_runtime_common::NetUid; + +pub(crate) use crate::{ + Error, Order, OrderSide, OrderStatus, OrderType, Orders, VersionedOrder, pallet::Event, +}; + +pub(crate) type LimitOrders = crate::pallet::Pallet; + +pub(crate) use super::mock::*; + +/// Check that a specific pallet event was emitted. +pub(crate) fn assert_event(event: Event) { + assert!( + System::events() + .iter() + .any(|r| r.event == RuntimeEvent::LimitOrders(event.clone())), + "expected event not found: {event:?}", + ); +} + +/// Build a signed order with a specific `max_slippage` value. +#[allow(clippy::too_many_arguments)] +pub(crate) fn make_signed_order_with_slippage( + keyring: AccountKeyring, + hotkey: AccountId, + netuid: subtensor_runtime_common::NetUid, + order_type: OrderType, + amount: u64, + limit_price: u64, + expiry: u64, + fee_rate: sp_runtime::Perbill, + fee_recipient: AccountId, + max_slippage: Option, +) -> crate::SignedOrder { + let order = crate::VersionedOrder::V1(crate::Order { + signer: keyring.to_account_id(), + hotkey, + netuid, + order_type, + amount, + limit_price, + expiry, + fee_rate, + fee_recipient, + relayer: None, + max_slippage, + chain_id: 945, + partial_fills_enabled: false, + }); + let sig = keyring.pair().sign(&order.encode()); + crate::SignedOrder { + order, + signature: sp_runtime::MultiSignature::Sr25519(sig), + partial_fill: None, + } +} + +mod cancel_order; +mod execute_batched_orders; +mod execute_batched_orders_fee_routing; +mod execute_batched_orders_swap_errors; +mod execute_orders; +mod execute_orders_skip_invalid; +mod max_slippage; +mod pallet_status; +mod partial_fill; +mod relayer; +mod simulate_partial_fill; diff --git a/pallets/limit-orders/src/tests/extrinsics/pallet_status.rs b/pallets/limit-orders/src/tests/extrinsics/pallet_status.rs new file mode 100644 index 0000000000..2b78ce13ef --- /dev/null +++ b/pallets/limit-orders/src/tests/extrinsics/pallet_status.rs @@ -0,0 +1,48 @@ +//! `set_pallet_status` root gating and disable filtering. + +use super::*; + +/// Root changes the pallet status, extrinsics are filtered +#[test] +fn root_disables_and_extrinsics_are_filtered() { + new_test_ext().execute_with(|| { + // Disable the pallet + assert_ok!(LimitOrders::set_pallet_status(RuntimeOrigin::root(), false)); + + let sell = make_signed_order( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::TakeProfit, + 500, + 0, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + + // Must succeed: collecting Bob's alpha must not rate-limit the pallet + // intermediary, so distributing alpha to Alice is not blocked. + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![sell]) + ), + Error::::LimitOrdersDisabled + ); + }); +} + +/// Non-root origin cannot disable the pallet +#[test] +fn non_root_cannot_disable_the_pallet() { + new_test_ext().execute_with(|| { + // Try disabling the pallet with charlie + assert_noop!( + LimitOrders::set_pallet_status(RuntimeOrigin::signed(charlie()), false), + DispatchError::BadOrigin + ); + }); +} diff --git a/pallets/limit-orders/src/tests/extrinsics/partial_fill.rs b/pallets/limit-orders/src/tests/extrinsics/partial_fill.rs new file mode 100644 index 0000000000..cb35bd2ddb --- /dev/null +++ b/pallets/limit-orders/src/tests/extrinsics/partial_fill.rs @@ -0,0 +1,417 @@ +//! Extrinsic tests: partial fill. + +use super::*; + +// ───────────────────────────────────────────────────────────────────────────── +// Partial fills — execute_orders +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn execute_orders_partial_fill_sets_partially_filled_status() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_tao_balance(alice(), 1_000); + + // Order for 1000 TAO; relayer is charlie (required for partial fills). + let signed = make_partial_fill_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + charlie(), + 400, // fill 400 out of 1000 + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + assert_eq!( + Orders::::get(id), + Some(OrderStatus::PartiallyFilled(400)) + ); + }); +} + +#[test] +fn execute_orders_second_partial_fill_completes_order() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_tao_balance(alice(), 1_000); + + let signed_first = make_partial_fill_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + charlie(), + 600, + ); + let id = order_id(&signed_first.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed_first.clone()]), + false, + )); + assert_eq!( + Orders::::get(id), + Some(OrderStatus::PartiallyFilled(600)) + ); + + // Re-submit the same signed order payload with a different partial_fill amount. + let mut signed_second = signed_first.clone(); + signed_second.partial_fill = Some(400); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed_second]), + false, + )); + assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); + }); +} + +#[test] +fn execute_orders_partial_fill_without_relayer_skipped() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_tao_balance(alice(), 1_000); + + // Build an order with partial_fills_enabled but no relayer set. + let inner = crate::Order { + signer: alice(), + hotkey: bob(), + netuid: netuid(), + order_type: OrderType::LimitBuy, + amount: 1_000, + limit_price: u64::MAX, + expiry: FAR_FUTURE, + fee_rate: Perbill::zero(), + fee_recipient: fee_recipient(), + relayer: None, // <-- no relayer + max_slippage: None, + chain_id: 945, + partial_fills_enabled: true, + }; + let versioned = VersionedOrder::V1(inner); + let sig = AccountKeyring::Alice.pair().sign(&versioned.encode()); + let signed = crate::SignedOrder { + order: versioned, + signature: sp_runtime::MultiSignature::Sr25519(sig), + partial_fill: Some(400), + }; + let id = order_id(&signed.order); + + // The order is skipped (best-effort), not reverting the batch. + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + // Nothing written to storage. + assert_eq!(Orders::::get(id), None); + assert_event(Event::OrderSkipped { + order_id: id, + reason: Error::::RelayerRequiredForPartialFill.into(), + }); + }); +} + +#[test] +fn execute_orders_partial_fill_exceeding_remaining_is_skipped() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_tao_balance(alice(), 1_000); + + // Pre-fill 700 of 1000. + let signed = make_partial_fill_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + charlie(), + 700, + ); + let id = order_id(&signed.order); + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed.clone()]), + false, + )); + assert_eq!( + Orders::::get(id), + Some(OrderStatus::PartiallyFilled(700)) + ); + + // Try to fill 500 more, but only 300 remain → should be skipped. + let mut over_fill = signed.clone(); + over_fill.partial_fill = Some(500); + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![over_fill]), + false, + )); + + // Status unchanged. + assert_eq!( + Orders::::get(id), + Some(OrderStatus::PartiallyFilled(700)) + ); + assert_event(Event::OrderSkipped { + order_id: id, + reason: Error::::IncorrectPartialFillAmount.into(), + }); + }); +} + +#[test] +fn execute_orders_partial_fill_none_on_partially_filled_is_skipped() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_tao_balance(alice(), 1_000); + + // Pre-fill 700 of 1000. + let signed = make_partial_fill_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + charlie(), + 700, + ); + let id = order_id(&signed.order); + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed.clone()]), + false, + )); + assert_eq!( + Orders::::get(id), + Some(OrderStatus::PartiallyFilled(700)) + ); + + // Re-submit the same signed order with partial_fill = None against an + // order already PartiallyFilled. The one-shot full-execution path must + // not fire here: it would re-swap the full order.amount (over-debiting + // the signer) and mark the order Fulfilled, discarding the 700 already + // filled. The fix rejects this with IncorrectPartialFillAmount → skipped. + let mut none_fill = signed.clone(); + none_fill.partial_fill = None; + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![none_fill]), + false, + )); + + // Status unchanged — NOT over-filled and NOT marked Fulfilled. + assert_eq!( + Orders::::get(id), + Some(OrderStatus::PartiallyFilled(700)) + ); + assert_event(Event::OrderSkipped { + order_id: id, + reason: Error::::IncorrectPartialFillAmount.into(), + }); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Partial fills — execute_batched_orders +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn execute_batched_orders_partial_fill_sets_partially_filled_status() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(400); + MockSwap::set_tao_balance(alice(), 1_000); + + let signed = make_partial_fill_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + charlie(), + 400, + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![signed]), + )); + + assert_eq!( + Orders::::get(id), + Some(OrderStatus::PartiallyFilled(400)) + ); + }); +} + +#[test] +fn execute_batched_orders_second_partial_fill_completes_order() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(600); + MockSwap::set_tao_balance(alice(), 1_000); + + let signed_first = make_partial_fill_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + charlie(), + 600, + ); + let id = order_id(&signed_first.order); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![signed_first.clone()]), + )); + assert_eq!( + Orders::::get(id), + Some(OrderStatus::PartiallyFilled(600)) + ); + + let mut signed_second = signed_first.clone(); + signed_second.partial_fill = Some(400); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![signed_second]), + )); + assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// In-batch order_id deduplication — regression tests +// ───────────────────────────────────────────────────────────────────────────── + +/// Regression: the same fully-signed `LimitBuy` order appearing twice in one +/// batch must hard-fail with `DuplicateOrderInBatch` rather than debiting the +/// signer twice. Pre-fix, `validate_and_classify` validated each entry against +/// the same pre-batch `Orders::get(order_id)` snapshot with no in-batch tracking, +/// so the signer was charged N× their signed amount. +/// +/// `assert_noop!` also asserts the storage root is unchanged, proving the +/// all-or-nothing batch rolled back. (The mock's TAO/alpha ledgers are +/// thread-local RefCell maps, not substrate storage, so we do not assert on +/// them here — see `mock.rs`.) We additionally assert `Orders::get` was never +/// written. +#[test] +fn execute_batched_orders_full_fill_duplicate_rejected() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(500); + MockSwap::set_tao_balance(alice(), 1_000); + + // Open-relay (relayer: None) fully-signed LimitBuy. + let order = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 600, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&order.order); + + // The same order twice in one batch. + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![order.clone(), order]), + ), + Error::::DuplicateOrderInBatch + ); + + // The batch rolled back: no order status was recorded. + assert!(Orders::::get(id).is_none()); + }); +} + +/// Regression: two `SignedOrder`s that share the same inner `VersionedOrder` +/// (so the same `order_id`, since `order_id` excludes `partial_fill` and the +/// signature) but carry *different* `partial_fill` values must still collide +/// and be caught by the in-batch dedup. This exercises the partial-fill path +/// (partial_fills_enabled = true, relayer set). +#[test] +fn execute_batched_orders_partial_fill_duplicate_rejected() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(400); + MockSwap::set_tao_balance(alice(), 1_000); + + // Same inner VersionedOrder; only the envelope `partial_fill` differs. + let first = make_partial_fill_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + charlie(), + 600, + ); + let mut second = first.clone(); + second.partial_fill = Some(400); + + // Same inner order ⇒ same order_id ⇒ caught by the dedup set. + assert_eq!(order_id(&first.order), order_id(&second.order)); + let id = order_id(&first.order); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![first, second]), + ), + Error::::DuplicateOrderInBatch + ); + + assert!(Orders::::get(id).is_none()); + }); +} diff --git a/pallets/limit-orders/src/tests/extrinsics/relayer.rs b/pallets/limit-orders/src/tests/extrinsics/relayer.rs new file mode 100644 index 0000000000..a7477e33d6 --- /dev/null +++ b/pallets/limit-orders/src/tests/extrinsics/relayer.rs @@ -0,0 +1,148 @@ +//! Extrinsic tests: relayer. + +use super::*; + +// ───────────────────────────────────────────────────────────────────────────── +// relayer enforcement +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn execute_orders_wrong_relayer_skipped() { + new_test_ext().execute_with(|| { + // Order locks execution to charlie(); submitting as bob() must be silently skipped. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(BoundedVec::truncate_from(vec![charlie()])), // only charlie may relay this order + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(bob()), // wrong relayer + bounded(vec![signed]), + false, + )); + + // Order not stored — it was skipped. + assert!(Orders::::get(id).is_none()); + assert_event(Event::OrderSkipped { + order_id: id, + reason: Error::::RelayerMissMatch.into(), + }); + }); +} + +#[test] +fn execute_orders_correct_relayer_executed() { + new_test_ext().execute_with(|| { + // Same order submitted by the designated relayer (charlie) — must succeed. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(BoundedVec::truncate_from(vec![charlie()])), // charlie is the designated relayer + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), // correct relayer + bounded(vec![signed]), + false, + )); + + assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); + assert_event(Event::OrderExecuted { + order_id: id, + signer: alice(), + netuid: netuid(), + order_type: OrderType::LimitBuy, + amount_in: 1_000, + amount_out: 0, + }); + }); +} + +#[test] +fn execute_batched_orders_wrong_relayer_fails_entire_batch() { + new_test_ext().execute_with(|| { + // In execute_batched_orders a relayer mismatch is a hard failure — the + // whole call is reverted, unlike the best-effort skip in execute_orders. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(BoundedVec::truncate_from(vec![charlie()])), // only charlie may relay this order + ); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(bob()), // wrong relayer + netuid(), + bounded(vec![signed]) + ), + Error::::RelayerMissMatch + ); + }); +} + +#[test] +fn execute_batched_orders_correct_relayer_succeeds() { + new_test_ext().execute_with(|| { + // Same order submitted by the designated relayer — must execute and + // distribute alpha to the buyer. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(1_000); + MockSwap::set_tao_balance(alice(), 1_000); + + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(BoundedVec::truncate_from(vec![charlie()])), // charlie is the designated relayer + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), // correct relayer + netuid(), + bounded(vec![signed]) + )); + + assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); + }); +} diff --git a/pallets/limit-orders/src/tests/extrinsics/simulate_partial_fill.rs b/pallets/limit-orders/src/tests/extrinsics/simulate_partial_fill.rs new file mode 100644 index 0000000000..d8fd9b166c --- /dev/null +++ b/pallets/limit-orders/src/tests/extrinsics/simulate_partial_fill.rs @@ -0,0 +1,168 @@ +//! Extrinsic tests: simulate partial fill. + +use super::*; + +// ───────────────────────────────────────────────────────────────────────────── +// MOCK_SIMULATE_PARTIAL_FILL — sim-swap detects partial fill before funds move +// ───────────────────────────────────────────────────────────────────────────── + +/// `execute_batched_orders` hard-fails the whole batch when the sim-swap for a +/// `LimitBuy` order detects a partial fill (price limit would stop the AMM +/// before consuming the full input). +#[test] +fn execute_batched_orders_buy_partial_fill_fails_batch() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_simulate_partial_fill(true); + + let order = make_signed_order_with_slippage( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, // limit_price always passes for a buy + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_parts(1)), // slippage field set; mock ignores value + ); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![order]), + ), + DispatchError::Other("slippage too high") + ); + }); +} + +/// `execute_orders` silently skips a `LimitBuy` order when the sim-swap detects +/// a partial fill: the order must not appear in storage and an `OrderSkipped` +/// event must be emitted. +#[test] +fn execute_orders_buy_partial_fill_skips_order() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_simulate_partial_fill(true); + + let order = make_signed_order_with_slippage( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, // limit_price always passes for a buy + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_parts(1)), // slippage field set; mock ignores value + ); + let id = order_id(&order.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![order]), + false, + )); + + // Order must not be stored — it was skipped, not fulfilled. + assert!(Orders::::get(id).is_none()); + + // An OrderSkipped event must have been emitted for this order. + assert!( + System::events().iter().any(|r| matches!( + &r.event, + RuntimeEvent::LimitOrders(Event::OrderSkipped { order_id, .. }) + if *order_id == id + )), + "expected OrderSkipped event for this order" + ); + }); +} + +/// `execute_batched_orders` hard-fails the whole batch when the sim-swap for a +/// `TakeProfit` (sell) order detects a partial fill. +#[test] +fn execute_batched_orders_sell_partial_fill_fails_batch() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_simulate_partial_fill(true); + // Seed alpha so the order passes the balance check before reaching the swap. + MockSwap::set_alpha_balance(alice(), bob(), netuid(), 1_000); + + let order = make_signed_order_with_slippage( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::TakeProfit, + 1_000, + 0, // limit_price = 0 → floor always passes for a TakeProfit + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_parts(1)), // slippage field set; mock ignores value + ); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![order]), + ), + DispatchError::Other("slippage too high") + ); + }); +} + +/// `execute_orders` silently skips a `TakeProfit` order when the sim-swap +/// detects a partial fill: the order must not appear in storage and an +/// `OrderSkipped` event must be emitted. +#[test] +fn execute_orders_sell_partial_fill_skips_order() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_simulate_partial_fill(true); + // Seed alpha so the order passes the balance check before reaching the swap. + MockSwap::set_alpha_balance(alice(), bob(), netuid(), 1_000); + + let order = make_signed_order_with_slippage( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::TakeProfit, + 1_000, + 0, // limit_price = 0 → floor always passes for a TakeProfit + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_parts(1)), // slippage field set; mock ignores value + ); + let id = order_id(&order.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![order]), + false, + )); + + // Order must not be stored — it was skipped, not fulfilled. + assert!(Orders::::get(id).is_none()); + + // An OrderSkipped event must have been emitted for this order. + assert!( + System::events().iter().any(|r| matches!( + &r.event, + RuntimeEvent::LimitOrders(Event::OrderSkipped { order_id, .. }) + if *order_id == id + )), + "expected OrderSkipped event for this order" + ); + }); +} diff --git a/pallets/limit-orders/src/tests/mod.rs b/pallets/limit-orders/src/tests/mod.rs index 95e0875b26..d6125292db 100644 --- a/pallets/limit-orders/src/tests/mod.rs +++ b/pallets/limit-orders/src/tests/mod.rs @@ -1,3 +1,5 @@ +//! Unit / integration tests for `pallet-limit-orders`. + pub mod auxiliary; pub mod extrinsics; pub mod migration; diff --git a/pallets/proxy/src/benchmarking.rs b/pallets/proxy/src/benchmarking.rs index 9bf21cb951..a5f8e2bac6 100644 --- a/pallets/proxy/src/benchmarking.rs +++ b/pallets/proxy/src/benchmarking.rs @@ -39,7 +39,11 @@ fn assert_has_event(generic_event: ::assert_has_event(generic_event.into()); } -fn add_proxies(n: u32, maybe_who: Option) -> Result<(), &'static str> { +/// Seed `n` default-type proxy delegates on `maybe_who` (or the whitelisted caller). +fn seed_benchmark_proxies( + n: u32, + maybe_who: Option, +) -> Result<(), &'static str> { let caller = maybe_who.unwrap_or_else(whitelisted_caller); T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value() / 2u32.into()); for i in 0..n { @@ -55,7 +59,8 @@ fn add_proxies(n: u32, maybe_who: Option) -> Result<(), Ok(()) } -fn add_announcements( +/// Seed `n` pending announcements from `maybe_who` against `maybe_real` (creating a proxy if needed). +fn seed_benchmark_announcements( n: u32, maybe_who: Option, maybe_real: Option, @@ -93,7 +98,7 @@ mod benchmarks { #[benchmark] fn proxy(p: Linear<1, { T::MaxProxies::get() - 1 }>) -> Result<(), BenchmarkError> { - add_proxies::(p, None)?; + seed_benchmark_proxies::(p, None)?; // In this case the caller is the "target" proxy let caller: T::AccountId = account("target", p - 1, SEED); T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value() / 2u32.into()); @@ -121,7 +126,7 @@ mod benchmarks { a: Linear<0, { T::MaxPending::get() - 1 }>, p: Linear<1, { T::MaxProxies::get() - 1 }>, ) -> Result<(), BenchmarkError> { - add_proxies::(p, None)?; + seed_benchmark_proxies::(p, None)?; // In this case the caller is the "target" proxy let caller: T::AccountId = account("pure", 0, SEED); let delegate: T::AccountId = account("target", p - 1, SEED); @@ -137,7 +142,7 @@ mod benchmarks { real_lookup.clone(), T::CallHasher::hash_of(&call), )?; - add_announcements::(a, Some(delegate.clone()), None)?; + seed_benchmark_announcements::(a, Some(delegate.clone()), None)?; #[extrinsic_call] _( @@ -158,7 +163,7 @@ mod benchmarks { a: Linear<0, { T::MaxPending::get() - 1 }>, p: Linear<1, { T::MaxProxies::get() - 1 }>, ) -> Result<(), BenchmarkError> { - add_proxies::(p, None)?; + seed_benchmark_proxies::(p, None)?; // In this case the caller is the "target" proxy let caller: T::AccountId = account("target", p - 1, SEED); T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value() / 2u32.into()); @@ -172,7 +177,7 @@ mod benchmarks { real_lookup.clone(), T::CallHasher::hash_of(&call), )?; - add_announcements::(a, Some(caller.clone()), None)?; + seed_benchmark_announcements::(a, Some(caller.clone()), None)?; #[extrinsic_call] _( @@ -192,7 +197,7 @@ mod benchmarks { a: Linear<0, { T::MaxPending::get() - 1 }>, p: Linear<1, { T::MaxProxies::get() - 1 }>, ) -> Result<(), BenchmarkError> { - add_proxies::(p, None)?; + seed_benchmark_proxies::(p, None)?; // In this case the caller is the "target" proxy let caller: T::AccountId = account("target", p - 1, SEED); let caller_lookup = T::Lookup::unlookup(caller.clone()); @@ -207,7 +212,7 @@ mod benchmarks { real_lookup, T::CallHasher::hash_of(&call), )?; - add_announcements::(a, Some(caller.clone()), None)?; + seed_benchmark_announcements::(a, Some(caller.clone()), None)?; #[extrinsic_call] _( @@ -227,14 +232,14 @@ mod benchmarks { a: Linear<0, { T::MaxPending::get() - 1 }>, p: Linear<1, { T::MaxProxies::get() - 1 }>, ) -> Result<(), BenchmarkError> { - add_proxies::(p, None)?; + seed_benchmark_proxies::(p, None)?; // In this case the caller is the "target" proxy let caller: T::AccountId = account("target", p - 1, SEED); T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value() / 2u32.into()); // ... and "real" is the traditional caller. This is not a typo. let real: T::AccountId = whitelisted_caller(); let real_lookup = T::Lookup::unlookup(real.clone()); - add_announcements::(a, Some(caller.clone()), None)?; + seed_benchmark_announcements::(a, Some(caller.clone()), None)?; let call: ::RuntimeCall = frame_system::Call::::remark { remark: vec![] }.into(); let call_hash = T::CallHasher::hash_of(&call); @@ -256,7 +261,7 @@ mod benchmarks { #[benchmark] fn add_proxy(p: Linear<1, { T::MaxProxies::get() - 1 }>) -> Result<(), BenchmarkError> { - add_proxies::(p, None)?; + seed_benchmark_proxies::(p, None)?; let caller: T::AccountId = whitelisted_caller(); let real = T::Lookup::unlookup(account("target", T::MaxProxies::get(), SEED)); @@ -276,7 +281,7 @@ mod benchmarks { #[benchmark] fn remove_proxy(p: Linear<1, { T::MaxProxies::get() - 1 }>) -> Result<(), BenchmarkError> { - add_proxies::(p, None)?; + seed_benchmark_proxies::(p, None)?; let caller: T::AccountId = whitelisted_caller(); let delegate = T::Lookup::unlookup(account("target", 0, SEED)); @@ -296,7 +301,7 @@ mod benchmarks { #[benchmark] fn remove_proxies(p: Linear<1, { T::MaxProxies::get() - 1 }>) -> Result<(), BenchmarkError> { - add_proxies::(p, None)?; + seed_benchmark_proxies::(p, None)?; let caller: T::AccountId = whitelisted_caller(); #[extrinsic_call] @@ -310,7 +315,7 @@ mod benchmarks { #[benchmark] fn create_pure(p: Linear<1, { T::MaxProxies::get() - 1 }>) -> Result<(), BenchmarkError> { - add_proxies::(p, None)?; + seed_benchmark_proxies::(p, None)?; let caller: T::AccountId = whitelisted_caller(); #[extrinsic_call] @@ -352,7 +357,7 @@ mod benchmarks { let pure_account = Pallet::::pure_account(&caller, &T::ProxyType::default(), 0, None).unwrap(); - add_proxies::(p, Some(pure_account.clone()))?; + seed_benchmark_proxies::(p, Some(pure_account.clone()))?; ensure!( Proxies::::contains_key(&pure_account), "pure proxy not created" @@ -493,7 +498,7 @@ mod benchmarks { #[benchmark] fn set_real_pays_fee(p: Linear<1, { T::MaxProxies::get() - 1 }>) -> Result<(), BenchmarkError> { - add_proxies::(p, None)?; + seed_benchmark_proxies::(p, None)?; let caller: T::AccountId = whitelisted_caller(); let delegate: T::AccountId = account("target", 0, SEED); let delegate_lookup = T::Lookup::unlookup(delegate.clone()); diff --git a/pallets/proxy/src/impls.rs b/pallets/proxy/src/impls.rs new file mode 100644 index 0000000000..c0fefe8a4c --- /dev/null +++ b/pallets/proxy/src/impls.rs @@ -0,0 +1,291 @@ +//! Proxy pallet helpers: deposits, announcements, pure accounts, and dispatch. + +use super::*; + +impl Pallet { + /// Read [`Proxies`] for `account`: `(delegates, reserved_deposit)`. + pub fn proxies( + account: T::AccountId, + ) -> ( + BoundedVec>, T::MaxProxies>, + BalanceOf, + ) { + Proxies::::get(account) + } + + /// Read [`Announcements`] for `account`: `(pending, reserved_deposit)`. + pub fn announcements( + account: T::AccountId, + ) -> ( + BoundedVec, BlockNumberFor>, T::MaxPending>, + BalanceOf, + ) { + Announcements::::get(account) + } + + /// Calculate the address of an pure account. + /// + /// - `who`: The spawner account. + /// - `proxy_type`: The type of the proxy that the sender will be registered as over the + /// new account. This will almost always be the most permissive `ProxyType` possible to + /// allow for maximum flexibility. + /// - `index`: A disambiguation index, in case this is called multiple times in the same + /// transaction (e.g. with `utility::batch`). Unless you're using `batch` you probably just + /// want to use `0`. + /// - `maybe_when`: The block height and extrinsic index of when the pure account was + /// created. None to use current block height and extrinsic index. + pub fn pure_account( + who: &T::AccountId, + proxy_type: &T::ProxyType, + index: u16, + maybe_when: Option<(BlockNumberFor, u32)>, + ) -> Result { + let (height, ext_index) = maybe_when.unwrap_or_else(|| { + ( + T::BlockNumberProvider::current_block_number(), + frame_system::Pallet::::extrinsic_index().unwrap_or_default(), + ) + }); + let entropy = ( + b"modlpy/proxy____", + who, + height, + ext_index, + proxy_type, + index, + ) + .using_encoded(blake2_256); + + T::AccountId::decode(&mut TrailingZeroInput::new(entropy.as_ref())) + .map_err(|_| Error::::InvalidDerivedAccountId.into()) + } + + /// Register a proxy account for the delegator that is able to make calls on its behalf. + /// + /// Parameters: + /// - `delegator`: The delegator account. + /// - `delegatee`: The account that the `delegator` would like to make a proxy. + /// - `proxy_type`: The permissions allowed for this proxy account. + /// - `delay`: The announcement period required of the initial proxy. Will generally be + /// zero. + pub fn add_proxy_delegate( + delegator: &T::AccountId, + delegatee: T::AccountId, + proxy_type: T::ProxyType, + delay: BlockNumberFor, + ) -> DispatchResult { + ensure!(delegator != &delegatee, Error::::NoSelfProxy); + Proxies::::try_mutate(delegator, |(proxies, deposit)| { + let proxy_def = ProxyDefinition { + delegate: delegatee.clone(), + proxy_type: proxy_type.clone(), + delay, + }; + let i = proxies + .binary_search(&proxy_def) + .err() + .ok_or(Error::::Duplicate)?; + proxies + .try_insert(i, proxy_def) + .map_err(|_| Error::::TooMany)?; + let new_deposit = Self::deposit(proxies.len() as u32); + if new_deposit > *deposit { + T::Currency::reserve(delegator, new_deposit.saturating_sub(*deposit))?; + } else if new_deposit < *deposit { + T::Currency::unreserve(delegator, (*deposit).saturating_sub(new_deposit)); + } + *deposit = new_deposit; + Self::deposit_event(Event::::ProxyAdded { + delegator: delegator.clone(), + delegatee, + proxy_type, + delay, + }); + Ok(()) + }) + } + + /// Unregister a proxy account for the delegator. + /// + /// Parameters: + /// - `delegator`: The delegator account. + /// - `delegatee`: The account that the `delegator` would like to make a proxy. + /// - `proxy_type`: The permissions allowed for this proxy account. + /// - `delay`: The announcement period required of the initial proxy. Will generally be + /// zero. + pub fn remove_proxy_delegate( + delegator: &T::AccountId, + delegatee: T::AccountId, + proxy_type: T::ProxyType, + delay: BlockNumberFor, + ) -> DispatchResult { + Proxies::::try_mutate_exists(delegator, |x| { + let (mut proxies, old_deposit) = x.take().ok_or(Error::::NotFound)?; + let proxy_def = ProxyDefinition { + delegate: delegatee.clone(), + proxy_type: proxy_type.clone(), + delay, + }; + let i = proxies + .binary_search(&proxy_def) + .ok() + .ok_or(Error::::NotFound)?; + proxies.remove(i); + let new_deposit = Self::deposit(proxies.len() as u32); + if new_deposit > old_deposit { + T::Currency::reserve(delegator, new_deposit.saturating_sub(old_deposit))?; + } else if new_deposit < old_deposit { + T::Currency::unreserve(delegator, old_deposit.saturating_sub(new_deposit)); + } + if !proxies.is_empty() { + *x = Some((proxies, new_deposit)) + } + // Clean up real-pays-fee flag for this specific proxy relationship + RealPaysFee::::remove(delegator, &delegatee); + + Self::deposit_event(Event::::ProxyRemoved { + delegator: delegator.clone(), + delegatee, + proxy_type, + delay, + }); + Ok(()) + }) + } + + /// Required reserve for `num_proxies` entries: `base + factor * n` (zero when `n == 0`). + pub fn deposit(num_proxies: u32) -> BalanceOf { + if num_proxies == 0 { + Zero::zero() + } else { + T::ProxyDepositBase::get() + .saturating_add(T::ProxyDepositFactor::get().saturating_mul(num_proxies.into())) + } + } + + /// Top up or release reserved funds so the lock matches `base + factor * len`. + /// + /// Returns `None` when `len == 0` (caller should clear the storage entry). + pub(crate) fn recompute_reserved_deposit( + who: &T::AccountId, + old_deposit: BalanceOf, + base: BalanceOf, + factor: BalanceOf, + len: usize, + ) -> Result>, DispatchError> { + let new_deposit = if len == 0 { + BalanceOf::::zero() + } else { + base.saturating_add(factor.saturating_mul((len as u32).into())) + }; + if new_deposit > old_deposit { + T::Currency::reserve(who, new_deposit.saturating_sub(old_deposit))?; + } else if new_deposit < old_deposit { + let excess = old_deposit.saturating_sub(new_deposit); + let remaining_unreserved = T::Currency::unreserve(who, excess); + if !remaining_unreserved.is_zero() { + defensive!( + "Failed to unreserve full amount. (Requested, Actual)", + (excess, excess.saturating_sub(remaining_unreserved)) + ); + } + } + Ok(if len == 0 { None } else { Some(new_deposit) }) + } + + /// Keep announcements for which `f` returns true; fails with [`Error::NotFound`] if none removed. + pub(crate) fn retain_proxy_announcements< + F: FnMut(&Announcement, BlockNumberFor>) -> bool, + >( + delegate: &T::AccountId, + f: F, + ) -> DispatchResult { + Announcements::::try_mutate_exists(delegate, |x| { + let (mut pending, old_deposit) = x.take().ok_or(Error::::NotFound)?; + let orig_pending_len = pending.len(); + pending.retain(f); + ensure!(orig_pending_len > pending.len(), Error::::NotFound); + *x = Self::recompute_reserved_deposit( + delegate, + old_deposit, + T::AnnouncementDepositBase::get(), + T::AnnouncementDepositFactor::get(), + pending.len(), + )? + .map(|deposit| (pending, deposit)); + Ok(()) + }) + } + + /// Locate the proxy definition for `delegate` acting on `real`, optionally matching `force_proxy_type`. + pub fn find_proxy( + real: &T::AccountId, + delegate: &T::AccountId, + force_proxy_type: Option, + ) -> Result>, DispatchError> { + let f = |x: &ProxyDefinition>| -> bool { + &x.delegate == delegate && force_proxy_type.as_ref().is_none_or(|y| &x.proxy_type == y) + }; + Ok(Proxies::::get(real) + .0 + .into_iter() + .find(f) + .ok_or(Error::::NotProxy)?) + } + + /// Dispatch `call` as `real` under `def.proxy_type` filters (privilege escalation guards included). + pub(crate) fn dispatch_filtered_proxy_call( + def: ProxyDefinition>, + real: T::AccountId, + call: ::RuntimeCall, + ) { + use frame::traits::{InstanceFilter as _, OriginTrait as _}; + // This is a freshly authenticated new account, the origin restrictions doesn't apply. + let mut origin: T::RuntimeOrigin = frame_system::RawOrigin::Signed(real.clone()).into(); + origin.add_filter(move |c: &::RuntimeCall| { + let c = ::RuntimeCall::from_ref(c); + // We make sure the proxy call does access this pallet to change modify proxies. + match c.is_sub_type() { + // Proxy call cannot add or remove a proxy with more permissions than it already + // has. + Some(Call::add_proxy { proxy_type, .. }) + | Some(Call::remove_proxy { proxy_type, .. }) + if !def.proxy_type.is_superset(proxy_type) => + { + false + } + // Proxy call cannot remove all proxies or kill pure proxies unless it has full + // permissions. + Some(Call::remove_proxies { .. }) | Some(Call::kill_pure { .. }) + if def.proxy_type != T::ProxyType::default() => + { + false + } + _ => def.proxy_type.filter(c), + } + }); + let e = call.dispatch(origin); + + LastCallResult::::insert(real, e.map(|_| ()).map_err(|e| e.error)); + + Self::deposit_event(Event::ProxyExecuted { + result: e.map(|_| ()).map_err(|e| e.error), + }); + } + + /// Removes all proxy delegates for a given delegator. + /// + /// Parameters: + /// - `delegator`: The delegator account. + pub fn remove_all_proxy_delegates(delegator: &T::AccountId) { + let (_, old_deposit) = Proxies::::take(delegator); + T::Currency::unreserve(delegator, old_deposit); + // Clean up all real-pays-fee flags for this delegator + let _ = RealPaysFee::::clear_prefix(delegator, u32::MAX, None); + } + + /// Check if the real account has opted in to paying fees for a specific delegate. + pub fn is_real_pays_fee(real: &T::AccountId, delegate: &T::AccountId) -> bool { + RealPaysFee::::contains_key(real, delegate) + } +} diff --git a/pallets/proxy/src/lib.rs b/pallets/proxy/src/lib.rs index 1fca855327..3975bcfc60 100644 --- a/pallets/proxy/src/lib.rs +++ b/pallets/proxy/src/lib.rs @@ -16,20 +16,26 @@ // limitations under the License. //! # Proxy Pallet -//! A pallet allowing accounts to give permission to other accounts to dispatch types of calls from -//! their signed origin. //! -//! The accounts to which permission is delegated may be required to announce the action that they -//! wish to execute some duration prior to execution happens. In this case, the target account may -//! reject the announcement and in doing so, veto the execution. +//! Lets a **real** (delegator) account authorize **delegate** accounts to dispatch filtered +//! [`RuntimeCall`](Config::RuntimeCall)s as if signed by the real account. +//! +//! Delayed proxies require an [`announce`](Pallet::announce) of the call hash and a waiting period +//! before [`proxy_announced`](Pallet::proxy_announced); the real account can +//! [`reject_announcement`](Pallet::reject_announcement) to veto. Pure accounts +//! ([`create_pure`](Pallet::create_pure) / [`kill_pure`](Pallet::kill_pure)) are keyless accounts +//! controlled only through proxy. Optional [`RealPaysFee`] lets the real account pay extrinsic fees +//! for a delegate's proxy calls. //! //! - [`Config`] //! - [`Call`] +//! - [`Proxies`] / [`Announcements`] / [`RealPaysFee`] // Ensure we're `no_std` when compiling for Wasm. #![cfg_attr(not(feature = "std"), no_std)] mod benchmarking; +mod impls; mod tests; pub mod weights; @@ -54,8 +60,8 @@ pub type BlockNumberFor = type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; -/// The parameters under which a particular account has a proxy relationship with some other -/// account. +/// One delegate entry under a real account: who may act, which call subset, +/// and announcement delay. #[derive( Encode, Decode, @@ -72,28 +78,28 @@ type AccountIdLookupOf = <::Lookup as StaticLookup )] #[freeze_struct("a37bb67fe5520678")] pub struct ProxyDefinition { - /// The account which may act on behalf of another. + /// Delegate account authorized to act on behalf of the real (storage-map key) account. pub delegate: AccountId, - /// A value defining the subset of calls that it is allowed to make. + /// Call subset this delegate may dispatch (`InstanceFilter` / privilege rules apply). pub proxy_type: ProxyType, - /// The number of blocks that an announcement must be in place for before the corresponding - /// call may be dispatched. If zero, then no announcement is needed. + /// Blocks an announcement must age before `proxy_announced` may run. + /// If zero, then no announcement is needed and immediate `proxy` is allowed. pub delay: BlockNumber, } -/// Details surrounding a specific instance of an announcement to make a call. +/// Pending delayed-proxy announcement: call hash posted by a delegate against a real account. #[derive(Encode, Decode, Clone, Copy, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] #[freeze_struct("4c1b5c8c3bc489ad")] pub struct Announcement { - /// The account which made the announcement. + /// Real account on whose behalf the announced call will run. real: AccountId, - /// The hash of the call to be made. + /// Hash of the call that must later be supplied to `proxy_announced`. call_hash: Hash, - /// The height at which the announcement was made. + /// Block number when the announcement was recorded (delay measured from here). height: BlockNumber, } -/// The type of deposit +/// Which reserved-balance bucket [`Event::DepositPoked`] refers to. #[derive( Encode, Decode, @@ -107,9 +113,9 @@ pub struct Announcement { DecodeWithMemTracking, )] pub enum DepositKind { - /// Proxy registration deposit + /// Deposit locked against `Proxies` entries for the account. Proxies, - /// Announcement deposit + /// Deposit locked against pending `Announcements` for the account. Announcements, } @@ -248,7 +254,7 @@ pub mod pallet { let def = Self::find_proxy(&real, &who, force_proxy_type)?; ensure!(def.delay.is_zero(), Error::::Unannounced); - Self::do_proxy(def, real, *call); + Self::dispatch_filtered_proxy_call(def, real, *call); Ok(()) } @@ -449,7 +455,7 @@ pub mod pallet { pending .try_push(announcement) .map_err(|_| Error::::TooMany)?; - let new_deposit = Self::rejig_deposit( + let new_deposit = Self::recompute_reserved_deposit( &who, *deposit, T::AnnouncementDepositBase::get(), @@ -492,7 +498,9 @@ pub mod pallet { ) -> DispatchResult { let who = ensure_signed(origin)?; let real = T::Lookup::lookup(real)?; - Self::edit_announcements(&who, |ann| ann.real != real || ann.call_hash != call_hash)?; + Self::retain_proxy_announcements(&who, |ann| { + ann.real != real || ann.call_hash != call_hash + })?; Ok(()) } @@ -519,7 +527,7 @@ pub mod pallet { ) -> DispatchResult { let who = ensure_signed(origin)?; let delegate = T::Lookup::lookup(delegate)?; - Self::edit_announcements(&delegate, |ann| { + Self::retain_proxy_announcements(&delegate, |ann| { ann.real != who || ann.call_hash != call_hash })?; @@ -560,14 +568,14 @@ pub mod pallet { let call_hash = T::CallHasher::hash_of(&call); let now = T::BlockNumberProvider::current_block_number(); - Self::edit_announcements(&delegate, |ann| { + Self::retain_proxy_announcements(&delegate, |ann| { ann.real != real || ann.call_hash != call_hash || now.saturating_sub(ann.height) < def.delay }) .map_err(|_| Error::::Unannounced)?; - Self::do_proxy(def, real, *call); + Self::dispatch_filtered_proxy_call(def, real, *call); Ok(()) } @@ -589,7 +597,7 @@ pub mod pallet { // Check and update proxy deposits Proxies::::try_mutate_exists(&who, |maybe_proxies| -> DispatchResult { let (proxies, old_deposit) = maybe_proxies.take().unwrap_or_default(); - let maybe_new_deposit = Self::rejig_deposit( + let maybe_new_deposit = Self::recompute_reserved_deposit( &who, old_deposit, T::ProxyDepositBase::get(), @@ -630,7 +638,7 @@ pub mod pallet { // Check and update announcement deposits Announcements::::try_mutate_exists(&who, |maybe_announcements| -> DispatchResult { let (announcements, old_deposit) = maybe_announcements.take().unwrap_or_default(); - let maybe_new_deposit = Self::rejig_deposit( + let maybe_new_deposit = Self::recompute_reserved_deposit( &who, old_deposit, T::AnnouncementDepositBase::get(), @@ -717,55 +725,54 @@ pub mod pallet { #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { - /// A proxy was executed correctly, with the given. + /// A proxied call finished; `result` is the inner dispatch outcome (filter failures included). ProxyExecuted { result: DispatchResult }, - /// A pure account has been created by new proxy with given - /// disambiguation index and proxy type. + /// A keyless pure account was spawned; `who` is the initial delegate over `pure`. PureCreated { pure: T::AccountId, who: T::AccountId, proxy_type: T::ProxyType, disambiguation_index: u16, }, - /// A pure proxy was killed by its spawner. + /// Pure account removed; deposit unreserved to `spawner`. Funds left on `pure` are lost. PureKilled { - // The pure proxy account that was destroyed. + /// The pure proxy account that was destroyed. pure: T::AccountId, - // The account that created the pure proxy. + /// The account that created the pure proxy. spawner: T::AccountId, - // The proxy type of the pure proxy that was destroyed. + /// The proxy type of the pure proxy that was destroyed. proxy_type: T::ProxyType, - // The index originally passed to `create_pure` when this pure proxy was created. + /// The index originally passed to `create_pure` when this pure proxy was created. disambiguation_index: u16, }, - /// An announcement was placed to make a call in the future. + /// Delegate announced `call_hash` for a future delayed proxy on behalf of `real`. Announced { real: T::AccountId, proxy: T::AccountId, call_hash: CallHashOf, }, - /// A proxy was added. + /// `delegator` authorized `delegatee` with `proxy_type` and announcement `delay`. ProxyAdded { delegator: T::AccountId, delegatee: T::AccountId, proxy_type: T::ProxyType, delay: BlockNumberFor, }, - /// A proxy was removed. + /// Proxy relationship removed (exact `proxy_type` + `delay` match). ProxyRemoved { delegator: T::AccountId, delegatee: T::AccountId, proxy_type: T::ProxyType, delay: BlockNumberFor, }, - /// A deposit stored for proxies or announcements was poked / updated. + /// Reserved deposit for proxies or announcements was recomputed (e.g. via `poke_deposit`). DepositPoked { who: T::AccountId, kind: DepositKind, old_deposit: BalanceOf, new_deposit: BalanceOf, }, - /// The real-pays-fee setting was updated for a proxy relationship. + /// Fee-payer preference for `(real, delegate)` proxy calls was set (`pays_fee == true` → real pays). RealPaysFeeSet { real: T::AccountId, delegate: T::AccountId, @@ -775,38 +782,39 @@ pub mod pallet { #[pallet::error] pub enum Error { - /// There are too many proxies registered or too many announcements pending. + /// `MaxProxies` or `MaxPending` capacity exceeded. TooMany, - /// Proxy registration not found. + /// No matching proxy definition or announcement to edit/remove. NotFound, - /// Sender is not a proxy of the account to be proxied. + /// Caller is not a registered delegate for the target real account. NotProxy, - /// A call which is incompatible with the proxy type's filter was attempted. + /// Inner call failed the `ProxyType` instance filter. Unproxyable, - /// Account is already a proxy. + /// Identical proxy definition (or pure account identity) already exists. Duplicate, - /// Call may not be made by proxy because it may escalate its privileges. + /// Proxy attempted a privilege-escalating call (e.g. broader `add_proxy` / `kill_pure`). NoPermission, - /// Announcement, if made at all, was made too recently. + /// Delayed proxy missing a mature announcement for this call hash. Unannounced, - /// Cannot add self as proxy. + /// Delegator tried to add itself as its own delegate. NoSelfProxy, - /// Invariant violated: deposit recomputation returned None after updating announcements. + /// Announcement deposit recomputation returned `None` while announcements remain (logic bug). AnnouncementDepositInvariantViolated, - /// Failed to derive a valid account id from the provided entropy. + /// `AccountId` could not be decoded from pure-account entropy. InvalidDerivedAccountId, } #[pallet::hooks] impl Hooks> for Pallet { fn on_finalize(_n: SystemBlockNumberFor) { - // clear this map on end of each block + // Ephemeral per-block cache for the latest proxied dispatch result. let _ = LastCallResult::::clear(u32::MAX, None); } } - /// The set of account proxies. Maps the account which has delegated to the accounts - /// which are being delegated to, together with the amount held on deposit. + /// Delegator → `(sorted proxy definitions, reserved deposit)`. + /// + /// Deposit is `ProxyDepositBase + ProxyDepositFactor * len` while non-empty. #[pallet::storage] pub type Proxies = StorageMap< _, @@ -822,7 +830,9 @@ pub mod pallet { ValueQuery, >; - /// The announcements made by the proxy (key). + /// Delegate → `(pending announcements, reserved deposit)`. + /// + /// Deposit is `AnnouncementDepositBase + AnnouncementDepositFactor * len` while non-empty. #[pallet::storage] pub type Announcements = StorageMap< _, @@ -835,15 +845,13 @@ pub mod pallet { ValueQuery, >; - /// The result of the last call made by the proxy (key). + /// Latest proxied dispatch result keyed by the **real** account; cleared each block in `on_finalize`. #[pallet::storage] pub type LastCallResult = StorageMap<_, Twox64Concat, T::AccountId, DispatchResult, OptionQuery>; - /// Tracks which (real, delegate) pairs have opted in to the real account paying - /// transaction fees for proxy calls made by the delegate. - /// Existence of an entry means the real account pays; absence means the delegate pays - /// (default). + /// Opt-in: when `(real, delegate)` is present, the real account pays fees for that delegate's + /// proxy extrinsics; absent means the delegate pays (default). Cleared when the proxy is removed. #[pallet::storage] pub type RealPaysFee = StorageDoubleMap< _, @@ -871,284 +879,3 @@ pub mod pallet { } } } - -impl Pallet { - /// Public function to proxies storage. - pub fn proxies( - account: T::AccountId, - ) -> ( - BoundedVec>, T::MaxProxies>, - BalanceOf, - ) { - Proxies::::get(account) - } - - /// Public function to announcements storage. - pub fn announcements( - account: T::AccountId, - ) -> ( - BoundedVec, BlockNumberFor>, T::MaxPending>, - BalanceOf, - ) { - Announcements::::get(account) - } - - /// Calculate the address of an pure account. - /// - /// - `who`: The spawner account. - /// - `proxy_type`: The type of the proxy that the sender will be registered as over the - /// new account. This will almost always be the most permissive `ProxyType` possible to - /// allow for maximum flexibility. - /// - `index`: A disambiguation index, in case this is called multiple times in the same - /// transaction (e.g. with `utility::batch`). Unless you're using `batch` you probably just - /// want to use `0`. - /// - `maybe_when`: The block height and extrinsic index of when the pure account was - /// created. None to use current block height and extrinsic index. - pub fn pure_account( - who: &T::AccountId, - proxy_type: &T::ProxyType, - index: u16, - maybe_when: Option<(BlockNumberFor, u32)>, - ) -> Result { - let (height, ext_index) = maybe_when.unwrap_or_else(|| { - ( - T::BlockNumberProvider::current_block_number(), - frame_system::Pallet::::extrinsic_index().unwrap_or_default(), - ) - }); - let entropy = ( - b"modlpy/proxy____", - who, - height, - ext_index, - proxy_type, - index, - ) - .using_encoded(blake2_256); - - T::AccountId::decode(&mut TrailingZeroInput::new(entropy.as_ref())) - .map_err(|_| Error::::InvalidDerivedAccountId.into()) - } - - /// Register a proxy account for the delegator that is able to make calls on its behalf. - /// - /// Parameters: - /// - `delegator`: The delegator account. - /// - `delegatee`: The account that the `delegator` would like to make a proxy. - /// - `proxy_type`: The permissions allowed for this proxy account. - /// - `delay`: The announcement period required of the initial proxy. Will generally be - /// zero. - pub fn add_proxy_delegate( - delegator: &T::AccountId, - delegatee: T::AccountId, - proxy_type: T::ProxyType, - delay: BlockNumberFor, - ) -> DispatchResult { - ensure!(delegator != &delegatee, Error::::NoSelfProxy); - Proxies::::try_mutate(delegator, |(proxies, deposit)| { - let proxy_def = ProxyDefinition { - delegate: delegatee.clone(), - proxy_type: proxy_type.clone(), - delay, - }; - let i = proxies - .binary_search(&proxy_def) - .err() - .ok_or(Error::::Duplicate)?; - proxies - .try_insert(i, proxy_def) - .map_err(|_| Error::::TooMany)?; - let new_deposit = Self::deposit(proxies.len() as u32); - if new_deposit > *deposit { - T::Currency::reserve(delegator, new_deposit.saturating_sub(*deposit))?; - } else if new_deposit < *deposit { - T::Currency::unreserve(delegator, (*deposit).saturating_sub(new_deposit)); - } - *deposit = new_deposit; - Self::deposit_event(Event::::ProxyAdded { - delegator: delegator.clone(), - delegatee, - proxy_type, - delay, - }); - Ok(()) - }) - } - - /// Unregister a proxy account for the delegator. - /// - /// Parameters: - /// - `delegator`: The delegator account. - /// - `delegatee`: The account that the `delegator` would like to make a proxy. - /// - `proxy_type`: The permissions allowed for this proxy account. - /// - `delay`: The announcement period required of the initial proxy. Will generally be - /// zero. - pub fn remove_proxy_delegate( - delegator: &T::AccountId, - delegatee: T::AccountId, - proxy_type: T::ProxyType, - delay: BlockNumberFor, - ) -> DispatchResult { - Proxies::::try_mutate_exists(delegator, |x| { - let (mut proxies, old_deposit) = x.take().ok_or(Error::::NotFound)?; - let proxy_def = ProxyDefinition { - delegate: delegatee.clone(), - proxy_type: proxy_type.clone(), - delay, - }; - let i = proxies - .binary_search(&proxy_def) - .ok() - .ok_or(Error::::NotFound)?; - proxies.remove(i); - let new_deposit = Self::deposit(proxies.len() as u32); - if new_deposit > old_deposit { - T::Currency::reserve(delegator, new_deposit.saturating_sub(old_deposit))?; - } else if new_deposit < old_deposit { - T::Currency::unreserve(delegator, old_deposit.saturating_sub(new_deposit)); - } - if !proxies.is_empty() { - *x = Some((proxies, new_deposit)) - } - // Clean up real-pays-fee flag for this specific proxy relationship - RealPaysFee::::remove(delegator, &delegatee); - - Self::deposit_event(Event::::ProxyRemoved { - delegator: delegator.clone(), - delegatee, - proxy_type, - delay, - }); - Ok(()) - }) - } - - pub fn deposit(num_proxies: u32) -> BalanceOf { - if num_proxies == 0 { - Zero::zero() - } else { - T::ProxyDepositBase::get() - .saturating_add(T::ProxyDepositFactor::get().saturating_mul(num_proxies.into())) - } - } - - fn rejig_deposit( - who: &T::AccountId, - old_deposit: BalanceOf, - base: BalanceOf, - factor: BalanceOf, - len: usize, - ) -> Result>, DispatchError> { - let new_deposit = if len == 0 { - BalanceOf::::zero() - } else { - base.saturating_add(factor.saturating_mul((len as u32).into())) - }; - if new_deposit > old_deposit { - T::Currency::reserve(who, new_deposit.saturating_sub(old_deposit))?; - } else if new_deposit < old_deposit { - let excess = old_deposit.saturating_sub(new_deposit); - let remaining_unreserved = T::Currency::unreserve(who, excess); - if !remaining_unreserved.is_zero() { - defensive!( - "Failed to unreserve full amount. (Requested, Actual)", - (excess, excess.saturating_sub(remaining_unreserved)) - ); - } - } - Ok(if len == 0 { None } else { Some(new_deposit) }) - } - - fn edit_announcements< - F: FnMut(&Announcement, BlockNumberFor>) -> bool, - >( - delegate: &T::AccountId, - f: F, - ) -> DispatchResult { - Announcements::::try_mutate_exists(delegate, |x| { - let (mut pending, old_deposit) = x.take().ok_or(Error::::NotFound)?; - let orig_pending_len = pending.len(); - pending.retain(f); - ensure!(orig_pending_len > pending.len(), Error::::NotFound); - *x = Self::rejig_deposit( - delegate, - old_deposit, - T::AnnouncementDepositBase::get(), - T::AnnouncementDepositFactor::get(), - pending.len(), - )? - .map(|deposit| (pending, deposit)); - Ok(()) - }) - } - - pub fn find_proxy( - real: &T::AccountId, - delegate: &T::AccountId, - force_proxy_type: Option, - ) -> Result>, DispatchError> { - let f = |x: &ProxyDefinition>| -> bool { - &x.delegate == delegate && force_proxy_type.as_ref().is_none_or(|y| &x.proxy_type == y) - }; - Ok(Proxies::::get(real) - .0 - .into_iter() - .find(f) - .ok_or(Error::::NotProxy)?) - } - - fn do_proxy( - def: ProxyDefinition>, - real: T::AccountId, - call: ::RuntimeCall, - ) { - use frame::traits::{InstanceFilter as _, OriginTrait as _}; - // This is a freshly authenticated new account, the origin restrictions doesn't apply. - let mut origin: T::RuntimeOrigin = frame_system::RawOrigin::Signed(real.clone()).into(); - origin.add_filter(move |c: &::RuntimeCall| { - let c = ::RuntimeCall::from_ref(c); - // We make sure the proxy call does access this pallet to change modify proxies. - match c.is_sub_type() { - // Proxy call cannot add or remove a proxy with more permissions than it already - // has. - Some(Call::add_proxy { proxy_type, .. }) - | Some(Call::remove_proxy { proxy_type, .. }) - if !def.proxy_type.is_superset(proxy_type) => - { - false - } - // Proxy call cannot remove all proxies or kill pure proxies unless it has full - // permissions. - Some(Call::remove_proxies { .. }) | Some(Call::kill_pure { .. }) - if def.proxy_type != T::ProxyType::default() => - { - false - } - _ => def.proxy_type.filter(c), - } - }); - let e = call.dispatch(origin); - - LastCallResult::::insert(real, e.map(|_| ()).map_err(|e| e.error)); - - Self::deposit_event(Event::ProxyExecuted { - result: e.map(|_| ()).map_err(|e| e.error), - }); - } - - /// Removes all proxy delegates for a given delegator. - /// - /// Parameters: - /// - `delegator`: The delegator account. - pub fn remove_all_proxy_delegates(delegator: &T::AccountId) { - let (_, old_deposit) = Proxies::::take(delegator); - T::Currency::unreserve(delegator, old_deposit); - // Clean up all real-pays-fee flags for this delegator - let _ = RealPaysFee::::clear_prefix(delegator, u32::MAX, None); - } - - /// Check if the real account has opted in to paying fees for a specific delegate. - pub fn is_real_pays_fee(real: &T::AccountId, delegate: &T::AccountId) -> bool { - RealPaysFee::::contains_key(real, delegate) - } -} diff --git a/pallets/proxy/src/tests.rs b/pallets/proxy/src/tests.rs deleted file mode 100644 index 5bc5be2415..0000000000 --- a/pallets/proxy/src/tests.rs +++ /dev/null @@ -1,1375 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Tests for Proxy Pallet - -#![cfg(test)] -#![allow( - clippy::arithmetic_side_effects, - clippy::unwrap_used, - clippy::indexing_slicing -)] - -use super::*; -use crate as proxy; -use alloc::{vec, vec::Vec}; -use frame::testing_prelude::*; - -type Block = frame_system::mocking::MockBlock; - -construct_runtime!( - pub enum Test { - System: frame_system = 1, - Balances: pallet_balances = 2, - Proxy: proxy = 3, - Utility: pallet_utility = 4, - } -); - -#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] -impl frame_system::Config for Test { - type Block = Block; - type BaseCallFilter = BaseFilter; - type AccountData = pallet_balances::AccountData; -} - -#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] -impl pallet_balances::Config for Test { - type ReserveIdentifier = [u8; 8]; - type AccountStore = System; -} - -impl pallet_utility::Config for Test { - type RuntimeCall = RuntimeCall; - type PalletsOrigin = OriginCaller; - type WeightInfo = (); -} - -#[derive( - Copy, - Clone, - Eq, - PartialEq, - Ord, - PartialOrd, - Encode, - Decode, - DecodeWithMemTracking, - RuntimeDebug, - MaxEncodedLen, - scale_info::TypeInfo, -)] -pub enum ProxyType { - Any, - JustTransfer, - JustUtility, -} -impl Default for ProxyType { - fn default() -> Self { - Self::Any - } -} -impl frame::traits::InstanceFilter for ProxyType { - fn filter(&self, c: &RuntimeCall) -> bool { - match self { - ProxyType::Any => true, - ProxyType::JustTransfer => { - matches!( - c, - RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { .. }) - ) - } - ProxyType::JustUtility => matches!(c, RuntimeCall::Utility { .. }), - } - } - fn is_superset(&self, o: &Self) -> bool { - self == &ProxyType::Any || self == o - } -} -pub struct BaseFilter; -impl Contains for BaseFilter { - fn contains(c: &RuntimeCall) -> bool { - match *c { - // Remark is used as a no-op call in the benchmarking - RuntimeCall::System(SystemCall::remark { .. }) => true, - RuntimeCall::System(_) => false, - _ => true, - } - } -} - -parameter_types! { - pub static ProxyDepositBase: u64 = 1; - pub static ProxyDepositFactor: u64 = 1; - pub static AnnouncementDepositBase: u64 = 1; - pub static AnnouncementDepositFactor: u64 = 1; -} - -impl Config for Test { - type RuntimeCall = RuntimeCall; - type Currency = Balances; - type ProxyType = ProxyType; - type ProxyDepositBase = ProxyDepositBase; - type ProxyDepositFactor = ProxyDepositFactor; - type MaxProxies = ConstU32<4>; - type WeightInfo = (); - type CallHasher = BlakeTwo256; - type MaxPending = ConstU32<2>; - type AnnouncementDepositBase = AnnouncementDepositBase; - type AnnouncementDepositFactor = AnnouncementDepositFactor; - type BlockNumberProvider = frame_system::Pallet; -} - -use super::{Call as ProxyCall, Event as ProxyEvent}; -use frame_system::Call as SystemCall; -use pallet_balances::{Call as BalancesCall, Error as BalancesError, Event as BalancesEvent}; -use pallet_subtensor_utility as pallet_utility; -use pallet_subtensor_utility::{Call as UtilityCall, Event as UtilityEvent}; - -type SystemError = frame_system::Error; - -pub fn new_test_ext() -> TestState { - let mut t = frame_system::GenesisConfig::::default() - .build_storage() - .unwrap(); - pallet_balances::GenesisConfig:: { - balances: vec![(1, 10), (2, 10), (3, 10), (4, 10), (5, 3)], - ..Default::default() - } - .assimilate_storage(&mut t) - .unwrap(); - let mut ext = TestState::new(t); - ext.execute_with(|| System::set_block_number(1)); - ext -} - -fn last_events(n: usize) -> Vec { - frame_system::Pallet::::events() - .into_iter() - .rev() - .take(n) - .rev() - .map(|e| e.event) - .collect() -} - -fn expect_events(e: Vec) { - assert_eq!(last_events(e.len()), e); -} - -fn call_transfer(dest: u64, value: u64) -> RuntimeCall { - RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest, value }) -} - -#[test] -fn announcement_works() { - new_test_ext().execute_with(|| { - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(1), - 3, - ProxyType::Any, - 1 - )); - System::assert_last_event( - ProxyEvent::ProxyAdded { - delegator: 1, - delegatee: 3, - proxy_type: ProxyType::Any, - delay: 1, - } - .into(), - ); - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(2), - 3, - ProxyType::Any, - 1 - )); - assert_eq!(Balances::reserved_balance(3), 0); - - assert_ok!(Proxy::announce(RuntimeOrigin::signed(3), 1, [1; 32].into())); - let announcements = Announcements::::get(3); - assert_eq!( - announcements.0, - vec![Announcement { - real: 1, - call_hash: [1; 32].into(), - height: 1 - }] - ); - assert_eq!(Balances::reserved_balance(3), announcements.1); - - assert_ok!(Proxy::announce(RuntimeOrigin::signed(3), 2, [2; 32].into())); - let announcements = Announcements::::get(3); - assert_eq!( - announcements.0, - vec![ - Announcement { - real: 1, - call_hash: [1; 32].into(), - height: 1 - }, - Announcement { - real: 2, - call_hash: [2; 32].into(), - height: 1 - }, - ] - ); - assert_eq!(Balances::reserved_balance(3), announcements.1); - - assert_noop!( - Proxy::announce(RuntimeOrigin::signed(3), 2, [3; 32].into()), - Error::::TooMany - ); - }); -} - -#[test] -fn remove_announcement_works() { - new_test_ext().execute_with(|| { - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(1), - 3, - ProxyType::Any, - 1 - )); - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(2), - 3, - ProxyType::Any, - 1 - )); - assert_ok!(Proxy::announce(RuntimeOrigin::signed(3), 1, [1; 32].into())); - assert_ok!(Proxy::announce(RuntimeOrigin::signed(3), 2, [2; 32].into())); - let e = Error::::NotFound; - assert_noop!( - Proxy::remove_announcement(RuntimeOrigin::signed(3), 1, [0; 32].into()), - e - ); - assert_ok!(Proxy::remove_announcement( - RuntimeOrigin::signed(3), - 1, - [1; 32].into() - )); - let announcements = Announcements::::get(3); - assert_eq!( - announcements.0, - vec![Announcement { - real: 2, - call_hash: [2; 32].into(), - height: 1 - }] - ); - assert_eq!(Balances::reserved_balance(3), announcements.1); - }); -} - -#[test] -fn reject_announcement_works() { - new_test_ext().execute_with(|| { - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(1), - 3, - ProxyType::Any, - 1 - )); - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(2), - 3, - ProxyType::Any, - 1 - )); - assert_ok!(Proxy::announce(RuntimeOrigin::signed(3), 1, [1; 32].into())); - assert_ok!(Proxy::announce(RuntimeOrigin::signed(3), 2, [2; 32].into())); - let e = Error::::NotFound; - assert_noop!( - Proxy::reject_announcement(RuntimeOrigin::signed(1), 3, [0; 32].into()), - e - ); - let e = Error::::NotFound; - assert_noop!( - Proxy::reject_announcement(RuntimeOrigin::signed(4), 3, [1; 32].into()), - e - ); - assert_ok!(Proxy::reject_announcement( - RuntimeOrigin::signed(1), - 3, - [1; 32].into() - )); - let announcements = Announcements::::get(3); - assert_eq!( - announcements.0, - vec![Announcement { - real: 2, - call_hash: [2; 32].into(), - height: 1 - }] - ); - assert_eq!(Balances::reserved_balance(3), announcements.1); - }); -} - -#[test] -fn announcer_must_be_proxy() { - new_test_ext().execute_with(|| { - assert_noop!( - Proxy::announce(RuntimeOrigin::signed(2), 1, H256::zero()), - Error::::NotProxy - ); - }); -} - -#[test] -fn calling_proxy_doesnt_remove_announcement() { - new_test_ext().execute_with(|| { - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(1), - 2, - ProxyType::Any, - 0 - )); - - let call = Box::new(call_transfer(6, 1)); - let call_hash = BlakeTwo256::hash_of(&call); - - assert_ok!(Proxy::announce(RuntimeOrigin::signed(2), 1, call_hash)); - assert_ok!(Proxy::proxy(RuntimeOrigin::signed(2), 1, None, call)); - - // The announcement is not removed by calling proxy. - let announcements = Announcements::::get(2); - assert_eq!( - announcements.0, - vec![Announcement { - real: 1, - call_hash, - height: 1 - }] - ); - }); -} - -#[test] -fn delayed_requires_pre_announcement() { - new_test_ext().execute_with(|| { - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(1), - 2, - ProxyType::Any, - 1 - )); - let call = Box::new(call_transfer(6, 1)); - let e = Error::::Unannounced; - assert_noop!( - Proxy::proxy(RuntimeOrigin::signed(2), 1, None, call.clone()), - e - ); - let e = Error::::Unannounced; - assert_noop!( - Proxy::proxy_announced(RuntimeOrigin::signed(0), 2, 1, None, call.clone()), - e - ); - let call_hash = BlakeTwo256::hash_of(&call); - assert_ok!(Proxy::announce(RuntimeOrigin::signed(2), 1, call_hash)); - frame_system::Pallet::::set_block_number(2); - assert_ok!(Proxy::proxy_announced( - RuntimeOrigin::signed(0), - 2, - 1, - None, - call.clone() - )); - }); -} - -#[test] -fn proxy_announced_removes_announcement_and_returns_deposit() { - new_test_ext().execute_with(|| { - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(1), - 3, - ProxyType::Any, - 1 - )); - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(2), - 3, - ProxyType::Any, - 1 - )); - let call = Box::new(call_transfer(6, 1)); - let call_hash = BlakeTwo256::hash_of(&call); - assert_ok!(Proxy::announce(RuntimeOrigin::signed(3), 1, call_hash)); - assert_ok!(Proxy::announce(RuntimeOrigin::signed(3), 2, call_hash)); - // Too early to execute announced call - let e = Error::::Unannounced; - assert_noop!( - Proxy::proxy_announced(RuntimeOrigin::signed(0), 3, 1, None, call.clone()), - e - ); - - frame_system::Pallet::::set_block_number(2); - assert_ok!(Proxy::proxy_announced( - RuntimeOrigin::signed(0), - 3, - 1, - None, - call.clone() - )); - let announcements = Announcements::::get(3); - assert_eq!( - announcements.0, - vec![Announcement { - real: 2, - call_hash, - height: 1 - }] - ); - assert_eq!(Balances::reserved_balance(3), announcements.1); - }); -} - -#[test] -fn filtering_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 1000); - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(1), - 2, - ProxyType::Any, - 0 - )); - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(1), - 3, - ProxyType::JustTransfer, - 0 - )); - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(1), - 4, - ProxyType::JustUtility, - 0 - )); - - let call = Box::new(call_transfer(6, 1)); - assert_ok!(Proxy::proxy( - RuntimeOrigin::signed(2), - 1, - None, - call.clone() - )); - System::assert_last_event(ProxyEvent::ProxyExecuted { result: Ok(()) }.into()); - assert_ok!(Proxy::proxy( - RuntimeOrigin::signed(3), - 1, - None, - call.clone() - )); - System::assert_last_event(ProxyEvent::ProxyExecuted { result: Ok(()) }.into()); - assert_ok!(Proxy::proxy( - RuntimeOrigin::signed(4), - 1, - None, - call.clone() - )); - System::assert_last_event( - ProxyEvent::ProxyExecuted { - result: Err(SystemError::CallFiltered.into()), - } - .into(), - ); - - let derivative_id = Utility::derivative_account_id(1, 0).unwrap(); - Balances::make_free_balance_be(&derivative_id, 1000); - let inner = Box::new(call_transfer(6, 1)); - - let call = Box::new(RuntimeCall::Utility(UtilityCall::as_derivative { - index: 0, - call: inner.clone(), - })); - assert_ok!(Proxy::proxy( - RuntimeOrigin::signed(2), - 1, - None, - call.clone() - )); - System::assert_last_event(ProxyEvent::ProxyExecuted { result: Ok(()) }.into()); - assert_ok!(Proxy::proxy( - RuntimeOrigin::signed(3), - 1, - None, - call.clone() - )); - System::assert_last_event( - ProxyEvent::ProxyExecuted { - result: Err(SystemError::CallFiltered.into()), - } - .into(), - ); - assert_ok!(Proxy::proxy( - RuntimeOrigin::signed(4), - 1, - None, - call.clone() - )); - System::assert_last_event( - ProxyEvent::ProxyExecuted { - result: Err(SystemError::CallFiltered.into()), - } - .into(), - ); - - let call = Box::new(RuntimeCall::Utility(UtilityCall::batch { - calls: vec![*inner], - })); - assert_ok!(Proxy::proxy( - RuntimeOrigin::signed(2), - 1, - None, - call.clone() - )); - expect_events(vec![ - UtilityEvent::BatchCompleted.into(), - ProxyEvent::ProxyExecuted { result: Ok(()) }.into(), - ]); - assert_ok!(Proxy::proxy( - RuntimeOrigin::signed(3), - 1, - None, - call.clone() - )); - System::assert_last_event( - ProxyEvent::ProxyExecuted { - result: Err(SystemError::CallFiltered.into()), - } - .into(), - ); - assert_ok!(Proxy::proxy( - RuntimeOrigin::signed(4), - 1, - None, - call.clone() - )); - expect_events(vec![ - UtilityEvent::BatchInterrupted { - index: 0, - error: SystemError::CallFiltered.into(), - } - .into(), - ProxyEvent::ProxyExecuted { result: Ok(()) }.into(), - ]); - - let inner = Box::new(RuntimeCall::Proxy(ProxyCall::new_call_variant_add_proxy( - 5, - ProxyType::Any, - 0, - ))); - let call = Box::new(RuntimeCall::Utility(UtilityCall::batch { - calls: vec![*inner], - })); - assert_ok!(Proxy::proxy( - RuntimeOrigin::signed(2), - 1, - None, - call.clone() - )); - expect_events(vec![ - UtilityEvent::BatchCompleted.into(), - ProxyEvent::ProxyExecuted { result: Ok(()) }.into(), - ]); - assert_ok!(Proxy::proxy( - RuntimeOrigin::signed(3), - 1, - None, - call.clone() - )); - System::assert_last_event( - ProxyEvent::ProxyExecuted { - result: Err(SystemError::CallFiltered.into()), - } - .into(), - ); - assert_ok!(Proxy::proxy( - RuntimeOrigin::signed(4), - 1, - None, - call.clone() - )); - expect_events(vec![ - UtilityEvent::BatchInterrupted { - index: 0, - error: SystemError::CallFiltered.into(), - } - .into(), - ProxyEvent::ProxyExecuted { result: Ok(()) }.into(), - ]); - - let call = Box::new(RuntimeCall::Proxy(ProxyCall::remove_proxies {})); - assert_ok!(Proxy::proxy( - RuntimeOrigin::signed(3), - 1, - None, - call.clone() - )); - System::assert_last_event( - ProxyEvent::ProxyExecuted { - result: Err(SystemError::CallFiltered.into()), - } - .into(), - ); - assert_ok!(Proxy::proxy( - RuntimeOrigin::signed(4), - 1, - None, - call.clone() - )); - System::assert_last_event( - ProxyEvent::ProxyExecuted { - result: Err(SystemError::CallFiltered.into()), - } - .into(), - ); - assert_ok!(Proxy::proxy( - RuntimeOrigin::signed(2), - 1, - None, - call.clone() - )); - expect_events(vec![ - BalancesEvent::::Unreserved { who: 1, amount: 5 }.into(), - ProxyEvent::ProxyExecuted { result: Ok(()) }.into(), - ]); - }); -} - -#[test] -fn add_remove_proxies_works() { - new_test_ext().execute_with(|| { - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(1), - 2, - ProxyType::Any, - 0 - )); - assert_noop!( - Proxy::add_proxy(RuntimeOrigin::signed(1), 2, ProxyType::Any, 0), - Error::::Duplicate - ); - assert_eq!(Balances::reserved_balance(1), 2); - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(1), - 2, - ProxyType::JustTransfer, - 0 - )); - assert_eq!(Balances::reserved_balance(1), 3); - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(1), - 3, - ProxyType::Any, - 0 - )); - assert_eq!(Balances::reserved_balance(1), 4); - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(1), - 4, - ProxyType::JustUtility, - 0 - )); - assert_eq!(Balances::reserved_balance(1), 5); - assert_noop!( - Proxy::add_proxy(RuntimeOrigin::signed(1), 4, ProxyType::Any, 0), - Error::::TooMany - ); - assert_noop!( - Proxy::remove_proxy(RuntimeOrigin::signed(1), 3, ProxyType::JustTransfer, 0), - Error::::NotFound - ); - assert_ok!(Proxy::remove_proxy( - RuntimeOrigin::signed(1), - 4, - ProxyType::JustUtility, - 0 - )); - System::assert_last_event( - ProxyEvent::ProxyRemoved { - delegator: 1, - delegatee: 4, - proxy_type: ProxyType::JustUtility, - delay: 0, - } - .into(), - ); - assert_eq!(Balances::reserved_balance(1), 4); - assert_ok!(Proxy::remove_proxy( - RuntimeOrigin::signed(1), - 3, - ProxyType::Any, - 0 - )); - assert_eq!(Balances::reserved_balance(1), 3); - System::assert_last_event( - ProxyEvent::ProxyRemoved { - delegator: 1, - delegatee: 3, - proxy_type: ProxyType::Any, - delay: 0, - } - .into(), - ); - assert_ok!(Proxy::remove_proxy( - RuntimeOrigin::signed(1), - 2, - ProxyType::Any, - 0 - )); - assert_eq!(Balances::reserved_balance(1), 2); - System::assert_last_event( - ProxyEvent::ProxyRemoved { - delegator: 1, - delegatee: 2, - proxy_type: ProxyType::Any, - delay: 0, - } - .into(), - ); - assert_ok!(Proxy::remove_proxy( - RuntimeOrigin::signed(1), - 2, - ProxyType::JustTransfer, - 0 - )); - assert_eq!(Balances::reserved_balance(1), 0); - System::assert_last_event( - ProxyEvent::ProxyRemoved { - delegator: 1, - delegatee: 2, - proxy_type: ProxyType::JustTransfer, - delay: 0, - } - .into(), - ); - assert_noop!( - Proxy::add_proxy(RuntimeOrigin::signed(1), 1, ProxyType::Any, 0), - Error::::NoSelfProxy - ); - }); -} - -#[test] -fn cannot_add_proxy_without_balance() { - new_test_ext().execute_with(|| { - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(5), - 3, - ProxyType::Any, - 0 - )); - assert_eq!(Balances::reserved_balance(5), 2); - assert_noop!( - Proxy::add_proxy(RuntimeOrigin::signed(5), 4, ProxyType::Any, 0), - DispatchError::ConsumerRemaining, - ); - }); -} - -#[test] -fn proxying_works() { - new_test_ext().execute_with(|| { - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(1), - 2, - ProxyType::JustTransfer, - 0 - )); - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(1), - 3, - ProxyType::Any, - 0 - )); - - let call = Box::new(call_transfer(6, 1)); - assert_noop!( - Proxy::proxy(RuntimeOrigin::signed(4), 1, None, call.clone()), - Error::::NotProxy - ); - assert_noop!( - Proxy::proxy( - RuntimeOrigin::signed(2), - 1, - Some(ProxyType::Any), - call.clone() - ), - Error::::NotProxy - ); - assert_ok!(Proxy::proxy( - RuntimeOrigin::signed(2), - 1, - None, - call.clone() - )); - System::assert_last_event(ProxyEvent::ProxyExecuted { result: Ok(()) }.into()); - assert_eq!(Balances::free_balance(6), 1); - - let call = Box::new(RuntimeCall::System(SystemCall::set_code { code: vec![] })); - assert_ok!(Proxy::proxy( - RuntimeOrigin::signed(3), - 1, - None, - call.clone() - )); - System::assert_last_event( - ProxyEvent::ProxyExecuted { - result: Err(SystemError::CallFiltered.into()), - } - .into(), - ); - - let call = Box::new(RuntimeCall::Balances(BalancesCall::transfer_keep_alive { - dest: 6, - value: 1, - })); - assert_ok!( - RuntimeCall::Proxy(super::Call::new_call_variant_proxy(1, None, call.clone())) - .dispatch(RuntimeOrigin::signed(2)) - ); - System::assert_last_event( - ProxyEvent::ProxyExecuted { - result: Err(SystemError::CallFiltered.into()), - } - .into(), - ); - assert_ok!(Proxy::proxy( - RuntimeOrigin::signed(3), - 1, - None, - call.clone() - )); - System::assert_last_event(ProxyEvent::ProxyExecuted { result: Ok(()) }.into()); - assert_eq!(Balances::free_balance(6), 2); - }); -} - -#[test] -fn pure_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 11); // An extra one for the ED. - assert_ok!(Proxy::create_pure( - RuntimeOrigin::signed(1), - ProxyType::Any, - 0, - 0 - )); - let anon = Proxy::pure_account(&1, &ProxyType::Any, 0, None).unwrap(); - System::assert_last_event( - ProxyEvent::PureCreated { - pure: anon, - who: 1, - proxy_type: ProxyType::Any, - disambiguation_index: 0, - } - .into(), - ); - - // other calls to pure allowed as long as they're not exactly the same. - assert_ok!(Proxy::create_pure( - RuntimeOrigin::signed(1), - ProxyType::JustTransfer, - 0, - 0 - )); - assert_ok!(Proxy::create_pure( - RuntimeOrigin::signed(1), - ProxyType::Any, - 0, - 1 - )); - let anon2 = Proxy::pure_account(&2, &ProxyType::Any, 0, None).unwrap(); - assert_ok!(Proxy::create_pure( - RuntimeOrigin::signed(2), - ProxyType::Any, - 0, - 0 - )); - assert_noop!( - Proxy::create_pure(RuntimeOrigin::signed(1), ProxyType::Any, 0, 0), - Error::::Duplicate - ); - System::set_extrinsic_index(1); - assert_ok!(Proxy::create_pure( - RuntimeOrigin::signed(1), - ProxyType::Any, - 0, - 0 - )); - System::set_extrinsic_index(0); - System::set_block_number(2); - assert_ok!(Proxy::create_pure( - RuntimeOrigin::signed(1), - ProxyType::Any, - 0, - 0 - )); - - let call = Box::new(call_transfer(6, 1)); - assert_ok!(Balances::transfer_allow_death( - RuntimeOrigin::signed(3), - anon, - 5 - )); - assert_ok!(Proxy::proxy(RuntimeOrigin::signed(1), anon, None, call)); - System::assert_last_event(ProxyEvent::ProxyExecuted { result: Ok(()) }.into()); - assert_eq!(Balances::free_balance(6), 1); - - let call = Box::new(RuntimeCall::Proxy(ProxyCall::new_call_variant_kill_pure( - 1, - ProxyType::Any, - 0, - 1, - 0, - ))); - assert_ok!(Proxy::proxy( - RuntimeOrigin::signed(2), - anon2, - None, - call.clone() - )); - let de = DispatchError::from(Error::::NoPermission).stripped(); - System::assert_last_event(ProxyEvent::ProxyExecuted { result: Err(de) }.into()); - assert_noop!( - Proxy::kill_pure(RuntimeOrigin::signed(1), 1, ProxyType::Any, 0, 1, 0), - Error::::NoPermission - ); - assert_eq!(Balances::free_balance(1), 1); - assert_ok!(Proxy::proxy( - RuntimeOrigin::signed(1), - anon, - None, - call.clone() - )); - assert_eq!(Balances::free_balance(1), 3); - assert_noop!( - Proxy::proxy(RuntimeOrigin::signed(1), anon, None, call.clone()), - Error::::NotProxy - ); - }); -} - -#[test] -fn poke_deposit_works_for_proxy_deposits() { - new_test_ext().execute_with(|| { - // Add a proxy and check initial deposit - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(1), - 2, - ProxyType::Any, - 0 - )); - assert_eq!(Balances::reserved_balance(1), 2); // Base(1) + Factor(1) * 1 - - // Change the proxy deposit base to trigger deposit update - ProxyDepositBase::set(2); - let result = Proxy::poke_deposit(RuntimeOrigin::signed(1)); - assert_ok!(result.as_ref()); - assert_eq!(result.unwrap().pays_fee, Pays::No); - assert_eq!(Balances::reserved_balance(1), 3); // New Base(2) + Factor(1) * 1 - System::assert_last_event( - ProxyEvent::DepositPoked { - who: 1, - kind: DepositKind::Proxies, - old_deposit: 2, - new_deposit: 3, - } - .into(), - ); - assert!(System::events().iter().any(|record| matches!( - record.event, - RuntimeEvent::Proxy(Event::DepositPoked { .. }) - ))); - }); -} - -#[test] -fn poke_deposit_works_for_announcement_deposits() { - new_test_ext().execute_with(|| { - // Setup proxy and make announcement - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(1), - 3, - ProxyType::Any, - 1 - )); - assert_eq!(Balances::reserved_balance(1), 2); // Base(1) + Factor(1) * 1 - assert_ok!(Proxy::announce(RuntimeOrigin::signed(3), 1, [1; 32].into())); - let announcements = Announcements::::get(3); - assert_eq!( - announcements.0, - vec![Announcement { - real: 1, - call_hash: [1; 32].into(), - height: 1 - }] - ); - assert_eq!(Balances::reserved_balance(3), announcements.1); - let initial_deposit = Balances::reserved_balance(3); - - // Change announcement deposit base to trigger update - AnnouncementDepositBase::set(2); - let result = Proxy::poke_deposit(RuntimeOrigin::signed(3)); - assert_ok!(result.as_ref()); - assert_eq!(result.unwrap().pays_fee, Pays::No); - let new_deposit = initial_deposit.saturating_add(1); // Base increased by 1 - assert_eq!(Balances::reserved_balance(3), new_deposit); - System::assert_last_event( - ProxyEvent::DepositPoked { - who: 3, - kind: DepositKind::Announcements, - old_deposit: initial_deposit, - new_deposit, - } - .into(), - ); - assert!(System::events().iter().any(|record| matches!( - record.event, - RuntimeEvent::Proxy(Event::DepositPoked { .. }) - ))); - }); -} - -#[test] -fn poke_deposit_charges_fee_when_deposit_unchanged() { - new_test_ext().execute_with(|| { - // Add a proxy and check initial deposit - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(1), - 3, - ProxyType::Any, - 0 - )); - assert_eq!(Balances::reserved_balance(1), 2); // Base(1) + Factor(1) * 1 - - // Poke the deposit without changing deposit required and check fee - let result = Proxy::poke_deposit(RuntimeOrigin::signed(1)); - assert_ok!(result.as_ref()); - assert_eq!(result.unwrap().pays_fee, Pays::Yes); // Pays fee - assert_eq!(Balances::reserved_balance(1), 2); // No change - - // No event emitted - assert!(!System::events().iter().any(|record| matches!( - record.event, - RuntimeEvent::Proxy(Event::DepositPoked { .. }) - ))); - - // Add an announcement and check initial deposit - assert_ok!(Proxy::announce(RuntimeOrigin::signed(3), 1, [1; 32].into())); - let announcements = Announcements::::get(3); - assert_eq!( - announcements.0, - vec![Announcement { - real: 1, - call_hash: [1; 32].into(), - height: 1 - }] - ); - assert_eq!(Balances::reserved_balance(3), announcements.1); - let initial_deposit = Balances::reserved_balance(3); - - // Poke the deposit without changing deposit required and check fee - let result = Proxy::poke_deposit(RuntimeOrigin::signed(3)); - assert_ok!(result.as_ref()); - assert_eq!(result.unwrap().pays_fee, Pays::Yes); // Pays fee - assert_eq!(Balances::reserved_balance(3), initial_deposit); // No change - - // No event emitted - assert!(!System::events().iter().any(|record| matches!( - record.event, - RuntimeEvent::Proxy(Event::DepositPoked { .. }) - ))); - }); -} - -#[test] -fn poke_deposit_handles_insufficient_balance() { - new_test_ext().execute_with(|| { - // Setup with account that has minimal balance - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(5), - 3, - ProxyType::Any, - 0 - )); - let initial_deposit = Balances::reserved_balance(5); - - // Change deposit base to require more than available balance - ProxyDepositBase::set(10); - - // Poking should fail due to insufficient balance - assert_noop!( - Proxy::poke_deposit(RuntimeOrigin::signed(5)), - BalancesError::::InsufficientBalance, - ); - - // Original deposit should remain unchanged - assert_eq!(Balances::reserved_balance(5), initial_deposit); - }); -} - -#[test] -fn poke_deposit_updates_both_proxy_and_announcement_deposits() { - new_test_ext().execute_with(|| { - // Setup both proxy and announcement for the same account - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(1), - 2, - ProxyType::Any, - 0 - )); - assert_eq!(Balances::reserved_balance(1), 2); // Base(1) + Factor(1) * 1 - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(2), - 3, - ProxyType::Any, - 1 - )); - assert_eq!(Balances::reserved_balance(2), 2); // Base(1) + Factor(1) * 1 - assert_ok!(Proxy::announce(RuntimeOrigin::signed(2), 1, [1; 32].into())); - let announcements = Announcements::::get(2); - assert_eq!( - announcements.0, - vec![Announcement { - real: 1, - call_hash: [1; 32].into(), - height: 1 - }] - ); - assert_eq!(announcements.1, 2); // Base(1) + Factor(1) * 1 - - // Record initial deposits - let initial_proxy_deposit = Proxies::::get(2).1; - let initial_announcement_deposit = Announcements::::get(2).1; - - // Total reserved = deposit for proxy + deposit for announcement - assert_eq!( - Balances::reserved_balance(2), - initial_proxy_deposit.saturating_add(initial_announcement_deposit) - ); - - // Change both deposit requirements - ProxyDepositBase::set(2); - AnnouncementDepositBase::set(2); - - // Poke deposits - should update both deposits and emit two events - let result = Proxy::poke_deposit(RuntimeOrigin::signed(2)); - assert_ok!(result.as_ref()); - assert_eq!(result.unwrap().pays_fee, Pays::No); - - // Check both deposits were updated - let (_, new_proxy_deposit) = Proxies::::get(2); - let (_, new_announcement_deposit) = Announcements::::get(2); - assert_eq!(new_proxy_deposit, 3); // Base(2) + Factor(1) * 1 - assert_eq!(new_announcement_deposit, 3); // Base(2) + Factor(1) * 1 - assert_eq!( - Balances::reserved_balance(2), - new_proxy_deposit.saturating_add(new_announcement_deposit) - ); - - // Verify both events were emitted in the correct order - let events = System::events(); - let relevant_events: Vec<_> = events - .iter() - .filter(|record| { - matches!( - record.event, - RuntimeEvent::Proxy(ProxyEvent::DepositPoked { .. }) - ) - }) - .collect(); - - assert_eq!(relevant_events.len(), 2); - - // First event should be for Proxies - assert_eq!( - relevant_events[0].event, - ProxyEvent::DepositPoked { - who: 2, - kind: DepositKind::Proxies, - old_deposit: initial_proxy_deposit, - new_deposit: new_proxy_deposit, - } - .into() - ); - - // Second event should be for Announcements - assert_eq!( - relevant_events[1].event, - ProxyEvent::DepositPoked { - who: 2, - kind: DepositKind::Announcements, - old_deposit: initial_announcement_deposit, - new_deposit: new_announcement_deposit, - } - .into() - ); - - // Poking again should charge fee as nothing changes - let result = Proxy::poke_deposit(RuntimeOrigin::signed(2)); - assert_ok!(result.as_ref()); - assert_eq!(result.unwrap().pays_fee, Pays::Yes); - - // Verify deposits remained the same - assert_eq!(Proxies::::get(2).1, new_proxy_deposit); - assert_eq!(Announcements::::get(2).1, new_announcement_deposit); - assert_eq!( - Balances::reserved_balance(2), - new_proxy_deposit.saturating_add(new_announcement_deposit) - ); - }); -} - -#[test] -fn poke_deposit_fails_for_unsigned_origin() { - new_test_ext().execute_with(|| { - assert_noop!( - Proxy::poke_deposit(RuntimeOrigin::none()), - DispatchError::BadOrigin, - ); - }); -} - -#[test] -fn set_real_pays_fee_works() { - new_test_ext().execute_with(|| { - // Account 1 adds account 3 as proxy - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(1), - 3, - ProxyType::Any, - 0 - )); - - // Account 1 (real) enables real-pays-fee for delegate 3 - assert_ok!(Proxy::set_real_pays_fee(RuntimeOrigin::signed(1), 3, true)); - assert!(Proxy::is_real_pays_fee(&1, &3)); - System::assert_last_event( - ProxyEvent::RealPaysFeeSet { - real: 1, - delegate: 3, - pays_fee: true, - } - .into(), - ); - - // Disable it - assert_ok!(Proxy::set_real_pays_fee(RuntimeOrigin::signed(1), 3, false)); - assert!(!Proxy::is_real_pays_fee(&1, &3)); - System::assert_last_event( - ProxyEvent::RealPaysFeeSet { - real: 1, - delegate: 3, - pays_fee: false, - } - .into(), - ); - }); -} - -#[test] -fn set_real_pays_fee_fails_without_proxy() { - new_test_ext().execute_with(|| { - // No proxy relationship between 1 and 3 - assert_noop!( - Proxy::set_real_pays_fee(RuntimeOrigin::signed(1), 3, true), - Error::::NotProxy, - ); - }); -} - -#[test] -fn set_real_pays_fee_fails_unsigned() { - new_test_ext().execute_with(|| { - assert_noop!( - Proxy::set_real_pays_fee(RuntimeOrigin::none(), 3, true), - DispatchError::BadOrigin, - ); - }); -} - -#[test] -fn set_real_pays_fee_fails_root() { - new_test_ext().execute_with(|| { - assert_noop!( - Proxy::set_real_pays_fee(RuntimeOrigin::root(), 3, true), - DispatchError::BadOrigin, - ); - }); -} - -#[test] -fn real_pays_fee_cleaned_on_remove_proxy() { - new_test_ext().execute_with(|| { - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(1), - 3, - ProxyType::Any, - 0 - )); - assert_ok!(Proxy::set_real_pays_fee(RuntimeOrigin::signed(1), 3, true)); - assert!(Proxy::is_real_pays_fee(&1, &3)); - - // Remove the proxy - assert_ok!(Proxy::remove_proxy( - RuntimeOrigin::signed(1), - 3, - ProxyType::Any, - 0 - )); - - // Flag should be cleaned up - assert!(!Proxy::is_real_pays_fee(&1, &3)); - }); -} - -#[test] -fn real_pays_fee_cleaned_on_remove_proxies() { - new_test_ext().execute_with(|| { - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(1), - 2, - ProxyType::Any, - 0 - )); - assert_ok!(Proxy::add_proxy( - RuntimeOrigin::signed(1), - 3, - ProxyType::Any, - 0 - )); - assert_ok!(Proxy::set_real_pays_fee(RuntimeOrigin::signed(1), 2, true)); - assert_ok!(Proxy::set_real_pays_fee(RuntimeOrigin::signed(1), 3, true)); - assert!(Proxy::is_real_pays_fee(&1, &2)); - assert!(Proxy::is_real_pays_fee(&1, &3)); - - // Remove all proxies - assert_ok!(Proxy::remove_proxies(RuntimeOrigin::signed(1))); - - // Both flags should be cleaned up - assert!(!Proxy::is_real_pays_fee(&1, &2)); - assert!(!Proxy::is_real_pays_fee(&1, &3)); - }); -} diff --git a/pallets/proxy/src/tests/announcements.rs b/pallets/proxy/src/tests/announcements.rs new file mode 100644 index 0000000000..5db1cd0581 --- /dev/null +++ b/pallets/proxy/src/tests/announcements.rs @@ -0,0 +1,274 @@ +//! Announcement delay / announce / remove / reject / proxy_announced tests. + +use super::mock::*; +use crate::*; +use alloc::{boxed::Box, vec}; +use frame::testing_prelude::*; + +#[test] +fn announcement_works() { + new_test_ext().execute_with(|| { + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(1), + 3, + ProxyType::Any, + 1 + )); + System::assert_last_event( + ProxyEvent::ProxyAdded { + delegator: 1, + delegatee: 3, + proxy_type: ProxyType::Any, + delay: 1, + } + .into(), + ); + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(2), + 3, + ProxyType::Any, + 1 + )); + assert_eq!(Balances::reserved_balance(3), 0); + + assert_ok!(Proxy::announce(RuntimeOrigin::signed(3), 1, [1; 32].into())); + let announcements = Announcements::::get(3); + assert_eq!( + announcements.0, + vec![Announcement { + real: 1, + call_hash: [1; 32].into(), + height: 1 + }] + ); + assert_eq!(Balances::reserved_balance(3), announcements.1); + + assert_ok!(Proxy::announce(RuntimeOrigin::signed(3), 2, [2; 32].into())); + let announcements = Announcements::::get(3); + assert_eq!( + announcements.0, + vec![ + Announcement { + real: 1, + call_hash: [1; 32].into(), + height: 1 + }, + Announcement { + real: 2, + call_hash: [2; 32].into(), + height: 1 + }, + ] + ); + assert_eq!(Balances::reserved_balance(3), announcements.1); + + assert_noop!( + Proxy::announce(RuntimeOrigin::signed(3), 2, [3; 32].into()), + Error::::TooMany + ); + }); +} + +#[test] +fn remove_announcement_works() { + new_test_ext().execute_with(|| { + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(1), + 3, + ProxyType::Any, + 1 + )); + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(2), + 3, + ProxyType::Any, + 1 + )); + assert_ok!(Proxy::announce(RuntimeOrigin::signed(3), 1, [1; 32].into())); + assert_ok!(Proxy::announce(RuntimeOrigin::signed(3), 2, [2; 32].into())); + let e = Error::::NotFound; + assert_noop!( + Proxy::remove_announcement(RuntimeOrigin::signed(3), 1, [0; 32].into()), + e + ); + assert_ok!(Proxy::remove_announcement( + RuntimeOrigin::signed(3), + 1, + [1; 32].into() + )); + let announcements = Announcements::::get(3); + assert_eq!( + announcements.0, + vec![Announcement { + real: 2, + call_hash: [2; 32].into(), + height: 1 + }] + ); + assert_eq!(Balances::reserved_balance(3), announcements.1); + }); +} + +#[test] +fn reject_announcement_works() { + new_test_ext().execute_with(|| { + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(1), + 3, + ProxyType::Any, + 1 + )); + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(2), + 3, + ProxyType::Any, + 1 + )); + assert_ok!(Proxy::announce(RuntimeOrigin::signed(3), 1, [1; 32].into())); + assert_ok!(Proxy::announce(RuntimeOrigin::signed(3), 2, [2; 32].into())); + let e = Error::::NotFound; + assert_noop!( + Proxy::reject_announcement(RuntimeOrigin::signed(1), 3, [0; 32].into()), + e + ); + let e = Error::::NotFound; + assert_noop!( + Proxy::reject_announcement(RuntimeOrigin::signed(4), 3, [1; 32].into()), + e + ); + assert_ok!(Proxy::reject_announcement( + RuntimeOrigin::signed(1), + 3, + [1; 32].into() + )); + let announcements = Announcements::::get(3); + assert_eq!( + announcements.0, + vec![Announcement { + real: 2, + call_hash: [2; 32].into(), + height: 1 + }] + ); + assert_eq!(Balances::reserved_balance(3), announcements.1); + }); +} + +#[test] +fn announcer_must_be_proxy() { + new_test_ext().execute_with(|| { + assert_noop!( + Proxy::announce(RuntimeOrigin::signed(2), 1, H256::zero()), + Error::::NotProxy + ); + }); +} + +#[test] +fn calling_proxy_doesnt_remove_announcement() { + new_test_ext().execute_with(|| { + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(1), + 2, + ProxyType::Any, + 0 + )); + + let call = Box::new(call_transfer(6, 1)); + let call_hash = BlakeTwo256::hash_of(&call); + + assert_ok!(Proxy::announce(RuntimeOrigin::signed(2), 1, call_hash)); + assert_ok!(Proxy::proxy(RuntimeOrigin::signed(2), 1, None, call)); + + // The announcement is not removed by calling proxy. + let announcements = Announcements::::get(2); + assert_eq!( + announcements.0, + vec![Announcement { + real: 1, + call_hash, + height: 1 + }] + ); + }); +} + +#[test] +fn delayed_requires_pre_announcement() { + new_test_ext().execute_with(|| { + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(1), + 2, + ProxyType::Any, + 1 + )); + let call = Box::new(call_transfer(6, 1)); + let e = Error::::Unannounced; + assert_noop!( + Proxy::proxy(RuntimeOrigin::signed(2), 1, None, call.clone()), + e + ); + let e = Error::::Unannounced; + assert_noop!( + Proxy::proxy_announced(RuntimeOrigin::signed(0), 2, 1, None, call.clone()), + e + ); + let call_hash = BlakeTwo256::hash_of(&call); + assert_ok!(Proxy::announce(RuntimeOrigin::signed(2), 1, call_hash)); + frame_system::Pallet::::set_block_number(2); + assert_ok!(Proxy::proxy_announced( + RuntimeOrigin::signed(0), + 2, + 1, + None, + call.clone() + )); + }); +} + +#[test] +fn proxy_announced_removes_announcement_and_returns_deposit() { + new_test_ext().execute_with(|| { + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(1), + 3, + ProxyType::Any, + 1 + )); + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(2), + 3, + ProxyType::Any, + 1 + )); + let call = Box::new(call_transfer(6, 1)); + let call_hash = BlakeTwo256::hash_of(&call); + assert_ok!(Proxy::announce(RuntimeOrigin::signed(3), 1, call_hash)); + assert_ok!(Proxy::announce(RuntimeOrigin::signed(3), 2, call_hash)); + // Too early to execute announced call + let e = Error::::Unannounced; + assert_noop!( + Proxy::proxy_announced(RuntimeOrigin::signed(0), 3, 1, None, call.clone()), + e + ); + + frame_system::Pallet::::set_block_number(2); + assert_ok!(Proxy::proxy_announced( + RuntimeOrigin::signed(0), + 3, + 1, + None, + call.clone() + )); + let announcements = Announcements::::get(3); + assert_eq!( + announcements.0, + vec![Announcement { + real: 2, + call_hash, + height: 1 + }] + ); + assert_eq!(Balances::reserved_balance(3), announcements.1); + }); +} diff --git a/pallets/proxy/src/tests/mock.rs b/pallets/proxy/src/tests/mock.rs new file mode 100644 index 0000000000..999587c9ec --- /dev/null +++ b/pallets/proxy/src/tests/mock.rs @@ -0,0 +1,161 @@ +//! Test runtime and helpers for the proxy pallet. + +#![allow( + clippy::arithmetic_side_effects, + clippy::unwrap_used, + clippy::indexing_slicing +)] + +use crate as proxy; +use crate::*; +use alloc::{vec, vec::Vec}; +use frame::testing_prelude::*; +use frame_system::Call as SystemCall; +use pallet_balances::Call as BalancesCall; +use pallet_subtensor_utility as pallet_utility; + +type Block = frame_system::mocking::MockBlock; + +construct_runtime!( + pub enum Test { + System: frame_system = 1, + Balances: pallet_balances = 2, + Proxy: proxy = 3, + Utility: pallet_utility = 4, + } +); + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type Block = Block; + type BaseCallFilter = BaseFilter; + type AccountData = pallet_balances::AccountData; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Test { + type ReserveIdentifier = [u8; 8]; + type AccountStore = System; +} + +impl pallet_utility::Config for Test { + type RuntimeCall = RuntimeCall; + type PalletsOrigin = OriginCaller; + type WeightInfo = (); +} + +/// Proxy permission kinds used by unit tests (`Any` is the most permissive / default). +#[derive( + Copy, + Clone, + Eq, + PartialEq, + Ord, + PartialOrd, + Encode, + Decode, + DecodeWithMemTracking, + RuntimeDebug, + MaxEncodedLen, + scale_info::TypeInfo, +)] +pub enum ProxyType { + Any, + JustTransfer, + JustUtility, +} +impl Default for ProxyType { + fn default() -> Self { + Self::Any + } +} +impl frame::traits::InstanceFilter for ProxyType { + fn filter(&self, c: &RuntimeCall) -> bool { + match self { + ProxyType::Any => true, + ProxyType::JustTransfer => { + matches!( + c, + RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { .. }) + ) + } + ProxyType::JustUtility => matches!(c, RuntimeCall::Utility { .. }), + } + } + fn is_superset(&self, o: &Self) -> bool { + self == &ProxyType::Any || self == o + } +} + +/// Base call filter for the test runtime (blocks most `System` calls except `remark`). +pub struct BaseFilter; +impl Contains for BaseFilter { + fn contains(c: &RuntimeCall) -> bool { + match *c { + // Remark is used as a no-op call in the benchmarking + RuntimeCall::System(SystemCall::remark { .. }) => true, + RuntimeCall::System(_) => false, + _ => true, + } + } +} + +parameter_types! { + pub static ProxyDepositBase: u64 = 1; + pub static ProxyDepositFactor: u64 = 1; + pub static AnnouncementDepositBase: u64 = 1; + pub static AnnouncementDepositFactor: u64 = 1; +} + +impl Config for Test { + type RuntimeCall = RuntimeCall; + type Currency = Balances; + type ProxyType = ProxyType; + type ProxyDepositBase = ProxyDepositBase; + type ProxyDepositFactor = ProxyDepositFactor; + type MaxProxies = ConstU32<4>; + type WeightInfo = (); + type CallHasher = BlakeTwo256; + type MaxPending = ConstU32<2>; + type AnnouncementDepositBase = AnnouncementDepositBase; + type AnnouncementDepositFactor = AnnouncementDepositFactor; + type BlockNumberProvider = frame_system::Pallet; +} + +pub use crate::{Call as ProxyCall, Event as ProxyEvent}; + +pub type SystemError = frame_system::Error; + +/// Build a test externalities with funded accounts `(1..=4)=10` and `5=3`. +pub fn new_test_ext() -> TestState { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .unwrap(); + pallet_balances::GenesisConfig:: { + balances: vec![(1, 10), (2, 10), (3, 10), (4, 10), (5, 3)], + ..Default::default() + } + .assimilate_storage(&mut t) + .unwrap(); + let mut ext = TestState::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +pub fn last_events(n: usize) -> Vec { + frame_system::Pallet::::events() + .into_iter() + .rev() + .take(n) + .rev() + .map(|e| e.event) + .collect() +} + +pub fn expect_events(e: Vec) { + assert_eq!(last_events(e.len()), e); +} + +pub fn call_transfer(dest: u64, value: u64) -> RuntimeCall { + RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest, value }) +} diff --git a/pallets/proxy/src/tests/mod.rs b/pallets/proxy/src/tests/mod.rs new file mode 100644 index 0000000000..9350594865 --- /dev/null +++ b/pallets/proxy/src/tests/mod.rs @@ -0,0 +1,20 @@ +//! Proxy pallet unit tests, split by concept for discoverability. + +#![cfg(test)] +#![allow( + clippy::arithmetic_side_effects, + clippy::unwrap_used, + clippy::indexing_slicing +)] + +mod mock; + +mod announcements; +mod poke_deposit; +mod proxy_lifecycle; +mod proxy_type_filter; +mod pure_proxy; +mod real_pays_fee; + +#[allow(unused_imports)] // used by `impl_benchmark_test_suite!` in benchmarking.rs +pub use mock::{Test, new_test_ext}; diff --git a/pallets/proxy/src/tests/poke_deposit.rs b/pallets/proxy/src/tests/poke_deposit.rs new file mode 100644 index 0000000000..53960d2e07 --- /dev/null +++ b/pallets/proxy/src/tests/poke_deposit.rs @@ -0,0 +1,288 @@ +//! poke_deposit deposit recomputation and fee-waiver tests. + +use super::mock::*; +use crate::*; +use alloc::vec; +use frame::testing_prelude::*; +use pallet_balances::Error as BalancesError; + +#[test] +fn poke_deposit_works_for_proxy_deposits() { + new_test_ext().execute_with(|| { + // Add a proxy and check initial deposit + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(1), + 2, + ProxyType::Any, + 0 + )); + assert_eq!(Balances::reserved_balance(1), 2); // Base(1) + Factor(1) * 1 + + // Change the proxy deposit base to trigger deposit update + ProxyDepositBase::set(2); + let result = Proxy::poke_deposit(RuntimeOrigin::signed(1)); + assert_ok!(result.as_ref()); + assert_eq!(result.unwrap().pays_fee, Pays::No); + assert_eq!(Balances::reserved_balance(1), 3); // New Base(2) + Factor(1) * 1 + System::assert_last_event( + ProxyEvent::DepositPoked { + who: 1, + kind: DepositKind::Proxies, + old_deposit: 2, + new_deposit: 3, + } + .into(), + ); + assert!(System::events().iter().any(|record| matches!( + record.event, + RuntimeEvent::Proxy(Event::DepositPoked { .. }) + ))); + }); +} + +#[test] +fn poke_deposit_works_for_announcement_deposits() { + new_test_ext().execute_with(|| { + // Setup proxy and make announcement + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(1), + 3, + ProxyType::Any, + 1 + )); + assert_eq!(Balances::reserved_balance(1), 2); // Base(1) + Factor(1) * 1 + assert_ok!(Proxy::announce(RuntimeOrigin::signed(3), 1, [1; 32].into())); + let announcements = Announcements::::get(3); + assert_eq!( + announcements.0, + vec![Announcement { + real: 1, + call_hash: [1; 32].into(), + height: 1 + }] + ); + assert_eq!(Balances::reserved_balance(3), announcements.1); + let initial_deposit = Balances::reserved_balance(3); + + // Change announcement deposit base to trigger update + AnnouncementDepositBase::set(2); + let result = Proxy::poke_deposit(RuntimeOrigin::signed(3)); + assert_ok!(result.as_ref()); + assert_eq!(result.unwrap().pays_fee, Pays::No); + let new_deposit = initial_deposit.saturating_add(1); // Base increased by 1 + assert_eq!(Balances::reserved_balance(3), new_deposit); + System::assert_last_event( + ProxyEvent::DepositPoked { + who: 3, + kind: DepositKind::Announcements, + old_deposit: initial_deposit, + new_deposit, + } + .into(), + ); + assert!(System::events().iter().any(|record| matches!( + record.event, + RuntimeEvent::Proxy(Event::DepositPoked { .. }) + ))); + }); +} + +#[test] +fn poke_deposit_charges_fee_when_deposit_unchanged() { + new_test_ext().execute_with(|| { + // Add a proxy and check initial deposit + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(1), + 3, + ProxyType::Any, + 0 + )); + assert_eq!(Balances::reserved_balance(1), 2); // Base(1) + Factor(1) * 1 + + // Poke the deposit without changing deposit required and check fee + let result = Proxy::poke_deposit(RuntimeOrigin::signed(1)); + assert_ok!(result.as_ref()); + assert_eq!(result.unwrap().pays_fee, Pays::Yes); // Pays fee + assert_eq!(Balances::reserved_balance(1), 2); // No change + + // No event emitted + assert!(!System::events().iter().any(|record| matches!( + record.event, + RuntimeEvent::Proxy(Event::DepositPoked { .. }) + ))); + + // Add an announcement and check initial deposit + assert_ok!(Proxy::announce(RuntimeOrigin::signed(3), 1, [1; 32].into())); + let announcements = Announcements::::get(3); + assert_eq!( + announcements.0, + vec![Announcement { + real: 1, + call_hash: [1; 32].into(), + height: 1 + }] + ); + assert_eq!(Balances::reserved_balance(3), announcements.1); + let initial_deposit = Balances::reserved_balance(3); + + // Poke the deposit without changing deposit required and check fee + let result = Proxy::poke_deposit(RuntimeOrigin::signed(3)); + assert_ok!(result.as_ref()); + assert_eq!(result.unwrap().pays_fee, Pays::Yes); // Pays fee + assert_eq!(Balances::reserved_balance(3), initial_deposit); // No change + + // No event emitted + assert!(!System::events().iter().any(|record| matches!( + record.event, + RuntimeEvent::Proxy(Event::DepositPoked { .. }) + ))); + }); +} + +#[test] +fn poke_deposit_handles_insufficient_balance() { + new_test_ext().execute_with(|| { + // Setup with account that has minimal balance + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(5), + 3, + ProxyType::Any, + 0 + )); + let initial_deposit = Balances::reserved_balance(5); + + // Change deposit base to require more than available balance + ProxyDepositBase::set(10); + + // Poking should fail due to insufficient balance + assert_noop!( + Proxy::poke_deposit(RuntimeOrigin::signed(5)), + BalancesError::::InsufficientBalance, + ); + + // Original deposit should remain unchanged + assert_eq!(Balances::reserved_balance(5), initial_deposit); + }); +} + +#[test] +fn poke_deposit_updates_both_proxy_and_announcement_deposits() { + new_test_ext().execute_with(|| { + // Setup both proxy and announcement for the same account + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(1), + 2, + ProxyType::Any, + 0 + )); + assert_eq!(Balances::reserved_balance(1), 2); // Base(1) + Factor(1) * 1 + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(2), + 3, + ProxyType::Any, + 1 + )); + assert_eq!(Balances::reserved_balance(2), 2); // Base(1) + Factor(1) * 1 + assert_ok!(Proxy::announce(RuntimeOrigin::signed(2), 1, [1; 32].into())); + let announcements = Announcements::::get(2); + assert_eq!( + announcements.0, + vec![Announcement { + real: 1, + call_hash: [1; 32].into(), + height: 1 + }] + ); + assert_eq!(announcements.1, 2); // Base(1) + Factor(1) * 1 + + // Record initial deposits + let initial_proxy_deposit = Proxies::::get(2).1; + let initial_announcement_deposit = Announcements::::get(2).1; + + // Total reserved = deposit for proxy + deposit for announcement + assert_eq!( + Balances::reserved_balance(2), + initial_proxy_deposit.saturating_add(initial_announcement_deposit) + ); + + // Change both deposit requirements + ProxyDepositBase::set(2); + AnnouncementDepositBase::set(2); + + // Poke deposits - should update both deposits and emit two events + let result = Proxy::poke_deposit(RuntimeOrigin::signed(2)); + assert_ok!(result.as_ref()); + assert_eq!(result.unwrap().pays_fee, Pays::No); + + // Check both deposits were updated + let (_, new_proxy_deposit) = Proxies::::get(2); + let (_, new_announcement_deposit) = Announcements::::get(2); + assert_eq!(new_proxy_deposit, 3); // Base(2) + Factor(1) * 1 + assert_eq!(new_announcement_deposit, 3); // Base(2) + Factor(1) * 1 + assert_eq!( + Balances::reserved_balance(2), + new_proxy_deposit.saturating_add(new_announcement_deposit) + ); + + // Verify both events were emitted in the correct order + let events = System::events(); + let relevant_events: Vec<_> = events + .iter() + .filter(|record| { + matches!( + record.event, + RuntimeEvent::Proxy(ProxyEvent::DepositPoked { .. }) + ) + }) + .collect(); + + assert_eq!(relevant_events.len(), 2); + + // First event should be for Proxies + assert_eq!( + relevant_events[0].event, + ProxyEvent::DepositPoked { + who: 2, + kind: DepositKind::Proxies, + old_deposit: initial_proxy_deposit, + new_deposit: new_proxy_deposit, + } + .into() + ); + + // Second event should be for Announcements + assert_eq!( + relevant_events[1].event, + ProxyEvent::DepositPoked { + who: 2, + kind: DepositKind::Announcements, + old_deposit: initial_announcement_deposit, + new_deposit: new_announcement_deposit, + } + .into() + ); + + // Poking again should charge fee as nothing changes + let result = Proxy::poke_deposit(RuntimeOrigin::signed(2)); + assert_ok!(result.as_ref()); + assert_eq!(result.unwrap().pays_fee, Pays::Yes); + + // Verify deposits remained the same + assert_eq!(Proxies::::get(2).1, new_proxy_deposit); + assert_eq!(Announcements::::get(2).1, new_announcement_deposit); + assert_eq!( + Balances::reserved_balance(2), + new_proxy_deposit.saturating_add(new_announcement_deposit) + ); + }); +} + +#[test] +fn poke_deposit_fails_for_unsigned_origin() { + new_test_ext().execute_with(|| { + assert_noop!( + Proxy::poke_deposit(RuntimeOrigin::none()), + DispatchError::BadOrigin, + ); + }); +} diff --git a/pallets/proxy/src/tests/proxy_lifecycle.rs b/pallets/proxy/src/tests/proxy_lifecycle.rs new file mode 100644 index 0000000000..e54cbcd759 --- /dev/null +++ b/pallets/proxy/src/tests/proxy_lifecycle.rs @@ -0,0 +1,217 @@ +//! add_proxy / remove_proxy / deposit and basic `proxy` dispatch tests. + +use super::mock::*; +use crate::*; +use alloc::boxed::Box; +use frame::testing_prelude::*; +use frame_system::Call as SystemCall; +use pallet_balances::Call as BalancesCall; + +#[test] +fn add_remove_proxies_works() { + new_test_ext().execute_with(|| { + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(1), + 2, + ProxyType::Any, + 0 + )); + assert_noop!( + Proxy::add_proxy(RuntimeOrigin::signed(1), 2, ProxyType::Any, 0), + Error::::Duplicate + ); + assert_eq!(Balances::reserved_balance(1), 2); + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(1), + 2, + ProxyType::JustTransfer, + 0 + )); + assert_eq!(Balances::reserved_balance(1), 3); + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(1), + 3, + ProxyType::Any, + 0 + )); + assert_eq!(Balances::reserved_balance(1), 4); + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(1), + 4, + ProxyType::JustUtility, + 0 + )); + assert_eq!(Balances::reserved_balance(1), 5); + assert_noop!( + Proxy::add_proxy(RuntimeOrigin::signed(1), 4, ProxyType::Any, 0), + Error::::TooMany + ); + assert_noop!( + Proxy::remove_proxy(RuntimeOrigin::signed(1), 3, ProxyType::JustTransfer, 0), + Error::::NotFound + ); + assert_ok!(Proxy::remove_proxy( + RuntimeOrigin::signed(1), + 4, + ProxyType::JustUtility, + 0 + )); + System::assert_last_event( + ProxyEvent::ProxyRemoved { + delegator: 1, + delegatee: 4, + proxy_type: ProxyType::JustUtility, + delay: 0, + } + .into(), + ); + assert_eq!(Balances::reserved_balance(1), 4); + assert_ok!(Proxy::remove_proxy( + RuntimeOrigin::signed(1), + 3, + ProxyType::Any, + 0 + )); + assert_eq!(Balances::reserved_balance(1), 3); + System::assert_last_event( + ProxyEvent::ProxyRemoved { + delegator: 1, + delegatee: 3, + proxy_type: ProxyType::Any, + delay: 0, + } + .into(), + ); + assert_ok!(Proxy::remove_proxy( + RuntimeOrigin::signed(1), + 2, + ProxyType::Any, + 0 + )); + assert_eq!(Balances::reserved_balance(1), 2); + System::assert_last_event( + ProxyEvent::ProxyRemoved { + delegator: 1, + delegatee: 2, + proxy_type: ProxyType::Any, + delay: 0, + } + .into(), + ); + assert_ok!(Proxy::remove_proxy( + RuntimeOrigin::signed(1), + 2, + ProxyType::JustTransfer, + 0 + )); + assert_eq!(Balances::reserved_balance(1), 0); + System::assert_last_event( + ProxyEvent::ProxyRemoved { + delegator: 1, + delegatee: 2, + proxy_type: ProxyType::JustTransfer, + delay: 0, + } + .into(), + ); + assert_noop!( + Proxy::add_proxy(RuntimeOrigin::signed(1), 1, ProxyType::Any, 0), + Error::::NoSelfProxy + ); + }); +} + +#[test] +fn cannot_add_proxy_without_balance() { + new_test_ext().execute_with(|| { + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(5), + 3, + ProxyType::Any, + 0 + )); + assert_eq!(Balances::reserved_balance(5), 2); + assert_noop!( + Proxy::add_proxy(RuntimeOrigin::signed(5), 4, ProxyType::Any, 0), + DispatchError::ConsumerRemaining, + ); + }); +} + +#[test] +fn proxying_works() { + new_test_ext().execute_with(|| { + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(1), + 2, + ProxyType::JustTransfer, + 0 + )); + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(1), + 3, + ProxyType::Any, + 0 + )); + + let call = Box::new(call_transfer(6, 1)); + assert_noop!( + Proxy::proxy(RuntimeOrigin::signed(4), 1, None, call.clone()), + Error::::NotProxy + ); + assert_noop!( + Proxy::proxy( + RuntimeOrigin::signed(2), + 1, + Some(ProxyType::Any), + call.clone() + ), + Error::::NotProxy + ); + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(2), + 1, + None, + call.clone() + )); + System::assert_last_event(ProxyEvent::ProxyExecuted { result: Ok(()) }.into()); + assert_eq!(Balances::free_balance(6), 1); + + let call = Box::new(RuntimeCall::System(SystemCall::set_code { code: vec![] })); + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(3), + 1, + None, + call.clone() + )); + System::assert_last_event( + ProxyEvent::ProxyExecuted { + result: Err(SystemError::CallFiltered.into()), + } + .into(), + ); + + let call = Box::new(RuntimeCall::Balances(BalancesCall::transfer_keep_alive { + dest: 6, + value: 1, + })); + assert_ok!( + RuntimeCall::Proxy(ProxyCall::new_call_variant_proxy(1, None, call.clone())) + .dispatch(RuntimeOrigin::signed(2)) + ); + System::assert_last_event( + ProxyEvent::ProxyExecuted { + result: Err(SystemError::CallFiltered.into()), + } + .into(), + ); + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(3), + 1, + None, + call.clone() + )); + System::assert_last_event(ProxyEvent::ProxyExecuted { result: Ok(()) }.into()); + assert_eq!(Balances::free_balance(6), 2); + }); +} diff --git a/pallets/proxy/src/tests/proxy_type_filter.rs b/pallets/proxy/src/tests/proxy_type_filter.rs new file mode 100644 index 0000000000..9a244fcf03 --- /dev/null +++ b/pallets/proxy/src/tests/proxy_type_filter.rs @@ -0,0 +1,222 @@ +//! ProxyType InstanceFilter behavior through `proxy` (including nested utility). + +use super::mock::*; +use crate::*; +use alloc::{boxed::Box, vec}; +use frame::testing_prelude::*; +use pallet_balances::Event as BalancesEvent; +use pallet_subtensor_utility::{Call as UtilityCall, Event as UtilityEvent}; + +#[test] +fn filtering_works() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 1000); + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(1), + 2, + ProxyType::Any, + 0 + )); + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(1), + 3, + ProxyType::JustTransfer, + 0 + )); + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(1), + 4, + ProxyType::JustUtility, + 0 + )); + + let call = Box::new(call_transfer(6, 1)); + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(2), + 1, + None, + call.clone() + )); + System::assert_last_event(ProxyEvent::ProxyExecuted { result: Ok(()) }.into()); + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(3), + 1, + None, + call.clone() + )); + System::assert_last_event(ProxyEvent::ProxyExecuted { result: Ok(()) }.into()); + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(4), + 1, + None, + call.clone() + )); + System::assert_last_event( + ProxyEvent::ProxyExecuted { + result: Err(SystemError::CallFiltered.into()), + } + .into(), + ); + + let derivative_id = Utility::derivative_account_id(1, 0).unwrap(); + Balances::make_free_balance_be(&derivative_id, 1000); + let inner = Box::new(call_transfer(6, 1)); + + let call = Box::new(RuntimeCall::Utility(UtilityCall::as_derivative { + index: 0, + call: inner.clone(), + })); + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(2), + 1, + None, + call.clone() + )); + System::assert_last_event(ProxyEvent::ProxyExecuted { result: Ok(()) }.into()); + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(3), + 1, + None, + call.clone() + )); + System::assert_last_event( + ProxyEvent::ProxyExecuted { + result: Err(SystemError::CallFiltered.into()), + } + .into(), + ); + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(4), + 1, + None, + call.clone() + )); + System::assert_last_event( + ProxyEvent::ProxyExecuted { + result: Err(SystemError::CallFiltered.into()), + } + .into(), + ); + + let call = Box::new(RuntimeCall::Utility(UtilityCall::batch { + calls: vec![*inner], + })); + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(2), + 1, + None, + call.clone() + )); + expect_events(vec![ + UtilityEvent::BatchCompleted.into(), + ProxyEvent::ProxyExecuted { result: Ok(()) }.into(), + ]); + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(3), + 1, + None, + call.clone() + )); + System::assert_last_event( + ProxyEvent::ProxyExecuted { + result: Err(SystemError::CallFiltered.into()), + } + .into(), + ); + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(4), + 1, + None, + call.clone() + )); + expect_events(vec![ + UtilityEvent::BatchInterrupted { + index: 0, + error: SystemError::CallFiltered.into(), + } + .into(), + ProxyEvent::ProxyExecuted { result: Ok(()) }.into(), + ]); + + let inner = Box::new(RuntimeCall::Proxy(ProxyCall::new_call_variant_add_proxy( + 5, + ProxyType::Any, + 0, + ))); + let call = Box::new(RuntimeCall::Utility(UtilityCall::batch { + calls: vec![*inner], + })); + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(2), + 1, + None, + call.clone() + )); + expect_events(vec![ + UtilityEvent::BatchCompleted.into(), + ProxyEvent::ProxyExecuted { result: Ok(()) }.into(), + ]); + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(3), + 1, + None, + call.clone() + )); + System::assert_last_event( + ProxyEvent::ProxyExecuted { + result: Err(SystemError::CallFiltered.into()), + } + .into(), + ); + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(4), + 1, + None, + call.clone() + )); + expect_events(vec![ + UtilityEvent::BatchInterrupted { + index: 0, + error: SystemError::CallFiltered.into(), + } + .into(), + ProxyEvent::ProxyExecuted { result: Ok(()) }.into(), + ]); + + let call = Box::new(RuntimeCall::Proxy(ProxyCall::remove_proxies {})); + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(3), + 1, + None, + call.clone() + )); + System::assert_last_event( + ProxyEvent::ProxyExecuted { + result: Err(SystemError::CallFiltered.into()), + } + .into(), + ); + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(4), + 1, + None, + call.clone() + )); + System::assert_last_event( + ProxyEvent::ProxyExecuted { + result: Err(SystemError::CallFiltered.into()), + } + .into(), + ); + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(2), + 1, + None, + call.clone() + )); + expect_events(vec![ + BalancesEvent::::Unreserved { who: 1, amount: 5 }.into(), + ProxyEvent::ProxyExecuted { result: Ok(()) }.into(), + ]); + }); +} diff --git a/pallets/proxy/src/tests/pure_proxy.rs b/pallets/proxy/src/tests/pure_proxy.rs new file mode 100644 index 0000000000..b83cbb3efd --- /dev/null +++ b/pallets/proxy/src/tests/pure_proxy.rs @@ -0,0 +1,111 @@ +//! create_pure / kill_pure pure-account lifecycle tests. + +use super::mock::*; +use crate::*; +use alloc::boxed::Box; +use frame::testing_prelude::*; + +#[test] +fn pure_works() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 11); // An extra one for the ED. + assert_ok!(Proxy::create_pure( + RuntimeOrigin::signed(1), + ProxyType::Any, + 0, + 0 + )); + let anon = Proxy::pure_account(&1, &ProxyType::Any, 0, None).unwrap(); + System::assert_last_event( + ProxyEvent::PureCreated { + pure: anon, + who: 1, + proxy_type: ProxyType::Any, + disambiguation_index: 0, + } + .into(), + ); + + // other calls to pure allowed as long as they're not exactly the same. + assert_ok!(Proxy::create_pure( + RuntimeOrigin::signed(1), + ProxyType::JustTransfer, + 0, + 0 + )); + assert_ok!(Proxy::create_pure( + RuntimeOrigin::signed(1), + ProxyType::Any, + 0, + 1 + )); + let anon2 = Proxy::pure_account(&2, &ProxyType::Any, 0, None).unwrap(); + assert_ok!(Proxy::create_pure( + RuntimeOrigin::signed(2), + ProxyType::Any, + 0, + 0 + )); + assert_noop!( + Proxy::create_pure(RuntimeOrigin::signed(1), ProxyType::Any, 0, 0), + Error::::Duplicate + ); + System::set_extrinsic_index(1); + assert_ok!(Proxy::create_pure( + RuntimeOrigin::signed(1), + ProxyType::Any, + 0, + 0 + )); + System::set_extrinsic_index(0); + System::set_block_number(2); + assert_ok!(Proxy::create_pure( + RuntimeOrigin::signed(1), + ProxyType::Any, + 0, + 0 + )); + + let call = Box::new(call_transfer(6, 1)); + assert_ok!(Balances::transfer_allow_death( + RuntimeOrigin::signed(3), + anon, + 5 + )); + assert_ok!(Proxy::proxy(RuntimeOrigin::signed(1), anon, None, call)); + System::assert_last_event(ProxyEvent::ProxyExecuted { result: Ok(()) }.into()); + assert_eq!(Balances::free_balance(6), 1); + + let call = Box::new(RuntimeCall::Proxy(ProxyCall::new_call_variant_kill_pure( + 1, + ProxyType::Any, + 0, + 1, + 0, + ))); + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(2), + anon2, + None, + call.clone() + )); + let de = DispatchError::from(Error::::NoPermission).stripped(); + System::assert_last_event(ProxyEvent::ProxyExecuted { result: Err(de) }.into()); + assert_noop!( + Proxy::kill_pure(RuntimeOrigin::signed(1), 1, ProxyType::Any, 0, 1, 0), + Error::::NoPermission + ); + assert_eq!(Balances::free_balance(1), 1); + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(1), + anon, + None, + call.clone() + )); + assert_eq!(Balances::free_balance(1), 3); + assert_noop!( + Proxy::proxy(RuntimeOrigin::signed(1), anon, None, call.clone()), + Error::::NotProxy + ); + }); +} diff --git a/pallets/proxy/src/tests/real_pays_fee.rs b/pallets/proxy/src/tests/real_pays_fee.rs new file mode 100644 index 0000000000..719e07c353 --- /dev/null +++ b/pallets/proxy/src/tests/real_pays_fee.rs @@ -0,0 +1,127 @@ +//! set_real_pays_fee preference and cleanup on proxy removal. + +use super::mock::*; +use crate::*; +use frame::testing_prelude::*; + +#[test] +fn set_real_pays_fee_works() { + new_test_ext().execute_with(|| { + // Account 1 adds account 3 as proxy + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(1), + 3, + ProxyType::Any, + 0 + )); + + // Account 1 (real) enables real-pays-fee for delegate 3 + assert_ok!(Proxy::set_real_pays_fee(RuntimeOrigin::signed(1), 3, true)); + assert!(Proxy::is_real_pays_fee(&1, &3)); + System::assert_last_event( + ProxyEvent::RealPaysFeeSet { + real: 1, + delegate: 3, + pays_fee: true, + } + .into(), + ); + + // Disable it + assert_ok!(Proxy::set_real_pays_fee(RuntimeOrigin::signed(1), 3, false)); + assert!(!Proxy::is_real_pays_fee(&1, &3)); + System::assert_last_event( + ProxyEvent::RealPaysFeeSet { + real: 1, + delegate: 3, + pays_fee: false, + } + .into(), + ); + }); +} + +#[test] +fn set_real_pays_fee_fails_without_proxy() { + new_test_ext().execute_with(|| { + // No proxy relationship between 1 and 3 + assert_noop!( + Proxy::set_real_pays_fee(RuntimeOrigin::signed(1), 3, true), + Error::::NotProxy, + ); + }); +} + +#[test] +fn set_real_pays_fee_fails_unsigned() { + new_test_ext().execute_with(|| { + assert_noop!( + Proxy::set_real_pays_fee(RuntimeOrigin::none(), 3, true), + DispatchError::BadOrigin, + ); + }); +} + +#[test] +fn set_real_pays_fee_fails_root() { + new_test_ext().execute_with(|| { + assert_noop!( + Proxy::set_real_pays_fee(RuntimeOrigin::root(), 3, true), + DispatchError::BadOrigin, + ); + }); +} + +#[test] +fn real_pays_fee_cleaned_on_remove_proxy() { + new_test_ext().execute_with(|| { + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(1), + 3, + ProxyType::Any, + 0 + )); + assert_ok!(Proxy::set_real_pays_fee(RuntimeOrigin::signed(1), 3, true)); + assert!(Proxy::is_real_pays_fee(&1, &3)); + + // Remove the proxy + assert_ok!(Proxy::remove_proxy( + RuntimeOrigin::signed(1), + 3, + ProxyType::Any, + 0 + )); + + // Flag should be cleaned up + assert!(!Proxy::is_real_pays_fee(&1, &3)); + }); +} + +#[test] +fn real_pays_fee_cleaned_on_remove_proxies() { + new_test_ext().execute_with(|| { + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(1), + 2, + ProxyType::Any, + 0 + )); + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(1), + 3, + ProxyType::Any, + 0 + )); + assert_ok!(Proxy::set_real_pays_fee(RuntimeOrigin::signed(1), 2, true)); + assert_ok!(Proxy::set_real_pays_fee(RuntimeOrigin::signed(1), 3, true)); + assert!(Proxy::is_real_pays_fee(&1, &2)); + assert!(Proxy::is_real_pays_fee(&1, &3)); + + // Remove all proxies + assert_ok!(Proxy::remove_proxies(RuntimeOrigin::signed(1))); + + // Both flags should be cleaned up + assert!(!Proxy::is_real_pays_fee(&1, &2)); + assert!(!Proxy::is_real_pays_fee(&1, &3)); + }); +} diff --git a/pallets/shield/src/benchmarking.rs b/pallets/shield/src/benchmarking.rs index 74e156ec5c..25c80a1e61 100644 --- a/pallets/shield/src/benchmarking.rs +++ b/pallets/shield/src/benchmarking.rs @@ -1,3 +1,5 @@ +//! Runtime benchmarks for MevShield extrinsics (key announce, encrypt submit, queue admin). + use super::*; use frame_benchmarking::v2::*; diff --git a/pallets/shield/src/extension.rs b/pallets/shield/src/extension.rs index 5e5a85f7af..c97d95b096 100644 --- a/pallets/shield/src/extension.rs +++ b/pallets/shield/src/extension.rs @@ -1,3 +1,5 @@ +//! Signed-extension that rejects malformed `submit_encrypted` ciphertexts before they enter the pool. + use crate::{Call, Config, ShieldedTransaction}; use codec::{Decode, DecodeWithMemTracking, Encode}; use frame_support::pallet_prelude::*; @@ -12,11 +14,13 @@ use sp_runtime::transaction_validity::TransactionSource; use subtensor_macros::freeze_struct; use subtensor_runtime_common::CustomTransactionError; +// Doc lives on the module: adding `///` here would change the freeze_struct hash. #[freeze_struct("dabd89c6963de25d")] #[derive(Default, Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo)] pub struct CheckShieldedTxValidity(PhantomData); impl CheckShieldedTxValidity { + /// Construct the zero-sized extension marker. pub fn new() -> Self { Self(Default::default()) } diff --git a/pallets/shield/src/lib.rs b/pallets/shield/src/lib.rs index 6fa5ab5e39..b4b24ab865 100644 --- a/pallets/shield/src/lib.rs +++ b/pallets/shield/src/lib.rs @@ -1,4 +1,11 @@ -// pallets/mev-shield/src/lib.rs +//! # MevShield pallet +//! +//! Encrypts user extrinsics to the block author's ML-KEM-768 key so mempool +//! observers cannot frontrun plaintext calls. Clients encrypt to [`NextKey`] +//! (N+2 author); the inherent [`Call::announce_next_key`] rotates +//! `CurrentKey` ← `PendingKey` ← `NextKey` each block. Separately, +//! [`Call::store_encrypted`] queues ciphertext for deferred +//! [`Pallet::process_pending_extrinsics`] dispatch in `on_initialize`. #![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; @@ -57,17 +64,20 @@ const MAX_EXTRINSIC_DEPTH: u32 = 8; /// to discourage abuse of the encrypted extrinsic queue. const STORE_ENCRYPTED_WEIGHT: u64 = 20_000_000_000; +/// Fixed dispatch weight for [`Call::store_encrypted`] (not the benchmark figure). pub fn store_encrypted_weight() -> Weight { Weight::from_parts(STORE_ENCRYPTED_WEIGHT, 0) } -/// Trait for decrypting stored extrinsics before dispatch. +/// Runtime hook that turns queued `store_encrypted` bytes into a `RuntimeCall`. +/// +/// Production may decrypt ciphertext; tests often SCALE-decode plaintext call bytes only. pub trait ExtrinsicDecryptor { - /// Decrypt the stored bytes and return the decoded RuntimeCall. + /// Decrypt (or decode) stored bytes into a dispatchable `RuntimeCall`. fn decrypt(data: &[u8]) -> Result; } -/// Default implementation that always returns an error. +/// Placeholder decryptor: always fails so misconfigured runtimes cannot silently dispatch. impl ExtrinsicDecryptor for () { fn decrypt(_data: &[u8]) -> Result { Err(DispatchError::Other("ExtrinsicDecryptor not implemented")) @@ -79,164 +89,160 @@ pub mod pallet { use super::*; use crate::weights::WeightInfo; + /// MevShield configuration: Aura authority ids, author lookup, and decryptor for the queue. #[pallet::config] pub trait Config: frame_system::Config { - /// The identifier type for an authority. + /// Aura (or equivalent) authority id used as [`AuthorKeys`] map key. type AuthorityId: Member + Parameter + MaybeSerializeDeserialize + MaxEncodedLen; - /// A way to find the current and next block author. + /// Resolves current and N+2 authors for key rotation in [`Call::announce_next_key`]. type FindAuthors: FindAuthors; - /// The overarching call type for dispatching stored extrinsics. + /// Call type decoded/dispatched from [`PendingExtrinsics`] in `on_initialize`. type RuntimeCall: Parameter + Dispatchable + GetDispatchInfo; - /// Decryptor for stored extrinsics. + /// Turns queued ciphertext/encoded bytes into [`Self::RuntimeCall`]. type ExtrinsicDecryptor: ExtrinsicDecryptor<::RuntimeCall>; - /// Weight information for extrinsics in this pallet. + /// Extrinsic weights for this pallet (see generated `weights` module). type WeightInfo: WeightInfo; } #[pallet::pallet] pub struct Pallet(_); - /// Current block author's ML-KEM-768 encapsulation key (internal, not for encryption). + /// Current author's ML-KEM-768 encapsulation key after rotation (proposer-internal; not client encrypt target). #[pallet::storage] pub type CurrentKey = StorageValue<_, ShieldEncKey, OptionQuery>; - /// Next block author's key, staged here before promoting to `CurrentKey`. + /// N+1 author's key; becomes [`CurrentKey`] next block. Hash of this key validates in-flight shielded txs. #[pallet::storage] pub type PendingKey = StorageValue<_, ShieldEncKey, OptionQuery>; - /// Key users should encrypt with (N+2 author's key). + /// N+2 author's ML-KEM-768 encapsulation key — the client encrypt target for new shielded txs. #[pallet::storage] pub type NextKey = StorageValue<_, ShieldEncKey, OptionQuery>; - /// Per-author ML-KEM-768 encapsulation key, updated each time the author produces a block. + /// Last announced ML-KEM-768 encapsulation key per authority id (source for staging [`NextKey`]). #[pallet::storage] pub type AuthorKeys = StorageMap<_, Twox64Concat, T::AuthorityId, ShieldEncKey, OptionQuery>; - /// Block number at which `PendingKey` is no longer valid (exclusive upper bound). - /// Updated every block during rotation. + /// Exclusive upper block bound for trusting [`PendingKey`] (set to `now + 2` when present). #[pallet::storage] pub type PendingKeyExpiresAt = StorageValue<_, BlockNumberFor, OptionQuery>; - /// Block number at which `NextKey` is no longer valid (exclusive upper bound). - /// Updated every block during rotation. + /// Exclusive upper block bound for trusting [`NextKey`] (set to `now + 3` when present). #[pallet::storage] pub type NextKeyExpiresAt = StorageValue<_, BlockNumberFor, OptionQuery>; - /// Stores whether some migration has been run. + /// Idempotency flags for runtime migrations keyed by migration name bytes. #[pallet::storage] pub type HasMigrationRun = StorageMap<_, Identity, BoundedVec, bool, ValueQuery>; - /// Maximum size of a single encoded call. + /// Max SCALE/ciphertext bytes accepted by [`Call::store_encrypted`] (8192). pub type MaxEncryptedCallSize = ConstU32<8192>; - /// Default maximum number of pending extrinsics. + /// Default for [`MaxPendingExtrinsicsLimit`] when unset (100). pub type DefaultMaxPendingExtrinsics = ConstU32<100>; - /// Configurable maximum number of pending extrinsics. - /// Defaults to 100 if not explicitly set via `set_max_pending_extrinsics`. + /// Cap on [`PendingExtrinsics`] count; `store_encrypted` fails with [`Error::TooManyPendingExtrinsics`] when full. #[pallet::storage] pub type MaxPendingExtrinsicsLimit = StorageValue<_, u32, ValueQuery, DefaultMaxPendingExtrinsics>; - /// Default extrinsic lifetime in blocks. + /// Default for [`ExtrinsicLifetime`] when unset (10 blocks). pub const DEFAULT_EXTRINSIC_LIFETIME: u32 = 10; - /// Configurable extrinsic lifetime (max block difference between submission and execution). - /// Defaults to 10 blocks if not explicitly set. + /// Max age (`current - submitted_at`) before a queued extrinsic is dropped as expired. #[pallet::storage] pub type ExtrinsicLifetime = StorageValue<_, u32, ValueQuery, ConstU32>; - /// Default maximum weight allowed for on_initialize processing. + /// Default ref_time budget for processing the pending queue in `on_initialize`. pub const DEFAULT_ON_INITIALIZE_WEIGHT: u64 = 500_000_000_000; - /// Absolute maximum weight for on_initialize: half the total block weight (2s of 4s). + /// Hard ceiling for [`OnInitializeWeight`] / [`MaxExtrinsicWeight`] admin sets (half of 4s block). pub const MAX_ON_INITIALIZE_WEIGHT: u64 = 2_000_000_000_000; - /// Configurable maximum weight for on_initialize processing. - /// Defaults to 500_000_000_000 ref_time if not explicitly set. + /// Aggregate ref_time budget for [`Pallet::process_pending_extrinsics`]; excess items emit [`Event::ExtrinsicPostponed`]. #[pallet::storage] pub type OnInitializeWeight = StorageValue<_, u64, ValueQuery, ConstU64>; - /// Default maximum weight for a single extrinsic. + /// Default per-call ref_time cap during queue processing. pub const DEFAULT_MAX_EXTRINSIC_WEIGHT: u64 = 50_000_000_000; - /// Configurable maximum weight for a single extrinsic dispatched during on_initialize. - /// Extrinsics exceeding this limit are removed from the queue. + /// Per-call ref_time cap; overweight queued calls are removed with [`Event::ExtrinsicWeightExceeded`]. #[pallet::storage] pub type MaxExtrinsicWeight = StorageValue<_, u64, ValueQuery, ConstU64>; - /// A pending extrinsic stored for later execution. + /// One queued item: submitter, opaque call bytes, and submission block for lifetime checks. #[freeze_struct("f13d2a9d7bd4767d")] #[derive(Clone, Encode, Decode, TypeInfo, MaxEncodedLen, PartialEq, Debug)] #[scale_info(skip_type_params(T))] pub struct PendingExtrinsic { - /// The account that submitted the extrinsic. + /// Signed origin that will be used when the call is later dispatched. pub who: T::AccountId, - /// The encoded call data. + /// Opaque bytes passed to [`ExtrinsicDecryptor::decrypt`] (often SCALE-encoded call in tests). pub encrypted_call: BoundedVec, - /// The block number when the extrinsic was submitted. + /// Block number at insert time; compared against [`ExtrinsicLifetime`] during processing. pub submitted_at: BlockNumberFor, } - /// Storage map for encrypted extrinsics to be executed in on_initialize. - /// Uses u32 index for O(1) insertion and removal. Count is maintained automatically. + /// Counted queue of deferred calls, keyed by monotonic u32 index (gaps allowed; count is authoritative). #[pallet::storage] pub type PendingExtrinsics = CountedStorageMap<_, Identity, u32, PendingExtrinsic, OptionQuery>; - /// Next index to use when inserting a pending extrinsic (unique auto-increment). + /// Next free index for [`PendingExtrinsics`] inserts; never decremented (unique auto-increment). #[pallet::storage] pub type NextPendingExtrinsicIndex = StorageValue<_, u32, ValueQuery>; + /// MevShield events: shielded submit, queue lifecycle, and admin limit updates. #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { - /// Encrypted wrapper accepted. + /// `submit_encrypted` accepted; `id` is `hash(who, ciphertext)`. EncryptedSubmitted { id: T::Hash, who: T::AccountId }, - /// Encrypted extrinsic was stored for later execution. + /// Call bytes enqueued under `index` for later `on_initialize` dispatch. ExtrinsicStored { index: u32, who: T::AccountId }, - /// Extrinsic decode failed during on_initialize. + /// [`ExtrinsicDecryptor`] failed; item removed from the queue. ExtrinsicDecodeFailed { index: u32 }, - /// Extrinsic dispatch failed during on_initialize. + /// Decrypted call dispatched but returned an error; item already removed. ExtrinsicDispatchFailed { index: u32, error: DispatchError }, - /// Extrinsic was successfully dispatched during on_initialize. + /// Queued call dispatched successfully under the original signed origin. ExtrinsicDispatched { index: u32 }, - /// Extrinsic expired (exceeded max block lifetime). + /// Item age exceeded [`ExtrinsicLifetime`]; removed without dispatch. ExtrinsicExpired { index: u32 }, - /// Extrinsic postponed due to weight limit. + /// Remaining [`OnInitializeWeight`] budget insufficient; left in queue for a later block. ExtrinsicPostponed { index: u32 }, - /// Maximum pending extrinsics limit was updated. + /// Root updated [`MaxPendingExtrinsicsLimit`]. MaxPendingExtrinsicsNumberSet { value: u32 }, - /// Maximum on_initialize weight was updated. + /// Root updated [`OnInitializeWeight`]. OnInitializeWeightSet { value: u64 }, - /// Extrinsic lifetime was updated. + /// Root updated [`ExtrinsicLifetime`]. ExtrinsicLifetimeSet { value: u32 }, - /// Maximum per-extrinsic weight was updated. + /// Root updated [`MaxExtrinsicWeight`]. MaxExtrinsicWeightSet { value: u64 }, - /// Extrinsic exceeded the per-extrinsic weight limit and was removed. + /// Call weight exceeded [`MaxExtrinsicWeight`]; removed without dispatch. ExtrinsicWeightExceeded { index: u32 }, } + /// MevShield dispatch errors for key announce and queue admin/store paths. #[pallet::error] pub enum Error { - /// The announced ML‑KEM encapsulation key length is invalid. + /// Announced key length ≠ [`MLKEM768_ENC_KEY_LEN`]. BadEncKeyLen, - /// Unreachable. + /// Inherent ran without a resolvable current author (`FindAuthors` returned `None`). Unreachable, - /// Too many pending extrinsics in storage. + /// [`PendingExtrinsics`] count already at [`MaxPendingExtrinsicsLimit`]. TooManyPendingExtrinsics, - /// Weight exceeds the absolute maximum (half of total block weight). + /// Admin weight argument exceeded [`MAX_ON_INITIALIZE_WEIGHT`]. WeightExceedsAbsoluteMax, } @@ -362,7 +368,10 @@ pub mod pallet { Ok(()) } - /// Store an encrypted extrinsic for later execution in on_initialize. + /// Enqueue opaque call bytes for deferred dispatch in `on_initialize`. + /// + /// Fails with [`Error::TooManyPendingExtrinsics`] when the counted queue is full. + /// Weight is the fixed [`store_encrypted_weight`] (above the benchmark) to deter spam. #[allow(unknown_lints, benchmarked_weight_not_plugged)] #[pallet::call_index(2)] #[pallet::weight(store_encrypted_weight())] @@ -480,8 +489,9 @@ pub mod pallet { } impl Pallet { - /// Process pending encrypted extrinsics up to the weight limit. - /// Returns the total weight consumed. + /// Drain [`PendingExtrinsics`] from oldest index until empty, expired, overweight, or budget exhausted. + /// + /// Returns total weight consumed (DB + successful/failed dispatch weights). Postponed items stay queued. pub fn process_pending_extrinsics() -> Weight { let next_index = NextPendingExtrinsicIndex::::get(); let count = PendingExtrinsics::::count(); @@ -578,6 +588,9 @@ impl Pallet { weight } + /// If `uxt` is a checked `submit_encrypted` call, parse its wire ciphertext into [`ShieldedTransaction`]. + /// + /// Returns `None` for non-shield calls, bad signatures, malformed ciphertext, or extrinsic depth > [`MAX_EXTRINSIC_DEPTH`]. pub fn try_decode_shielded_tx( uxt: ExtrinsicOf, ) -> Option @@ -613,20 +626,23 @@ impl Pallet { ShieldedTransaction::parse(ciphertext) } + /// True when `key_hash` equals `twox_128(PendingKey)` — the key clients encrypted toward one block ago. pub fn is_shielded_using_current_key(key_hash: &[u8; 16]) -> bool { let pending = PendingKey::::get(); let pending_hash = pending.as_ref().map(|k| twox_128(&k[..])); pending_hash.as_ref() == Some(key_hash) } + /// Decrypt `shielded_tx` with raw ML-KEM-768 decapsulation key bytes and SCALE-decode the inner extrinsic. pub fn try_unshield_tx( dec_key_bytes: alloc::vec::Vec, shielded_tx: ShieldedTransaction, ) -> Option<::Extrinsic> { - let plaintext = unshield(&dec_key_bytes, &shielded_tx).or_else(|| { - log::debug!(target: LOG_TARGET, "Failed to unshield transaction"); - None - })?; + let plaintext = + decrypt_shielded_ciphertext(&dec_key_bytes, &shielded_tx).or_else(|| { + log::debug!(target: LOG_TARGET, "Failed to unshield transaction"); + None + })?; if plaintext.is_empty() { return None; @@ -638,8 +654,11 @@ impl Pallet { } } +/// Looks up the current block author and the author two slots ahead for key rotation. pub trait FindAuthors { + /// Authority producing the block in which `announce_next_key` runs. fn find_current_author() -> Option; + /// Authority two slots ahead whose [`AuthorKeys`] entry stages into [`NextKey`]. fn find_next_next_author() -> Option; } @@ -652,11 +671,8 @@ impl FindAuthors for () { } } -/// Decrypt a shielded transaction using the raw decapsulation key bytes. -/// -/// Performs ML-KEM-768 decapsulation followed by XChaCha20-Poly1305 AEAD decryption. -/// Runs entirely in WASM — no host functions needed. -fn unshield( +/// ML-KEM-768 decapsulate + XChaCha20-Poly1305 decrypt; WASM-only (no host crypto). +fn decrypt_shielded_ciphertext( dec_key_bytes: &[u8], shielded_tx: &ShieldedTransaction, ) -> Option> { diff --git a/pallets/shield/src/migrations/migrate_clear_v1_storage.rs b/pallets/shield/src/migrations/migrate_clear_v1_storage.rs index e3c55d8713..53b031490e 100644 --- a/pallets/shield/src/migrations/migrate_clear_v1_storage.rs +++ b/pallets/shield/src/migrations/migrate_clear_v1_storage.rs @@ -1,9 +1,15 @@ -use super::*; +//! One-shot clear of removed MevShield v1 maps and reset of [`CurrentKey`]. + +use crate::{Config, CurrentKey, HasMigrationRun}; +use frame_support::pallet_prelude::{BoundedVec, Get, Weight}; use frame_support::storage::unhashed; use scale_info::prelude::string::String; use sp_io::hashing::twox_128; /// Clears removed v1 storage items (`Submissions`, `KeyHashByBlock`) and resets `CurrentKey`. +/// +/// Idempotent via [`HasMigrationRun`] key `"migrate_clear_v1_storage"`. Does not touch +/// [`NextKey`] / [`AuthorKeys`]. Storage prefix is the runtime pallet name `MevShield`. pub fn migrate_clear_v1_storage() -> Weight { let migration_name = b"migrate_clear_v1_storage".to_vec(); let bounded_name = BoundedVec::truncate_from(migration_name.clone()); diff --git a/pallets/shield/src/migrations/mod.rs b/pallets/shield/src/migrations/mod.rs index 1069de5297..daeedcb16b 100644 --- a/pallets/shield/src/migrations/mod.rs +++ b/pallets/shield/src/migrations/mod.rs @@ -1,4 +1,3 @@ -use crate::*; -use frame_support::{traits::Get, weights::Weight}; +//! Runtime migrations for `pallet-shield` / MevShield storage layout changes. pub mod migrate_clear_v1_storage; diff --git a/pallets/shield/src/mock.rs b/pallets/shield/src/mock.rs index 1bb6fa018a..716acf16b3 100644 --- a/pallets/shield/src/mock.rs +++ b/pallets/shield/src/mock.rs @@ -1,3 +1,5 @@ +//! Test runtime and helpers for `pallet-shield` unit tests and benchmarks. + use crate as pallet_shield; use stp_shield::MLKEM768_ENC_KEY_LEN; @@ -13,7 +15,9 @@ use stp_shield::ShieldEncKey; pub type Block = frame_system::mocking::MockBlock; +/// Extrinsic type that can carry real signatures for `try_decode_shielded_tx` tests. pub type DecodableExtrinsic = generic::UncheckedExtrinsic; +/// Block type paired with [`DecodableExtrinsic`]. pub type DecodableBlock = generic::Block, DecodableExtrinsic>; @@ -65,6 +69,7 @@ thread_local! { static MOCK_NEXT_NEXT: RefCell>> = const { RefCell::new(None) }; } +/// [`FindAuthors`] backed by thread-local overrides, with Aura slot fallback for benchmarks. pub struct MockFindAuthors; impl pallet_shield::FindAuthors for MockFindAuthors { @@ -87,7 +92,7 @@ impl pallet_shield::FindAuthors for MockFindAuthors { } } -/// Mock decryptor that just decodes the bytes without decryption. +/// Test decryptor: SCALE-decodes call bytes with no crypto (plaintext queue tests). pub struct MockDecryptor; impl pallet_shield::ExtrinsicDecryptor for MockDecryptor { @@ -104,6 +109,7 @@ impl pallet_shield::Config for Test { type WeightInfo = (); } +/// Empty genesis plus a memory keystore extension. pub fn new_test_ext() -> sp_io::TestExternalities { let mut ext: sp_io::TestExternalities = RuntimeGenesisConfig::default() .build_storage() @@ -115,30 +121,35 @@ pub fn new_test_ext() -> sp_io::TestExternalities { ext } -pub fn valid_pk() -> ShieldEncKey { +/// Fixed-length ML-KEM-768 encapsulation key filled with `0x42`. +pub fn valid_shield_enc_key() -> ShieldEncKey { BoundedVec::truncate_from(vec![0x42; MLKEM768_ENC_KEY_LEN]) } -pub fn valid_pk_b() -> ShieldEncKey { +/// Alternate fixed-length encapsulation key filled with `0x99`. +pub fn valid_shield_enc_key_b() -> ShieldEncKey { BoundedVec::truncate_from(vec![0x99; MLKEM768_ENC_KEY_LEN]) } -/// Create a deterministic `AuraId` from a simple index for tests. +/// Deterministic `AuraId` from a single-byte index (repeats the byte across the public key). pub fn author(n: u8) -> AuraId { AuraId::from(sr25519::Public::from_raw([n; 32])) } -pub fn set_authors(current: Option, next_next: Option) { +/// Override [`MockFindAuthors`] current and N+2 author for the calling thread. +pub fn set_mock_authors(current: Option, next_next: Option) { MOCK_CURRENT.with(|c| *c.borrow_mut() = current); MOCK_NEXT_NEXT.with(|n| *n.borrow_mut() = Some(next_next)); } +/// Wrap `call` in `depth` layers of `utility.batch` for extrinsic-depth tests. pub fn nest_call(call: RuntimeCall, depth: usize) -> RuntimeCall { (0..depth).fold(call, |inner, _| { RuntimeCall::Utility(pallet_subtensor_utility::Call::batch { calls: vec![inner] }) }) } +/// Build shield wire ciphertext: `key_hash || kem_len_le || kem_ct || nonce || aead_ct`. pub fn build_wire_ciphertext( key_hash: &[u8; 16], kem_ct: &[u8], diff --git a/pallets/shield/src/tests.rs b/pallets/shield/src/tests.rs deleted file mode 100644 index 622b5be74a..0000000000 --- a/pallets/shield/src/tests.rs +++ /dev/null @@ -1,1164 +0,0 @@ -use crate::mock::*; -use crate::{ - AuthorKeys, CurrentKey, Error, ExtrinsicLifetime, HasMigrationRun, MaxExtrinsicWeight, - MaxPendingExtrinsicsLimit, NextKey, NextKeyExpiresAt, NextPendingExtrinsicIndex, - OnInitializeWeight, PendingExtrinsic, PendingExtrinsics, PendingKey, PendingKeyExpiresAt, -}; -use codec::Encode; -use frame_support::{BoundedVec, assert_noop, assert_ok}; -use sp_runtime::testing::TestSignature; -use sp_runtime::traits::{Block as BlockT, Hash}; -use stp_shield::{MLKEM768_ENC_KEY_LEN, ShieldEncKey, ShieldKeystore, ShieldedTransaction}; - -use chacha20poly1305::{ - KeyInit, XChaCha20Poly1305, XNonce, - aead::{Aead, Payload}, -}; -use ml_kem::{ - EncodedSizeUser, MlKem768Params, - kem::{Encapsulate, EncapsulationKey}, -}; -use rand_chacha::{ChaChaRng, rand_core::SeedableRng}; -use stc_shield::MemoryShieldKeystore; - -/// Simulates a 3-validator round-robin (authors 1, 2, 3) over 5 blocks. -/// Each block calls `announce_next_key` and verifies the full pipeline: -/// CurrentKey, PendingKey, NextKey, AuthorKeys, expirations, and -/// `is_shielded_using_current_key`. -#[test] -fn key_rotation_round_robin() { - new_test_ext().execute_with(|| { - let key_of = - |n: u8| -> ShieldEncKey { BoundedVec::truncate_from(vec![n; MLKEM768_ENC_KEY_LEN]) }; - let hash_of = |pk: &ShieldEncKey| sp_io::hashing::twox_128(&pk[..]); - - // 3 validators in round-robin: 1, 2, 3, 1, 2. - let authors = [1u8, 2, 3, 1, 2]; - let next_next = |block: usize| -> Option { authors.get(block + 2).copied() }; - - // ── Block 1: author=1, next_next=3 ────────────────────────────── - // Pipeline is empty; author(3) has no AuthorKeys yet. - System::set_block_number(1); - set_authors(Some(author(1)), next_next(0).map(author)); - assert_ok!(MevShield::announce_next_key( - RuntimeOrigin::none(), - Some(key_of(1)), - )); - - assert!(CurrentKey::::get().is_none()); - assert!(PendingKey::::get().is_none()); - assert!(NextKey::::get().is_none()); - assert_eq!(AuthorKeys::::get(author(1)), Some(key_of(1))); - assert!(PendingKeyExpiresAt::::get().is_none()); - assert!(NextKeyExpiresAt::::get().is_none()); - // Nothing in PendingKey → is_shielded always false. - assert!(!MevShield::is_shielded_using_current_key(&[0xFF; 16])); - - // ── Block 2: author=2, next_next=1 ────────────────────────────── - // author(1) registered in block 1 → NextKey picks up key_of(1). - System::set_block_number(2); - set_authors(Some(author(2)), next_next(1).map(author)); - assert_ok!(MevShield::announce_next_key( - RuntimeOrigin::none(), - Some(key_of(2)), - )); - - assert!(CurrentKey::::get().is_none()); - assert!(PendingKey::::get().is_none()); - assert_eq!(NextKey::::get(), Some(key_of(1))); - assert_eq!(AuthorKeys::::get(author(2)), Some(key_of(2))); - assert!(PendingKeyExpiresAt::::get().is_none()); - assert_eq!(NextKeyExpiresAt::::get(), Some(5)); // 2 + 3 - - // ── Block 3: author=3, next_next=2 ────────────────────────────── - // NextKey(key_of(1)) → PendingKey; next_next=author(2) has key_of(2) → NextKey. - System::set_block_number(3); - set_authors(Some(author(3)), next_next(2).map(author)); - assert_ok!(MevShield::announce_next_key( - RuntimeOrigin::none(), - Some(key_of(3)), - )); - - assert!(CurrentKey::::get().is_none()); - assert_eq!(PendingKey::::get(), Some(key_of(1))); - assert_eq!(NextKey::::get(), Some(key_of(2))); - assert_eq!(AuthorKeys::::get(author(3)), Some(key_of(3))); - assert_eq!(PendingKeyExpiresAt::::get(), Some(5)); // 3 + 2 - assert_eq!(NextKeyExpiresAt::::get(), Some(6)); // 3 + 3 - // PendingKey = key_of(1) → is_shielded matches its hash. - assert!(MevShield::is_shielded_using_current_key(&hash_of(&key_of( - 1 - )))); - assert!(!MevShield::is_shielded_using_current_key(&hash_of( - &key_of(2) - ))); - assert!(!MevShield::is_shielded_using_current_key(&[0xFF; 16])); - - // ── Block 4: author=1, next_next=out of bounds ────────────────── - // Full pipeline: PendingKey(key_of(1)) → CurrentKey, NextKey(key_of(2)) → PendingKey. - System::set_block_number(4); - set_authors(Some(author(1)), next_next(3).map(author)); - assert_ok!(MevShield::announce_next_key( - RuntimeOrigin::none(), - Some(key_of(1)), - )); - - assert_eq!(CurrentKey::::get(), Some(key_of(1))); - assert_eq!(PendingKey::::get(), Some(key_of(2))); - assert!(NextKey::::get().is_none()); - assert_eq!(AuthorKeys::::get(author(1)), Some(key_of(1))); - assert_eq!(PendingKeyExpiresAt::::get(), Some(6)); // 4 + 2 - assert!(NextKeyExpiresAt::::get().is_none()); - // PendingKey = key_of(2). - assert!(MevShield::is_shielded_using_current_key(&hash_of(&key_of( - 2 - )))); - assert!(!MevShield::is_shielded_using_current_key(&hash_of( - &key_of(1) - ))); - - // ── Block 5: author=2, next_next=none ─────────────────────────── - // PendingKey(key_of(2)) → CurrentKey; pipeline drains. - System::set_block_number(5); - set_authors(Some(author(2)), None); - assert_ok!(MevShield::announce_next_key( - RuntimeOrigin::none(), - Some(key_of(2)), - )); - - assert_eq!(CurrentKey::::get(), Some(key_of(2))); - assert!(PendingKey::::get().is_none()); - assert!(NextKey::::get().is_none()); - assert!(PendingKeyExpiresAt::::get().is_none()); - assert!(NextKeyExpiresAt::::get().is_none()); - }); -} - -/// AuthorKeys is read *before* being updated, so when current == next_next -/// the NextKey picks up the old key, not the newly announced one. -#[test] -fn announce_rotations_use_pre_update_author_keys() { - new_test_ext().execute_with(|| { - set_authors(Some(author(1)), Some(author(1))); - - let old_pk = valid_pk(); - let new_pk = valid_pk_b(); - AuthorKeys::::insert(author(1), old_pk.clone()); - - assert_ok!(MevShield::announce_next_key( - RuntimeOrigin::none(), - Some(new_pk.clone()), - )); - - assert_eq!(NextKey::::get(), Some(old_pk)); - assert_eq!(AuthorKeys::::get(author(1)), Some(new_pk)); - }); -} - -#[test] -fn announce_rejects_signed_origin() { - new_test_ext().execute_with(|| { - set_authors(Some(author(1)), None); - assert_noop!( - MevShield::announce_next_key(RuntimeOrigin::signed(1), Some(valid_pk())), - sp_runtime::DispatchError::BadOrigin - ); - }); -} - -#[test] -fn announce_rejects_bad_pk_length() { - new_test_ext().execute_with(|| { - set_authors(Some(author(1)), None); - let bad_pk: ShieldEncKey = BoundedVec::truncate_from(vec![0x01; 100]); - - assert_noop!( - MevShield::announce_next_key(RuntimeOrigin::none(), Some(bad_pk)), - Error::::BadEncKeyLen - ); - }); -} - -#[test] -fn announce_none_pk_removes_author_key() { - new_test_ext().execute_with(|| { - set_authors(Some(author(1)), None); - AuthorKeys::::insert(author(1), valid_pk()); - - assert_ok!(MevShield::announce_next_key(RuntimeOrigin::none(), None)); - - assert!(AuthorKeys::::get(author(1)).is_none()); - }); -} - -#[test] -fn announce_fails_when_no_current_author() { - new_test_ext().execute_with(|| { - set_authors(None, None); - - assert_noop!( - MevShield::announce_next_key(RuntimeOrigin::none(), Some(valid_pk())), - Error::::Unreachable - ); - }); -} - -#[test] -fn submit_encrypted_emits_event() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - let ciphertext = BoundedVec::truncate_from(vec![0xAA; 64]); - let who: u64 = 1; - - assert_ok!(MevShield::submit_encrypted( - RuntimeOrigin::signed(who), - ciphertext.clone(), - )); - - let expected_id = ::Hashing::hash_of(&(who, &ciphertext)); - - System::assert_last_event( - crate::Event::::EncryptedSubmitted { - id: expected_id, - who, - } - .into(), - ); - }); -} - -#[test] -fn submit_encrypted_rejects_unsigned() { - new_test_ext().execute_with(|| { - let ciphertext = BoundedVec::truncate_from(vec![0xAA; 64]); - - assert_noop!( - MevShield::submit_encrypted(RuntimeOrigin::none(), ciphertext), - sp_runtime::DispatchError::BadOrigin - ); - }); -} - -#[test] -fn try_decode_shielded_tx_parses_bare_submit_encrypted() { - new_test_ext().execute_with(|| { - let key_hash = [0xAB; 16]; - let kem_ct = vec![0xCC; 32]; - let nonce = [0xDD; 24]; - let aead_ct = vec![0xEE; 64]; - - let ciphertext = build_wire_ciphertext(&key_hash, &kem_ct, &nonce, &aead_ct); - let call = RuntimeCall::MevShield(crate::Call::submit_encrypted { - ciphertext: BoundedVec::truncate_from(ciphertext), - }); - let uxt = DecodableExtrinsic::new_bare(call); - - let result = crate::Pallet::::try_decode_shielded_tx::< - DecodableBlock, - frame_system::ChainContext, - >(uxt); - assert!(result.is_some()); - - let shielded = result.unwrap(); - assert_eq!(shielded.key_hash, key_hash); - assert_eq!(shielded.kem_ct, kem_ct); - assert_eq!(shielded.nonce, nonce); - assert_eq!(shielded.aead_ct, aead_ct); - }); -} - -#[test] -fn try_decode_shielded_tx_returns_none_for_non_shield_call() { - new_test_ext().execute_with(|| { - let call = RuntimeCall::System(frame_system::Call::remark { remark: vec![] }); - let uxt = DecodableExtrinsic::new_bare(call); - - let result = crate::Pallet::::try_decode_shielded_tx::< - DecodableBlock, - frame_system::ChainContext, - >(uxt); - assert!(result.is_none()); - }); -} - -#[test] -fn try_decode_shielded_tx_returns_none_for_bad_signature() { - new_test_ext().execute_with(|| { - let ciphertext = build_wire_ciphertext(&[0xAB; 16], &[0xCC; 32], &[0xDD; 24], &[0xEE; 64]); - let call = RuntimeCall::MevShield(crate::Call::submit_encrypted { - ciphertext: BoundedVec::truncate_from(ciphertext), - }); - let bad_sig = TestSignature(1, vec![0xFF; 32]); - let uxt = DecodableExtrinsic::new_signed(call, 1u64, bad_sig, ()); - - let result = crate::Pallet::::try_decode_shielded_tx::< - DecodableBlock, - frame_system::ChainContext, - >(uxt); - assert!(result.is_none()); - }); -} - -#[test] -fn try_decode_shielded_tx_returns_none_for_malformed_ciphertext() { - new_test_ext().execute_with(|| { - let call = RuntimeCall::MevShield(crate::Call::submit_encrypted { - ciphertext: BoundedVec::truncate_from(vec![0u8; 5]), - }); - let uxt = DecodableExtrinsic::new_bare(call); - - let result = crate::Pallet::::try_decode_shielded_tx::< - DecodableBlock, - frame_system::ChainContext, - >(uxt); - assert!(result.is_none()); - }); -} - -#[test] -fn try_decode_shielded_tx_returns_none_when_depth_exceeded() { - new_test_ext().execute_with(|| { - let ciphertext = build_wire_ciphertext(&[0xAB; 16], &[0xCC; 32], &[0xDD; 24], &[0xEE; 64]); - let inner = RuntimeCall::MevShield(crate::Call::submit_encrypted { - ciphertext: BoundedVec::truncate_from(ciphertext), - }); - let call = nest_call(inner, 8); - let uxt = DecodableExtrinsic::new_bare(call); - - let result = crate::Pallet::::try_decode_shielded_tx::< - DecodableBlock, - frame_system::ChainContext, - >(uxt); - assert!(result.is_none()); - }); -} - -#[test] -fn try_unshield_tx_decrypts_extrinsic() { - let mut rng = ChaChaRng::from_seed([42u8; 32]); - let keystore = MemoryShieldKeystore::new(); - - // Client side: read the announced encapsulation key and encapsulate. - let pk_bytes = keystore.next_enc_key().unwrap(); - let enc_key = - EncapsulationKey::::from_bytes(pk_bytes.as_slice().try_into().unwrap()); - let (kem_ct, shared_secret) = enc_key.encapsulate(&mut rng).unwrap(); - - // Build the inner extrinsic that we'll encrypt. - let inner_call = RuntimeCall::System(frame_system::Call::remark { - remark: vec![1, 2, 3], - }); - let inner_uxt = ::Extrinsic::new_bare(inner_call); - let plaintext = inner_uxt.encode(); - - // AEAD encrypt the extrinsic bytes. - let nonce = [42u8; 24]; - let cipher = XChaCha20Poly1305::new(shared_secret.as_slice().into()); - let aead_ct = cipher - .encrypt( - XNonce::from_slice(&nonce), - Payload { - msg: &plaintext, - aad: &[], - }, - ) - .unwrap(); - - // Roll keystore so next -> current (author side). - keystore.roll_for_next_slot().unwrap(); - let dec_key_bytes = keystore.current_dec_key().unwrap(); - - let shielded_tx = ShieldedTransaction { - key_hash: [0u8; 16], - kem_ct: kem_ct.as_slice().to_vec(), - nonce, - aead_ct, - }; - - let result = crate::Pallet::::try_unshield_tx::(dec_key_bytes, shielded_tx); - assert!(result.is_some()); - - let decoded = result.unwrap(); - assert_eq!(decoded.encode(), inner_uxt.encode()); -} - -// --------------------------------------------------------------------------- -// Migration tests -// --------------------------------------------------------------------------- - -mod migration_tests { - use super::*; - use crate::migrations::migrate_clear_v1_storage::migrate_clear_v1_storage; - use sp_io::hashing::twox_128; - - #[test] - fn migrate_clear_v1_storage_works() { - new_test_ext().execute_with(|| { - // Seed legacy storage that should be cleared. - seed_legacy_map("Submissions", 5); - seed_legacy_map("KeyHashByBlock", 3); - CurrentKey::::put(valid_pk()); - - // Current storage that must survive. - NextKey::::put(valid_pk()); - AuthorKeys::::insert(author(1), valid_pk_b()); - - // Sanity: legacy values exist. - assert_eq!(count_keys("Submissions"), 5); - assert_eq!(count_keys("KeyHashByBlock"), 3); - assert!(CurrentKey::::get().is_some()); - - migrate_clear_v1_storage::(); - - // Legacy storage cleared. - assert_eq!(count_keys("Submissions"), 0); - assert_eq!(count_keys("KeyHashByBlock"), 0); - assert!(CurrentKey::::get().is_none()); - - // Current storage untouched. - assert_eq!(NextKey::::get(), Some(valid_pk())); - assert_eq!(AuthorKeys::::get(author(1)), Some(valid_pk_b())); - - // Migration was recorded. - let mig_key = BoundedVec::truncate_from(b"migrate_clear_v1_storage".to_vec()); - assert!(HasMigrationRun::::get(&mig_key)); - - // Idempotent: re-run doesn't touch new data. - CurrentKey::::put(valid_pk_b()); - migrate_clear_v1_storage::(); - assert_eq!(CurrentKey::::get(), Some(valid_pk_b())); - }); - } - - fn seed_legacy_map(storage_name: &str, count: u32) { - let mut prefix = Vec::new(); - prefix.extend_from_slice(&twox_128(b"MevShield")); - prefix.extend_from_slice(&twox_128(storage_name.as_bytes())); - - for i in 0..count { - let mut key = prefix.clone(); - key.extend_from_slice(&i.to_le_bytes()); - sp_io::storage::set(&key, &[1u8; 32]); - } - } - - fn count_keys(storage_name: &str) -> u32 { - let mut prefix = Vec::new(); - prefix.extend_from_slice(&twox_128(b"MevShield")); - prefix.extend_from_slice(&twox_128(storage_name.as_bytes())); - - let mut count = 0u32; - let mut next_key = sp_io::storage::next_key(&prefix); - while let Some(key) = next_key { - if !key.starts_with(&prefix) { - break; - } - count += 1; - next_key = sp_io::storage::next_key(&key); - } - count - } -} - -// --------------------------------------------------------------------------- -// Encrypted extrinsics storage tests -// --------------------------------------------------------------------------- - -mod encrypted_extrinsics_tests { - use super::*; - use frame_support::traits::Hooks; - - #[test] - fn store_encrypted_works() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - let call = RuntimeCall::System(frame_system::Call::remark { - remark: vec![1, 2, 3], - }); - let encoded_call = BoundedVec::truncate_from(call.encode()); - let who: u64 = 1; - - assert_ok!(MevShield::store_encrypted( - RuntimeOrigin::signed(who), - encoded_call.clone(), - )); - - // Verify the extrinsic was stored at index 0 with account ID - let expected = PendingExtrinsic:: { - who, - encrypted_call: encoded_call, - submitted_at: 1, - }; - assert_eq!(PendingExtrinsics::::get(0), Some(expected)); - assert_eq!(NextPendingExtrinsicIndex::::get(), 1); - assert_eq!(PendingExtrinsics::::count(), 1); - - // Verify event was emitted with index - System::assert_last_event( - crate::Event::::ExtrinsicStored { index: 0, who }.into(), - ); - }); - } - - #[test] - fn on_initialize_decodes_and_dispatches() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - // Store an encoded remark call - let call = RuntimeCall::System(frame_system::Call::remark { - remark: vec![1, 2, 3], - }); - let encoded_call = BoundedVec::truncate_from(call.encode()); - - assert_ok!(MevShield::store_encrypted( - RuntimeOrigin::signed(1), - encoded_call, - )); - - // Verify there's a pending extrinsic - assert_eq!(NextPendingExtrinsicIndex::::get(), 1); - assert_eq!(PendingExtrinsics::::count(), 1); - assert!(PendingExtrinsics::::get(0).is_some()); - - // Run on_initialize - MevShield::on_initialize(2); - - // Verify storage was cleared but NextPendingExtrinsicIndex stays (unique auto-increment) - assert!(PendingExtrinsics::::get(0).is_none()); - assert_eq!(NextPendingExtrinsicIndex::::get(), 1); - assert_eq!(PendingExtrinsics::::count(), 0); - - // Verify ExtrinsicDispatched event was emitted - System::assert_has_event(crate::Event::::ExtrinsicDispatched { index: 0 }.into()); - }); - } - - #[test] - fn on_initialize_handles_decode_failure() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - // Store invalid bytes that can't be decoded as a call - let invalid_bytes = BoundedVec::truncate_from(vec![0xFF, 0xFF, 0xFF, 0xFF]); - - assert_ok!(MevShield::store_encrypted( - RuntimeOrigin::signed(1), - invalid_bytes, - )); - - // Run on_initialize - MevShield::on_initialize(2); - - // Verify storage was cleared - assert!(PendingExtrinsics::::get(0).is_none()); - - // Verify ExtrinsicDecodeFailed event was emitted - System::assert_has_event( - crate::Event::::ExtrinsicDecodeFailed { index: 0 }.into(), - ); - }); - } - - #[test] - fn on_initialize_handles_dispatch_failure() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - // A root-only call dispatched from a signed origin will fail. - let failing_call = - RuntimeCall::System(frame_system::Call::set_heap_pages { pages: 64 }); - - assert_ok!(MevShield::store_encrypted( - RuntimeOrigin::signed(1), - BoundedVec::truncate_from(failing_call.encode()), - )); - - // Verify there is 1 pending extrinsic - assert_eq!(NextPendingExtrinsicIndex::::get(), 1); - assert_eq!(PendingExtrinsics::::count(), 1); - assert!(PendingExtrinsics::::get(0).is_some()); - - // Run on_initialize - MevShield::on_initialize(2); - - // Verify storage was cleared - assert!(PendingExtrinsics::::get(0).is_none()); - - // Verify the call failed - System::assert_has_event( - crate::Event::::ExtrinsicDispatchFailed { - index: 0, - error: sp_runtime::DispatchError::BadOrigin, - } - .into(), - ); - }); - } - - #[test] - fn store_encrypted_rejects_when_full() { - new_test_ext().execute_with(|| { - let max = MaxPendingExtrinsicsLimit::::get(); - - let call = RuntimeCall::System(frame_system::Call::remark { remark: vec![1] }); - let encoded_call = BoundedVec::truncate_from(call.encode()); - - // Fill up the pending extrinsics storage to max - for _ in 0..max { - assert_ok!(MevShield::store_encrypted( - RuntimeOrigin::signed(1), - encoded_call.clone(), - )); - } - - // The next one should fail - assert_noop!( - MevShield::store_encrypted(RuntimeOrigin::signed(1), encoded_call), - Error::::TooManyPendingExtrinsics - ); - }); - } - - #[test] - fn on_initialize_processes_mixed_success_and_failure() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - // Store a valid call - let valid_call = RuntimeCall::System(frame_system::Call::remark { - remark: vec![1, 2, 3], - }); - assert_ok!(MevShield::store_encrypted( - RuntimeOrigin::signed(1), - BoundedVec::truncate_from(valid_call.encode()), - )); - - // Store invalid bytes - let invalid_bytes = BoundedVec::truncate_from(vec![0xFF, 0xFF]); - assert_ok!(MevShield::store_encrypted( - RuntimeOrigin::signed(1), - invalid_bytes, - )); - - // Store another valid call - let valid_call2 = RuntimeCall::System(frame_system::Call::remark { - remark: vec![4, 5, 6], - }); - assert_ok!(MevShield::store_encrypted( - RuntimeOrigin::signed(1), - BoundedVec::truncate_from(valid_call2.encode()), - )); - - // Run on_initialize - MevShield::on_initialize(2); - - // Verify storage was cleared - assert!(PendingExtrinsics::::get(0).is_none()); - assert!(PendingExtrinsics::::get(1).is_none()); - assert!(PendingExtrinsics::::get(2).is_none()); - - // Verify correct events were emitted - System::assert_has_event(crate::Event::::ExtrinsicDispatched { index: 0 }.into()); - System::assert_has_event( - crate::Event::::ExtrinsicDecodeFailed { index: 1 }.into(), - ); - System::assert_has_event(crate::Event::::ExtrinsicDispatched { index: 2 }.into()); - }); - } - - #[test] - fn on_initialize_expires_old_extrinsics() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - // Store an extrinsic at block 1 - let call = RuntimeCall::System(frame_system::Call::remark { - remark: vec![1, 2, 3], - }); - assert_ok!(MevShield::store_encrypted( - RuntimeOrigin::signed(1), - BoundedVec::truncate_from(call.encode()), - )); - - // Verify the extrinsic was stored with submitted_at = 1 - let pending = PendingExtrinsics::::get(0).unwrap(); - assert_eq!(pending.submitted_at, 1); - - // Run on_initialize at block 12 (1 + 10 + 1 = 12, which is > MAX_EXTRINSIC_LIFETIME) - // MAX_EXTRINSIC_LIFETIME is 10, so at block 12, age is 11 which exceeds the limit - System::set_block_number(12); - MevShield::on_initialize(12); - - // Verify storage was cleared - assert!(PendingExtrinsics::::get(0).is_none()); - assert_eq!(PendingExtrinsics::::count(), 0); - - // Verify ExtrinsicExpired event was emitted (not ExtrinsicDispatched) - System::assert_has_event(crate::Event::::ExtrinsicExpired { index: 0 }.into()); - }); - } - - #[test] - fn on_initialize_does_not_expire_recent_extrinsics() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - // Store an extrinsic at block 1 - let call = RuntimeCall::System(frame_system::Call::remark { - remark: vec![1, 2, 3], - }); - assert_ok!(MevShield::store_encrypted( - RuntimeOrigin::signed(1), - BoundedVec::truncate_from(call.encode()), - )); - - // Run on_initialize at block 11 (age is 10, which equals MAX_EXTRINSIC_LIFETIME) - // Should NOT expire since we check age > MAX, not age >= - System::set_block_number(11); - MevShield::on_initialize(11); - - // Verify storage was cleared (extrinsic was dispatched, not expired) - assert!(PendingExtrinsics::::get(0).is_none()); - - // Verify ExtrinsicDispatched event was emitted (not ExtrinsicExpired) - System::assert_has_event(crate::Event::::ExtrinsicDispatched { index: 0 }.into()); - }); - } - - #[test] - fn on_initialize_emits_dispatch_failed_on_bad_origin() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - // set_heap_pages requires Root origin, so dispatching with Signed will fail - let call = RuntimeCall::System(frame_system::Call::set_heap_pages { pages: 10 }); - assert_ok!(MevShield::store_encrypted( - RuntimeOrigin::signed(1), - BoundedVec::truncate_from(call.encode()), - )); - - // Run on_initialize - MevShield::on_initialize(2); - - // Verify storage was cleared - assert!(PendingExtrinsics::::get(0).is_none()); - assert_eq!(PendingExtrinsics::::count(), 0); - - // Verify ExtrinsicDispatchFailed event was emitted - System::assert_has_event( - crate::Event::::ExtrinsicDispatchFailed { - index: 0, - error: sp_runtime::DispatchError::BadOrigin, - } - .into(), - ); - }); - } - - #[test] - fn on_initialize_handles_missing_slots() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - // Manually create a gap in indices by directly manipulating storage - let call = RuntimeCall::System(frame_system::Call::remark { - remark: vec![1, 2, 3], - }); - let pending = PendingExtrinsic:: { - who: 1, - encrypted_call: BoundedVec::truncate_from(call.encode()), - submitted_at: 1, - }; - - // Insert at index 5, leaving 0-4 empty - PendingExtrinsics::::insert(5, pending); - NextPendingExtrinsicIndex::::put(6); - - // Run on_initialize - should handle the gap and process index 5 - MevShield::on_initialize(2); - - // Verify the extrinsic at index 5 was processed - assert!(PendingExtrinsics::::get(5).is_none()); - assert_eq!(PendingExtrinsics::::count(), 0); - - // Verify ExtrinsicDispatched event for index 5 - System::assert_has_event(crate::Event::::ExtrinsicDispatched { index: 5 }.into()); - }); - } - - #[test] - fn multiple_accounts_dispatch_with_correct_origins() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - let user_a: u64 = 100; - let user_b: u64 = 200; - - // User A submits a remark_with_event - let call_a = - RuntimeCall::System(frame_system::Call::remark_with_event { remark: vec![0xAA] }); - assert_ok!(MevShield::store_encrypted( - RuntimeOrigin::signed(user_a), - BoundedVec::truncate_from(call_a.encode()), - )); - - // User B submits a remark_with_event - let call_b = - RuntimeCall::System(frame_system::Call::remark_with_event { remark: vec![0xBB] }); - assert_ok!(MevShield::store_encrypted( - RuntimeOrigin::signed(user_b), - BoundedVec::truncate_from(call_b.encode()), - )); - - // Run on_initialize - MevShield::on_initialize(2); - - // Verify both events have correct senders - let hash_a = ::Hashing::hash(&[0xAAu8]); - let hash_b = ::Hashing::hash(&[0xBBu8]); - - System::assert_has_event( - frame_system::Event::::Remarked { - sender: user_a, - hash: hash_a, - } - .into(), - ); - System::assert_has_event( - frame_system::Event::::Remarked { - sender: user_b, - hash: hash_b, - } - .into(), - ); - }); - } - - #[test] - fn expiration_mixed_with_valid_extrinsics() { - new_test_ext().execute_with(|| { - // Submit first extrinsic at block 1 - System::set_block_number(1); - let old_call = RuntimeCall::System(frame_system::Call::remark { remark: vec![0x01] }); - assert_ok!(MevShield::store_encrypted( - RuntimeOrigin::signed(1), - BoundedVec::truncate_from(old_call.encode()), - )); - - // Submit second extrinsic at block 10 - System::set_block_number(10); - let new_call = RuntimeCall::System(frame_system::Call::remark { remark: vec![0x02] }); - assert_ok!(MevShield::store_encrypted( - RuntimeOrigin::signed(2), - BoundedVec::truncate_from(new_call.encode()), - )); - - // Run on_initialize at block 12 - // First extrinsic: age = 12 - 1 = 11 > 10, should expire - // Second extrinsic: age = 12 - 10 = 2 <= 10, should dispatch - System::set_block_number(12); - MevShield::on_initialize(12); - - // Verify both were removed from storage - assert!(PendingExtrinsics::::get(0).is_none()); - assert!(PendingExtrinsics::::get(1).is_none()); - assert_eq!(PendingExtrinsics::::count(), 0); - - // Verify first expired, second dispatched - System::assert_has_event(crate::Event::::ExtrinsicExpired { index: 0 }.into()); - System::assert_has_event(crate::Event::::ExtrinsicDispatched { index: 1 }.into()); - }); - } - - #[test] - fn set_max_pending_extrinsics_number_works() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - // Default is 100 - assert_eq!(MaxPendingExtrinsicsLimit::::get(), 100); - - assert_ok!(MevShield::set_max_pending_extrinsics_number( - RuntimeOrigin::root(), - 50, - )); - - assert_eq!(MaxPendingExtrinsicsLimit::::get(), 50); - - System::assert_last_event( - crate::Event::::MaxPendingExtrinsicsNumberSet { value: 50 }.into(), - ); - }); - } - - #[test] - fn set_max_pending_extrinsics_number_rejects_signed_origin() { - new_test_ext().execute_with(|| { - assert_noop!( - MevShield::set_max_pending_extrinsics_number(RuntimeOrigin::signed(1), 50), - sp_runtime::DispatchError::BadOrigin - ); - }); - } - - #[test] - fn set_max_pending_extrinsics_number_enforced_on_store() { - new_test_ext().execute_with(|| { - // Set limit to 2 - assert_ok!(MevShield::set_max_pending_extrinsics_number( - RuntimeOrigin::root(), - 2, - )); - - let call = RuntimeCall::System(frame_system::Call::remark { remark: vec![1] }); - let encoded_call = BoundedVec::truncate_from(call.encode()); - - // First two should succeed - assert_ok!(MevShield::store_encrypted( - RuntimeOrigin::signed(1), - encoded_call.clone(), - )); - assert_ok!(MevShield::store_encrypted( - RuntimeOrigin::signed(1), - encoded_call.clone(), - )); - - // Third should fail - assert_noop!( - MevShield::store_encrypted(RuntimeOrigin::signed(1), encoded_call), - Error::::TooManyPendingExtrinsics - ); - }); - } - - #[test] - fn set_on_initialize_weight_works() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - assert_eq!( - OnInitializeWeight::::get(), - crate::DEFAULT_ON_INITIALIZE_WEIGHT - ); - - assert_ok!(MevShield::set_on_initialize_weight( - RuntimeOrigin::root(), - 1_000_000, - )); - - assert_eq!(OnInitializeWeight::::get(), 1_000_000); - - System::assert_last_event( - crate::Event::::OnInitializeWeightSet { value: 1_000_000 }.into(), - ); - }); - } - - #[test] - fn set_on_initialize_weight_rejects_signed_origin() { - new_test_ext().execute_with(|| { - assert_noop!( - MevShield::set_on_initialize_weight(RuntimeOrigin::signed(1), 1_000_000), - sp_runtime::DispatchError::BadOrigin - ); - }); - } - - #[test] - fn set_on_initialize_weight_rejects_above_absolute_max() { - new_test_ext().execute_with(|| { - // Exactly at absolute max should succeed - assert_ok!(MevShield::set_on_initialize_weight( - RuntimeOrigin::root(), - crate::MAX_ON_INITIALIZE_WEIGHT, - )); - - // Above absolute max should fail - assert_noop!( - MevShield::set_on_initialize_weight( - RuntimeOrigin::root(), - crate::MAX_ON_INITIALIZE_WEIGHT + 1, - ), - Error::::WeightExceedsAbsoluteMax - ); - }); - } - - #[test] - fn set_on_initialize_weight_enforced_on_processing() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - // Set weight to 0 so nothing can be processed - assert_ok!(MevShield::set_on_initialize_weight( - RuntimeOrigin::root(), - 0, - )); - - // Store an extrinsic - let call = RuntimeCall::System(frame_system::Call::remark { remark: vec![1] }); - assert_ok!(MevShield::store_encrypted( - RuntimeOrigin::signed(1), - BoundedVec::truncate_from(call.encode()), - )); - - assert_eq!(PendingExtrinsics::::count(), 1); - - // Run on_initialize — should postpone due to weight limit - MevShield::on_initialize(2); - - // Extrinsic should still be pending (postponed) - assert_eq!(PendingExtrinsics::::count(), 1); - System::assert_has_event(crate::Event::::ExtrinsicPostponed { index: 0 }.into()); - }); - } - - #[test] - fn set_stored_extrinsic_lifetime_works() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - assert_eq!( - ExtrinsicLifetime::::get(), - crate::DEFAULT_EXTRINSIC_LIFETIME - ); - - assert_ok!(MevShield::set_stored_extrinsic_lifetime( - RuntimeOrigin::root(), - 20 - )); - - assert_eq!(ExtrinsicLifetime::::get(), 20); - - System::assert_last_event( - crate::Event::::ExtrinsicLifetimeSet { value: 20 }.into(), - ); - }); - } - - #[test] - fn set_stored_extrinsic_lifetime_rejects_signed_origin() { - new_test_ext().execute_with(|| { - assert_noop!( - MevShield::set_stored_extrinsic_lifetime(RuntimeOrigin::signed(1), 20), - sp_runtime::DispatchError::BadOrigin - ); - }); - } - - #[test] - fn set_stored_extrinsic_lifetime_enforced_on_expiration() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - // Set lifetime to 2 blocks - assert_ok!(MevShield::set_stored_extrinsic_lifetime( - RuntimeOrigin::root(), - 2 - )); - - // Store an extrinsic at block 1 - let call = RuntimeCall::System(frame_system::Call::remark { remark: vec![1] }); - assert_ok!(MevShield::store_encrypted( - RuntimeOrigin::signed(1), - BoundedVec::truncate_from(call.encode()), - )); - - // At block 4: age = 4 - 1 = 3 > 2, should expire - System::set_block_number(4); - MevShield::on_initialize(4); - - assert!(PendingExtrinsics::::get(0).is_none()); - assert_eq!(PendingExtrinsics::::count(), 0); - System::assert_has_event(crate::Event::::ExtrinsicExpired { index: 0 }.into()); - }); - } - - #[test] - fn set_max_extrinsic_weight_works() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - assert_eq!( - MaxExtrinsicWeight::::get(), - crate::DEFAULT_MAX_EXTRINSIC_WEIGHT - ); - - assert_ok!(MevShield::set_max_extrinsic_weight( - RuntimeOrigin::root(), - 1_000_000, - )); - - assert_eq!(MaxExtrinsicWeight::::get(), 1_000_000); - - System::assert_last_event( - crate::Event::::MaxExtrinsicWeightSet { value: 1_000_000 }.into(), - ); - }); - } - - #[test] - fn set_max_extrinsic_weight_rejects_signed_origin() { - new_test_ext().execute_with(|| { - assert_noop!( - MevShield::set_max_extrinsic_weight(RuntimeOrigin::signed(1), 1_000_000), - sp_runtime::DispatchError::BadOrigin - ); - }); - } - - #[test] - fn set_max_extrinsic_weight_rejects_above_absolute_max() { - new_test_ext().execute_with(|| { - // Exactly at absolute max should succeed - assert_ok!(MevShield::set_max_extrinsic_weight( - RuntimeOrigin::root(), - crate::MAX_ON_INITIALIZE_WEIGHT, - )); - - // Above absolute max should fail - assert_noop!( - MevShield::set_max_extrinsic_weight( - RuntimeOrigin::root(), - crate::MAX_ON_INITIALIZE_WEIGHT + 1, - ), - Error::::WeightExceedsAbsoluteMax - ); - }); - } - - #[test] - fn max_extrinsic_weight_is_enforced() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - // Set per-extrinsic weight to 0 so all extrinsics exceed the limit - assert_ok!(MevShield::set_max_extrinsic_weight( - RuntimeOrigin::root(), - 0, - )); - - // Store an extrinsic - let call = RuntimeCall::System(frame_system::Call::remark { remark: vec![1] }); - assert_ok!(MevShield::store_encrypted( - RuntimeOrigin::signed(1), - BoundedVec::truncate_from(call.encode()), - )); - - assert_eq!(PendingExtrinsics::::count(), 1); - - // Run on_initialize — should remove the extrinsic (weight exceeded) - MevShield::on_initialize(2); - - // Extrinsic should be removed (not postponed) - assert_eq!(PendingExtrinsics::::count(), 0); - assert!(PendingExtrinsics::::get(0).is_none()); - System::assert_has_event( - crate::Event::::ExtrinsicWeightExceeded { index: 0 }.into(), - ); - }); - } -} diff --git a/pallets/shield/src/tests/admin_queue_limits.rs b/pallets/shield/src/tests/admin_queue_limits.rs new file mode 100644 index 0000000000..2258b48e0a --- /dev/null +++ b/pallets/shield/src/tests/admin_queue_limits.rs @@ -0,0 +1,295 @@ +//! Tests for root-configurable shield queue weight and lifetime limits. + +use crate::mock::*; +use crate::{ + Error, ExtrinsicLifetime, MaxExtrinsicWeight, MaxPendingExtrinsicsLimit, OnInitializeWeight, + PendingExtrinsics, +}; +use codec::Encode; +use frame_support::traits::Hooks; +use frame_support::{BoundedVec, assert_noop, assert_ok}; + +#[test] +fn set_max_pending_extrinsics_number_works() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + // Default is 100 + assert_eq!(MaxPendingExtrinsicsLimit::::get(), 100); + + assert_ok!(MevShield::set_max_pending_extrinsics_number( + RuntimeOrigin::root(), + 50, + )); + + assert_eq!(MaxPendingExtrinsicsLimit::::get(), 50); + + System::assert_last_event( + crate::Event::::MaxPendingExtrinsicsNumberSet { value: 50 }.into(), + ); + }); +} + +#[test] +fn set_max_pending_extrinsics_number_rejects_signed_origin() { + new_test_ext().execute_with(|| { + assert_noop!( + MevShield::set_max_pending_extrinsics_number(RuntimeOrigin::signed(1), 50), + sp_runtime::DispatchError::BadOrigin + ); + }); +} + +#[test] +fn set_max_pending_extrinsics_number_enforced_on_store() { + new_test_ext().execute_with(|| { + // Set limit to 2 + assert_ok!(MevShield::set_max_pending_extrinsics_number( + RuntimeOrigin::root(), + 2, + )); + + let call = RuntimeCall::System(frame_system::Call::remark { remark: vec![1] }); + let encoded_call = BoundedVec::truncate_from(call.encode()); + + // First two should succeed + assert_ok!(MevShield::store_encrypted( + RuntimeOrigin::signed(1), + encoded_call.clone(), + )); + assert_ok!(MevShield::store_encrypted( + RuntimeOrigin::signed(1), + encoded_call.clone(), + )); + + // Third should fail + assert_noop!( + MevShield::store_encrypted(RuntimeOrigin::signed(1), encoded_call), + Error::::TooManyPendingExtrinsics + ); + }); +} + +#[test] +fn set_on_initialize_weight_works() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + assert_eq!( + OnInitializeWeight::::get(), + crate::DEFAULT_ON_INITIALIZE_WEIGHT + ); + + assert_ok!(MevShield::set_on_initialize_weight( + RuntimeOrigin::root(), + 1_000_000, + )); + + assert_eq!(OnInitializeWeight::::get(), 1_000_000); + + System::assert_last_event( + crate::Event::::OnInitializeWeightSet { value: 1_000_000 }.into(), + ); + }); +} + +#[test] +fn set_on_initialize_weight_rejects_signed_origin() { + new_test_ext().execute_with(|| { + assert_noop!( + MevShield::set_on_initialize_weight(RuntimeOrigin::signed(1), 1_000_000), + sp_runtime::DispatchError::BadOrigin + ); + }); +} + +#[test] +fn set_on_initialize_weight_rejects_above_absolute_max() { + new_test_ext().execute_with(|| { + // Exactly at absolute max should succeed + assert_ok!(MevShield::set_on_initialize_weight( + RuntimeOrigin::root(), + crate::MAX_ON_INITIALIZE_WEIGHT, + )); + + // Above absolute max should fail + assert_noop!( + MevShield::set_on_initialize_weight( + RuntimeOrigin::root(), + crate::MAX_ON_INITIALIZE_WEIGHT + 1, + ), + Error::::WeightExceedsAbsoluteMax + ); + }); +} + +#[test] +fn set_on_initialize_weight_enforced_on_processing() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + // Set weight to 0 so nothing can be processed + assert_ok!(MevShield::set_on_initialize_weight( + RuntimeOrigin::root(), + 0, + )); + + // Store an extrinsic + let call = RuntimeCall::System(frame_system::Call::remark { remark: vec![1] }); + assert_ok!(MevShield::store_encrypted( + RuntimeOrigin::signed(1), + BoundedVec::truncate_from(call.encode()), + )); + + assert_eq!(PendingExtrinsics::::count(), 1); + + // Run on_initialize — should postpone due to weight limit + MevShield::on_initialize(2); + + // Extrinsic should still be pending (postponed) + assert_eq!(PendingExtrinsics::::count(), 1); + System::assert_has_event(crate::Event::::ExtrinsicPostponed { index: 0 }.into()); + }); +} + +#[test] +fn set_stored_extrinsic_lifetime_works() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + assert_eq!( + ExtrinsicLifetime::::get(), + crate::DEFAULT_EXTRINSIC_LIFETIME + ); + + assert_ok!(MevShield::set_stored_extrinsic_lifetime( + RuntimeOrigin::root(), + 20 + )); + + assert_eq!(ExtrinsicLifetime::::get(), 20); + + System::assert_last_event(crate::Event::::ExtrinsicLifetimeSet { value: 20 }.into()); + }); +} + +#[test] +fn set_stored_extrinsic_lifetime_rejects_signed_origin() { + new_test_ext().execute_with(|| { + assert_noop!( + MevShield::set_stored_extrinsic_lifetime(RuntimeOrigin::signed(1), 20), + sp_runtime::DispatchError::BadOrigin + ); + }); +} + +#[test] +fn set_stored_extrinsic_lifetime_enforced_on_expiration() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + // Set lifetime to 2 blocks + assert_ok!(MevShield::set_stored_extrinsic_lifetime( + RuntimeOrigin::root(), + 2 + )); + + // Store an extrinsic at block 1 + let call = RuntimeCall::System(frame_system::Call::remark { remark: vec![1] }); + assert_ok!(MevShield::store_encrypted( + RuntimeOrigin::signed(1), + BoundedVec::truncate_from(call.encode()), + )); + + // At block 4: age = 4 - 1 = 3 > 2, should expire + System::set_block_number(4); + MevShield::on_initialize(4); + + assert!(PendingExtrinsics::::get(0).is_none()); + assert_eq!(PendingExtrinsics::::count(), 0); + System::assert_has_event(crate::Event::::ExtrinsicExpired { index: 0 }.into()); + }); +} + +#[test] +fn set_max_extrinsic_weight_works() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + assert_eq!( + MaxExtrinsicWeight::::get(), + crate::DEFAULT_MAX_EXTRINSIC_WEIGHT + ); + + assert_ok!(MevShield::set_max_extrinsic_weight( + RuntimeOrigin::root(), + 1_000_000, + )); + + assert_eq!(MaxExtrinsicWeight::::get(), 1_000_000); + + System::assert_last_event( + crate::Event::::MaxExtrinsicWeightSet { value: 1_000_000 }.into(), + ); + }); +} + +#[test] +fn set_max_extrinsic_weight_rejects_signed_origin() { + new_test_ext().execute_with(|| { + assert_noop!( + MevShield::set_max_extrinsic_weight(RuntimeOrigin::signed(1), 1_000_000), + sp_runtime::DispatchError::BadOrigin + ); + }); +} + +#[test] +fn set_max_extrinsic_weight_rejects_above_absolute_max() { + new_test_ext().execute_with(|| { + // Exactly at absolute max should succeed + assert_ok!(MevShield::set_max_extrinsic_weight( + RuntimeOrigin::root(), + crate::MAX_ON_INITIALIZE_WEIGHT, + )); + + // Above absolute max should fail + assert_noop!( + MevShield::set_max_extrinsic_weight( + RuntimeOrigin::root(), + crate::MAX_ON_INITIALIZE_WEIGHT + 1, + ), + Error::::WeightExceedsAbsoluteMax + ); + }); +} + +#[test] +fn max_extrinsic_weight_is_enforced() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + // Set per-extrinsic weight to 0 so all extrinsics exceed the limit + assert_ok!(MevShield::set_max_extrinsic_weight( + RuntimeOrigin::root(), + 0, + )); + + // Store an extrinsic + let call = RuntimeCall::System(frame_system::Call::remark { remark: vec![1] }); + assert_ok!(MevShield::store_encrypted( + RuntimeOrigin::signed(1), + BoundedVec::truncate_from(call.encode()), + )); + + assert_eq!(PendingExtrinsics::::count(), 1); + + // Run on_initialize — should remove the extrinsic (weight exceeded) + MevShield::on_initialize(2); + + // Extrinsic should be removed (not postponed) + assert_eq!(PendingExtrinsics::::count(), 0); + assert!(PendingExtrinsics::::get(0).is_none()); + System::assert_has_event(crate::Event::::ExtrinsicWeightExceeded { index: 0 }.into()); + }); +} diff --git a/pallets/shield/src/tests/announce_next_key.rs b/pallets/shield/src/tests/announce_next_key.rs new file mode 100644 index 0000000000..60fbb4663f --- /dev/null +++ b/pallets/shield/src/tests/announce_next_key.rs @@ -0,0 +1,190 @@ +//! Tests for `announce_next_key` key rotation and author-key bookkeeping. + +use crate::mock::*; +use crate::{ + AuthorKeys, CurrentKey, Error, NextKey, NextKeyExpiresAt, PendingKey, PendingKeyExpiresAt, +}; +use frame_support::{BoundedVec, assert_noop, assert_ok}; +use stp_shield::{MLKEM768_ENC_KEY_LEN, ShieldEncKey}; + +/// Simulates a 3-validator round-robin (authors 1, 2, 3) over 5 blocks. +/// Each block calls `announce_next_key` and verifies the full pipeline: +/// CurrentKey, PendingKey, NextKey, AuthorKeys, expirations, and +/// `is_shielded_using_current_key`. +#[test] +fn key_rotation_round_robin() { + new_test_ext().execute_with(|| { + let key_of = + |n: u8| -> ShieldEncKey { BoundedVec::truncate_from(vec![n; MLKEM768_ENC_KEY_LEN]) }; + let hash_of = |pk: &ShieldEncKey| sp_io::hashing::twox_128(&pk[..]); + + // 3 validators in round-robin: 1, 2, 3, 1, 2. + let authors = [1u8, 2, 3, 1, 2]; + let next_next = |block: usize| -> Option { authors.get(block + 2).copied() }; + + // ── Block 1: author=1, next_next=3 ────────────────────────────── + // Pipeline is empty; author(3) has no AuthorKeys yet. + System::set_block_number(1); + set_mock_authors(Some(author(1)), next_next(0).map(author)); + assert_ok!(MevShield::announce_next_key( + RuntimeOrigin::none(), + Some(key_of(1)), + )); + + assert!(CurrentKey::::get().is_none()); + assert!(PendingKey::::get().is_none()); + assert!(NextKey::::get().is_none()); + assert_eq!(AuthorKeys::::get(author(1)), Some(key_of(1))); + assert!(PendingKeyExpiresAt::::get().is_none()); + assert!(NextKeyExpiresAt::::get().is_none()); + // Nothing in PendingKey → is_shielded always false. + assert!(!MevShield::is_shielded_using_current_key(&[0xFF; 16])); + + // ── Block 2: author=2, next_next=1 ────────────────────────────── + // author(1) registered in block 1 → NextKey picks up key_of(1). + System::set_block_number(2); + set_mock_authors(Some(author(2)), next_next(1).map(author)); + assert_ok!(MevShield::announce_next_key( + RuntimeOrigin::none(), + Some(key_of(2)), + )); + + assert!(CurrentKey::::get().is_none()); + assert!(PendingKey::::get().is_none()); + assert_eq!(NextKey::::get(), Some(key_of(1))); + assert_eq!(AuthorKeys::::get(author(2)), Some(key_of(2))); + assert!(PendingKeyExpiresAt::::get().is_none()); + assert_eq!(NextKeyExpiresAt::::get(), Some(5)); // 2 + 3 + + // ── Block 3: author=3, next_next=2 ────────────────────────────── + // NextKey(key_of(1)) → PendingKey; next_next=author(2) has key_of(2) → NextKey. + System::set_block_number(3); + set_mock_authors(Some(author(3)), next_next(2).map(author)); + assert_ok!(MevShield::announce_next_key( + RuntimeOrigin::none(), + Some(key_of(3)), + )); + + assert!(CurrentKey::::get().is_none()); + assert_eq!(PendingKey::::get(), Some(key_of(1))); + assert_eq!(NextKey::::get(), Some(key_of(2))); + assert_eq!(AuthorKeys::::get(author(3)), Some(key_of(3))); + assert_eq!(PendingKeyExpiresAt::::get(), Some(5)); // 3 + 2 + assert_eq!(NextKeyExpiresAt::::get(), Some(6)); // 3 + 3 + // PendingKey = key_of(1) → is_shielded matches its hash. + assert!(MevShield::is_shielded_using_current_key(&hash_of(&key_of( + 1 + )))); + assert!(!MevShield::is_shielded_using_current_key(&hash_of( + &key_of(2) + ))); + assert!(!MevShield::is_shielded_using_current_key(&[0xFF; 16])); + + // ── Block 4: author=1, next_next=out of bounds ────────────────── + // Full pipeline: PendingKey(key_of(1)) → CurrentKey, NextKey(key_of(2)) → PendingKey. + System::set_block_number(4); + set_mock_authors(Some(author(1)), next_next(3).map(author)); + assert_ok!(MevShield::announce_next_key( + RuntimeOrigin::none(), + Some(key_of(1)), + )); + + assert_eq!(CurrentKey::::get(), Some(key_of(1))); + assert_eq!(PendingKey::::get(), Some(key_of(2))); + assert!(NextKey::::get().is_none()); + assert_eq!(AuthorKeys::::get(author(1)), Some(key_of(1))); + assert_eq!(PendingKeyExpiresAt::::get(), Some(6)); // 4 + 2 + assert!(NextKeyExpiresAt::::get().is_none()); + // PendingKey = key_of(2). + assert!(MevShield::is_shielded_using_current_key(&hash_of(&key_of( + 2 + )))); + assert!(!MevShield::is_shielded_using_current_key(&hash_of( + &key_of(1) + ))); + + // ── Block 5: author=2, next_next=none ─────────────────────────── + // PendingKey(key_of(2)) → CurrentKey; pipeline drains. + System::set_block_number(5); + set_mock_authors(Some(author(2)), None); + assert_ok!(MevShield::announce_next_key( + RuntimeOrigin::none(), + Some(key_of(2)), + )); + + assert_eq!(CurrentKey::::get(), Some(key_of(2))); + assert!(PendingKey::::get().is_none()); + assert!(NextKey::::get().is_none()); + assert!(PendingKeyExpiresAt::::get().is_none()); + assert!(NextKeyExpiresAt::::get().is_none()); + }); +} + +/// AuthorKeys is read *before* being updated, so when current == next_next +/// the NextKey picks up the old key, not the newly announced one. +#[test] +fn announce_rotations_use_pre_update_author_keys() { + new_test_ext().execute_with(|| { + set_mock_authors(Some(author(1)), Some(author(1))); + + let old_pk = valid_shield_enc_key(); + let new_pk = valid_shield_enc_key_b(); + AuthorKeys::::insert(author(1), old_pk.clone()); + + assert_ok!(MevShield::announce_next_key( + RuntimeOrigin::none(), + Some(new_pk.clone()), + )); + + assert_eq!(NextKey::::get(), Some(old_pk)); + assert_eq!(AuthorKeys::::get(author(1)), Some(new_pk)); + }); +} + +#[test] +fn announce_rejects_signed_origin() { + new_test_ext().execute_with(|| { + set_mock_authors(Some(author(1)), None); + assert_noop!( + MevShield::announce_next_key(RuntimeOrigin::signed(1), Some(valid_shield_enc_key())), + sp_runtime::DispatchError::BadOrigin + ); + }); +} + +#[test] +fn announce_rejects_bad_pk_length() { + new_test_ext().execute_with(|| { + set_mock_authors(Some(author(1)), None); + let bad_pk: ShieldEncKey = BoundedVec::truncate_from(vec![0x01; 100]); + + assert_noop!( + MevShield::announce_next_key(RuntimeOrigin::none(), Some(bad_pk)), + Error::::BadEncKeyLen + ); + }); +} + +#[test] +fn announce_none_pk_removes_author_key() { + new_test_ext().execute_with(|| { + set_mock_authors(Some(author(1)), None); + AuthorKeys::::insert(author(1), valid_shield_enc_key()); + + assert_ok!(MevShield::announce_next_key(RuntimeOrigin::none(), None)); + + assert!(AuthorKeys::::get(author(1)).is_none()); + }); +} + +#[test] +fn announce_fails_when_no_current_author() { + new_test_ext().execute_with(|| { + set_mock_authors(None, None); + + assert_noop!( + MevShield::announce_next_key(RuntimeOrigin::none(), Some(valid_shield_enc_key())), + Error::::Unreachable + ); + }); +} diff --git a/pallets/shield/src/tests/migrate_clear_v1_storage.rs b/pallets/shield/src/tests/migrate_clear_v1_storage.rs new file mode 100644 index 0000000000..95b53214c5 --- /dev/null +++ b/pallets/shield/src/tests/migrate_clear_v1_storage.rs @@ -0,0 +1,78 @@ +//! Tests for `migrate_clear_v1_storage` clearing removed MevShield v1 items. + +use crate::migrations::migrate_clear_v1_storage::migrate_clear_v1_storage; +use crate::mock::*; +use crate::{AuthorKeys, CurrentKey, HasMigrationRun, NextKey}; +use frame_support::BoundedVec; +use sp_io::hashing::twox_128; + +#[test] +fn migrate_clear_v1_storage_works() { + new_test_ext().execute_with(|| { + // Seed legacy storage that should be cleared. + seed_legacy_map("Submissions", 5); + seed_legacy_map("KeyHashByBlock", 3); + CurrentKey::::put(valid_shield_enc_key()); + + // Current storage that must survive. + NextKey::::put(valid_shield_enc_key()); + AuthorKeys::::insert(author(1), valid_shield_enc_key_b()); + + // Sanity: legacy values exist. + assert_eq!(count_keys("Submissions"), 5); + assert_eq!(count_keys("KeyHashByBlock"), 3); + assert!(CurrentKey::::get().is_some()); + + migrate_clear_v1_storage::(); + + // Legacy storage cleared. + assert_eq!(count_keys("Submissions"), 0); + assert_eq!(count_keys("KeyHashByBlock"), 0); + assert!(CurrentKey::::get().is_none()); + + // Current storage untouched. + assert_eq!(NextKey::::get(), Some(valid_shield_enc_key())); + assert_eq!( + AuthorKeys::::get(author(1)), + Some(valid_shield_enc_key_b()) + ); + + // Migration was recorded. + let mig_key = BoundedVec::truncate_from(b"migrate_clear_v1_storage".to_vec()); + assert!(HasMigrationRun::::get(&mig_key)); + + // Idempotent: re-run doesn't touch new data. + CurrentKey::::put(valid_shield_enc_key_b()); + migrate_clear_v1_storage::(); + assert_eq!(CurrentKey::::get(), Some(valid_shield_enc_key_b())); + }); +} + +fn seed_legacy_map(storage_name: &str, count: u32) { + let mut prefix = Vec::new(); + prefix.extend_from_slice(&twox_128(b"MevShield")); + prefix.extend_from_slice(&twox_128(storage_name.as_bytes())); + + for i in 0..count { + let mut key = prefix.clone(); + key.extend_from_slice(&i.to_le_bytes()); + sp_io::storage::set(&key, &[1u8; 32]); + } +} + +fn count_keys(storage_name: &str) -> u32 { + let mut prefix = Vec::new(); + prefix.extend_from_slice(&twox_128(b"MevShield")); + prefix.extend_from_slice(&twox_128(storage_name.as_bytes())); + + let mut count = 0u32; + let mut next_key = sp_io::storage::next_key(&prefix); + while let Some(key) = next_key { + if !key.starts_with(&prefix) { + break; + } + count += 1; + next_key = sp_io::storage::next_key(&key); + } + count +} diff --git a/pallets/shield/src/tests/mod.rs b/pallets/shield/src/tests/mod.rs new file mode 100644 index 0000000000..b19a7d63d8 --- /dev/null +++ b/pallets/shield/src/tests/mod.rs @@ -0,0 +1,9 @@ +//! Unit tests for `pallet-shield` (MEV shield encrypted extrinsic queue + key rotation). + +mod admin_queue_limits; +mod announce_next_key; +mod migrate_clear_v1_storage; +mod store_encrypted; +mod submit_encrypted; +mod try_decode_shielded_tx; +mod try_unshield_tx; diff --git a/pallets/shield/src/tests/store_encrypted.rs b/pallets/shield/src/tests/store_encrypted.rs new file mode 100644 index 0000000000..fa31c42f3e --- /dev/null +++ b/pallets/shield/src/tests/store_encrypted.rs @@ -0,0 +1,408 @@ +//! Tests for `store_encrypted` queueing and `on_initialize` pending-extrinsic processing. + +use crate::mock::*; +use crate::{ + Error, MaxPendingExtrinsicsLimit, NextPendingExtrinsicIndex, PendingExtrinsic, + PendingExtrinsics, +}; +use codec::Encode; +use frame_support::traits::Hooks; +use frame_support::{BoundedVec, assert_noop, assert_ok}; +use sp_runtime::traits::Hash; + +#[test] +fn store_encrypted_works() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + let call = RuntimeCall::System(frame_system::Call::remark { + remark: vec![1, 2, 3], + }); + let encoded_call = BoundedVec::truncate_from(call.encode()); + let who: u64 = 1; + + assert_ok!(MevShield::store_encrypted( + RuntimeOrigin::signed(who), + encoded_call.clone(), + )); + + // Verify the extrinsic was stored at index 0 with account ID + let expected = PendingExtrinsic:: { + who, + encrypted_call: encoded_call, + submitted_at: 1, + }; + assert_eq!(PendingExtrinsics::::get(0), Some(expected)); + assert_eq!(NextPendingExtrinsicIndex::::get(), 1); + assert_eq!(PendingExtrinsics::::count(), 1); + + // Verify event was emitted with index + System::assert_last_event(crate::Event::::ExtrinsicStored { index: 0, who }.into()); + }); +} + +#[test] +fn on_initialize_decodes_and_dispatches() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + // Store an encoded remark call + let call = RuntimeCall::System(frame_system::Call::remark { + remark: vec![1, 2, 3], + }); + let encoded_call = BoundedVec::truncate_from(call.encode()); + + assert_ok!(MevShield::store_encrypted( + RuntimeOrigin::signed(1), + encoded_call, + )); + + // Verify there's a pending extrinsic + assert_eq!(NextPendingExtrinsicIndex::::get(), 1); + assert_eq!(PendingExtrinsics::::count(), 1); + assert!(PendingExtrinsics::::get(0).is_some()); + + // Run on_initialize + MevShield::on_initialize(2); + + // Verify storage was cleared but NextPendingExtrinsicIndex stays (unique auto-increment) + assert!(PendingExtrinsics::::get(0).is_none()); + assert_eq!(NextPendingExtrinsicIndex::::get(), 1); + assert_eq!(PendingExtrinsics::::count(), 0); + + // Verify ExtrinsicDispatched event was emitted + System::assert_has_event(crate::Event::::ExtrinsicDispatched { index: 0 }.into()); + }); +} + +#[test] +fn on_initialize_handles_decode_failure() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + // Store invalid bytes that can't be decoded as a call + let invalid_bytes = BoundedVec::truncate_from(vec![0xFF, 0xFF, 0xFF, 0xFF]); + + assert_ok!(MevShield::store_encrypted( + RuntimeOrigin::signed(1), + invalid_bytes, + )); + + // Run on_initialize + MevShield::on_initialize(2); + + // Verify storage was cleared + assert!(PendingExtrinsics::::get(0).is_none()); + + // Verify ExtrinsicDecodeFailed event was emitted + System::assert_has_event(crate::Event::::ExtrinsicDecodeFailed { index: 0 }.into()); + }); +} + +#[test] +fn on_initialize_handles_dispatch_failure() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + // A root-only call dispatched from a signed origin will fail. + let failing_call = RuntimeCall::System(frame_system::Call::set_heap_pages { pages: 64 }); + + assert_ok!(MevShield::store_encrypted( + RuntimeOrigin::signed(1), + BoundedVec::truncate_from(failing_call.encode()), + )); + + // Verify there is 1 pending extrinsic + assert_eq!(NextPendingExtrinsicIndex::::get(), 1); + assert_eq!(PendingExtrinsics::::count(), 1); + assert!(PendingExtrinsics::::get(0).is_some()); + + // Run on_initialize + MevShield::on_initialize(2); + + // Verify storage was cleared + assert!(PendingExtrinsics::::get(0).is_none()); + + // Verify the call failed + System::assert_has_event( + crate::Event::::ExtrinsicDispatchFailed { + index: 0, + error: sp_runtime::DispatchError::BadOrigin, + } + .into(), + ); + }); +} + +#[test] +fn store_encrypted_rejects_when_full() { + new_test_ext().execute_with(|| { + let max = MaxPendingExtrinsicsLimit::::get(); + + let call = RuntimeCall::System(frame_system::Call::remark { remark: vec![1] }); + let encoded_call = BoundedVec::truncate_from(call.encode()); + + // Fill up the pending extrinsics storage to max + for _ in 0..max { + assert_ok!(MevShield::store_encrypted( + RuntimeOrigin::signed(1), + encoded_call.clone(), + )); + } + + // The next one should fail + assert_noop!( + MevShield::store_encrypted(RuntimeOrigin::signed(1), encoded_call), + Error::::TooManyPendingExtrinsics + ); + }); +} + +#[test] +fn on_initialize_processes_mixed_success_and_failure() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + // Store a valid call + let valid_call = RuntimeCall::System(frame_system::Call::remark { + remark: vec![1, 2, 3], + }); + assert_ok!(MevShield::store_encrypted( + RuntimeOrigin::signed(1), + BoundedVec::truncate_from(valid_call.encode()), + )); + + // Store invalid bytes + let invalid_bytes = BoundedVec::truncate_from(vec![0xFF, 0xFF]); + assert_ok!(MevShield::store_encrypted( + RuntimeOrigin::signed(1), + invalid_bytes, + )); + + // Store another valid call + let valid_call2 = RuntimeCall::System(frame_system::Call::remark { + remark: vec![4, 5, 6], + }); + assert_ok!(MevShield::store_encrypted( + RuntimeOrigin::signed(1), + BoundedVec::truncate_from(valid_call2.encode()), + )); + + // Run on_initialize + MevShield::on_initialize(2); + + // Verify storage was cleared + assert!(PendingExtrinsics::::get(0).is_none()); + assert!(PendingExtrinsics::::get(1).is_none()); + assert!(PendingExtrinsics::::get(2).is_none()); + + // Verify correct events were emitted + System::assert_has_event(crate::Event::::ExtrinsicDispatched { index: 0 }.into()); + System::assert_has_event(crate::Event::::ExtrinsicDecodeFailed { index: 1 }.into()); + System::assert_has_event(crate::Event::::ExtrinsicDispatched { index: 2 }.into()); + }); +} + +#[test] +fn on_initialize_expires_old_extrinsics() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + // Store an extrinsic at block 1 + let call = RuntimeCall::System(frame_system::Call::remark { + remark: vec![1, 2, 3], + }); + assert_ok!(MevShield::store_encrypted( + RuntimeOrigin::signed(1), + BoundedVec::truncate_from(call.encode()), + )); + + // Verify the extrinsic was stored with submitted_at = 1 + let pending = PendingExtrinsics::::get(0).unwrap(); + assert_eq!(pending.submitted_at, 1); + + // Run on_initialize at block 12 (1 + 10 + 1 = 12, which is > MAX_EXTRINSIC_LIFETIME) + // MAX_EXTRINSIC_LIFETIME is 10, so at block 12, age is 11 which exceeds the limit + System::set_block_number(12); + MevShield::on_initialize(12); + + // Verify storage was cleared + assert!(PendingExtrinsics::::get(0).is_none()); + assert_eq!(PendingExtrinsics::::count(), 0); + + // Verify ExtrinsicExpired event was emitted (not ExtrinsicDispatched) + System::assert_has_event(crate::Event::::ExtrinsicExpired { index: 0 }.into()); + }); +} + +#[test] +fn on_initialize_does_not_expire_recent_extrinsics() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + // Store an extrinsic at block 1 + let call = RuntimeCall::System(frame_system::Call::remark { + remark: vec![1, 2, 3], + }); + assert_ok!(MevShield::store_encrypted( + RuntimeOrigin::signed(1), + BoundedVec::truncate_from(call.encode()), + )); + + // Run on_initialize at block 11 (age is 10, which equals MAX_EXTRINSIC_LIFETIME) + // Should NOT expire since we check age > MAX, not age >= + System::set_block_number(11); + MevShield::on_initialize(11); + + // Verify storage was cleared (extrinsic was dispatched, not expired) + assert!(PendingExtrinsics::::get(0).is_none()); + + // Verify ExtrinsicDispatched event was emitted (not ExtrinsicExpired) + System::assert_has_event(crate::Event::::ExtrinsicDispatched { index: 0 }.into()); + }); +} + +#[test] +fn on_initialize_emits_dispatch_failed_on_bad_origin() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + // set_heap_pages requires Root origin, so dispatching with Signed will fail + let call = RuntimeCall::System(frame_system::Call::set_heap_pages { pages: 10 }); + assert_ok!(MevShield::store_encrypted( + RuntimeOrigin::signed(1), + BoundedVec::truncate_from(call.encode()), + )); + + // Run on_initialize + MevShield::on_initialize(2); + + // Verify storage was cleared + assert!(PendingExtrinsics::::get(0).is_none()); + assert_eq!(PendingExtrinsics::::count(), 0); + + // Verify ExtrinsicDispatchFailed event was emitted + System::assert_has_event( + crate::Event::::ExtrinsicDispatchFailed { + index: 0, + error: sp_runtime::DispatchError::BadOrigin, + } + .into(), + ); + }); +} + +#[test] +fn on_initialize_handles_missing_slots() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + // Manually create a gap in indices by directly manipulating storage + let call = RuntimeCall::System(frame_system::Call::remark { + remark: vec![1, 2, 3], + }); + let pending = PendingExtrinsic:: { + who: 1, + encrypted_call: BoundedVec::truncate_from(call.encode()), + submitted_at: 1, + }; + + // Insert at index 5, leaving 0-4 empty + PendingExtrinsics::::insert(5, pending); + NextPendingExtrinsicIndex::::put(6); + + // Run on_initialize - should handle the gap and process index 5 + MevShield::on_initialize(2); + + // Verify the extrinsic at index 5 was processed + assert!(PendingExtrinsics::::get(5).is_none()); + assert_eq!(PendingExtrinsics::::count(), 0); + + // Verify ExtrinsicDispatched event for index 5 + System::assert_has_event(crate::Event::::ExtrinsicDispatched { index: 5 }.into()); + }); +} + +#[test] +fn multiple_accounts_dispatch_with_correct_origins() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + let user_a: u64 = 100; + let user_b: u64 = 200; + + // User A submits a remark_with_event + let call_a = + RuntimeCall::System(frame_system::Call::remark_with_event { remark: vec![0xAA] }); + assert_ok!(MevShield::store_encrypted( + RuntimeOrigin::signed(user_a), + BoundedVec::truncate_from(call_a.encode()), + )); + + // User B submits a remark_with_event + let call_b = + RuntimeCall::System(frame_system::Call::remark_with_event { remark: vec![0xBB] }); + assert_ok!(MevShield::store_encrypted( + RuntimeOrigin::signed(user_b), + BoundedVec::truncate_from(call_b.encode()), + )); + + // Run on_initialize + MevShield::on_initialize(2); + + // Verify both events have correct senders + let hash_a = ::Hashing::hash(&[0xAAu8]); + let hash_b = ::Hashing::hash(&[0xBBu8]); + + System::assert_has_event( + frame_system::Event::::Remarked { + sender: user_a, + hash: hash_a, + } + .into(), + ); + System::assert_has_event( + frame_system::Event::::Remarked { + sender: user_b, + hash: hash_b, + } + .into(), + ); + }); +} + +#[test] +fn expiration_mixed_with_valid_extrinsics() { + new_test_ext().execute_with(|| { + // Submit first extrinsic at block 1 + System::set_block_number(1); + let old_call = RuntimeCall::System(frame_system::Call::remark { remark: vec![0x01] }); + assert_ok!(MevShield::store_encrypted( + RuntimeOrigin::signed(1), + BoundedVec::truncate_from(old_call.encode()), + )); + + // Submit second extrinsic at block 10 + System::set_block_number(10); + let new_call = RuntimeCall::System(frame_system::Call::remark { remark: vec![0x02] }); + assert_ok!(MevShield::store_encrypted( + RuntimeOrigin::signed(2), + BoundedVec::truncate_from(new_call.encode()), + )); + + // Run on_initialize at block 12 + // First extrinsic: age = 12 - 1 = 11 > 10, should expire + // Second extrinsic: age = 12 - 10 = 2 <= 10, should dispatch + System::set_block_number(12); + MevShield::on_initialize(12); + + // Verify both were removed from storage + assert!(PendingExtrinsics::::get(0).is_none()); + assert!(PendingExtrinsics::::get(1).is_none()); + assert_eq!(PendingExtrinsics::::count(), 0); + + // Verify first expired, second dispatched + System::assert_has_event(crate::Event::::ExtrinsicExpired { index: 0 }.into()); + System::assert_has_event(crate::Event::::ExtrinsicDispatched { index: 1 }.into()); + }); +} diff --git a/pallets/shield/src/tests/submit_encrypted.rs b/pallets/shield/src/tests/submit_encrypted.rs new file mode 100644 index 0000000000..61af4aa724 --- /dev/null +++ b/pallets/shield/src/tests/submit_encrypted.rs @@ -0,0 +1,42 @@ +//! Tests for the `submit_encrypted` user-facing wrapper extrinsic. + +use crate::mock::*; +use frame_support::{BoundedVec, assert_noop, assert_ok}; +use sp_runtime::traits::Hash; + +#[test] +fn submit_encrypted_emits_event() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + let ciphertext = BoundedVec::truncate_from(vec![0xAA; 64]); + let who: u64 = 1; + + assert_ok!(MevShield::submit_encrypted( + RuntimeOrigin::signed(who), + ciphertext.clone(), + )); + + let expected_id = ::Hashing::hash_of(&(who, &ciphertext)); + + System::assert_last_event( + crate::Event::::EncryptedSubmitted { + id: expected_id, + who, + } + .into(), + ); + }); +} + +#[test] +fn submit_encrypted_rejects_unsigned() { + new_test_ext().execute_with(|| { + let ciphertext = BoundedVec::truncate_from(vec![0xAA; 64]); + + assert_noop!( + MevShield::submit_encrypted(RuntimeOrigin::none(), ciphertext), + sp_runtime::DispatchError::BadOrigin + ); + }); +} diff --git a/pallets/shield/src/tests/try_decode_shielded_tx.rs b/pallets/shield/src/tests/try_decode_shielded_tx.rs new file mode 100644 index 0000000000..9798f22db0 --- /dev/null +++ b/pallets/shield/src/tests/try_decode_shielded_tx.rs @@ -0,0 +1,99 @@ +//! Tests for `Pallet::try_decode_shielded_tx` extrinsic parsing. + +use crate::mock::*; +use frame_support::BoundedVec; +use sp_runtime::testing::TestSignature; + +#[test] +fn try_decode_shielded_tx_parses_bare_submit_encrypted() { + new_test_ext().execute_with(|| { + let key_hash = [0xAB; 16]; + let kem_ct = vec![0xCC; 32]; + let nonce = [0xDD; 24]; + let aead_ct = vec![0xEE; 64]; + + let ciphertext = build_wire_ciphertext(&key_hash, &kem_ct, &nonce, &aead_ct); + let call = RuntimeCall::MevShield(crate::Call::submit_encrypted { + ciphertext: BoundedVec::truncate_from(ciphertext), + }); + let uxt = DecodableExtrinsic::new_bare(call); + + let result = crate::Pallet::::try_decode_shielded_tx::< + DecodableBlock, + frame_system::ChainContext, + >(uxt); + assert!(result.is_some()); + + let shielded = result.unwrap(); + assert_eq!(shielded.key_hash, key_hash); + assert_eq!(shielded.kem_ct, kem_ct); + assert_eq!(shielded.nonce, nonce); + assert_eq!(shielded.aead_ct, aead_ct); + }); +} + +#[test] +fn try_decode_shielded_tx_returns_none_for_non_shield_call() { + new_test_ext().execute_with(|| { + let call = RuntimeCall::System(frame_system::Call::remark { remark: vec![] }); + let uxt = DecodableExtrinsic::new_bare(call); + + let result = crate::Pallet::::try_decode_shielded_tx::< + DecodableBlock, + frame_system::ChainContext, + >(uxt); + assert!(result.is_none()); + }); +} + +#[test] +fn try_decode_shielded_tx_returns_none_for_bad_signature() { + new_test_ext().execute_with(|| { + let ciphertext = build_wire_ciphertext(&[0xAB; 16], &[0xCC; 32], &[0xDD; 24], &[0xEE; 64]); + let call = RuntimeCall::MevShield(crate::Call::submit_encrypted { + ciphertext: BoundedVec::truncate_from(ciphertext), + }); + let bad_sig = TestSignature(1, vec![0xFF; 32]); + let uxt = DecodableExtrinsic::new_signed(call, 1u64, bad_sig, ()); + + let result = crate::Pallet::::try_decode_shielded_tx::< + DecodableBlock, + frame_system::ChainContext, + >(uxt); + assert!(result.is_none()); + }); +} + +#[test] +fn try_decode_shielded_tx_returns_none_for_malformed_ciphertext() { + new_test_ext().execute_with(|| { + let call = RuntimeCall::MevShield(crate::Call::submit_encrypted { + ciphertext: BoundedVec::truncate_from(vec![0u8; 5]), + }); + let uxt = DecodableExtrinsic::new_bare(call); + + let result = crate::Pallet::::try_decode_shielded_tx::< + DecodableBlock, + frame_system::ChainContext, + >(uxt); + assert!(result.is_none()); + }); +} + +#[test] +fn try_decode_shielded_tx_returns_none_when_depth_exceeded() { + new_test_ext().execute_with(|| { + let ciphertext = build_wire_ciphertext(&[0xAB; 16], &[0xCC; 32], &[0xDD; 24], &[0xEE; 64]); + let inner = RuntimeCall::MevShield(crate::Call::submit_encrypted { + ciphertext: BoundedVec::truncate_from(ciphertext), + }); + let call = nest_call(inner, 8); + let uxt = DecodableExtrinsic::new_bare(call); + + let result = crate::Pallet::::try_decode_shielded_tx::< + DecodableBlock, + frame_system::ChainContext, + >(uxt); + assert!(result.is_none()); + }); +} diff --git a/pallets/shield/src/tests/try_unshield_tx.rs b/pallets/shield/src/tests/try_unshield_tx.rs new file mode 100644 index 0000000000..7093ecb94e --- /dev/null +++ b/pallets/shield/src/tests/try_unshield_tx.rs @@ -0,0 +1,66 @@ +//! Tests for `Pallet::try_unshield_tx` ML-KEM + AEAD decryption. + +use crate::mock::*; +use codec::Encode; +use sp_runtime::traits::Block as BlockT; +use stp_shield::{ShieldKeystore, ShieldedTransaction}; + +use chacha20poly1305::{ + KeyInit, XChaCha20Poly1305, XNonce, + aead::{Aead, Payload}, +}; +use ml_kem::{ + EncodedSizeUser, MlKem768Params, + kem::{Encapsulate, EncapsulationKey}, +}; +use rand_chacha::{ChaChaRng, rand_core::SeedableRng}; +use stc_shield::MemoryShieldKeystore; + +#[test] +fn try_unshield_tx_decrypts_extrinsic() { + let mut rng = ChaChaRng::from_seed([42u8; 32]); + let keystore = MemoryShieldKeystore::new(); + + // Client side: read the announced encapsulation key and encapsulate. + let pk_bytes = keystore.next_enc_key().unwrap(); + let enc_key = + EncapsulationKey::::from_bytes(pk_bytes.as_slice().try_into().unwrap()); + let (kem_ct, shared_secret) = enc_key.encapsulate(&mut rng).unwrap(); + + // Build the inner extrinsic that we'll encrypt. + let inner_call = RuntimeCall::System(frame_system::Call::remark { + remark: vec![1, 2, 3], + }); + let inner_uxt = ::Extrinsic::new_bare(inner_call); + let plaintext = inner_uxt.encode(); + + // AEAD encrypt the extrinsic bytes. + let nonce = [42u8; 24]; + let cipher = XChaCha20Poly1305::new(shared_secret.as_slice().into()); + let aead_ct = cipher + .encrypt( + XNonce::from_slice(&nonce), + Payload { + msg: &plaintext, + aad: &[], + }, + ) + .unwrap(); + + // Roll keystore so next -> current (author side). + keystore.roll_for_next_slot().unwrap(); + let dec_key_bytes = keystore.current_dec_key().unwrap(); + + let shielded_tx = ShieldedTransaction { + key_hash: [0u8; 16], + kem_ct: kem_ct.as_slice().to_vec(), + nonce, + aead_ct, + }; + + let result = crate::Pallet::::try_unshield_tx::(dec_key_bytes, shielded_tx); + assert!(result.is_some()); + + let decoded = result.unwrap(); + assert_eq!(decoded.encode(), inner_uxt.encode()); +} diff --git a/pallets/subtensor/rpc/src/lib.rs b/pallets/subtensor/rpc/src/lib.rs index 0c11869704..da430c35a2 100644 --- a/pallets/subtensor/rpc/src/lib.rs +++ b/pallets/subtensor/rpc/src/lib.rs @@ -1,4 +1,19 @@ -//! RPC interface for the custom Subtensor rpc methods +//! JSON-RPC surface for Subtensor custom queries (`delegateInfo_*`, `neuronInfo_*`, +//! `subnetInfo_*`, `stakeInfo_*`). +//! +//! Each method resolves an optional block hash (default: best), calls the matching +//! [`subtensor_custom_rpc_runtime_api`] trait, and returns **SCALE-encoded** bytes +//! (except emission / lock-cost / prune helpers that return typed values). +//! +//! # Frozen names +//! +//! `#[method(name = "…")]` strings are Tier C — never rename. Rust trait method names +//! on [`SubtensorCustomRpcApi`] match those strings for searchability; keep them aligned. +//! +//! # Related crate +//! +//! Runtime API declarations: `subtensor-custom-rpc-runtime-api` +//! (`pallets/subtensor/runtime-api`). use codec::{Decode, Encode}; use jsonrpsee::{ @@ -18,16 +33,23 @@ pub use subtensor_custom_rpc_runtime_api::{ SubnetRegistrationRuntimeApi, }; +/// Custom Subtensor JSON-RPC methods (jsonrpsee client + server). +/// +/// Most getters return SCALE-encoded pallet `rpc_info` structs as `Vec` for +/// substrate-facing clients; decode with the matching type from `pallet_subtensor::rpc_info`. #[rpc(client, server)] -pub trait SubtensorCustomApi { +pub trait SubtensorCustomRpcApi { + /// All delegates (`DelegateInfo`), SCALE-encoded. #[method(name = "delegateInfo_getDelegates")] fn get_delegates(&self, at: Option) -> RpcResult>; + /// One delegate by SCALE-encoded [`AccountId32`] hotkey bytes. #[method(name = "delegateInfo_getDelegate")] fn get_delegate( &self, delegate_account_vec: Vec, at: Option, ) -> RpcResult>; + /// Delegates that a coldkey has stake on (`get_delegated`), SCALE-encoded. #[method(name = "delegateInfo_getDelegated")] fn get_delegated( &self, @@ -35,8 +57,10 @@ pub trait SubtensorCustomApi { at: Option, ) -> RpcResult>; + /// Lite neurons for a subnet, SCALE-encoded. #[method(name = "neuronInfo_getNeuronsLite")] fn get_neurons_lite(&self, netuid: NetUid, at: Option) -> RpcResult>; + /// One lite neuron by uid, SCALE-encoded. #[method(name = "neuronInfo_getNeuronLite")] fn get_neuron_lite( &self, @@ -44,36 +68,50 @@ pub trait SubtensorCustomApi { uid: u16, at: Option, ) -> RpcResult>; + /// Full neurons for a subnet, SCALE-encoded. #[method(name = "neuronInfo_getNeurons")] fn get_neurons(&self, netuid: NetUid, at: Option) -> RpcResult>; + /// One full neuron by uid, SCALE-encoded. #[method(name = "neuronInfo_getNeuron")] fn get_neuron(&self, netuid: NetUid, uid: u16, at: Option) -> RpcResult>; + /// Legacy subnet info for one netuid, SCALE-encoded. #[method(name = "subnetInfo_getSubnetInfo")] fn get_subnet_info(&self, netuid: NetUid, at: Option) -> RpcResult>; + /// Legacy subnet info for all netuids, SCALE-encoded. #[method(name = "subnetInfo_getSubnetsInfo")] fn get_subnets_info(&self, at: Option) -> RpcResult>; + /// `SubnetInfov2` for one netuid, SCALE-encoded. #[method(name = "subnetInfo_getSubnetInfo_v2")] fn get_subnet_info_v2(&self, netuid: NetUid, at: Option) -> RpcResult>; + /// `SubnetInfov2` for all netuids, SCALE-encoded. #[method(name = "subnetInfo_getSubnetsInfo_v2")] fn get_subnets_info_v2(&self, at: Option) -> RpcResult>; + /// Deprecated hyperparams v1; prefer on-chain `get_subnet_hyperparams_v3` via runtime API. #[method(name = "subnetInfo_getSubnetHyperparams")] fn get_subnet_hyperparams(&self, netuid: NetUid, at: Option) -> RpcResult>; + /// Deprecated hyperparams v2; prefer on-chain `get_subnet_hyperparams_v3` via runtime API. #[method(name = "subnetInfo_getSubnetHyperparamsV2")] fn get_subnet_hyperparams_v2( &self, netuid: NetUid, at: Option, ) -> RpcResult>; + /// Dynamic pool info for all subnets, SCALE-encoded. #[method(name = "subnetInfo_getAllDynamicInfo")] fn get_all_dynamic_info(&self, at: Option) -> RpcResult>; + /// Dynamic pool info for one subnet, SCALE-encoded. #[method(name = "subnetInfo_getDynamicInfo")] fn get_dynamic_info(&self, netuid: NetUid, at: Option) -> RpcResult>; + /// Root metagraphs for all subnets, SCALE-encoded. #[method(name = "subnetInfo_getAllMetagraphs")] fn get_all_metagraphs(&self, at: Option) -> RpcResult>; + /// Root metagraph for one subnet, SCALE-encoded. #[method(name = "subnetInfo_getMetagraph")] fn get_metagraph(&self, netuid: NetUid, at: Option) -> RpcResult>; + /// All mechanism metagraphs, SCALE-encoded. #[method(name = "subnetInfo_getAllMechagraphs")] fn get_all_mechagraphs(&self, at: Option) -> RpcResult>; + /// Mechanism metagraph for `(netuid, mecid)`, SCALE-encoded. #[method(name = "subnetInfo_getMechagraph")] fn get_mechagraph( &self, @@ -81,13 +119,17 @@ pub trait SubtensorCustomApi { mecid: MechId, at: Option, ) -> RpcResult>; + /// Show-subnet state snapshot, SCALE-encoded. #[method(name = "subnetInfo_getSubnetState")] fn get_subnet_state(&self, netuid: NetUid, at: Option) -> RpcResult>; + /// Network-wide TAO block emission (rao), not SCALE-wrapped. #[method(name = "subnetInfo_getBlockEmission")] fn get_block_emission(&self, at: Option) -> RpcResult; + /// TAO lock cost to register a new subnet (rao); calls `get_network_registration_cost`. #[method(name = "subnetInfo_getLockCost")] fn get_network_lock_cost(&self, at: Option) -> RpcResult; + /// Partial root metagraph columns, SCALE-encoded. #[method(name = "subnetInfo_getSelectiveMetagraph")] fn get_selective_metagraph( &self, @@ -95,6 +137,7 @@ pub trait SubtensorCustomApi { metagraph_index: Vec, at: Option, ) -> RpcResult>; + /// Coldkey auto-stake hotkey for a subnet, SCALE-encoded `Option`. #[method(name = "subnetInfo_getColdkeyAutoStakeHotkey")] fn get_coldkey_auto_stake_hotkey( &self, @@ -102,6 +145,7 @@ pub trait SubtensorCustomApi { netuid: NetUid, at: Option, ) -> RpcResult>; + /// Partial mechanism metagraph columns, SCALE-encoded. #[method(name = "subnetInfo_getSelectiveMechagraph")] fn get_selective_mechagraph( &self, @@ -110,10 +154,13 @@ pub trait SubtensorCustomApi { metagraph_index: Vec, at: Option, ) -> RpcResult>; + /// Netuid next up for pruning, if any. #[method(name = "subnetInfo_getSubnetToPrune")] fn get_subnet_to_prune(&self, at: Option) -> RpcResult>; + /// Subnet account id, SCALE-encoded; errors if the subnet does not exist. #[method(name = "subnetInfo_getSubnetAccountId")] fn get_subnet_account_id(&self, netuid: NetUid, at: Option) -> RpcResult>; + /// Coldkey lock state on a subnet, SCALE-encoded. #[method(name = "stakeInfo_getColdkeyLock")] fn get_coldkey_lock( &self, @@ -123,14 +170,15 @@ pub trait SubtensorCustomApi { ) -> RpcResult>; } -pub struct SubtensorCustom { +/// Node-side RPC handler that forwards to Subtensor custom runtime APIs. +pub struct SubtensorCustomRpc { /// Shared reference to the client. client: Arc, _marker: std::marker::PhantomData

, } -impl SubtensorCustom { - /// Creates a new instance of the TransactionPayment Rpc helper. +impl SubtensorCustomRpc { + /// Creates a new Subtensor custom RPC handler around `client`. pub fn new(client: Arc) -> Self { Self { client, @@ -139,29 +187,58 @@ impl SubtensorCustom { } } +impl SubtensorCustomRpc +where + Block: BlockT, + C: HeaderBackend, +{ + /// `at` if provided, otherwise the client's best block hash. + fn block_hash_or_best(&self, at: Option) -> Block::Hash { + at.unwrap_or_else(|| self.client.info().best_hash) + } +} + +/// Maps a runtime-API `Result` to SCALE-encoded `Vec`, or a JSON-RPC runtime error. +fn scale_encode_runtime_api_result(result: Result, context: &str) -> RpcResult> +where + T: Encode, + E: core::fmt::Debug, +{ + match result { + Ok(value) => Ok(value.encode()), + Err(e) => Err(SubtensorRpcError::RuntimeError(format!("{context}: {e:?}")).into()), + } +} + +/// Decodes an [`AccountId32`] from raw bytes for delegate RPC account arguments. +fn decode_account_id32_arg(account_bytes: &[u8], context: &str) -> RpcResult { + AccountId32::decode(&mut &account_bytes[..]) + .map_err(|e| SubtensorRpcError::RuntimeError(format!("{context}: {e:?}")).into()) +} + /// Error type of this RPC api. -pub enum Error { +pub enum SubtensorRpcError { /// The call to runtime failed. RuntimeError(String), } -impl From for ErrorObjectOwned { - fn from(e: Error) -> Self { +impl From for ErrorObjectOwned { + fn from(e: SubtensorRpcError) -> Self { match e { - Error::RuntimeError(e) => ErrorObject::owned(1, e, None::<()>), + SubtensorRpcError::RuntimeError(e) => ErrorObject::owned(1, e, None::<()>), } } } -impl From for i32 { - fn from(e: Error) -> i32 { +impl From for i32 { + fn from(e: SubtensorRpcError) -> i32 { match e { - Error::RuntimeError(_) => 1, + SubtensorRpcError::RuntimeError(_) => 1, } } } -impl SubtensorCustomApiServer<::Hash> for SubtensorCustom +impl SubtensorCustomRpcApiServer<::Hash> for SubtensorCustomRpc where Block: BlockT, C: ProvideRuntimeApi + HeaderBackend + Send + Sync + 'static, @@ -173,14 +250,8 @@ where { fn get_delegates(&self, at: Option<::Hash>) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - match api.get_delegates(at) { - Ok(result) => Ok(result.encode()), - Err(e) => { - Err(Error::RuntimeError(format!("Unable to get delegates info: {e:?}")).into()) - } - } + let at = self.block_hash_or_best(at); + scale_encode_runtime_api_result(api.get_delegates(at), "Unable to get delegates info") } fn get_delegate( @@ -189,22 +260,13 @@ where at: Option<::Hash>, ) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - let delegate_account = match AccountId32::decode(&mut &delegate_account_vec[..]) { - Ok(delegate_account) => delegate_account, - Err(e) => { - return Err( - Error::RuntimeError(format!("Unable to get delegates info: {e:?}")).into(), - ); - } - }; - match api.get_delegate(at, delegate_account) { - Ok(result) => Ok(result.encode()), - Err(e) => { - Err(Error::RuntimeError(format!("Unable to get delegates info: {e:?}")).into()) - } - } + let at = self.block_hash_or_best(at); + let delegate_account = + decode_account_id32_arg(&delegate_account_vec, "Unable to get delegates info")?; + scale_encode_runtime_api_result( + api.get_delegate(at, delegate_account), + "Unable to get delegates info", + ) } fn get_delegated( @@ -213,22 +275,13 @@ where at: Option<::Hash>, ) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - let delegatee_account = match AccountId32::decode(&mut &delegatee_account_vec[..]) { - Ok(delegatee_account) => delegatee_account, - Err(e) => { - return Err( - Error::RuntimeError(format!("Unable to get delegates info: {e:?}")).into(), - ); - } - }; - match api.get_delegated(at, delegatee_account) { - Ok(result) => Ok(result.encode()), - Err(e) => { - Err(Error::RuntimeError(format!("Unable to get delegates info: {e:?}")).into()) - } - } + let at = self.block_hash_or_best(at); + let delegatee_account = + decode_account_id32_arg(&delegatee_account_vec, "Unable to get delegates info")?; + scale_encode_runtime_api_result( + api.get_delegated(at, delegatee_account), + "Unable to get delegates info", + ) } fn get_neurons_lite( @@ -237,14 +290,11 @@ where at: Option<::Hash>, ) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - match api.get_neurons_lite(at, netuid) { - Ok(result) => Ok(result.encode()), - Err(e) => { - Err(Error::RuntimeError(format!("Unable to get neurons lite info: {e:?}")).into()) - } - } + let at = self.block_hash_or_best(at); + scale_encode_runtime_api_result( + api.get_neurons_lite(at, netuid), + "Unable to get neurons lite info", + ) } fn get_neuron_lite( @@ -254,14 +304,11 @@ where at: Option<::Hash>, ) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - match api.get_neuron_lite(at, netuid, uid) { - Ok(result) => Ok(result.encode()), - Err(e) => { - Err(Error::RuntimeError(format!("Unable to get neurons lite info: {e:?}")).into()) - } - } + let at = self.block_hash_or_best(at); + scale_encode_runtime_api_result( + api.get_neuron_lite(at, netuid, uid), + "Unable to get neurons lite info", + ) } fn get_neurons( @@ -270,12 +317,8 @@ where at: Option<::Hash>, ) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - match api.get_neurons(at, netuid) { - Ok(result) => Ok(result.encode()), - Err(e) => Err(Error::RuntimeError(format!("Unable to get neurons info: {e:?}")).into()), - } + let at = self.block_hash_or_best(at); + scale_encode_runtime_api_result(api.get_neurons(at, netuid), "Unable to get neurons info") } fn get_neuron( @@ -285,12 +328,11 @@ where at: Option<::Hash>, ) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - match api.get_neuron(at, netuid, uid) { - Ok(result) => Ok(result.encode()), - Err(e) => Err(Error::RuntimeError(format!("Unable to get neuron info: {e:?}")).into()), - } + let at = self.block_hash_or_best(at); + scale_encode_runtime_api_result( + api.get_neuron(at, netuid, uid), + "Unable to get neuron info", + ) } fn get_subnet_info( @@ -299,12 +341,11 @@ where at: Option<::Hash>, ) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - match api.get_subnet_info(at, netuid) { - Ok(result) => Ok(result.encode()), - Err(e) => Err(Error::RuntimeError(format!("Unable to get subnet info: {e:?}")).into()), - } + let at = self.block_hash_or_best(at); + scale_encode_runtime_api_result( + api.get_subnet_info(at, netuid), + "Unable to get subnet info", + ) } #[allow(deprecated)] @@ -314,12 +355,11 @@ where at: Option<::Hash>, ) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - match api.get_subnet_hyperparams(at, netuid) { - Ok(result) => Ok(result.encode()), - Err(e) => Err(Error::RuntimeError(format!("Unable to get subnet info: {e:?}")).into()), - } + let at = self.block_hash_or_best(at); + scale_encode_runtime_api_result( + api.get_subnet_hyperparams(at, netuid), + "Unable to get subnet hyperparams", + ) } #[allow(deprecated)] @@ -329,45 +369,32 @@ where at: Option<::Hash>, ) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - match api.get_subnet_hyperparams_v2(at, netuid) { - Ok(result) => Ok(result.encode()), - Err(e) => Err(Error::RuntimeError(format!("Unable to get subnet info: {e:?}")).into()), - } + let at = self.block_hash_or_best(at); + scale_encode_runtime_api_result( + api.get_subnet_hyperparams_v2(at, netuid), + "Unable to get subnet hyperparams v2", + ) } fn get_all_dynamic_info(&self, at: Option<::Hash>) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - match api.get_all_dynamic_info(at) { - Ok(result) => Ok(result.encode()), - Err(e) => Err(Error::RuntimeError(format!( - "Unable to get dynamic subnets info: {e:?}" - )) - .into()), - } + let at = self.block_hash_or_best(at); + scale_encode_runtime_api_result( + api.get_all_dynamic_info(at), + "Unable to get dynamic subnets info", + ) } fn get_all_metagraphs(&self, at: Option<::Hash>) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - match api.get_all_metagraphs(at) { - Ok(result) => Ok(result.encode()), - Err(e) => Err(Error::RuntimeError(format!("Unable to get metagraps: {e:?}")).into()), - } + let at = self.block_hash_or_best(at); + scale_encode_runtime_api_result(api.get_all_metagraphs(at), "Unable to get metagraphs") } fn get_all_mechagraphs(&self, at: Option<::Hash>) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - match api.get_all_mechagraphs(at) { - Ok(result) => Ok(result.encode()), - Err(e) => Err(Error::RuntimeError(format!("Unable to get metagraps: {e:?}")).into()), - } + let at = self.block_hash_or_best(at); + scale_encode_runtime_api_result(api.get_all_mechagraphs(at), "Unable to get mechagraphs") } fn get_dynamic_info( @@ -376,15 +403,11 @@ where at: Option<::Hash>, ) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - match api.get_dynamic_info(at, netuid) { - Ok(result) => Ok(result.encode()), - Err(e) => Err(Error::RuntimeError(format!( - "Unable to get dynamic subnets info: {e:?}" - )) - .into()), - } + let at = self.block_hash_or_best(at); + scale_encode_runtime_api_result( + api.get_dynamic_info(at, netuid), + "Unable to get dynamic subnet info", + ) } fn get_metagraph( @@ -393,14 +416,8 @@ where at: Option<::Hash>, ) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - match api.get_metagraph(at, netuid) { - Ok(result) => Ok(result.encode()), - Err(e) => Err(Error::RuntimeError(format!( - "Unable to get dynamic subnets info: {e:?}" - )) - .into()), - } + let at = self.block_hash_or_best(at); + scale_encode_runtime_api_result(api.get_metagraph(at, netuid), "Unable to get metagraph") } fn get_mechagraph( @@ -410,14 +427,11 @@ where at: Option<::Hash>, ) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - match api.get_mechagraph(at, netuid, mecid) { - Ok(result) => Ok(result.encode()), - Err(e) => Err(Error::RuntimeError(format!( - "Unable to get dynamic subnets info: {e:?}" - )) - .into()), - } + let at = self.block_hash_or_best(at); + scale_encode_runtime_api_result( + api.get_mechagraph(at, netuid, mecid), + "Unable to get mechagraph", + ) } fn get_subnet_state( @@ -426,24 +440,17 @@ where at: Option<::Hash>, ) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - match api.get_subnet_state(at, netuid) { - Ok(result) => Ok(result.encode()), - Err(e) => { - Err(Error::RuntimeError(format!("Unable to get subnet state info: {e:?}")).into()) - } - } + let at = self.block_hash_or_best(at); + scale_encode_runtime_api_result( + api.get_subnet_state(at, netuid), + "Unable to get subnet state info", + ) } fn get_subnets_info(&self, at: Option<::Hash>) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - match api.get_subnets_info(at) { - Ok(result) => Ok(result.encode()), - Err(e) => Err(Error::RuntimeError(format!("Unable to get subnets info: {e:?}")).into()), - } + let at = self.block_hash_or_best(at); + scale_encode_runtime_api_result(api.get_subnets_info(at), "Unable to get subnets info") } fn get_subnet_info_v2( @@ -452,38 +459,34 @@ where at: Option<::Hash>, ) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - match api.get_subnet_info_v2(at, netuid) { - Ok(result) => Ok(result.encode()), - Err(e) => Err(Error::RuntimeError(format!("Unable to get subnet info: {e:?}")).into()), - } + let at = self.block_hash_or_best(at); + scale_encode_runtime_api_result( + api.get_subnet_info_v2(at, netuid), + "Unable to get subnet info", + ) } fn get_subnets_info_v2(&self, at: Option<::Hash>) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - match api.get_subnets_info_v2(at) { - Ok(result) => Ok(result.encode()), - Err(e) => Err(Error::RuntimeError(format!("Unable to get subnets info: {e:?}")).into()), - } + let at = self.block_hash_or_best(at); + scale_encode_runtime_api_result(api.get_subnets_info_v2(at), "Unable to get subnets info") } fn get_block_emission(&self, at: Option<::Hash>) -> RpcResult { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); + let at = self.block_hash_or_best(at); - api.get_block_emission(at) - .map_err(|e| Error::RuntimeError(format!("Unable to get block emission: {e:?}")).into()) + api.get_block_emission(at).map_err(|e| { + SubtensorRpcError::RuntimeError(format!("Unable to get block emission: {e:?}")).into() + }) } fn get_network_lock_cost(&self, at: Option<::Hash>) -> RpcResult { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); + let at = self.block_hash_or_best(at); api.get_network_registration_cost(at).map_err(|e| { - Error::RuntimeError(format!("Unable to get subnet lock cost: {e:?}")).into() + SubtensorRpcError::RuntimeError(format!("Unable to get subnet lock cost: {e:?}")).into() }) } @@ -494,14 +497,11 @@ where at: Option<::Hash>, ) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - match api.get_selective_metagraph(at, netuid, metagraph_index) { - Ok(result) => Ok(result.encode()), - Err(e) => { - Err(Error::RuntimeError(format!("Unable to get selective metagraph: {e:?}")).into()) - } - } + let at = self.block_hash_or_best(at); + scale_encode_runtime_api_result( + api.get_selective_metagraph(at, netuid, metagraph_index), + "Unable to get selective metagraph", + ) } fn get_coldkey_auto_stake_hotkey( @@ -511,15 +511,11 @@ where at: Option<::Hash>, ) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - match api.get_coldkey_auto_stake_hotkey(at, coldkey, netuid) { - Ok(result) => Ok(result.encode()), - Err(e) => Err(Error::RuntimeError(format!( - "Unable to get coldkey auto stake hotkey: {e:?}" - )) - .into()), - } + let at = self.block_hash_or_best(at); + scale_encode_runtime_api_result( + api.get_coldkey_auto_stake_hotkey(at, coldkey, netuid), + "Unable to get coldkey auto stake hotkey", + ) } fn get_selective_mechagraph( @@ -530,14 +526,11 @@ where at: Option<::Hash>, ) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - match api.get_selective_mechagraph(at, netuid, mecid, metagraph_index) { - Ok(result) => Ok(result.encode()), - Err(e) => { - Err(Error::RuntimeError(format!("Unable to get selective metagraph: {e:?}")).into()) - } - } + let at = self.block_hash_or_best(at); + scale_encode_runtime_api_result( + api.get_selective_mechagraph(at, netuid, mecid, metagraph_index), + "Unable to get selective mechagraph", + ) } fn get_subnet_to_prune( @@ -545,14 +538,11 @@ where at: Option<::Hash>, ) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); + let at = self.block_hash_or_best(at); - match api.get_subnet_to_prune(at) { - Ok(result) => Ok(result), - Err(e) => { - Err(Error::RuntimeError(format!("Unable to get subnet to prune: {e:?}")).into()) - } - } + api.get_subnet_to_prune(at).map_err(|e| { + SubtensorRpcError::RuntimeError(format!("Unable to get subnet to prune: {e:?}")).into() + }) } fn get_subnet_account_id( @@ -561,11 +551,13 @@ where at: Option<::Hash>, ) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); + let at = self.block_hash_or_best(at); match api.get_subnet_account_id(at, netuid) { Ok(result) => Ok(result.encode()), - Err(_) => Err(Error::RuntimeError("Subnet does not exist".to_string()).into()), + Err(_) => { + Err(SubtensorRpcError::RuntimeError("Subnet does not exist".to_string()).into()) + } } } @@ -576,11 +568,10 @@ where at: Option<::Hash>, ) -> RpcResult> { let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - match api.get_coldkey_lock(at, coldkey, netuid) { - Ok(result) => Ok(result.encode()), - Err(e) => Err(Error::RuntimeError(format!("Unable to get coldkey lock: {e:?}")).into()), - } + let at = self.block_hash_or_best(at); + scale_encode_runtime_api_result( + api.get_coldkey_lock(at, coldkey, netuid), + "Unable to get coldkey lock", + ) } } diff --git a/pallets/subtensor/runtime-api/src/lib.rs b/pallets/subtensor/runtime-api/src/lib.rs index 174cd7b553..a802f6ad86 100644 --- a/pallets/subtensor/runtime-api/src/lib.rs +++ b/pallets/subtensor/runtime-api/src/lib.rs @@ -1,3 +1,19 @@ +//! Custom Subtensor runtime APIs consumed by the node JSON-RPC layer and indexers. +//! +//! Trait and method **names are frozen** (Tier C): clients and `decl_runtime_apis!` +//! versioning depend on them. Prefer docs and call-site clarity over renames. +//! +//! # Where implementations live +//! +//! - Runtime wiring: `runtime/src/lib.rs` (`impl …RuntimeApi for Runtime`) +//! - Pallet builders: [`pallet_subtensor::rpc_info`] (delegate / neuron / subnet / +//! stake / metagraph views), plus staking lock / coinbase helpers for a few queries +//! +//! # Related crate +//! +//! JSON-RPC method strings and SCALE-encoding wrappers: +//! `subtensor-custom-rpc` (`pallets/subtensor/rpc`). + #![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; use alloc::collections::BTreeMap; @@ -21,66 +37,107 @@ use subtensor_runtime_common::{ AlphaBalance, MechId, NetUid, ProxyFilterInfo, ProxyTypeInfo, TaoBalance, }; -// Here we declare the runtime API. It is implemented it the `impl` block in -// src/neuron_info.rs, src/subnet_info.rs, and src/delegate_info.rs sp_api::decl_runtime_apis! { + /// Delegate (validator hotkey) RPC views: take, nominators, registrations, returns. pub trait DelegateInfoRuntimeApi { + /// All hotkeys currently in `Delegates`, each with a full nominator list. fn get_delegates() -> Vec>; + /// One delegate hotkey, or `None` if it is not in `Delegates`. fn get_delegate( delegate_account: AccountId32 ) -> Option>; + /// Delegates that `delegatee_account` (coldkey) has stake on, with `(netuid, alpha)` per row. fn get_delegated( delegatee_account: AccountId32 ) -> Vec<(DelegateInfo, (Compact, Compact))>; } + /// Per-uid neuron views for a subnet (full and lite). pub trait NeuronInfoRuntimeApi { + /// Full [`NeuronInfo`] for every uid on `netuid`. fn get_neurons(netuid: NetUid) -> Vec>; + /// Full [`NeuronInfo`] for one uid, or `None` if unset / out of range. fn get_neuron(netuid: NetUid, uid: u16) -> Option>; + /// Lite [`NeuronInfoLite`] rows for every uid on `netuid`. fn get_neurons_lite(netuid: NetUid) -> Vec>; + /// Lite [`NeuronInfoLite`] for one uid, or `None` if unset / out of range. fn get_neuron_lite(netuid: NetUid, uid: u16) -> Option>; } + /// Subnet metadata, hyperparams, dynamic pool state, and metagraph snapshots. pub trait SubnetInfoRuntimeApi { + /// Legacy [`SubnetInfo`] for one subnet. fn get_subnet_info(netuid: NetUid) -> Option>; + /// Legacy [`SubnetInfo`] for all netuids (sparse: `None` gaps allowed). fn get_subnets_info() -> Vec>>; + /// [`SubnetInfov2`] for one subnet. fn get_subnet_info_v2(netuid: NetUid) -> Option>; + /// [`SubnetInfov2`] for all netuids (sparse: `None` gaps allowed). fn get_subnets_info_v2() -> Vec>>; #[deprecated(note = "Use `get_subnet_hyperparams_v3` instead.")] fn get_subnet_hyperparams(netuid: NetUid) -> Option; #[deprecated(note = "Use `get_subnet_hyperparams_v3` instead.")] fn get_subnet_hyperparams_v2(netuid: NetUid) -> Option; + /// Current subnet hyperparameters (`SubnetHyperparamsV3`). #[api_version(2)] fn get_subnet_hyperparams_v3(netuid: NetUid) -> Option; + /// [`DynamicInfo`] for every subnet (sparse). fn get_all_dynamic_info() -> Vec>>; + /// Root mechanism metagraph for every subnet (sparse). fn get_all_metagraphs() -> Vec>>; + /// Root mechanism [`Metagraph`] for one subnet. fn get_metagraph(netuid: NetUid) -> Option>; + /// All mechanisms' metagraphs across subnets (sparse). fn get_all_mechagraphs() -> Vec>>; + /// [`Metagraph`] for one `(netuid, mecid)` mechanism. fn get_mechagraph(netuid: NetUid, mecid: MechId) -> Option>; + /// [`DynamicInfo`] for one subnet. fn get_dynamic_info(netuid: NetUid) -> Option>; + /// [`SubnetState`] show-subnet snapshot for one netuid. fn get_subnet_state(netuid: NetUid) -> Option>; + /// Partial root metagraph: only columns listed in `metagraph_indexes`. fn get_selective_metagraph(netuid: NetUid, metagraph_indexes: Vec) -> Option>; + /// Hotkey selected for coldkey auto-stake on `netuid`, if any. fn get_coldkey_auto_stake_hotkey(coldkey: AccountId32, netuid: NetUid) -> Option; + /// Partial mechanism metagraph for `(netuid, subid)`; `subid` is a [`MechId`]. fn get_selective_mechagraph(netuid: NetUid, subid: MechId, metagraph_indexes: Vec) -> Option>; + /// Netuid that would be pruned next under current immunity / emission rules, if any. fn get_subnet_to_prune() -> Option; + /// Subnet's on-chain account id (treasury / subnet key), if the subnet exists. fn get_subnet_account_id(netuid: NetUid) -> Option; + /// Absolute block when the next tempo epoch starts for `netuid`. fn get_next_epoch_start_block(netuid: NetUid) -> Option; + /// Network-wide TAO emission for the current block (rao). fn get_block_emission() -> TaoBalance; } + /// Stake positions, fees, coldkey locks, and hotkey conviction queries. pub trait StakeInfoRuntimeApi { + /// All stake positions owned by one coldkey. fn get_stake_info_for_coldkey( coldkey_account: AccountId32 ) -> Vec>; + /// Stake positions for many coldkeys: `(coldkey, positions)`. fn get_stake_info_for_coldkeys( coldkey_accounts: Vec ) -> Vec<(AccountId32, Vec>)>; + /// Single `(hotkey, coldkey, netuid)` stake row, if present. fn get_stake_info_for_hotkey_coldkey_netuid( hotkey_account: AccountId32, coldkey_account: AccountId32, netuid: NetUid ) -> Option>; + /// Per-coldkey, per-netuid stake availability; `netuids == None` means all subnets. fn get_stake_availability_for_coldkeys( coldkey_accounts: Vec, netuids: Option> ) -> BTreeMap>; + /// Fee (rao) to move `amount` between optional origin/destination stake endpoints. fn get_stake_fee( origin: Option<(AccountId32, NetUid)>, origin_coldkey_account: AccountId32, destination: Option<(AccountId32, NetUid)>, destination_coldkey_account: AccountId32, amount: u64 ) -> u64; + /// Coldkey lock state on `netuid`, if a lock exists. fn get_coldkey_lock(coldkey: AccountId32, netuid: NetUid) -> Option; + /// Hotkey conviction score on `netuid` (`U64F64`). fn get_hotkey_conviction(hotkey: AccountId32, netuid: NetUid) -> U64F64; + /// Hotkey with the highest conviction on `netuid`, if any. fn get_most_convicted_hotkey_on_subnet(netuid: NetUid) -> Option; } + /// Cost (TAO / rao) to register a new subnet at the current block. pub trait SubnetRegistrationRuntimeApi { + /// Lock cost required to create a new subnet (rao). fn get_network_registration_cost() -> TaoBalance; } + /// Proxy type catalog and which calls each proxy type may dispatch. pub trait ProxyFilterRuntimeApi { + /// All registered proxy type descriptors. fn get_proxy_types() -> Vec; + /// Filter rules; `proxy_types == None` returns filters for every type. fn get_proxy_filters(proxy_types: Option>) -> Vec; } } diff --git a/pallets/subtensor/src/benchmarks/benchmarks.rs b/pallets/subtensor/src/benchmarks/benchmarks.rs index 375bdb61ea..7ecc3142de 100644 --- a/pallets/subtensor/src/benchmarks/benchmarks.rs +++ b/pallets/subtensor/src/benchmarks/benchmarks.rs @@ -1,4 +1,12 @@ -//! Subtensor pallet benchmarking. +//! Subtensor pallet runtime benchmarks (`runtime-benchmarks` feature). +//! +//! Each `#[benchmark]` fn name matches a dispatchable / WeightInfo method and +//! must stay frozen. Setup lives in [`helpers`]; search there for reserve seeding, +//! registration funding, stake-lock stubs, and EVM-association fixtures. +//! +//! Domain groups below (registration, weights, staking, coldkey swap, block_step, +//! transaction extensions, mechanisms) are search anchors only — they do not +//! change measured extrinsic names. #![allow( clippy::arithmetic_side_effects, clippy::unwrap_used, @@ -47,6 +55,7 @@ mod pallet_benchmarks { use super::helpers::*; use super::*; + // --- Registration & serving ------------------------------------------------- #[benchmark] fn register() { let netuid = NetUid::from(1); @@ -77,6 +86,7 @@ mod pallet_benchmarks { ); } + // --- Weights (commit / reveal / batch) -------------------------------------- #[benchmark] fn set_weights() { let netuid = NetUid::from(1); @@ -137,6 +147,7 @@ mod pallet_benchmarks { ); } + // --- Staking, collateral, locks --------------------------------------------- #[benchmark] fn add_stake() { let netuid = NetUid::from(1); @@ -154,9 +165,9 @@ mod pallet_benchmarks { let total_stake = TaoBalance::from(1_000_000_000); let amount = TaoBalance::from(60_000_000); - seed_swap_reserves::(netuid); - add_balance_to_coldkey_account::(&coldkey, total_stake.into()); - add_lock::(&coldkey, netuid); + seed_default_subnet_amm_reserves::(netuid); + credit_benchmark_coldkey_tao::(&coldkey, total_stake.into()); + seed_zero_alpha_stake_lock::(&coldkey, netuid); assert_ok!(Subtensor::::burned_register( RawOrigin::Signed(coldkey.clone()).into(), @@ -191,7 +202,7 @@ mod pallet_benchmarks { Subtensor::::set_max_allowed_uids(netuid, 4096); Subtensor::::set_burn(netuid, benchmark_registration_burn()); - seed_swap_reserves::(netuid); + seed_default_subnet_amm_reserves::(netuid); fund_for_registration::(netuid, &caller); assert_ok!(Subtensor::::burned_register( @@ -231,7 +242,7 @@ mod pallet_benchmarks { Subtensor::::set_max_allowed_uids(netuid, 4096); Subtensor::::set_burn(netuid, benchmark_registration_burn()); - seed_swap_reserves::(netuid); + seed_default_subnet_amm_reserves::(netuid); fund_for_registration::(netuid, &caller); assert_ok!(Subtensor::::burned_register( @@ -299,7 +310,7 @@ mod pallet_benchmarks { Subtensor::::set_network_rate_limit(1); let amount: u64 = 100_000_000_000_000u64.saturating_mul(2); - add_balance_to_coldkey_account::(&coldkey, amount.into()); + credit_benchmark_coldkey_tao::(&coldkey, amount.into()); #[extrinsic_call] _(RawOrigin::Signed(coldkey.clone()), hotkey.clone()); @@ -330,7 +341,7 @@ mod pallet_benchmarks { SubtokenEnabled::::insert(netuid, true); Subtensor::::set_burn(netuid, benchmark_registration_burn()); - seed_swap_reserves::(netuid); + seed_default_subnet_amm_reserves::(netuid); fund_for_registration::(netuid, &coldkey); assert_ok!(Subtensor::::burned_register( @@ -364,7 +375,7 @@ mod pallet_benchmarks { Subtensor::::set_difficulty(netuid, 1); Subtensor::::set_burn(netuid, benchmark_registration_burn()); - seed_swap_reserves::(netuid); + seed_default_subnet_amm_reserves::(netuid); fund_for_registration::(netuid, &coldkey); assert_ok!(Subtensor::::burned_register( @@ -425,7 +436,7 @@ mod pallet_benchmarks { SubtokenEnabled::::insert(netuid, true); Subtensor::::set_burn(netuid, benchmark_registration_burn()); - seed_swap_reserves::(netuid); + seed_default_subnet_amm_reserves::(netuid); fund_for_registration::(netuid, &coldkey); assert_ok!(Subtensor::::burned_register( @@ -443,6 +454,7 @@ mod pallet_benchmarks { ); } + // --- Coldkey / hotkey identity swaps ---------------------------------------- #[benchmark] fn announce_coldkey_swap() { let coldkey: T::AccountId = account("old_coldkey", 0, 0); @@ -451,7 +463,7 @@ mod pallet_benchmarks { let ed = ::ExistentialDeposit::get(); let swap_cost = Subtensor::::get_key_swap_cost(); - add_balance_to_coldkey_account::(&coldkey, swap_cost + ed); + credit_benchmark_coldkey_tao::(&coldkey, swap_cost + ed); #[extrinsic_call] _(RawOrigin::Signed(coldkey), new_coldkey_hash); @@ -474,7 +486,7 @@ mod pallet_benchmarks { Subtensor::::set_network_registration_allowed(netuid, true); SubtokenEnabled::::insert(netuid, true); Subtensor::::set_burn(netuid, benchmark_registration_burn()); - seed_swap_reserves::(netuid); + seed_default_subnet_amm_reserves::(netuid); fund_for_registration::(netuid, &old_coldkey); Subtensor::::set_difficulty(netuid, 1); @@ -521,7 +533,7 @@ mod pallet_benchmarks { Subtensor::::set_difficulty(netuid, 1); SubtokenEnabled::::insert(netuid, true); Subtensor::::set_burn(netuid, benchmark_registration_burn()); - seed_swap_reserves::(netuid); + seed_default_subnet_amm_reserves::(netuid); fund_for_registration::(netuid, &old_coldkey); @@ -531,7 +543,7 @@ mod pallet_benchmarks { hotkey1.clone(), )); - add_balance_to_coldkey_account::(&old_coldkey, free_balance_old); + credit_benchmark_coldkey_tao::(&old_coldkey, free_balance_old); let name: Vec = b"The fourth Coolest Identity".to_vec(); let identity = ChainIdentityV2 { name, @@ -620,7 +632,7 @@ mod pallet_benchmarks { SubtokenEnabled::::insert(netuid, true); Subtensor::::set_burn(netuid, benchmark_registration_burn()); - seed_swap_reserves::(netuid); + seed_default_subnet_amm_reserves::(netuid); fund_for_registration::(netuid, &coldkey); assert_ok!(Subtensor::::burned_register( @@ -764,6 +776,7 @@ mod pallet_benchmarks { ); } + // --- Block step / epoch hooks ----------------------------------------------- #[benchmark] fn block_step() { setup_block_step_benchmark::(); @@ -784,7 +797,7 @@ mod pallet_benchmarks { Subtensor::::set_network_registration_allowed(netuid, true); Subtensor::::set_burn(netuid, benchmark_registration_burn()); - seed_swap_reserves::(netuid); + seed_default_subnet_amm_reserves::(netuid); fund_for_registration::(netuid, &coldkey); SubnetOwner::::set(netuid, coldkey.clone()); @@ -825,13 +838,13 @@ mod pallet_benchmarks { let hotkey: T::AccountId = account("Alice", 0, seed); let initial_balance = TaoBalance::from(900_000_000_000_u64); - add_balance_to_coldkey_account::(&coldkey.clone(), initial_balance); - add_lock::(&coldkey, netuid); + credit_benchmark_coldkey_tao::(&coldkey.clone(), initial_balance); + seed_zero_alpha_stake_lock::(&coldkey, netuid); // Price = 0.01 let tao_reserve = TaoBalance::from(1_000_000_000_000_u64); let alpha_in = AlphaBalance::from(100_000_000_000_000_u64); - set_reserves::(netuid, tao_reserve, alpha_in); + set_subnet_amm_reserves::(netuid, tao_reserve, alpha_in); assert_ok!(Subtensor::::burned_register( RawOrigin::Signed(coldkey.clone()).into(), @@ -874,8 +887,8 @@ mod pallet_benchmarks { let burn_fee = Subtensor::::get_burn(netuid); let stake_tao = DefaultMinStake::::get().saturating_mul(10.into()); let deposit = burn_fee.saturating_mul(2.into()).saturating_add(stake_tao); - add_balance_to_coldkey_account::(&coldkey, deposit.into()); - add_lock::(&coldkey, netuid); + credit_benchmark_coldkey_tao::(&coldkey, deposit.into()); + seed_zero_alpha_stake_lock::(&coldkey, netuid); assert_ok!(Subtensor::::burned_register( RawOrigin::Signed(coldkey.clone()).into(), @@ -883,7 +896,7 @@ mod pallet_benchmarks { origin.clone() )); - set_reserves::(netuid, deposit, AlphaBalance::from(deposit.to_u64())); + set_subnet_amm_reserves::(netuid, deposit, AlphaBalance::from(deposit.to_u64())); TotalStake::::set(deposit); assert_ok!(Subtensor::::add_stake_limit( @@ -933,7 +946,7 @@ mod pallet_benchmarks { // Price = 0.01 let tao_reserve = TaoBalance::from(1_000_000_000_000_u64); let alpha_in = AlphaBalance::from(100_000_000_000_000_u64); - set_reserves::(netuid, tao_reserve, alpha_in); + set_subnet_amm_reserves::(netuid, tao_reserve, alpha_in); // Registration now requires keep-alive coverage of the burn; fund // above burn + ED rather than exactly the burn amount. @@ -946,7 +959,7 @@ mod pallet_benchmarks { )); let staked_amt = TaoBalance::from(100_000_000_000_u64); - add_balance_to_coldkey_account::(&coldkey.clone(), staked_amt); + credit_benchmark_coldkey_tao::(&coldkey.clone(), staked_amt); assert_ok!(Subtensor::::add_stake( RawOrigin::Signed(coldkey.clone()).into(), @@ -987,11 +1000,11 @@ mod pallet_benchmarks { let tao_reserve = TaoBalance::from(1_000_000_000_000_u64); let alpha_in = AlphaBalance::from(100_000_000_000_000_u64); - set_reserves::(netuid, tao_reserve, alpha_in); + set_subnet_amm_reserves::(netuid, tao_reserve, alpha_in); // Registration now requires keep-alive coverage of the burn. fund_for_registration::(netuid, &coldkey); - add_lock::(&coldkey, netuid); + seed_zero_alpha_stake_lock::(&coldkey, netuid); assert_ok!(Subtensor::::burned_register( RawOrigin::Signed(coldkey.clone()).into(), @@ -1000,7 +1013,7 @@ mod pallet_benchmarks { )); let staked_amt = TaoBalance::from(100_000_000_000_u64); - add_balance_to_coldkey_account::(&coldkey.clone(), staked_amt); + credit_benchmark_coldkey_tao::(&coldkey.clone(), staked_amt); assert_ok!(Subtensor::::add_stake( RawOrigin::Signed(coldkey.clone()).into(), @@ -1046,7 +1059,7 @@ mod pallet_benchmarks { let tao_reserve = TaoBalance::from(150_000_000_000_u64); let alpha_in = AlphaBalance::from(100_000_000_000_u64); - set_reserves::(netuid1, tao_reserve, alpha_in); + set_subnet_amm_reserves::(netuid1, tao_reserve, alpha_in); SubnetTAO::::insert(netuid2, tao_reserve); Subtensor::::increase_total_stake(1_000_000_000_000_u64.into()); @@ -1056,9 +1069,9 @@ mod pallet_benchmarks { let limit_swap = TaoBalance::from(1_000_000_000_u64); let amount_to_be_staked = TaoBalance::from(440_000_000_000_u64); let amount_swapped = AlphaBalance::from(30_000_000_000_u64); - add_balance_to_coldkey_account::(&coldkey.clone(), amount); - add_lock::(&coldkey, netuid1); - add_lock::(&coldkey, netuid2); + credit_benchmark_coldkey_tao::(&coldkey.clone(), amount); + seed_zero_alpha_stake_lock::(&coldkey, netuid1); + seed_zero_alpha_stake_lock::(&coldkey, netuid2); assert_ok!(Subtensor::::burned_register( RawOrigin::Signed(coldkey.clone()).into(), @@ -1106,8 +1119,8 @@ mod pallet_benchmarks { let reg_fee = Subtensor::::get_burn(netuid); let stake_tao = DefaultMinStake::::get().saturating_mul(10.into()); let deposit = reg_fee.saturating_mul(2.into()).saturating_add(stake_tao); - add_balance_to_coldkey_account::(&coldkey, deposit.into()); - add_lock::(&coldkey, netuid); + credit_benchmark_coldkey_tao::(&coldkey, deposit.into()); + seed_zero_alpha_stake_lock::(&coldkey, netuid); assert_ok!(Subtensor::::burned_register( RawOrigin::Signed(coldkey.clone()).into(), @@ -1115,7 +1128,7 @@ mod pallet_benchmarks { hot.clone() )); - set_reserves::(netuid, deposit, AlphaBalance::from(deposit.to_u64())); + set_subnet_amm_reserves::(netuid, deposit, AlphaBalance::from(deposit.to_u64())); TotalStake::::set(deposit); assert_ok!(Subtensor::::add_stake_limit( @@ -1158,8 +1171,8 @@ mod pallet_benchmarks { let reg_fee = Subtensor::::get_burn(netuid); let stake_tao = DefaultMinStake::::get().saturating_mul(10.into()); let deposit = reg_fee.saturating_mul(2.into()).saturating_add(stake_tao); - add_balance_to_coldkey_account::(&coldkey, deposit.into()); - add_lock::(&coldkey, netuid); + credit_benchmark_coldkey_tao::(&coldkey, deposit.into()); + seed_zero_alpha_stake_lock::(&coldkey, netuid); assert_ok!(Subtensor::::burned_register( RawOrigin::Signed(coldkey.clone()).into(), @@ -1167,7 +1180,7 @@ mod pallet_benchmarks { hot.clone() )); - set_reserves::(netuid, deposit, AlphaBalance::from(deposit.to_u64())); + set_subnet_amm_reserves::(netuid, deposit, AlphaBalance::from(deposit.to_u64())); TotalStake::::set(deposit); assert_ok!(Subtensor::::add_stake_limit( @@ -1213,8 +1226,8 @@ mod pallet_benchmarks { let deposit = reg_fee .saturating_mul(2.into()) .saturating_add(TaoBalance::from(collateral_alpha.to_u64()).saturating_mul(2.into())); - add_balance_to_coldkey_account::(&coldkey, deposit.into()); - add_lock::(&coldkey, netuid); + credit_benchmark_coldkey_tao::(&coldkey, deposit.into()); + seed_zero_alpha_stake_lock::(&coldkey, netuid); assert_ok!(Subtensor::::burned_register( RawOrigin::Signed(coldkey.clone()).into(), @@ -1222,7 +1235,7 @@ mod pallet_benchmarks { hot.clone() )); - set_reserves::(netuid, deposit, AlphaBalance::from(deposit.to_u64())); + set_subnet_amm_reserves::(netuid, deposit, AlphaBalance::from(deposit.to_u64())); TotalStake::::set(deposit); // Moving price ≈ 1 so shortfall alpha maps 1:1 into TAO for the buy. SubnetMovingPrice::::insert(netuid, I96F32::from_num(1)); @@ -1302,9 +1315,9 @@ mod pallet_benchmarks { let reg_fee = Subtensor::::get_burn(netuid1); let stake_tao = DefaultMinStake::::get().saturating_mul(10.into()); let deposit = reg_fee.saturating_mul(2.into()).saturating_add(stake_tao); - add_balance_to_coldkey_account::(&coldkey, deposit.into()); - add_lock::(&coldkey, netuid1); - add_lock::(&coldkey, netuid2); + credit_benchmark_coldkey_tao::(&coldkey, deposit.into()); + seed_zero_alpha_stake_lock::(&coldkey, netuid1); + seed_zero_alpha_stake_lock::(&coldkey, netuid2); assert_ok!(Subtensor::::burned_register( RawOrigin::Signed(coldkey.clone()).into(), @@ -1312,8 +1325,8 @@ mod pallet_benchmarks { hot.clone() )); - set_reserves::(netuid1, deposit, AlphaBalance::from(deposit.to_u64())); - set_reserves::(netuid2, deposit, AlphaBalance::from(deposit.to_u64())); + set_subnet_amm_reserves::(netuid1, deposit, AlphaBalance::from(deposit.to_u64())); + set_subnet_amm_reserves::(netuid2, deposit, AlphaBalance::from(deposit.to_u64())); TotalStake::::set(deposit); assert_ok!(Subtensor::::add_stake_limit( @@ -1352,7 +1365,7 @@ mod pallet_benchmarks { Subtensor::::set_weights_set_rate_limit(netuid, 0); Subtensor::::set_burn(netuid, benchmark_registration_burn()); - seed_swap_reserves::(netuid); + seed_default_subnet_amm_reserves::(netuid); fund_for_registration::(netuid, &hotkey); assert_ok!(Subtensor::::burned_register( @@ -1396,7 +1409,7 @@ mod pallet_benchmarks { Subtensor::::set_weights_set_rate_limit(netuid, 0); Subtensor::::set_burn(netuid, benchmark_registration_burn()); - seed_swap_reserves::(netuid); + seed_default_subnet_amm_reserves::(netuid); fund_for_registration::(netuid, &hotkey); assert_ok!(Subtensor::::burned_register( @@ -1454,7 +1467,7 @@ mod pallet_benchmarks { Subtensor::::set_network_registration_allowed(1.into(), true); Subtensor::::set_network_rate_limit(1); let amount: u64 = 9_999_999_999_999; - add_balance_to_coldkey_account::(&coldkey, amount.into()); + credit_benchmark_coldkey_tao::(&coldkey, amount.into()); #[extrinsic_call] _( @@ -1482,7 +1495,7 @@ mod pallet_benchmarks { SubtokenEnabled::::insert(netuid, true); Subtensor::::set_burn(netuid, benchmark_registration_burn()); - seed_swap_reserves::(netuid); + seed_default_subnet_amm_reserves::(netuid); fund_for_registration::(netuid, &caller); assert_ok!(Subtensor::::burned_register( @@ -1524,10 +1537,10 @@ mod pallet_benchmarks { Subtensor::::set_network_registration_allowed(netuid, true); SubtokenEnabled::::insert(netuid, true); Subtensor::::set_burn(netuid, benchmark_registration_burn()); - seed_swap_reserves::(netuid); + seed_default_subnet_amm_reserves::(netuid); let deposit: u64 = 1_000_000_000u64.saturating_mul(2); - add_balance_to_coldkey_account::(&coldkey, deposit.into()); + credit_benchmark_coldkey_tao::(&coldkey, deposit.into()); assert_ok!(Subtensor::::burned_register( RawOrigin::Signed(coldkey.clone()).into(), @@ -1599,7 +1612,7 @@ mod pallet_benchmarks { Subtensor::::init_new_network(netuid, 1); Subtensor::::set_max_allowed_uids(netuid, 1); SubtokenEnabled::::insert(netuid, true); - seed_swap_reserves::(netuid); + seed_default_subnet_amm_reserves::(netuid); SubnetAlphaOut::::insert(netuid, subnet_alpha); Subtensor::::append_neuron(netuid, &old, 0); } @@ -1621,7 +1634,7 @@ mod pallet_benchmarks { Owner::::insert(&old, &coldkey); let ed = ::ExistentialDeposit::get(); let cost = Subtensor::::get_key_swap_cost(); - add_balance_to_coldkey_account::(&coldkey, cost + ed); + credit_benchmark_coldkey_tao::(&coldkey, cost + ed); #[extrinsic_call] _(RawOrigin::Signed(coldkey.clone()), old, new, None); @@ -1663,7 +1676,7 @@ mod pallet_benchmarks { let hotkey: T::AccountId = account("Alice", 0, seed); Subtensor::::set_burn(netuid, benchmark_registration_burn()); - set_reserves::( + set_subnet_amm_reserves::( netuid, TaoBalance::from(150_000_000_000_u64), AlphaBalance::from(100_000_000_000_u64), @@ -1680,7 +1693,7 @@ mod pallet_benchmarks { )); let staked_amt = TaoBalance::from(100_000_000_000_u64); - add_balance_to_coldkey_account::(&coldkey.clone(), staked_amt); + credit_benchmark_coldkey_tao::(&coldkey.clone(), staked_amt); assert_ok!(Subtensor::::add_stake( RawOrigin::Signed(coldkey.clone()).into(), @@ -1714,11 +1727,11 @@ mod pallet_benchmarks { let tao_reserve = TaoBalance::from(1_000_000_000_000_u64); let alpha_in = AlphaBalance::from(100_000_000_000_000_u64); - set_reserves::(netuid, tao_reserve, alpha_in); + set_subnet_amm_reserves::(netuid, tao_reserve, alpha_in); // Registration now requires keep-alive coverage of the burn. fund_for_registration::(netuid, &coldkey); - add_lock::(&coldkey, netuid); + seed_zero_alpha_stake_lock::(&coldkey, netuid); assert_ok!(Subtensor::::burned_register( RawOrigin::Signed(coldkey.clone()).into(), @@ -1734,7 +1747,7 @@ mod pallet_benchmarks { .saturating_to_num::() .into(); let staked_amt = TaoBalance::from(1_000_000_000_u64); - add_balance_to_coldkey_account::(&coldkey.clone(), staked_amt); + credit_benchmark_coldkey_tao::(&coldkey.clone(), staked_amt); assert_ok!(Subtensor::::add_stake( RawOrigin::Signed(coldkey.clone()).into(), @@ -1763,7 +1776,7 @@ mod pallet_benchmarks { let cap = TaoBalance::from(2_000_000_000_000_u64); // 2000 TAO let funds_account: T::AccountId = account("funds", 0, 0); - add_balance_to_coldkey_account::(&funds_account, cap.into()); + credit_benchmark_coldkey_tao::(&funds_account, cap.into()); pallet_crowdloan::Crowdloans::::insert( crowdloan_id, @@ -1822,7 +1835,7 @@ mod pallet_benchmarks { let cap = TaoBalance::from(2_000_000_000_000_u64); // 2000 TAO let funds_account: T::AccountId = account("funds", 0, 0); - add_balance_to_coldkey_account::(&funds_account, cap); + credit_benchmark_coldkey_tao::(&funds_account, cap); pallet_crowdloan::Crowdloans::::insert( crowdloan_id, @@ -1913,7 +1926,7 @@ mod pallet_benchmarks { SubtokenEnabled::::insert(netuid, true); Subtensor::::set_burn(netuid, benchmark_registration_burn()); - seed_swap_reserves::(netuid); + seed_default_subnet_amm_reserves::(netuid); fund_for_registration::(netuid, &hotkey); assert_ok!(Subtensor::::burned_register( @@ -1949,7 +1962,7 @@ mod pallet_benchmarks { Subtensor::::set_network_registration_allowed(netuid, true); let amount = 900_000_000_000u64; - add_balance_to_coldkey_account::(&coldkey.clone(), amount.into()); + credit_benchmark_coldkey_tao::(&coldkey.clone(), amount.into()); assert_ok!(Subtensor::::burned_register( RawOrigin::Signed(coldkey.clone()).into(), @@ -1977,7 +1990,7 @@ mod pallet_benchmarks { let netuid = Subtensor::::get_next_netuid(); let lock_cost = Subtensor::::get_network_lock_cost(); - add_balance_to_coldkey_account::(&coldkey, lock_cost.into()); + credit_benchmark_coldkey_tao::(&coldkey, lock_cost.into()); assert_ok!(Subtensor::::register_network( RawOrigin::Signed(coldkey.clone()).into(), @@ -2051,7 +2064,7 @@ mod pallet_benchmarks { let netuid = Subtensor::::get_next_netuid(); let lock_cost = Subtensor::::get_network_lock_cost(); - add_balance_to_coldkey_account::(&coldkey, lock_cost.into()); + credit_benchmark_coldkey_tao::(&coldkey, lock_cost.into()); assert_ok!(Subtensor::::register_network( RawOrigin::Signed(coldkey.clone()).into(), @@ -2102,12 +2115,12 @@ mod pallet_benchmarks { let balance_update = TaoBalance::from(900_000_000_000_u64); let limit = TaoBalance::from(6_000_000_000_u64); let amount = TaoBalance::from(44_000_000_000_u64); - add_balance_to_coldkey_account::(&coldkey.clone(), balance_update); - add_lock::(&coldkey, netuid); + credit_benchmark_coldkey_tao::(&coldkey.clone(), balance_update); + seed_zero_alpha_stake_lock::(&coldkey, netuid); let tao_reserve = TaoBalance::from(150_000_000_000_u64); let alpha_in = AlphaBalance::from(100_000_000_000_u64); - set_reserves::(netuid, tao_reserve, alpha_in); + set_subnet_amm_reserves::(netuid, tao_reserve, alpha_in); assert_ok!(Subtensor::::burned_register( RawOrigin::Signed(coldkey.clone()).into(), @@ -2152,9 +2165,9 @@ mod pallet_benchmarks { let total_stake = TaoBalance::from(1_000_000_000); let amount = AlphaBalance::from(60_000_000); - seed_swap_reserves::(netuid); + seed_default_subnet_amm_reserves::(netuid); let burn = Subtensor::::get_burn(netuid); - add_balance_to_coldkey_account::( + credit_benchmark_coldkey_tao::( &coldkey, total_stake .saturating_mul(2.into()) @@ -2202,9 +2215,9 @@ mod pallet_benchmarks { let total_stake = TaoBalance::from(1_000_000_000); let amount = AlphaBalance::from(60_000_000); - seed_swap_reserves::(netuid); + seed_default_subnet_amm_reserves::(netuid); let burn = Subtensor::::get_burn(netuid); - add_balance_to_coldkey_account::( + credit_benchmark_coldkey_tao::( &coldkey, total_stake .saturating_mul(2.into()) @@ -2249,6 +2262,7 @@ mod pallet_benchmarks { ); } + // --- EVM key association ---------------------------------------------------- #[benchmark] fn associate_evm_key() { let netuid = NetUid::from(1); @@ -2263,7 +2277,7 @@ mod pallet_benchmarks { Subtensor::::set_max_allowed_uids(netuid, 4096); Subtensor::::set_burn(netuid, benchmark_registration_burn()); - seed_swap_reserves::(netuid); + seed_default_subnet_amm_reserves::(netuid); fund_for_registration::(netuid, &coldkey); assert_ok!(Subtensor::::burned_register( @@ -2292,7 +2306,7 @@ mod pallet_benchmarks { let block_number = Subtensor::::get_current_block_as_u64(); let evm_secret_key = benchmark_evm_secret_key(); - let evm_key = evm_key_from_secret_key(&evm_secret_key); + let evm_key = benchmark_evm_address_from_secret_key(&evm_secret_key); let signature = signature_for_associate_evm_key::(&hotkey, block_number, &evm_secret_key); @@ -2331,6 +2345,7 @@ mod pallet_benchmarks { _(RawOrigin::Signed(coldkey.clone()), netuid); } + // --- Transaction-extension weight paths ------------------------------------- #[benchmark] fn check_coldkey_swap_extension() { let coldkey: T::AccountId = account("coldkey", 0, 1); @@ -2338,7 +2353,7 @@ mod pallet_benchmarks { let hotkey: T::AccountId = account("hotkey", 0, 1); let new_coldkey_hash: T::Hash = ::Hashing::hash_of(&new_coldkey); let now = frame_system::Pallet::::block_number(); - let call = runtime_call::(Call::::register_network { hotkey }); + let call = into_runtime_call_from_subtensor::(Call::::register_network { hotkey }); ColdkeySwapAnnouncements::::insert(&coldkey, (now, new_coldkey_hash)); ColdkeySwapDisputes::::insert(&coldkey, now); @@ -2362,7 +2377,7 @@ mod pallet_benchmarks { let salt: Vec = vec![8]; let version_key = 0_u64; - setup_extension_neuron::(netuid, &hotkey); + setup_neuron_for_tx_extension_benchmark::(netuid, &hotkey); Subtensor::::set_stake_threshold(0); let commit_hash = Subtensor::::get_commit_hash( @@ -2409,7 +2424,7 @@ mod pallet_benchmarks { version_key: 0, }; - setup_extension_neuron::(netuid, &hotkey); + setup_neuron_for_tx_extension_benchmark::(netuid, &hotkey); Subtensor::::set_commit_reveal_weights_enabled(netuid, false); Subtensor::::set_weights_set_rate_limit(netuid, 1); Subtensor::::set_last_update_for_uid(netuid_index, 0, 1); @@ -2473,7 +2488,7 @@ mod pallet_benchmarks { signature: ecdsa::Signature::from_raw([0_u8; 65]), }; - setup_extension_neuron::(netuid, &hotkey); + setup_neuron_for_tx_extension_benchmark::(netuid, &hotkey); set_benchmark_block_number::(block_number); #[block] @@ -2482,6 +2497,7 @@ mod pallet_benchmarks { } } + // --- Mechanism weights ------------------------------------------------------ #[benchmark] fn set_mechanism_weights(n: Linear<1, 4096>) { let mecid = subtensor_runtime_common::MechId::MAIN; @@ -2545,7 +2561,7 @@ mod pallet_benchmarks { Subtensor::::set_stake_threshold(0); Subtensor::::set_commit_reveal_weights_enabled(netuid, true); Subtensor::::set_burn(netuid, benchmark_registration_burn()); - set_reserves::( + set_subnet_amm_reserves::( netuid, TaoBalance::from(1_000_000_000_000_u64), AlphaBalance::from(1_000_000_000_000_000_u64), @@ -2701,7 +2717,7 @@ mod pallet_benchmarks { SubtokenEnabled::::insert(netuid, true); Subtensor::::set_network_registration_allowed(netuid, true); Burn::::insert(netuid, benchmark_registration_burn()); - seed_swap_reserves::(netuid); + seed_default_subnet_amm_reserves::(netuid); fund_for_registration::(netuid, &coldkey); assert_ok!(Subtensor::::burned_register( @@ -2722,7 +2738,7 @@ mod pallet_benchmarks { Owner::::insert(&old_hotkey, &coldkey); let cost = Subtensor::::get_key_swap_cost(); - add_balance_to_coldkey_account::(&coldkey, cost.into()); + credit_benchmark_coldkey_tao::(&coldkey, cost.into()); #[extrinsic_call] _( @@ -2778,7 +2794,7 @@ mod pallet_benchmarks { Subtensor::::set_network_registration_allowed(netuid, true); SubtokenEnabled::::insert(netuid, true); Burn::::insert(netuid, benchmark_registration_burn()); - seed_swap_reserves::(netuid); + seed_default_subnet_amm_reserves::(netuid); fund_for_registration::(netuid, &coldkey); assert_ok!(Subtensor::::burned_register( @@ -2873,7 +2889,7 @@ mod pallet_benchmarks { Subtensor::::set_network_registration_allowed(netuid, true); SubtokenEnabled::::insert(netuid, true); Burn::::insert(netuid, benchmark_registration_burn()); - seed_swap_reserves::(netuid); + seed_default_subnet_amm_reserves::(netuid); fund_for_registration::(netuid, &coldkey); assert_ok!(Subtensor::::burned_register( @@ -2881,7 +2897,7 @@ mod pallet_benchmarks { netuid, hotkey.clone(), )); - add_lock::(&coldkey, netuid); + seed_zero_alpha_stake_lock::(&coldkey, netuid); #[extrinsic_call] _(RawOrigin::Signed(coldkey.clone()), netuid, true); diff --git a/pallets/subtensor/src/benchmarks/helpers.rs b/pallets/subtensor/src/benchmarks/helpers.rs index 2f45e98e80..cea36efe97 100644 --- a/pallets/subtensor/src/benchmarks/helpers.rs +++ b/pallets/subtensor/src/benchmarks/helpers.rs @@ -1,12 +1,21 @@ +//! Shared setup helpers for Subtensor pallet runtime benchmarks. +//! +//! These `pub(super)` functions seed storage (AMM reserves, neurons, stake locks, +//! EVM keys) so individual `#[benchmark]` cases measure extrinsic work rather +//! than registration or mint costs. Helper names are intentionally distinct from +//! extrinsic / WeightInfo method names — those stay frozen to match dispatchables. + use super::*; -pub(super) fn seed_swap_reserves(netuid: NetUid) { +/// Seed default TAO/alpha pool reserves used by most stake and registration benchmarks. +pub(super) fn seed_default_subnet_amm_reserves(netuid: NetUid) { let tao_reserve = TaoBalance::from(150_000_000_000_u64); let alpha_in = AlphaBalance::from(100_000_000_000_u64); - set_reserves::(netuid, tao_reserve, alpha_in); + set_subnet_amm_reserves::(netuid, tao_reserve, alpha_in); } -pub(super) fn set_reserves( +/// Write `SubnetTAO` / `SubnetAlphaIn` for `netuid` (benchmark AMM liquidity only). +pub(super) fn set_subnet_amm_reserves( netuid: NetUid, tao_reserve: TaoBalance, alpha_in: AlphaBalance, @@ -15,11 +24,13 @@ pub(super) fn set_reserves( SubnetAlphaIn::::insert(netuid, alpha_in); } +/// Fixed burn amount used when benchmarks need a cheap, deterministic registration fee. pub(super) fn benchmark_registration_burn() -> TaoBalance { TaoBalance::from(1_000_000) } -pub(super) fn add_balance_to_coldkey_account(coldkey: &T::AccountId, tao: TaoBalance) { +/// Mint `tao` and credit it to `coldkey` via the pallet spend path (benchmark funding). +pub(super) fn credit_benchmark_coldkey_tao(coldkey: &T::AccountId, tao: TaoBalance) { let credit = Subtensor::::mint_tao(tao); let _ = Subtensor::::spend_tao(coldkey, credit, tao).unwrap(); } @@ -51,9 +62,7 @@ pub(super) fn seed_miner_collateral_position( }); } -/// This helper funds an account with: -/// - 2x burn fee -/// - 100x DefaultMinStake +/// Fund `who` with 2× subnet burn plus 100× `DefaultMinStake` for burned registration. pub(super) fn fund_for_registration(netuid: NetUid, who: &T::AccountId) { let burn = Subtensor::::get_burn(netuid); let min_stake = DefaultMinStake::::get(); @@ -62,15 +71,17 @@ pub(super) fn fund_for_registration(netuid: NetUid, who: &T::AccountI .saturating_mul(2.into()) .saturating_add(min_stake.saturating_mul(100.into())); - add_balance_to_coldkey_account::(who, deposit.into()); + credit_benchmark_coldkey_tao::(who, deposit.into()); } +/// Build a dense `(uid, u16::MAX)` weight/bond row covering `0..uid_count`. pub(super) fn dense_benchmark_weights(uid_count: u16) -> Vec<(u16, u16)> { (0..uid_count) .map(|uid| (uid, u16::MAX)) .collect::>() } +/// Create coldkey/hotkey accounts, append a neuron, and optionally stake `stake` alpha. pub(super) fn seed_benchmark_neuron( netuid: NetUid, hotkey_label: &'static str, @@ -92,6 +103,7 @@ pub(super) fn seed_benchmark_neuron( (hotkey, coldkey) } +/// Fill a subnet to `DefaultMaxAllowedUids` with dense weights/bonds for registration worst-case. pub(super) fn setup_full_subnet_registration_benchmark( netuid: NetUid, hotkey_label: &'static str, @@ -110,7 +122,7 @@ pub(super) fn setup_full_subnet_registration_benchmark( Subtensor::::set_max_registrations_per_block(netuid, uid_count); Subtensor::::set_target_registrations_per_interval(netuid, uid_count); Subtensor::::set_burn(netuid, benchmark_registration_burn()); - seed_swap_reserves::(netuid); + seed_default_subnet_amm_reserves::(netuid); let netuid_index = NetUidStorageIndex::from(netuid); let dense_weights = dense_benchmark_weights(uid_count); @@ -131,6 +143,7 @@ pub(super) fn setup_full_subnet_registration_benchmark( assert_eq!(Subtensor::::get_subnetwork_n(netuid), uid_count); } +/// Populate root with max allowed validators and dense weights/bonds for `root_register`. pub(super) fn setup_full_root_registration_benchmark() { Subtensor::::init_new_network(NetUid::ROOT, 1); SubtokenEnabled::::insert(NetUid::ROOT, true); @@ -175,8 +188,8 @@ pub(super) fn setup_full_root_registration_benchmark() { ); } -/// Add a zero lock to a random hotkey just so that the lock records exist -pub(super) fn add_lock(coldkey: &T::AccountId, netuid: NetUid) { +/// Insert empty `Lock` / `HotkeyLock` rows so stake paths touch lock-index bookkeeping. +pub(super) fn seed_zero_alpha_stake_lock(coldkey: &T::AccountId, netuid: NetUid) { let hotkey: T::AccountId = account("RandomHotkey", 0, 999); Lock::::insert( (coldkey, netuid, hotkey.clone()), @@ -197,6 +210,7 @@ pub(super) fn add_lock(coldkey: &T::AccountId, netuid: NetUid) { ); } +/// Force `frame_system` block number for benchmarks that depend on tempo / epoch timing. pub(super) fn set_benchmark_block_number(block_number: u64) { let block_number: BlockNumberFor = match block_number.try_into() { Ok(block_number) => block_number, @@ -252,7 +266,7 @@ pub(super) fn setup_block_step_benchmark() { PendingEpochAt::::insert(NetUid::ROOT, 0); SubtokenEnabled::::insert(NetUid::ROOT, true); SubnetEmissionEnabled::::insert(NetUid::ROOT, true); - set_reserves::( + set_subnet_amm_reserves::( NetUid::ROOT, TaoBalance::from(SUBNET_TAO_RESERVE), AlphaBalance::from(SUBNET_ALPHA_RESERVE), @@ -303,7 +317,7 @@ pub(super) fn setup_block_step_benchmark() { Subtensor::::set_max_registrations_per_block(netuid, MAINNET_NEURONS_PER_SUBNET); Subtensor::::set_target_registrations_per_interval(netuid, MAINNET_NEURONS_PER_SUBNET); Subtensor::::set_burn(netuid, benchmark_registration_burn()); - set_reserves::( + set_subnet_amm_reserves::( netuid, TaoBalance::from(SUBNET_TAO_RESERVE), AlphaBalance::from(SUBNET_ALPHA_RESERVE), @@ -381,16 +395,24 @@ pub(super) fn setup_block_step_benchmark() { } } -pub(super) fn runtime_call(call: Call) -> ::RuntimeCall { +/// Lift a pallet `Call` into the runtime `RuntimeCall` enum for extension benchmarks. +pub(super) fn into_runtime_call_from_subtensor( + call: Call, +) -> ::RuntimeCall { ::RuntimeCall::from(call).into() } -pub(super) fn setup_extension_neuron(netuid: NetUid, hotkey: &T::AccountId) { +/// Init a subnet and append `hotkey` so transaction-extension benchmarks have a live neuron. +pub(super) fn setup_neuron_for_tx_extension_benchmark( + netuid: NetUid, + hotkey: &T::AccountId, +) { Subtensor::::init_new_network(netuid, 0); Subtensor::::set_max_allowed_uids(netuid, GLOBAL_MAX_SUBNET_COUNT); Subtensor::::append_neuron(netuid, hotkey, 0); } +/// Deterministic secp256k1 secret used by EVM-association benchmarks. pub(super) fn benchmark_evm_secret_key() -> libsecp256k1::SecretKey { let seed = [42u8; 32]; @@ -400,7 +422,8 @@ pub(super) fn benchmark_evm_secret_key() -> libsecp256k1::SecretKey { } } -pub(super) fn evm_key_from_secret_key(secret_key: &libsecp256k1::SecretKey) -> H160 { +/// Derive the H160 address from a secp256k1 secret (keccak of the uncompressed pubkey). +pub(super) fn benchmark_evm_address_from_secret_key(secret_key: &libsecp256k1::SecretKey) -> H160 { let public_key = libsecp256k1::PublicKey::from_secret_key(secret_key); let uncompressed = public_key.serialize(); @@ -419,6 +442,7 @@ pub(super) fn evm_key_from_secret_key(secret_key: &libsecp256k1::SecretKey) -> H H160::from_slice(evm_key_bytes) } +/// ECDSA signature over EIP-191(hotkey ‖ keccak(block_number)) for `associate_evm_key`. pub(super) fn signature_for_associate_evm_key( hotkey: &T::AccountId, block_number: u64, @@ -452,6 +476,7 @@ pub(super) fn signature_for_associate_evm_key( ecdsa::Signature::from_raw(signature) } +/// Register up to `uid_count` neurons (capped at 4096) with validator permits for weight benchmarks. pub(super) fn setup_worst_case_registered_subnet( label: &'static str, netuid: NetUid, @@ -472,7 +497,7 @@ pub(super) fn setup_worst_case_registered_subnet( Subtensor::::set_difficulty(netuid, 1); Subtensor::::set_weights_set_rate_limit(netuid, 0); Subtensor::::set_stake_threshold(0); - set_reserves::( + set_subnet_amm_reserves::( netuid, TaoBalance::from(1_000_000_000_000_u64), AlphaBalance::from(1_000_000_000_000_000_u64), @@ -510,6 +535,7 @@ pub(super) fn setup_worst_case_registered_subnet( ) } +/// Worst-case registered subnet plus commit-reveal salt for mechanism weight benchmarks. pub(super) fn setup_mechanism_weight_benchmark( _mecid: subtensor_runtime_common::MechId, uid_count: u32, diff --git a/pallets/subtensor/src/coinbase/alpha.rs b/pallets/subtensor/src/coinbase/alpha.rs index 98ad874471..ec93c14eb3 100644 --- a/pallets/subtensor/src/coinbase/alpha.rs +++ b/pallets/subtensor/src/coinbase/alpha.rs @@ -1,15 +1,21 @@ +//! Alpha mint / resolve / recycle helpers used by the coinbase and staking paths. +//! +//! Mint returns a [`PositiveAlphaImbalance`]; callers must resolve it into +//! [`SubnetAlphaOut`] (outstanding) or [`SubnetAlphaIn`] (pool reserve), or recycle/burn +//! via the alpha-assets pallet. + use pallet_alpha_assets::{AlphaAssetsInterface, PositiveAlphaImbalance}; use subtensor_runtime_common::{AlphaBalance, NetUid, Token}; use super::*; impl Pallet { - /// Create alpha and return the resulting imbalance for later resolution. + /// Mint `amount` alpha on `netuid` and return the imbalance for later resolution. pub fn mint_alpha(netuid: NetUid, amount: AlphaBalance) -> PositiveAlphaImbalance { T::AlphaAssets::mint_alpha(netuid, amount) } - /// Resolve alpha imbalance into outstanding alpha on the subnet. + /// Resolve alpha imbalance into outstanding alpha ([`SubnetAlphaOut`]) on the subnet. pub fn resolve_to_alpha_out(imbalance: PositiveAlphaImbalance) { let netuid = imbalance.netuid(); let amount = imbalance.amount(); @@ -22,7 +28,7 @@ impl Pallet { }); } - /// Resolve alpha imbalance into alpha held in the subnet reserve. + /// Resolve alpha imbalance into alpha held in the subnet pool reserve ([`SubnetAlphaIn`]). pub fn resolve_to_alpha_in(imbalance: PositiveAlphaImbalance) { let netuid = imbalance.netuid(); let amount = imbalance.amount(); @@ -35,7 +41,8 @@ impl Pallet { }); } - /// Recycle alpha (reduce total alpha issuance) + /// Recycle alpha: decrease [`SubnetAlphaOut`] and call alpha-assets recycle (reduces + /// total alpha issuance). pub fn recycle_subnet_alpha(netuid: NetUid, amount: AlphaBalance) { if amount.is_zero() { return; @@ -48,7 +55,7 @@ impl Pallet { let _ = T::AlphaAssets::recycle_alpha(netuid, amount); } - /// Burn alpha (no change to total alpha issuance) + /// Burn alpha via alpha-assets without changing [`SubnetAlphaOut`] (issuance unchanged). pub fn burn_subnet_alpha(netuid: NetUid, amount: AlphaBalance) { if amount.is_zero() { return; diff --git a/pallets/subtensor/src/coinbase/block_emission.rs b/pallets/subtensor/src/coinbase/block_emission.rs index 9d1f725179..e8044d511f 100644 --- a/pallets/subtensor/src/coinbase/block_emission.rs +++ b/pallets/subtensor/src/coinbase/block_emission.rs @@ -1,42 +1,36 @@ +//! Block TAO emission schedule (logarithmic decay toward the hard supply cap). + use super::*; -// use frame_support::traits::{Currency as BalancesCurrency, Get, Imbalance}; -use crate::coinbase::tao::CreditOf; -use frame_support::traits::{Get, Imbalance}; +use crate::coinbase::tao::TaoCreditOf; +use frame_support::traits::Imbalance; use safe_math::*; use substrate_fixed::{transcendental::log2, types::I96F32}; impl Pallet { - /// Calculates the block emission based on the total issuance and mints corresponding - /// amount of TAO. - /// - /// This function computes the block emission by applying a logarithmic function - /// to the total issuance of the network. The formula used takes into account - /// the current total issuance and adjusts the emission rate accordingly to ensure - /// a smooth issuance curve. The emission rate decreases as the total issuance increases, - /// following a logarithmic decay. + /// Mint this block's TAO emission as a currency credit (or zero credit if none). /// - /// # Returns - /// * `Result`: The calculated block emission rate or error. - /// - pub fn get_block_emission() -> CreditOf { + /// Uses [`Pallet::calculate_block_emission`] then [`Pallet::mint_tao`]. The credit is + /// later spent by [`Pallet::run_coinbase`]. + pub fn get_block_emission() -> TaoCreditOf { let maybe_tao_to_mint = Self::calculate_block_emission(); if let Ok(tao_to_mint) = maybe_tao_to_mint && !tao_to_mint.is_zero() { return Self::mint_tao(tao_to_mint.into()); } - CreditOf::::zero() + TaoCreditOf::::zero() } - /// Calculates the block emission based on the total issuance only, no minting happens. + /// Block emission in TAO for the current total issuance — no minting. pub fn calculate_block_emission() -> Result { - // Convert the total issuance to a fixed-point number for calculation. Self::get_block_emission_for_issuance(Self::get_total_issuance().into()).map(Into::into) } - /// Returns the block emission for an issuance value. + /// Block emission (rao) for a hypothetical `issuance` under the log₂ residual schedule. + /// + /// Returns `0` when issuance is at or above [`TotalSupply`]. The curve floors the log + /// residual so emission steps down in powers of two relative to [`DefaultBlockEmission`]. pub fn get_block_emission_for_issuance(issuance: u64) -> Result { - // Convert issuance to a float for calculations below. let total_issuance: I96F32 = I96F32::saturating_from_num(issuance); // Check to prevent division by zero when the total supply is reached // and creating an issuance greater than the total supply. diff --git a/pallets/subtensor/src/coinbase/block_step.rs b/pallets/subtensor/src/coinbase/block_step.rs index 00f1ac16a9..ca6aece433 100644 --- a/pallets/subtensor/src/coinbase/block_step.rs +++ b/pallets/subtensor/src/coinbase/block_step.rs @@ -1,9 +1,12 @@ +//! Per-block hook body: registration prices → mint → reveal → coinbase → EMA/root updates. + use super::*; use substrate_fixed::types::U96F32; use subtensor_runtime_common::NetUid; impl Pallet { - /// Executes the necessary operations for each block. + /// Ordered per-block coinbase pipeline (see module docs). Invoked from the runtime + /// `on_initialize` / block-step hook. pub fn block_step() -> Result<(), &'static str> { let block_number: u64 = Self::get_current_block_as_u64(); let last_block_hash: T::Hash = >::parent_hash(); @@ -21,44 +24,42 @@ impl Pallet { Self::run_coinbase(block_emission); // --- 5. Update moving prices AFTER using them for emissions. Self::update_moving_prices(); - // --- 6. Update roop prop AFTER using them for emissions. + // --- 6. Update root prop AFTER using them for emissions. Self::update_root_prop(); // --- 7. Set pending children on the epoch; but only after the coinbase has been run. - Self::try_set_pending_children(block_number); + Self::set_pending_children_after_epoch(block_number); // --- 8. Run auto-claim root divs. Self::run_auto_claim_root_divs(last_block_hash); // --- 9. Populate root coldkey maps. Self::populate_root_coldkey_staking_maps(); Self::populate_root_coldkey_staking_maps_v2(); - // Return ok. Ok(()) } - fn try_set_pending_children(block_number: u64) { - // Called *after* `run_coinbase` has advanced `LastEpochBlock` for any - // subnet whose epoch slot fired this block — `should_run_epoch` is no - // longer true. Detect "epoch just fired" by `LastEpochBlock == block`. + /// After `run_coinbase` advances `LastEpochBlock`, apply pending children for any subnet + /// whose epoch just fired (`LastEpochBlock == block_number`). + fn set_pending_children_after_epoch(block_number: u64) { for netuid in Self::get_all_subnet_netuids() { if LastEpochBlock::::get(netuid) == block_number { Self::do_set_pending_children(netuid); } } } + + /// Advance the alpha-price EMA for every subnet that received emission this block. pub fn update_moving_prices() { let subnets_to_emit_to: Vec = Self::get_subnets_to_emit_to(&Self::get_all_subnet_netuids()); - // Only update price EMA for subnets that we emit to. for netuid_i in subnets_to_emit_to.iter() { - // Update moving prices after using them above. Self::update_moving_price(*netuid_i); } } + /// Refresh [`RootProp`] for every emit-eligible subnet from [`Pallet::root_proportion`]. pub fn update_root_prop() { let subnets_to_emit_to: Vec = Self::get_subnets_to_emit_to(&Self::get_all_subnet_netuids()); - // Only root_prop for subnets that we emit to. for netuid_i in subnets_to_emit_to.iter() { let root_prop = Self::root_proportion(*netuid_i); @@ -66,6 +67,10 @@ impl Pallet { } } + /// Root's share of a subnet's stake-weighted mass: + /// `tao_weight / (tao_weight + alpha_issuance)` where `tao_weight = root_tao * TAO_WEIGHT`. + /// + /// Caps alpha injection into older pools (see [`Pallet::compute_subnet_emission_terms`]). pub fn root_proportion(netuid: NetUid) -> U96F32 { let alpha_issuance = U96F32::from_num(Self::get_alpha_issuance(netuid)); let root_tao: U96F32 = U96F32::from_num(Self::get_subnet_tao(NetUid::ROOT)); @@ -78,6 +83,8 @@ impl Pallet { root_proportion } + /// Reveal matured CRv3 commits for every non-root subnet not deferred by the per-block + /// epoch cap. pub fn reveal_crv3_commits() { let current_block = Self::get_current_block_as_u64(); let subnets: Vec = Self::get_all_subnet_netuids() @@ -91,7 +98,6 @@ impl Pallet { if deferred.contains(&netuid) { continue; } - // Reveal matured weights. if let Err(e) = Self::reveal_crv3_commits_for_subnet(netuid) { log::warn!("Failed to reveal commits for subnet {netuid} due to error: {e:?}"); }; diff --git a/pallets/subtensor/src/coinbase/mod.rs b/pallets/subtensor/src/coinbase/mod.rs index 5184e2e3c0..784dba276b 100644 --- a/pallets/subtensor/src/coinbase/mod.rs +++ b/pallets/subtensor/src/coinbase/mod.rs @@ -1,4 +1,30 @@ +//! Coinbase / emission pipeline for Subtensor. +//! +//! Each block, [`Pallet::block_step`] (via the runtime hook): +//! 1. Updates registration prices +//! 2. Mints this block's TAO ([`block_emission`]) +//! 3. Reveals matured CRv3 weight commits ([`reveal_commits`]) +//! 4. Runs the coinbase ([`run_coinbase`]) — inject liquidity, accumulate pending alpha, +//! drain on epoch, pay dividends +//! 5. Updates moving prices / root proportions +//! 6. Applies pending children, auto-claims root divs, refreshes root coldkey maps +//! +//! ## Search anchors +//! +//! | Module | Owns | +//! |--------|------| +//! | [`tao`] | TAO mint/burn/recycle/transfer and registration locks | +//! | [`alpha`] | Alpha mint/resolve/recycle/burn into subnet reserves | +//! | [`block_emission`] | Logarithmic TAO emission schedule vs total issuance | +//! | [`subnet_emissions`] | Which subnets emit and how TAO is shared among them | +//! | [`run_coinbase`] | Injection, pending drain, dividend payout | +//! | [`tempo_control`] | Owner/root tempo, activity cutoff, epoch trigger | +//! | [`root`] | Root registration, network lock cost, prune candidate | +//! | [`reveal_commits`] | Timelock (drand) weight reveal | +//! | [`block_step`] | Ordered per-block orchestration of the above | + use super::*; + pub mod alpha; pub mod block_emission; pub mod block_step; diff --git a/pallets/subtensor/src/coinbase/reveal_commits.rs b/pallets/subtensor/src/coinbase/reveal_commits.rs index a5cddd6856..ab381cbb44 100644 --- a/pallets/subtensor/src/coinbase/reveal_commits.rs +++ b/pallets/subtensor/src/coinbase/reveal_commits.rs @@ -1,7 +1,13 @@ +//! Commit-reveal v3 weight reveal using drand timelock encryption (TLE). +//! +//! On each block (via [`Pallet::reveal_crv3_commits`]), matured commits for epoch +//! `current_epoch - reveal_period` are decrypted with the drand pulse and applied through +//! [`Pallet::do_set_mechanism_weights`]. Missing pulses are re-queued until the pulse lands. + use super::*; use ark_serialize::CanonicalDeserialize; use codec::Decode; -use frame_support::{dispatch, traits::OriginTrait}; +use frame_support::dispatch; use scale_info::prelude::collections::VecDeque; use subtensor_runtime_common::{MechId, NetUid}; use tle::{ @@ -11,11 +17,11 @@ use tle::{ }; use w3f_bls::EngineBLS; -/// Contains all necessary information to set weights. +/// Decrypted CRv3 weight payload: hotkey plus uids/values/version_key. /// -/// In the context of commit-reveal v3, this is the payload which should be -/// encrypted, compressed, serialized, and submitted to the `commit_crv3_weights` -/// extrinsic. +/// In commit-reveal v3 this is the payload clients encrypt, compress, serialize, +/// and submit to the `commit_crv3_weights` extrinsic before the reveal epoch. +/// Reveal applies it via `do_set_mechanism_weights` after drand TLE decryption. #[derive(Encode, Decode)] #[freeze_struct("b6833b5029be4127")] pub struct WeightsTlockPayload { @@ -25,7 +31,7 @@ pub struct WeightsTlockPayload { pub version_key: u64, } -/// For the old structure +/// Pre-hotkey CRv3 payload (uids/values/version_key only); still accepted on reveal. #[derive(Encode, Decode)] #[freeze_struct("304e55f41267caa")] pub struct LegacyWeightsTlockPayload { @@ -35,7 +41,9 @@ pub struct LegacyWeightsTlockPayload { } impl Pallet { - /// The `reveal_crv3_commits` function is run at the very beginning of epoch `n`, + /// Decrypt and apply CRv3 weight commits whose reveal epoch is `current - reveal_period`. + /// + /// Commits whose drand round is missing stay queued for retry within the reveal epoch. pub fn reveal_crv3_commits_for_subnet(netuid: NetUid) -> dispatch::DispatchResult { let reveal_period = Self::get_reveal_period(netuid); // If the subnet is deferred past this block the diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index 7aeb0430b2..d34735d2b5 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -15,6 +15,11 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +//! Root-subnet registration, network lock-cost schedule, and prune-candidate selection. +//! +//! Also hosts thin getters/setters for network immunity / min-lock / rate-limit last-block +//! storage used by registration and admin paths. + use super::*; use safe_math::*; use substrate_fixed::types::{I64F64, U64F64}; @@ -54,7 +59,7 @@ impl Pallet { /// pub fn contains_invalid_root_uids(netuids: &[NetUid]) -> bool { for netuid in netuids { - if !Self::if_subnet_exist(*netuid) { + if !Self::subnet_exists(*netuid) { log::debug!("contains_invalid_root_uids: netuid {netuid:?} does not exist"); return true; } @@ -78,7 +83,7 @@ impl Pallet { // --- 0. Get the unique identifier (UID) for the root network. let current_block_number: u64 = Self::get_current_block_as_u64(); ensure!( - Self::if_subnet_exist(NetUid::ROOT), + Self::subnet_exists(NetUid::ROOT), Error::::RootNetworkDoesNotExist ); @@ -232,46 +237,59 @@ impl Pallet { lock_cost } + /// Block at which `netuid` was registered ([`NetworkRegisteredAt`]). pub fn get_network_registered_block(netuid: NetUid) -> u64 { NetworkRegisteredAt::::get(netuid) } + /// Registration-order counter for `netuid` ([`RegisteredSubnetCounter`]). pub fn get_registered_subnet_counter(netuid: NetUid) -> u64 { RegisteredSubnetCounter::::get(netuid) } + /// Blocks of immunity from pruning after registration. pub fn get_network_immunity_period() -> u64 { NetworkImmunityPeriod::::get() } + /// Set the network immunity period (emits [`Event::NetworkImmunityPeriodSet`]). pub fn set_network_immunity_period(net_immunity_period: u64) { NetworkImmunityPeriod::::set(net_immunity_period); Self::deposit_event(Event::NetworkImmunityPeriodSet(net_immunity_period)); } + /// Set delay before `start_call` is allowed on a new subnet. pub fn set_start_call_delay(delay: u64) { StartCallDelay::::set(delay); Self::deposit_event(Event::StartCallDelaySet(delay)); } + /// Set the floor for [`Pallet::get_network_lock_cost`]. pub fn set_network_min_lock(net_min_lock: TaoBalance) { NetworkMinLockCost::::set(net_min_lock); Self::deposit_event(Event::NetworkMinLockCostSet(net_min_lock)); } + /// Minimum network registration lock cost. pub fn get_network_min_lock() -> TaoBalance { NetworkMinLockCost::::get() } + /// Record the lock cost paid by the most recent network registration. pub fn set_network_last_lock(net_last_lock: TaoBalance) { NetworkLastLockCost::::set(net_last_lock); } + /// Lock cost paid by the most recent network registration. pub fn get_network_last_lock() -> TaoBalance { NetworkLastLockCost::::get() } + /// Block of the last network registration (via [`RateLimitKey::NetworkLastRegistered`]). pub fn get_network_last_lock_block() -> u64 { Self::get_rate_limited_last_block(&RateLimitKey::NetworkLastRegistered) } + /// Stamp the last network-registration block for rate limiting / lock decay. pub fn set_network_last_lock_block(block: u64) { Self::set_rate_limited_last_block(&RateLimitKey::NetworkLastRegistered, block); } + /// Set how quickly [`Pallet::get_network_lock_cost`] decays toward the minimum. pub fn set_lock_reduction_interval(interval: u64) { NetworkLockReductionInterval::::set(interval); Self::deposit_event(Event::NetworkLockCostReductionIntervalSet(interval)); } + /// Lock-cost reduction interval, scaled by current block emission vs 1 TAO. pub fn get_lock_reduction_interval() -> u64 { let interval: I64F64 = I64F64::saturating_from_num(NetworkLockReductionInterval::::get()); @@ -286,16 +304,20 @@ impl Pallet { let halved_interval: I64F64 = interval.saturating_mul(halving); halved_interval.saturating_to_num::() } + /// Last block when `rate_limit_key` was used ([`LastRateLimitedBlock`]). pub fn get_rate_limited_last_block(rate_limit_key: &RateLimitKey) -> u64 { LastRateLimitedBlock::::get(rate_limit_key) } + /// Record that `rate_limit_key` was used at `block`. pub fn set_rate_limited_last_block(rate_limit_key: &RateLimitKey, block: u64) { LastRateLimitedBlock::::insert(rate_limit_key, block); } + /// Clear the last-used block for `rate_limit_key`. pub fn remove_rate_limited_last_block(rate_limit_key: &RateLimitKey) { LastRateLimitedBlock::::remove(rate_limit_key); } + /// Lowest moving-price non-root subnet outside its immunity period (ties → earliest registration). pub fn get_network_to_prune() -> Option { let current_block: u64 = Self::get_current_block_as_u64(); diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase/dividend_distribution.rs similarity index 51% rename from pallets/subtensor/src/coinbase/run_coinbase.rs rename to pallets/subtensor/src/coinbase/run_coinbase/dividend_distribution.rs index e3425addba..493952312b 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase/dividend_distribution.rs @@ -1,509 +1,15 @@ +//! Epoch dividend / incentive calculation and stake payout. + use super::*; -use crate::coinbase::tao::CreditOf; -use alloc::collections::{BTreeMap, BTreeSet}; -use frame_support::traits::Imbalance; +use super::{as_u96f32, to_u64}; +use alloc::collections::BTreeMap; use safe_math::*; -use substrate_fixed::types::{U64F64, U96F32}; -use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; -use subtensor_swap_interface::SwapHandler; - -// Distribute dividends to each hotkey -macro_rules! asfloat { - ($val:expr) => { - U96F32::saturating_from_num($val) - }; -} - -macro_rules! tou64 { - ($val:expr) => { - $val.saturating_to_num::() - }; -} +use substrate_fixed::types::U96F32; +use subtensor_runtime_common::{AlphaBalance, NetUid, Token}; impl Pallet { - pub fn run_coinbase(block_emission_credit: CreditOf) { - // --- 0. Get current block. - let current_block: u64 = Self::get_current_block_as_u64(); - let block_emission = U96F32::saturating_from_num(block_emission_credit.peek()); - log::debug!( - "Running coinbase for block {current_block:?} with block emission: {block_emission:?}" - ); - - // Reset per-block root sell counters from the previous block. - // Root sells happen after coinbase, so their accumulated values - // are consumed here at the start of the next block. - let _ = SubnetRootSellTao::::clear(u32::MAX, None); - - // --- 1. Get all subnets (excluding root). - let subnets: Vec = Self::get_all_subnet_netuids() - .into_iter() - .filter(|netuid| *netuid != NetUid::ROOT) - .collect(); - log::debug!("All subnets: {subnets:?}"); - - // --- 2. Get subnets to emit to - let subnets_to_emit_to: Vec = Self::get_subnets_to_emit_to(&subnets); - log::debug!("Subnets to emit to: {subnets_to_emit_to:?}"); - - // --- 3. Get emissions for subnets to emit to - let subnet_emissions = - Self::get_subnet_block_emissions(&subnets_to_emit_to, block_emission); - log::debug!("Subnet emissions: {subnet_emissions:?}"); - let root_sell_flag = Self::get_network_root_sell_flag(&subnets_to_emit_to); - log::debug!("Root sell flag: {root_sell_flag:?}"); - - // --- 4. Emit to subnets for this block. - Self::emit_to_subnets( - &subnets_to_emit_to, - &subnet_emissions, - block_emission_credit, - root_sell_flag, - ); - - // --- 5. Drain pending emissions. - let emissions_to_distribute = Self::drain_pending(&subnets, current_block); - - // --- 6. Distribute the emissions to the subnets. - Self::distribute_emissions_to_subnets(&emissions_to_distribute); - } - - pub fn inject_and_maybe_swap( - subnets_to_emit_to: &[NetUid], - tao_in: &BTreeMap, - alpha_in: &BTreeMap, - excess_tao: &BTreeMap, - credit: CreditOf, - ) { - let mut remaining_credit = credit; - for netuid_i in subnets_to_emit_to.iter() { - let maybe_subnet_account_id = Self::get_subnet_account_id(*netuid_i); - if let Some(subnet_account_id) = maybe_subnet_account_id { - let tao_in_i: TaoBalance = - tou64!(*tao_in.get(netuid_i).unwrap_or(&asfloat!(0))).into(); - let alpha_in_i: AlphaBalance = - tou64!(*alpha_in.get(netuid_i).unwrap_or(&asfloat!(0))).into(); - let tao_to_swap_with: TaoBalance = - tou64!(excess_tao.get(netuid_i).unwrap_or(&asfloat!(0))).into(); - - // Clear per-block pool-side emission counters up front so a subnet - // disabled this block does not display stale values from an earlier block. - SubnetExcessTao::::insert(*netuid_i, TaoBalance::ZERO); - SubnetTaoInEmission::::insert(*netuid_i, TaoBalance::ZERO); - - if tao_to_swap_with > TaoBalance::ZERO { - // Turn excess_tao portion of credit into TaoBalance on subnet account - match Self::spend_tao(&subnet_account_id, remaining_credit, tao_to_swap_with) { - Ok(remainder) => { - remaining_credit = remainder; - - let buy_swap_result = Self::swap_tao_for_alpha( - *netuid_i, - tao_to_swap_with, - T::SwapInterface::max_price(), - true, - ); - match buy_swap_result { - Ok(buy_swap_result_ok) => { - let bought_alpha: AlphaBalance = - buy_swap_result_ok.amount_paid_out.into(); - SubnetProtocolAlpha::::mutate(*netuid_i, |total| { - *total = total.saturating_add(bought_alpha); - }); - - // Record actual excess TAO that entered pool. - let actual_excess: TaoBalance = - buy_swap_result_ok.amount_paid_in; - SubnetExcessTao::::insert(*netuid_i, actual_excess); - Self::record_protocol_inflow(*netuid_i, actual_excess); - } - Err(error) => { - match Self::withdraw_tao_as_credit( - &subnet_account_id, - tao_to_swap_with, - ) { - Ok(refund_credit) => { - remaining_credit = - remaining_credit.merge(refund_credit); - } - Err(withdraw_error) => { - log::error!( - "Failed to revert excess TAO deposit after swap failure: netuid_i = {netuid_i:?}, tao_to_swap_with = {tao_to_swap_with:?}, swap_error = {error:?}, withdraw_error = {withdraw_error:?}" - ); - } - } - } - } - } - Err(remainder) => { - remaining_credit = remainder; - let remaining_balance = remaining_credit.peek(); - log::error!( - "Failed to spend credit: tao_to_swap_with = {tao_to_swap_with:?}, netuid_i = {netuid_i:?}, remaining_balance = {remaining_balance:?}" - ); - } - } - } - - // Materialize this block's TAO before updating balancer reservoir - // state. If spending fails, do not let the swap pallet consume - // reservoir state as if this block's TAO arrived. - let materialized_tao_delta = if tao_in_i.is_zero() { - TaoBalance::ZERO - } else { - match Self::spend_tao(&subnet_account_id, remaining_credit, tao_in_i) { - Ok(remainder) => { - remaining_credit = remainder; - tao_in_i - } - Err(remainder) => { - remaining_credit = remainder; - let remaining_balance = remaining_credit.peek(); - log::error!( - "Failed to spend credit: tao_delta = {tao_in_i:?}, netuid_i = {netuid_i:?}, remaining_balance = {remaining_balance:?}" - ); - TaoBalance::ZERO - } - } - }; - - // Decide which current/reservoir liquidity can become price-active - // without pushing balancer weights out of range. Only already - // materialized current TAO is offered to the swap pallet. - let (price_active_tao, price_active_alpha) = - T::SwapInterface::adjust_protocol_liquidity( - *netuid_i, - materialized_tao_delta, - alpha_in_i, - ); - - // Materialize this block's alpha emission, then add only the - // price-active portion to the pool reserve. The price-active - // portion may include alpha that was materialized in an earlier - // block and held in the reservoir. - let _ = Self::mint_alpha(*netuid_i, alpha_in_i); - SubnetAlphaInEmission::::insert(*netuid_i, price_active_alpha); - Self::increase_provided_alpha_reserve(*netuid_i, price_active_alpha); - - // Add only the price-active TAO to the pool reserve. This may - // include TAO materialized in an earlier block and held in the - // reservoir. - if !price_active_tao.is_zero() { - SubnetTaoInEmission::::insert(*netuid_i, price_active_tao); - Self::increase_provided_tao_reserve(*netuid_i, price_active_tao); - TotalStake::::mutate(|total| { - *total = total.saturating_add(price_active_tao); - }); - Self::record_protocol_inflow(*netuid_i, price_active_tao); - } - } - } - - // Remaining imbalance should be zero at this point. If not, log error and burn. - let remaining_balance = remaining_credit.peek(); - if !remaining_balance.is_zero() { - // log::error!("Unspent imbalance remains: remaining_balance = {remaining_balance:?}"); - Self::recycle_credit(remaining_credit); - } - } - - pub fn get_subnet_terms( - subnet_emissions: &BTreeMap, - ) -> ( - BTreeMap, - BTreeMap, - BTreeMap, - BTreeMap, - ) { - // Computation is described in detail in the dtao whitepaper. - let mut tao_in: BTreeMap = BTreeMap::new(); - let mut alpha_in: BTreeMap = BTreeMap::new(); - let mut alpha_out: BTreeMap = BTreeMap::new(); - let mut excess_tao: BTreeMap = BTreeMap::new(); - - // Only calculate for subnets that we are emitting to. - for (&netuid_i, &tao_emission_i) in subnet_emissions.iter() { - // Get alpha_emission this block. - let alpha_emission_i: U96F32 = asfloat!( - Self::get_block_emission_for_issuance(Self::get_alpha_issuance(netuid_i).into()) - .unwrap_or(0) - ); - log::debug!("alpha_emission_i: {alpha_emission_i:?}"); - - // Get subnet price. - let price_i: U96F32 = - U96F32::saturating_from_num(T::SwapInterface::current_alpha_price(netuid_i.into())); - log::debug!("price_i: {price_i:?}"); - - let mut tao_in_i: U96F32 = tao_emission_i; - let alpha_out_i: U96F32 = alpha_emission_i; - let mut alpha_in_i: U96F32 = tao_emission_i.safe_div_or(price_i, U96F32::from_num(0.0)); - - // Cap alpha injection by the subnet's root proportion of its alpha emission. - // root_proportion = tao_weight / (tao_weight + alpha_issuance), so as a subnet - // ages its alpha issuance grows, root_proportion shrinks, and the injection cap - // falls. The TAO emission that can no longer be injected as liquidity becomes - // excess TAO and is routed into chain buys instead. This is what transitions - // older subnets from liquidity injection to chain buys over time. - let root_proportion_i: U96F32 = Self::root_proportion(netuid_i); - let alpha_injection_cap: U96F32 = root_proportion_i.saturating_mul(alpha_emission_i); - if alpha_in_i > alpha_injection_cap { - alpha_in_i = alpha_injection_cap; - tao_in_i = alpha_in_i.saturating_mul(price_i); - } - - let excess_amount: U96F32 = tao_emission_i.saturating_sub(tao_in_i); - excess_tao.insert(netuid_i, excess_amount); - - // Insert values into maps - tao_in.insert(netuid_i, tao_in_i); - alpha_in.insert(netuid_i, alpha_in_i); - alpha_out.insert(netuid_i, alpha_out_i); - } - (tao_in, alpha_in, alpha_out, excess_tao) - } - - pub fn emit_to_subnets( - subnets_to_emit_to: &[NetUid], - subnet_emissions: &BTreeMap, - credit: CreditOf, - root_sell_flag: bool, - ) { - // --- 1. Get subnet terms (tao_in, alpha_in, and alpha_out) - // and excess_tao amounts. - let (tao_in, alpha_in, alpha_out, excess_amount) = Self::get_subnet_terms(subnet_emissions); - - log::debug!("tao_in: {tao_in:?}"); - log::debug!("alpha_in: {alpha_in:?}"); - log::debug!("alpha_out: {alpha_out:?}"); - log::debug!("excess_amount: {excess_amount:?}"); - - // --- 2. Inject TAO and ALPHA to pool and swap with excess TAO. - Self::inject_and_maybe_swap( - subnets_to_emit_to, - &tao_in, - &alpha_in, - &excess_amount, - credit, - ); - - // --- 3. Inject ALPHA for participants. - let cut_percent: U96F32 = Self::get_float_subnet_owner_cut(); - - for netuid_i in subnets_to_emit_to.iter() { - // Get alpha_out for this block. - let mut alpha_out_i: U96F32 = *alpha_out.get(netuid_i).unwrap_or(&asfloat!(0)); - - let alpha_created: AlphaBalance = AlphaBalance::from(tou64!(alpha_out_i)); - SubnetAlphaOutEmission::::insert(*netuid_i, alpha_created); - - // Mint and resolve outstanding alpha - Self::resolve_to_alpha_out(Self::mint_alpha(*netuid_i, alpha_created)); - - // Calculate the owner cut. - if Self::get_owner_cut_enabled(*netuid_i) { - let owner_cut_i: U96F32 = alpha_out_i.saturating_mul(cut_percent); - log::debug!("owner_cut_i: {owner_cut_i:?}"); - // Deduct owner cut from alpha_out. - alpha_out_i = alpha_out_i.saturating_sub(owner_cut_i); - // Accumulate the owner cut in pending. - PendingOwnerCut::::mutate(*netuid_i, |total| { - *total = total.saturating_add(tou64!(owner_cut_i).into()); - }); - } - - // Get root proportional dividends. - let root_proportion = Self::root_proportion(*netuid_i); - log::debug!("root_proportion: {root_proportion:?}"); - - // Get root alpha from root prop. - let root_alpha: U96F32 = root_proportion - .saturating_mul(alpha_out_i) // Total alpha emission per block remaining. - .saturating_mul(asfloat!(0.5)); // 50% to validators. - log::debug!("root_alpha: {root_alpha:?}"); - - // Get pending server alpha, which is the miner cut of the alpha out. - // Currently miner cut is 50% of the alpha out. - let pending_server_alpha = alpha_out_i.saturating_mul(asfloat!(0.5)); - log::debug!("pending_server_alpha: {pending_server_alpha:?}"); - // The total validator alpha is the remaining alpha out minus the server alpha. - let total_validator_alpha = alpha_out_i.saturating_sub(pending_server_alpha); - log::debug!("total_validator_alpha: {total_validator_alpha:?}"); - // The alpha validators don't get the root alpha. - let pending_validator_alpha = total_validator_alpha.saturating_sub(root_alpha); - log::debug!("pending_validator_alpha: {pending_validator_alpha:?}"); - - // Accumulate the server alpha emission. - PendingServerEmission::::mutate(*netuid_i, |total| { - *total = total.saturating_add(tou64!(pending_server_alpha).into()); - }); - // Accumulate the validator alpha emission. - PendingValidatorEmission::::mutate(*netuid_i, |total| { - *total = total.saturating_add(tou64!(pending_validator_alpha).into()); - }); - - if root_sell_flag { - // Only accumulate root alpha divs if root sell is allowed. - PendingRootAlphaDivs::::mutate(*netuid_i, |total| { - *total = total.saturating_add(tou64!(root_alpha).into()); - }); - } else { - // If we are not selling the root alpha, we should recycle it. - Self::recycle_subnet_alpha(*netuid_i, AlphaBalance::from(tou64!(root_alpha))); - } - } - } - - /// Subnets whose epoch slot is due *this* block but is deferred by the per-block - /// cap (`MaxEpochsPerBlock`). - pub fn epochs_deferred_this_block(subnets: &[NetUid], current_block: u64) -> BTreeSet { - let cap = Self::get_max_epochs_per_block() as u32; - let mut deferred: BTreeSet = BTreeSet::new(); - let mut epochs_run_this_block: u32 = 0; - - for &netuid in subnets.iter() { - if !Self::should_run_epoch(netuid, current_block) { - continue; - } - // Per-block cap — due subnets beyond the limit are deferred. - if epochs_run_this_block >= cap { - deferred.insert(netuid); - continue; - } - if Self::is_epoch_input_state_consistent(netuid) { - epochs_run_this_block = epochs_run_this_block.saturating_add(1); - } - } - deferred - } - - pub fn drain_pending( - subnets: &[NetUid], - current_block: u64, - ) -> BTreeMap { - // Map of netuid to (pending_server_alpha, pending_validator_alpha, pending_root_alpha, pending_owner_cut). - let mut emissions_to_distribute: BTreeMap< - NetUid, - (AlphaBalance, AlphaBalance, AlphaBalance, AlphaBalance), - > = BTreeMap::new(); - // Per-block cap on number of epochs that may run; the rest are deferred 1 block forward - // by setting `PendingEpochAt`. - let max_epochs_per_block = Self::get_max_epochs_per_block() as u32; - let mut epochs_run_this_block: u32 = 0; - - for &netuid in subnets.iter() { - // Keep the scheduler age bounded per subnet. `tempo + 1` is enough to - // record that a due epoch missed its slot while avoiding an unbounded - // public counter when the epoch is repeatedly deferred or its input - // state remains inconsistent. - let tempo = Self::get_tempo(netuid); - let max_blocks_since_last_step = u64::from(tempo).saturating_add(1); - BlocksSinceLastStep::::mutate(netuid, |total| { - *total = total.saturating_add(1).min(max_blocks_since_last_step) - }); - - if !Self::should_run_epoch_with_tempo(netuid, current_block, tempo) { - continue; - } - - // Per-block cap — defer if already at limit. - if epochs_run_this_block >= max_epochs_per_block { - let next_block = current_block.saturating_add(1); - PendingEpochAt::::insert(netuid, next_block); - Self::deposit_event(Event::EpochDeferred { - netuid, - from_block: current_block, - to_block: next_block, - }); - continue; - } - - if Self::is_epoch_input_state_consistent(netuid) { - // Reset blocks-since counter; LastMechansimStepBlock is written - // post-distribute (see the caller), so bonds masking can read the - // previous successful run. - BlocksSinceLastStep::::insert(netuid, 0); - - // Get and drain the subnet pending emission. - let pending_server_alpha = PendingServerEmission::::get(netuid); - PendingServerEmission::::insert(netuid, AlphaBalance::ZERO); - - let pending_validator_alpha = PendingValidatorEmission::::get(netuid); - PendingValidatorEmission::::insert(netuid, AlphaBalance::ZERO); - - // Get and drain the pending Alpha for root divs. - let pending_root_alpha = PendingRootAlphaDivs::::get(netuid); - PendingRootAlphaDivs::::insert(netuid, AlphaBalance::ZERO); - - // Get and drain the pending owner cut. - let owner_cut = PendingOwnerCut::::get(netuid); - PendingOwnerCut::::insert(netuid, AlphaBalance::ZERO); - - // Save the emissions to distribute. - emissions_to_distribute.insert( - netuid, - ( - pending_server_alpha, - pending_validator_alpha, - pending_root_alpha, - owner_cut, - ), - ); - epochs_run_this_block = epochs_run_this_block.saturating_add(1); - - // Change subnet owner based on conviction. - Self::change_subnet_owner_if_needed(netuid); - } else { - // Schedule advances below; execution skipped. Pending emissions accumulate - // and will be drained by the next successful epoch. - Self::deposit_event(Event::EpochSkipped { - netuid, - block: current_block, - }); - } - - // Advance the schedule unconditionally — the slot is consumed. - LastEpochBlock::::insert(netuid, current_block); - PendingEpochAt::::insert(netuid, 0); - SubnetEpochIndex::::mutate(netuid, |idx| *idx = idx.saturating_add(1)); - } - emissions_to_distribute - } - - pub fn distribute_emissions_to_subnets( - emissions_to_distribute: &BTreeMap< - NetUid, - (AlphaBalance, AlphaBalance, AlphaBalance, AlphaBalance), - >, - ) { - let current_block = Self::get_current_block_as_u64(); - for ( - &netuid, - &(pending_server_alpha, pending_validator_alpha, pending_root_alpha, pending_owner_cut), - ) in emissions_to_distribute.iter() - { - // Distribute the emission to the subnet. - Self::distribute_emission( - netuid, - pending_server_alpha, - pending_validator_alpha, - pending_root_alpha, - pending_owner_cut, - ); - LastMechansimStepBlock::::insert(netuid, current_block); - } - } - - pub fn get_network_root_sell_flag(subnets_to_emit_to: &[NetUid]) -> bool { - let total_ema_price: U64F64 = subnets_to_emit_to - .iter() - .map(|netuid| Self::get_moving_alpha_price(*netuid)) - .sum(); - - // If the total EMA price is less than or equal to 1 - // then we WILL NOT root sell. - total_ema_price > U64F64::saturating_from_num(1) - } - + /// Fold epoch `(hotkey, incentive, dividend)` rows into per-hotkey incentive totals and + /// parent-aware dividend totals (via [`Pallet::get_parent_child_dividends_distribution`]). pub fn calculate_dividends_and_incentives( netuid: NetUid, hotkey_emission: Vec<(T::AccountId, AlphaBalance, AlphaBalance)>, @@ -527,8 +33,8 @@ impl Pallet { for (parent, parent_div) in div_tuples { dividends .entry(parent) - .and_modify(|e| *e = e.saturating_add(asfloat!(parent_div))) - .or_insert(asfloat!(parent_div)); + .and_modify(|e| *e = e.saturating_add(as_u96f32!(parent_div))) + .or_insert(as_u96f32!(parent_div)); } } log::debug!("incentives: {incentives:?}"); @@ -537,6 +43,8 @@ impl Pallet { (incentives, dividends) } + /// Split each hotkey's dividend into proportional alpha vs root-alpha claimables using + /// subnet stake vs root stake weighted by `tao_weight`. pub fn calculate_dividend_distribution( pending_alpha: AlphaBalance, pending_root_alpha: AlphaBalance, @@ -554,12 +62,12 @@ impl Pallet { log::debug!("tao_weight: {tao_weight:?}"); // Setup. - let zero: U96F32 = asfloat!(0.0); + let zero: U96F32 = as_u96f32!(0.0); // Accumulate root alpha divs and alpha_divs. For each hotkey we compute their // local and root dividend proportion based on their alpha_stake/root_stake - let mut total_root_divs: U96F32 = asfloat!(0); - let mut total_alpha_divs: U96F32 = asfloat!(0); + let mut total_root_divs: U96F32 = as_u96f32!(0); + let mut total_alpha_divs: U96F32 = as_u96f32!(0); let mut root_dividends: BTreeMap = BTreeMap::new(); let mut alpha_dividends: BTreeMap = BTreeMap::new(); for (hotkey, dividend) in dividends { @@ -567,9 +75,9 @@ impl Pallet { let alpha_stake = alpha_stake.to_u64(); let root_stake = root_stake.to_u64(); // Get hotkey ALPHA on subnet. - let alpha_stake = asfloat!(alpha_stake); + let alpha_stake = as_u96f32!(alpha_stake); // Get hotkey TAO on root. - let root_stake = asfloat!(root_stake); + let root_stake = as_u96f32!(root_stake); // Convert TAO to alpha with weight. let root_alpha = root_stake.saturating_mul(tao_weight); @@ -609,7 +117,7 @@ impl Pallet { let root_share: U96F32 = root_divs.checked_div(total_root_divs).unwrap_or(zero); log::debug!("hotkey: {hotkey:?}, root_share: {root_share:?}"); // Root proportion in alpha - let root_alpha: U96F32 = asfloat!(pending_root_alpha).saturating_mul(root_share); + let root_alpha: U96F32 = as_u96f32!(pending_root_alpha).saturating_mul(root_share); log::debug!("hotkey: {hotkey:?}, root_alpha: {root_alpha:?}"); // Record root dividends as TAO. root_alpha_dividends @@ -627,7 +135,7 @@ impl Pallet { log::debug!("hotkey: {hotkey:?}, alpha_share: {alpha_share:?}"); // Compute the proportional pending_alpha to this hotkey. - let prop_alpha = asfloat!(pending_alpha).saturating_mul(alpha_share); + let prop_alpha = as_u96f32!(pending_alpha).saturating_mul(alpha_share); log::debug!("hotkey: {hotkey:?}, prop_alpha: {prop_alpha:?}"); // Record the proportional alpha dividends. prop_alpha_dividends @@ -640,7 +148,9 @@ impl Pallet { (prop_alpha_dividends, root_alpha_dividends) } - fn get_owner_hotkeys(netuid: NetUid, coldkey: &T::AccountId) -> Vec { + /// Hotkeys immune from miner emission on this subnet: SN owner hotkey first, then the + /// coldkey's owned hotkeys ordered by newest registration. + fn owner_immune_hotkeys_on_subnet(netuid: NetUid, coldkey: &T::AccountId) -> Vec { // Gather (block, uid, hotkey) only for hotkeys that have a UID and a registration block. let mut triples: Vec<(u64, u16, T::AccountId)> = OwnedHotkeys::::get(coldkey) .into_iter() @@ -673,6 +183,8 @@ impl Pallet { owner_hotkeys } + /// Pay owner cut, miner incentives (recycle/burn immune keys), and validator alpha / + /// root-alpha dividends, updating per-subnet dividend storage. pub fn distribute_dividends_and_incentives( netuid: NetUid, owner_cut: AlphaBalance, @@ -705,7 +217,7 @@ impl Pallet { // Distribute mining incentives. let subnet_owner_coldkey = SubnetOwner::::get(netuid); - let owner_hotkeys = Self::get_owner_hotkeys(netuid, &subnet_owner_coldkey); + let owner_hotkeys = Self::owner_immune_hotkeys_on_subnet(netuid, &subnet_owner_coldkey); log::debug!("incentives: owner hotkeys: {owner_hotkeys:?}"); // Track total miner emission vs the portion withheld from miners this tempo // (directed to an owner/immune hotkey) to record the withheld proportion. @@ -792,11 +304,11 @@ impl Pallet { let _ = AlphaDividendsPerSubnet::::clear_prefix(netuid, u32::MAX, None); for (hotkey, alpha_divs) in alpha_dividends { let owner: T::AccountId = Owner::::get(&hotkey); - let total: AlphaBalance = tou64!(alpha_divs).into(); + let total: AlphaBalance = to_u64!(alpha_divs).into(); let alpha_take: U96F32 = Self::get_hotkey_take_float(&hotkey).saturating_mul(alpha_divs); let nominator_divs: U96F32 = alpha_divs.saturating_sub(alpha_take); - let take: AlphaBalance = tou64!(alpha_take).into(); + let take: AlphaBalance = to_u64!(alpha_take).into(); let captured = Self::settle_miner_collateral(netuid, &hotkey, &owner, total, take); let liquid_take = take.saturating_sub(captured); if !liquid_take.is_zero() { @@ -808,7 +320,7 @@ impl Pallet { liquid_take, ); } - let nominator_alpha: AlphaBalance = tou64!(nominator_divs).into(); + let nominator_alpha: AlphaBalance = to_u64!(nominator_divs).into(); if !nominator_alpha.is_zero() { log::debug!("hotkey: {hotkey:?} alpha_divs: {nominator_divs:?}"); Self::increase_stake_for_hotkey_on_subnet(&hotkey, netuid, nominator_alpha); @@ -825,11 +337,11 @@ impl Pallet { let _ = RootAlphaDividendsPerSubnet::::clear_prefix(netuid, u32::MAX, None); for (hotkey, root_alpha) in root_alpha_dividends { let owner: T::AccountId = Owner::::get(&hotkey); - let total: AlphaBalance = tou64!(root_alpha).into(); + let total: AlphaBalance = to_u64!(root_alpha).into(); let alpha_take: U96F32 = Self::get_hotkey_take_float(&hotkey).saturating_mul(root_alpha); let root_claimable: U96F32 = root_alpha.saturating_sub(alpha_take); - let take: AlphaBalance = tou64!(alpha_take).into(); + let take: AlphaBalance = to_u64!(alpha_take).into(); let captured = Self::settle_miner_collateral(netuid, &hotkey, &owner, total, take); let liquid_take = take.saturating_sub(captured); if !liquid_take.is_zero() { @@ -842,7 +354,7 @@ impl Pallet { ); } - let root_claimable_alpha: AlphaBalance = tou64!(root_claimable).into(); + let root_claimable_alpha: AlphaBalance = to_u64!(root_claimable).into(); if !root_claimable_alpha.is_zero() { Self::increase_root_claimable_for_hotkey_and_subnet( &hotkey, @@ -857,6 +369,7 @@ impl Pallet { } } + /// Map each hotkey to `(alpha_stake_on_subnet, root_tao_stake)` for dividend weighting. pub fn get_stake_map( netuid: NetUid, hotkeys: Vec<&T::AccountId>, @@ -872,6 +385,7 @@ impl Pallet { stake_map } + /// Run incentive/dividend aggregation then alpha vs root split for one subnet epoch. pub fn calculate_dividend_and_incentive_distribution( netuid: NetUid, pending_root_alpha: AlphaBalance, @@ -901,6 +415,8 @@ impl Pallet { (incentives, (alpha_dividends, root_alpha_dividends)) } + /// Run [`Pallet::epoch_with_mechanisms`] for drained pending alpha and pay out the + /// resulting incentives and dividends. pub fn distribute_emission( netuid: NetUid, pending_server_alpha: AlphaBalance, @@ -1140,72 +656,4 @@ impl Pallet { dividend_tuples } - - /// Checks if the epoch should run for a given subnet based on the current block. - /// - /// # Arguments - /// * `netuid`: The unique identifier of the subnet. - /// - /// # Returns - /// * `bool`: True if the epoch should run, false otherwise. - pub fn should_run_epoch(netuid: NetUid, current_block: u64) -> bool { - let tempo = Self::get_tempo(netuid); - Self::should_run_epoch_with_tempo(netuid, current_block, tempo) - } - - /// Same predicate as `should_run_epoch`, using an already-loaded tempo so - /// callers that also need the tempo do not charge a duplicate storage read. - fn should_run_epoch_with_tempo(netuid: NetUid, current_block: u64, tempo: u16) -> bool { - if tempo == 0 { - return false; - } - let pending = PendingEpochAt::::get(netuid); - if pending > 0 && current_block >= pending { - return true; - } - if BlocksSinceLastStep::::get(netuid) > u64::from(tempo) { - return true; - } - let last = LastEpochBlock::::get(netuid); - let blocks_since = current_block.saturating_sub(last); - blocks_since >= tempo as u64 - } - - /// Returns the number of blocks remaining before the next automatic epoch under the - /// stateful scheduler (period `tempo`, anchored on `LastEpochBlock`). Does NOT account for: - /// - `PendingEpochAt` (owner-triggered manual fire — could happen sooner), - /// - `BlocksSinceLastStep > tempo` safety-net, - /// - per-block-cap defer (could push the actual fire one or more blocks later) - /// Used by the admin-freeze-window predicate and external tooling. Returns `u64::MAX` when - /// `tempo == 0` (legacy defensive short-circuit). - pub fn blocks_until_next_auto_epoch(netuid: NetUid, tempo: u16, block_number: u64) -> u64 { - if tempo == 0 { - return u64::MAX; - } - let last = LastEpochBlock::::get(netuid); - // Period is `tempo`: next firing at `last + tempo`. - let next_auto = last.saturating_add(tempo as u64); - next_auto.saturating_sub(block_number) - } - - /// Returns the absolute block number at which the next epoch is expected to fire for the - /// given subnet, considering both the automatic schedule (`LastEpochBlock + tempo`) and - /// any owner-triggered `PendingEpochAt`. Returns `None` if `tempo == 0` (subnet does not run). - /// Does NOT account for the per-block cap deferral or the `BlocksSinceLastStep > tempo` - /// safety-net (which can fire earlier under extreme drift). - pub fn get_next_epoch_start_block(netuid: NetUid) -> Option { - let tempo = Self::get_tempo(netuid); - if tempo == 0 { - return None; - } - let last = LastEpochBlock::::get(netuid); - let auto_next = last.saturating_add(tempo as u64); - - let pending = PendingEpochAt::::get(netuid); - if pending > 0 { - Some(auto_next.min(pending)) - } else { - Some(auto_next) - } - } } diff --git a/pallets/subtensor/src/coinbase/run_coinbase/drain_pending_emissions.rs b/pallets/subtensor/src/coinbase/run_coinbase/drain_pending_emissions.rs new file mode 100644 index 0000000000..ab860eda12 --- /dev/null +++ b/pallets/subtensor/src/coinbase/run_coinbase/drain_pending_emissions.rs @@ -0,0 +1,222 @@ +//! Epoch-slot drain of pending subnet emissions and tempo scheduling helpers. + +use super::*; +use alloc::collections::{BTreeMap, BTreeSet}; +use subtensor_runtime_common::{AlphaBalance, NetUid}; + +impl Pallet { + /// Subnets whose epoch slot is due *this* block but is deferred by the per-block + /// cap (`MaxEpochsPerBlock`). + pub fn epochs_deferred_this_block(subnets: &[NetUid], current_block: u64) -> BTreeSet { + let cap = Self::get_max_epochs_per_block() as u32; + let mut deferred: BTreeSet = BTreeSet::new(); + let mut epochs_run_this_block: u32 = 0; + + for &netuid in subnets.iter() { + if !Self::should_run_epoch(netuid, current_block) { + continue; + } + // Per-block cap — due subnets beyond the limit are deferred. + if epochs_run_this_block >= cap { + deferred.insert(netuid); + continue; + } + if Self::epoch_keys_have_unique_hotkeys(netuid) { + epochs_run_this_block = epochs_run_this_block.saturating_add(1); + } + } + deferred + } + + /// On each subnet whose epoch is due this block (respecting `MaxEpochsPerBlock`), drain + /// pending server/validator/root/owner alpha into the return map and advance the epoch + /// schedule (`LastEpochBlock`, `SubnetEpochIndex`, clear `PendingEpochAt`). + /// + /// Deferred or inconsistent-input subnets keep accumulating pending emissions. + pub fn drain_pending_subnet_emissions( + subnets: &[NetUid], + current_block: u64, + ) -> BTreeMap { + // Map of netuid to (pending_server_alpha, pending_validator_alpha, pending_root_alpha, pending_owner_cut). + let mut emissions_to_distribute: BTreeMap< + NetUid, + (AlphaBalance, AlphaBalance, AlphaBalance, AlphaBalance), + > = BTreeMap::new(); + // Per-block cap on number of epochs that may run; the rest are deferred 1 block forward + // by setting `PendingEpochAt`. + let max_epochs_per_block = Self::get_max_epochs_per_block() as u32; + let mut epochs_run_this_block: u32 = 0; + + for &netuid in subnets.iter() { + // Keep the scheduler age bounded per subnet. `tempo + 1` is enough to + // record that a due epoch missed its slot while avoiding an unbounded + // public counter when the epoch is repeatedly deferred or its input + // state remains inconsistent. + let tempo = Self::get_tempo(netuid); + let max_blocks_since_last_step = u64::from(tempo).saturating_add(1); + BlocksSinceLastStep::::mutate(netuid, |total| { + *total = total.saturating_add(1).min(max_blocks_since_last_step) + }); + + if !Self::should_run_epoch_given_tempo(netuid, current_block, tempo) { + continue; + } + + // Per-block cap — defer if already at limit. + if epochs_run_this_block >= max_epochs_per_block { + let next_block = current_block.saturating_add(1); + PendingEpochAt::::insert(netuid, next_block); + Self::deposit_event(Event::EpochDeferred { + netuid, + from_block: current_block, + to_block: next_block, + }); + continue; + } + + if Self::epoch_keys_have_unique_hotkeys(netuid) { + // Reset blocks-since counter; LastMechansimStepBlock is written + // post-distribute (see the caller), so bonds masking can read the + // previous successful run. + BlocksSinceLastStep::::insert(netuid, 0); + + // Get and drain the subnet pending emission. + let pending_server_alpha = PendingServerEmission::::get(netuid); + PendingServerEmission::::insert(netuid, AlphaBalance::ZERO); + + let pending_validator_alpha = PendingValidatorEmission::::get(netuid); + PendingValidatorEmission::::insert(netuid, AlphaBalance::ZERO); + + // Get and drain the pending Alpha for root divs. + let pending_root_alpha = PendingRootAlphaDivs::::get(netuid); + PendingRootAlphaDivs::::insert(netuid, AlphaBalance::ZERO); + + // Get and drain the pending owner cut. + let owner_cut = PendingOwnerCut::::get(netuid); + PendingOwnerCut::::insert(netuid, AlphaBalance::ZERO); + + // Save the emissions to distribute. + emissions_to_distribute.insert( + netuid, + ( + pending_server_alpha, + pending_validator_alpha, + pending_root_alpha, + owner_cut, + ), + ); + epochs_run_this_block = epochs_run_this_block.saturating_add(1); + + // Change subnet owner based on conviction. + Self::change_subnet_owner_if_needed(netuid); + } else { + // Schedule advances below; execution skipped. Pending emissions accumulate + // and will be drained by the next successful epoch. + Self::deposit_event(Event::EpochSkipped { + netuid, + block: current_block, + }); + } + + // Advance the schedule unconditionally — the slot is consumed. + LastEpochBlock::::insert(netuid, current_block); + PendingEpochAt::::insert(netuid, 0); + SubnetEpochIndex::::mutate(netuid, |idx| *idx = idx.saturating_add(1)); + } + emissions_to_distribute + } + + /// For each drained subnet, run [`Pallet::distribute_emission`] and stamp + /// [`LastMechansimStepBlock`] (note the historical misspelling of that storage item). + pub fn distribute_emissions_to_subnets( + emissions_to_distribute: &BTreeMap< + NetUid, + (AlphaBalance, AlphaBalance, AlphaBalance, AlphaBalance), + >, + ) { + let current_block = Self::get_current_block_as_u64(); + for ( + &netuid, + &(pending_server_alpha, pending_validator_alpha, pending_root_alpha, pending_owner_cut), + ) in emissions_to_distribute.iter() + { + // Distribute the emission to the subnet. + Self::distribute_emission( + netuid, + pending_server_alpha, + pending_validator_alpha, + pending_root_alpha, + pending_owner_cut, + ); + LastMechansimStepBlock::::insert(netuid, current_block); + } + } + + /// Checks if the epoch should run for a given subnet based on the current block. + /// + /// # Arguments + /// * `netuid`: The unique identifier of the subnet. + /// + /// # Returns + /// * `bool`: True if the epoch should run, false otherwise. + pub fn should_run_epoch(netuid: NetUid, current_block: u64) -> bool { + let tempo = Self::get_tempo(netuid); + Self::should_run_epoch_given_tempo(netuid, current_block, tempo) + } + + /// Same predicate as [`Pallet::should_run_epoch`], using an already-loaded tempo so + /// callers that also need the tempo do not charge a duplicate storage read. + fn should_run_epoch_given_tempo(netuid: NetUid, current_block: u64, tempo: u16) -> bool { + if tempo == 0 { + return false; + } + let pending = PendingEpochAt::::get(netuid); + if pending > 0 && current_block >= pending { + return true; + } + if BlocksSinceLastStep::::get(netuid) > u64::from(tempo) { + return true; + } + let last = LastEpochBlock::::get(netuid); + let blocks_since = current_block.saturating_sub(last); + blocks_since >= tempo as u64 + } + + /// Returns the number of blocks remaining before the next automatic epoch under the + /// stateful scheduler (period `tempo`, anchored on `LastEpochBlock`). Does NOT account for: + /// - `PendingEpochAt` (owner-triggered manual fire — could happen sooner), + /// - `BlocksSinceLastStep > tempo` safety-net, + /// - per-block-cap defer (could push the actual fire one or more blocks later) + /// Used by the admin-freeze-window predicate and external tooling. Returns `u64::MAX` when + /// `tempo == 0` (legacy defensive short-circuit). + pub fn blocks_until_next_auto_epoch(netuid: NetUid, tempo: u16, block_number: u64) -> u64 { + if tempo == 0 { + return u64::MAX; + } + let last = LastEpochBlock::::get(netuid); + // Period is `tempo`: next firing at `last + tempo`. + let next_auto = last.saturating_add(tempo as u64); + next_auto.saturating_sub(block_number) + } + + /// Returns the absolute block number at which the next epoch is expected to fire for the + /// given subnet, considering both the automatic schedule (`LastEpochBlock + tempo`) and + /// any owner-triggered `PendingEpochAt`. Returns `None` if `tempo == 0` (subnet does not run). + /// Does NOT account for the per-block cap deferral or the `BlocksSinceLastStep > tempo` + /// safety-net (which can fire earlier under extreme drift). + pub fn get_next_epoch_start_block(netuid: NetUid) -> Option { + let tempo = Self::get_tempo(netuid); + if tempo == 0 { + return None; + } + let last = LastEpochBlock::::get(netuid); + let auto_next = last.saturating_add(tempo as u64); + + let pending = PendingEpochAt::::get(netuid); + if pending > 0 { + Some(auto_next.min(pending)) + } else { + Some(auto_next) + } + } +} diff --git a/pallets/subtensor/src/coinbase/run_coinbase/emission_injection.rs b/pallets/subtensor/src/coinbase/run_coinbase/emission_injection.rs new file mode 100644 index 0000000000..1ef019739b --- /dev/null +++ b/pallets/subtensor/src/coinbase/run_coinbase/emission_injection.rs @@ -0,0 +1,319 @@ +//! Pool liquidity injection and per-block pending alpha accumulation. + +use super::*; +use super::{as_u96f32, to_u64}; +use crate::coinbase::tao::TaoCreditOf; +use alloc::collections::BTreeMap; +use frame_support::traits::Imbalance; +use safe_math::*; +use substrate_fixed::types::{U64F64, U96F32}; +use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; +use subtensor_swap_interface::SwapHandler; + +impl Pallet { + /// Spend minted TAO credit into each subnet account: swap `excess_tao` for protocol + /// alpha, then materialize `tao_in`/`alpha_in` into price-active pool reserves via the + /// swap pallet's balancer reservoir. + pub fn inject_pool_liquidity_and_swap_excess( + subnets_to_emit_to: &[NetUid], + tao_in: &BTreeMap, + alpha_in: &BTreeMap, + excess_tao: &BTreeMap, + credit: TaoCreditOf, + ) { + let mut remaining_credit = credit; + for netuid_i in subnets_to_emit_to.iter() { + let maybe_subnet_account_id = Self::get_subnet_account_id(*netuid_i); + if let Some(subnet_account_id) = maybe_subnet_account_id { + let tao_in_i: TaoBalance = + to_u64!(*tao_in.get(netuid_i).unwrap_or(&as_u96f32!(0))).into(); + let alpha_in_i: AlphaBalance = + to_u64!(*alpha_in.get(netuid_i).unwrap_or(&as_u96f32!(0))).into(); + let tao_to_swap_with: TaoBalance = + to_u64!(excess_tao.get(netuid_i).unwrap_or(&as_u96f32!(0))).into(); + + // Clear per-block pool-side emission counters up front so a subnet + // disabled this block does not display stale values from an earlier block. + SubnetExcessTao::::insert(*netuid_i, TaoBalance::ZERO); + SubnetTaoInEmission::::insert(*netuid_i, TaoBalance::ZERO); + + if tao_to_swap_with > TaoBalance::ZERO { + // Turn excess_tao portion of credit into TaoBalance on subnet account + match Self::spend_tao(&subnet_account_id, remaining_credit, tao_to_swap_with) { + Ok(remainder) => { + remaining_credit = remainder; + + let buy_swap_result = Self::swap_tao_for_alpha( + *netuid_i, + tao_to_swap_with, + T::SwapInterface::max_price(), + true, + ); + match buy_swap_result { + Ok(buy_swap_result_ok) => { + let bought_alpha: AlphaBalance = + buy_swap_result_ok.amount_paid_out.into(); + SubnetProtocolAlpha::::mutate(*netuid_i, |total| { + *total = total.saturating_add(bought_alpha); + }); + + // Record actual excess TAO that entered pool. + let actual_excess: TaoBalance = + buy_swap_result_ok.amount_paid_in; + SubnetExcessTao::::insert(*netuid_i, actual_excess); + Self::record_protocol_inflow(*netuid_i, actual_excess); + } + Err(error) => { + match Self::withdraw_tao_as_credit( + &subnet_account_id, + tao_to_swap_with, + ) { + Ok(refund_credit) => { + remaining_credit = + remaining_credit.merge(refund_credit); + } + Err(withdraw_error) => { + log::error!( + "Failed to revert excess TAO deposit after swap failure: netuid_i = {netuid_i:?}, tao_to_swap_with = {tao_to_swap_with:?}, swap_error = {error:?}, withdraw_error = {withdraw_error:?}" + ); + } + } + } + } + } + Err(remainder) => { + remaining_credit = remainder; + let remaining_balance = remaining_credit.peek(); + log::error!( + "Failed to spend credit: tao_to_swap_with = {tao_to_swap_with:?}, netuid_i = {netuid_i:?}, remaining_balance = {remaining_balance:?}" + ); + } + } + } + + // Materialize this block's TAO before updating balancer reservoir + // state. If spending fails, do not let the swap pallet consume + // reservoir state as if this block's TAO arrived. + let materialized_tao_delta = if tao_in_i.is_zero() { + TaoBalance::ZERO + } else { + match Self::spend_tao(&subnet_account_id, remaining_credit, tao_in_i) { + Ok(remainder) => { + remaining_credit = remainder; + tao_in_i + } + Err(remainder) => { + remaining_credit = remainder; + let remaining_balance = remaining_credit.peek(); + log::error!( + "Failed to spend credit: tao_delta = {tao_in_i:?}, netuid_i = {netuid_i:?}, remaining_balance = {remaining_balance:?}" + ); + TaoBalance::ZERO + } + } + }; + + // Decide which current/reservoir liquidity can become price-active + // without pushing balancer weights out of range. Only already + // materialized current TAO is offered to the swap pallet. + let (price_active_tao, price_active_alpha) = + T::SwapInterface::adjust_protocol_liquidity( + *netuid_i, + materialized_tao_delta, + alpha_in_i, + ); + + // Materialize this block's alpha emission, then add only the + // price-active portion to the pool reserve. The price-active + // portion may include alpha that was materialized in an earlier + // block and held in the reservoir. + let _ = Self::mint_alpha(*netuid_i, alpha_in_i); + SubnetAlphaInEmission::::insert(*netuid_i, price_active_alpha); + Self::increase_provided_alpha_reserve(*netuid_i, price_active_alpha); + + // Add only the price-active TAO to the pool reserve. This may + // include TAO materialized in an earlier block and held in the + // reservoir. + if !price_active_tao.is_zero() { + SubnetTaoInEmission::::insert(*netuid_i, price_active_tao); + Self::increase_provided_tao_reserve(*netuid_i, price_active_tao); + TotalStake::::mutate(|total| { + *total = total.saturating_add(price_active_tao); + }); + Self::record_protocol_inflow(*netuid_i, price_active_tao); + } + } + } + + // Remaining imbalance should be zero at this point. If not, log error and burn. + let remaining_balance = remaining_credit.peek(); + if !remaining_balance.is_zero() { + // log::error!("Unspent imbalance remains: remaining_balance = {remaining_balance:?}"); + Self::recycle_credit(remaining_credit); + } + } + + /// Split each subnet's TAO emission into `(tao_in, alpha_in, alpha_out, excess_tao)` + /// using spot price and the root-proportion injection cap (dTAO whitepaper). + pub fn compute_subnet_emission_terms( + subnet_emissions: &BTreeMap, + ) -> ( + BTreeMap, + BTreeMap, + BTreeMap, + BTreeMap, + ) { + // Computation is described in detail in the dtao whitepaper. + let mut tao_in: BTreeMap = BTreeMap::new(); + let mut alpha_in: BTreeMap = BTreeMap::new(); + let mut alpha_out: BTreeMap = BTreeMap::new(); + let mut excess_tao: BTreeMap = BTreeMap::new(); + + // Only calculate for subnets that we are emitting to. + for (&netuid_i, &tao_emission_i) in subnet_emissions.iter() { + // Get alpha_emission this block. + let alpha_emission_i: U96F32 = as_u96f32!( + Self::get_block_emission_for_issuance(Self::get_alpha_issuance(netuid_i).into()) + .unwrap_or(0) + ); + log::debug!("alpha_emission_i: {alpha_emission_i:?}"); + + // Get subnet price. + let price_i: U96F32 = + U96F32::saturating_from_num(T::SwapInterface::current_alpha_price(netuid_i.into())); + log::debug!("price_i: {price_i:?}"); + + let mut tao_in_i: U96F32 = tao_emission_i; + let alpha_out_i: U96F32 = alpha_emission_i; + let mut alpha_in_i: U96F32 = tao_emission_i.safe_div_or(price_i, U96F32::from_num(0.0)); + + // Cap alpha injection by the subnet's root proportion of its alpha emission. + // root_proportion = tao_weight / (tao_weight + alpha_issuance), so as a subnet + // ages its alpha issuance grows, root_proportion shrinks, and the injection cap + // falls. The TAO emission that can no longer be injected as liquidity becomes + // excess TAO and is routed into chain buys instead. This is what transitions + // older subnets from liquidity injection to chain buys over time. + let root_proportion_i: U96F32 = Self::root_proportion(netuid_i); + let alpha_injection_cap: U96F32 = root_proportion_i.saturating_mul(alpha_emission_i); + if alpha_in_i > alpha_injection_cap { + alpha_in_i = alpha_injection_cap; + tao_in_i = alpha_in_i.saturating_mul(price_i); + } + + let excess_amount: U96F32 = tao_emission_i.saturating_sub(tao_in_i); + excess_tao.insert(netuid_i, excess_amount); + + // Insert values into maps + tao_in.insert(netuid_i, tao_in_i); + alpha_in.insert(netuid_i, alpha_in_i); + alpha_out.insert(netuid_i, alpha_out_i); + } + (tao_in, alpha_in, alpha_out, excess_tao) + } + + /// Inject pool liquidity for this block, mint outstanding `alpha_out`, take the owner + /// cut, and accumulate pending server/validator/root alpha for later epoch drain. + pub fn emit_to_subnets( + subnets_to_emit_to: &[NetUid], + subnet_emissions: &BTreeMap, + credit: TaoCreditOf, + root_sell_flag: bool, + ) { + // --- 1. Get subnet terms (tao_in, alpha_in, and alpha_out) + // and excess_tao amounts. + let (tao_in, alpha_in, alpha_out, excess_amount) = Self::compute_subnet_emission_terms(subnet_emissions); + + log::debug!("tao_in: {tao_in:?}"); + log::debug!("alpha_in: {alpha_in:?}"); + log::debug!("alpha_out: {alpha_out:?}"); + log::debug!("excess_amount: {excess_amount:?}"); + + // --- 2. Inject TAO and ALPHA to pool and swap with excess TAO. + Self::inject_pool_liquidity_and_swap_excess( + subnets_to_emit_to, + &tao_in, + &alpha_in, + &excess_amount, + credit, + ); + + // --- 3. Inject ALPHA for participants. + let cut_percent: U96F32 = Self::get_float_subnet_owner_cut(); + + for netuid_i in subnets_to_emit_to.iter() { + // Get alpha_out for this block. + let mut alpha_out_i: U96F32 = *alpha_out.get(netuid_i).unwrap_or(&as_u96f32!(0)); + + let alpha_created: AlphaBalance = AlphaBalance::from(to_u64!(alpha_out_i)); + SubnetAlphaOutEmission::::insert(*netuid_i, alpha_created); + + // Mint and resolve outstanding alpha + Self::resolve_to_alpha_out(Self::mint_alpha(*netuid_i, alpha_created)); + + // Calculate the owner cut. + if Self::get_owner_cut_enabled(*netuid_i) { + let owner_cut_i: U96F32 = alpha_out_i.saturating_mul(cut_percent); + log::debug!("owner_cut_i: {owner_cut_i:?}"); + // Deduct owner cut from alpha_out. + alpha_out_i = alpha_out_i.saturating_sub(owner_cut_i); + // Accumulate the owner cut in pending. + PendingOwnerCut::::mutate(*netuid_i, |total| { + *total = total.saturating_add(to_u64!(owner_cut_i).into()); + }); + } + + // Get root proportional dividends. + let root_proportion = Self::root_proportion(*netuid_i); + log::debug!("root_proportion: {root_proportion:?}"); + + // Get root alpha from root prop. + let root_alpha: U96F32 = root_proportion + .saturating_mul(alpha_out_i) // Total alpha emission per block remaining. + .saturating_mul(as_u96f32!(0.5)); // 50% to validators. + log::debug!("root_alpha: {root_alpha:?}"); + + // Get pending server alpha, which is the miner cut of the alpha out. + // Currently miner cut is 50% of the alpha out. + let pending_server_alpha = alpha_out_i.saturating_mul(as_u96f32!(0.5)); + log::debug!("pending_server_alpha: {pending_server_alpha:?}"); + // The total validator alpha is the remaining alpha out minus the server alpha. + let total_validator_alpha = alpha_out_i.saturating_sub(pending_server_alpha); + log::debug!("total_validator_alpha: {total_validator_alpha:?}"); + // The alpha validators don't get the root alpha. + let pending_validator_alpha = total_validator_alpha.saturating_sub(root_alpha); + log::debug!("pending_validator_alpha: {pending_validator_alpha:?}"); + + // Accumulate the server alpha emission. + PendingServerEmission::::mutate(*netuid_i, |total| { + *total = total.saturating_add(to_u64!(pending_server_alpha).into()); + }); + // Accumulate the validator alpha emission. + PendingValidatorEmission::::mutate(*netuid_i, |total| { + *total = total.saturating_add(to_u64!(pending_validator_alpha).into()); + }); + + if root_sell_flag { + // Only accumulate root alpha divs if root sell is allowed. + PendingRootAlphaDivs::::mutate(*netuid_i, |total| { + *total = total.saturating_add(to_u64!(root_alpha).into()); + }); + } else { + // If we are not selling the root alpha, we should recycle it. + Self::recycle_subnet_alpha(*netuid_i, AlphaBalance::from(to_u64!(root_alpha))); + } + } + } + + /// `true` when the sum of emit-eligible subnet EMA alpha prices exceeds 1 — only then + /// are root alpha dividends accumulated (otherwise recycled). + pub fn get_network_root_sell_flag(subnets_to_emit_to: &[NetUid]) -> bool { + let total_ema_price: U64F64 = subnets_to_emit_to + .iter() + .map(|netuid| Self::get_moving_alpha_price(*netuid)) + .sum(); + + // If the total EMA price is less than or equal to 1 + // then we WILL NOT root sell. + total_ema_price > U64F64::saturating_from_num(1) + } +} diff --git a/pallets/subtensor/src/coinbase/run_coinbase/fixed_point.rs b/pallets/subtensor/src/coinbase/run_coinbase/fixed_point.rs new file mode 100644 index 0000000000..3cb4388a22 --- /dev/null +++ b/pallets/subtensor/src/coinbase/run_coinbase/fixed_point.rs @@ -0,0 +1,15 @@ +//! Local fixed-point conversion macros for coinbase emission math. + +macro_rules! as_u96f32 { + ($val:expr) => { + ::substrate_fixed::types::U96F32::saturating_from_num($val) + }; +} +pub(crate) use as_u96f32; + +macro_rules! to_u64 { + ($val:expr) => { + $val.saturating_to_num::() + }; +} +pub(crate) use to_u64; diff --git a/pallets/subtensor/src/coinbase/run_coinbase/mod.rs b/pallets/subtensor/src/coinbase/run_coinbase/mod.rs new file mode 100644 index 0000000000..5878017872 --- /dev/null +++ b/pallets/subtensor/src/coinbase/run_coinbase/mod.rs @@ -0,0 +1,87 @@ +//! Per-block coinbase: mint distribution, pool injection, pending drain, and dividend payout. +//! +//! ## Pipeline (called from [`crate::Pallet::block_step`]) +//! +//! 1. [`Pallet::run_coinbase`] — orchestrates steps 2–5 for the block's minted TAO credit. +//! 2. [`Pallet::get_subnet_block_emissions`] / [`Pallet::emit_to_subnets`] — split TAO by +//! price shares, inject pool liquidity (`tao_in`/`alpha_in`), swap excess TAO, and +//! accumulate pending alpha (server / validator / root / owner cut). +//! 3. [`Pallet::drain_pending_subnet_emissions`] — on each subnet's epoch slot (tempo / trigger / defer), +//! take pending alpha and advance `LastEpochBlock`. +//! 4. [`Pallet::distribute_emissions_to_subnets`] — run consensus epoch and pay incentives +//! / dividends / root claimables. +//! +//! ## Search anchors +//! +//! | Module | Owns | +//! |--------|------| +//! | [`emission_injection`] | `inject_pool_liquidity_and_swap_excess`, `compute_subnet_emission_terms`, `emit_to_subnets` | +//! | [`drain_pending_emissions`] | epoch scheduling + pending drain | +//! | [`dividend_distribution`] | incentive/dividend split and stake payout | + +use super::*; +use crate::coinbase::tao::TaoCreditOf; +use frame_support::traits::Imbalance; +use substrate_fixed::types::U96F32; +use subtensor_runtime_common::NetUid; + +mod dividend_distribution; +mod drain_pending_emissions; +mod emission_injection; +mod fixed_point; + +#[allow(unused_imports)] +pub(crate) use fixed_point::{as_u96f32, to_u64}; + +impl Pallet { + /// Distribute this block's minted TAO credit across eligible subnets, then drain and + /// pay any subnet whose epoch slot fires this block. + /// + /// Resets [`SubnetRootSellTao`] counters at the start (prior block's root sells are + /// consumed here). Unused credit is recycled via [`Pallet::recycle_credit`]. + pub fn run_coinbase(block_emission_credit: TaoCreditOf) { + // --- 0. Get current block. + let current_block: u64 = Self::get_current_block_as_u64(); + let block_emission = U96F32::saturating_from_num(block_emission_credit.peek()); + log::debug!( + "Running coinbase for block {current_block:?} with block emission: {block_emission:?}" + ); + + // Reset per-block root sell counters from the previous block. + // Root sells happen after coinbase, so their accumulated values + // are consumed here at the start of the next block. + let _ = SubnetRootSellTao::::clear(u32::MAX, None); + + // --- 1. Get all subnets (excluding root). + let subnets: Vec = Self::get_all_subnet_netuids() + .into_iter() + .filter(|netuid| *netuid != NetUid::ROOT) + .collect(); + log::debug!("All subnets: {subnets:?}"); + + // --- 2. Get subnets to emit to + let subnets_to_emit_to: Vec = Self::get_subnets_to_emit_to(&subnets); + log::debug!("Subnets to emit to: {subnets_to_emit_to:?}"); + + // --- 3. Get emissions for subnets to emit to + let subnet_emissions = + Self::get_subnet_block_emissions(&subnets_to_emit_to, block_emission); + log::debug!("Subnet emissions: {subnet_emissions:?}"); + let root_sell_flag = Self::get_network_root_sell_flag(&subnets_to_emit_to); + log::debug!("Root sell flag: {root_sell_flag:?}"); + + // --- 4. Emit to subnets for this block. + Self::emit_to_subnets( + &subnets_to_emit_to, + &subnet_emissions, + block_emission_credit, + root_sell_flag, + ); + + // --- 5. Drain pending emissions. + let emissions_to_distribute = Self::drain_pending_subnet_emissions(&subnets, current_block); + + // --- 6. Distribute the emissions to the subnets. + Self::distribute_emissions_to_subnets(&emissions_to_distribute); + } +} diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs index 08f2e0929d..83a4ba56c2 100644 --- a/pallets/subtensor/src/coinbase/subnet_emissions.rs +++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs @@ -1,3 +1,10 @@ +//! Subnet emission eligibility and TAO share allocation across emit-eligible subnets. +//! +//! [`Pallet::get_subnets_to_emit_to`] filters candidates; [`Pallet::subnet_emission_shares`] (price EMA, +//! miner-burn weighted) feeds [`Pallet::get_subnet_block_emissions`]. Flow-based shares +//! ([`Pallet::emission_shares_from_tao_flow`]) remain available but unused while net-flow +//! emission is off the hot path. + use super::*; use alloc::collections::BTreeMap; use safe_math::FixedExt; @@ -15,9 +22,8 @@ impl Pallet { /// started emissions, have subtokens enabled, and currently allow network /// registration. /// - /// AI-readable: This output is passed to `get_shares_flow`, so changing these - /// eligibility rules also changes which subnet user TAO flow EMAs and protocol - /// flow EMAs are advanced during emission sharing. + /// Also gates which subnets advance TAO-flow / protocol-flow EMAs when flow-based + /// shares ([`Pallet::emission_shares_from_tao_flow`]) are used. pub fn get_subnets_to_emit_to(subnets: &[NetUid]) -> Vec { // Filter out root subnet. // Filter out subnets with no first emission block number. @@ -31,13 +37,17 @@ impl Pallet { .collect() } + /// Map each emit-eligible subnet to its TAO share of `block_emission`. + /// + /// Emit-disabled subnets stay in the map at zero TAO (alpha_out path still runs); + /// their share is redistributed across enabled subnets. pub fn get_subnet_block_emissions( subnets_to_emit_to: &[NetUid], block_emission: U96F32, ) -> BTreeMap { // Disabled subnets get zero TAO-side emission, redistributed to enabled subnets. // They stay in the map so the normal alpha_out path still runs. - let shares = Self::get_shares(subnets_to_emit_to); + let shares = Self::subnet_emission_shares(subnets_to_emit_to); log::debug!("Subnet emission shares = {shares:?}"); let zero = U64F64::saturating_from_num(0.0); @@ -74,38 +84,45 @@ impl Pallet { }) .collect::>() } + /// Add user TAO inflow to this block's [`SubnetTaoFlow`] accumulator. pub fn record_tao_inflow(netuid: NetUid, tao: TaoBalance) { SubnetTaoFlow::::mutate(netuid, |flow| { *flow = flow.saturating_add(u64::from(tao) as i64); }); } + /// Subtract user TAO outflow from this block's [`SubnetTaoFlow`] accumulator. pub fn record_tao_outflow(netuid: NetUid, tao: TaoBalance) { SubnetTaoFlow::::mutate(netuid, |flow| { *flow = flow.saturating_sub(u64::from(tao) as i64) }); } + /// Clear [`SubnetTaoFlow`] after an EMA update consumes the block accumulator. pub fn reset_tao_outflow(netuid: NetUid) { SubnetTaoFlow::::remove(netuid); } + /// Add protocol-side TAO inflow to [`SubnetProtocolFlow`] (liquidity injection / buys). pub fn record_protocol_inflow(netuid: NetUid, tao: TaoBalance) { SubnetProtocolFlow::::mutate(netuid, |flow| { *flow = flow.saturating_add(u64::from(tao) as i64); }); } + /// Subtract protocol-side TAO outflow from [`SubnetProtocolFlow`]. pub fn record_protocol_outflow(netuid: NetUid, tao: TaoBalance) { SubnetProtocolFlow::::mutate(netuid, |flow| { *flow = flow.saturating_sub(u64::from(tao) as i64); }); } + /// Clear [`SubnetProtocolFlow`] after a protocol EMA update. pub fn reset_protocol_flow(netuid: NetUid) { SubnetProtocolFlow::::remove(netuid); } + /// Advance [`SubnetEmaProtocolFlow`] once per block from [`SubnetProtocolFlow`], then reset. fn update_ema_protocol_flow(netuid: NetUid) -> I64F64 { let current_block: u64 = Self::get_current_block_as_u64(); @@ -129,10 +146,9 @@ impl Pallet { } } - // Update SubnetEmaTaoFlow if needed and return its value for - // the current block + /// Advance [`SubnetEmaTaoFlow`] once per block from [`SubnetTaoFlow`], then reset the accumulator. #[allow(dead_code)] - fn get_ema_flow(netuid: NetUid) -> I64F64 { + fn update_ema_tao_flow(netuid: NetUid) -> I64F64 { let current_block: u64 = Self::get_current_block_as_u64(); // Calculate net ema flow for the next block @@ -158,11 +174,9 @@ impl Pallet { } } - // 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}}} + /// Lower clip for flow shares: `max(TaoFlowCutoff, min(min(S_i, 0)))`. #[allow(dead_code)] - fn get_lower_limit(ema_flows: &BTreeMap) -> I64F64 { + fn tao_flow_ema_lower_limit(ema_flows: &BTreeMap) -> I64F64 { let zero = I64F64::saturating_from_num(0); let min_flow = ema_flows .values() @@ -173,12 +187,13 @@ impl Pallet { flow_cutoff.max(*min_flow) } - // Estimate the upper value of pow with hardcoded p = 2 - fn pow_estimate(val: U64F64) -> U64F64 { + /// Cheap `val²` upper-bound used when sizing the `safe_pow` scale factor. + fn square_u64f64_estimate(val: U64F64) -> U64F64 { val.saturating_mul(val) } - fn safe_pow(val: U64F64, p: U64F64) -> U64F64 { + /// `val.pow(p)` via `exp(p * ln(val))` in I32F32, returning 0 when `ln` underflows. + fn safe_pow_u64f64(val: U64F64, p: U64F64) -> U64F64 { // If val is too low so that ln(val) doesn't fit I32F32::MIN, // return 0 from the function let zero = U64F64::saturating_from_num(0); @@ -193,7 +208,8 @@ impl Pallet { } } - fn inplace_scale(offset_flows: &mut BTreeMap) { + /// Scale flow values in place so the maximum becomes `1.0`. + fn inplace_scale_flows_max_to_one(offset_flows: &mut BTreeMap) { let zero = U64F64::saturating_from_num(0); let flow_max = offset_flows.values().copied().max().unwrap_or(zero); @@ -206,6 +222,7 @@ impl Pallet { } } + /// Scale then `pow(p)`-normalize flow weights in place (avoids I32F32 overflow). pub(crate) fn inplace_pow_normalize(offset_flows: &mut BTreeMap, p: U64F64) { // Scale offset flows so that that are no overflows and underflows when we use safe_pow: // flow_factor * subnet_count * (flow_max ^ p) <= I32F32::MAX @@ -213,12 +230,12 @@ impl Pallet { let subnet_count = offset_flows.len(); // Pre-scale to max 1.0 - Self::inplace_scale(offset_flows); + Self::inplace_scale_flows_max_to_one(offset_flows); // Scale to maximize precision let flow_max = offset_flows.values().copied().max().unwrap_or(zero); log::debug!("Offset flow max: {flow_max:?}"); - let flow_max_pow_est = Self::pow_estimate(flow_max); + let flow_max_pow_est = Self::square_u64f64_estimate(flow_max); log::debug!("flow_max_pow_est: {flow_max_pow_est:?}"); let max_times_count = @@ -240,27 +257,28 @@ impl Pallet { .clone() .into_values() .map(|flow| flow_factor.saturating_mul(flow)) - .map(|scaled_flow| Self::safe_pow(scaled_flow, p)) + .map(|scaled_flow| Self::safe_pow_u64f64(scaled_flow, p)) .sum(); log::debug!("Scaled offset flow sum: {sum:?}"); // Normalize in-place for flow in offset_flows.values_mut() { let scaled_flow = flow_factor.saturating_mul(*flow); - *flow = Self::safe_pow(scaled_flow, p).safe_div(sum); + *flow = Self::safe_pow_u64f64(scaled_flow, p).safe_div(sum); } } } - // Implementation of shares that uses TAO flow + /// Emission shares from user/protocol TAO-flow EMAs (net-flow mode). Currently unused + /// on the hot path while price-EMA shares are selected in [`Pallet::subnet_emission_shares`]. #[allow(dead_code)] - fn get_shares_flow(subnets_to_emit_to: &[NetUid]) -> BTreeMap { + fn emission_shares_from_tao_flow(subnets_to_emit_to: &[NetUid]) -> BTreeMap { 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). // Note: - // User TAO EMAs are updated every time this method runs because get_ema_flow() + // User TAO EMAs are updated every time this method runs because update_ema_tao_flow() // is called before the NetTaoFlowEnabled branch. Protocol EMAs are different: // update_ema_protocol_flow() is only called while NetTaoFlowEnabled is true. // If net flow is disabled, protocol flow keeps accumulating in SubnetProtocolFlow @@ -269,7 +287,7 @@ impl Pallet { let subnet_emas: Vec<(NetUid, I64F64, I64F64)> = subnets_to_emit_to .iter() .map(|netuid| { - let user_ema = Self::get_ema_flow(*netuid); + let user_ema = Self::update_ema_tao_flow(*netuid); let protocol_ema = Self::update_ema_protocol_flow(*netuid); (*netuid, user_ema, protocol_ema) }) @@ -325,7 +343,7 @@ impl Pallet { // Clip the EMA flow with lower limit L // z[i] = max{S[i] − L, 0} - let lower_limit = Self::get_lower_limit(&ema_flows); + let lower_limit = Self::tao_flow_ema_lower_limit(&ema_flows); log::debug!("Lower flow limit: {lower_limit:?}"); let mut offset_flows = ema_flows .iter() @@ -347,12 +365,11 @@ impl Pallet { offset_flows } - // Price-based emission shares: each subnet's share is its EMA price normalized - // by the sum of EMA prices. Emit-disabled subnets are zeroed and their share - // redistributed to enabled subnets in `get_subnet_block_emissions`, so the - // effective emission is e_i = p_i / sum(p_j) over emit-enabled subnets. - pub(crate) fn get_shares(subnets_to_emit_to: &[NetUid]) -> BTreeMap { - let price_shares = Self::get_shares_price_ema(subnets_to_emit_to); + /// Price-EMA emission shares, reweighted by `(1 - MinerBurned)` and renormalized. + /// + /// Emit-disabled subnets are zeroed later in [`Pallet::get_subnet_block_emissions`]. + pub(crate) fn subnet_emission_shares(subnets_to_emit_to: &[NetUid]) -> BTreeMap { + let price_shares = Self::emission_shares_from_price_ema(subnets_to_emit_to); // Weight each subnet's price share by (1 - miner_burned), then // renormalize. The effective emission is proportional to @@ -388,9 +405,8 @@ impl Pallet { } } - // Implementation of shares that uses subnet EMA prices (SubnetMovingPrice), - // not the active/spot alpha price. - fn get_shares_price_ema(subnets_to_emit_to: &[NetUid]) -> BTreeMap { + /// Normalize each subnet's [`SubnetMovingPrice`] by the sum of EMA prices (not spot). + fn emission_shares_from_price_ema(subnets_to_emit_to: &[NetUid]) -> BTreeMap { // Get sum of alpha moving prices let total_moving_prices = subnets_to_emit_to .iter() diff --git a/pallets/subtensor/src/coinbase/tao.rs b/pallets/subtensor/src/coinbase/tao.rs index d9c7d43590..d18bb89a70 100644 --- a/pallets/subtensor/src/coinbase/tao.rs +++ b/pallets/subtensor/src/coinbase/tao.rs @@ -1,9 +1,13 @@ -/// This file contains all critical operations with TAO and Alpha: -/// -/// - Minting, burning, recycling, and transferring -/// - Reading colkey TAO balances -/// - Access to subnet TAO reserves -/// +//! TAO currency operations for Subtensor: mint, burn, recycle, transfer, and registration locks. +//! +//! Deliberately does **not** treat the subnet account's free balance as the pool reserve — +//! use [`Pallet::get_subnet_tao`] ([`SubnetTAO`]) because the account may also hold locked TAO. +//! +//! Mint workflow for the coinbase: +//! 1. [`Pallet::mint_tao`] in block emission +//! 2. [`Pallet::spend_tao`] while distributing to subnets +//! 3. [`Pallet::recycle_credit`] for any leftover credit +//! use frame_support::traits::{ Imbalance, LockableCurrency, WithdrawReasons, fungible::Mutate, @@ -18,10 +22,12 @@ use subtensor_runtime_common::{NetUid, TaoBalance}; use super::*; -pub type BalanceOf = +/// Currency balance type for Subtensor's TAO (`Config::Currency`). +pub type TaoCurrencyBalanceOf = <::Currency as fungible::Inspect<::AccountId>>::Balance; -pub type CreditOf = Credit<::AccountId, ::Currency>; +/// Fungible credit (imbalance) produced by [`Pallet::mint_tao`] / withdraw paths. +pub type TaoCreditOf = Credit<::AccountId, ::Currency>; pub const MAX_TAO_ISSUANCE: u64 = 21_000_000_000_000_000_u64; @@ -36,13 +42,13 @@ impl Pallet { SubnetTAO::::get(netuid) } - /// Internal function that transfers TAO and allows the origin account to be reaped. - /// - /// Dust collection is handled by the runtime's Balances `DustRemoval` implementation. - fn transfer_allow_death_update_ti( + /// Transfer TAO allowing the origin account to be reaped (existential-deposit dust + /// handled by the runtime Balances `DustRemoval` impl). Does not touch pallet + /// [`TotalIssuance`] — name historically suggested otherwise. + fn transfer_tao_allow_death( origin_coldkey: &T::AccountId, destination_coldkey: &T::AccountId, - amount: BalanceOf, + amount: TaoCurrencyBalanceOf, ) -> DispatchResult { ::Currency::transfer( origin_coldkey, @@ -61,7 +67,7 @@ impl Pallet { pub fn transfer_tao( origin_coldkey: &T::AccountId, destination_coldkey: &T::AccountId, - amount: BalanceOf, + amount: TaoCurrencyBalanceOf, ) -> DispatchResult { // Get full balance including ED let max_transferrable = Self::get_coldkey_balance(origin_coldkey); @@ -70,7 +76,7 @@ impl Pallet { Error::::InsufficientTaoBalance ); - Self::transfer_allow_death_update_ti(origin_coldkey, destination_coldkey, amount) + Self::transfer_tao_allow_death(origin_coldkey, destination_coldkey, amount) } /// Transfer all transferable TAO from `origin_coldkey` to `destination_coldkey`, @@ -96,7 +102,7 @@ impl Pallet { ); if !amount_to_transfer.is_zero() { - Self::transfer_allow_death_update_ti( + Self::transfer_tao_allow_death( origin_coldkey, destination_coldkey, amount_to_transfer, @@ -128,8 +134,8 @@ impl Pallet { pub fn transfer_tao_to_subnet( netuid: NetUid, origin_coldkey: &T::AccountId, - amount: BalanceOf, - ) -> Result, DispatchError> { + amount: TaoCurrencyBalanceOf, + ) -> Result, DispatchError> { if amount.is_zero() { return Ok(0.into()); } @@ -164,25 +170,23 @@ impl Pallet { pub fn transfer_tao_from_subnet( netuid: NetUid, coldkey: &T::AccountId, - amount: BalanceOf, + amount: TaoCurrencyBalanceOf, ) -> DispatchResult { let subnet_account: T::AccountId = Self::get_subnet_account_id(netuid).ok_or(Error::::SubnetNotExists)?; Self::transfer_tao(&subnet_account, coldkey, amount) } - /// Permanently remove TAO amount from existence by moving to the burn - /// address. Does not effect issuance rate - pub fn burn_tao(coldkey: &T::AccountId, amount: BalanceOf) -> DispatchResult { + /// Move TAO to the burn address. Does **not** reduce pallet [`TotalIssuance`]. + pub fn burn_tao(coldkey: &T::AccountId, amount: TaoCurrencyBalanceOf) -> DispatchResult { let burn_address: T::AccountId = T::BurnAccountId::get().into_account_truncating(); Self::transfer_tao(coldkey, &burn_address, amount)?; Ok(()) } - /// Remove TAO from existence and reduce total issuance. - /// Effects issuance rate by reducing TI. - /// Does not allow the account to drop below ED. - pub fn recycle_tao(coldkey: &T::AccountId, amount: BalanceOf) -> DispatchResult { + /// Destroy TAO and reduce pallet [`TotalIssuance`] (affects the emission schedule). + /// Preserves the account existential deposit. + pub fn recycle_tao(coldkey: &T::AccountId, amount: TaoCurrencyBalanceOf) -> DispatchResult { // Ensure that the coldkey doesn't drop below ED let max_preserving_amount = ::Currency::reducible_balance( coldkey, @@ -212,15 +216,16 @@ impl Pallet { Ok(()) } + /// Whether `coldkey` has at least `amount` transferable (expendable) balance. pub fn can_remove_balance_from_coldkey_account( coldkey: &T::AccountId, - amount: BalanceOf, + amount: TaoCurrencyBalanceOf, ) -> bool { amount <= Self::get_coldkey_balance(coldkey) } /// Returns the full coldkey balance including existential deposit - pub fn get_coldkey_balance(coldkey: &T::AccountId) -> BalanceOf { + pub fn get_coldkey_balance(coldkey: &T::AccountId) -> TaoCurrencyBalanceOf { ::Currency::reducible_balance( coldkey, Preservation::Expendable, @@ -228,8 +233,8 @@ impl Pallet { ) } - /// Returns the balance that can be transfered without killing account - pub fn get_keep_alive_balance(coldkey: &T::AccountId) -> BalanceOf { + /// Reducible balance that preserves the account (keep-alive / above ED). + pub fn get_keep_alive_balance(coldkey: &T::AccountId) -> TaoCurrencyBalanceOf { ::Currency::reducible_balance( coldkey, Preservation::Preserve, @@ -237,13 +242,11 @@ impl Pallet { ) } - /// Create TAO and return the imbalance. + /// Issue up to `amount` TAO (hard-capped at [`MAX_TAO_ISSUANCE`]) and bump [`TotalIssuance`]. /// - /// The mint workflow is following: - /// 1. mint_tao in block_emission - /// 2. spend_tao in run_coinbase (distribute to subnets) - /// 3. None should be left, so burn the remainder using burn_credit for records - pub fn mint_tao(amount: BalanceOf) -> CreditOf { + /// Coinbase path: mint here → [`Pallet::spend_tao`] in run_coinbase → [`Pallet::recycle_credit`] + /// for any leftover. + pub fn mint_tao(amount: TaoCurrencyBalanceOf) -> TaoCreditOf { // Hard-limit maximum issuance to 21M TAO. Never issue more. let current_issuance = ::Currency::total_issuance(); @@ -264,9 +267,9 @@ impl Pallet { /// Return the remaining credit or error pub fn spend_tao( coldkey: &T::AccountId, - credit: CreditOf, - part: BalanceOf, - ) -> Result, CreditOf> { + credit: TaoCreditOf, + part: TaoCurrencyBalanceOf, + ) -> Result, TaoCreditOf> { // Reject overspending. if credit.peek() < part { return Err(credit); @@ -286,8 +289,8 @@ impl Pallet { /// changing total issuance. pub fn withdraw_tao_as_credit( coldkey: &T::AccountId, - amount: BalanceOf, - ) -> Result, DispatchError> { + amount: TaoCurrencyBalanceOf, + ) -> Result, DispatchError> { let credit = ::Currency::withdraw( coldkey, amount, @@ -299,8 +302,8 @@ impl Pallet { Ok(credit) } - /// Finalizes the unused part of the minted TAO. - pub fn recycle_credit(credit: CreditOf) { + /// Drop leftover minted credit and subtract it from pallet [`TotalIssuance`]. + pub fn recycle_credit(credit: TaoCreditOf) { let amount = credit.peek(); if !amount.is_zero() { // Some credit is remaining: Decrease subtensor pallet total issuance @@ -315,10 +318,12 @@ impl Pallet { } } + /// Pallet-tracked total TAO issuance ([`TotalIssuance`]), used by the emission curve. pub fn get_total_issuance() -> TaoBalance { TotalIssuance::::get() } + /// 8-byte Balances lock id: `rglk` prefix + little-endian `lock_id`. fn get_network_registration_lock_identifier(lock_id: u32) -> [u8; 8] { let mut id: frame_support::traits::LockIdentifier = [0; 8]; id[..4].copy_from_slice(&TAO_REGISTRATION_LOCK_PREFIX); @@ -326,9 +331,10 @@ impl Pallet { id } + /// Lock `amount` TAO on `coldkey` under the network-registration lock id. pub fn lock_network_registration_cost( coldkey: &T::AccountId, - amount: BalanceOf, + amount: TaoCurrencyBalanceOf, lock_id: u32, ) -> DispatchResult { ensure!( @@ -348,6 +354,7 @@ impl Pallet { Ok(()) } + /// Remove the network-registration Balances lock for `lock_id` on `coldkey`. pub fn unlock_network_registration_cost( coldkey: &T::AccountId, lock_id: u32, diff --git a/pallets/subtensor/src/coinbase/tempo_control.rs b/pallets/subtensor/src/coinbase/tempo_control.rs index 98c0081114..769b552310 100644 --- a/pallets/subtensor/src/coinbase/tempo_control.rs +++ b/pallets/subtensor/src/coinbase/tempo_control.rs @@ -1,3 +1,8 @@ +//! Owner/root dispatch helpers for tempo, activity cutoff, and manual epoch trigger. +//! +//! These implement the bodies behind admin-utils / owner extrinsics (`do_set_tempo`, +//! `do_set_activity_cutoff_factor`, `do_trigger_epoch`). + use super::*; use crate::Error; use frame_support::pallet_prelude::DispatchResult; @@ -16,7 +21,7 @@ impl Pallet { pub fn do_set_tempo(origin: OriginFor, netuid: NetUid, tempo: u16) -> DispatchResult { let maybe_who = Self::ensure_subnet_owner_or_root(origin, netuid)?; - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); if maybe_who.is_some() { ensure!( diff --git a/pallets/subtensor/src/epoch/math.rs b/pallets/subtensor/src/epoch/math.rs deleted file mode 100644 index 6a53c9767b..0000000000 --- a/pallets/subtensor/src/epoch/math.rs +++ /dev/null @@ -1,1594 +0,0 @@ -// we get a compiler warning for this , even though the trait is used in the -// quantile function. -use crate::alloc::borrow::ToOwned; -use safe_math::*; -use sp_runtime::traits::CheckedAdd; - -use sp_std::vec; -use substrate_fixed::transcendental::{exp, ln}; -use substrate_fixed::types::{I32F32, I64F64}; - -use sp_std::vec::Vec; - -pub fn get_safe(slice: &[T], idx: usize) -> T { - slice.get(idx).copied().unwrap_or_default() -} - -pub fn fixed(val: f32) -> I32F32 { - I32F32::saturating_from_num(val) -} - -pub fn fixed_to_u16(x: I32F32) -> u16 { - x.saturating_to_num::() -} - -pub fn fixed_to_u64(x: I32F32) -> u64 { - x.saturating_to_num::() -} - -pub fn fixed64_to_u64(x: I64F64) -> u64 { - x.saturating_to_num::() -} - -pub fn fixed64_to_fixed32(x: I64F64) -> I32F32 { - I32F32::saturating_from_num(x) -} - -pub fn fixed32_to_fixed64(x: I32F32) -> I64F64 { - I64F64::saturating_from_num(x) -} - -pub fn u16_to_fixed(x: u16) -> I32F32 { - I32F32::saturating_from_num(x) -} - -pub fn u16_proportion_to_fixed(x: u16) -> I32F32 { - I32F32::saturating_from_num(x).safe_div(I32F32::saturating_from_num(u16::MAX)) -} - -pub fn fixed_to_fixed_u16_proportion(x: I32F32) -> I32F32 { - x.safe_div(I32F32::saturating_from_num(u16::MAX)) -} - -pub fn fixed_proportion_to_u16(x: I32F32) -> u16 { - fixed_to_u16(x.saturating_mul(I32F32::saturating_from_num(u16::MAX))) -} - -pub fn vec_fixed32_to_u64(vec: Vec) -> Vec { - vec.into_iter().map(fixed_to_u64).collect() -} - -pub fn vec_fixed64_to_fixed32(vec: Vec) -> Vec { - vec.into_iter().map(fixed64_to_fixed32).collect() -} - -pub fn vec_fixed32_to_fixed64(vec: Vec) -> Vec { - vec.into_iter().map(fixed32_to_fixed64).collect() -} - -pub fn vec_fixed64_to_u64(vec: Vec) -> Vec { - vec.into_iter().map(fixed64_to_u64).collect() -} - -pub fn vec_fixed_proportions_to_u16(vec: Vec) -> Vec { - vec.into_iter().map(fixed_proportion_to_u16).collect() -} - -// Max-upscale vector and convert to u16 so max_value = u16::MAX. Assumes non-negative normalized input. -pub fn vec_max_upscale_to_u16(vec: &[I32F32]) -> Vec { - let u16_max: I32F32 = I32F32::saturating_from_num(u16::MAX); - let threshold: I32F32 = I32F32::saturating_from_num(32768); - let max_value: Option<&I32F32> = vec.iter().max(); - match max_value { - Some(val) => { - if *val == I32F32::saturating_from_num(0) { - return vec - .iter() - .map(|e: &I32F32| e.saturating_mul(u16_max).saturating_to_num::()) - .collect(); - } - if *val > threshold { - return vec - .iter() - .map(|e: &I32F32| { - e.saturating_mul(u16_max.safe_div(*val)) - .round() - .saturating_to_num::() - }) - .collect(); - } - vec.iter() - .map(|e: &I32F32| { - e.saturating_mul(u16_max) - .safe_div(*val) - .round() - .saturating_to_num::() - }) - .collect() - } - None => { - let sum: I32F32 = vec.iter().sum(); - vec.iter() - .map(|e: &I32F32| { - e.saturating_mul(u16_max) - .safe_div(sum) - .saturating_to_num::() - }) - .collect() - } - } -} - -// Max-upscale u16 vector and convert to u16 so max_value = u16::MAX. Assumes u16 vector input. -pub fn vec_u16_max_upscale_to_u16(vec: &[u16]) -> Vec { - let vec_fixed: Vec = vec - .iter() - .map(|e: &u16| I32F32::saturating_from_num(*e)) - .collect(); - vec_max_upscale_to_u16(&vec_fixed) -} - -// Checks if u16 vector, when normalized, has a max value not greater than a u16 ratio max_limit. -pub fn check_vec_max_limited(vec: &[u16], max_limit: u16) -> bool { - let max_limit_fixed: I32F32 = - I32F32::saturating_from_num(max_limit).safe_div(I32F32::saturating_from_num(u16::MAX)); - let mut vec_fixed: Vec = vec - .iter() - .map(|e: &u16| I32F32::saturating_from_num(*e)) - .collect(); - inplace_normalize(&mut vec_fixed); - let max_value: Option<&I32F32> = vec_fixed.iter().max(); - max_value.is_none_or(|v| *v <= max_limit_fixed) -} - -pub fn sum(x: &[I32F32]) -> I32F32 { - x.iter().sum() -} - -// Sums a Vector of type that has CheckedAdd trait. -// Returns None if overflow occurs during sum using T::checked_add. -// Returns Some(T::default()) if input vector is empty. -pub fn checked_sum(x: &[T]) -> Option -where - T: Copy + Default + CheckedAdd, -{ - let mut iter = x.iter(); - let Some(mut sum) = iter.next().copied() else { - return Some(T::default()); - }; - for i in iter { - sum = sum.checked_add(i)?; - } - Some(sum) -} - -// Return true when vector sum is zero. -pub fn is_zero(vector: &[I32F32]) -> bool { - let vector_sum: I32F32 = sum(vector); - vector_sum == I32F32::saturating_from_num(0) -} - -// Exp safe function with I32F32 output of I32F32 input. -pub fn exp_safe(input: I32F32) -> I32F32 { - let min_input: I32F32 = I32F32::saturating_from_num(-20); // <= 1/exp(-20) = 485 165 195,4097903 - let max_input: I32F32 = I32F32::saturating_from_num(20); // <= exp(20) = 485 165 195,4097903 - let mut safe_input: I32F32 = input; - if input < min_input { - safe_input = min_input; - } else if max_input < input { - safe_input = max_input; - } - let output: I32F32; - match exp(safe_input) { - Ok(val) => { - output = val; - } - Err(_err) => { - if safe_input <= 0 { - output = I32F32::saturating_from_num(0); - } else { - output = I32F32::max_value(); - } - } - } - output -} - -// Sigmoid safe function with I32F32 output of I32F32 input with offset kappa and (recommended) scaling 0 < rho <= 40. -pub fn sigmoid_safe(input: I32F32, rho: I32F32, kappa: I32F32) -> I32F32 { - let one: I32F32 = I32F32::saturating_from_num(1); - let offset: I32F32 = input.saturating_sub(kappa); // (input - kappa) - let neg_rho: I32F32 = rho.saturating_mul(one.saturating_neg()); // -rho - let exp_input: I32F32 = neg_rho.saturating_mul(offset); // -rho*(input-kappa) - let exp_output: I32F32 = exp_safe(exp_input); // exp(-rho*(input-kappa)) - let denominator: I32F32 = exp_output.saturating_add(one); // 1 + exp(-rho*(input-kappa)) - let sigmoid_output: I32F32 = one.safe_div(denominator); // 1 / (1 + exp(-rho*(input-kappa))) - sigmoid_output -} - -// Returns a bool vector where an item is true if the vector item is in topk values. -pub fn is_topk(vector: &[I32F32], k: usize) -> Vec { - let n: usize = vector.len(); - let mut result: Vec = vec![true; n]; - if n < k { - return result; - } - let mut idxs: Vec = (0..n).collect(); - idxs.sort_by_key(|&idx| get_safe(vector, idx)); // ascending stable sort - for &idx in idxs.iter().take(n.saturating_sub(k)) { - if let Some(cell) = result.get_mut(idx) { - *cell = false; - } - } - result -} - -// Returns a bool vector where an item is true if the vector item is in topk values and is non-zero. -pub fn is_topk_nonzero(vector: &[I32F32], k: usize) -> Vec { - let n: usize = vector.len(); - let mut result: Vec = vector.iter().map(|&elem| elem != I32F32::from(0)).collect(); - if n < k { - return result; - } - let mut idxs: Vec = (0..n).collect(); - idxs.sort_by_key(|&idx| get_safe(vector, idx)); // ascending stable sort - for &idx in idxs.iter().take(n.saturating_sub(k)) { - if let Some(cell) = result.get_mut(idx) { - *cell = false; - } - } - result -} - -// Returns a normalized (sum to 1 except 0) copy of the input vector. -pub fn normalize(x: &[I32F32]) -> Vec { - let x_sum: I32F32 = sum(x); - if x_sum != I32F32::saturating_from_num(0.0_f32) { - x.iter().map(|xi| xi.safe_div(x_sum)).collect() - } else { - x.to_vec() - } -} - -// Normalizes (sum to 1 except 0) the input vector directly in-place. -pub fn inplace_normalize(x: &mut [I32F32]) { - let x_sum: I32F32 = x.iter().sum(); - if x_sum == I32F32::saturating_from_num(0.0_f32) { - return; - } - x.iter_mut() - .for_each(|value| *value = value.safe_div(x_sum)); -} - -// Normalizes (sum to 1 except 0) the input vector directly in-place, using the sum arg. -pub fn inplace_normalize_using_sum(x: &mut [I32F32], x_sum: I32F32) { - if x_sum == I32F32::saturating_from_num(0.0_f32) { - return; - } - x.iter_mut() - .for_each(|value| *value = value.safe_div(x_sum)); -} - -// Normalizes (sum to 1 except 0) the I64F64 input vector directly in-place. -pub fn inplace_normalize_64(x: &mut [I64F64]) { - let x_sum: I64F64 = x.iter().sum(); - if x_sum == I64F64::saturating_from_num(0) { - return; - } - x.iter_mut() - .for_each(|value| *value = value.safe_div(x_sum)); -} - -/// Normalizes (sum to 1 except 0) each row (dim=0) of a I64F64 matrix in-place. -pub fn inplace_row_normalize_64(x: &mut [Vec]) { - for row in x { - let row_sum: I64F64 = row.iter().sum(); - if row_sum > I64F64::saturating_from_num(0.0_f64) { - row.iter_mut() - .for_each(|x_ij: &mut I64F64| *x_ij = x_ij.safe_div(row_sum)); - } - } -} - -/// Returns x / y for input vectors x and y, if y == 0 return 0. -pub fn vecdiv(x: &[I32F32], y: &[I32F32]) -> Vec { - if x.len() != y.len() { - log::error!( - "math error: vecdiv input lengths are not equal: {:?} != {:?}", - x.len(), - y.len() - ); - } - - let zero = I32F32::saturating_from_num(0); - - let mut out = Vec::with_capacity(x.len()); - for (i, x_i) in x.iter().enumerate() { - let y_i = y.get(i).copied().unwrap_or(zero); - out.push(x_i.safe_div(y_i)); - } - out -} - -// Normalizes (sum to 1 except 0) each row (dim=0) of a matrix in-place. -pub fn inplace_row_normalize(x: &mut [Vec]) { - for row in x { - let row_sum: I32F32 = row.iter().sum(); - if row_sum > I32F32::saturating_from_num(0.0_f32) { - row.iter_mut() - .for_each(|x_ij: &mut I32F32| *x_ij = x_ij.safe_div(row_sum)); - } - } -} - -// Normalizes (sum to 1 except 0) each row (dim=0) of a sparse matrix in-place. -pub fn inplace_row_normalize_sparse(sparse_matrix: &mut [Vec<(u16, I32F32)>]) { - for sparse_row in sparse_matrix.iter_mut() { - let row_sum: I32F32 = sparse_row.iter().map(|(_j, value)| *value).sum(); - if row_sum > I32F32::saturating_from_num(0.0) { - sparse_row - .iter_mut() - .for_each(|(_j, value)| *value = value.safe_div(row_sum)); - } - } -} - -// Sum across each row (dim=0) of a matrix. -pub fn row_sum(x: &[Vec]) -> Vec { - if let Some(first_row) = x.first() - && first_row.is_empty() - { - return vec![]; - } - x.iter().map(|row| row.iter().sum()).collect() -} - -// Sum across each row (dim=0) of a sparse matrix. -pub fn row_sum_sparse(sparse_matrix: &[Vec<(u16, I32F32)>]) -> Vec { - sparse_matrix - .iter() - .map(|row| row.iter().map(|(_, value)| value).sum()) - .collect() -} - -// Normalizes (sum to 1 except 0) each column (dim=1) of a sparse matrix in-place. -pub fn inplace_col_normalize_sparse(sparse_matrix: &mut [Vec<(u16, I32F32)>], columns: u16) { - let zero = I32F32::saturating_from_num(0.0); - let mut col_sum: Vec = vec![zero; columns as usize]; - - // Pass 1: accumulate column sums. - for sparse_row in sparse_matrix.iter() { - for &(j, value) in sparse_row.iter() { - if let Some(sum) = col_sum.get_mut(j as usize) { - *sum = sum.saturating_add(value); - } - } - } - - // Pass 2: normalize by column sums where non-zero. - for sparse_row in sparse_matrix.iter_mut() { - for (j, value) in sparse_row.iter_mut() { - let denom = col_sum.get(*j as usize).copied().unwrap_or(zero); - if denom != zero { - *value = value.safe_div(denom); - } - } - } -} - -// Normalizes (sum to 1 except 0) each column (dim=1) of a matrix in-place. -// If a row is shorter/longer than the accumulator, pad with zeroes accordingly. -pub fn inplace_col_normalize(x: &mut [Vec]) { - let zero = I32F32::saturating_from_num(0.0); - - // Build column sums; treat missing entries as zero, but don't modify rows. - let mut col_sums: Vec = Vec::new(); - for row in x.iter() { - if col_sums.len() < row.len() { - col_sums.resize(row.len(), zero); - } - let mut sums_it = col_sums.iter_mut(); - for v in row.iter() { - if let Some(sum) = sums_it.next() { - *sum = sum.saturating_add(*v); - } else { - break; - } - } - } - - if col_sums.is_empty() { - return; - } - - // Normalize only existing elements in each row. - for row in x.iter_mut() { - let mut sums_it = col_sums.iter(); - for m in row.iter_mut() { - if let Some(sum) = sums_it.next() { - if *sum != zero { - *m = m.safe_div(*sum); - } - } else { - break; - } - } - } -} - -// Max-upscale each column (dim=1) of a sparse matrix in-place. -pub fn inplace_col_max_upscale_sparse(sparse_matrix: &mut [Vec<(u16, I32F32)>], columns: u16) { - let zero = I32F32::saturating_from_num(0.0); - let mut col_max: Vec = vec![zero; columns as usize]; - - // Pass 1: compute per-column max - for sparse_row in sparse_matrix.iter() { - for (j, value) in sparse_row.iter() { - if let Some(m) = col_max.get_mut(*j as usize) - && *m < *value - { - *m = *value; - } - } - } - - // Pass 2: divide each nonzero entry by its column max - for sparse_row in sparse_matrix.iter_mut() { - for (j, value) in sparse_row.iter_mut() { - let m = col_max.get(*j as usize).copied().unwrap_or(zero); - if m != zero { - *value = value.safe_div(m); - } - } - } -} - -// Max-upscale each column (dim=1) of a matrix in-place. -pub fn inplace_col_max_upscale(x: &mut [Vec]) { - let zero = I32F32::saturating_from_num(0.0); - - // Find the widest row to size the column-max buffer; don't modify rows. - let max_cols = x.iter().map(|r| r.len()).max().unwrap_or(0); - if max_cols == 0 { - return; - } - - // Pass 1: compute per-column maxima across existing entries only. - let mut col_maxes = vec![zero; max_cols]; - for row in x.iter() { - let mut max_it = col_maxes.iter_mut(); - for v in row.iter() { - if let Some(m) = max_it.next() { - if *m < *v { - *m = *v; - } - } else { - break; - } - } - } - - // Pass 2: divide each existing entry by its column max (if non-zero). - for row in x.iter_mut() { - let mut max_it = col_maxes.iter(); - for val in row.iter_mut() { - if let Some(&m) = max_it.next() { - if m != zero { - *val = val.safe_div(m); - } - } else { - break; - } - } - } -} - -// Apply mask to vector, mask=true will mask out, i.e. set to 0. -pub fn inplace_mask_vector(mask: &[bool], vector: &mut [I32F32]) { - if mask.len() != vector.len() { - log::error!( - "math error: inplace_mask_vector input lengths are not equal: {:?} != {:?}", - mask.len(), - vector.len() - ); - } - - if mask.is_empty() { - return; - } - let zero: I32F32 = I32F32::saturating_from_num(0.0); - for (i, v) in vector.iter_mut().enumerate() { - if *mask.get(i).unwrap_or(&true) { - *v = zero; - } - } -} - -// Apply mask to matrix, mask=true will mask out, i.e. set to 0. -pub fn inplace_mask_matrix(mask: &[Vec], matrix: &mut [Vec]) { - if mask.len() != matrix.len() { - log::error!( - "math error: inplace_mask_matrix input sizes are not equal: {:?} != {:?}", - mask.len(), - matrix.len() - ); - } - let Some(first_row) = mask.first() else { - return; - }; - if first_row.is_empty() { - return; - } - let zero: I32F32 = I32F32::saturating_from_num(0.0); - for (r, row) in matrix.iter_mut().enumerate() { - let mask_row_opt = mask.get(r); - for (c, val) in row.iter_mut().enumerate() { - let should_zero = mask_row_opt - .and_then(|mr| mr.get(c)) - .copied() - .unwrap_or(true); - if should_zero { - *val = zero; - } - } - } -} - -// Apply row mask to matrix, mask=true will mask out, i.e. set to 0. -pub fn inplace_mask_rows(mask: &[bool], matrix: &mut [Vec]) { - if mask.len() != matrix.len() { - log::error!( - "math error: inplace_mask_rows input sizes are not equal: {:?} != {:?}", - mask.len(), - matrix.len() - ); - } - let Some(first_row) = matrix.first() else { - return; - }; - let cols = first_row.len(); - let zero: I32F32 = I32F32::saturating_from_num(0); - for (r, row) in matrix.iter_mut().enumerate() { - if mask.get(r).copied().unwrap_or(true) { - *row = vec![zero; cols]; - } - } -} - -// Apply column mask to matrix, mask=true will mask out, i.e. set to 0. -// Assumes each column has the same length. -pub fn inplace_mask_cols(mask: &[bool], matrix: &mut [Vec]) { - if mask.len() != matrix.len() { - log::error!( - "math error: inplace_mask_cols input sizes are not equal: {:?} != {:?}", - mask.len(), - matrix.len() - ); - } - if matrix.is_empty() { - return; - }; - let zero: I32F32 = I32F32::saturating_from_num(0); - for row in matrix.iter_mut() { - for (c, elem) in row.iter_mut().enumerate() { - if mask.get(c).copied().unwrap_or(true) { - *elem = zero; - } - } - } -} - -// Mask out the diagonal of the input matrix in-place. -pub fn inplace_mask_diag(matrix: &mut [Vec]) { - let Some(first_row) = matrix.first() else { - return; - }; - if first_row.is_empty() { - return; - } - // Weights that we use this function for are always a square matrix. - // If something not square is passed to this function, it's safe to return - // with no action. Log error if this happens. - if matrix.len() != first_row.len() { - log::error!( - "math error: inplace_mask_diag: matrix.len {:?} != first_row.len {:?}", - matrix.len(), - first_row.len() - ); - return; - } - - let zero: I32F32 = I32F32::saturating_from_num(0.0); - matrix.iter_mut().enumerate().for_each(|(idx, row)| { - let Some(elem) = row.get_mut(idx) else { - // Should not happen since matrix is square - return; - }; - *elem = zero; - }); -} - -// Remove cells from sparse matrix where the mask function of a scalar and a vector is true. -pub fn scalar_vec_mask_sparse_matrix( - sparse_matrix: &[Vec<(u16, I32F32)>], - scalar: u64, - vector: &[u64], - mask_fn: &dyn Fn(u64, u64) -> bool, -) -> Vec> { - let mut result: Vec> = Vec::with_capacity(sparse_matrix.len()); - - for row in sparse_matrix.iter() { - let mut out_row: Vec<(u16, I32F32)> = Vec::with_capacity(row.len()); - for &(j, value) in row.iter() { - let vj = vector.get(j as usize).copied().unwrap_or(0); - if !mask_fn(scalar, vj) { - out_row.push((j, value)); - } - } - result.push(out_row); - } - - result -} - -// Mask out the diagonal of the input matrix in-place, except for the diagonal entry at except_index. -pub fn inplace_mask_diag_except_index(matrix: &mut [Vec], except_index: u16) { - let Some(first_row) = matrix.first() else { - return; - }; - if first_row.is_empty() { - return; - } - if matrix.len() != first_row.len() { - log::error!( - "math error: inplace_mask_diag input matrix is now square: {:?} != {:?}", - matrix.len(), - first_row.len() - ); - return; - } - let diag_at_index = matrix - .get(except_index as usize) - .and_then(|row| row.get(except_index as usize)) - .cloned(); - - inplace_mask_diag(matrix); - - matrix.get_mut(except_index as usize).map(|row| { - row.get_mut(except_index as usize).map(|value| { - if let Some(diag_at_index) = diag_at_index { - *value = diag_at_index; - } - }) - }); -} - -// Return a new sparse matrix that replaces masked rows with an empty vector placeholder. -pub fn mask_rows_sparse( - mask: &[bool], - sparse_matrix: &[Vec<(u16, I32F32)>], -) -> Vec> { - let mut out = Vec::with_capacity(sparse_matrix.len()); - for (i, sparse_row) in sparse_matrix.iter().enumerate() { - if mask.get(i).copied().unwrap_or(true) { - out.push(Vec::new()); - } else { - out.push(sparse_row.clone()); - } - } - out -} - -// Return a new sparse matrix with a masked out diagonal of input sparse matrix. -pub fn mask_diag_sparse(sparse_matrix: &[Vec<(u16, I32F32)>]) -> Vec> { - sparse_matrix - .iter() - .enumerate() - .map(|(i, sparse_row)| { - sparse_row - .iter() - .filter(|(j, _)| i != (*j as usize)) - .copied() - .collect() - }) - .collect() -} - -// Return a new sparse matrix with a masked out diagonal of input sparse matrix, -// except for the diagonal entry at except_index. -pub fn mask_diag_sparse_except_index( - sparse_matrix: &[Vec<(u16, I32F32)>], - except_index: u16, -) -> Vec> { - sparse_matrix - .iter() - .enumerate() - .map(|(i, sparse_row)| { - sparse_row - .iter() - .filter(|(j, _)| { - // Is not a diagonal OR is the diagonal at except_index - i != (*j as usize) || (i == except_index as usize && *j == except_index) - }) - .copied() - .collect() - }) - .collect() -} - -// Remove cells from sparse matrix where the mask function of two vectors is true. -pub fn vec_mask_sparse_matrix( - sparse_matrix: &[Vec<(u16, I32F32)>], - first_vector: &[u64], - second_vector: &[u64], - mask_fn: &dyn Fn(u64, u64) -> bool, -) -> Vec> { - let mut result: Vec> = Vec::with_capacity(sparse_matrix.len()); - let mut fv_it = first_vector.iter(); - for row in sparse_matrix.iter() { - let fv = fv_it.next().copied().unwrap_or(0); - let mut out_row: Vec<(u16, I32F32)> = Vec::with_capacity(row.len()); - for &(j, val) in row.iter() { - let sv = second_vector.get(j as usize).copied().unwrap_or(0); - if !mask_fn(fv, sv) { - out_row.push((j, val)); - } - } - result.push(out_row); - } - result -} - -// Row-wise matrix-vector hadamard product. -pub fn row_hadamard(matrix: &[Vec], vector: &[I32F32]) -> Vec> { - let Some(first_row) = matrix.first() else { - return vec![vec![]]; - }; - if first_row.is_empty() { - return vec![vec![]]; - } - - let mut out = Vec::with_capacity(matrix.len()); - let mut vec_it = vector.iter(); - - for row in matrix.iter() { - let Some(&scale) = vec_it.next() else { break }; - let mut new_row = Vec::with_capacity(row.len()); - for m_val in row.iter() { - new_row.push(scale.saturating_mul(*m_val)); - } - out.push(new_row); - } - - out -} - -// Row-wise sparse matrix-vector hadamard product. -pub fn row_hadamard_sparse( - sparse_matrix: &[Vec<(u16, I32F32)>], - vector: &[I32F32], -) -> Vec> { - let mut out = Vec::with_capacity(sparse_matrix.len()); - let mut vec_it = vector.iter(); - - for sparse_row in sparse_matrix.iter() { - let Some(&scale) = vec_it.next() else { break }; - let mut new_row = Vec::with_capacity(sparse_row.len()); - for &(j, val) in sparse_row.iter() { - new_row.push((j, val.saturating_mul(scale))); - } - out.push(new_row); - } - - out -} - -// Row-wise matrix-vector product, column-wise sum: result_j = SUM(i) vector_i * matrix_ij. -pub fn matmul(matrix: &[Vec], vector: &[I32F32]) -> Vec { - let Some(first_row) = matrix.first() else { - return vec![]; - }; - let cols = first_row.len(); - if cols == 0 { - return vec![]; - } - if matrix.len() != vector.len() { - log::error!( - "math error: matmul input sizes are not equal: {:?} != {:?}", - matrix.len(), - vector.len() - ); - } - - let zero = I32F32::saturating_from_num(0.0); - let mut acc = vec![zero; cols]; - - let mut vec_it = vector.iter(); - for row in matrix.iter() { - // Use 0 if the vector ran out (rows beyond vector length contribute nothing). - let scale = vec_it.next().copied().unwrap_or(zero); - - let mut acc_it = acc.iter_mut(); - for m_val in row.iter() { - if let Some(a) = acc_it.next() { - *a = a.saturating_add(scale.saturating_mul(*m_val)); - } else { - // Ignore elements beyond the accumulator width (first row’s length). - break; - } - } - } - - acc -} - -// Column-wise matrix-vector product, row-wise sum: result_i = SUM(j) vector_j * matrix_ij. -pub fn matmul_transpose(matrix: &[Vec], vector: &[I32F32]) -> Vec { - let Some(first_row) = matrix.first() else { - return vec![]; - }; - if first_row.is_empty() { - return vec![]; - } - if vector.len() != first_row.len() { - log::error!( - "math error: matmul_transpose matrix width doesn't match to vector height: {:?} != {:?}", - first_row.len(), - vector.len() - ); - } - - let zero = I32F32::saturating_from_num(0.0); - let mut out = Vec::with_capacity(matrix.len()); - - for row in matrix.iter() { - let mut sum = zero; - let mut v_it = vector.iter(); - for m in row.iter() { - if let Some(&v) = v_it.next() { - sum = sum.saturating_add(m.saturating_mul(v)); - } else { - break; - } - } - out.push(sum); - } - - out -} - -// Row-wise sparse_matrix-vector product, column-wise sum: result_j = SUM(i) vector_i * matrix_ij. -pub fn matmul_sparse( - sparse_matrix: &[Vec<(u16, I32F32)>], - vector: &[I32F32], - columns: u16, -) -> Vec { - let zero = I32F32::saturating_from_num(0.0); - let mut result = vec![zero; columns as usize]; - - let mut vec_it = vector.iter(); - for row in sparse_matrix.iter() { - let scale = vec_it.next().copied().unwrap_or(zero); - for &(j, val) in row.iter() { - if let Some(r) = result.get_mut(j as usize) { - *r = r.saturating_add(scale.saturating_mul(val)); - } - } - } - - result -} - -// Column-wise sparse_matrix-vector product, row-wise sum: result_i = SUM(j) vector_j * matrix_ij. -pub fn matmul_transpose_sparse( - sparse_matrix: &[Vec<(u16, I32F32)>], - vector: &[I32F32], -) -> Vec { - let zero = I32F32::saturating_from_num(0.0); - let mut result = vec![zero; sparse_matrix.len()]; - - let mut out_it = result.iter_mut(); - for row in sparse_matrix.iter() { - let Some(out_cell) = out_it.next() else { break }; - let mut acc = zero; - for &(j, val) in row.iter() { - let v = vector.get(j as usize).copied().unwrap_or(zero); - acc = acc.saturating_add(v.saturating_mul(val)); - } - *out_cell = acc; - } - - result -} - -// Set inplace matrix values above column threshold to threshold value. -pub fn inplace_col_clip(x: &mut [Vec], col_threshold: &[I32F32]) { - for row in x.iter_mut() { - let mut thr_it = col_threshold.iter(); - for value in row.iter_mut() { - if let Some(th) = thr_it.next() { - // Clip: value = min(value, threshold) - *value = *th.min(&*value); - } else { - // No more thresholds; stop for this row. - break; - } - } - } -} - -// Return sparse matrix with values above column threshold set to threshold value. -pub fn col_clip_sparse( - sparse_matrix: &[Vec<(u16, I32F32)>], - col_threshold: &[I32F32], -) -> Vec> { - let zero = I32F32::saturating_from_num(0.0); - let mut result = Vec::with_capacity(sparse_matrix.len()); - - for row in sparse_matrix.iter() { - let mut out_row: Vec<(u16, I32F32)> = Vec::with_capacity(row.len()); - for &(j, val) in row.iter() { - let th = col_threshold.get(j as usize).copied().unwrap_or(zero); - if th < val { - if th > zero { - // clip down to threshold, but drop if threshold <= 0 - out_row.push((j, th)); - } - } else { - // keep original - out_row.push((j, val)); - } - } - result.push(out_row); - } - - result -} - -// Stake-weighted median score finding algorithm, based on a mid pivot binary search. -// Normally a random pivot is used, but to ensure full determinism the mid point is chosen instead. -// Assumes relatively random score order for efficiency, typically less than O(nlogn) complexity. -// -// # Args: -// * 'stake': ( &[I32F32] ): -// - stake, assumed to be normalized. -// -// * 'score': ( &[I32F32] ): -// - score for which median is sought, 0 <= score <= 1 -// -// * 'partition_idx' ( &[usize] ): -// - indices as input partition -// -// * 'minority' ( I32F32 ): -// - minority_ratio = 1 - majority_ratio -// -// * 'partition_lo' ( I32F32 ): -// - lower edge of stake for partition, where partition is a segment [lo, hi] inside stake integral [0, 1]. -// -// * 'partition_hi' ( I32F32 ): -// - higher edge of stake for partition, where partition is a segment [lo, hi] inside stake integral [0, 1]. -// -// # Returns: -// * 'median': ( I32F32 ): -// - median via random pivot binary search. -// -pub fn weighted_median( - stake: &[I32F32], - score: &[I32F32], - partition_idx: &[usize], - minority: I32F32, - mut partition_lo: I32F32, - mut partition_hi: I32F32, -) -> I32F32 { - let zero = I32F32::saturating_from_num(0.0); - if stake.len() != score.len() { - log::error!( - "math error: weighted_median stake and score have different lengths: {:?} != {:?}", - stake.len(), - score.len() - ); - return zero; - } - let mut current_partition_index: Vec = partition_idx.to_vec(); - let mut iteration_counter: usize = 0; - let iteration_limit = partition_idx.len(); - let mut lower: Vec = vec![]; - let mut upper: Vec = vec![]; - - loop { - let n = current_partition_index.len(); - if n == 0 { - return zero; - } - if n == 1 { - if let Some(&only_idx) = current_partition_index.first() { - return get_safe::(score, only_idx); - } else { - return zero; - } - } - let mid_idx: usize = n.safe_div(2); - let pivot: I32F32 = get_safe::( - score, - current_partition_index.get(mid_idx).copied().unwrap_or(0), - ); - let mut lo_stake: I32F32 = I32F32::saturating_from_num(0); - let mut hi_stake: I32F32 = I32F32::saturating_from_num(0); - - for idx in current_partition_index.clone() { - if get_safe::(score, idx) == pivot { - continue; - } - if get_safe::(score, idx) < pivot { - lo_stake = lo_stake.saturating_add(get_safe::(stake, idx)); - lower.push(idx); - } else { - hi_stake = hi_stake.saturating_add(get_safe::(stake, idx)); - upper.push(idx); - } - } - if (minority < partition_lo.saturating_add(lo_stake)) && (!lower.is_empty()) { - current_partition_index = lower.clone(); - partition_hi = partition_lo.saturating_add(lo_stake); - } else if (partition_hi.saturating_sub(hi_stake) <= minority) && (!upper.is_empty()) { - current_partition_index = upper.clone(); - partition_lo = partition_hi.saturating_sub(hi_stake); - } else { - return pivot; - } - - lower.clear(); - upper.clear(); - - // Safety limit: We should never need more than iteration_limit iterations. - iteration_counter = iteration_counter.saturating_add(1); - if iteration_counter > iteration_limit { - break; - } - } - zero -} - -/// Column-wise weighted median, e.g. stake-weighted median scores per server (column) over all validators (rows). -pub fn weighted_median_col( - stake: &[I32F32], - score: &[Vec], - majority: I32F32, -) -> Vec { - let zero = I32F32::saturating_from_num(0.0); - - // Determine number of columns from the first row. - let columns = score.first().map(|r| r.len()).unwrap_or(0); - let mut median = vec![zero; columns]; - - // Iterate columns into `median`. - let mut c = 0usize; - for med_cell in median.iter_mut() { - let mut use_stake: Vec = Vec::new(); - let mut use_score: Vec = Vec::new(); - - // Iterate rows aligned with `stake` length. - let mut r = 0usize; - while r < stake.len() { - let st = get_safe::(stake, r); - if st > zero { - // Fetch row safely; if it's missing or has wrong width, push zeros to both. - if let Some(row) = score.get(r) { - if row.len() == columns { - let val = row.get(c).copied().unwrap_or(zero); - use_stake.push(st); - use_score.push(val); - } else { - use_stake.push(zero); - use_score.push(zero); - log::error!( - "math error: weighted_median_col row.len() != columns: {:?} != {:?}", - row.len(), - columns - ); - } - } else { - // Missing row: insert zeroes. - use_stake.push(zero); - use_score.push(zero); - } - } - r = r.saturating_add(1); - } - - if !use_stake.is_empty() { - inplace_normalize(&mut use_stake); - let stake_sum: I32F32 = use_stake.iter().sum(); - let minority: I32F32 = stake_sum.saturating_sub(majority); - - let idxs: Vec = (0..use_stake.len()).collect(); - *med_cell = weighted_median( - &use_stake, - &use_score, - idxs.as_slice(), - minority, - zero, - stake_sum, - ); - } - - c = c.saturating_add(1); - } - median -} - -/// Column-wise weighted median, e.g. stake-weighted median scores per server (column) over all validators (rows). -pub fn weighted_median_col_sparse( - stake: &[I32F32], - score: &[Vec<(u16, I32F32)>], - columns: u16, - majority: I32F32, -) -> Vec { - let zero = I32F32::saturating_from_num(0.0); - - // Keep only positive-stake rows; normalize them. - let mut use_stake: Vec = stake.iter().copied().filter(|&s| s > zero).collect(); - inplace_normalize(&mut use_stake); - - let stake_sum: I32F32 = use_stake.iter().sum(); - let minority: I32F32 = stake_sum.saturating_sub(majority); - let stake_idx: Vec = (0..use_stake.len()).collect(); - - // use_score: columns x use_stake.len(), prefilled with zeros. - let mut use_score: Vec> = (0..columns as usize) - .map(|_| vec![zero; use_stake.len()]) - .collect(); - - // Fill use_score by walking stake and score together, counting positives with k. - let mut k: usize = 0; - let mut stake_it = stake.iter(); - let mut score_it = score.iter(); - - while let (Some(&s), Some(sparse_row)) = (stake_it.next(), score_it.next()) { - if s > zero { - for &(c, val) in sparse_row.iter() { - if let Some(col_vec) = use_score.get_mut(c as usize) - && let Some(cell) = col_vec.get_mut(k) - { - *cell = val; - } - } - k = k.saturating_add(1); - } - } - - // Compute weighted median per column. - let mut median: Vec = Vec::with_capacity(columns as usize); - for col_vec in use_score.iter() { - median.push(weighted_median( - &use_stake, - col_vec, - stake_idx.as_slice(), - minority, - zero, - stake_sum, - )); - } - - median -} - -// Element-wise interpolation of two matrices: Result = A + ratio * (B - A). -// ratio has intended range [0, 1] -// ratio=0: Result = A -// ratio=1: Result = B -pub fn interpolate(mat1: &[Vec], mat2: &[Vec], ratio: I32F32) -> Vec> { - if ratio == I32F32::saturating_from_num(0.0) { - return mat1.to_owned(); - } - if ratio == I32F32::saturating_from_num(1.0) { - return mat2.to_owned(); - } - if mat1.is_empty() || mat1.first().map(|r| r.is_empty()).unwrap_or(true) { - return vec![vec![]]; - } - if mat1.len() != mat2.len() { - log::error!( - "math error: interpolate mat1.len() != mat2.len(): {:?} != {:?}", - mat1.len(), - mat2.len() - ); - } - - let zero = I32F32::saturating_from_num(0.0); - let cols = mat1.first().map(|r| r.len()).unwrap_or(0); - - // Pre-size result to mat1's shape (row count = mat1.len(), col count = first row of mat1). - let mut result: Vec> = { - let mut out = Vec::with_capacity(mat1.len()); - for _ in mat1.iter() { - out.push(vec![zero; cols]); - } - out - }; - - // Walk rows of mat1, mat2, and result in lockstep; stop when any iterator ends. - let mut m2_it = mat2.iter(); - let mut out_it = result.iter_mut(); - - for row1 in mat1.iter() { - let (Some(row2), Some(out_row)) = (m2_it.next(), out_it.next()) else { - log::error!("math error: interpolate: No more rows in mat2"); - break; - }; - if row1.len() != row2.len() { - log::error!( - "math error: interpolate row1.len() != row2.len(): {:?} != {:?}", - row1.len(), - row2.len() - ); - } - - // Walk elements of row1, row2, and out_row in lockstep; stop at the shortest. - let mut r1_it = row1.iter(); - let mut r2_it = row2.iter(); - let mut out_cell_it = out_row.iter_mut(); - - while let (Some(v1), Some(v2), Some(out_cell)) = - (r1_it.next(), r2_it.next(), out_cell_it.next()) - { - *out_cell = (*v1).saturating_add(ratio.saturating_mul((*v2).saturating_sub(*v1))); - } - // Any remaining cells in `out_row` (beyond min row length) stay as zero (pre-filled). - } - - result -} - -// Element-wise interpolation of two sparse matrices: Result = A + ratio * (B - A). -// ratio has intended range [0, 1] -// ratio=0: Result = A -// ratio=1: Result = B -pub fn interpolate_sparse( - mat1: &[Vec<(u16, I32F32)>], - mat2: &[Vec<(u16, I32F32)>], - columns: u16, - ratio: I32F32, -) -> Vec> { - if ratio == I32F32::saturating_from_num(0) { - return mat1.to_owned(); - } - if ratio == I32F32::saturating_from_num(1) { - return mat2.to_owned(); - } - if mat1.len() != mat2.len() { - // In case if sizes mismatch, return clipped weights - log::error!( - "math error: interpolate_sparse: mat1.len() != mat2.len(): {:?} != {:?}", - mat1.len(), - mat2.len() - ); - return mat2.to_owned(); - } - let rows = mat1.len(); - let zero: I32F32 = I32F32::saturating_from_num(0); - let mut result: Vec> = vec![vec![]; rows]; - for i in 0..rows { - let mut row1: Vec = vec![zero; columns as usize]; - if let Some(row) = mat1.get(i) { - for (j, value) in row { - if let Some(entry) = row1.get_mut(*j as usize) { - *entry = *value; - } - } - } - let mut row2: Vec = vec![zero; columns as usize]; - if let Some(row) = mat2.get(i) { - for (j, value) in row { - if let Some(entry) = row2.get_mut(*j as usize) { - *entry = *value; - } - } - } - for j in 0..columns as usize { - let v1 = row1.get(j).unwrap_or(&zero); - let v2 = row2.get(j).unwrap_or(&zero); - let interp = v1.saturating_add(ratio.saturating_mul(v2.saturating_sub(*v1))); - if zero < interp - && let Some(res) = result.get_mut(i) - { - res.push((j as u16, interp)); - } - } - } - result -} - -// Element-wise product of two vectors. -pub fn vec_mul(a: &[I32F32], b: &[I32F32]) -> Vec { - let mut out = Vec::with_capacity(core::cmp::min(a.len(), b.len())); - let mut ai = a.iter(); - let mut bi = b.iter(); - - while let (Some(x), Some(y)) = (ai.next(), bi.next()) { - out.push(x.checked_mul(*y).unwrap_or_default()); - } - - out -} - -// Element-wise product of matrix and vector -pub fn mat_vec_mul(matrix: &[Vec], vector: &[I32F32]) -> Vec> { - let Some(first_row) = matrix.first() else { - return vec![vec![]]; - }; - if first_row.is_empty() { - return vec![vec![]]; - } - - let mut out = Vec::with_capacity(matrix.len()); - for row in matrix.iter() { - out.push(vec_mul(row, vector)); - } - out -} - -// Element-wise product of matrix and vector -pub fn mat_vec_mul_sparse( - matrix: &[Vec<(u16, I32F32)>], - vector: &[I32F32], -) -> Vec> { - let mut result: Vec> = vec![vec![]; matrix.len()]; - for (i, matrix_row) in matrix.iter().enumerate() { - for (j, value) in matrix_row.iter() { - if let Some(vector_value) = vector.get(*j as usize) { - let new_value = value.saturating_mul(*vector_value); - if new_value != I32F32::saturating_from_num(0.0) - && let Some(result_row) = result.get_mut(i) - { - result_row.push((*j, new_value)); - } - } - } - } - result -} - -/// Clamp the input value between high and low. -/// Note: assumes high > low -pub fn clamp_value(value: I32F32, low: I32F32, high: I32F32) -> I32F32 { - // First, clamp the value to ensure it does not exceed the upper bound (high). - // If the value is greater than 'high', it will be set to 'high'. - // otherwise it remains unchanged. - value - .min(I32F32::from_num(high)) - // Next, clamp the value to ensure it does not go below the lower bound (_low). - // If the value (after the first clamping) is less than 'low', it will be set to 'low'. - // otherwise it remains unchanged. - .max(I32F32::from_num(low)) -} - -// Return matrix exponential moving average: `alpha * a_ij + one_minus_alpha * b_ij`. -// `alpha` is the EMA coefficient, how much to add of the new observation, typically small, -// higher alpha discounts older observations faster. -pub fn mat_ema(new: &[Vec], old: &[Vec], alpha: I32F32) -> Vec> { - let Some(first_row) = new.first() else { - return vec![vec![]]; - }; - if first_row.is_empty() { - return vec![vec![]; 1]; - } - - let one_minus_alpha = I32F32::saturating_from_num(1.0).saturating_sub(alpha); - - let mut out = Vec::with_capacity(new.len()); - let mut old_it = old.iter(); - - for new_row in new.iter() { - let Some(old_row) = old_it.next() else { break }; - - let mut row_out = Vec::with_capacity(core::cmp::min(new_row.len(), old_row.len())); - let mut n_it = new_row.iter(); - let mut o_it = old_row.iter(); - - while let (Some(&n), Some(&o)) = (n_it.next(), o_it.next()) { - row_out.push( - alpha - .saturating_mul(n) - .saturating_add(one_minus_alpha.saturating_mul(o)), - ); - } - - out.push(row_out); - } - - out -} - -// Return sparse matrix exponential moving average: `alpha * a_ij + one_minus_alpha * b_ij`. -// `alpha` is the EMA coefficient, how much to add of the new observation, typically small, -// higher alpha discounts older observations faster. -pub fn mat_ema_sparse( - new: &[Vec<(u16, I32F32)>], - old: &[Vec<(u16, I32F32)>], - alpha: I32F32, -) -> Vec> { - if new.len() != old.len() { - log::error!( - "math error: mat_ema_sparse: new.len() == old.len(): {:?} != {:?}", - new.len(), - old.len() - ); - } - - let zero = I32F32::saturating_from_num(0.0); - let one_minus_alpha = I32F32::saturating_from_num(1.0).saturating_sub(alpha); - - let n = new.len(); // assume square (rows = cols) - if n == 0 { - return Vec::new(); - } - - let mut result: Vec> = Vec::with_capacity(n); - let mut old_it = old.iter(); - - for new_row in new.iter() { - let mut acc_row = vec![zero; n]; - - // Add alpha * new - for &(j, v) in new_row.iter() { - if let Some(cell) = acc_row.get_mut(j as usize) { - *cell = cell.saturating_add(alpha.saturating_mul(v)); - } - } - - // Add (1 - alpha) * old - if let Some(orow) = old_it.next() { - for &(j, v) in orow.iter() { - if let Some(cell) = acc_row.get_mut(j as usize) { - *cell = cell.saturating_add(one_minus_alpha.saturating_mul(v)); - } - } - } - - // Densified row -> sparse (keep positives) - let mut out_row: Vec<(u16, I32F32)> = Vec::new(); - for (j, &val) in acc_row.iter().enumerate() { - if val > zero { - out_row.push((j as u16, val)); - } - } - - result.push(out_row); - } - - result -} - -/// Calculates the exponential moving average (EMA) for a sparse matrix using dynamic alpha values. -pub fn mat_ema_alpha_sparse( - new: &[Vec<(u16, I32F32)>], - old: &[Vec<(u16, I32F32)>], - alpha: &[Vec], -) -> Vec> { - // If shapes don't match, just return `new` - if new.len() != old.len() || new.len() != alpha.len() { - log::error!( - "math error: mat_ema_alpha_sparse shapes don't match: {:?} vs. {:?} vs. {:?}", - old.len(), - new.len(), - alpha.len() - ); - return new.to_owned(); - } - - let zero = I32F32::saturating_from_num(0.0); - let one = I32F32::saturating_from_num(1.0); - - let mut result: Vec> = Vec::with_capacity(new.len()); - let mut old_it = old.iter(); - let mut alf_it = alpha.iter(); - - for new_row in new.iter() { - let Some(old_row) = old_it.next() else { break }; - let Some(alpha_row) = alf_it.next() else { - break; - }; - - // Densified accumulator sized to alpha_row length (columns outside are ignored). - let mut decayed_values = vec![zero; alpha_row.len()]; - - // Apply (1 - alpha_j) * old_ij into accumulator. - for &(j, old_val) in old_row.iter() { - if let (Some(&a), Some(cell)) = ( - alpha_row.get(j as usize), - decayed_values.get_mut(j as usize), - ) { - *cell = one.saturating_sub(a).saturating_mul(old_val); - } - } - - // Add alpha_j * new_ij, clamp to [0, 1], and emit sparse entries > 0. - let mut out_row: Vec<(u16, I32F32)> = Vec::new(); - for &(j, new_val) in new_row.iter() { - if let (Some(&a), Some(&decayed)) = - (alpha_row.get(j as usize), decayed_values.get(j as usize)) - { - let inc = a.saturating_mul(new_val).max(zero); - let val = decayed.saturating_add(inc).min(one); - if val > zero { - out_row.push((j, val)); - } - } - } - - result.push(out_row); - } - - result -} - -/// Calculates the exponential moving average (EMA) for a dense matrix using dynamic alpha values. -pub fn mat_ema_alpha( - new: &[Vec], // Weights - old: &[Vec], // Bonds - alpha: &[Vec], -) -> Vec> { - // Empty or degenerate input - if new.is_empty() || new.first().map(|r| r.is_empty()).unwrap_or(true) { - return vec![vec![]]; - } - - // If outer dimensions don't match, return bonds unchanged - if new.len() != old.len() || new.len() != alpha.len() { - log::error!( - "math error: mat_ema_alpha shapes don't match: {:?} vs. {:?} vs. {:?}", - old.len(), - new.len(), - alpha.len() - ); - return old.to_owned(); - } - - // Ensure each corresponding row has matching length; otherwise return `new` unchanged. - let mut old_it = old.iter(); - let mut alp_it = alpha.iter(); - for nrow in new.iter() { - let (Some(orow), Some(arow)) = (old_it.next(), alp_it.next()) else { - return new.to_owned(); - }; - if nrow.len() != orow.len() || nrow.len() != arow.len() { - return new.to_owned(); - } - } - - let zero = I32F32::saturating_from_num(0.0); - let one = I32F32::saturating_from_num(1.0); - - // Compute EMA: result = (1 - α) * old + α * new, clamped to [0, 1]. - let mut out: Vec> = Vec::with_capacity(new.len()); - let mut old_it = old.iter(); - let mut alp_it = alpha.iter(); - - for nrow in new.iter() { - let (Some(orow), Some(arow)) = (old_it.next(), alp_it.next()) else { - break; - }; - - let mut r: Vec = Vec::with_capacity(nrow.len()); - let mut n_it = nrow.iter(); - let mut o_it = orow.iter(); - let mut a_it = arow.iter(); - - while let (Some(&n), Some(&o), Some(&a)) = (n_it.next(), o_it.next(), a_it.next()) { - let one_minus_a = one.saturating_sub(a); - let decayed = one_minus_a.saturating_mul(o); - let inc = a.saturating_mul(n).max(zero); - r.push(decayed.saturating_add(inc).min(one)); - } - - out.push(r); - } - - out -} - -/// Safe ln function, returns 0 if value is 0. -pub fn safe_ln(value: I32F32) -> I32F32 { - ln(value).unwrap_or(I32F32::saturating_from_num(0.0)) -} diff --git a/pallets/subtensor/src/epoch/math/ema_interpolate.rs b/pallets/subtensor/src/epoch/math/ema_interpolate.rs new file mode 100644 index 0000000000..c044ea7ed3 --- /dev/null +++ b/pallets/subtensor/src/epoch/math/ema_interpolate.rs @@ -0,0 +1,422 @@ +//! Bonds EMA (fixed and per-edge alpha), matrix interpolate, and clamp/ln helpers. + +use crate::alloc::borrow::ToOwned; +use sp_std::vec; +use sp_std::vec::Vec; +use substrate_fixed::transcendental::ln; +use substrate_fixed::types::I32F32; + +pub fn interpolate(mat1: &[Vec], mat2: &[Vec], ratio: I32F32) -> Vec> { + if ratio == I32F32::saturating_from_num(0.0) { + return mat1.to_owned(); + } + if ratio == I32F32::saturating_from_num(1.0) { + return mat2.to_owned(); + } + if mat1.is_empty() || mat1.first().map(|r| r.is_empty()).unwrap_or(true) { + return vec![vec![]]; + } + if mat1.len() != mat2.len() { + log::error!( + "math error: interpolate mat1.len() != mat2.len(): {:?} != {:?}", + mat1.len(), + mat2.len() + ); + } + + let zero = I32F32::saturating_from_num(0.0); + let cols = mat1.first().map(|r| r.len()).unwrap_or(0); + + // Pre-size result to mat1's shape (row count = mat1.len(), col count = first row of mat1). + let mut result: Vec> = { + let mut out = Vec::with_capacity(mat1.len()); + for _ in mat1.iter() { + out.push(vec![zero; cols]); + } + out + }; + + // Walk rows of mat1, mat2, and result in lockstep; stop when any iterator ends. + let mut m2_it = mat2.iter(); + let mut out_it = result.iter_mut(); + + for row1 in mat1.iter() { + let (Some(row2), Some(out_row)) = (m2_it.next(), out_it.next()) else { + log::error!("math error: interpolate: No more rows in mat2"); + break; + }; + if row1.len() != row2.len() { + log::error!( + "math error: interpolate row1.len() != row2.len(): {:?} != {:?}", + row1.len(), + row2.len() + ); + } + + // Walk elements of row1, row2, and out_row in lockstep; stop at the shortest. + let mut r1_it = row1.iter(); + let mut r2_it = row2.iter(); + let mut out_cell_it = out_row.iter_mut(); + + while let (Some(v1), Some(v2), Some(out_cell)) = + (r1_it.next(), r2_it.next(), out_cell_it.next()) + { + *out_cell = (*v1).saturating_add(ratio.saturating_mul((*v2).saturating_sub(*v1))); + } + // Any remaining cells in `out_row` (beyond min row length) stay as zero (pre-filled). + } + + result +} + +// Element-wise interpolation of two sparse matrices: Result = A + ratio * (B - A). +// ratio has intended range [0, 1] +// ratio=0: Result = A +// ratio=1: Result = B +pub fn interpolate_sparse( + mat1: &[Vec<(u16, I32F32)>], + mat2: &[Vec<(u16, I32F32)>], + columns: u16, + ratio: I32F32, +) -> Vec> { + if ratio == I32F32::saturating_from_num(0) { + return mat1.to_owned(); + } + if ratio == I32F32::saturating_from_num(1) { + return mat2.to_owned(); + } + if mat1.len() != mat2.len() { + // In case if sizes mismatch, return clipped weights + log::error!( + "math error: interpolate_sparse: mat1.len() != mat2.len(): {:?} != {:?}", + mat1.len(), + mat2.len() + ); + return mat2.to_owned(); + } + let rows = mat1.len(); + let zero: I32F32 = I32F32::saturating_from_num(0); + let mut result: Vec> = vec![vec![]; rows]; + for i in 0..rows { + let mut row1: Vec = vec![zero; columns as usize]; + if let Some(row) = mat1.get(i) { + for (j, value) in row { + if let Some(entry) = row1.get_mut(*j as usize) { + *entry = *value; + } + } + } + let mut row2: Vec = vec![zero; columns as usize]; + if let Some(row) = mat2.get(i) { + for (j, value) in row { + if let Some(entry) = row2.get_mut(*j as usize) { + *entry = *value; + } + } + } + for j in 0..columns as usize { + let v1 = row1.get(j).unwrap_or(&zero); + let v2 = row2.get(j).unwrap_or(&zero); + let interp = v1.saturating_add(ratio.saturating_mul(v2.saturating_sub(*v1))); + if zero < interp + && let Some(res) = result.get_mut(i) + { + res.push((j as u16, interp)); + } + } + } + result +} + +// Element-wise product of two vectors. +pub fn vec_mul(a: &[I32F32], b: &[I32F32]) -> Vec { + let mut out = Vec::with_capacity(core::cmp::min(a.len(), b.len())); + let mut ai = a.iter(); + let mut bi = b.iter(); + + while let (Some(x), Some(y)) = (ai.next(), bi.next()) { + out.push(x.checked_mul(*y).unwrap_or_default()); + } + + out +} + +// Element-wise product of matrix and vector +pub fn mat_vec_mul(matrix: &[Vec], vector: &[I32F32]) -> Vec> { + let Some(first_row) = matrix.first() else { + return vec![vec![]]; + }; + if first_row.is_empty() { + return vec![vec![]]; + } + + let mut out = Vec::with_capacity(matrix.len()); + for row in matrix.iter() { + out.push(vec_mul(row, vector)); + } + out +} + +// Element-wise product of matrix and vector +pub fn mat_vec_mul_sparse( + matrix: &[Vec<(u16, I32F32)>], + vector: &[I32F32], +) -> Vec> { + let mut result: Vec> = vec![vec![]; matrix.len()]; + for (i, matrix_row) in matrix.iter().enumerate() { + for (j, value) in matrix_row.iter() { + if let Some(vector_value) = vector.get(*j as usize) { + let new_value = value.saturating_mul(*vector_value); + if new_value != I32F32::saturating_from_num(0.0) + && let Some(result_row) = result.get_mut(i) + { + result_row.push((*j, new_value)); + } + } + } + } + result +} + +/// Clamp `value` into `[low, high]` (assumes `high > low`). +pub fn clamp_i32f32(value: I32F32, low: I32F32, high: I32F32) -> I32F32 { + // First, clamp the value to ensure it does not exceed the upper bound (high). + // If the value is greater than 'high', it will be set to 'high'. + // otherwise it remains unchanged. + value + .min(I32F32::from_num(high)) + // Next, clamp the value to ensure it does not go below the lower bound (_low). + // If the value (after the first clamping) is less than 'low', it will be set to 'low'. + // otherwise it remains unchanged. + .max(I32F32::from_num(low)) +} + +// Return matrix exponential moving average: `alpha * a_ij + one_minus_alpha * b_ij`. +// `alpha` is the EMA coefficient, how much to add of the new observation, typically small, +// higher alpha discounts older observations faster. +pub fn mat_ema(new: &[Vec], old: &[Vec], alpha: I32F32) -> Vec> { + let Some(first_row) = new.first() else { + return vec![vec![]]; + }; + if first_row.is_empty() { + return vec![vec![]; 1]; + } + + let one_minus_alpha = I32F32::saturating_from_num(1.0).saturating_sub(alpha); + + let mut out = Vec::with_capacity(new.len()); + let mut old_it = old.iter(); + + for new_row in new.iter() { + let Some(old_row) = old_it.next() else { break }; + + let mut row_out = Vec::with_capacity(core::cmp::min(new_row.len(), old_row.len())); + let mut n_it = new_row.iter(); + let mut o_it = old_row.iter(); + + while let (Some(&n), Some(&o)) = (n_it.next(), o_it.next()) { + row_out.push( + alpha + .saturating_mul(n) + .saturating_add(one_minus_alpha.saturating_mul(o)), + ); + } + + out.push(row_out); + } + + out +} + +// Return sparse matrix exponential moving average: `alpha * a_ij + one_minus_alpha * b_ij`. +// `alpha` is the EMA coefficient, how much to add of the new observation, typically small, +// higher alpha discounts older observations faster. +pub fn mat_ema_sparse( + new: &[Vec<(u16, I32F32)>], + old: &[Vec<(u16, I32F32)>], + alpha: I32F32, +) -> Vec> { + if new.len() != old.len() { + log::error!( + "math error: mat_ema_sparse: new.len() == old.len(): {:?} != {:?}", + new.len(), + old.len() + ); + } + + let zero = I32F32::saturating_from_num(0.0); + let one_minus_alpha = I32F32::saturating_from_num(1.0).saturating_sub(alpha); + + let n = new.len(); // assume square (rows = cols) + if n == 0 { + return Vec::new(); + } + + let mut result: Vec> = Vec::with_capacity(n); + let mut old_it = old.iter(); + + for new_row in new.iter() { + let mut acc_row = vec![zero; n]; + + // Add alpha * new + for &(j, v) in new_row.iter() { + if let Some(cell) = acc_row.get_mut(j as usize) { + *cell = cell.saturating_add(alpha.saturating_mul(v)); + } + } + + // Add (1 - alpha) * old + if let Some(orow) = old_it.next() { + for &(j, v) in orow.iter() { + if let Some(cell) = acc_row.get_mut(j as usize) { + *cell = cell.saturating_add(one_minus_alpha.saturating_mul(v)); + } + } + } + + // Densified row -> sparse (keep positives) + let mut out_row: Vec<(u16, I32F32)> = Vec::new(); + for (j, &val) in acc_row.iter().enumerate() { + if val > zero { + out_row.push((j as u16, val)); + } + } + + result.push(out_row); + } + + result +} + +/// Calculates the exponential moving average (EMA) for a sparse matrix using dynamic alpha values. +pub fn mat_ema_alpha_sparse( + new: &[Vec<(u16, I32F32)>], + old: &[Vec<(u16, I32F32)>], + alpha: &[Vec], +) -> Vec> { + // If shapes don't match, just return `new` + if new.len() != old.len() || new.len() != alpha.len() { + log::error!( + "math error: mat_ema_alpha_sparse shapes don't match: {:?} vs. {:?} vs. {:?}", + old.len(), + new.len(), + alpha.len() + ); + return new.to_owned(); + } + + let zero = I32F32::saturating_from_num(0.0); + let one = I32F32::saturating_from_num(1.0); + + let mut result: Vec> = Vec::with_capacity(new.len()); + let mut old_it = old.iter(); + let mut alf_it = alpha.iter(); + + for new_row in new.iter() { + let Some(old_row) = old_it.next() else { break }; + let Some(alpha_row) = alf_it.next() else { + break; + }; + + // Densified accumulator sized to alpha_row length (columns outside are ignored). + let mut decayed_values = vec![zero; alpha_row.len()]; + + // Apply (1 - alpha_j) * old_ij into accumulator. + for &(j, old_val) in old_row.iter() { + if let (Some(&a), Some(cell)) = ( + alpha_row.get(j as usize), + decayed_values.get_mut(j as usize), + ) { + *cell = one.saturating_sub(a).saturating_mul(old_val); + } + } + + // Add alpha_j * new_ij, clamp to [0, 1], and emit sparse entries > 0. + let mut out_row: Vec<(u16, I32F32)> = Vec::new(); + for &(j, new_val) in new_row.iter() { + if let (Some(&a), Some(&decayed)) = + (alpha_row.get(j as usize), decayed_values.get(j as usize)) + { + let inc = a.saturating_mul(new_val).max(zero); + let val = decayed.saturating_add(inc).min(one); + if val > zero { + out_row.push((j, val)); + } + } + } + + result.push(out_row); + } + + result +} + +/// Calculates the exponential moving average (EMA) for a dense matrix using dynamic alpha values. +pub fn mat_ema_alpha( + new: &[Vec], // Weights + old: &[Vec], // Bonds + alpha: &[Vec], +) -> Vec> { + // Empty or degenerate input + if new.is_empty() || new.first().map(|r| r.is_empty()).unwrap_or(true) { + return vec![vec![]]; + } + + // If outer dimensions don't match, return bonds unchanged + if new.len() != old.len() || new.len() != alpha.len() { + log::error!( + "math error: mat_ema_alpha shapes don't match: {:?} vs. {:?} vs. {:?}", + old.len(), + new.len(), + alpha.len() + ); + return old.to_owned(); + } + + // Ensure each corresponding row has matching length; otherwise return `new` unchanged. + let mut old_it = old.iter(); + let mut alp_it = alpha.iter(); + for nrow in new.iter() { + let (Some(orow), Some(arow)) = (old_it.next(), alp_it.next()) else { + return new.to_owned(); + }; + if nrow.len() != orow.len() || nrow.len() != arow.len() { + return new.to_owned(); + } + } + + let zero = I32F32::saturating_from_num(0.0); + let one = I32F32::saturating_from_num(1.0); + + // Compute EMA: result = (1 - α) * old + α * new, clamped to [0, 1]. + let mut out: Vec> = Vec::with_capacity(new.len()); + let mut old_it = old.iter(); + let mut alp_it = alpha.iter(); + + for nrow in new.iter() { + let (Some(orow), Some(arow)) = (old_it.next(), alp_it.next()) else { + break; + }; + + let mut r: Vec = Vec::with_capacity(nrow.len()); + let mut n_it = nrow.iter(); + let mut o_it = orow.iter(); + let mut a_it = arow.iter(); + + while let (Some(&n), Some(&o), Some(&a)) = (n_it.next(), o_it.next(), a_it.next()) { + let one_minus_a = one.saturating_sub(a); + let decayed = one_minus_a.saturating_mul(o); + let inc = a.saturating_mul(n).max(zero); + r.push(decayed.saturating_add(inc).min(one)); + } + + out.push(r); + } + + out +} + +/// Natural log for positive `I32F32`; returns 0 when `value` is 0. +pub fn ln_or_zero(value: I32F32) -> I32F32 { + ln(value).unwrap_or(I32F32::saturating_from_num(0.0)) +} diff --git a/pallets/subtensor/src/epoch/math/fixed_conversions.rs b/pallets/subtensor/src/epoch/math/fixed_conversions.rs new file mode 100644 index 0000000000..83019479fc --- /dev/null +++ b/pallets/subtensor/src/epoch/math/fixed_conversions.rs @@ -0,0 +1,127 @@ +//! I32F32 / I64F64 / u16 conversion and max-upscale helpers for epoch consensus math. + +use safe_math::*; +use sp_std::vec::Vec; +use substrate_fixed::types::{I32F32, I64F64}; + +/// Index into `slice`, or `T::default()` when out of bounds (used by top-k / median). +pub fn copy_at_or_default(slice: &[T], idx: usize) -> T { + slice.get(idx).copied().unwrap_or_default() +} + +/// Convert an `f32` literal into epoch `I32F32` fixed-point. +pub fn fixed(val: f32) -> I32F32 { + I32F32::saturating_from_num(val) +} + +pub fn fixed_to_u16(x: I32F32) -> u16 { + x.saturating_to_num::() +} + +pub fn fixed_to_u64(x: I32F32) -> u64 { + x.saturating_to_num::() +} + +pub fn fixed64_to_u64(x: I64F64) -> u64 { + x.saturating_to_num::() +} + +pub fn fixed64_to_fixed32(x: I64F64) -> I32F32 { + I32F32::saturating_from_num(x) +} + +pub fn fixed32_to_fixed64(x: I32F32) -> I64F64 { + I64F64::saturating_from_num(x) +} + +pub fn u16_to_fixed(x: u16) -> I32F32 { + I32F32::saturating_from_num(x) +} + +/// Map a raw `u16` proportion (`0..=u16::MAX`) into `I32F32` in `0..=1`. +pub fn u16_proportion_to_fixed(x: u16) -> I32F32 { + I32F32::saturating_from_num(x).safe_div(I32F32::saturating_from_num(u16::MAX)) +} + +/// Scale an `I32F32` absolute value down by `u16::MAX` (bond/weight storage proportion). +pub fn i32f32_as_u16_proportion(x: I32F32) -> I32F32 { + x.safe_div(I32F32::saturating_from_num(u16::MAX)) +} + +pub fn fixed_proportion_to_u16(x: I32F32) -> u16 { + fixed_to_u16(x.saturating_mul(I32F32::saturating_from_num(u16::MAX))) +} + +pub fn vec_fixed32_to_u64(vec: Vec) -> Vec { + vec.into_iter().map(fixed_to_u64).collect() +} + +pub fn vec_fixed64_to_fixed32(vec: Vec) -> Vec { + vec.into_iter().map(fixed64_to_fixed32).collect() +} + +pub fn vec_fixed32_to_fixed64(vec: Vec) -> Vec { + vec.into_iter().map(fixed32_to_fixed64).collect() +} + +pub fn vec_fixed64_to_u64(vec: Vec) -> Vec { + vec.into_iter().map(fixed64_to_u64).collect() +} + +pub fn vec_fixed_proportions_to_u16(vec: Vec) -> Vec { + vec.into_iter().map(fixed_proportion_to_u16).collect() +} + +/// Max-upscale a non-negative vector so the max becomes `u16::MAX`, then cast to `u16`. +pub fn vec_max_upscale_to_u16(vec: &[I32F32]) -> Vec { + let u16_max: I32F32 = I32F32::saturating_from_num(u16::MAX); + let threshold: I32F32 = I32F32::saturating_from_num(32768); + let max_value: Option<&I32F32> = vec.iter().max(); + match max_value { + Some(val) => { + if *val == I32F32::saturating_from_num(0) { + return vec + .iter() + .map(|e: &I32F32| e.saturating_mul(u16_max).saturating_to_num::()) + .collect(); + } + if *val > threshold { + return vec + .iter() + .map(|e: &I32F32| { + e.saturating_mul(u16_max.safe_div(*val)) + .round() + .saturating_to_num::() + }) + .collect(); + } + vec.iter() + .map(|e: &I32F32| { + e.saturating_mul(u16_max) + .safe_div(*val) + .round() + .saturating_to_num::() + }) + .collect() + } + None => { + let sum: I32F32 = vec.iter().sum(); + vec.iter() + .map(|e: &I32F32| { + e.saturating_mul(u16_max) + .safe_div(sum) + .saturating_to_num::() + }) + .collect() + } + } +} + +/// Max-upscale a `u16` vector so the max becomes `u16::MAX`. +pub fn vec_u16_max_upscale_to_u16(vec: &[u16]) -> Vec { + let vec_fixed: Vec = vec + .iter() + .map(|e: &u16| I32F32::saturating_from_num(*e)) + .collect(); + vec_max_upscale_to_u16(&vec_fixed) +} diff --git a/pallets/subtensor/src/epoch/math/matmul_clip.rs b/pallets/subtensor/src/epoch/math/matmul_clip.rs new file mode 100644 index 0000000000..6f7da540cf --- /dev/null +++ b/pallets/subtensor/src/epoch/math/matmul_clip.rs @@ -0,0 +1,238 @@ +//! Sparse/dense matmul, Hadamard products, and per-column clipping used by Yuma consensus. + +use sp_std::vec; +use sp_std::vec::Vec; +use substrate_fixed::types::I32F32; + +pub fn row_hadamard(matrix: &[Vec], vector: &[I32F32]) -> Vec> { + let Some(first_row) = matrix.first() else { + return vec![vec![]]; + }; + if first_row.is_empty() { + return vec![vec![]]; + } + + let mut out = Vec::with_capacity(matrix.len()); + let mut vec_it = vector.iter(); + + for row in matrix.iter() { + let Some(&scale) = vec_it.next() else { break }; + let mut new_row = Vec::with_capacity(row.len()); + for m_val in row.iter() { + new_row.push(scale.saturating_mul(*m_val)); + } + out.push(new_row); + } + + out +} + +// Row-wise sparse matrix-vector hadamard product. +pub fn row_hadamard_sparse( + sparse_matrix: &[Vec<(u16, I32F32)>], + vector: &[I32F32], +) -> Vec> { + let mut out = Vec::with_capacity(sparse_matrix.len()); + let mut vec_it = vector.iter(); + + for sparse_row in sparse_matrix.iter() { + let Some(&scale) = vec_it.next() else { break }; + let mut new_row = Vec::with_capacity(sparse_row.len()); + for &(j, val) in sparse_row.iter() { + new_row.push((j, val.saturating_mul(scale))); + } + out.push(new_row); + } + + out +} + +// Row-wise matrix-vector product, column-wise sum: result_j = SUM(i) vector_i * matrix_ij. +pub fn matmul(matrix: &[Vec], vector: &[I32F32]) -> Vec { + let Some(first_row) = matrix.first() else { + return vec![]; + }; + let cols = first_row.len(); + if cols == 0 { + return vec![]; + } + if matrix.len() != vector.len() { + log::error!( + "math error: matmul input sizes are not equal: {:?} != {:?}", + matrix.len(), + vector.len() + ); + } + + let zero = I32F32::saturating_from_num(0.0); + let mut acc = vec![zero; cols]; + + let mut vec_it = vector.iter(); + for row in matrix.iter() { + // Use 0 if the vector ran out (rows beyond vector length contribute nothing). + let scale = vec_it.next().copied().unwrap_or(zero); + + let mut acc_it = acc.iter_mut(); + for m_val in row.iter() { + if let Some(a) = acc_it.next() { + *a = a.saturating_add(scale.saturating_mul(*m_val)); + } else { + // Ignore elements beyond the accumulator width (first row’s length). + break; + } + } + } + + acc +} + +// Column-wise matrix-vector product, row-wise sum: result_i = SUM(j) vector_j * matrix_ij. +pub fn matmul_transpose(matrix: &[Vec], vector: &[I32F32]) -> Vec { + let Some(first_row) = matrix.first() else { + return vec![]; + }; + if first_row.is_empty() { + return vec![]; + } + if vector.len() != first_row.len() { + log::error!( + "math error: matmul_transpose matrix width doesn't match to vector height: {:?} != {:?}", + first_row.len(), + vector.len() + ); + } + + let zero = I32F32::saturating_from_num(0.0); + let mut out = Vec::with_capacity(matrix.len()); + + for row in matrix.iter() { + let mut sum = zero; + let mut v_it = vector.iter(); + for m in row.iter() { + if let Some(&v) = v_it.next() { + sum = sum.saturating_add(m.saturating_mul(v)); + } else { + break; + } + } + out.push(sum); + } + + out +} + +// Row-wise sparse_matrix-vector product, column-wise sum: result_j = SUM(i) vector_i * matrix_ij. +pub fn matmul_sparse( + sparse_matrix: &[Vec<(u16, I32F32)>], + vector: &[I32F32], + columns: u16, +) -> Vec { + let zero = I32F32::saturating_from_num(0.0); + let mut result = vec![zero; columns as usize]; + + let mut vec_it = vector.iter(); + for row in sparse_matrix.iter() { + let scale = vec_it.next().copied().unwrap_or(zero); + for &(j, val) in row.iter() { + if let Some(r) = result.get_mut(j as usize) { + *r = r.saturating_add(scale.saturating_mul(val)); + } + } + } + + result +} + +// Column-wise sparse_matrix-vector product, row-wise sum: result_i = SUM(j) vector_j * matrix_ij. +pub fn matmul_transpose_sparse( + sparse_matrix: &[Vec<(u16, I32F32)>], + vector: &[I32F32], +) -> Vec { + let zero = I32F32::saturating_from_num(0.0); + let mut result = vec![zero; sparse_matrix.len()]; + + let mut out_it = result.iter_mut(); + for row in sparse_matrix.iter() { + let Some(out_cell) = out_it.next() else { break }; + let mut acc = zero; + for &(j, val) in row.iter() { + let v = vector.get(j as usize).copied().unwrap_or(zero); + acc = acc.saturating_add(v.saturating_mul(val)); + } + *out_cell = acc; + } + + result +} + +// Set inplace matrix values above column threshold to threshold value. +pub fn inplace_col_clip(x: &mut [Vec], col_threshold: &[I32F32]) { + for row in x.iter_mut() { + let mut thr_it = col_threshold.iter(); + for value in row.iter_mut() { + if let Some(th) = thr_it.next() { + // Clip: value = min(value, threshold) + *value = *th.min(&*value); + } else { + // No more thresholds; stop for this row. + break; + } + } + } +} + +// Return sparse matrix with values above column threshold set to threshold value. +pub fn col_clip_sparse( + sparse_matrix: &[Vec<(u16, I32F32)>], + col_threshold: &[I32F32], +) -> Vec> { + let zero = I32F32::saturating_from_num(0.0); + let mut result = Vec::with_capacity(sparse_matrix.len()); + + for row in sparse_matrix.iter() { + let mut out_row: Vec<(u16, I32F32)> = Vec::with_capacity(row.len()); + for &(j, val) in row.iter() { + let th = col_threshold.get(j as usize).copied().unwrap_or(zero); + if th < val { + if th > zero { + // clip down to threshold, but drop if threshold <= 0 + out_row.push((j, th)); + } + } else { + // keep original + out_row.push((j, val)); + } + } + result.push(out_row); + } + + result +} + +// Stake-weighted median score finding algorithm, based on a mid pivot binary search. +// Normally a random pivot is used, but to ensure full determinism the mid point is chosen instead. +// Assumes relatively random score order for efficiency, typically less than O(nlogn) complexity. +// +// # Args: +// * 'stake': ( &[I32F32] ): +// - stake, assumed to be normalized. +// +// * 'score': ( &[I32F32] ): +// - score for which median is sought, 0 <= score <= 1 +// +// * 'partition_idx' ( &[usize] ): +// - indices as input partition +// +// * 'minority' ( I32F32 ): +// - minority_ratio = 1 - majority_ratio +// +// * 'partition_lo' ( I32F32 ): +// - lower edge of stake for partition, where partition is a segment [lo, hi] inside stake integral [0, 1]. +// +// * 'partition_hi' ( I32F32 ): +// - higher edge of stake for partition, where partition is a segment [lo, hi] inside stake integral [0, 1]. +// +// # Returns: +// * 'median': ( I32F32 ): +// - median via random pivot binary search. +// diff --git a/pallets/subtensor/src/epoch/math/matrix_normalize_mask.rs b/pallets/subtensor/src/epoch/math/matrix_normalize_mask.rs new file mode 100644 index 0000000000..041d4afab8 --- /dev/null +++ b/pallets/subtensor/src/epoch/math/matrix_normalize_mask.rs @@ -0,0 +1,437 @@ +//! Row/column normalize, max-upscale, and boolean masks for dense/sparse weight matrices. + +use safe_math::*; +use sp_std::vec; +use sp_std::vec::Vec; +use substrate_fixed::types::I32F32; + +/// Normalizes (sum to 1 except 0) each row (dim=0) of a matrix in-place. +pub fn inplace_row_normalize(x: &mut [Vec]) { + for row in x { + let row_sum: I32F32 = row.iter().sum(); + if row_sum > I32F32::saturating_from_num(0.0_f32) { + row.iter_mut() + .for_each(|x_ij: &mut I32F32| *x_ij = x_ij.safe_div(row_sum)); + } + } +} + +// Normalizes (sum to 1 except 0) each row (dim=0) of a sparse matrix in-place. +pub fn inplace_row_normalize_sparse(sparse_matrix: &mut [Vec<(u16, I32F32)>]) { + for sparse_row in sparse_matrix.iter_mut() { + let row_sum: I32F32 = sparse_row.iter().map(|(_j, value)| *value).sum(); + if row_sum > I32F32::saturating_from_num(0.0) { + sparse_row + .iter_mut() + .for_each(|(_j, value)| *value = value.safe_div(row_sum)); + } + } +} + +// Sum across each row (dim=0) of a matrix. +pub fn row_sum(x: &[Vec]) -> Vec { + if let Some(first_row) = x.first() + && first_row.is_empty() + { + return vec![]; + } + x.iter().map(|row| row.iter().sum()).collect() +} + +// Sum across each row (dim=0) of a sparse matrix. +pub fn row_sum_sparse(sparse_matrix: &[Vec<(u16, I32F32)>]) -> Vec { + sparse_matrix + .iter() + .map(|row| row.iter().map(|(_, value)| value).sum()) + .collect() +} + +// Normalizes (sum to 1 except 0) each column (dim=1) of a sparse matrix in-place. +pub fn inplace_col_normalize_sparse(sparse_matrix: &mut [Vec<(u16, I32F32)>], columns: u16) { + let zero = I32F32::saturating_from_num(0.0); + let mut col_sum: Vec = vec![zero; columns as usize]; + + // Pass 1: accumulate column sums. + for sparse_row in sparse_matrix.iter() { + for &(j, value) in sparse_row.iter() { + if let Some(sum) = col_sum.get_mut(j as usize) { + *sum = sum.saturating_add(value); + } + } + } + + // Pass 2: normalize by column sums where non-zero. + for sparse_row in sparse_matrix.iter_mut() { + for (j, value) in sparse_row.iter_mut() { + let denom = col_sum.get(*j as usize).copied().unwrap_or(zero); + if denom != zero { + *value = value.safe_div(denom); + } + } + } +} + +// Normalizes (sum to 1 except 0) each column (dim=1) of a matrix in-place. +// If a row is shorter/longer than the accumulator, pad with zeroes accordingly. +pub fn inplace_col_normalize(x: &mut [Vec]) { + let zero = I32F32::saturating_from_num(0.0); + + // Build column sums; treat missing entries as zero, but don't modify rows. + let mut col_sums: Vec = Vec::new(); + for row in x.iter() { + if col_sums.len() < row.len() { + col_sums.resize(row.len(), zero); + } + let mut sums_it = col_sums.iter_mut(); + for v in row.iter() { + if let Some(sum) = sums_it.next() { + *sum = sum.saturating_add(*v); + } else { + break; + } + } + } + + if col_sums.is_empty() { + return; + } + + // Normalize only existing elements in each row. + for row in x.iter_mut() { + let mut sums_it = col_sums.iter(); + for m in row.iter_mut() { + if let Some(sum) = sums_it.next() { + if *sum != zero { + *m = m.safe_div(*sum); + } + } else { + break; + } + } + } +} + +// Max-upscale each column (dim=1) of a sparse matrix in-place. +pub fn inplace_col_max_upscale_sparse(sparse_matrix: &mut [Vec<(u16, I32F32)>], columns: u16) { + let zero = I32F32::saturating_from_num(0.0); + let mut col_max: Vec = vec![zero; columns as usize]; + + // Pass 1: compute per-column max + for sparse_row in sparse_matrix.iter() { + for (j, value) in sparse_row.iter() { + if let Some(m) = col_max.get_mut(*j as usize) + && *m < *value + { + *m = *value; + } + } + } + + // Pass 2: divide each nonzero entry by its column max + for sparse_row in sparse_matrix.iter_mut() { + for (j, value) in sparse_row.iter_mut() { + let m = col_max.get(*j as usize).copied().unwrap_or(zero); + if m != zero { + *value = value.safe_div(m); + } + } + } +} + +// Max-upscale each column (dim=1) of a matrix in-place. +pub fn inplace_col_max_upscale(x: &mut [Vec]) { + let zero = I32F32::saturating_from_num(0.0); + + // Find the widest row to size the column-max buffer; don't modify rows. + let max_cols = x.iter().map(|r| r.len()).max().unwrap_or(0); + if max_cols == 0 { + return; + } + + // Pass 1: compute per-column maxima across existing entries only. + let mut col_maxes = vec![zero; max_cols]; + for row in x.iter() { + let mut max_it = col_maxes.iter_mut(); + for v in row.iter() { + if let Some(m) = max_it.next() { + if *m < *v { + *m = *v; + } + } else { + break; + } + } + } + + // Pass 2: divide each existing entry by its column max (if non-zero). + for row in x.iter_mut() { + let mut max_it = col_maxes.iter(); + for val in row.iter_mut() { + if let Some(&m) = max_it.next() { + if m != zero { + *val = val.safe_div(m); + } + } else { + break; + } + } + } +} + +// Apply mask to vector, mask=true will mask out, i.e. set to 0. +pub fn inplace_mask_vector(mask: &[bool], vector: &mut [I32F32]) { + if mask.len() != vector.len() { + log::error!( + "math error: inplace_mask_vector input lengths are not equal: {:?} != {:?}", + mask.len(), + vector.len() + ); + } + + if mask.is_empty() { + return; + } + let zero: I32F32 = I32F32::saturating_from_num(0.0); + for (i, v) in vector.iter_mut().enumerate() { + if *mask.get(i).unwrap_or(&true) { + *v = zero; + } + } +} + +// Apply mask to matrix, mask=true will mask out, i.e. set to 0. +pub fn inplace_mask_matrix(mask: &[Vec], matrix: &mut [Vec]) { + if mask.len() != matrix.len() { + log::error!( + "math error: inplace_mask_matrix input sizes are not equal: {:?} != {:?}", + mask.len(), + matrix.len() + ); + } + let Some(first_row) = mask.first() else { + return; + }; + if first_row.is_empty() { + return; + } + let zero: I32F32 = I32F32::saturating_from_num(0.0); + for (r, row) in matrix.iter_mut().enumerate() { + let mask_row_opt = mask.get(r); + for (c, val) in row.iter_mut().enumerate() { + let should_zero = mask_row_opt + .and_then(|mr| mr.get(c)) + .copied() + .unwrap_or(true); + if should_zero { + *val = zero; + } + } + } +} + +// Apply row mask to matrix, mask=true will mask out, i.e. set to 0. +pub fn inplace_mask_rows(mask: &[bool], matrix: &mut [Vec]) { + if mask.len() != matrix.len() { + log::error!( + "math error: inplace_mask_rows input sizes are not equal: {:?} != {:?}", + mask.len(), + matrix.len() + ); + } + let Some(first_row) = matrix.first() else { + return; + }; + let cols = first_row.len(); + let zero: I32F32 = I32F32::saturating_from_num(0); + for (r, row) in matrix.iter_mut().enumerate() { + if mask.get(r).copied().unwrap_or(true) { + *row = vec![zero; cols]; + } + } +} + +// Apply column mask to matrix, mask=true will mask out, i.e. set to 0. +// Assumes each column has the same length. +pub fn inplace_mask_cols(mask: &[bool], matrix: &mut [Vec]) { + if mask.len() != matrix.len() { + log::error!( + "math error: inplace_mask_cols input sizes are not equal: {:?} != {:?}", + mask.len(), + matrix.len() + ); + } + if matrix.is_empty() { + return; + }; + let zero: I32F32 = I32F32::saturating_from_num(0); + for row in matrix.iter_mut() { + for (c, elem) in row.iter_mut().enumerate() { + if mask.get(c).copied().unwrap_or(true) { + *elem = zero; + } + } + } +} + +// Mask out the diagonal of the input matrix in-place. +pub fn inplace_mask_diag(matrix: &mut [Vec]) { + let Some(first_row) = matrix.first() else { + return; + }; + if first_row.is_empty() { + return; + } + // Weights that we use this function for are always a square matrix. + // If something not square is passed to this function, it's safe to return + // with no action. Log error if this happens. + if matrix.len() != first_row.len() { + log::error!( + "math error: inplace_mask_diag: matrix.len {:?} != first_row.len {:?}", + matrix.len(), + first_row.len() + ); + return; + } + + let zero: I32F32 = I32F32::saturating_from_num(0.0); + matrix.iter_mut().enumerate().for_each(|(idx, row)| { + let Some(elem) = row.get_mut(idx) else { + // Should not happen since matrix is square + return; + }; + *elem = zero; + }); +} + +// Remove cells from sparse matrix where the mask function of a scalar and a vector is true. +pub fn scalar_vec_mask_sparse_matrix( + sparse_matrix: &[Vec<(u16, I32F32)>], + scalar: u64, + vector: &[u64], + mask_fn: &dyn Fn(u64, u64) -> bool, +) -> Vec> { + let mut result: Vec> = Vec::with_capacity(sparse_matrix.len()); + + for row in sparse_matrix.iter() { + let mut out_row: Vec<(u16, I32F32)> = Vec::with_capacity(row.len()); + for &(j, value) in row.iter() { + let vj = vector.get(j as usize).copied().unwrap_or(0); + if !mask_fn(scalar, vj) { + out_row.push((j, value)); + } + } + result.push(out_row); + } + + result +} + +// Mask out the diagonal of the input matrix in-place, except for the diagonal entry at except_index. +pub fn inplace_mask_diag_except_index(matrix: &mut [Vec], except_index: u16) { + let Some(first_row) = matrix.first() else { + return; + }; + if first_row.is_empty() { + return; + } + if matrix.len() != first_row.len() { + log::error!( + "math error: inplace_mask_diag input matrix is now square: {:?} != {:?}", + matrix.len(), + first_row.len() + ); + return; + } + let diag_at_index = matrix + .get(except_index as usize) + .and_then(|row| row.get(except_index as usize)) + .cloned(); + + inplace_mask_diag(matrix); + + matrix.get_mut(except_index as usize).map(|row| { + row.get_mut(except_index as usize).map(|value| { + if let Some(diag_at_index) = diag_at_index { + *value = diag_at_index; + } + }) + }); +} + +// Return a new sparse matrix that replaces masked rows with an empty vector placeholder. +pub fn mask_rows_sparse( + mask: &[bool], + sparse_matrix: &[Vec<(u16, I32F32)>], +) -> Vec> { + let mut out = Vec::with_capacity(sparse_matrix.len()); + for (i, sparse_row) in sparse_matrix.iter().enumerate() { + if mask.get(i).copied().unwrap_or(true) { + out.push(Vec::new()); + } else { + out.push(sparse_row.clone()); + } + } + out +} + +// Return a new sparse matrix with a masked out diagonal of input sparse matrix. +pub fn mask_diag_sparse(sparse_matrix: &[Vec<(u16, I32F32)>]) -> Vec> { + sparse_matrix + .iter() + .enumerate() + .map(|(i, sparse_row)| { + sparse_row + .iter() + .filter(|(j, _)| i != (*j as usize)) + .copied() + .collect() + }) + .collect() +} + +// Return a new sparse matrix with a masked out diagonal of input sparse matrix, +// except for the diagonal entry at except_index. +pub fn mask_diag_sparse_except_index( + sparse_matrix: &[Vec<(u16, I32F32)>], + except_index: u16, +) -> Vec> { + sparse_matrix + .iter() + .enumerate() + .map(|(i, sparse_row)| { + sparse_row + .iter() + .filter(|(j, _)| { + // Is not a diagonal OR is the diagonal at except_index + i != (*j as usize) || (i == except_index as usize && *j == except_index) + }) + .copied() + .collect() + }) + .collect() +} + +// Remove cells from sparse matrix where the mask function of two vectors is true. +pub fn vec_mask_sparse_matrix( + sparse_matrix: &[Vec<(u16, I32F32)>], + first_vector: &[u64], + second_vector: &[u64], + mask_fn: &dyn Fn(u64, u64) -> bool, +) -> Vec> { + let mut result: Vec> = Vec::with_capacity(sparse_matrix.len()); + let mut fv_it = first_vector.iter(); + for row in sparse_matrix.iter() { + let fv = fv_it.next().copied().unwrap_or(0); + let mut out_row: Vec<(u16, I32F32)> = Vec::with_capacity(row.len()); + for &(j, val) in row.iter() { + let sv = second_vector.get(j as usize).copied().unwrap_or(0); + if !mask_fn(fv, sv) { + out_row.push((j, val)); + } + } + result.push(out_row); + } + result +} + +// Row-wise matrix-vector hadamard product. diff --git a/pallets/subtensor/src/epoch/math/mod.rs b/pallets/subtensor/src/epoch/math/mod.rs new file mode 100644 index 0000000000..18bf281494 --- /dev/null +++ b/pallets/subtensor/src/epoch/math/mod.rs @@ -0,0 +1,29 @@ +//! Fixed-point linear algebra for Yuma consensus / epoch emission. +//! +//! These helpers operate on `I32F32` / `I64F64` stake-weight and bond matrices produced +//! by [`super::run_epoch`]. Callers outside this module typically `use crate::epoch::math::*`. +//! +//! ## Search anchors +//! +//! | Module | Owns | +//! |--------|------| +//! | [`fixed_conversions`] | u16 ↔ fixed proportions, max-upscale to u16 | +//! | [`vector_ops`] | normalize, top-k, exp/sigmoid, elementwise div | +//! | [`matrix_normalize_mask`] | row/col normalize & boolean masks (dense/sparse) | +//! | [`matmul_clip`] | matmul, Hadamard, column clip | +//! | [`weighted_median`] | stake-weighted median consensus | +//! | [`ema_interpolate`] | bonds EMA, interpolate, [`clamp_i32f32`], [`ln_or_zero`] | + +mod ema_interpolate; +mod fixed_conversions; +mod matmul_clip; +mod matrix_normalize_mask; +mod vector_ops; +mod weighted_median; + +pub use ema_interpolate::*; +pub use fixed_conversions::*; +pub use matmul_clip::*; +pub use matrix_normalize_mask::*; +pub use vector_ops::*; +pub use weighted_median::*; diff --git a/pallets/subtensor/src/epoch/math/vector_ops.rs b/pallets/subtensor/src/epoch/math/vector_ops.rs new file mode 100644 index 0000000000..6416582551 --- /dev/null +++ b/pallets/subtensor/src/epoch/math/vector_ops.rs @@ -0,0 +1,191 @@ +//! Vector sum/normalize, top-k masks, and clamped exp/sigmoid for epoch scoring. + +use super::copy_at_or_default; +use safe_math::*; +use sp_runtime::traits::CheckedAdd; +use sp_std::vec; +use sp_std::vec::Vec; +use substrate_fixed::transcendental::exp; +use substrate_fixed::types::{I32F32, I64F64}; + +/// After normalizing `vec` as proportions, true iff no entry exceeds `max_limit / u16::MAX`. +pub fn check_vec_max_limited(vec: &[u16], max_limit: u16) -> bool { + let max_limit_fixed: I32F32 = + I32F32::saturating_from_num(max_limit).safe_div(I32F32::saturating_from_num(u16::MAX)); + let mut vec_fixed: Vec = vec + .iter() + .map(|e: &u16| I32F32::saturating_from_num(*e)) + .collect(); + inplace_normalize(&mut vec_fixed); + let max_value: Option<&I32F32> = vec_fixed.iter().max(); + max_value.is_none_or(|v| *v <= max_limit_fixed) +} + +pub fn sum(x: &[I32F32]) -> I32F32 { + x.iter().sum() +} + +// Sums a Vector of type that has CheckedAdd trait. +// Returns None if overflow occurs during sum using T::checked_add. +// Returns Some(T::default()) if input vector is empty. +pub fn checked_sum(x: &[T]) -> Option +where + T: Copy + Default + CheckedAdd, +{ + let mut iter = x.iter(); + let Some(mut sum) = iter.next().copied() else { + return Some(T::default()); + }; + for i in iter { + sum = sum.checked_add(i)?; + } + Some(sum) +} + +// Return true when vector sum is zero. +pub fn is_zero(vector: &[I32F32]) -> bool { + let vector_sum: I32F32 = sum(vector); + vector_sum == I32F32::saturating_from_num(0) +} + +/// `exp(input)` with input clamped to `[-20, 20]` to avoid fixed-point overflow. +pub fn exp_safe(input: I32F32) -> I32F32 { + let min_input: I32F32 = I32F32::saturating_from_num(-20); // <= 1/exp(-20) = 485 165 195,4097903 + let max_input: I32F32 = I32F32::saturating_from_num(20); // <= exp(20) = 485 165 195,4097903 + let mut safe_input: I32F32 = input; + if input < min_input { + safe_input = min_input; + } else if max_input < input { + safe_input = max_input; + } + let output: I32F32; + match exp(safe_input) { + Ok(val) => { + output = val; + } + Err(_err) => { + if safe_input <= 0 { + output = I32F32::saturating_from_num(0); + } else { + output = I32F32::max_value(); + } + } + } + output +} + +/// Consensus sigmoid: `1 / (1 + exp(-rho * (input - kappa)))` using [`exp_safe`]. +pub fn sigmoid_safe(input: I32F32, rho: I32F32, kappa: I32F32) -> I32F32 { + let one: I32F32 = I32F32::saturating_from_num(1); + let offset: I32F32 = input.saturating_sub(kappa); // (input - kappa) + let neg_rho: I32F32 = rho.saturating_mul(one.saturating_neg()); // -rho + let exp_input: I32F32 = neg_rho.saturating_mul(offset); // -rho*(input-kappa) + let exp_output: I32F32 = exp_safe(exp_input); // exp(-rho*(input-kappa)) + let denominator: I32F32 = exp_output.saturating_add(one); // 1 + exp(-rho*(input-kappa)) + let sigmoid_output: I32F32 = one.safe_div(denominator); // 1 / (1 + exp(-rho*(input-kappa))) + sigmoid_output +} + +// Returns a bool vector where an item is true if the vector item is in topk values. +pub fn is_topk(vector: &[I32F32], k: usize) -> Vec { + let n: usize = vector.len(); + let mut result: Vec = vec![true; n]; + if n < k { + return result; + } + let mut idxs: Vec = (0..n).collect(); + idxs.sort_by_key(|&idx| copy_at_or_default(vector, idx)); // ascending stable sort + for &idx in idxs.iter().take(n.saturating_sub(k)) { + if let Some(cell) = result.get_mut(idx) { + *cell = false; + } + } + result +} + +// Returns a bool vector where an item is true if the vector item is in topk values and is non-zero. +pub fn is_topk_nonzero_i32f32(vector: &[I32F32], k: usize) -> Vec { + let n: usize = vector.len(); + let mut result: Vec = vector.iter().map(|&elem| elem != I32F32::from(0)).collect(); + if n < k { + return result; + } + let mut idxs: Vec = (0..n).collect(); + idxs.sort_by_key(|&idx| copy_at_or_default(vector, idx)); // ascending stable sort + for &idx in idxs.iter().take(n.saturating_sub(k)) { + if let Some(cell) = result.get_mut(idx) { + *cell = false; + } + } + result +} + +// Returns a normalized (sum to 1 except 0) copy of the input vector. +pub fn normalize(x: &[I32F32]) -> Vec { + let x_sum: I32F32 = sum(x); + if x_sum != I32F32::saturating_from_num(0.0_f32) { + x.iter().map(|xi| xi.safe_div(x_sum)).collect() + } else { + x.to_vec() + } +} + +// Normalizes (sum to 1 except 0) the input vector directly in-place. +pub fn inplace_normalize(x: &mut [I32F32]) { + let x_sum: I32F32 = x.iter().sum(); + if x_sum == I32F32::saturating_from_num(0.0_f32) { + return; + } + x.iter_mut() + .for_each(|value| *value = value.safe_div(x_sum)); +} + +// Normalizes (sum to 1 except 0) the input vector directly in-place, using the sum arg. +pub fn inplace_normalize_i32f32_with_sum(x: &mut [I32F32], x_sum: I32F32) { + if x_sum == I32F32::saturating_from_num(0.0_f32) { + return; + } + x.iter_mut() + .for_each(|value| *value = value.safe_div(x_sum)); +} + +// Normalizes (sum to 1 except 0) the I64F64 input vector directly in-place. +pub fn inplace_normalize_64(x: &mut [I64F64]) { + let x_sum: I64F64 = x.iter().sum(); + if x_sum == I64F64::saturating_from_num(0) { + return; + } + x.iter_mut() + .for_each(|value| *value = value.safe_div(x_sum)); +} + +/// Normalizes (sum to 1 except 0) each row (dim=0) of a I64F64 matrix in-place. +pub fn inplace_row_normalize_64(x: &mut [Vec]) { + for row in x { + let row_sum: I64F64 = row.iter().sum(); + if row_sum > I64F64::saturating_from_num(0.0_f64) { + row.iter_mut() + .for_each(|x_ij: &mut I64F64| *x_ij = x_ij.safe_div(row_sum)); + } + } +} + +/// Returns x / y for input vectors x and y, if y == 0 return 0. +pub fn elementwise_safe_div(x: &[I32F32], y: &[I32F32]) -> Vec { + if x.len() != y.len() { + log::error!( + "math error: elementwise_safe_div input lengths are not equal: {:?} != {:?}", + x.len(), + y.len() + ); + } + + let zero = I32F32::saturating_from_num(0); + + let mut out = Vec::with_capacity(x.len()); + for (i, x_i) in x.iter().enumerate() { + let y_i = y.get(i).copied().unwrap_or(zero); + out.push(x_i.safe_div(y_i)); + } + out +} diff --git a/pallets/subtensor/src/epoch/math/weighted_median.rs b/pallets/subtensor/src/epoch/math/weighted_median.rs new file mode 100644 index 0000000000..7c85e92b29 --- /dev/null +++ b/pallets/subtensor/src/epoch/math/weighted_median.rs @@ -0,0 +1,213 @@ +//! Stake-weighted median consensus: scalar, dense column-wise, and sparse column-wise. + +use super::{copy_at_or_default, inplace_normalize}; +use safe_math::*; +use sp_std::vec; +use sp_std::vec::Vec; +use substrate_fixed::types::I32F32; + +pub fn weighted_median( + stake: &[I32F32], + score: &[I32F32], + partition_idx: &[usize], + minority: I32F32, + mut partition_lo: I32F32, + mut partition_hi: I32F32, +) -> I32F32 { + let zero = I32F32::saturating_from_num(0.0); + if stake.len() != score.len() { + log::error!( + "math error: weighted_median stake and score have different lengths: {:?} != {:?}", + stake.len(), + score.len() + ); + return zero; + } + let mut current_partition_index: Vec = partition_idx.to_vec(); + let mut iteration_counter: usize = 0; + let iteration_limit = partition_idx.len(); + let mut lower: Vec = vec![]; + let mut upper: Vec = vec![]; + + loop { + let n = current_partition_index.len(); + if n == 0 { + return zero; + } + if n == 1 { + if let Some(&only_idx) = current_partition_index.first() { + return copy_at_or_default::(score, only_idx); + } else { + return zero; + } + } + let mid_idx: usize = n.safe_div(2); + let pivot: I32F32 = copy_at_or_default::( + score, + current_partition_index.get(mid_idx).copied().unwrap_or(0), + ); + let mut lo_stake: I32F32 = I32F32::saturating_from_num(0); + let mut hi_stake: I32F32 = I32F32::saturating_from_num(0); + + for idx in current_partition_index.clone() { + if copy_at_or_default::(score, idx) == pivot { + continue; + } + if copy_at_or_default::(score, idx) < pivot { + lo_stake = lo_stake.saturating_add(copy_at_or_default::(stake, idx)); + lower.push(idx); + } else { + hi_stake = hi_stake.saturating_add(copy_at_or_default::(stake, idx)); + upper.push(idx); + } + } + if (minority < partition_lo.saturating_add(lo_stake)) && (!lower.is_empty()) { + current_partition_index = lower.clone(); + partition_hi = partition_lo.saturating_add(lo_stake); + } else if (partition_hi.saturating_sub(hi_stake) <= minority) && (!upper.is_empty()) { + current_partition_index = upper.clone(); + partition_lo = partition_hi.saturating_sub(hi_stake); + } else { + return pivot; + } + + lower.clear(); + upper.clear(); + + // Safety limit: We should never need more than iteration_limit iterations. + iteration_counter = iteration_counter.saturating_add(1); + if iteration_counter > iteration_limit { + break; + } + } + zero +} + +/// Column-wise weighted median, e.g. stake-weighted median scores per server (column) over all validators (rows). +pub fn weighted_median_col( + stake: &[I32F32], + score: &[Vec], + majority: I32F32, +) -> Vec { + let zero = I32F32::saturating_from_num(0.0); + + // Determine number of columns from the first row. + let columns = score.first().map(|r| r.len()).unwrap_or(0); + let mut median = vec![zero; columns]; + + // Iterate columns into `median`. + let mut c = 0usize; + for med_cell in median.iter_mut() { + let mut use_stake: Vec = Vec::new(); + let mut use_score: Vec = Vec::new(); + + // Iterate rows aligned with `stake` length. + let mut r = 0usize; + while r < stake.len() { + let st = copy_at_or_default::(stake, r); + if st > zero { + // Fetch row safely; if it's missing or has wrong width, push zeros to both. + if let Some(row) = score.get(r) { + if row.len() == columns { + let val = row.get(c).copied().unwrap_or(zero); + use_stake.push(st); + use_score.push(val); + } else { + use_stake.push(zero); + use_score.push(zero); + log::error!( + "math error: weighted_median_col row.len() != columns: {:?} != {:?}", + row.len(), + columns + ); + } + } else { + // Missing row: insert zeroes. + use_stake.push(zero); + use_score.push(zero); + } + } + r = r.saturating_add(1); + } + + if !use_stake.is_empty() { + inplace_normalize(&mut use_stake); + let stake_sum: I32F32 = use_stake.iter().sum(); + let minority: I32F32 = stake_sum.saturating_sub(majority); + + let idxs: Vec = (0..use_stake.len()).collect(); + *med_cell = weighted_median( + &use_stake, + &use_score, + idxs.as_slice(), + minority, + zero, + stake_sum, + ); + } + + c = c.saturating_add(1); + } + median +} + +/// Column-wise weighted median, e.g. stake-weighted median scores per server (column) over all validators (rows). +pub fn weighted_median_col_sparse( + stake: &[I32F32], + score: &[Vec<(u16, I32F32)>], + columns: u16, + majority: I32F32, +) -> Vec { + let zero = I32F32::saturating_from_num(0.0); + + // Keep only positive-stake rows; normalize them. + let mut use_stake: Vec = stake.iter().copied().filter(|&s| s > zero).collect(); + inplace_normalize(&mut use_stake); + + let stake_sum: I32F32 = use_stake.iter().sum(); + let minority: I32F32 = stake_sum.saturating_sub(majority); + let stake_idx: Vec = (0..use_stake.len()).collect(); + + // use_score: columns x use_stake.len(), prefilled with zeros. + let mut use_score: Vec> = (0..columns as usize) + .map(|_| vec![zero; use_stake.len()]) + .collect(); + + // Fill use_score by walking stake and score together, counting positives with k. + let mut k: usize = 0; + let mut stake_it = stake.iter(); + let mut score_it = score.iter(); + + while let (Some(&s), Some(sparse_row)) = (stake_it.next(), score_it.next()) { + if s > zero { + for &(c, val) in sparse_row.iter() { + if let Some(col_vec) = use_score.get_mut(c as usize) + && let Some(cell) = col_vec.get_mut(k) + { + *cell = val; + } + } + k = k.saturating_add(1); + } + } + + // Compute weighted median per column. + let mut median: Vec = Vec::with_capacity(columns as usize); + for col_vec in use_score.iter() { + median.push(weighted_median( + &use_stake, + col_vec, + stake_idx.as_slice(), + minority, + zero, + stake_sum, + )); + } + + median +} + +// Element-wise interpolation of two matrices: Result = A + ratio * (B - A). +// ratio has intended range [0, 1] +// ratio=0: Result = A +// ratio=1: Result = B diff --git a/pallets/subtensor/src/epoch/mod.rs b/pallets/subtensor/src/epoch/mod.rs index 3b22f940e6..797d8cd41c 100644 --- a/pallets/subtensor/src/epoch/mod.rs +++ b/pallets/subtensor/src/epoch/mod.rs @@ -1,3 +1,13 @@ +//! Epoch consensus math and per-subnet emission scoring (Yuma). +//! +//! ## Search anchors +//! +//! - [`math`] — fixed-point vector/matrix helpers (normalize, matmul, weighted median, bonds EMA) +//! - [`run_epoch`] — [`run_epoch::epoch_mechanism`], persistence, liquid-alpha bonds +//! +//! Storage vectors written by epoch (`Incentive`, `Bonds`, `Emission`, …) live in the pallet +//! storage map; this module computes and persists them at tempo boundaries. + use super::*; pub mod math; pub mod run_epoch; diff --git a/pallets/subtensor/src/epoch/run_epoch.rs b/pallets/subtensor/src/epoch/run_epoch.rs deleted file mode 100644 index 7396695003..0000000000 --- a/pallets/subtensor/src/epoch/run_epoch.rs +++ /dev/null @@ -1,1660 +0,0 @@ -use super::*; -use crate::epoch::math::*; -use alloc::collections::{BTreeMap, BTreeSet}; -use frame_support::IterableStorageDoubleMap; -use safe_math::*; -use sp_runtime::PerU16; -use sp_std::collections::btree_map::IntoIter; -use sp_std::vec; -use substrate_fixed::types::{I32F32, I64F64, I96F32}; -use subtensor_runtime_common::{AlphaBalance, MechId, NetUid, NetUidStorageIndex}; - -#[derive(Debug, Default)] -pub struct EpochTerms { - pub uid: usize, - pub dividend: u16, - pub incentive: u16, - pub validator_emission: AlphaBalance, - pub server_emission: AlphaBalance, - pub stake_weight: u16, - pub active: bool, - pub emission: AlphaBalance, - pub consensus: u16, - pub validator_trust: u16, - pub new_validator_permit: bool, - pub bond: Vec<(u16, u16)>, - pub stake: AlphaBalance, -} - -pub struct EpochOutput(pub BTreeMap); - -impl EpochOutput { - pub fn as_map(&self) -> &BTreeMap { - &self.0 - } -} - -impl IntoIterator for EpochOutput -where - T: frame_system::Config, - T::AccountId: Ord, -{ - type Item = (T::AccountId, EpochTerms); - type IntoIter = IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -#[macro_export] -macro_rules! extract_from_sorted_terms { - ($sorted:expr, $field:ident) => {{ - ($sorted) - .iter() - .copied() - .map(|t| t.$field) - .collect::>() - }}; -} - -impl Pallet { - /// Legacy epoch function interface (TODO: Is only used for tests, remove) - pub fn epoch( - netuid: NetUid, - rao_emission: AlphaBalance, - ) -> Vec<(T::AccountId, AlphaBalance, AlphaBalance)> { - // Run mechanism-style epoch - let output = Self::epoch_mechanism(netuid, MechId::MAIN, rao_emission); - - // Persist values in legacy format - Self::persist_mechanism_epoch_terms(netuid, MechId::MAIN, output.as_map()); - Self::persist_netuid_epoch_terms(netuid, output.as_map()); - - // Remap and return - output - .into_iter() - .map(|(hotkey, terms)| (hotkey, terms.server_emission, terms.validator_emission)) - .collect() - } - - /// Legacy epoch_dense function interface (TODO: Is only used for tests, remove) - pub fn epoch_dense( - netuid: NetUid, - rao_emission: AlphaBalance, - ) -> Vec<(T::AccountId, AlphaBalance, AlphaBalance)> { - Self::epoch_dense_mechanism(netuid, MechId::MAIN, rao_emission) - } - - /// Persists per-mechanism epoch output in state - pub fn persist_mechanism_epoch_terms( - netuid: NetUid, - mecid: MechId, - output: &BTreeMap, - ) { - let netuid_index = Self::get_mechanism_storage_index(netuid, mecid); - let mut terms_sorted: sp_std::vec::Vec<&EpochTerms> = output.values().collect(); - terms_sorted.sort_unstable_by_key(|t| t.uid); - - let incentive = extract_from_sorted_terms!(terms_sorted, incentive); - let bonds: Vec> = terms_sorted - .iter() - .cloned() - .map(|t| t.bond.clone()) - .collect::>(); - - // Epoch math stays in raw u16; wrap into PerU16 only at the storage boundary. - let incentive: Vec = incentive.into_iter().map(PerU16::from_parts).collect(); - Incentive::::insert(netuid_index, incentive); - - let server_emission = extract_from_sorted_terms!(terms_sorted, server_emission); - Self::deposit_event(Event::IncentiveAlphaEmittedToMiners { - netuid: netuid_index, - emissions: server_emission, - }); - - bonds - .into_iter() - .enumerate() - .for_each(|(uid_usize, bond_vec)| { - let uid: u16 = uid_usize.try_into().unwrap_or_default(); - Bonds::::insert(netuid_index, uid, bond_vec); - }); - } - - /// Persists per-netuid epoch output in state - pub fn persist_netuid_epoch_terms(netuid: NetUid, output: &BTreeMap) { - let mut terms_sorted: sp_std::vec::Vec<&EpochTerms> = output.values().collect(); - terms_sorted.sort_unstable_by_key(|t| t.uid); - - let active = extract_from_sorted_terms!(terms_sorted, active); - let emission = extract_from_sorted_terms!(terms_sorted, emission); - let consensus = extract_from_sorted_terms!(terms_sorted, consensus); - let dividend = extract_from_sorted_terms!(terms_sorted, dividend); - let validator_trust = extract_from_sorted_terms!(terms_sorted, validator_trust); - let new_validator_permit = extract_from_sorted_terms!(terms_sorted, new_validator_permit); - let stake_weight = extract_from_sorted_terms!(terms_sorted, stake_weight); - - // Epoch math stays in raw u16; wrap into PerU16 only at the storage boundary. - let consensus: Vec = consensus.into_iter().map(PerU16::from_parts).collect(); - let dividend: Vec = dividend.into_iter().map(PerU16::from_parts).collect(); - let validator_trust: Vec = validator_trust - .into_iter() - .map(PerU16::from_parts) - .collect(); - - Active::::insert(netuid, active.clone()); - Emission::::insert(netuid, emission); - Consensus::::insert(netuid, consensus); - Dividends::::insert(netuid, dividend); - ValidatorTrust::::insert(netuid, validator_trust); - ValidatorPermit::::insert(netuid, new_validator_permit); - StakeWeight::::insert(netuid, stake_weight); - } - - /// Calculates reward consensus and returns the emissions for uids/hotkeys in a given `netuid`. - /// (Dense version used only for testing purposes.) - #[allow(clippy::indexing_slicing)] - pub fn epoch_dense_mechanism( - netuid: NetUid, - mecid: MechId, - rao_emission: AlphaBalance, - ) -> Vec<(T::AccountId, AlphaBalance, AlphaBalance)> { - // Calculate netuid storage index - let netuid_index = Self::get_mechanism_storage_index(netuid, mecid); - - // Get subnetwork size. - let n: u16 = Self::get_subnetwork_n(netuid); - log::trace!("n: {n:?}"); - - // ====================== - // == Active & updated == - // ====================== - - // Get current block. - let current_block: u64 = Self::get_current_block_as_u64(); - log::trace!("current_block: {current_block:?}"); - - // Get tempo. - let tempo: u64 = Self::get_tempo(netuid).into(); - log::trace!("tempo: {tempo:?}"); - - // Get activity cutoff. - let activity_cutoff: u64 = Self::get_activity_cutoff_blocks(netuid); - log::trace!("activity_cutoff: {activity_cutoff:?}"); - - // Last update vector. - let last_update: Vec = Self::get_last_update(netuid_index); - log::trace!("Last update: {:?}", &last_update); - - // Inactive mask. - let inactive: Vec = last_update - .iter() - .map(|updated| updated.saturating_add(activity_cutoff) < current_block) - .collect(); - log::trace!("Inactive: {:?}", inactive.clone()); - - // Logical negation of inactive. - let active: Vec = inactive.iter().map(|&b| !b).collect(); - - // Block at registration vector (block when each neuron was most recently registered). - let block_at_registration: Vec = Self::get_block_at_registration(netuid); - log::trace!("Block at registration: {:?}", &block_at_registration); - - // Outdated matrix, outdated_ij=True if i has last updated (weights) after j has last registered. - let outdated: Vec> = last_update - .iter() - .map(|updated| { - block_at_registration - .iter() - .map(|registered| updated <= registered) - .collect() - }) - .collect(); - log::trace!("Outdated: {:?}", &outdated); - - // Recently registered matrix, recently_ij=True if last_tempo was *before* j was last registered. - // Mask if: the last tempo block happened *before* the registration block - // ==> last_tempo <= registered - // For dynamic tempo - we pick previous-successful-epoch block: `LastMechansimStepBlock + 1` - let lms = LastMechansimStepBlock::::get(netuid); - let last_tempo: u64 = if lms == 0 { - current_block.saturating_sub(tempo) - } else { - lms.saturating_add(1) - }; - let recently_registered: Vec = block_at_registration - .iter() - .map(|registered| last_tempo <= *registered) - .collect(); - log::trace!("Recently registered: {:?}", &recently_registered); - - // =========== - // == Stake == - // =========== - - let hotkeys: Vec<(u16, T::AccountId)> = - as IterableStorageDoubleMap>::iter_prefix(netuid) - .collect(); - log::trace!("hotkeys: {:?}", &hotkeys); - - // Access network stake as normalized vector. - let (total_stake, _alpha_stake, _tao_stake): (Vec, Vec, Vec) = - Self::get_stake_weights_for_network(netuid); - - // Get the minimum stake required. - let min_stake = Self::get_stake_threshold(); - - // Set stake of validators that doesn't meet the staking threshold to 0 as filter. - let mut filtered_stake: Vec = total_stake - .iter() - .map(|&s| { - if fixed64_to_u64(s) < min_stake { - return I64F64::from(0); - } - s - }) - .collect(); - log::debug!("Filtered stake: {:?}", &filtered_stake); - - inplace_normalize_64(&mut filtered_stake); - let stake: Vec = vec_fixed64_to_fixed32(filtered_stake); - log::trace!("S: {:?}", &stake); - - // ======================= - // == Validator permits == - // ======================= - - // Get validator permits. - let validator_permits: Vec = Self::get_validator_permit(netuid); - log::trace!("validator_permits: {validator_permits:?}"); - - // Logical negation of validator_permits. - let validator_forbids: Vec = validator_permits.iter().map(|&b| !b).collect(); - - // Get max allowed validators. - let max_allowed_validators: u16 = Self::get_max_allowed_validators(netuid); - log::trace!("max_allowed_validators: {max_allowed_validators:?}"); - - // Get new validator permits. - let new_validator_permits: Vec = - is_topk_nonzero(&stake, max_allowed_validators as usize); - log::trace!("new_validator_permits: {new_validator_permits:?}"); - - // ================== - // == Active Stake == - // ================== - - let mut active_stake: Vec = stake.clone(); - - // Remove inactive stake. - inplace_mask_vector(&inactive, &mut active_stake); - - // Remove non-validator stake. - inplace_mask_vector(&validator_forbids, &mut active_stake); - - // Normalize active stake. - inplace_normalize(&mut active_stake); - log::trace!("S: {:?}", &active_stake); - - // ============= - // == Weights == - // ============= - - // Get owner uid. - let owner_uid: Option = Self::get_owner_uid(netuid); - - // Access network weights row unnormalized. - let mut weights: Vec> = Self::get_weights(netuid_index); - log::trace!("W: {:?}", &weights); - - // Mask weights that are not from permitted validators. - inplace_mask_rows(&validator_forbids, &mut weights); - log::trace!("W (permit): {:?}", &weights); - - // Remove self-weight by masking diagonal; keep owner_uid self-weight. - if let Some(owner_uid) = owner_uid { - inplace_mask_diag_except_index(&mut weights, owner_uid); - } else { - inplace_mask_diag(&mut weights); - } - - inplace_mask_diag(&mut weights); - log::trace!("W (permit+diag): {:?}", &weights); - - // Mask outdated weights: remove weights referring to deregistered neurons. - inplace_mask_matrix(&outdated, &mut weights); - log::trace!("W (permit+diag+outdate): {:?}", &weights); - - // Normalize remaining weights. - inplace_row_normalize(&mut weights); - log::trace!("W (mask+norm): {:?}", &weights); - - // ================================ - // == Consensus, Validator Trust == - // ================================ - - // Consensus majority ratio, e.g. 51%. - let kappa: I32F32 = Self::get_float_kappa(netuid); - // Calculate consensus as stake-weighted median of weights. - let consensus: Vec = weighted_median_col(&active_stake, &weights, kappa); - // Clip weights at majority consensus. - let mut clipped_weights: Vec> = weights.clone(); - inplace_col_clip(&mut clipped_weights, &consensus); - // Calculate validator trust as sum of clipped weights set by validator. - let validator_trust: Vec = row_sum(&clipped_weights); - - // ==================================== - // == Ranks, Server Trust, Incentive == - // ==================================== - - // Compute ranks: r_j = SUM(i) w_ij * s_i - let mut ranks: Vec = matmul(&clipped_weights, &active_stake); - - inplace_normalize(&mut ranks); - let incentive: Vec = ranks.clone(); - log::trace!("I: {:?}", &incentive); - - // ========================= - // == Bonds and Dividends == - // ========================= - - // Get validator bonds penalty in [0, 1]. - let bonds_penalty: I32F32 = Self::get_float_bonds_penalty(netuid); - // Calculate weights for bonds, apply bonds penalty to weights. - // bonds_penalty = 0: weights_for_bonds = weights.clone() - // bonds_penalty = 1: weights_for_bonds = clipped_weights.clone() - let weights_for_bonds: Vec> = - interpolate(&weights, &clipped_weights, bonds_penalty); - - let mut dividends: Vec; - let mut ema_bonds: Vec>; - if Yuma3On::::get(netuid) { - // Access network bonds. - let mut bonds: Vec> = Self::get_bonds_fixed_proportion(netuid_index); - inplace_mask_cols(&recently_registered, &mut bonds); // mask outdated bonds - log::trace!("B: {:?}", &bonds); - - // Compute the Exponential Moving Average (EMA) of bonds. - ema_bonds = Self::compute_bonds(netuid, &weights_for_bonds, &bonds, &consensus); - log::trace!("emaB: {:?}", &ema_bonds); - - // Normalize EMA bonds. - let mut ema_bonds_norm = ema_bonds.clone(); - inplace_col_normalize(&mut ema_bonds_norm); - log::trace!("emaB norm: {:?}", &ema_bonds_norm); - - // # === Dividend Calculation=== - let total_bonds_per_validator: Vec = - row_sum(&mat_vec_mul(&ema_bonds_norm, &incentive)); - log::trace!( - "total_bonds_per_validator: {:?}", - &total_bonds_per_validator - ); - - dividends = vec_mul(&total_bonds_per_validator, &active_stake); - inplace_normalize(&mut dividends); - log::trace!("D: {:?}", ÷nds); - } else { - // original Yuma - liquid alpha disabled - // Access network bonds. - let mut bonds: Vec> = Self::get_bonds(netuid_index); - // Remove bonds referring to neurons that have registered since last tempo. - inplace_mask_cols(&recently_registered, &mut bonds); // mask recently registered bonds - inplace_col_normalize(&mut bonds); // sum_i b_ij = 1 - log::trace!("B: {:?}", &bonds); - - // Compute bonds delta column normalized. - let mut bonds_delta: Vec> = row_hadamard(&weights_for_bonds, &active_stake); // ΔB = W◦S - inplace_col_normalize(&mut bonds_delta); // sum_i b_ij = 1 - log::trace!("ΔB: {:?}", &bonds_delta); - - // Compute the Exponential Moving Average (EMA) of bonds. - ema_bonds = Self::compute_ema_bonds_normal(&bonds_delta, &bonds, netuid); - inplace_col_normalize(&mut ema_bonds); // sum_i b_ij = 1 - log::trace!("emaB: {:?}", &ema_bonds); - - // Compute dividends: d_i = SUM(j) b_ij * inc_j - dividends = matmul_transpose(&ema_bonds, &incentive); - inplace_normalize(&mut dividends); - log::trace!("Dividends: {:?}", ÷nds); - - // Column max-upscale EMA bonds for storage: max_i w_ij = 1. - inplace_col_max_upscale(&mut ema_bonds); - } - - // ================================= - // == Emission and Pruning scores == - // ================================= - - // Compute emission scores. - - // Compute normalized emission scores. range: I32F32(0, 1) - // Compute normalized emission scores. range: I32F32(0, 1) - let combined_emission: Vec = incentive - .iter() - .zip(dividends.clone()) - .map(|(ii, di)| ii.saturating_add(di)) - .collect(); - let emission_sum: I32F32 = combined_emission.iter().sum(); - - let mut normalized_server_emission: Vec = incentive.clone(); // Servers get incentive. - let mut normalized_validator_emission: Vec = dividends.clone(); // Validators get dividends. - let mut normalized_combined_emission: Vec = combined_emission.clone(); - // Normalize on the sum of incentive + dividends. - inplace_normalize_using_sum(&mut normalized_server_emission, emission_sum); - inplace_normalize_using_sum(&mut normalized_validator_emission, emission_sum); - inplace_normalize(&mut normalized_combined_emission); - - // If emission is zero, replace emission with normalized stake. - if emission_sum == I32F32::from(0) { - // no weights set | outdated weights | self_weights - if is_zero(&active_stake) { - // no active stake - normalized_validator_emission.clone_from(&stake); // do not mask inactive, assumes stake is normalized - normalized_combined_emission.clone_from(&stake); - } else { - normalized_validator_emission.clone_from(&active_stake); // emission proportional to inactive-masked normalized stake - normalized_combined_emission.clone_from(&active_stake); - } - } - - // Compute rao based emission scores. range: I96F32(0, rao_emission) - let float_rao_emission: I96F32 = I96F32::saturating_from_num(rao_emission); - - let server_emission: Vec = normalized_server_emission - .iter() - .map(|se: &I32F32| I96F32::saturating_from_num(*se).saturating_mul(float_rao_emission)) - .collect(); - let server_emission: Vec = server_emission - .iter() - .map(|e: &I96F32| e.saturating_to_num::().into()) - .collect(); - - let validator_emission: Vec = normalized_validator_emission - .iter() - .map(|ve: &I32F32| I96F32::saturating_from_num(*ve).saturating_mul(float_rao_emission)) - .collect(); - let validator_emission: Vec = validator_emission - .iter() - .map(|e: &I96F32| e.saturating_to_num::().into()) - .collect(); - - // Used only to track combined emission in the storage. - let combined_emission: Vec = normalized_combined_emission - .iter() - .map(|ce: &I32F32| I96F32::saturating_from_num(*ce).saturating_mul(float_rao_emission)) - .collect(); - let combined_emission: Vec = combined_emission - .iter() - .map(|e: &I96F32| AlphaBalance::from(e.saturating_to_num::())) - .collect(); - - log::trace!("nSE: {:?}", &normalized_server_emission); - log::trace!("SE: {:?}", &server_emission); - log::trace!("nVE: {:?}", &normalized_validator_emission); - log::trace!("VE: {:?}", &validator_emission); - log::trace!("nCE: {:?}", &normalized_combined_emission); - log::trace!("CE: {:?}", &combined_emission); - - // =================== - // == Value storage == - // =================== - let cloned_emission = combined_emission.clone(); - let cloned_stake_weight: Vec = stake - .iter() - .map(|xi| fixed_proportion_to_u16(*xi)) - .collect::>(); - let cloned_consensus: Vec = consensus - .iter() - .map(|xi| fixed_proportion_to_u16(*xi)) - .collect::>(); - let cloned_incentive: Vec = incentive - .iter() - .map(|xi| fixed_proportion_to_u16(*xi)) - .collect::>(); - let cloned_dividends: Vec = dividends - .iter() - .map(|xi| fixed_proportion_to_u16(*xi)) - .collect::>(); - let cloned_validator_trust: Vec = validator_trust - .iter() - .map(|xi| fixed_proportion_to_u16(*xi)) - .collect::>(); - StakeWeight::::insert(netuid, cloned_stake_weight.clone()); - Active::::insert(netuid, active.clone()); - Emission::::insert(netuid, cloned_emission); - // Epoch math stays in raw u16; wrap into PerU16 only at the storage boundary. - Consensus::::insert( - netuid, - cloned_consensus - .into_iter() - .map(PerU16::from_parts) - .collect::>(), - ); - Incentive::::insert( - NetUidStorageIndex::from(netuid), - cloned_incentive - .into_iter() - .map(PerU16::from_parts) - .collect::>(), - ); - Dividends::::insert( - netuid, - cloned_dividends - .into_iter() - .map(PerU16::from_parts) - .collect::>(), - ); - ValidatorTrust::::insert( - netuid, - cloned_validator_trust - .into_iter() - .map(PerU16::from_parts) - .collect::>(), - ); - ValidatorPermit::::insert(netuid, new_validator_permits.clone()); - - new_validator_permits - .iter() - .zip(validator_permits) - .zip(ema_bonds) - .enumerate() - .for_each(|(i, ((new_permit, validator_permit), ema_bond))| { - // Set bonds only if uid retains validator permit, otherwise clear bonds. - if *new_permit { - let new_bonds_row: Vec<(u16, u16)> = (0..n) - .zip(vec_fixed_proportions_to_u16(ema_bond.clone())) - .collect(); - Bonds::::insert(netuid_index, i as u16, new_bonds_row); - } else if validator_permit { - // Only overwrite the intersection. - let new_empty_bonds_row: Vec<(u16, u16)> = vec![]; - Bonds::::insert(netuid_index, i as u16, new_empty_bonds_row); - } - }); - - hotkeys - .into_iter() - .map(|(uid_i, hotkey)| { - ( - hotkey, - server_emission[uid_i as usize], - validator_emission[uid_i as usize], - ) - }) - .collect() - } - - /// Calculates reward consensus values, then updates rank, trust, consensus, incentive, dividend, pruning_score, emission and bonds, and - /// returns the emissions for uids/hotkeys in a given `netuid`. - /// - /// # Arguments - /// * `netuid`: The network to distribute the emission onto. - /// - /// * `rao_emission`: The total emission for the epoch. - /// - /// * `debug`: Print debugging outputs. - /// - pub fn epoch_mechanism( - netuid: NetUid, - mecid: MechId, - rao_emission: AlphaBalance, - ) -> EpochOutput { - // Calculate netuid storage index - let netuid_index = Self::get_mechanism_storage_index(netuid, mecid); - - // Initialize output keys (neuron hotkeys) and UIDs - let mut terms_map: BTreeMap = Keys::::iter_prefix(netuid) - .map(|(uid, hotkey)| { - ( - hotkey, - EpochTerms { - uid: uid as usize, - ..Default::default() - }, - ) - }) - .collect(); - - // Get subnetwork size. - let n = Self::get_subnetwork_n(netuid); - log::trace!("Number of Neurons in Network: {n:?}"); - - // ====================== - // == Active & updated == - // ====================== - - // Get current block. - let current_block: u64 = Self::get_current_block_as_u64(); - log::trace!("current_block: {current_block:?}"); - - // Get tempo. - let tempo: u64 = Self::get_tempo(netuid).into(); - log::trace!("tempo:\n{tempo:?}\n"); - - // Get activity cutoff. - let activity_cutoff: u64 = Self::get_activity_cutoff_blocks(netuid); - log::trace!("activity_cutoff: {activity_cutoff:?}"); - - // Last update vector. - let last_update: Vec = Self::get_last_update(netuid_index); - log::trace!("Last update: {:?}", &last_update); - - // Inactive mask. - let inactive: Vec = last_update - .iter() - .map(|updated| updated.saturating_add(activity_cutoff) < current_block) - .collect(); - log::debug!("Inactive: {:?}", inactive.clone()); - - // Logical negation of inactive. - let active: Vec = inactive.iter().map(|&b| !b).collect(); - - // Block at registration vector (block when each neuron was most recently registered). - let block_at_registration: Vec = Self::get_block_at_registration(netuid); - log::trace!("Block at registration: {:?}", &block_at_registration); - - // =========== - // == Stake == - // =========== - - // Access network stake as normalized vector. - let (total_stake, _alpha_stake, _tao_stake): (Vec, Vec, Vec) = - Self::get_stake_weights_for_network(netuid); - - // Get the minimum stake required. - let min_stake = Self::get_stake_threshold(); - - // Get owner uid. - let owner_uid: Option = Self::get_owner_uid(netuid); - - // Set stake of validators that doesn't meet the staking threshold to 0 as filter. - let mut filtered_stake: Vec = total_stake - .iter() - .enumerate() - .map(|(uid, &s)| { - if owner_uid != Some(uid as u16) && fixed64_to_u64(s) < min_stake { - return I64F64::from(0); - } - s - }) - .collect(); - log::debug!("Filtered stake: {:?}", &filtered_stake); - - inplace_normalize_64(&mut filtered_stake); - let stake: Vec = vec_fixed64_to_fixed32(filtered_stake); - log::debug!("Normalised Stake: {:?}", &stake); - - // ======================= - // == Validator permits == - // ======================= - - // Get current validator permits. - let mut validator_permits: Vec = Self::get_validator_permit(netuid); - if let Some(owner_uid) = owner_uid - && let Some(owner_permit) = validator_permits.get_mut(owner_uid as usize) - { - *owner_permit = true; - } - log::trace!("validator_permits: {validator_permits:?}"); - - // Logical negation of validator_permits. - let validator_forbids: Vec = validator_permits.iter().map(|&b| !b).collect(); - - // Get max allowed validators. - let max_allowed_validators: u16 = Self::get_max_allowed_validators(netuid); - log::trace!("max_allowed_validators: {max_allowed_validators:?}"); - - // Get new validator permits. - let mut new_validator_permits: Vec = - is_topk_nonzero(&stake, max_allowed_validators as usize); - if let Some(owner_uid) = owner_uid - && let Some(owner_permit) = new_validator_permits.get_mut(owner_uid as usize) - { - *owner_permit = true; - } - log::trace!("new_validator_permits: {new_validator_permits:?}"); - - // ================== - // == Active Stake == - // ================== - - let mut active_stake: Vec = stake.clone(); - - // Remove inactive stake. - inplace_mask_vector(&inactive, &mut active_stake); - - // Remove non-validator stake. - inplace_mask_vector(&validator_forbids, &mut active_stake); - - // Normalize active stake. - inplace_normalize(&mut active_stake); - log::trace!("Active Stake: {:?}", &active_stake); - - // ============= - // == Weights == - // ============= - - // Access network weights row unnormalized. - let mut weights: Vec> = Self::get_weights_sparse(netuid_index); - log::trace!("Weights: {:?}", &weights); - - // Mask weights that are not from permitted validators. - weights = mask_rows_sparse(&validator_forbids, &weights); - log::trace!("Weights (permit): {:?}", &weights); - - // Remove self-weight by masking diagonal; keep owner_uid self-weight. - if let Some(owner_uid) = owner_uid { - weights = mask_diag_sparse_except_index(&weights, owner_uid); - } else { - weights = mask_diag_sparse(&weights); - } - log::trace!("Weights (permit+diag): {:?}", &weights); - - // Remove weights referring to deregistered neurons. - weights = vec_mask_sparse_matrix( - &weights, - &last_update, - &block_at_registration, - &|updated, registered| updated <= registered, - ); - log::trace!("Weights (permit+diag+outdate): {:?}", &weights); - - if Self::get_commit_reveal_weights_enabled(netuid) { - let mut commit_blocks: Vec = vec![u64::MAX; n as usize]; // MAX ⇒ “no active commit” - - // helper: hotkey → uid - let uid_of = |acct: &T::AccountId| terms_map.get(acct).map(|t| t.uid); - - // ---------- v2 ------------------------------------------------------ - // `WeightCommits` tuple: (hash, commit_epoch, commit_block, _). - // Expiry keys off `commit_epoch`; the column mask compares the absolute - // `commit_block` against `block_at_registration` (both block numbers). - for (who, q) in WeightCommits::::iter_prefix(netuid_index) { - for (_, commit_epoch, commit_block, _) in q.iter() { - if !Self::is_commit_expired(netuid, *commit_epoch) { - if let Some(cell) = uid_of(&who).and_then(|i| commit_blocks.get_mut(i)) { - *cell = (*cell).min(*commit_block); - } - break; // earliest active found - } - } - } - - // ---------- v4 ------------------------------------------------------ - // `TimelockedWeightCommits` is keyed by `commit_epoch`; the value tuple - // carries the absolute `commit_block` in field 1. - for (commit_epoch, q) in TimelockedWeightCommits::::iter_prefix(netuid_index) { - if Self::is_commit_expired(netuid, commit_epoch) { - continue; - } - for (who, commit_block, ..) in q.iter() { - if let Some(cell) = uid_of(who).and_then(|i| commit_blocks.get_mut(i)) { - *cell = (*cell).min(*commit_block); - } - } - } - - weights = vec_mask_sparse_matrix( - &weights, - &commit_blocks, - &block_at_registration, - &|cb, reg| cb < reg, - ); - - log::trace!( - "Commit-reveal column mask applied ({} masked rows)", - commit_blocks.iter().filter(|&&cb| cb != u64::MAX).count() - ); - } - - // Normalize remaining weights. - inplace_row_normalize_sparse(&mut weights); - log::trace!("Weights (mask+norm): {:?}", &weights); - - // ================================ - // == Consensus, Validator Trust == - // ================================ - - // Consensus majority ratio, e.g. 51%. - let kappa: I32F32 = Self::get_float_kappa(netuid); - // Calculate consensus as stake-weighted median of weights. - let consensus: Vec = weighted_median_col_sparse(&active_stake, &weights, n, kappa); - log::trace!("Consensus: {:?}", &consensus); - - // Clip weights at majority consensus. - let clipped_weights: Vec> = col_clip_sparse(&weights, &consensus); - log::trace!("Clipped Weights: {:?}", &clipped_weights); - - // Calculate validator trust as sum of clipped weights set by validator. - let validator_trust: Vec = row_sum_sparse(&clipped_weights); - log::trace!("Validator Trust: {:?}", &validator_trust); - - // ============================= - // == Ranks, Trust, Incentive == - // ============================= - - // Compute ranks: r_j = SUM(i) w_ij * s_i. - let mut ranks: Vec = matmul_sparse(&clipped_weights, &active_stake, n); - - inplace_normalize(&mut ranks); // range: I32F32(0, 1) - let incentive: Vec = ranks.clone(); - log::trace!("Incentive (=Rank): {:?}", &incentive); - - // ========================= - // == Bonds and Dividends == - // ========================= - - // Get validator bonds penalty in [0, 1]. - let bonds_penalty: I32F32 = Self::get_float_bonds_penalty(netuid); - // Calculate weights for bonds, apply bonds penalty to weights. - // bonds_penalty = 0: weights_for_bonds = weights.clone() - // bonds_penalty = 1: weights_for_bonds = clipped_weights.clone() - let weights_for_bonds: Vec> = - interpolate_sparse(&weights, &clipped_weights, n, bonds_penalty); - - let mut dividends: Vec; - let mut ema_bonds: Vec>; - if Yuma3On::::get(netuid) { - // Access network bonds. - let mut bonds = Self::get_bonds_sparse_fixed_proportion(netuid_index); - log::trace!("Bonds: {:?}", &bonds); - - // Remove bonds referring to neurons that have registered since last tempo. - // Mask if: the last tempo block happened *before* the registration block - // ==> last_tempo <= registered - // For dynamic tempo - we pick previous-successful-epoch block: `LastMechansimStepBlock + 1` - let lms = LastMechansimStepBlock::::get(netuid); - let last_tempo: u64 = if lms == 0 { - current_block.saturating_sub(tempo) - } else { - lms.saturating_add(1) - }; - bonds = scalar_vec_mask_sparse_matrix( - &bonds, - last_tempo, - &block_at_registration, - &|last_tempo, registered| last_tempo <= registered, - ); - log::trace!("Bonds: (mask) {:?}", &bonds); - - // Compute the Exponential Moving Average (EMA) of bonds. - log::trace!("weights_for_bonds: {:?}", &weights_for_bonds); - ema_bonds = - Self::compute_bonds_sparse(netuid_index, &weights_for_bonds, &bonds, &consensus); - log::trace!("emaB: {:?}", &ema_bonds); - - // Normalize EMA bonds. - let mut ema_bonds_norm = ema_bonds.clone(); - inplace_col_normalize_sparse(&mut ema_bonds_norm, n); // sum_i b_ij = 1 - log::trace!("emaB norm: {:?}", &ema_bonds_norm); - - // # === Dividend Calculation=== - let total_bonds_per_validator: Vec = - row_sum_sparse(&mat_vec_mul_sparse(&ema_bonds_norm, &incentive)); - log::trace!( - "total_bonds_per_validator: {:?}", - &total_bonds_per_validator - ); - - dividends = vec_mul(&total_bonds_per_validator, &active_stake); - inplace_normalize(&mut dividends); - log::trace!("Dividends: {:?}", ÷nds); - } else { - // original Yuma - liquid alpha disabled - // Access network bonds. - let mut bonds: Vec> = Self::get_bonds_sparse(netuid_index); - log::trace!("B: {:?}", &bonds); - - // Remove bonds referring to neurons that have registered since last tempo. - // Mask if: the last tempo block happened *before* the registration block - // ==> last_tempo <= registered - // For dynamic tempo - we pick previous-successful-epoch block: `LastMechansimStepBlock + 1` - let lms = LastMechansimStepBlock::::get(netuid); - let last_tempo: u64 = if lms == 0 { - current_block.saturating_sub(tempo) - } else { - lms.saturating_add(1) - }; - bonds = scalar_vec_mask_sparse_matrix( - &bonds, - last_tempo, - &block_at_registration, - &|last_tempo, registered| last_tempo <= registered, - ); - log::trace!("B (outdatedmask): {:?}", &bonds); - - // Normalize remaining bonds: sum_i b_ij = 1. - inplace_col_normalize_sparse(&mut bonds, n); - log::trace!("B (mask+norm): {:?}", &bonds); - - // Compute bonds delta column normalized. - let mut bonds_delta: Vec> = - row_hadamard_sparse(&weights_for_bonds, &active_stake); // ΔB = W◦S (outdated W masked) - log::trace!("ΔB: {:?}", &bonds_delta); - - // Normalize bonds delta. - inplace_col_normalize_sparse(&mut bonds_delta, n); // sum_i b_ij = 1 - log::trace!("ΔB (norm): {:?}", &bonds_delta); - - // Compute the Exponential Moving Average (EMA) of bonds. - ema_bonds = Self::compute_ema_bonds_normal_sparse(&bonds_delta, &bonds, netuid_index); - // Normalize EMA bonds. - inplace_col_normalize_sparse(&mut ema_bonds, n); // sum_i b_ij = 1 - log::trace!("Exponential Moving Average Bonds: {:?}", &ema_bonds); - - // Compute dividends: d_i = SUM(j) b_ij * inc_j. - // range: I32F32(0, 1) - dividends = matmul_transpose_sparse(&ema_bonds, &incentive); - inplace_normalize(&mut dividends); - log::trace!("Dividends: {:?}", ÷nds); - - // Column max-upscale EMA bonds for storage: max_i w_ij = 1. - inplace_col_max_upscale_sparse(&mut ema_bonds, n); - } - - // ================================= - // == Emission and Pruning scores == - // ================================= - - // Compute normalized emission scores. range: I32F32(0, 1) - let combined_emission: Vec = incentive - .iter() - .zip(dividends.clone()) - .map(|(ii, di)| ii.saturating_add(di)) - .collect(); - let emission_sum: I32F32 = combined_emission.iter().sum(); - - let mut normalized_server_emission: Vec = incentive.clone(); // Servers get incentive. - let mut normalized_validator_emission: Vec = dividends.clone(); // Validators get dividends. - let mut normalized_combined_emission: Vec = combined_emission.clone(); - // Normalize on the sum of incentive + dividends. - inplace_normalize_using_sum(&mut normalized_server_emission, emission_sum); - inplace_normalize_using_sum(&mut normalized_validator_emission, emission_sum); - inplace_normalize(&mut normalized_combined_emission); - - // If emission is zero, replace emission with normalized stake. - if emission_sum == I32F32::from(0) { - // no weights set | outdated weights | self_weights - if is_zero(&active_stake) { - // no active stake - normalized_validator_emission.clone_from(&stake); // do not mask inactive, assumes stake is normalized - normalized_combined_emission.clone_from(&stake); - } else { - normalized_validator_emission.clone_from(&active_stake); // emission proportional to inactive-masked normalized stake - normalized_combined_emission.clone_from(&active_stake); - } - } - - // Compute rao based emission scores. range: I96F32(0, rao_emission) - let float_rao_emission: I96F32 = I96F32::saturating_from_num(rao_emission); - - let server_emission: Vec = normalized_server_emission - .iter() - .map(|se: &I32F32| I96F32::saturating_from_num(*se).saturating_mul(float_rao_emission)) - .collect(); - let server_emission: Vec = server_emission - .iter() - .map(|e: &I96F32| e.saturating_to_num::().into()) - .collect(); - - let validator_emission: Vec = normalized_validator_emission - .iter() - .map(|ve: &I32F32| I96F32::saturating_from_num(*ve).saturating_mul(float_rao_emission)) - .collect(); - let validator_emission: Vec = validator_emission - .iter() - .map(|e: &I96F32| e.saturating_to_num::().into()) - .collect(); - - // Only used to track emission in storage. - let combined_emission: Vec = normalized_combined_emission - .iter() - .map(|ce: &I32F32| I96F32::saturating_from_num(*ce).saturating_mul(float_rao_emission)) - .collect(); - let combined_emission: Vec = combined_emission - .iter() - .map(|e: &I96F32| AlphaBalance::from(e.saturating_to_num::())) - .collect(); - - log::trace!( - "Normalized Server Emission: {:?}", - &normalized_server_emission - ); - log::trace!("Server Emission: {:?}", &server_emission); - log::trace!( - "Normalized Validator Emission: {:?}", - &normalized_validator_emission - ); - log::trace!("Validator Emission: {:?}", &validator_emission); - log::trace!( - "Normalized Combined Emission: {:?}", - &normalized_combined_emission - ); - log::trace!("Combined Emission: {:?}", &combined_emission); - - // =========================== - // == Populate epoch output == - // =========================== - let cloned_stake_weight: Vec = stake - .iter() - .map(|xi| fixed_proportion_to_u16(*xi)) - .collect::>(); - let cloned_emission = combined_emission.clone(); - let cloned_consensus: Vec = consensus - .iter() - .map(|xi| fixed_proportion_to_u16(*xi)) - .collect::>(); - let cloned_incentive: Vec = incentive - .iter() - .map(|xi| fixed_proportion_to_u16(*xi)) - .collect::>(); - let cloned_dividends: Vec = dividends - .iter() - .map(|xi| fixed_proportion_to_u16(*xi)) - .collect::>(); - let cloned_validator_trust: Vec = validator_trust - .iter() - .map(|xi| fixed_proportion_to_u16(*xi)) - .collect::>(); - let raw_stake: Vec = total_stake - .iter() - .map(|s| s.saturating_to_num::()) - .collect::>(); - - for (_hotkey, terms) in terms_map.iter_mut() { - terms.dividend = cloned_dividends.get(terms.uid).copied().unwrap_or_default(); - terms.incentive = cloned_incentive.get(terms.uid).copied().unwrap_or_default(); - terms.validator_emission = validator_emission - .get(terms.uid) - .copied() - .unwrap_or_default(); - terms.server_emission = server_emission.get(terms.uid).copied().unwrap_or_default(); - terms.stake_weight = cloned_stake_weight - .get(terms.uid) - .copied() - .unwrap_or_default(); - terms.active = active.get(terms.uid).copied().unwrap_or_default(); - terms.emission = cloned_emission.get(terms.uid).copied().unwrap_or_default(); - terms.consensus = cloned_consensus.get(terms.uid).copied().unwrap_or_default(); - terms.validator_trust = cloned_validator_trust - .get(terms.uid) - .copied() - .unwrap_or_default(); - terms.new_validator_permit = new_validator_permits - .get(terms.uid) - .copied() - .unwrap_or_default(); - terms.stake = raw_stake.get(terms.uid).copied().unwrap_or_default().into(); - let old_validator_permit = validator_permits - .get(terms.uid) - .copied() - .unwrap_or_default(); - - // Bonds - if terms.new_validator_permit { - let ema_bond = ema_bonds.get(terms.uid).cloned().unwrap_or_default(); - terms.bond = ema_bond - .iter() - .map(|(j, value)| (*j, fixed_proportion_to_u16(*value))) - .collect(); - } else if old_validator_permit { - // Only overwrite the intersection. - terms.bond = vec![]; - } - } - - EpochOutput(terms_map) - } - - pub fn get_float_rho(netuid: NetUid) -> I32F32 { - I32F32::saturating_from_num(Self::get_rho(netuid)) - } - pub fn get_float_kappa(netuid: NetUid) -> I32F32 { - I32F32::saturating_from_num(Self::get_kappa(netuid)) - .safe_div(I32F32::saturating_from_num(u16::MAX)) - } - pub fn get_float_bonds_penalty(netuid: NetUid) -> I32F32 { - I32F32::saturating_from_num(Self::get_bonds_penalty(netuid)) - .safe_div(I32F32::saturating_from_num(u16::MAX)) - } - - pub fn get_block_at_registration(netuid: NetUid) -> Vec { - let n = Self::get_subnetwork_n(netuid); - let block_at_registration: Vec = (0..n) - .map(|neuron_uid| { - if Keys::::contains_key(netuid, neuron_uid) { - Self::get_neuron_block_at_registration(netuid, neuron_uid) - } else { - 0 - } - }) - .collect(); - block_at_registration - } - - /// Output unnormalized sparse weights, input weights are assumed to be row max-upscaled in u16. - pub fn get_weights_sparse(netuid_index: NetUidStorageIndex) -> Vec> { - let (netuid, _) = Self::get_netuid_and_subid(netuid_index).unwrap_or_default(); - let n = Self::get_subnetwork_n(netuid) as usize; - let mut weights: Vec> = vec![vec![]; n]; - for (uid_i, weights_i) in - Weights::::iter_prefix(netuid_index).filter(|(uid_i, _)| *uid_i < n as u16) - { - for (uid_j, weight_ij) in weights_i.iter().filter(|(uid_j, _)| *uid_j < n as u16) { - if let Some(row) = weights.get_mut(uid_i as usize) { - row.push((*uid_j, I32F32::saturating_from_num(*weight_ij))); - } else { - log::error!("math error: uid_i {uid_i:?} is filtered to be less than n"); - } - } - } - weights - } - - /// Output unnormalized weights in [n, n] matrix, input weights are assumed to be row max-upscaled in u16. - pub fn get_weights(netuid_index: NetUidStorageIndex) -> Vec> { - let (netuid, _) = Self::get_netuid_and_subid(netuid_index).unwrap_or_default(); - let n = Self::get_subnetwork_n(netuid) as usize; - let mut weights: Vec> = vec![vec![I32F32::saturating_from_num(0.0); n]; n]; - for (uid_i, weights_vec) in - Weights::::iter_prefix(netuid_index).filter(|(uid_i, _)| *uid_i < n as u16) - { - for (uid_j, weight_ij) in weights_vec - .into_iter() - .filter(|(uid_j, _)| *uid_j < n as u16) - { - if let Some(cell) = weights - .get_mut(uid_i as usize) - .and_then(|row| row.get_mut(uid_j as usize)) - { - *cell = I32F32::saturating_from_num(weight_ij); - } - } - } - weights - } - - /// Output unnormalized sparse bonds, input bonds are assumed to be column max-upscaled in u16. - pub fn get_bonds_sparse(netuid_index: NetUidStorageIndex) -> Vec> { - let (netuid, _) = Self::get_netuid_and_subid(netuid_index).unwrap_or_default(); - let n = Self::get_subnetwork_n(netuid) as usize; - let mut bonds: Vec> = vec![vec![]; n]; - for (uid_i, bonds_vec) in - Bonds::::iter_prefix(netuid_index).filter(|(uid_i, _)| *uid_i < n as u16) - { - for (uid_j, bonds_ij) in bonds_vec { - if let Some(row) = bonds.get_mut(uid_i as usize) { - row.push((uid_j, u16_to_fixed(bonds_ij))); - } else { - // If the index is unexpectedly out of bounds, skip and log math error - log::error!( - "math error: bonds row index out of bounds (uid_i={uid_i}, n={n}, netuid_index={netuid_index})", - ); - } - } - } - - bonds - } - - /// Output unnormalized bonds in [n, n] matrix, input bonds are assumed to be column max-upscaled in u16. - pub fn get_bonds(netuid_index: NetUidStorageIndex) -> Vec> { - let (netuid, _) = Self::get_netuid_and_subid(netuid_index).unwrap_or_default(); - let n: usize = Self::get_subnetwork_n(netuid) as usize; - let mut bonds: Vec> = vec![vec![I32F32::saturating_from_num(0.0); n]; n]; - for (uid_i, bonds_vec) in - Bonds::::iter_prefix(netuid_index).filter(|(uid_i, _)| *uid_i < n as u16) - { - for (uid_j, bonds_ij) in bonds_vec.into_iter().filter(|(uid_j, _)| *uid_j < n as u16) { - if let Some(row) = bonds.get_mut(uid_i as usize) { - if let Some(cell) = row.get_mut(uid_j as usize) { - *cell = u16_to_fixed(bonds_ij); - } else { - log::error!( - "math error: uid_j index out of bounds (uid_i={uid_i}, uid_j={uid_j}, n={n}, netuid_index={netuid_index})" - ); - } - } else { - log::error!( - "math error: uid_i row index out of bounds (uid_i={uid_i}, n={n}, netuid_index={netuid_index})" - ); - } - } - } - - bonds - } - - pub fn get_bonds_fixed_proportion(netuid: NetUidStorageIndex) -> Vec> { - let mut bonds = Self::get_bonds(netuid); - bonds.iter_mut().for_each(|bonds_row| { - bonds_row - .iter_mut() - .for_each(|bond| *bond = fixed_to_fixed_u16_proportion(*bond)); - }); - bonds - } - - pub fn get_bonds_sparse_fixed_proportion( - netuid: NetUidStorageIndex, - ) -> Vec> { - let mut bonds = Self::get_bonds_sparse(netuid); - bonds.iter_mut().for_each(|bonds_row| { - bonds_row - .iter_mut() - .for_each(|(_, bond)| *bond = fixed_to_fixed_u16_proportion(*bond)); - }); - bonds - } - - /// Compute the Exponential Moving Average (EMA) of bonds using a normal alpha value for a sparse matrix. - /// - /// # Arguments - /// * `bonds_delta`: A vector of bond deltas. - /// * `bonds`: A vector of bonds. - /// * `netuid`: The network ID. - /// - /// # Returns - /// A vector of EMA bonds. - pub fn compute_ema_bonds_normal_sparse( - bonds_delta: &[Vec<(u16, I32F32)>], - bonds: &[Vec<(u16, I32F32)>], - netuid_index: NetUidStorageIndex, - ) -> Vec> { - let (netuid, _) = Self::get_netuid_and_subid(netuid_index).unwrap_or_default(); - - // Retrieve the bonds moving average for the given network ID and scale it down. - let bonds_moving_average: I64F64 = - I64F64::saturating_from_num(Self::get_bonds_moving_average(netuid)) - .safe_div(I64F64::saturating_from_num(1_000_000)); - - // Calculate the alpha value for the EMA calculation. - // Alpha is derived by subtracting the scaled bonds moving average from 1. - let alpha: I32F32 = I32F32::saturating_from_num(1) - .saturating_sub(I32F32::saturating_from_num(bonds_moving_average)); - - // Compute the Exponential Moving Average (EMA) of bonds using the calculated alpha value. - let ema_bonds = mat_ema_sparse(bonds_delta, bonds, alpha); - - // Log the computed EMA bonds for debugging purposes. - log::trace!("Exponential Moving Average Bonds Normal: {ema_bonds:?}"); - - // Return the computed EMA bonds. - ema_bonds - } - - /// Compute the Exponential Moving Average (EMA) of bonds using a normal alpha value. - /// - /// # Arguments - /// * `bonds_delta`: A vector of bond deltas. - /// * `bonds`: A vector of bonds. - /// * `netuid`: The network ID. - /// - /// # Returns - /// A vector of EMA bonds. - pub fn compute_ema_bonds_normal( - bonds_delta: &[Vec], - bonds: &[Vec], - netuid: NetUid, - ) -> Vec> { - // Retrieve the bonds moving average for the given network ID and scale it down. - let bonds_moving_average: I64F64 = - I64F64::saturating_from_num(Self::get_bonds_moving_average(netuid)) - .safe_div(I64F64::saturating_from_num(1_000_000)); - - // Calculate the alpha value for the EMA calculation. - // Alpha is derived by subtracting the scaled bonds moving average from 1. - let alpha: I32F32 = I32F32::saturating_from_num(1) - .saturating_sub(I32F32::saturating_from_num(bonds_moving_average)); - - // Compute the Exponential Moving Average (EMA) of bonds using the calculated alpha value. - let ema_bonds = mat_ema(bonds_delta, bonds, alpha); - - // Log the computed EMA bonds for debugging purposes. - log::trace!("Exponential Moving Average Bonds Normal: {ema_bonds:?}"); - - // Return the computed EMA bonds. - ema_bonds - } - - /// Compute the Exponential Moving Average (EMA) of bonds based on the Liquid Alpha setting - /// - /// # Arguments - /// * `netuid`: The network ID. - /// * `weights`: A vector of weights. - /// * `bonds`: A vector of bonds. - /// * `consensus`: A vector of consensus values. - /// * `active_stake`: A vector of active stake values. - /// - /// # Returns - /// A vector of EMA bonds. - pub fn compute_bonds( - netuid: NetUid, - weights: &[Vec], // weights_for_bonds - bonds: &[Vec], - consensus: &[I32F32], - ) -> Vec> { - // Check if Liquid Alpha is enabled, consensus is not empty, and contains non-zero values. - if LiquidAlphaOn::::get(netuid) - && !consensus.is_empty() - && consensus - .iter() - .any(|&c| c != I32F32::saturating_from_num(0)) - { - // Liquid Alpha is enabled, compute the liquid alphas matrix. - let alphas: Vec> = - Self::compute_liquid_alpha_values(netuid, weights, bonds, consensus); - log::trace!("alphas: {:?}", &alphas); - - // Compute the Exponential Moving Average (EMA) of bonds using the provided clamped alpha values. - mat_ema_alpha(weights, bonds, &alphas) - } else { - // Liquid Alpha is disabled, compute the liquid alpha value. - let alpha: I32F32 = Self::compute_disabled_liquid_alpha(netuid); - - // Compute the Exponential Moving Average (EMA) of bonds using the calculated alpha value. - mat_ema(weights, bonds, alpha) - } - } - - /// Compute the Exponential Moving Average (EMA) of bonds based on the Liquid Alpha setting for a sparse matrix. - /// - /// # Arguments - /// * `netuid`: The network ID. - /// * `weights`: A vector of weights. - /// * `bonds`: A vector of bonds. - /// * `consensus`: A vector of consensus values. - /// * `active_stake`: A vector of active stake values. - /// - /// # Returns - /// A vector of EMA bonds. - pub fn compute_bonds_sparse( - netuid_index: NetUidStorageIndex, - weights: &[Vec<(u16, I32F32)>], - bonds: &[Vec<(u16, I32F32)>], - consensus: &[I32F32], - ) -> Vec> { - let (netuid, _) = Self::get_netuid_and_subid(netuid_index).unwrap_or_default(); - - // Check if Liquid Alpha is enabled, consensus is not empty, and contains non-zero values. - if LiquidAlphaOn::::get(netuid) - && !consensus.is_empty() - && consensus - .iter() - .any(|&c| c != I32F32::saturating_from_num(0)) - { - // Liquid Alpha is enabled, compute the liquid alphas matrix. - let alphas: Vec> = - Self::compute_liquid_alpha_values_sparse(netuid, weights, bonds, consensus); - log::trace!("alphas: {:?}", &alphas); - - // Compute the Exponential Moving Average (EMA) of bonds using the provided clamped alpha values. - mat_ema_alpha_sparse(weights, bonds, &alphas) - } else { - // Liquid Alpha is disabled, compute the liquid alpha value. - let alpha: I32F32 = Self::compute_disabled_liquid_alpha(netuid); - - // Compute the Exponential Moving Average (EMA) of bonds using the calculated alpha value. - mat_ema_sparse(weights, bonds, alpha) - } - } - - /// Compute liquid alphas matrix - /// There is a separate alpha param for each validator-miner binding - /// - /// # Arguments - /// * `netuid`: The network ID. - /// * `weights`: A vector of weights. - /// * `bonds`: A vector of bonds. - /// * `consensus`: A vector of consensus values. - /// - /// # Returns - /// A matrix of alphas - pub fn compute_liquid_alpha_values( - netuid: NetUid, - weights: &[Vec], // current epoch weights - bonds: &[Vec], // previous epoch bonds - consensus: &[I32F32], // previous epoch consensus weights - ) -> Vec> { - let mut alphas = Vec::new(); - - if weights.len() != bonds.len() { - log::error!( - "math error: compute_liquid_alpha_values: weights and bonds have different lengths: {:?} != {:?}", - weights.len(), - bonds.len() - ); - return alphas; - } - - // Get the high and low alpha values for the network. - let alpha_sigmoid_steepness: I32F32 = Self::get_alpha_sigmoid_steepness(netuid); - let (alpha_low, alpha_high): (I32F32, I32F32) = Self::get_alpha_values_32(netuid); - - for (w_row, b_row) in weights.iter().zip(bonds.iter()) { - let mut row_alphas = Vec::new(); - - for ((weight, bond), consensus_val) in - w_row.iter().zip(b_row.iter()).zip(consensus.iter()) - { - let alpha = Self::alpha_sigmoid( - *consensus_val, - *weight, - *bond, - alpha_low, - alpha_high, - alpha_sigmoid_steepness, - ); - row_alphas.push(alpha); - } - alphas.push(row_alphas); - } - alphas - } - - /// Compute liquid alphas sparse matrix - /// There is a separate alpha param for each validator-miner binding - /// - /// # Arguments - /// * `netuid`: The network ID. - /// * `weights`: A vector of weights. - /// * `bonds`: A vector of bonds. - /// * `consensus`: A vector of consensus values. - /// - /// # Returns - /// A dense matrix of alphas - pub fn compute_liquid_alpha_values_sparse( - netuid: NetUid, - weights: &[Vec<(u16, I32F32)>], // current epoch weights - bonds: &[Vec<(u16, I32F32)>], // previous epoch bonds - consensus: &[I32F32], // previous epoch consensus weights - ) -> Vec> { - let mut alphas = Vec::with_capacity(consensus.len()); - - if weights.len() != bonds.len() { - log::error!( - "math error: compute_liquid_alpha_values: weights and bonds have different lengths: {:?} != {:?}", - weights.len(), - bonds.len() - ); - return alphas; - } - - let alpha_sigmoid_steepness: I32F32 = Self::get_alpha_sigmoid_steepness(netuid); - let (alpha_low, alpha_high): (I32F32, I32F32) = Self::get_alpha_values_32(netuid); - - let zero = I32F32::from_num(0.0); - - // iterate over rows - for (w_row, b_row) in weights.iter().zip(bonds.iter()) { - let mut row_alphas = Vec::with_capacity(w_row.len()); - let mut w_iter = w_row.iter().peekable(); - let mut b_iter = b_row.iter().peekable(); - for (j_pos, consensus_val) in consensus.iter().enumerate() { - let j = j_pos as u16; - - let mut weight = zero; - while let Some(&&(i, val)) = w_iter.peek() { - if i < j { - w_iter.next(); - } else { - if i == j { - weight = val; - } - break; - } - } - - let mut bond = zero; - while let Some(&&(i, val)) = b_iter.peek() { - if i < j { - b_iter.next(); - } else { - if i == j { - bond = val; - } - break; - } - } - - let alpha = Self::alpha_sigmoid( - *consensus_val, - weight, - bond, - alpha_low, - alpha_high, - alpha_sigmoid_steepness, - ); - row_alphas.push(alpha); - } - alphas.push(row_alphas); - } - alphas - } - - /// Helper function to compute the alpha value using a sigmoid function. - pub fn alpha_sigmoid( - consensus: I32F32, - weight: I32F32, - bond: I32F32, - alpha_low: I32F32, - alpha_high: I32F32, - alpha_sigmoid_steepness: I32F32, - ) -> I32F32 { - let zero = I32F32::from_num(0.0); - let one = I32F32::from_num(1.0); - - let diff_buy = clamp_value(weight.saturating_sub(consensus), zero, one); - let diff_sell = clamp_value(bond.saturating_sub(weight), zero, one); - let combined_diff = if weight >= bond { diff_buy } else { diff_sell }; - - // sigmoid = 1. / (1. + e^(-steepness * (combined_diff - 0.5))) - let sigmoid = one.saturating_div( - one.saturating_add(exp_safe( - alpha_sigmoid_steepness - .saturating_div(I32F32::from_num(-100)) - .saturating_mul(combined_diff.saturating_sub(I32F32::from_num(0.5))), - )), - ); - let alpha = - alpha_low.saturating_add(sigmoid.saturating_mul(alpha_high.saturating_sub(alpha_low))); - - clamp_value(alpha, alpha_low, alpha_high) - } - - pub fn compute_disabled_liquid_alpha(netuid: NetUid) -> I32F32 { - // Retrieve the bonds moving average for the given network ID and scale it down. - let bonds_moving_average: I64F64 = I64F64::from_num(Self::get_bonds_moving_average(netuid)) - .saturating_div(I64F64::from_num(1_000_000)); - - // Calculate the alpha value for the EMA calculation. - // Alpha is derived by subtracting the scaled bonds moving average from 1. - let alpha: I32F32 = - I32F32::from_num(1).saturating_sub(I32F32::from_num(bonds_moving_average)); - alpha - } - - pub fn do_set_alpha_values( - origin: OriginFor, - netuid: NetUid, - alpha_low: u16, - alpha_high: u16, - ) -> Result<(), DispatchError> { - Self::ensure_subnet_owner_or_root(origin, netuid)?; - - ensure!( - Self::get_liquid_alpha_enabled(netuid), - Error::::LiquidAlphaDisabled - ); - - let max_u16: u32 = u16::MAX as u32; // 65535 - let min_alpha_low: u16 = (max_u16.safe_div(40)) as u16; // 1638 - let min_alpha_high: u16 = min_alpha_low; - - ensure!(alpha_high >= min_alpha_high, Error::::AlphaHighTooLow); - - ensure!( - alpha_low >= min_alpha_low && alpha_low <= alpha_high, - Error::::AlphaLowOutOfRange - ); - - AlphaValues::::insert(netuid, (alpha_low, alpha_high)); - - log::debug!( - "AlphaValuesSet( netuid: {netuid:?}, AlphaLow: {alpha_low:?}, AlphaHigh: {alpha_high:?} ) ", - ); - Ok(()) - } - - pub fn do_reset_bonds( - netuid_index: NetUidStorageIndex, - account_id: &T::AccountId, - ) -> Result<(), DispatchError> { - let (netuid, _) = Self::get_netuid_and_subid(netuid_index).unwrap_or_default(); - - // check bonds reset enabled for this subnet - let bonds_reset_enabled: bool = Self::get_bonds_reset(netuid); - if !bonds_reset_enabled { - return Ok(()); - } - - if let Ok(uid) = Self::get_uid_for_net_and_hotkey(netuid, account_id) { - for (i, bonds_vec) in Bonds::::iter_prefix(netuid_index) { - Bonds::::insert( - netuid_index, - i, - bonds_vec - .clone() - .iter() - .filter(|(j, _)| *j != uid) - .collect::>(), - ); - } - log::debug!("Reset bonds for {account_id:?}, netuid {netuid:?}"); - } else { - log::warn!( - "Uid not found for {account_id:?}, netuid {netuid:?} - skipping bonds reset" - ); - } - - Ok(()) - } - - /// This function ensures major assumptions made by epoch function: - /// 1. Keys map has no duplicate hotkeys - /// - pub fn is_epoch_input_state_consistent(netuid: NetUid) -> bool { - // Check if Keys map has duplicate hotkeys or uids - let mut hotkey_set: BTreeSet = BTreeSet::new(); - // `iter_prefix` over a double map yields (uid, value) for the given first key. - for (_uid, hotkey) in Keys::::iter_prefix(netuid) { - if !hotkey_set.insert(hotkey) { - log::error!("Duplicate hotkeys detected for netuid {netuid}"); - return false; - } - } - true - } -} diff --git a/pallets/subtensor/src/epoch/run_epoch/bonds_ema_liquid_alpha.rs b/pallets/subtensor/src/epoch/run_epoch/bonds_ema_liquid_alpha.rs new file mode 100644 index 0000000000..14d9ece8be --- /dev/null +++ b/pallets/subtensor/src/epoch/run_epoch/bonds_ema_liquid_alpha.rs @@ -0,0 +1,363 @@ +//! Bonds EMA (normal + liquid-alpha), liquid-alpha sigmoid, and alpha-bounds / bonds-reset helpers. + +use super::*; +use crate::epoch::math::*; +use alloc::collections::BTreeSet; +use safe_math::*; +use substrate_fixed::types::{I32F32, I64F64}; +use subtensor_runtime_common::{NetUid, NetUidStorageIndex}; + +impl Pallet { + /// Bonds EMA with a single subnet-wide alpha (sparse); used when liquid alpha is off. + pub fn ema_bonds_normal_sparse( + bonds_delta: &[Vec<(u16, I32F32)>], + bonds: &[Vec<(u16, I32F32)>], + netuid_index: NetUidStorageIndex, + ) -> Vec> { + let (netuid, _) = Self::get_netuid_and_subid(netuid_index).unwrap_or_default(); + + // Retrieve the bonds moving average for the given network ID and scale it down. + let bonds_moving_average: I64F64 = + I64F64::saturating_from_num(Self::get_bonds_moving_average(netuid)) + .safe_div(I64F64::saturating_from_num(1_000_000)); + + // Calculate the alpha value for the EMA calculation. + // Alpha is derived by subtracting the scaled bonds moving average from 1. + let alpha: I32F32 = I32F32::saturating_from_num(1) + .saturating_sub(I32F32::saturating_from_num(bonds_moving_average)); + + // Compute the Exponential Moving Average (EMA) of bonds using the calculated alpha value. + let ema_bonds = mat_ema_sparse(bonds_delta, bonds, alpha); + + // Log the computed EMA bonds for debugging purposes. + log::trace!("Exponential Moving Average Bonds Normal: {ema_bonds:?}"); + + // Return the computed EMA bonds. + ema_bonds + } + + /// Bonds EMA with a single subnet-wide alpha (dense); test / dense-epoch path. + pub fn ema_bonds_normal_dense( + bonds_delta: &[Vec], + bonds: &[Vec], + netuid: NetUid, + ) -> Vec> { + // Retrieve the bonds moving average for the given network ID and scale it down. + let bonds_moving_average: I64F64 = + I64F64::saturating_from_num(Self::get_bonds_moving_average(netuid)) + .safe_div(I64F64::saturating_from_num(1_000_000)); + + // Calculate the alpha value for the EMA calculation. + // Alpha is derived by subtracting the scaled bonds moving average from 1. + let alpha: I32F32 = I32F32::saturating_from_num(1) + .saturating_sub(I32F32::saturating_from_num(bonds_moving_average)); + + // Compute the Exponential Moving Average (EMA) of bonds using the calculated alpha value. + let ema_bonds = mat_ema(bonds_delta, bonds, alpha); + + // Log the computed EMA bonds for debugging purposes. + log::trace!("Exponential Moving Average Bonds Normal: {ema_bonds:?}"); + + // Return the computed EMA bonds. + ema_bonds + } + + pub fn compute_bonds( + netuid: NetUid, + weights: &[Vec], // weights_for_bonds + bonds: &[Vec], + consensus: &[I32F32], + ) -> Vec> { + // Check if Liquid Alpha is enabled, consensus is not empty, and contains non-zero values. + if LiquidAlphaOn::::get(netuid) + && !consensus.is_empty() + && consensus + .iter() + .any(|&c| c != I32F32::saturating_from_num(0)) + { + // Liquid Alpha is enabled, compute the liquid alphas matrix. + let alphas: Vec> = + Self::liquid_alpha_matrix_dense(netuid, weights, bonds, consensus); + log::trace!("alphas: {:?}", &alphas); + + // Compute the Exponential Moving Average (EMA) of bonds using the provided clamped alpha values. + mat_ema_alpha(weights, bonds, &alphas) + } else { + // Liquid Alpha is disabled, compute the liquid alpha value. + let alpha: I32F32 = Self::bonds_moving_average_alpha(netuid); + + // Compute the Exponential Moving Average (EMA) of bonds using the calculated alpha value. + mat_ema(weights, bonds, alpha) + } + } + + /// Sparse bonds EMA: liquid-alpha matrix when enabled, else [`Self::bonds_moving_average_alpha`]. + pub fn ema_bonds_liquid_or_normal_sparse( + netuid_index: NetUidStorageIndex, + weights: &[Vec<(u16, I32F32)>], + bonds: &[Vec<(u16, I32F32)>], + consensus: &[I32F32], + ) -> Vec> { + let (netuid, _) = Self::get_netuid_and_subid(netuid_index).unwrap_or_default(); + + // Check if Liquid Alpha is enabled, consensus is not empty, and contains non-zero values. + if LiquidAlphaOn::::get(netuid) + && !consensus.is_empty() + && consensus + .iter() + .any(|&c| c != I32F32::saturating_from_num(0)) + { + // Liquid Alpha is enabled, compute the liquid alphas matrix. + let alphas: Vec> = + Self::liquid_alpha_matrix_sparse(netuid, weights, bonds, consensus); + log::trace!("alphas: {:?}", &alphas); + + // Compute the Exponential Moving Average (EMA) of bonds using the provided clamped alpha values. + mat_ema_alpha_sparse(weights, bonds, &alphas) + } else { + // Liquid Alpha is disabled, compute the liquid alpha value. + let alpha: I32F32 = Self::bonds_moving_average_alpha(netuid); + + // Compute the Exponential Moving Average (EMA) of bonds using the calculated alpha value. + mat_ema_sparse(weights, bonds, alpha) + } + } + + /// Per validator-miner liquid-alpha values (dense) from weights, prior bonds, and consensus. + pub fn liquid_alpha_matrix_dense( + netuid: NetUid, + weights: &[Vec], // current epoch weights + bonds: &[Vec], // previous epoch bonds + consensus: &[I32F32], // previous epoch consensus weights + ) -> Vec> { + let mut alphas = Vec::new(); + + if weights.len() != bonds.len() { + log::error!( + "math error: liquid_alpha_matrix_dense: weights and bonds have different lengths: {:?} != {:?}", + weights.len(), + bonds.len() + ); + return alphas; + } + + // Get the high and low alpha values for the network. + let alpha_sigmoid_steepness: I32F32 = Self::get_alpha_sigmoid_steepness(netuid); + let (alpha_low, alpha_high): (I32F32, I32F32) = Self::get_alpha_values_32(netuid); + + for (w_row, b_row) in weights.iter().zip(bonds.iter()) { + let mut row_alphas = Vec::new(); + + for ((weight, bond), consensus_val) in + w_row.iter().zip(b_row.iter()).zip(consensus.iter()) + { + let alpha = Self::liquid_alpha_sigmoid( + *consensus_val, + *weight, + *bond, + alpha_low, + alpha_high, + alpha_sigmoid_steepness, + ); + row_alphas.push(alpha); + } + alphas.push(row_alphas); + } + alphas + } + + /// Per validator-miner liquid-alpha values (sparse weights/bonds to dense alpha matrix). + pub fn liquid_alpha_matrix_sparse( + netuid: NetUid, + weights: &[Vec<(u16, I32F32)>], // current epoch weights + bonds: &[Vec<(u16, I32F32)>], // previous epoch bonds + consensus: &[I32F32], // previous epoch consensus weights + ) -> Vec> { + let mut alphas = Vec::with_capacity(consensus.len()); + + if weights.len() != bonds.len() { + log::error!( + "math error: liquid_alpha_matrix_dense: weights and bonds have different lengths: {:?} != {:?}", + weights.len(), + bonds.len() + ); + return alphas; + } + + let alpha_sigmoid_steepness: I32F32 = Self::get_alpha_sigmoid_steepness(netuid); + let (alpha_low, alpha_high): (I32F32, I32F32) = Self::get_alpha_values_32(netuid); + + let zero = I32F32::from_num(0.0); + + // iterate over rows + for (w_row, b_row) in weights.iter().zip(bonds.iter()) { + let mut row_alphas = Vec::with_capacity(w_row.len()); + let mut w_iter = w_row.iter().peekable(); + let mut b_iter = b_row.iter().peekable(); + for (j_pos, consensus_val) in consensus.iter().enumerate() { + let j = j_pos as u16; + + let mut weight = zero; + while let Some(&&(i, val)) = w_iter.peek() { + if i < j { + w_iter.next(); + } else { + if i == j { + weight = val; + } + break; + } + } + + let mut bond = zero; + while let Some(&&(i, val)) = b_iter.peek() { + if i < j { + b_iter.next(); + } else { + if i == j { + bond = val; + } + break; + } + } + + let alpha = Self::liquid_alpha_sigmoid( + *consensus_val, + weight, + bond, + alpha_low, + alpha_high, + alpha_sigmoid_steepness, + ); + row_alphas.push(alpha); + } + alphas.push(row_alphas); + } + alphas + } + + /// Sigmoid liquid-alpha for one edge, clamped to `[alpha_low, alpha_high]`. + pub fn liquid_alpha_sigmoid( + consensus: I32F32, + weight: I32F32, + bond: I32F32, + alpha_low: I32F32, + alpha_high: I32F32, + alpha_sigmoid_steepness: I32F32, + ) -> I32F32 { + let zero = I32F32::from_num(0.0); + let one = I32F32::from_num(1.0); + + let diff_buy = clamp_i32f32(weight.saturating_sub(consensus), zero, one); + let diff_sell = clamp_i32f32(bond.saturating_sub(weight), zero, one); + let combined_diff = if weight >= bond { diff_buy } else { diff_sell }; + + // sigmoid = 1. / (1. + e^(-steepness * (combined_diff - 0.5))) + let sigmoid = one.saturating_div( + one.saturating_add(exp_safe( + alpha_sigmoid_steepness + .saturating_div(I32F32::from_num(-100)) + .saturating_mul(combined_diff.saturating_sub(I32F32::from_num(0.5))), + )), + ); + let alpha = + alpha_low.saturating_add(sigmoid.saturating_mul(alpha_high.saturating_sub(alpha_low))); + + clamp_i32f32(alpha, alpha_low, alpha_high) + } + + /// `1 - bonds_moving_average/1e6` — constant EMA alpha when liquid alpha is disabled. + pub fn bonds_moving_average_alpha(netuid: NetUid) -> I32F32 { + // Retrieve the bonds moving average for the given network ID and scale it down. + let bonds_moving_average: I64F64 = I64F64::from_num(Self::get_bonds_moving_average(netuid)) + .saturating_div(I64F64::from_num(1_000_000)); + + // Calculate the alpha value for the EMA calculation. + // Alpha is derived by subtracting the scaled bonds moving average from 1. + let alpha: I32F32 = + I32F32::from_num(1).saturating_sub(I32F32::from_num(bonds_moving_average)); + alpha + } + + /// Owner/root setter for liquid-alpha bounds (`AlphaValues`); enforces enabled + range checks. + pub fn do_set_alpha_values( + origin: OriginFor, + netuid: NetUid, + alpha_low: u16, + alpha_high: u16, + ) -> Result<(), DispatchError> { + Self::ensure_subnet_owner_or_root(origin, netuid)?; + + ensure!( + Self::get_liquid_alpha_enabled(netuid), + Error::::LiquidAlphaDisabled + ); + + let max_u16: u32 = u16::MAX as u32; // 65535 + let min_alpha_low: u16 = (max_u16.safe_div(40)) as u16; // 1638 + let min_alpha_high: u16 = min_alpha_low; + + ensure!(alpha_high >= min_alpha_high, Error::::AlphaHighTooLow); + + ensure!( + alpha_low >= min_alpha_low && alpha_low <= alpha_high, + Error::::AlphaLowOutOfRange + ); + + AlphaValues::::insert(netuid, (alpha_low, alpha_high)); + + log::debug!( + "AlphaValuesSet( netuid: {netuid:?}, AlphaLow: {alpha_low:?}, AlphaHigh: {alpha_high:?} ) ", + ); + Ok(()) + } + + /// Zero a hotkey column in `Bonds` when bonds-reset is enabled for the subnet. + pub fn reset_bonds_column_for_hotkey( + netuid_index: NetUidStorageIndex, + account_id: &T::AccountId, + ) -> Result<(), DispatchError> { + let (netuid, _) = Self::get_netuid_and_subid(netuid_index).unwrap_or_default(); + + // check bonds reset enabled for this subnet + let bonds_reset_enabled: bool = Self::get_bonds_reset(netuid); + if !bonds_reset_enabled { + return Ok(()); + } + + if let Ok(uid) = Self::get_uid_for_net_and_hotkey(netuid, account_id) { + for (i, bonds_vec) in Bonds::::iter_prefix(netuid_index) { + Bonds::::insert( + netuid_index, + i, + bonds_vec + .clone() + .iter() + .filter(|(j, _)| *j != uid) + .collect::>(), + ); + } + log::debug!("Reset bonds for {account_id:?}, netuid {netuid:?}"); + } else { + log::warn!( + "Uid not found for {account_id:?}, netuid {netuid:?} - skipping bonds reset" + ); + } + + Ok(()) + } + + /// Preflight: `Keys` for `netuid` must not contain duplicate hotkeys. + pub fn epoch_keys_have_unique_hotkeys(netuid: NetUid) -> bool { + // Check if Keys map has duplicate hotkeys or uids + let mut hotkey_set: BTreeSet = BTreeSet::new(); + // `iter_prefix` over a double map yields (uid, value) for the given first key. + for (_uid, hotkey) in Keys::::iter_prefix(netuid) { + if !hotkey_set.insert(hotkey) { + log::error!("Duplicate hotkeys detected for netuid {netuid}"); + return false; + } + } + true + } +} diff --git a/pallets/subtensor/src/epoch/run_epoch/epoch_dense.rs b/pallets/subtensor/src/epoch/run_epoch/epoch_dense.rs new file mode 100644 index 0000000000..c67f821aff --- /dev/null +++ b/pallets/subtensor/src/epoch/run_epoch/epoch_dense.rs @@ -0,0 +1,444 @@ +//! Dense-matrix epoch path (test-only). Production uses [`super::epoch_mechanism`]. + +use super::*; +use crate::epoch::math::*; +use frame_support::IterableStorageDoubleMap; +use sp_runtime::PerU16; +use sp_std::vec; +use substrate_fixed::types::{I32F32, I64F64, I96F32}; +use subtensor_runtime_common::{AlphaBalance, MechId, NetUid, NetUidStorageIndex}; + +impl Pallet { + /// Dense Yuma epoch (O(n^2) weights/bonds). Prefer [`Self::epoch_mechanism`] in production. + #[allow(clippy::indexing_slicing)] + pub fn epoch_dense_mechanism_for_tests( + netuid: NetUid, + mecid: MechId, + rao_emission: AlphaBalance, + ) -> Vec<(T::AccountId, AlphaBalance, AlphaBalance)> { + // Calculate netuid storage index + let netuid_index = Self::get_mechanism_storage_index(netuid, mecid); + + // Get subnetwork size. + let n: u16 = Self::get_subnetwork_n(netuid); + log::trace!("n: {n:?}"); + + // ====================== + // == Active & updated == + // ====================== + + // Get current block. + let current_block: u64 = Self::get_current_block_as_u64(); + log::trace!("current_block: {current_block:?}"); + + // Get tempo. + let tempo: u64 = Self::get_tempo(netuid).into(); + log::trace!("tempo: {tempo:?}"); + + // Get activity cutoff. + let activity_cutoff: u64 = Self::get_activity_cutoff_blocks(netuid); + log::trace!("activity_cutoff: {activity_cutoff:?}"); + + // Last update vector. + let last_update: Vec = Self::get_last_update(netuid_index); + log::trace!("Last update: {:?}", &last_update); + + // Inactive mask. + let inactive: Vec = last_update + .iter() + .map(|updated| updated.saturating_add(activity_cutoff) < current_block) + .collect(); + log::trace!("Inactive: {:?}", inactive.clone()); + + // Logical negation of inactive. + let active: Vec = inactive.iter().map(|&b| !b).collect(); + + // Block at registration vector (block when each neuron was most recently registered). + let block_at_registration: Vec = Self::neuron_block_at_registration(netuid); + log::trace!("Block at registration: {:?}", &block_at_registration); + + // Outdated matrix, outdated_ij=True if i has last updated (weights) after j has last registered. + let outdated: Vec> = last_update + .iter() + .map(|updated| { + block_at_registration + .iter() + .map(|registered| updated <= registered) + .collect() + }) + .collect(); + log::trace!("Outdated: {:?}", &outdated); + + // Recently registered matrix, recently_ij=True if last_tempo was *before* j was last registered. + // Mask if: the last tempo block happened *before* the registration block + // ==> last_tempo <= registered + // For dynamic tempo - we pick previous-successful-epoch block: `LastMechansimStepBlock + 1` + let lms = LastMechansimStepBlock::::get(netuid); + let last_tempo: u64 = if lms == 0 { + current_block.saturating_sub(tempo) + } else { + lms.saturating_add(1) + }; + let recently_registered: Vec = block_at_registration + .iter() + .map(|registered| last_tempo <= *registered) + .collect(); + log::trace!("Recently registered: {:?}", &recently_registered); + + // =========== + // == Stake == + // =========== + + let hotkeys: Vec<(u16, T::AccountId)> = + as IterableStorageDoubleMap>::iter_prefix(netuid) + .collect(); + log::trace!("hotkeys: {:?}", &hotkeys); + + // Access network stake as normalized vector. + let (total_stake, _alpha_stake, _tao_stake): (Vec, Vec, Vec) = + Self::get_stake_weights_for_network(netuid); + + // Get the minimum stake required. + let min_stake = Self::get_stake_threshold(); + + // Set stake of validators that doesn't meet the staking threshold to 0 as filter. + let mut filtered_stake: Vec = total_stake + .iter() + .map(|&s| { + if fixed64_to_u64(s) < min_stake { + return I64F64::from(0); + } + s + }) + .collect(); + log::debug!("Filtered stake: {:?}", &filtered_stake); + + inplace_normalize_64(&mut filtered_stake); + let stake: Vec = vec_fixed64_to_fixed32(filtered_stake); + log::trace!("S: {:?}", &stake); + + // ======================= + // == Validator permits == + // ======================= + + // Get validator permits. + let validator_permits: Vec = Self::get_validator_permit(netuid); + log::trace!("validator_permits: {validator_permits:?}"); + + // Logical negation of validator_permits. + let validator_forbids: Vec = validator_permits.iter().map(|&b| !b).collect(); + + // Get max allowed validators. + let max_allowed_validators: u16 = Self::get_max_allowed_validators(netuid); + log::trace!("max_allowed_validators: {max_allowed_validators:?}"); + + // Get new validator permits. + let new_validator_permits: Vec = + is_topk_nonzero_i32f32(&stake, max_allowed_validators as usize); + log::trace!("new_validator_permits: {new_validator_permits:?}"); + + // ================== + // == Active Stake == + // ================== + + let mut active_stake: Vec = stake.clone(); + + // Remove inactive stake. + inplace_mask_vector(&inactive, &mut active_stake); + + // Remove non-validator stake. + inplace_mask_vector(&validator_forbids, &mut active_stake); + + // Normalize active stake. + inplace_normalize(&mut active_stake); + log::trace!("S: {:?}", &active_stake); + + // ============= + // == Weights == + // ============= + + // Get owner uid. + let owner_uid: Option = Self::get_owner_uid(netuid); + + // Access network weights row unnormalized. + let mut weights: Vec> = Self::get_weights(netuid_index); + log::trace!("W: {:?}", &weights); + + // Mask weights that are not from permitted validators. + inplace_mask_rows(&validator_forbids, &mut weights); + log::trace!("W (permit): {:?}", &weights); + + // Remove self-weight by masking diagonal; keep owner_uid self-weight. + if let Some(owner_uid) = owner_uid { + inplace_mask_diag_except_index(&mut weights, owner_uid); + } else { + inplace_mask_diag(&mut weights); + } + + inplace_mask_diag(&mut weights); + log::trace!("W (permit+diag): {:?}", &weights); + + // Mask outdated weights: remove weights referring to deregistered neurons. + inplace_mask_matrix(&outdated, &mut weights); + log::trace!("W (permit+diag+outdate): {:?}", &weights); + + // Normalize remaining weights. + inplace_row_normalize(&mut weights); + log::trace!("W (mask+norm): {:?}", &weights); + + // ================================ + // == Consensus, Validator Trust == + // ================================ + + // Consensus majority ratio, e.g. 51%. + let kappa: I32F32 = Self::kappa_proportion_as_i32f32(netuid); + // Calculate consensus as stake-weighted median of weights. + let consensus: Vec = weighted_median_col(&active_stake, &weights, kappa); + // Clip weights at majority consensus. + let mut clipped_weights: Vec> = weights.clone(); + inplace_col_clip(&mut clipped_weights, &consensus); + // Calculate validator trust as sum of clipped weights set by validator. + let validator_trust: Vec = row_sum(&clipped_weights); + + // ==================================== + // == Ranks, Server Trust, Incentive == + // ==================================== + + // Compute ranks: r_j = SUM(i) w_ij * s_i + let mut ranks: Vec = matmul(&clipped_weights, &active_stake); + + inplace_normalize(&mut ranks); + let incentive: Vec = ranks.clone(); + log::trace!("I: {:?}", &incentive); + + // ========================= + // == Bonds and Dividends == + // ========================= + + // Get validator bonds penalty in [0, 1]. + let bonds_penalty: I32F32 = Self::bonds_penalty_proportion_as_i32f32(netuid); + // Calculate weights for bonds, apply bonds penalty to weights. + // bonds_penalty = 0: weights_for_bonds = weights.clone() + // bonds_penalty = 1: weights_for_bonds = clipped_weights.clone() + let weights_for_bonds: Vec> = + interpolate(&weights, &clipped_weights, bonds_penalty); + + let mut dividends: Vec; + let mut ema_bonds: Vec>; + if Yuma3On::::get(netuid) { + // Access network bonds. + let mut bonds: Vec> = Self::get_bonds_fixed_proportion(netuid_index); + inplace_mask_cols(&recently_registered, &mut bonds); // mask outdated bonds + log::trace!("B: {:?}", &bonds); + + // Compute the Exponential Moving Average (EMA) of bonds. + ema_bonds = Self::compute_bonds(netuid, &weights_for_bonds, &bonds, &consensus); + log::trace!("emaB: {:?}", &ema_bonds); + + // Normalize EMA bonds. + let mut ema_bonds_norm = ema_bonds.clone(); + inplace_col_normalize(&mut ema_bonds_norm); + log::trace!("emaB norm: {:?}", &ema_bonds_norm); + + // # === Dividend Calculation=== + let total_bonds_per_validator: Vec = + row_sum(&mat_vec_mul(&ema_bonds_norm, &incentive)); + log::trace!( + "total_bonds_per_validator: {:?}", + &total_bonds_per_validator + ); + + dividends = vec_mul(&total_bonds_per_validator, &active_stake); + inplace_normalize(&mut dividends); + log::trace!("D: {:?}", ÷nds); + } else { + // original Yuma - liquid alpha disabled + // Access network bonds. + let mut bonds: Vec> = Self::get_bonds(netuid_index); + // Remove bonds referring to neurons that have registered since last tempo. + inplace_mask_cols(&recently_registered, &mut bonds); // mask recently registered bonds + inplace_col_normalize(&mut bonds); // sum_i b_ij = 1 + log::trace!("B: {:?}", &bonds); + + // Compute bonds delta column normalized. + let mut bonds_delta: Vec> = row_hadamard(&weights_for_bonds, &active_stake); // ΔB = W◦S + inplace_col_normalize(&mut bonds_delta); // sum_i b_ij = 1 + log::trace!("ΔB: {:?}", &bonds_delta); + + // Compute the Exponential Moving Average (EMA) of bonds. + ema_bonds = Self::ema_bonds_normal_dense(&bonds_delta, &bonds, netuid); + inplace_col_normalize(&mut ema_bonds); // sum_i b_ij = 1 + log::trace!("emaB: {:?}", &ema_bonds); + + // Compute dividends: d_i = SUM(j) b_ij * inc_j + dividends = matmul_transpose(&ema_bonds, &incentive); + inplace_normalize(&mut dividends); + log::trace!("Dividends: {:?}", ÷nds); + + // Column max-upscale EMA bonds for storage: max_i w_ij = 1. + inplace_col_max_upscale(&mut ema_bonds); + } + + // ================================= + // == Emission and Pruning scores == + // ================================= + + // Compute emission scores. + + // Compute normalized emission scores. range: I32F32(0, 1) + // Compute normalized emission scores. range: I32F32(0, 1) + let combined_emission: Vec = incentive + .iter() + .zip(dividends.clone()) + .map(|(ii, di)| ii.saturating_add(di)) + .collect(); + let emission_sum: I32F32 = combined_emission.iter().sum(); + + let mut normalized_server_emission: Vec = incentive.clone(); // Servers get incentive. + let mut normalized_validator_emission: Vec = dividends.clone(); // Validators get dividends. + let mut normalized_combined_emission: Vec = combined_emission.clone(); + // Normalize on the sum of incentive + dividends. + inplace_normalize_i32f32_with_sum(&mut normalized_server_emission, emission_sum); + inplace_normalize_i32f32_with_sum(&mut normalized_validator_emission, emission_sum); + inplace_normalize(&mut normalized_combined_emission); + + // If emission is zero, replace emission with normalized stake. + if emission_sum == I32F32::from(0) { + // no weights set | outdated weights | self_weights + if is_zero(&active_stake) { + // no active stake + normalized_validator_emission.clone_from(&stake); // do not mask inactive, assumes stake is normalized + normalized_combined_emission.clone_from(&stake); + } else { + normalized_validator_emission.clone_from(&active_stake); // emission proportional to inactive-masked normalized stake + normalized_combined_emission.clone_from(&active_stake); + } + } + + // Compute rao based emission scores. range: I96F32(0, rao_emission) + let float_rao_emission: I96F32 = I96F32::saturating_from_num(rao_emission); + + let server_emission: Vec = normalized_server_emission + .iter() + .map(|se: &I32F32| I96F32::saturating_from_num(*se).saturating_mul(float_rao_emission)) + .collect(); + let server_emission: Vec = server_emission + .iter() + .map(|e: &I96F32| e.saturating_to_num::().into()) + .collect(); + + let validator_emission: Vec = normalized_validator_emission + .iter() + .map(|ve: &I32F32| I96F32::saturating_from_num(*ve).saturating_mul(float_rao_emission)) + .collect(); + let validator_emission: Vec = validator_emission + .iter() + .map(|e: &I96F32| e.saturating_to_num::().into()) + .collect(); + + // Used only to track combined emission in the storage. + let combined_emission: Vec = normalized_combined_emission + .iter() + .map(|ce: &I32F32| I96F32::saturating_from_num(*ce).saturating_mul(float_rao_emission)) + .collect(); + let combined_emission: Vec = combined_emission + .iter() + .map(|e: &I96F32| AlphaBalance::from(e.saturating_to_num::())) + .collect(); + + log::trace!("nSE: {:?}", &normalized_server_emission); + log::trace!("SE: {:?}", &server_emission); + log::trace!("nVE: {:?}", &normalized_validator_emission); + log::trace!("VE: {:?}", &validator_emission); + log::trace!("nCE: {:?}", &normalized_combined_emission); + log::trace!("CE: {:?}", &combined_emission); + + // =================== + // == Value storage == + // =================== + let cloned_emission = combined_emission.clone(); + let cloned_stake_weight: Vec = stake + .iter() + .map(|xi| fixed_proportion_to_u16(*xi)) + .collect::>(); + let cloned_consensus: Vec = consensus + .iter() + .map(|xi| fixed_proportion_to_u16(*xi)) + .collect::>(); + let cloned_incentive: Vec = incentive + .iter() + .map(|xi| fixed_proportion_to_u16(*xi)) + .collect::>(); + let cloned_dividends: Vec = dividends + .iter() + .map(|xi| fixed_proportion_to_u16(*xi)) + .collect::>(); + let cloned_validator_trust: Vec = validator_trust + .iter() + .map(|xi| fixed_proportion_to_u16(*xi)) + .collect::>(); + StakeWeight::::insert(netuid, cloned_stake_weight.clone()); + Active::::insert(netuid, active.clone()); + Emission::::insert(netuid, cloned_emission); + // Epoch math stays in raw u16; wrap into PerU16 only at the storage boundary. + Consensus::::insert( + netuid, + cloned_consensus + .into_iter() + .map(PerU16::from_parts) + .collect::>(), + ); + Incentive::::insert( + NetUidStorageIndex::from(netuid), + cloned_incentive + .into_iter() + .map(PerU16::from_parts) + .collect::>(), + ); + Dividends::::insert( + netuid, + cloned_dividends + .into_iter() + .map(PerU16::from_parts) + .collect::>(), + ); + ValidatorTrust::::insert( + netuid, + cloned_validator_trust + .into_iter() + .map(PerU16::from_parts) + .collect::>(), + ); + ValidatorPermit::::insert(netuid, new_validator_permits.clone()); + + new_validator_permits + .iter() + .zip(validator_permits) + .zip(ema_bonds) + .enumerate() + .for_each(|(i, ((new_permit, validator_permit), ema_bond))| { + // Set bonds only if uid retains validator permit, otherwise clear bonds. + if *new_permit { + let new_bonds_row: Vec<(u16, u16)> = (0..n) + .zip(vec_fixed_proportions_to_u16(ema_bond.clone())) + .collect(); + Bonds::::insert(netuid_index, i as u16, new_bonds_row); + } else if validator_permit { + // Only overwrite the intersection. + let new_empty_bonds_row: Vec<(u16, u16)> = vec![]; + Bonds::::insert(netuid_index, i as u16, new_empty_bonds_row); + } + }); + + hotkeys + .into_iter() + .map(|(uid_i, hotkey)| { + ( + hotkey, + server_emission[uid_i as usize], + validator_emission[uid_i as usize], + ) + }) + .collect() + } +} diff --git a/pallets/subtensor/src/epoch/run_epoch/epoch_mechanism.rs b/pallets/subtensor/src/epoch/run_epoch/epoch_mechanism.rs new file mode 100644 index 0000000000..db5b1d6b14 --- /dev/null +++ b/pallets/subtensor/src/epoch/run_epoch/epoch_mechanism.rs @@ -0,0 +1,536 @@ +//! Sparse Yuma-consensus epoch: activity masks, stake filter, weights→bonds→emission per mechanism. + +use super::*; +use crate::epoch::math::*; +use alloc::collections::BTreeMap; +use sp_std::vec; +use substrate_fixed::types::{I32F32, I64F64, I96F32}; +use subtensor_runtime_common::{AlphaBalance, MechId, NetUid}; + +impl Pallet { + /// Calculates reward consensus values, then updates rank, trust, consensus, incentive, dividend, pruning_score, emission and bonds, and + /// returns the emissions for uids/hotkeys in a given `netuid`. + /// + /// # Arguments + /// * `netuid`: The network to distribute the emission onto. + /// + /// * `rao_emission`: The total emission for the epoch. + /// + /// * `debug`: Print debugging outputs. + /// + pub fn epoch_mechanism( + netuid: NetUid, + mecid: MechId, + rao_emission: AlphaBalance, + ) -> HotkeyEpochTerms { + // Calculate netuid storage index + let netuid_index = Self::get_mechanism_storage_index(netuid, mecid); + + // Initialize output keys (neuron hotkeys) and UIDs + let mut terms_map: BTreeMap = Keys::::iter_prefix(netuid) + .map(|(uid, hotkey)| { + ( + hotkey, + EpochTerms { + uid: uid as usize, + ..Default::default() + }, + ) + }) + .collect(); + + // Get subnetwork size. + let n = Self::get_subnetwork_n(netuid); + log::trace!("Number of Neurons in Network: {n:?}"); + + // ====================== + // == Active & updated == + // ====================== + + // Get current block. + let current_block: u64 = Self::get_current_block_as_u64(); + log::trace!("current_block: {current_block:?}"); + + // Get tempo. + let tempo: u64 = Self::get_tempo(netuid).into(); + log::trace!("tempo:\n{tempo:?}\n"); + + // Get activity cutoff. + let activity_cutoff: u64 = Self::get_activity_cutoff_blocks(netuid); + log::trace!("activity_cutoff: {activity_cutoff:?}"); + + // Last update vector. + let last_update: Vec = Self::get_last_update(netuid_index); + log::trace!("Last update: {:?}", &last_update); + + // Inactive mask. + let inactive: Vec = last_update + .iter() + .map(|updated| updated.saturating_add(activity_cutoff) < current_block) + .collect(); + log::debug!("Inactive: {:?}", inactive.clone()); + + // Logical negation of inactive. + let active: Vec = inactive.iter().map(|&b| !b).collect(); + + // Block at registration vector (block when each neuron was most recently registered). + let block_at_registration: Vec = Self::neuron_block_at_registration(netuid); + log::trace!("Block at registration: {:?}", &block_at_registration); + + // =========== + // == Stake == + // =========== + + // Access network stake as normalized vector. + let (total_stake, _alpha_stake, _tao_stake): (Vec, Vec, Vec) = + Self::get_stake_weights_for_network(netuid); + + // Get the minimum stake required. + let min_stake = Self::get_stake_threshold(); + + // Get owner uid. + let owner_uid: Option = Self::get_owner_uid(netuid); + + // Set stake of validators that doesn't meet the staking threshold to 0 as filter. + let mut filtered_stake: Vec = total_stake + .iter() + .enumerate() + .map(|(uid, &s)| { + if owner_uid != Some(uid as u16) && fixed64_to_u64(s) < min_stake { + return I64F64::from(0); + } + s + }) + .collect(); + log::debug!("Filtered stake: {:?}", &filtered_stake); + + inplace_normalize_64(&mut filtered_stake); + let stake: Vec = vec_fixed64_to_fixed32(filtered_stake); + log::debug!("Normalised Stake: {:?}", &stake); + + // ======================= + // == Validator permits == + // ======================= + + // Get current validator permits. + let mut validator_permits: Vec = Self::get_validator_permit(netuid); + if let Some(owner_uid) = owner_uid + && let Some(owner_permit) = validator_permits.get_mut(owner_uid as usize) + { + *owner_permit = true; + } + log::trace!("validator_permits: {validator_permits:?}"); + + // Logical negation of validator_permits. + let validator_forbids: Vec = validator_permits.iter().map(|&b| !b).collect(); + + // Get max allowed validators. + let max_allowed_validators: u16 = Self::get_max_allowed_validators(netuid); + log::trace!("max_allowed_validators: {max_allowed_validators:?}"); + + // Get new validator permits. + let mut new_validator_permits: Vec = + is_topk_nonzero_i32f32(&stake, max_allowed_validators as usize); + if let Some(owner_uid) = owner_uid + && let Some(owner_permit) = new_validator_permits.get_mut(owner_uid as usize) + { + *owner_permit = true; + } + log::trace!("new_validator_permits: {new_validator_permits:?}"); + + // ================== + // == Active Stake == + // ================== + + let mut active_stake: Vec = stake.clone(); + + // Remove inactive stake. + inplace_mask_vector(&inactive, &mut active_stake); + + // Remove non-validator stake. + inplace_mask_vector(&validator_forbids, &mut active_stake); + + // Normalize active stake. + inplace_normalize(&mut active_stake); + log::trace!("Active Stake: {:?}", &active_stake); + + // ============= + // == Weights == + // ============= + + // Access network weights row unnormalized. + let mut weights: Vec> = Self::unnormalized_weights_sparse(netuid_index); + log::trace!("Weights: {:?}", &weights); + + // Mask weights that are not from permitted validators. + weights = mask_rows_sparse(&validator_forbids, &weights); + log::trace!("Weights (permit): {:?}", &weights); + + // Remove self-weight by masking diagonal; keep owner_uid self-weight. + if let Some(owner_uid) = owner_uid { + weights = mask_diag_sparse_except_index(&weights, owner_uid); + } else { + weights = mask_diag_sparse(&weights); + } + log::trace!("Weights (permit+diag): {:?}", &weights); + + // Remove weights referring to deregistered neurons. + weights = vec_mask_sparse_matrix( + &weights, + &last_update, + &block_at_registration, + &|updated, registered| updated <= registered, + ); + log::trace!("Weights (permit+diag+outdate): {:?}", &weights); + + if Self::get_commit_reveal_weights_enabled(netuid) { + let mut commit_blocks: Vec = vec![u64::MAX; n as usize]; // MAX ⇒ “no active commit” + + // helper: hotkey → uid + let uid_of = |acct: &T::AccountId| terms_map.get(acct).map(|t| t.uid); + + // ---------- v2 ------------------------------------------------------ + // `WeightCommits` tuple: (hash, commit_epoch, commit_block, _). + // Expiry keys off `commit_epoch`; the column mask compares the absolute + // `commit_block` against `block_at_registration` (both block numbers). + for (who, q) in WeightCommits::::iter_prefix(netuid_index) { + for (_, commit_epoch, commit_block, _) in q.iter() { + if !Self::is_commit_expired(netuid, *commit_epoch) { + if let Some(cell) = uid_of(&who).and_then(|i| commit_blocks.get_mut(i)) { + *cell = (*cell).min(*commit_block); + } + break; // earliest active found + } + } + } + + // ---------- v4 ------------------------------------------------------ + // `TimelockedWeightCommits` is keyed by `commit_epoch`; the value tuple + // carries the absolute `commit_block` in field 1. + for (commit_epoch, q) in TimelockedWeightCommits::::iter_prefix(netuid_index) { + if Self::is_commit_expired(netuid, commit_epoch) { + continue; + } + for (who, commit_block, ..) in q.iter() { + if let Some(cell) = uid_of(who).and_then(|i| commit_blocks.get_mut(i)) { + *cell = (*cell).min(*commit_block); + } + } + } + + weights = vec_mask_sparse_matrix( + &weights, + &commit_blocks, + &block_at_registration, + &|cb, reg| cb < reg, + ); + + log::trace!( + "Commit-reveal column mask applied ({} masked rows)", + commit_blocks.iter().filter(|&&cb| cb != u64::MAX).count() + ); + } + + // Normalize remaining weights. + inplace_row_normalize_sparse(&mut weights); + log::trace!("Weights (mask+norm): {:?}", &weights); + + // ================================ + // == Consensus, Validator Trust == + // ================================ + + // Consensus majority ratio, e.g. 51%. + let kappa: I32F32 = Self::kappa_proportion_as_i32f32(netuid); + // Calculate consensus as stake-weighted median of weights. + let consensus: Vec = weighted_median_col_sparse(&active_stake, &weights, n, kappa); + log::trace!("Consensus: {:?}", &consensus); + + // Clip weights at majority consensus. + let clipped_weights: Vec> = col_clip_sparse(&weights, &consensus); + log::trace!("Clipped Weights: {:?}", &clipped_weights); + + // Calculate validator trust as sum of clipped weights set by validator. + let validator_trust: Vec = row_sum_sparse(&clipped_weights); + log::trace!("Validator Trust: {:?}", &validator_trust); + + // ============================= + // == Ranks, Trust, Incentive == + // ============================= + + // Compute ranks: r_j = SUM(i) w_ij * s_i. + let mut ranks: Vec = matmul_sparse(&clipped_weights, &active_stake, n); + + inplace_normalize(&mut ranks); // range: I32F32(0, 1) + let incentive: Vec = ranks.clone(); + log::trace!("Incentive (=Rank): {:?}", &incentive); + + // ========================= + // == Bonds and Dividends == + // ========================= + + // Get validator bonds penalty in [0, 1]. + let bonds_penalty: I32F32 = Self::bonds_penalty_proportion_as_i32f32(netuid); + // Calculate weights for bonds, apply bonds penalty to weights. + // bonds_penalty = 0: weights_for_bonds = weights.clone() + // bonds_penalty = 1: weights_for_bonds = clipped_weights.clone() + let weights_for_bonds: Vec> = + interpolate_sparse(&weights, &clipped_weights, n, bonds_penalty); + + let mut dividends: Vec; + let mut ema_bonds: Vec>; + if Yuma3On::::get(netuid) { + // Access network bonds. + let mut bonds = Self::bonds_sparse_as_u16_proportion(netuid_index); + log::trace!("Bonds: {:?}", &bonds); + + // Remove bonds referring to neurons that have registered since last tempo. + // Mask if: the last tempo block happened *before* the registration block + // ==> last_tempo <= registered + // For dynamic tempo - we pick previous-successful-epoch block: `LastMechansimStepBlock + 1` + let lms = LastMechansimStepBlock::::get(netuid); + let last_tempo: u64 = if lms == 0 { + current_block.saturating_sub(tempo) + } else { + lms.saturating_add(1) + }; + bonds = scalar_vec_mask_sparse_matrix( + &bonds, + last_tempo, + &block_at_registration, + &|last_tempo, registered| last_tempo <= registered, + ); + log::trace!("Bonds: (mask) {:?}", &bonds); + + // Compute the Exponential Moving Average (EMA) of bonds. + log::trace!("weights_for_bonds: {:?}", &weights_for_bonds); + ema_bonds = Self::ema_bonds_liquid_or_normal_sparse( + netuid_index, + &weights_for_bonds, + &bonds, + &consensus, + ); + log::trace!("emaB: {:?}", &ema_bonds); + + // Normalize EMA bonds. + let mut ema_bonds_norm = ema_bonds.clone(); + inplace_col_normalize_sparse(&mut ema_bonds_norm, n); // sum_i b_ij = 1 + log::trace!("emaB norm: {:?}", &ema_bonds_norm); + + // # === Dividend Calculation=== + let total_bonds_per_validator: Vec = + row_sum_sparse(&mat_vec_mul_sparse(&ema_bonds_norm, &incentive)); + log::trace!( + "total_bonds_per_validator: {:?}", + &total_bonds_per_validator + ); + + dividends = vec_mul(&total_bonds_per_validator, &active_stake); + inplace_normalize(&mut dividends); + log::trace!("Dividends: {:?}", ÷nds); + } else { + // original Yuma - liquid alpha disabled + // Access network bonds. + let mut bonds: Vec> = Self::unnormalized_bonds_sparse(netuid_index); + log::trace!("B: {:?}", &bonds); + + // Remove bonds referring to neurons that have registered since last tempo. + // Mask if: the last tempo block happened *before* the registration block + // ==> last_tempo <= registered + // For dynamic tempo - we pick previous-successful-epoch block: `LastMechansimStepBlock + 1` + let lms = LastMechansimStepBlock::::get(netuid); + let last_tempo: u64 = if lms == 0 { + current_block.saturating_sub(tempo) + } else { + lms.saturating_add(1) + }; + bonds = scalar_vec_mask_sparse_matrix( + &bonds, + last_tempo, + &block_at_registration, + &|last_tempo, registered| last_tempo <= registered, + ); + log::trace!("B (outdatedmask): {:?}", &bonds); + + // Normalize remaining bonds: sum_i b_ij = 1. + inplace_col_normalize_sparse(&mut bonds, n); + log::trace!("B (mask+norm): {:?}", &bonds); + + // Compute bonds delta column normalized. + let mut bonds_delta: Vec> = + row_hadamard_sparse(&weights_for_bonds, &active_stake); // ΔB = W◦S (outdated W masked) + log::trace!("ΔB: {:?}", &bonds_delta); + + // Normalize bonds delta. + inplace_col_normalize_sparse(&mut bonds_delta, n); // sum_i b_ij = 1 + log::trace!("ΔB (norm): {:?}", &bonds_delta); + + // Compute the Exponential Moving Average (EMA) of bonds. + ema_bonds = Self::ema_bonds_normal_sparse(&bonds_delta, &bonds, netuid_index); + // Normalize EMA bonds. + inplace_col_normalize_sparse(&mut ema_bonds, n); // sum_i b_ij = 1 + log::trace!("Exponential Moving Average Bonds: {:?}", &ema_bonds); + + // Compute dividends: d_i = SUM(j) b_ij * inc_j. + // range: I32F32(0, 1) + dividends = matmul_transpose_sparse(&ema_bonds, &incentive); + inplace_normalize(&mut dividends); + log::trace!("Dividends: {:?}", ÷nds); + + // Column max-upscale EMA bonds for storage: max_i w_ij = 1. + inplace_col_max_upscale_sparse(&mut ema_bonds, n); + } + + // ================================= + // == Emission and Pruning scores == + // ================================= + + // Compute normalized emission scores. range: I32F32(0, 1) + let combined_emission: Vec = incentive + .iter() + .zip(dividends.clone()) + .map(|(ii, di)| ii.saturating_add(di)) + .collect(); + let emission_sum: I32F32 = combined_emission.iter().sum(); + + let mut normalized_server_emission: Vec = incentive.clone(); // Servers get incentive. + let mut normalized_validator_emission: Vec = dividends.clone(); // Validators get dividends. + let mut normalized_combined_emission: Vec = combined_emission.clone(); + // Normalize on the sum of incentive + dividends. + inplace_normalize_i32f32_with_sum(&mut normalized_server_emission, emission_sum); + inplace_normalize_i32f32_with_sum(&mut normalized_validator_emission, emission_sum); + inplace_normalize(&mut normalized_combined_emission); + + // If emission is zero, replace emission with normalized stake. + if emission_sum == I32F32::from(0) { + // no weights set | outdated weights | self_weights + if is_zero(&active_stake) { + // no active stake + normalized_validator_emission.clone_from(&stake); // do not mask inactive, assumes stake is normalized + normalized_combined_emission.clone_from(&stake); + } else { + normalized_validator_emission.clone_from(&active_stake); // emission proportional to inactive-masked normalized stake + normalized_combined_emission.clone_from(&active_stake); + } + } + + // Compute rao based emission scores. range: I96F32(0, rao_emission) + let float_rao_emission: I96F32 = I96F32::saturating_from_num(rao_emission); + + let server_emission: Vec = normalized_server_emission + .iter() + .map(|se: &I32F32| I96F32::saturating_from_num(*se).saturating_mul(float_rao_emission)) + .collect(); + let server_emission: Vec = server_emission + .iter() + .map(|e: &I96F32| e.saturating_to_num::().into()) + .collect(); + + let validator_emission: Vec = normalized_validator_emission + .iter() + .map(|ve: &I32F32| I96F32::saturating_from_num(*ve).saturating_mul(float_rao_emission)) + .collect(); + let validator_emission: Vec = validator_emission + .iter() + .map(|e: &I96F32| e.saturating_to_num::().into()) + .collect(); + + // Only used to track emission in storage. + let combined_emission: Vec = normalized_combined_emission + .iter() + .map(|ce: &I32F32| I96F32::saturating_from_num(*ce).saturating_mul(float_rao_emission)) + .collect(); + let combined_emission: Vec = combined_emission + .iter() + .map(|e: &I96F32| AlphaBalance::from(e.saturating_to_num::())) + .collect(); + + log::trace!( + "Normalized Server Emission: {:?}", + &normalized_server_emission + ); + log::trace!("Server Emission: {:?}", &server_emission); + log::trace!( + "Normalized Validator Emission: {:?}", + &normalized_validator_emission + ); + log::trace!("Validator Emission: {:?}", &validator_emission); + log::trace!( + "Normalized Combined Emission: {:?}", + &normalized_combined_emission + ); + log::trace!("Combined Emission: {:?}", &combined_emission); + + // =========================== + // == Populate epoch output == + // =========================== + let cloned_stake_weight: Vec = stake + .iter() + .map(|xi| fixed_proportion_to_u16(*xi)) + .collect::>(); + let cloned_emission = combined_emission.clone(); + let cloned_consensus: Vec = consensus + .iter() + .map(|xi| fixed_proportion_to_u16(*xi)) + .collect::>(); + let cloned_incentive: Vec = incentive + .iter() + .map(|xi| fixed_proportion_to_u16(*xi)) + .collect::>(); + let cloned_dividends: Vec = dividends + .iter() + .map(|xi| fixed_proportion_to_u16(*xi)) + .collect::>(); + let cloned_validator_trust: Vec = validator_trust + .iter() + .map(|xi| fixed_proportion_to_u16(*xi)) + .collect::>(); + let raw_stake: Vec = total_stake + .iter() + .map(|s| s.saturating_to_num::()) + .collect::>(); + + for (_hotkey, terms) in terms_map.iter_mut() { + terms.dividend = cloned_dividends.get(terms.uid).copied().unwrap_or_default(); + terms.incentive = cloned_incentive.get(terms.uid).copied().unwrap_or_default(); + terms.validator_emission = validator_emission + .get(terms.uid) + .copied() + .unwrap_or_default(); + terms.server_emission = server_emission.get(terms.uid).copied().unwrap_or_default(); + terms.stake_weight = cloned_stake_weight + .get(terms.uid) + .copied() + .unwrap_or_default(); + terms.active = active.get(terms.uid).copied().unwrap_or_default(); + terms.emission = cloned_emission.get(terms.uid).copied().unwrap_or_default(); + terms.consensus = cloned_consensus.get(terms.uid).copied().unwrap_or_default(); + terms.validator_trust = cloned_validator_trust + .get(terms.uid) + .copied() + .unwrap_or_default(); + terms.new_validator_permit = new_validator_permits + .get(terms.uid) + .copied() + .unwrap_or_default(); + terms.stake = raw_stake.get(terms.uid).copied().unwrap_or_default().into(); + let old_validator_permit = validator_permits + .get(terms.uid) + .copied() + .unwrap_or_default(); + + // Bonds + if terms.new_validator_permit { + let ema_bond = ema_bonds.get(terms.uid).cloned().unwrap_or_default(); + terms.bond = ema_bond + .iter() + .map(|(j, value)| (*j, fixed_proportion_to_u16(*value))) + .collect(); + } else if old_validator_permit { + // Only overwrite the intersection. + terms.bond = vec![]; + } + } + + HotkeyEpochTerms(terms_map) + } +} diff --git a/pallets/subtensor/src/epoch/run_epoch/epoch_terms.rs b/pallets/subtensor/src/epoch/run_epoch/epoch_terms.rs new file mode 100644 index 0000000000..01b9913e4e --- /dev/null +++ b/pallets/subtensor/src/epoch/run_epoch/epoch_terms.rs @@ -0,0 +1,62 @@ +//! Per-hotkey epoch outputs ([`EpochTerms`]) and the [`collect_sorted_epoch_field`] helper macro. + +use alloc::collections::BTreeMap; +use sp_std::collections::btree_map::IntoIter; +use sp_std::vec::Vec; +use subtensor_runtime_common::AlphaBalance; + +/// Per-uid consensus / emission fields produced by one epoch for a single hotkey. +/// +/// `dividend` / `incentive` / `consensus` / `validator_trust` / `stake_weight` are raw `u16` +/// proportions (max-upscaled); persistence wraps them in `PerU16` at the storage boundary. +/// `bond` is the sparse validator→miner bond row as `(uid, u16)` pairs. +#[derive(Debug, Default)] +pub struct EpochTerms { + pub uid: usize, + pub dividend: u16, + pub incentive: u16, + pub validator_emission: AlphaBalance, + pub server_emission: AlphaBalance, + pub stake_weight: u16, + pub active: bool, + pub emission: AlphaBalance, + pub consensus: u16, + pub validator_trust: u16, + pub new_validator_permit: bool, + pub bond: Vec<(u16, u16)>, + pub stake: AlphaBalance, +} + +/// Map of hotkey → [`EpochTerms`] returned by [`super::epoch_mechanism`]. +pub struct HotkeyEpochTerms(pub BTreeMap); + +impl HotkeyEpochTerms { + pub fn as_map(&self) -> &BTreeMap { + &self.0 + } +} + +impl IntoIterator for HotkeyEpochTerms +where + T: frame_system::Config, + T::AccountId: Ord, +{ + type Item = (T::AccountId, EpochTerms); + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +/// Collect one [`EpochTerms`] field from a uid-sorted `&EpochTerms` slice into a parallel `Vec`. +#[macro_export] +macro_rules! collect_sorted_epoch_field { + ($sorted:expr, $field:ident) => {{ + ($sorted) + .iter() + .copied() + .map(|t| t.$field) + .collect::>() + }}; +} diff --git a/pallets/subtensor/src/epoch/run_epoch/mod.rs b/pallets/subtensor/src/epoch/run_epoch/mod.rs new file mode 100644 index 0000000000..74ac2a2336 --- /dev/null +++ b/pallets/subtensor/src/epoch/run_epoch/mod.rs @@ -0,0 +1,26 @@ +//! Subnet epoch: Yuma consensus scoring and per-hotkey emission terms. +//! +//! Production path: [`epoch_mechanism`] → [`persist_mechanism_epoch_terms`] / +//! [`persist_netuid_epoch_terms`] (usually via the coinbase / mechanism runners). +//! +//! ## Search anchors +//! +//! | Module | Owns | +//! |--------|------| +//! | [`epoch_terms`] | [`EpochTerms`], [`HotkeyEpochTerms`], [`collect_sorted_epoch_field`] | +//! | [`persist_epoch_terms`] | legacy `epoch` wrappers + storage writes | +//! | [`epoch_mechanism`] | sparse production epoch | +//! | [`epoch_dense`] | dense epoch (tests only) | +//! | [`weights_bonds_loaders`] | read weights/bonds; kappa/rho fixed casts | +//! | [`bonds_ema_liquid_alpha`] | bonds EMA, liquid alpha, `do_set_alpha_values`, bonds reset | + +use super::*; + +mod bonds_ema_liquid_alpha; +mod epoch_dense; +mod epoch_mechanism; +mod epoch_terms; +mod persist_epoch_terms; +mod weights_bonds_loaders; + +pub use epoch_terms::{EpochTerms, HotkeyEpochTerms}; diff --git a/pallets/subtensor/src/epoch/run_epoch/persist_epoch_terms.rs b/pallets/subtensor/src/epoch/run_epoch/persist_epoch_terms.rs new file mode 100644 index 0000000000..3023f667bc --- /dev/null +++ b/pallets/subtensor/src/epoch/run_epoch/persist_epoch_terms.rs @@ -0,0 +1,102 @@ +//! Legacy `epoch` / `epoch_dense` test entrypoints and persistence of epoch vectors into storage. + +use super::*; +use alloc::collections::BTreeMap; +use sp_runtime::PerU16; +use sp_std::vec::Vec; +use subtensor_runtime_common::{AlphaBalance, MechId, NetUid}; + +impl Pallet { + /// Test helper: run [`Self::epoch_mechanism`] for `MechId::MAIN` and persist terms. + pub fn epoch( + netuid: NetUid, + rao_emission: AlphaBalance, + ) -> Vec<(T::AccountId, AlphaBalance, AlphaBalance)> { + // Run mechanism-style epoch + let output = Self::epoch_mechanism(netuid, MechId::MAIN, rao_emission); + + // Persist values in legacy format + Self::persist_mechanism_epoch_terms(netuid, MechId::MAIN, output.as_map()); + Self::persist_netuid_epoch_terms(netuid, output.as_map()); + + // Remap and return + output + .into_iter() + .map(|(hotkey, terms)| (hotkey, terms.server_emission, terms.validator_emission)) + .collect() + } + + /// Test helper: dense-matrix epoch for `MechId::MAIN`. + pub fn epoch_dense( + netuid: NetUid, + rao_emission: AlphaBalance, + ) -> Vec<(T::AccountId, AlphaBalance, AlphaBalance)> { + Self::epoch_dense_mechanism_for_tests(netuid, MechId::MAIN, rao_emission) + } + + /// Write mechanism-scoped `Incentive` / `Bonds` and emit `IncentiveAlphaEmittedToMiners`. + pub fn persist_mechanism_epoch_terms( + netuid: NetUid, + mecid: MechId, + output: &BTreeMap, + ) { + let netuid_index = Self::get_mechanism_storage_index(netuid, mecid); + let mut terms_sorted: sp_std::vec::Vec<&EpochTerms> = output.values().collect(); + terms_sorted.sort_unstable_by_key(|t| t.uid); + + let incentive = collect_sorted_epoch_field!(terms_sorted, incentive); + let bonds: Vec> = terms_sorted + .iter() + .cloned() + .map(|t| t.bond.clone()) + .collect::>(); + + // Epoch math stays in raw u16; wrap into PerU16 only at the storage boundary. + let incentive: Vec = incentive.into_iter().map(PerU16::from_parts).collect(); + Incentive::::insert(netuid_index, incentive); + + let server_emission = collect_sorted_epoch_field!(terms_sorted, server_emission); + Self::deposit_event(Event::IncentiveAlphaEmittedToMiners { + netuid: netuid_index, + emissions: server_emission, + }); + + bonds + .into_iter() + .enumerate() + .for_each(|(uid_usize, bond_vec)| { + let uid: u16 = uid_usize.try_into().unwrap_or_default(); + Bonds::::insert(netuid_index, uid, bond_vec); + }); + } + + /// Write netuid-scoped active/emission/consensus/dividend/validator vectors from epoch terms. + pub fn persist_netuid_epoch_terms(netuid: NetUid, output: &BTreeMap) { + let mut terms_sorted: sp_std::vec::Vec<&EpochTerms> = output.values().collect(); + terms_sorted.sort_unstable_by_key(|t| t.uid); + + let active = collect_sorted_epoch_field!(terms_sorted, active); + let emission = collect_sorted_epoch_field!(terms_sorted, emission); + let consensus = collect_sorted_epoch_field!(terms_sorted, consensus); + let dividend = collect_sorted_epoch_field!(terms_sorted, dividend); + let validator_trust = collect_sorted_epoch_field!(terms_sorted, validator_trust); + let new_validator_permit = collect_sorted_epoch_field!(terms_sorted, new_validator_permit); + let stake_weight = collect_sorted_epoch_field!(terms_sorted, stake_weight); + + // Epoch math stays in raw u16; wrap into PerU16 only at the storage boundary. + let consensus: Vec = consensus.into_iter().map(PerU16::from_parts).collect(); + let dividend: Vec = dividend.into_iter().map(PerU16::from_parts).collect(); + let validator_trust: Vec = validator_trust + .into_iter() + .map(PerU16::from_parts) + .collect(); + + Active::::insert(netuid, active.clone()); + Emission::::insert(netuid, emission); + Consensus::::insert(netuid, consensus); + Dividends::::insert(netuid, dividend); + ValidatorTrust::::insert(netuid, validator_trust); + ValidatorPermit::::insert(netuid, new_validator_permit); + StakeWeight::::insert(netuid, stake_weight); + } +} diff --git a/pallets/subtensor/src/epoch/run_epoch/weights_bonds_loaders.rs b/pallets/subtensor/src/epoch/run_epoch/weights_bonds_loaders.rs new file mode 100644 index 0000000000..8feccb9cc5 --- /dev/null +++ b/pallets/subtensor/src/epoch/run_epoch/weights_bonds_loaders.rs @@ -0,0 +1,155 @@ +//! Load unnormalized weights/bonds from storage and convert kappa/rho/bonds_penalty to fixed-point. + +use super::*; +use crate::epoch::math::*; +use safe_math::*; +use sp_std::vec; +use sp_std::vec::Vec; +use substrate_fixed::types::I32F32; +use subtensor_runtime_common::{NetUid, NetUidStorageIndex}; + +impl Pallet { + /// Subnet `rho` hyperparameter as `I32F32` (consensus sigmoid steepness). + pub fn rho_as_i32f32(netuid: NetUid) -> I32F32 { + I32F32::saturating_from_num(Self::get_rho(netuid)) + } + /// Subnet `kappa` as a `0..=1` proportion (`storage / u16::MAX`). + pub fn kappa_proportion_as_i32f32(netuid: NetUid) -> I32F32 { + I32F32::saturating_from_num(Self::get_kappa(netuid)) + .safe_div(I32F32::saturating_from_num(u16::MAX)) + } + /// Bonds penalty hyperparameter as a `0..=1` proportion. + pub fn bonds_penalty_proportion_as_i32f32(netuid: NetUid) -> I32F32 { + I32F32::saturating_from_num(Self::get_bonds_penalty(netuid)) + .safe_div(I32F32::saturating_from_num(u16::MAX)) + } + + /// Per-uid registration block for outdated-weight masking (`0` if the uid slot is empty). + pub fn neuron_block_at_registration(netuid: NetUid) -> Vec { + let n = Self::get_subnetwork_n(netuid); + let block_at_registration: Vec = (0..n) + .map(|neuron_uid| { + if Keys::::contains_key(netuid, neuron_uid) { + Self::get_neuron_block_at_registration(netuid, neuron_uid) + } else { + 0 + } + }) + .collect(); + block_at_registration + } + + /// Output unnormalized sparse weights, input weights are assumed to be row max-upscaled in u16. + pub fn unnormalized_weights_sparse(netuid_index: NetUidStorageIndex) -> Vec> { + let (netuid, _) = Self::get_netuid_and_subid(netuid_index).unwrap_or_default(); + let n = Self::get_subnetwork_n(netuid) as usize; + let mut weights: Vec> = vec![vec![]; n]; + for (uid_i, weights_i) in + Weights::::iter_prefix(netuid_index).filter(|(uid_i, _)| *uid_i < n as u16) + { + for (uid_j, weight_ij) in weights_i.iter().filter(|(uid_j, _)| *uid_j < n as u16) { + if let Some(row) = weights.get_mut(uid_i as usize) { + row.push((*uid_j, I32F32::saturating_from_num(*weight_ij))); + } else { + log::error!("math error: uid_i {uid_i:?} is filtered to be less than n"); + } + } + } + weights + } + + /// Output unnormalized weights in [n, n] matrix, input weights are assumed to be row max-upscaled in u16. + pub fn get_weights(netuid_index: NetUidStorageIndex) -> Vec> { + let (netuid, _) = Self::get_netuid_and_subid(netuid_index).unwrap_or_default(); + let n = Self::get_subnetwork_n(netuid) as usize; + let mut weights: Vec> = vec![vec![I32F32::saturating_from_num(0.0); n]; n]; + for (uid_i, weights_vec) in + Weights::::iter_prefix(netuid_index).filter(|(uid_i, _)| *uid_i < n as u16) + { + for (uid_j, weight_ij) in weights_vec + .into_iter() + .filter(|(uid_j, _)| *uid_j < n as u16) + { + if let Some(cell) = weights + .get_mut(uid_i as usize) + .and_then(|row| row.get_mut(uid_j as usize)) + { + *cell = I32F32::saturating_from_num(weight_ij); + } + } + } + weights + } + + /// Output unnormalized sparse bonds, input bonds are assumed to be column max-upscaled in u16. + /// Sparse bonds from storage as `I32F32` (column max-upscaled u16 input; not row-normalized). + pub fn unnormalized_bonds_sparse(netuid_index: NetUidStorageIndex) -> Vec> { + let (netuid, _) = Self::get_netuid_and_subid(netuid_index).unwrap_or_default(); + let n = Self::get_subnetwork_n(netuid) as usize; + let mut bonds: Vec> = vec![vec![]; n]; + for (uid_i, bonds_vec) in + Bonds::::iter_prefix(netuid_index).filter(|(uid_i, _)| *uid_i < n as u16) + { + for (uid_j, bonds_ij) in bonds_vec { + if let Some(row) = bonds.get_mut(uid_i as usize) { + row.push((uid_j, u16_to_fixed(bonds_ij))); + } else { + // If the index is unexpectedly out of bounds, skip and log math error + log::error!( + "math error: bonds row index out of bounds (uid_i={uid_i}, n={n}, netuid_index={netuid_index})", + ); + } + } + } + + bonds + } + + /// Output unnormalized bonds in [n, n] matrix, input bonds are assumed to be column max-upscaled in u16. + pub fn get_bonds(netuid_index: NetUidStorageIndex) -> Vec> { + let (netuid, _) = Self::get_netuid_and_subid(netuid_index).unwrap_or_default(); + let n: usize = Self::get_subnetwork_n(netuid) as usize; + let mut bonds: Vec> = vec![vec![I32F32::saturating_from_num(0.0); n]; n]; + for (uid_i, bonds_vec) in + Bonds::::iter_prefix(netuid_index).filter(|(uid_i, _)| *uid_i < n as u16) + { + for (uid_j, bonds_ij) in bonds_vec.into_iter().filter(|(uid_j, _)| *uid_j < n as u16) { + if let Some(row) = bonds.get_mut(uid_i as usize) { + if let Some(cell) = row.get_mut(uid_j as usize) { + *cell = u16_to_fixed(bonds_ij); + } else { + log::error!( + "math error: uid_j index out of bounds (uid_i={uid_i}, uid_j={uid_j}, n={n}, netuid_index={netuid_index})" + ); + } + } else { + log::error!( + "math error: uid_i row index out of bounds (uid_i={uid_i}, n={n}, netuid_index={netuid_index})" + ); + } + } + } + + bonds + } + + pub fn get_bonds_fixed_proportion(netuid: NetUidStorageIndex) -> Vec> { + let mut bonds = Self::get_bonds(netuid); + bonds.iter_mut().for_each(|bonds_row| { + bonds_row + .iter_mut() + .for_each(|bond| *bond = i32f32_as_u16_proportion(*bond)); + }); + bonds + } + + pub fn bonds_sparse_as_u16_proportion(netuid: NetUidStorageIndex) -> Vec> { + let mut bonds = Self::unnormalized_bonds_sparse(netuid); + bonds.iter_mut().for_each(|bonds_row| { + bonds_row + .iter_mut() + .for_each(|(_, bond)| *bond = i32f32_as_u16_proportion(*bond)); + }); + bonds + } +} diff --git a/pallets/subtensor/src/extensions/mod.rs b/pallets/subtensor/src/extensions/mod.rs index 9171c222be..4c781b7843 100644 --- a/pallets/subtensor/src/extensions/mod.rs +++ b/pallets/subtensor/src/extensions/mod.rs @@ -1,3 +1,21 @@ +//! Signed-transaction extensions for Subtensor (`TransactionExtension`). +//! +//! Unlike [`crate::guards`] (which run on every `dispatch`, including nested +//! proxy calls), types here participate in the outer signed extrinsic pipeline: +//! they validate and charge weight before the call is included, and map pallet +//! [`Error`](crate::Error) values onto +//! [`CustomTransactionError`](subtensor_runtime_common::CustomTransactionError) +//! for the pool. +//! +//! ## Search anchors +//! +//! | Type | Role | +//! |------|------| +//! | [`SubtensorTransactionExtension`] | Runs coldkey-swap + weight/rate/delegate/serve/EVM guard checks at validate time | +//! +//! Guard implementations live under [`crate::guards`]; this module only wires them +//! into `TransactionExtension::{weight, validate}`. + mod subtensor; pub use subtensor::*; diff --git a/pallets/subtensor/src/extensions/subtensor.rs b/pallets/subtensor/src/extensions/subtensor.rs index 7899ed855e..6d5c6778a7 100644 --- a/pallets/subtensor/src/extensions/subtensor.rs +++ b/pallets/subtensor/src/extensions/subtensor.rs @@ -1,6 +1,16 @@ +//! [`SubtensorTransactionExtension`]: signed-tx wiring for Subtensor dispatch guards. +//! +//! At `validate`, runs the same `check` / `applies_to` helpers as +//! [`crate::guards`]' `DispatchExtension` types so mempool rejection matches +//! pre-dispatch failure. Weight is the sum of those guards' extension weights. +//! +//! Deliberately does **not** re-run these checks in `prepare` (see +//! `impl_tx_ext_default!(…; prepare)`): dispatch-time guards remain authoritative +//! for nested / proxy paths. + use crate::{ Call, CheckColdkeySwap, CheckDelegateTake, CheckEvmKeyAssociation, CheckRateLimits, - CheckServingEndpoints, CheckWeights, Config, Error, guards::applicable_call, + CheckServingEndpoints, CheckWeights, Config, Error, guards::subtensor_call_if, }; use codec::{Decode, DecodeWithMemTracking, Encode}; use frame_support::{ @@ -20,9 +30,15 @@ use sp_std::marker::PhantomData; use subtensor_macros::freeze_struct; use subtensor_runtime_common::CustomTransactionError; -type CallOf = ::RuntimeCall; -type OriginOf = ::RuntimeOrigin; +/// Runtime-wide call type used by the signed transaction extension. +type RuntimeCallOf = ::RuntimeCall; +/// Origin type carried by [`RuntimeCallOf`] (`Signed` / `Root` / `None`). +type RuntimeOriginOf = ::RuntimeOrigin; +/// Maps select pallet [`Error`]s to mempool [`CustomTransactionError`] codes. +/// +/// Unlisted variants become [`CustomTransactionError::BadRequest`]. Prefer adding +/// a dedicated code here when clients must distinguish a rejection reason in the pool. #[allow(deprecated)] impl From> for CustomTransactionError { fn from(error: Error) -> Self { @@ -60,7 +76,12 @@ impl From> for CustomTransactionError { } } -#[freeze_struct("2e02eb32e5cb25d3")] +/// Signed `TransactionExtension` that runs Subtensor [`crate::guards`] at validate time. +/// +/// Zero-sized (`PhantomData` only). The on-wire `IDENTIFIER` string +/// `"SubtensorTransactionExtension"` is part of the extrinsic format — do not rename +/// without a coordinated client/runtime migration. +#[freeze_struct("58df59e2e22b4ca0")] #[derive(Default, Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo)] pub struct SubtensorTransactionExtension(pub PhantomData); @@ -75,13 +96,19 @@ impl SubtensorTransactionExtension { Self(Default::default()) } - fn check(origin: &OriginOf, call: &CallOf) -> Result<(), Error> + /// Run coldkey-swap plus applicable weight/rate/delegate/serve/EVM guard checks. + /// + /// Unsigned / non-signer origins skip all checks (same as the individual guards). + fn run_dispatch_extension_checks( + origin: &RuntimeOriginOf, + call: &RuntimeCallOf, + ) -> Result<(), Error> where T: pallet_shield::Config, - CallOf: Dispatchable> + RuntimeCallOf: Dispatchable> + IsSubType> + IsSubType>, - OriginOf: OriginTrait, + RuntimeOriginOf: OriginTrait, { let Some(who) = origin.as_signer() else { return Ok(()); @@ -89,19 +116,19 @@ impl SubtensorTransactionExtension { CheckColdkeySwap::::check(who, call)?; - if let Some(call) = applicable_call(call, CheckWeights::::applies_to) { + if let Some(call) = subtensor_call_if(call, CheckWeights::::applies_to) { CheckWeights::::check(who, call)?; } - if let Some(call) = applicable_call(call, CheckRateLimits::::applies_to) { + if let Some(call) = subtensor_call_if(call, CheckRateLimits::::applies_to) { CheckRateLimits::::check(who, call)?; } - if let Some(call) = applicable_call(call, CheckDelegateTake::::applies_to) { + if let Some(call) = subtensor_call_if(call, CheckDelegateTake::::applies_to) { CheckDelegateTake::::check(who, call)?; } - if let Some(call) = applicable_call(call, CheckServingEndpoints::::applies_to) { + if let Some(call) = subtensor_call_if(call, CheckServingEndpoints::::applies_to) { CheckServingEndpoints::::check(who, call)?; } - if let Some(call) = applicable_call(call, CheckEvmKeyAssociation::::applies_to) { + if let Some(call) = subtensor_call_if(call, CheckEvmKeyAssociation::::applies_to) { CheckEvmKeyAssociation::::check(who, call)?; } @@ -109,13 +136,16 @@ impl SubtensorTransactionExtension { } } -impl TransactionExtension> for SubtensorTransactionExtension +impl TransactionExtension> for SubtensorTransactionExtension where T: Config + pallet_shield::Config + Send + Sync + TypeInfo, - CallOf: Dispatchable, Info = DispatchInfo, PostInfo = PostDispatchInfo> - + IsSubType> + RuntimeCallOf: Dispatchable< + RuntimeOrigin = RuntimeOriginOf, + Info = DispatchInfo, + PostInfo = PostDispatchInfo, + > + IsSubType> + IsSubType>, - OriginOf: Clone + OriginTrait, + RuntimeOriginOf: Clone + OriginTrait, { const IDENTIFIER: &'static str = "SubtensorTransactionExtension"; @@ -123,32 +153,36 @@ where type Val = (); type Pre = (); - fn weight(&self, call: &CallOf) -> Weight { + fn weight(&self, call: &RuntimeCallOf) -> Weight { use DispatchExtension as DE; - as DE>>::weight(call) - .saturating_add( as DE>>::weight(call)) - .saturating_add( as DE>>::weight(call)) - .saturating_add( as DE>>::weight(call)) - .saturating_add( as DE>>::weight(call)) - .saturating_add( as DE>>::weight(call)) + as DE>>::weight(call) + .saturating_add( as DE>>::weight(call)) + .saturating_add( as DE>>::weight(call)) + .saturating_add( as DE>>::weight(call)) + .saturating_add( as DE>>::weight( + call, + )) + .saturating_add( as DE>>::weight( + call, + )) } fn validate( &self, - origin: OriginOf, - call: &CallOf, - _info: &DispatchInfoOf>, + origin: RuntimeOriginOf, + call: &RuntimeCallOf, + _info: &DispatchInfoOf>, _len: usize, _self_implicit: Self::Implicit, _inherited_implication: &impl Implication, _source: TransactionSource, - ) -> ValidateResult> { - Self::check(&origin, call) + ) -> ValidateResult> { + Self::run_dispatch_extension_checks(&origin, call) .map(|()| (Default::default(), (), origin)) .map_err(|error| TransactionValidityError::from(CustomTransactionError::from(error))) } - impl_tx_ext_default!(CallOf; prepare); + impl_tx_ext_default!(RuntimeCallOf; prepare); } #[cfg(test)] @@ -177,7 +211,7 @@ mod tests { DispatchInfoOf::<::RuntimeCall>::default() } - fn validate_signed( + fn validate_signed_transaction_extension( signer: U256, call: &RuntimeCall, ) -> Result { @@ -213,7 +247,7 @@ mod tests { new_test_ext(1).execute_with(|| { let call = RuntimeCall::System(frame_system::Call::remark { remark: vec![] }); - assert_ok!(validate_signed(U256::from(1), &call)); + assert_ok!(validate_signed_transaction_extension(U256::from(1), &call)); }); } @@ -230,11 +264,11 @@ mod tests { coldkey, (System::block_number(), new_coldkey_hash), ); - let err = validate_signed(coldkey, &call).unwrap_err(); + let err = validate_signed_transaction_extension(coldkey, &call).unwrap_err(); assert_eq!(err, CustomTransactionError::ColdkeyInSwapSchedule.into()); ColdkeySwapDisputes::::insert(coldkey, System::block_number()); - let err = validate_signed(coldkey, &call).unwrap_err(); + let err = validate_signed_transaction_extension(coldkey, &call).unwrap_err(); assert_eq!(err, CustomTransactionError::ColdkeySwapDisputed.into()); }); } @@ -273,7 +307,7 @@ mod tests { }); assert_eq!(call.get_dispatch_info().pays_fee, Pays::No); - let err = validate_signed(hotkey, &call).unwrap_err(); + let err = validate_signed_transaction_extension(hotkey, &call).unwrap_err(); assert_eq!(err, CustomTransactionError::RateLimitExceeded.into()); }); } diff --git a/pallets/subtensor/src/guards/check_coldkey_swap.rs b/pallets/subtensor/src/guards/check_coldkey_swap.rs index b0b7a57413..450b8784f8 100644 --- a/pallets/subtensor/src/guards/check_coldkey_swap.rs +++ b/pallets/subtensor/src/guards/check_coldkey_swap.rs @@ -1,4 +1,4 @@ -use super::{CallOf, DispatchableOriginOf}; +use super::{GuardsRuntimeCallOf, RuntimeCallOriginOf}; use crate::weights::WeightInfo; use crate::{Call, ColdkeySwapAnnouncements, ColdkeySwapDisputes, Config, Error}; use frame_support::{ @@ -20,7 +20,7 @@ use sp_std::marker::PhantomData; /// Non-signed origins pass through. /// /// Because this is a `DispatchExtension` (not a `TransactionExtension`), it fires at every -/// `call.dispatch(origin)` site — including inside the proxy pallet's `do_proxy()`. +/// `call.dispatch(origin)` site — including inside the proxy pallet's `dispatch_filtered_proxy_call()`. /// This means nested proxies of any depth are handled automatically with the real /// resolved origin. pub struct CheckColdkeySwap(PhantomData); @@ -28,9 +28,10 @@ pub struct CheckColdkeySwap(PhantomData); impl CheckColdkeySwap where T: Config + pallet_shield::Config, - CallOf: IsSubType> + IsSubType>, + GuardsRuntimeCallOf: IsSubType> + IsSubType>, { - pub fn check(who: &T::AccountId, call: &CallOf) -> Result<(), Error> { + /// Reject `who`'s call while a coldkey swap is announced or disputed for that account. + pub fn check(who: &T::AccountId, call: &GuardsRuntimeCallOf) -> Result<(), Error> { if !ColdkeySwapAnnouncements::::contains_key(who) { return Ok(()); } @@ -39,14 +40,15 @@ where return Err(Error::::ColdkeySwapDisputed); } - if Self::is_allowed_during_swap(call) { + if Self::is_call_allowed_during_coldkey_swap(call) { Ok(()) } else { Err(Error::::ColdkeySwapAnnounced) } } - fn is_allowed_during_swap(call: &CallOf) -> bool { + /// Swap lifecycle calls plus shield `submit_encrypted` (MEV-protected path). + fn is_call_allowed_during_coldkey_swap(call: &GuardsRuntimeCallOf) -> bool { matches!( call.is_sub_type(), Some( @@ -68,17 +70,18 @@ where ::RuntimeCall: Dispatchable + IsSubType> + IsSubType>, - DispatchableOriginOf: OriginTrait, + RuntimeCallOriginOf: OriginTrait, { type Pre = (); - fn weight(_call: &CallOf) -> Weight { + fn weight(_call: &GuardsRuntimeCallOf) -> Weight { + // Always charged: any signed call may be blocked by swap state. ::WeightInfo::check_coldkey_swap_extension() } fn pre_dispatch( - origin: &DispatchableOriginOf, - call: &CallOf, + origin: &RuntimeCallOriginOf, + call: &GuardsRuntimeCallOf, ) -> Result { // Only care about signed origins. // Root is already bypassed by the extension before we get here. diff --git a/pallets/subtensor/src/guards/check_delegate_take.rs b/pallets/subtensor/src/guards/check_delegate_take.rs index 5e2088ddf9..5d9a429360 100644 --- a/pallets/subtensor/src/guards/check_delegate_take.rs +++ b/pallets/subtensor/src/guards/check_delegate_take.rs @@ -1,4 +1,4 @@ -use super::{CallOf, DispatchableOriginOf, applicable_call}; +use super::{GuardsRuntimeCallOf, RuntimeCallOriginOf, subtensor_call_if}; use crate::weights::WeightInfo; use crate::{Call, Config, Error, Pallet}; use frame_support::{ @@ -11,11 +11,12 @@ use sp_std::marker::PhantomData; /// Dispatch extension for delegate-take bounds and ownership preconditions. /// -/// Signed increase/decrease take calls are checked before dispatch; unrelated -/// calls and non-signed origins pass through. +/// Signed `increase_take` / `decrease_take` calls are checked before dispatch; +/// unrelated calls and non-signed origins pass through. pub struct CheckDelegateTake(PhantomData); impl CheckDelegateTake { + /// Whether this guard should charge weight / run for `call`. pub(crate) fn applies_to(call: &Call) -> bool { matches!( call, @@ -23,6 +24,7 @@ impl CheckDelegateTake { ) } + /// Ensure take is within `[min, max]` and `who` owns the target hotkey. pub fn check(who: &T::AccountId, call: &Call) -> Result<(), Error> { match call { Call::increase_take { hotkey, take } | Call::decrease_take { hotkey, take } => { @@ -39,29 +41,29 @@ impl CheckDelegateTake { } } -impl DispatchExtension> for CheckDelegateTake +impl DispatchExtension> for CheckDelegateTake where T: Config, - CallOf: Dispatchable + IsSubType>, - DispatchableOriginOf: OriginTrait, + GuardsRuntimeCallOf: Dispatchable + IsSubType>, + RuntimeCallOriginOf: OriginTrait, { type Pre = (); - fn weight(call: &CallOf) -> Weight { - applicable_call(call, Self::applies_to) + fn weight(call: &GuardsRuntimeCallOf) -> Weight { + subtensor_call_if(call, Self::applies_to) .map(|_| ::WeightInfo::check_delegate_take_extension()) .unwrap_or(Weight::zero()) } fn pre_dispatch( - origin: &DispatchableOriginOf, - call: &CallOf, + origin: &RuntimeCallOriginOf, + call: &GuardsRuntimeCallOf, ) -> Result { let Some(who) = origin.as_signer() else { return Ok(()); }; - let Some(call) = applicable_call(call, Self::applies_to) else { + let Some(call) = subtensor_call_if(call, Self::applies_to) else { return Ok(()); }; diff --git a/pallets/subtensor/src/guards/check_evm_key_association.rs b/pallets/subtensor/src/guards/check_evm_key_association.rs index d9b69e1a7d..d3b91069b3 100644 --- a/pallets/subtensor/src/guards/check_evm_key_association.rs +++ b/pallets/subtensor/src/guards/check_evm_key_association.rs @@ -1,4 +1,4 @@ -use super::{CallOf, DispatchableOriginOf, applicable_call}; +use super::{GuardsRuntimeCallOf, RuntimeCallOriginOf, subtensor_call_if}; use crate::weights::WeightInfo; use crate::{Call, Config, Error, Pallet}; use frame_support::{ @@ -11,15 +11,18 @@ use sp_std::marker::PhantomData; /// Dispatch extension for EVM-key association preconditions. /// -/// Signed EVM-key association calls are checked for subnet registration and -/// cooldown before dispatch; unrelated calls and non-signed origins pass through. +/// Signed `associate_evm_key` calls require the signer hotkey to be registered on +/// `netuid` and outside the association cooldown; unrelated calls and non-signed +/// origins pass through. Signature / EIP-191 validation remains in the extrinsic. pub struct CheckEvmKeyAssociation(PhantomData); impl CheckEvmKeyAssociation { + /// Whether this guard should charge weight / run for `call`. pub(crate) fn applies_to(call: &Call) -> bool { matches!(call, Call::associate_evm_key { .. }) } + /// Ensure `who` is registered on `netuid` and past the EVM-associate rate limit. pub fn check(who: &T::AccountId, call: &Call) -> Result<(), Error> { match call { Call::associate_evm_key { netuid, .. } => { @@ -34,29 +37,29 @@ impl CheckEvmKeyAssociation { } } -impl DispatchExtension> for CheckEvmKeyAssociation +impl DispatchExtension> for CheckEvmKeyAssociation where T: Config, - CallOf: Dispatchable + IsSubType>, - DispatchableOriginOf: OriginTrait, + GuardsRuntimeCallOf: Dispatchable + IsSubType>, + RuntimeCallOriginOf: OriginTrait, { type Pre = (); - fn weight(call: &CallOf) -> Weight { - applicable_call(call, Self::applies_to) + fn weight(call: &GuardsRuntimeCallOf) -> Weight { + subtensor_call_if(call, Self::applies_to) .map(|_| ::WeightInfo::check_evm_key_association_extension()) .unwrap_or(Weight::zero()) } fn pre_dispatch( - origin: &DispatchableOriginOf, - call: &CallOf, + origin: &RuntimeCallOriginOf, + call: &GuardsRuntimeCallOf, ) -> Result { let Some(who) = origin.as_signer() else { return Ok(()); }; - let Some(call) = applicable_call(call, Self::applies_to) else { + let Some(call) = subtensor_call_if(call, Self::applies_to) else { return Ok(()); }; diff --git a/pallets/subtensor/src/guards/check_rate_limits.rs b/pallets/subtensor/src/guards/check_rate_limits.rs index e12c9d064b..66894402a6 100644 --- a/pallets/subtensor/src/guards/check_rate_limits.rs +++ b/pallets/subtensor/src/guards/check_rate_limits.rs @@ -1,4 +1,4 @@ -use super::{CallOf, DispatchableOriginOf, applicable_call}; +use super::{GuardsRuntimeCallOf, RuntimeCallOriginOf, subtensor_call_if}; use crate::weights::WeightInfo; use crate::{Call, Config, Error, Pallet, TransactionType}; use frame_support::{ @@ -12,11 +12,15 @@ use subtensor_runtime_common::{NetUid, NetUidStorageIndex}; /// Dispatch extension for rate-limit checks that are safe to reject before dispatch. /// -/// Signed weight and network-registration calls are checked before dispatch; -/// unrelated calls and non-signed origins pass through. +/// Covers weight commit/set (when commit-reveal is off for set paths) and +/// `register_network`. Unrelated calls and non-signed origins pass through. +/// +/// Deliberately does **not** rate-limit `set_weights` / `set_mechanism_weights` +/// when commit-reveal is enabled — those paths are gated elsewhere. pub struct CheckRateLimits(PhantomData); impl CheckRateLimits { + /// Whether this guard should charge weight / run for `call`. pub(crate) fn applies_to(call: &Call) -> bool { matches!( call, @@ -28,7 +32,11 @@ impl CheckRateLimits { ) } - fn check_weights_rate_limit( + /// Per-uid weight rate limit keyed by [`NetUidStorageIndex`]. + /// + /// If `who` is not registered on `netuid`, this returns `Ok` so the extrinsic + /// can surface the registration error instead of a rate-limit error. + fn ensure_uid_weights_rate_limit( who: &T::AccountId, netuid: NetUid, netuid_index: NetUidStorageIndex, @@ -46,24 +54,27 @@ impl CheckRateLimits { } } + /// Apply the rate-limit rules for a Subtensor weight / network-registration call. pub fn check(who: &T::AccountId, call: &Call) -> Result<(), Error> { match call { - Call::commit_weights { netuid, .. } => Self::check_weights_rate_limit( + Call::commit_weights { netuid, .. } => Self::ensure_uid_weights_rate_limit( who, *netuid, NetUidStorageIndex::from(*netuid), Error::::CommittingWeightsTooFast, ), - Call::commit_mechanism_weights { netuid, mecid, .. } => Self::check_weights_rate_limit( - who, - *netuid, - Pallet::::get_mechanism_storage_index(*netuid, *mecid), - Error::::CommittingWeightsTooFast, - ), + Call::commit_mechanism_weights { netuid, mecid, .. } => { + Self::ensure_uid_weights_rate_limit( + who, + *netuid, + Pallet::::get_mechanism_storage_index(*netuid, *mecid), + Error::::CommittingWeightsTooFast, + ) + } Call::set_weights { netuid, .. } if !Pallet::::get_commit_reveal_weights_enabled(*netuid) => { - Self::check_weights_rate_limit( + Self::ensure_uid_weights_rate_limit( who, *netuid, NetUidStorageIndex::from(*netuid), @@ -73,7 +84,7 @@ impl CheckRateLimits { Call::set_mechanism_weights { netuid, mecid, .. } if !Pallet::::get_commit_reveal_weights_enabled(*netuid) => { - Self::check_weights_rate_limit( + Self::ensure_uid_weights_rate_limit( who, *netuid, Pallet::::get_mechanism_storage_index(*netuid, *mecid), @@ -90,29 +101,29 @@ impl CheckRateLimits { } } -impl DispatchExtension> for CheckRateLimits +impl DispatchExtension> for CheckRateLimits where T: Config, - CallOf: Dispatchable + IsSubType>, - DispatchableOriginOf: OriginTrait, + GuardsRuntimeCallOf: Dispatchable + IsSubType>, + RuntimeCallOriginOf: OriginTrait, { type Pre = (); - fn weight(call: &CallOf) -> Weight { - applicable_call(call, Self::applies_to) + fn weight(call: &GuardsRuntimeCallOf) -> Weight { + subtensor_call_if(call, Self::applies_to) .map(|_| ::WeightInfo::check_rate_limits_extension()) .unwrap_or(Weight::zero()) } fn pre_dispatch( - origin: &DispatchableOriginOf, - call: &CallOf, + origin: &RuntimeCallOriginOf, + call: &GuardsRuntimeCallOf, ) -> Result { let Some(who) = origin.as_signer() else { return Ok(()); }; - let Some(call) = applicable_call(call, Self::applies_to) else { + let Some(call) = subtensor_call_if(call, Self::applies_to) else { return Ok(()); }; diff --git a/pallets/subtensor/src/guards/check_serving_endpoints.rs b/pallets/subtensor/src/guards/check_serving_endpoints.rs index f8b2da64ed..2469e0bf7e 100644 --- a/pallets/subtensor/src/guards/check_serving_endpoints.rs +++ b/pallets/subtensor/src/guards/check_serving_endpoints.rs @@ -1,4 +1,4 @@ -use super::{CallOf, DispatchableOriginOf, applicable_call}; +use super::{GuardsRuntimeCallOf, RuntimeCallOriginOf, subtensor_call_if}; use crate::weights::WeightInfo; use crate::{Call, Config, Error, Pallet}; use frame_support::{ @@ -9,13 +9,15 @@ use frame_support::{ use sp_runtime::traits::Dispatchable; use sp_std::marker::PhantomData; -/// Dispatch extension for axon/prometheus endpoint validation. +/// Dispatch extension for axon / prometheus endpoint validation. /// -/// Signed serving calls are checked before dispatch; unrelated calls and -/// non-signed origins pass through. +/// Signed `serve_axon`, `serve_axon_tls`, and `serve_prometheus` calls are +/// validated (registration, IP/port, serve rate limit) before dispatch; +/// unrelated calls and non-signed origins pass through. pub struct CheckServingEndpoints(PhantomData); impl CheckServingEndpoints { + /// Whether this guard should charge weight / run for `call`. pub(crate) fn applies_to(call: &Call) -> bool { matches!( call, @@ -23,6 +25,7 @@ impl CheckServingEndpoints { ) } + /// Run [`Pallet::validate_serve_axon`] / [`Pallet::validate_serve_prometheus`] for `call`. pub fn check(who: &T::AccountId, call: &Call) -> Result<(), Error> { match call { Call::serve_axon { @@ -71,29 +74,29 @@ impl CheckServingEndpoints { } } -impl DispatchExtension> for CheckServingEndpoints +impl DispatchExtension> for CheckServingEndpoints where T: Config, - CallOf: Dispatchable + IsSubType>, - DispatchableOriginOf: OriginTrait, + GuardsRuntimeCallOf: Dispatchable + IsSubType>, + RuntimeCallOriginOf: OriginTrait, { type Pre = (); - fn weight(call: &CallOf) -> Weight { - applicable_call(call, Self::applies_to) + fn weight(call: &GuardsRuntimeCallOf) -> Weight { + subtensor_call_if(call, Self::applies_to) .map(|_| ::WeightInfo::check_serving_endpoints_extension()) .unwrap_or(Weight::zero()) } fn pre_dispatch( - origin: &DispatchableOriginOf, - call: &CallOf, + origin: &RuntimeCallOriginOf, + call: &GuardsRuntimeCallOf, ) -> Result { let Some(who) = origin.as_signer() else { return Ok(()); }; - let Some(call) = applicable_call(call, Self::applies_to) else { + let Some(call) = subtensor_call_if(call, Self::applies_to) else { return Ok(()); }; diff --git a/pallets/subtensor/src/guards/check_weights.rs b/pallets/subtensor/src/guards/check_weights.rs index c116071ba6..97e85c499a 100644 --- a/pallets/subtensor/src/guards/check_weights.rs +++ b/pallets/subtensor/src/guards/check_weights.rs @@ -1,4 +1,4 @@ -use super::{CallOf, DispatchableOriginOf, applicable_call}; +use super::{GuardsRuntimeCallOf, RuntimeCallOriginOf, subtensor_call_if}; use crate::weights::WeightInfo; use crate::{Call, Config, Error, Pallet, WeightCommits}; use frame_support::{ @@ -11,15 +11,18 @@ use sp_runtime::traits::Dispatchable; use sp_std::{collections::vec_deque::VecDeque, marker::PhantomData, vec::Vec}; use subtensor_runtime_common::{NetUid, NetUidStorageIndex}; -type WeightCommitQueue = VecDeque<(H256, u64, u64, u64)>; +/// Queued weight commits: `(commit_hash, commit_epoch, _, _)` as stored in [`WeightCommits`]. +type WeightCommitEpochQueue = VecDeque<(H256, u64, u64, u64)>; /// Dispatch extension for weight-setting preconditions. /// /// Signed weight calls are checked for batch shape, min stake, and commit/reveal /// prerequisites before dispatch; unrelated calls and non-signed origins pass through. +/// Rate limits for the same calls live in [`super::CheckRateLimits`]. pub struct CheckWeights(PhantomData); impl CheckWeights { + /// Whether this guard should charge weight / run for `call`. pub(crate) fn applies_to(call: &Call) -> bool { matches!( call, @@ -38,13 +41,15 @@ impl CheckWeights { ) } + /// Batch lengths, min stake, then commit/reveal / timelock round checks. pub fn check(who: &T::AccountId, call: &Call) -> Result<(), Error> { - Self::check_input_lengths(call)?; - Self::check_min_stake(who, call)?; - Self::check_commit_reveal(who, call) + Self::ensure_batch_input_lengths(call)?; + Self::ensure_weights_min_stake(who, call)?; + Self::ensure_commit_reveal_ready(who, call) } - fn check_input_lengths(call: &Call) -> Result<(), Error> { + /// Parallel batch vectors must share a common length (`InputLengthsUnequal` otherwise). + fn ensure_batch_input_lengths(call: &Call) -> Result<(), Error> { let lengths_match = match call { Call::batch_commit_weights { netuids, @@ -76,7 +81,10 @@ impl CheckWeights { } } - fn ensure_min_stake(who: &T::AccountId, netuid: NetUid) -> Result<(), Error> { + fn ensure_hotkey_meets_weights_stake( + who: &T::AccountId, + netuid: NetUid, + ) -> Result<(), Error> { if Pallet::::check_weights_min_stake(who, netuid) { Ok(()) } else { @@ -84,7 +92,7 @@ impl CheckWeights { } } - fn check_min_stake(who: &T::AccountId, call: &Call) -> Result<(), Error> { + fn ensure_weights_min_stake(who: &T::AccountId, call: &Call) -> Result<(), Error> { match call { Call::commit_weights { netuid, .. } | Call::commit_mechanism_weights { netuid, .. } @@ -96,12 +104,12 @@ impl CheckWeights { | Call::commit_timelocked_weights { netuid, .. } | Call::commit_timelocked_mechanism_weights { netuid, .. } | Call::commit_crv3_mechanism_weights { netuid, .. } => { - Self::ensure_min_stake(who, *netuid) + Self::ensure_hotkey_meets_weights_stake(who, *netuid) } Call::batch_commit_weights { netuids, .. } | Call::batch_set_weights { netuids, .. } => { for netuid in netuids.iter() { - Self::ensure_min_stake(who, (*netuid).into())?; + Self::ensure_hotkey_meets_weights_stake(who, (*netuid).into())?; } Ok(()) } @@ -109,7 +117,8 @@ impl CheckWeights { } } - fn find_commit_epoch(commits: &WeightCommitQueue, hash: H256) -> Option { + /// Look up the epoch recorded for `hash` in a [`WeightCommitEpochQueue`]. + fn find_weight_commit_epoch(commits: &WeightCommitEpochQueue, hash: H256) -> Option { commits .iter() .find_map(|(commit_hash, commit_epoch, _, _)| { @@ -117,7 +126,8 @@ impl CheckWeights { }) } - fn check_reveal( + /// Single reveal: commit must exist and the current block must be in the reveal window. + fn ensure_reveal_in_window( who: &T::AccountId, netuid: NetUid, netuid_index: NetUidStorageIndex, @@ -129,8 +139,8 @@ impl CheckWeights { let commits = WeightCommits::::get(netuid_index, who).ok_or(Error::::NoWeightsCommitFound)?; let hash = Pallet::::get_commit_hash(who, netuid_index, uids, values, salt, version_key); - let commit_epoch = - Self::find_commit_epoch(&commits, hash).ok_or(Error::::NoWeightsCommitFound)?; + let commit_epoch = Self::find_weight_commit_epoch(&commits, hash) + .ok_or(Error::::NoWeightsCommitFound)?; if Pallet::::is_reveal_block_range(netuid, commit_epoch) { Ok(()) @@ -139,7 +149,7 @@ impl CheckWeights { } } - fn check_batch_reveal( + fn ensure_batch_reveal_in_window( who: &T::AccountId, netuid: NetUid, uids_list: &[Vec], @@ -166,8 +176,8 @@ impl CheckWeights { { let hash = Pallet::::get_commit_hash(who, netuid_index, uids, values, salt, *version_key); - let commit_epoch = - Self::find_commit_epoch(&commits, hash).ok_or(Error::::NoWeightsCommitFound)?; + let commit_epoch = Self::find_weight_commit_epoch(&commits, hash) + .ok_or(Error::::NoWeightsCommitFound)?; if !Pallet::::is_reveal_block_range(netuid, commit_epoch) { return Err(Error::::RevealTooEarly); @@ -177,7 +187,8 @@ impl CheckWeights { Ok(()) } - fn check_commit_reveal(who: &T::AccountId, call: &Call) -> Result<(), Error> { + /// Reveal-window / commit-exists checks, plus drand reveal-round freshness for timelocked commits. + fn ensure_commit_reveal_ready(who: &T::AccountId, call: &Call) -> Result<(), Error> { match call { Call::reveal_weights { netuid, @@ -185,7 +196,7 @@ impl CheckWeights { values, salt, version_key, - } => Self::check_reveal( + } => Self::ensure_reveal_in_window( who, *netuid, NetUidStorageIndex::from(*netuid), @@ -201,7 +212,7 @@ impl CheckWeights { values, salt, version_key, - } => Self::check_reveal( + } => Self::ensure_reveal_in_window( who, *netuid, Pallet::::get_mechanism_storage_index(*netuid, *mecid), @@ -216,7 +227,7 @@ impl CheckWeights { values_list, salts_list, version_keys, - } => Self::check_batch_reveal( + } => Self::ensure_batch_reveal_in_window( who, *netuid, uids_list, @@ -236,29 +247,29 @@ impl CheckWeights { } } -impl DispatchExtension> for CheckWeights +impl DispatchExtension> for CheckWeights where T: Config, - CallOf: Dispatchable + IsSubType>, - DispatchableOriginOf: OriginTrait, + GuardsRuntimeCallOf: Dispatchable + IsSubType>, + RuntimeCallOriginOf: OriginTrait, { type Pre = (); - fn weight(call: &CallOf) -> Weight { - applicable_call(call, Self::applies_to) + fn weight(call: &GuardsRuntimeCallOf) -> Weight { + subtensor_call_if(call, Self::applies_to) .map(|_| ::WeightInfo::check_weights_extension()) .unwrap_or(Weight::zero()) } fn pre_dispatch( - origin: &DispatchableOriginOf, - call: &CallOf, + origin: &RuntimeCallOriginOf, + call: &GuardsRuntimeCallOf, ) -> Result { let Some(who) = origin.as_signer() else { return Ok(()); }; - let Some(call) = applicable_call(call, Self::applies_to) else { + let Some(call) = subtensor_call_if(call, Self::applies_to) else { return Ok(()); }; diff --git a/pallets/subtensor/src/guards/mod.rs b/pallets/subtensor/src/guards/mod.rs index 3865352858..00e2566cca 100644 --- a/pallets/subtensor/src/guards/mod.rs +++ b/pallets/subtensor/src/guards/mod.rs @@ -1,3 +1,24 @@ +//! Pre-dispatch guards for Subtensor extrinsics (`DispatchExtension` + shared helpers). +//! +//! These types run at every `call.dispatch(origin)` site (including nested proxy +//! dispatches), rejecting invalid signed calls before the pallet extrinsic body. +//! The signed-tx path also reuses the same `check` / `applies_to` helpers from +//! [`crate::extensions::SubtensorTransactionExtension`]. +//! +//! ## Search anchors +//! +//! | Guard | Blocks / validates | +//! |-------|--------------------| +//! | [`CheckColdkeySwap`] | Non-swap calls while a coldkey swap is announced/disputed | +//! | [`CheckWeights`] | Weight batch shape, min stake, commit/reveal readiness | +//! | [`CheckRateLimits`] | Weight-set and `register_network` rate limits | +//! | [`CheckDelegateTake`] | Delegate take bounds + coldkey ownership | +//! | [`CheckServingEndpoints`] | Axon / prometheus serve preconditions | +//! | [`CheckEvmKeyAssociation`] | EVM-key association registration + cooldown | +//! +//! [`subtensor_call_if`] returns the inner [`Call`] when a guard's `applies_to` +//! predicate matches — used by weight accounting and by the transaction extension. + mod check_coldkey_swap; mod check_delegate_take; mod check_evm_key_association; @@ -16,16 +37,23 @@ pub use check_rate_limits::*; pub use check_serving_endpoints::*; pub use check_weights::*; -pub(crate) type CallOf = ::RuntimeCall; -pub(crate) type DispatchableOriginOf = as Dispatchable>::RuntimeOrigin; +/// Runtime-wide call type (`frame_system::Config::RuntimeCall`) used by guard extensions. +pub(crate) type GuardsRuntimeCallOf = ::RuntimeCall; + +/// Origin type carried by [`GuardsRuntimeCallOf`] dispatches (signed / root / none). +pub(crate) type RuntimeCallOriginOf = as Dispatchable>::RuntimeOrigin; -pub(crate) fn applicable_call( - call: &CallOf, +/// If `call` is a Subtensor [`Call`] and `applies_to` returns true, yield that call. +/// +/// Returns `None` for non-Subtensor calls or Subtensor calls outside the guard's +/// scope (so the guard charges zero weight and skips `pre_dispatch` work). +pub(crate) fn subtensor_call_if( + call: &GuardsRuntimeCallOf, applies_to: impl FnOnce(&Call) -> bool, ) -> Option<&Call> where T: Config, - CallOf: IsSubType>, + GuardsRuntimeCallOf: IsSubType>, { let call = call.is_sub_type()?; applies_to(call).then_some(call) diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index a39ec97424..ce2323ca54 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1221,6 +1221,7 @@ pub mod pallet { true } + /// Global floor (in blocks) for per-subnet `ActivityCutoff`; subnets cannot set activity cutoff below this. #[pallet::storage] pub type MinActivityCutoff = StorageValue<_, u16, ValueQuery, DefaultMinActivityCutoff>; @@ -1240,7 +1241,7 @@ pub mod pallet { pub type DissolveNetworkScheduleDuration = StorageValue<_, BlockNumberFor, ValueQuery, DefaultDissolveNetworkScheduleDuration>; - /// DMap ( netuid, coldkey ) --> blocknumber | last hotkey swap on network. + /// Block number of the last successful hotkey swap for a coldkey on a subnet; used for swap rate limits. #[pallet::storage] pub type LastHotkeySwapOnNetuid = StorageDoubleMap< _, @@ -1317,23 +1318,23 @@ pub mod pallet { #[pallet::storage] pub type TaoWeight = StorageValue<_, u64, ValueQuery, DefaultTaoWeight>; - /// ITEM --> CK burn + /// Fraction of coldkey swap fee burned, stored as u64 fixed-point (same scale as other global burn params). #[pallet::storage] pub type CKBurn = StorageValue<_, u64, ValueQuery, DefaultCKBurn>; - /// ITEM ( default_delegate_take ) + /// Global maximum validator delegate take as `PerU16` (parts per 65535). #[pallet::storage] pub type MaxDelegateTake = StorageValue<_, PerU16, ValueQuery, DefaultDelegateTake>; - /// ITEM ( min_delegate_take ) + /// Global minimum validator delegate take as `PerU16` (parts per 65535). #[pallet::storage] pub type MinDelegateTake = StorageValue<_, PerU16, ValueQuery, DefaultMinDelegateTake>; - /// ITEM ( default_childkey_take ) + /// Global maximum childkey take as `PerU16` (parts per 65535). #[pallet::storage] pub type MaxChildkeyTake = StorageValue<_, PerU16, ValueQuery, DefaultMaxChildKeyTake>; - /// ITEM ( min_childkey_take ) + /// Global minimum childkey take as `PerU16` (parts per 65535). #[pallet::storage] pub type MinChildkeyTake = StorageValue<_, PerU16, ValueQuery, DefaultMinChildKeyTake>; @@ -1369,7 +1370,7 @@ pub mod pallet { ValueQuery, >; - /// DMAP ( netuid, parent ) --> (Vec<(proportion,child)>, cool_down_block) + /// Pending child-key set for a parent on a subnet, with cool-down block before the linkage becomes active. #[pallet::storage] pub type PendingChildKeys = StorageDoubleMap< _, @@ -1382,7 +1383,7 @@ pub mod pallet { DefaultPendingChildkeys, >; - /// DMAP ( parent, netuid ) --> Vec<(proportion,child)> + /// Active child-key edges from a parent hotkey on a subnet; proportions are u64 fixed-point shares that must sum to at most 1.0. #[pallet::storage] pub type ChildKeys = StorageDoubleMap< _, @@ -1395,7 +1396,7 @@ pub mod pallet { DefaultAccountLinkage, >; - /// DMAP ( child, netuid ) --> Vec<(proportion,parent)> + /// Inverse of `ChildKeys`: parent edges into a child hotkey on a subnet with the same u64 proportion units. #[pallet::storage] pub type ParentKeys = StorageDoubleMap< _, @@ -1435,8 +1436,8 @@ pub mod pallet { >; // Coinbase - /// ITEM ( global_block_emission ) #[deprecated(note = "Use calculate_block_emission() or the block emission RPC instead.")] + /// Global TAO minted per block, in rao (1e9 rao = 1 TAO). #[pallet::storage] pub type BlockEmission = StorageValue<_, u64, ValueQuery, DefaultBlockEmission>; @@ -1466,24 +1467,24 @@ pub mod pallet { #[pallet::storage] pub type SubnetLimit = StorageValue<_, u16, ValueQuery, DefaultSubnetLimit>; - /// ITEM ( total_issuance ) + /// Sum of all circulating TAO, in rao; must stay consistent with mint/burn accounting. #[pallet::storage] pub type TotalIssuance = StorageValue<_, TaoBalance, ValueQuery, DefaultTotalIssuance>; - /// ITEM ( total_stake ) + /// Sum of all TAO currently staked into subnets, in rao. #[pallet::storage] pub type TotalStake = StorageValue<_, TaoBalance, ValueQuery, DefaultZeroTao>; - /// ITEM ( moving_alpha ) -- subnet moving alpha. + /// Global EMA smoothing factor for subnet moving price (`I96F32` fixed-point). #[pallet::storage] pub type SubnetMovingAlpha = StorageValue<_, I96F32, ValueQuery, DefaultMovingAlpha>; - /// MAP ( netuid ) --> moving_price | The subnet moving price. + /// Per-subnet EMA of alpha/TAO price as `I96F32` fixed-point. #[pallet::storage] pub type SubnetMovingPrice = StorageMap<_, Identity, NetUid, I96F32, ValueQuery, DefaultMovingPrice>; - /// MAP ( netuid ) --> root_prop | The subnet root proportion. + /// Per-subnet root emission proportion as `U96F32` fixed-point in [0, 1]. #[pallet::storage] pub type RootProp = StorageMap<_, Identity, NetUid, U96F32, ValueQuery, DefaultRootProp>; @@ -1808,6 +1809,7 @@ pub mod pallet { pub fn DefaultNetTaoFlowEnabled() -> bool { true } + /// When true, emission uses net TAO flow (user minus protocol); when false, uses gross user flow only. #[pallet::storage] pub type NetTaoFlowEnabled = StorageValue<_, bool, ValueQuery, DefaultNetTaoFlowEnabled>; @@ -1828,7 +1830,7 @@ pub mod pallet { I64F64::saturating_from_num(0) } #[pallet::storage] - /// ITEM --> TAO Flow Cutoff + /// Minimum net TAO flow (`I64F64`) a subnet must clear to receive flow-weighted emission share. pub type TaoFlowCutoff = StorageValue<_, I64F64, ValueQuery, DefaultFlowCutoff>; #[pallet::type_value] /// Default value for flow normalization exponent. @@ -1836,7 +1838,7 @@ pub mod pallet { U64F64::saturating_from_num(1) } #[pallet::storage] - /// ITEM --> Flow Normalization Exponent (p) + /// Exponent `p` (`U64F64`) applied when normalizing positive subnet flows into emission weights. pub type FlowNormExponent = StorageValue<_, U64F64, ValueQuery, DefaultFlowNormExponent>; #[pallet::type_value] @@ -1854,49 +1856,49 @@ pub mod pallet { 216_000 } #[pallet::storage] - /// ITEM --> Flow EMA smoothing factor (flow alpha), u64 normalized + /// Flow EMA alpha as u64 with 2^63 fixed-point scale (see `FlowHalfLife` for the default half-life in blocks). pub type FlowEmaSmoothingFactor = StorageValue<_, u64, ValueQuery, DefaultFlowEmaSmoothingFactor>; // Global Parameters - /// StorageItem Global Used Work. + /// PoW registration work already consumed, keyed by work hash; value is the block when first seen. #[pallet::storage] pub type UsedWork = StorageMap<_, Identity, Vec, u64, ValueQuery>; - /// ITEM( global_max_registrations_per_block ) + /// Per-subnet cap on registrations allowed in a single block. #[pallet::storage] pub type MaxRegistrationsPerBlock = StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultMaxRegistrationsPerBlock>; - /// ITEM( total_number_of_existing_networks ) + /// Count of currently existing subnets (active network entries). #[pallet::storage] pub type TotalNetworks = StorageValue<_, u16, ValueQuery>; - /// ITEM( network_immunity_period ) + /// Global immunity duration (in blocks) for newly registered subnets before pruning rules apply. #[pallet::storage] pub type NetworkImmunityPeriod = StorageValue<_, u64, ValueQuery, DefaultNetworkImmunityPeriod>; - /// ITEM( start_call_delay ) + /// Delay (in blocks) after subnet creation before `start_call` may enable emissions. #[pallet::storage] pub type StartCallDelay = StorageValue<_, u64, ValueQuery, T::InitialStartCallDelay>; - /// ITEM( min_network_lock_cost ) + /// Floor for the dynamic subnet registration lock cost, in rao. #[pallet::storage] pub type NetworkMinLockCost = StorageValue<_, TaoBalance, ValueQuery, DefaultNetworkMinLockCost>; - /// ITEM( last_network_lock_cost ) + /// Most recent subnet registration lock cost charged, in rao; feeds the lock-cost schedule. #[pallet::storage] pub type NetworkLastLockCost = StorageValue<_, TaoBalance, ValueQuery, DefaultNetworkMinLockCost>; - /// ITEM( network_lock_reduction_interval ) + /// Interval (in blocks) over which the network lock cost decays toward `NetworkMinLockCost`. #[pallet::storage] pub type NetworkLockReductionInterval = StorageValue<_, u64, ValueQuery, DefaultNetworkLockReductionInterval>; - /// ITEM( subnet_owner_cut ) + /// Global owner cut of subnet emissions as `PerU16` (parts per 65535). #[pallet::storage] pub type SubnetOwnerCut = StorageValue<_, u16, ValueQuery, DefaultSubnetOwnerCut>; @@ -1906,12 +1908,12 @@ pub mod pallet { true } - /// MAP ( netuid ) --> owner_cut_enabled + /// Per-subnet toggle: when false, the owner cut is not paid out for that subnet. #[pallet::storage] pub type OwnerCutEnabled = StorageMap<_, Identity, NetUid, bool, ValueQuery, DefaultOwnerCutEnabled>; - /// ITEM( network_rate_limit ) + /// Minimum blocks between successful network registrations (global rate limit). #[pallet::storage] pub type NetworkRateLimit = StorageValue<_, u64, ValueQuery, DefaultNetworkRateLimit>; @@ -1919,7 +1921,7 @@ pub mod pallet { #[pallet::storage] pub type NominatorMinRequiredStake = StorageValue<_, u64, ValueQuery, DefaultZeroU64>; - /// ITEM( weights_version_key_rate_limit ) --- Rate limit in tempos. + /// Minimum tempos between `WeightsVersionKey` updates for a subnet. #[pallet::storage] pub type WeightsVersionKeyRateLimit = StorageValue<_, u64, ValueQuery, DefaultWeightsVersionKeyRateLimit>; @@ -1931,23 +1933,23 @@ pub mod pallet { StorageMap<_, Identity, RateLimitKey, u64, ValueQuery, DefaultZeroU64>; // Subnet Locks - /// MAP ( netuid ) --> transfer_toggle + /// Per-subnet toggle allowing alpha token transfers when true. #[pallet::storage] pub type TransferToggle = StorageMap<_, Identity, NetUid, bool, ValueQuery, DefaultTrue>; - /// MAP ( netuid ) --> total_subnet_locked + /// Total TAO locked into a subnet's registration/lock accounting, in rao. #[pallet::storage] pub type SubnetLocked = StorageMap<_, Identity, NetUid, TaoBalance, ValueQuery, DefaultZeroTao>; - /// MAP ( netuid ) --> largest_locked + /// Largest single lock contribution observed for a subnet, in rao. #[pallet::storage] pub type LargestLocked = StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultZeroU64>; // Tempos - /// MAP ( netuid ) --> tempo + /// Subnet epoch length in blocks; consensus and emission steps align to this cadence. #[pallet::storage] pub type Tempo = StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultTempo>; @@ -1996,12 +1998,12 @@ pub mod pallet { StorageMap<_, Identity, NetUid, u32, ValueQuery, DefaultActivityCutoffFactorMilli>; // Subnet Parameters - /// MAP ( netuid ) --> block number of first emission + /// Block number when a subnet first became eligible to emit; zero/default means not yet started. #[pallet::storage] pub type FirstEmissionBlockNumber = StorageMap<_, Identity, NetUid, u64, OptionQuery>; - /// MAP ( netuid ) --> subnet mechanism + /// Mechanism identifier for the subnet's consensus/emission path (dynamic vs root-style). #[pallet::storage] pub type SubnetMechanism = StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultZeroU16>; @@ -2010,12 +2012,12 @@ pub mod pallet { #[pallet::storage] pub type SubnetworkN = StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultN>; - /// MAP ( netuid ) --> network_is_added + /// Whether `netuid` currently exists as an added subnet (false after dissolve). #[pallet::storage] pub type NetworksAdded = StorageMap<_, Identity, NetUid, bool, ValueQuery, DefaultNeworksAdded>; - /// DMAP ( hotkey, netuid ) --> bool + /// True when the hotkey holds a UID on the given subnet. #[pallet::storage] pub type IsNetworkMember = StorageDoubleMap< _, @@ -2028,17 +2030,17 @@ pub mod pallet { DefaultIsNetworkMember, >; - /// MAP ( netuid ) --> network_registration_allowed + /// When true, burn/regular registration is allowed on the subnet. #[pallet::storage] pub type NetworkRegistrationAllowed = StorageMap<_, Identity, NetUid, bool, ValueQuery, DefaultRegistrationAllowed>; - /// MAP ( netuid ) --> network_pow_allowed + /// When true, proof-of-work registration is allowed on the subnet. #[pallet::storage] pub type NetworkPowRegistrationAllowed = StorageMap<_, Identity, NetUid, bool, ValueQuery, DefaultRegistrationAllowed>; - /// MAP ( netuid ) --> block_created + /// Block number when the subnet was registered/created. #[pallet::storage] pub type NetworkRegisteredAt = StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultNetworkRegisteredAt>; @@ -2055,22 +2057,22 @@ pub mod pallet { #[pallet::storage] pub type RegisteredSubnetCounter = StorageMap<_, Identity, NetUid, u64, ValueQuery>; - /// MAP ( netuid ) --> pending_server_emission + /// Accumulated unpaid miner/server emission for the subnet, in alpha rao-equivalent units. #[pallet::storage] pub type PendingServerEmission = StorageMap<_, Identity, NetUid, AlphaBalance, ValueQuery, DefaultZeroAlpha>; - /// MAP ( netuid ) --> pending_validator_emission + /// Accumulated unpaid validator emission for the subnet, in alpha units. #[pallet::storage] pub type PendingValidatorEmission = StorageMap<_, Identity, NetUid, AlphaBalance, ValueQuery, DefaultZeroAlpha>; - /// MAP ( netuid ) --> pending_root_alpha_emission + /// Accumulated unpaid root alpha dividends for the subnet, in alpha units. #[pallet::storage] pub type PendingRootAlphaDivs = StorageMap<_, Identity, NetUid, AlphaBalance, ValueQuery, DefaultZeroAlpha>; - /// MAP ( netuid ) --> pending_owner_cut + /// Accumulated unpaid owner-cut emission for the subnet, in alpha units. #[pallet::storage] pub type PendingOwnerCut = StorageMap<_, Identity, NetUid, AlphaBalance, ValueQuery, DefaultZeroAlpha>; @@ -2094,76 +2096,76 @@ pub mod pallet { pub type BlocksSinceLastStep = StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultBlocksSinceLastStep>; - /// MAP ( netuid ) --> last_mechanism_step_block + /// Block of the last successful mechanism/epoch step for the subnet. #[pallet::storage] pub type LastMechansimStepBlock = StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultLastMechanismStepBlock>; - /// MAP ( netuid ) --> subnet_owner + /// Coldkey that owns the subnet and may set owner hyperparameters. #[pallet::storage] pub type SubnetOwner = StorageMap<_, Identity, NetUid, T::AccountId, ValueQuery, DefaultSubnetOwner>; - /// MAP ( netuid ) --> subnet_owner_hotkey + /// Hotkey designated by the subnet owner for owner-cut / identity linkage. #[pallet::storage] pub type SubnetOwnerHotkey = StorageMap<_, Identity, NetUid, T::AccountId, ValueQuery, DefaultSubnetOwner>; - /// MAP ( netuid ) --> recycle_or_burn + /// Per-subnet policy selecting whether registration fees are recycled or burned. #[pallet::storage] pub type RecycleOrBurn = StorageMap<_, Identity, NetUid, RecycleOrBurnEnum, ValueQuery, DefaultRecycleOrBurn>; - /// MAP ( netuid ) --> serving_rate_limit + /// Minimum blocks between axon/prometheus serve updates for a hotkey on the subnet. #[pallet::storage] pub type ServingRateLimit = StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultServingRateLimit>; - /// MAP ( netuid ) --> Rho + /// Yuma consensus rho hyperparameter for the subnet (`u16` scaled consensus constant). #[pallet::storage] pub type Rho = StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultRho>; - /// MAP ( netuid ) --> AlphaSigmoidSteepness + /// Steepness of the alpha sigmoid used in consensus weighting (stored as `i16`). #[pallet::storage] pub type AlphaSigmoidSteepness = StorageMap<_, Identity, NetUid, i16, ValueQuery, DefaultAlphaSigmoidSteepness>; - /// MAP ( netuid ) --> Kappa + /// Yuma consensus kappa majority threshold for the subnet (`u16` scaled). #[pallet::storage] pub type Kappa = StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultKappa>; - /// MAP ( netuid ) --> registrations_this_interval + /// Registration count in the current difficulty/burn adjustment interval. #[pallet::storage] pub type RegistrationsThisInterval = StorageMap<_, Identity, NetUid, u16, ValueQuery>; - /// MAP ( netuid ) --> pow_registrations_this_interval + /// PoW registration count in the current adjustment interval. #[pallet::storage] pub type POWRegistrationsThisInterval = StorageMap<_, Identity, NetUid, u16, ValueQuery>; - /// MAP ( netuid ) --> burn_registrations_this_interval + /// Burn registration count in the current adjustment interval. #[pallet::storage] pub type BurnRegistrationsThisInterval = StorageMap<_, Identity, NetUid, u16, ValueQuery>; - /// MAP ( netuid ) --> min_allowed_uids + /// Minimum UID capacity the subnet must keep (cannot shrink below this). #[pallet::storage] pub type MinAllowedUids = StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultMinAllowedUids>; - /// MAP ( netuid ) --> max_allowed_uids + /// Maximum UIDs allowed on the subnet (hard capacity). #[pallet::storage] pub type MaxAllowedUids = StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultMaxAllowedUids>; - /// MAP ( netuid ) --> immunity_period + /// Newly registered UID immunity duration in blocks before pruning eligibility. #[pallet::storage] pub type ImmunityPeriod = StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultImmunityPeriod>; - /// MAP ( netuid ) --> activity_cutoff // #[deprecated(note = "Replaced by `ActivityCutoffFactorMilli` (per-mille of `Tempo`).")] + /// Legacy activity cutoff in blocks; prefer `ActivityCutoffFactorMilli` (per-mille of tempo). #[pallet::storage] pub type ActivityCutoff = StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultActivityCutoff>; @@ -2173,67 +2175,67 @@ pub mod pallet { u16::MAX } - /// MAP ( netuid ) --> max_weight_limit + /// Maximum weight value a validator may set to a peer (`u16` weight units). #[pallet::storage] pub type MaxWeightsLimit = StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultMaxWeightsLimit>; - /// MAP ( netuid ) --> weights_version_key + /// Subnet weights version key; setters must match this value to submit weights. #[pallet::storage] pub type WeightsVersionKey = StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultWeightsVersionKey>; - /// MAP ( netuid ) --> min_allowed_weights + /// Minimum number of nonzero weights a validator must set when committing weights. #[pallet::storage] pub type MinAllowedWeights = StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultMinAllowedWeights>; - /// MAP ( netuid ) --> max_allowed_validators + /// Maximum number of UIDs granted validator permit on the subnet. #[pallet::storage] pub type MaxAllowedValidators = StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultMaxAllowedValidators>; - /// MAP ( netuid ) --> adjustment_interval + /// Length (in blocks) of the registration burn/difficulty adjustment window. #[pallet::storage] pub type AdjustmentInterval = StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultAdjustmentInterval>; - /// MAP ( netuid ) --> bonds_moving_average + /// EMA coefficient for bonds updates (`u64` fixed-point moving-average factor). #[pallet::storage] pub type BondsMovingAverage = StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultBondsMovingAverage>; - /// MAP ( netuid ) --> bonds_penalty + /// Penalty applied to bonds when a validator is inactive (`u16` scaled). #[pallet::storage] pub type BondsPenalty = StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultBondsPenalty>; - /// MAP ( netuid ) --> bonds_reset + /// When true, bonds are reset on the next applicable epoch transition. #[pallet::storage] pub type BondsResetOn = StorageMap<_, Identity, NetUid, bool, ValueQuery, DefaultBondsResetOn>; - /// MAP ( netuid ) --> weights_set_rate_limit + /// Minimum blocks between weight-setting extrinsics for a UID on the subnet. #[pallet::storage] pub type WeightsSetRateLimit = StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultWeightsSetRateLimit>; - /// MAP ( netuid ) --> validator_prune_len + /// Number of lowest-ranked validators pruned per epoch when over capacity. #[pallet::storage] pub type ValidatorPruneLen = StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultValidatorPruneLen>; - /// MAP ( netuid ) --> scaling_law_power + /// Power-law exponent for stake/weight scaling (`u16` scaled). #[pallet::storage] pub type ScalingLawPower = StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultScalingLawPower>; - /// MAP ( netuid ) --> target_registrations_this_interval + /// Target registrations per adjustment interval used to tune burn/difficulty. #[pallet::storage] pub type TargetRegistrationsPerInterval = StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultTargetRegistrationsPerInterval>; - /// MAP ( netuid ) --> adjustment_alpha + /// EMA smoothing factor for burn/difficulty adjustments (`u64` fixed-point). #[pallet::storage] pub type AdjustmentAlpha = StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultAdjustmentAlpha>; @@ -2243,50 +2245,50 @@ pub mod pallet { pub type CommitRevealWeightsEnabled = StorageMap<_, Identity, NetUid, bool, ValueQuery, DefaultCommitRevealWeightsEnabled>; - /// MAP ( netuid ) --> Burn + /// Current burn registration cost for the subnet, in rao. #[pallet::storage] pub type Burn = StorageMap<_, Identity, NetUid, TaoBalance, ValueQuery, DefaultBurn>; - /// MAP ( netuid ) --> Difficulty + /// Current PoW registration difficulty for the subnet. #[pallet::storage] pub type Difficulty = StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultDifficulty>; - /// MAP ( netuid ) --> MinBurn + /// Floor for dynamic burn registration cost, in rao. #[pallet::storage] pub type MinBurn = StorageMap<_, Identity, NetUid, TaoBalance, ValueQuery, DefaultMinBurn>; - /// MAP ( netuid ) --> MaxBurn + /// Ceiling for dynamic burn registration cost, in rao. #[pallet::storage] pub type MaxBurn = StorageMap<_, Identity, NetUid, TaoBalance, ValueQuery, DefaultMaxBurn>; - /// MAP ( netuid ) --> MinDifficulty + /// Floor for dynamic PoW registration difficulty. #[pallet::storage] pub type MinDifficulty = StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultMinDifficulty>; - /// MAP ( netuid ) --> MaxDifficulty + /// Ceiling for dynamic PoW registration difficulty. #[pallet::storage] pub type MaxDifficulty = StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultMaxDifficulty>; - /// MAP ( netuid ) --> Block at last adjustment. + /// Block when burn/difficulty was last adjusted for the subnet. #[pallet::storage] pub type LastAdjustmentBlock = StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultLastAdjustmentBlock>; - /// MAP ( netuid ) --> Registrations of this Block. + /// Registrations accepted on this subnet in the current block (reset each block). #[pallet::storage] pub type RegistrationsThisBlock = StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultRegistrationsThisBlock>; - /// MAP ( netuid ) --> Halving time of average moving price. + /// Halving time (in blocks) for the subnet moving-price EMA. #[pallet::storage] pub type EMAPriceHalvingBlocks = StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultEMAPriceMovingBlocks>; - /// MAP ( netuid ) --> global_RAO_recycled_for_registration + /// Cumulative RAO recycled via registration fees on the subnet. #[pallet::storage] pub type RAORecycledForRegistration = StorageMap< _, @@ -2297,20 +2299,20 @@ pub mod pallet { DefaultRAORecycledForRegistration, >; - /// ITEM ( max_epochs_per_block ) + /// Hard cap on how many subnet epochs may execute in a single block. #[pallet::storage] pub type MaxEpochsPerBlock = StorageValue<_, u8, ValueQuery, DefaultMaxEpochsPerBlock>; - /// ITEM ( tx_rate_limit ) + /// Global minimum blocks between general rate-limited transactions for an account. #[pallet::storage] pub type TxRateLimit = StorageValue<_, u64, ValueQuery, DefaultTxRateLimit>; - /// ITEM ( tx_delegate_take_rate_limit ) + /// Minimum blocks between delegate-take updates for an account. #[pallet::storage] pub type TxDelegateTakeRateLimit = StorageValue<_, u64, ValueQuery, DefaultTxDelegateTakeRateLimit>; - /// ITEM ( tx_childkey_take_rate_limit ) + /// Minimum blocks between childkey-take updates for an account. #[pallet::storage] pub type TxChildkeyTakeRateLimit = StorageValue<_, u64, ValueQuery, DefaultTxChildKeyTakeRateLimit>; @@ -2325,7 +2327,7 @@ pub mod pallet { pub type Yuma3On = StorageMap<_, Blake2_128Concat, NetUid, bool, ValueQuery, DefaultYuma3>; - /// MAP ( netuid ) --> (alpha_low, alpha_high) + /// Liquid-alpha bounds `(alpha_low, alpha_high)` as `u16` pairs for the subnet. #[pallet::storage] pub type AlphaValues = StorageMap<_, Identity, NetUid, (u16, u16), ValueQuery, DefaultAlphaValues>; @@ -2335,20 +2337,20 @@ pub mod pallet { pub type SubtokenEnabled = StorageMap<_, Identity, NetUid, bool, ValueQuery, DefaultFalse>; - /// ITEM ( dissolve_cleanup_queue ) Networks dissolved but some storage not removed yet + /// Netuids whose dissolve completed but residual storage still needs chunked cleanup. #[pallet::storage] pub type DissolveCleanupQueue = StorageValue<_, Vec, ValueQuery>; - /// ITEM ( current_dissolve_cleanup_status ) dissolve status for the network + /// In-progress dissolve cleanup cursor/status for the network currently being swept. #[pallet::storage] pub type CurrentDissolveCleanupStatus = StorageValue<_, DissolveCleanupStatus, OptionQuery>; - /// ITEM ( network_registration_queue ) Network registrations waiting to be executed. + /// Queued network registrations waiting for the start-block schedule to execute. #[pallet::storage] pub type NetworkRegistrationQueue = StorageValue<_, Vec>>, ValueQuery>; - /// MAP ( coldkey ) --> lock_id + /// Next proxy/lock id counter used while holding registration lock deposits. #[pallet::storage] pub type NetworkRegistrationLockId = StorageValue<_, u32, ValueQuery>; @@ -2414,7 +2416,7 @@ pub mod pallet { 1 } - /// MAP ( netuid ) --> Burn key limit + /// Max owner-associated UIDs that may remain immune from pruning on the subnet. #[pallet::storage] pub type ImmuneOwnerUidsLimit = StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultImmuneOwnerUidsLimit>; @@ -2425,12 +2427,12 @@ pub mod pallet { pub type StakeWeight = StorageMap<_, Identity, NetUid, Vec, ValueQuery, EmptyU16Vec>; - /// DMAP ( netuid, hotkey ) --> uid + /// Hotkey → UID map on a subnet; absent means the hotkey is not registered there. #[pallet::storage] pub type Uids = StorageDoubleMap<_, Identity, NetUid, Blake2_128Concat, T::AccountId, u16, OptionQuery>; - /// DMAP ( netuid, uid ) --> hotkey + /// UID → hotkey inverse map; UID indices are dense in `0..SubnetworkN`. #[pallet::storage] pub type Keys = StorageDoubleMap< _, @@ -2443,51 +2445,51 @@ pub mod pallet { DefaultKey, >; - /// MAP ( netuid ) --> (hotkey, se, ve) + /// Pending per-hotkey emission tuples `(hotkey, server_emission, validator_emission)` awaiting distribution. #[pallet::storage] pub type LoadedEmission = StorageMap<_, Identity, NetUid, Vec<(T::AccountId, u64, u64)>, OptionQuery>; - /// MAP ( netuid ) --> active + /// Per-UID activity flags from the last epoch (`true` = set weights recently enough). #[pallet::storage] pub type Active = StorageMap<_, Identity, NetUid, Vec, ValueQuery, EmptyBoolVec>; - /// MAP ( netuid ) --> consensus + /// Per-UID consensus ranks from the last epoch as `PerU16`. #[pallet::storage] pub type Consensus = StorageMap<_, Identity, NetUid, Vec, ValueQuery, EmptyPerU16Vec>; - /// MAP ( netuid ) --> incentive + /// Per-UID miner incentive from the last epoch as `PerU16` (indexed by mechanism storage index). #[pallet::storage] pub type Incentive = StorageMap<_, Identity, NetUidStorageIndex, Vec, ValueQuery, EmptyPerU16Vec>; - /// MAP ( netuid ) --> dividends + /// Per-UID validator dividends from the last epoch as `PerU16`. #[pallet::storage] pub type Dividends = StorageMap<_, Identity, NetUid, Vec, ValueQuery, EmptyPerU16Vec>; - /// MAP ( netuid ) --> emission + /// Per-UID alpha emission from the last epoch, in alpha units. #[pallet::storage] pub type Emission = StorageMap<_, Identity, NetUid, Vec, ValueQuery>; - /// MAP ( netuid ) --> last_update + /// Per-UID block of last weights update (mechanism-scoped storage index). #[pallet::storage] pub type LastUpdate = StorageMap<_, Identity, NetUidStorageIndex, Vec, ValueQuery, EmptyU64Vec>; - /// MAP ( netuid ) --> validator_trust + /// Per-UID validator trust scores from the last epoch as `PerU16`. #[pallet::storage] pub type ValidatorTrust = StorageMap<_, Identity, NetUid, Vec, ValueQuery, EmptyPerU16Vec>; - /// MAP ( netuid ) --> validator_permit + /// Per-UID validator permit flags (`true` means the UID may set weights). #[pallet::storage] pub type ValidatorPermit = StorageMap<_, Identity, NetUid, Vec, ValueQuery, EmptyBoolVec>; - /// DMAP ( netuid, uid ) --> weights + /// Sparse weight edges from a UID: `Vec<(target_uid, weight_u16)>` on a mechanism index. #[pallet::storage] pub type Weights = StorageDoubleMap< _, @@ -2500,7 +2502,7 @@ pub mod pallet { DefaultWeights, >; - /// DMAP ( netuid, uid ) --> bonds + /// Sparse bond edges from a UID: `Vec<(target_uid, bond_u16)>` on a mechanism index. #[pallet::storage] pub type Bonds = StorageDoubleMap< _, @@ -2513,7 +2515,7 @@ pub mod pallet { DefaultBonds, >; - /// DMAP ( netuid, uid ) --> block_at_registration + /// Block when each UID registered; anchors immunity-period calculations. #[pallet::storage] pub type BlockAtRegistration = StorageDoubleMap< _, @@ -2526,7 +2528,7 @@ pub mod pallet { DefaultBlockAtRegistration, >; - /// MAP ( netuid, hotkey ) --> axon_info + /// Latest axon endpoint metadata published by a hotkey on the subnet. #[pallet::storage] pub type Axons = StorageDoubleMap< _, @@ -2538,7 +2540,7 @@ pub mod pallet { OptionQuery, >; - /// MAP ( netuid, hotkey ) --> certificate + /// TLS/neuron certificate bytes published by a hotkey on the subnet. #[pallet::storage] pub type NeuronCertificates = StorageDoubleMap< _, @@ -2550,7 +2552,7 @@ pub mod pallet { OptionQuery, >; - /// MAP ( netuid, hotkey ) --> prometheus_info + /// Latest prometheus endpoint metadata published by a hotkey on the subnet. #[pallet::storage] pub type Prometheus = StorageDoubleMap< _, @@ -2562,12 +2564,12 @@ pub mod pallet { OptionQuery, >; - /// MAP ( coldkey ) --> identity + /// On-chain coldkey identity profile (`ChainIdentityOfV2`), if set. #[pallet::storage] pub type IdentitiesV2 = StorageMap<_, Blake2_128Concat, T::AccountId, ChainIdentityOfV2, OptionQuery>; - /// MAP ( netuid ) --> SubnetIdentityOfV3 + /// On-chain subnet identity profile (`SubnetIdentityOfV3`), if set. #[pallet::storage] pub type SubnetIdentitiesV3 = StorageMap<_, Blake2_128Concat, NetUid, SubnetIdentityOfV3, OptionQuery>; @@ -2586,26 +2588,26 @@ pub mod pallet { ValueQuery, >; - /// MAP ( key ) --> last_block #[deprecated] + /// Block of the account's last rate-limited extrinsic (general tx rate limit). #[pallet::storage] pub type LastTxBlock = StorageMap<_, Identity, T::AccountId, u64, ValueQuery, DefaultLastTxBlock>; - /// MAP ( key ) --> last_tx_block_childkey_take #[deprecated] + /// Deprecated: block of last childkey-take update; prefer keyed rate-limit maps. #[pallet::storage] pub type LastTxBlockChildKeyTake = StorageMap<_, Identity, T::AccountId, u64, ValueQuery, DefaultLastTxBlock>; - /// MAP ( key ) --> last_tx_block_delegate_take #[deprecated] + /// Deprecated: block of last delegate-take update; prefer keyed rate-limit maps. #[pallet::storage] pub type LastTxBlockDelegateTake = StorageMap<_, Identity, T::AccountId, u64, ValueQuery, DefaultLastTxBlock>; - /// ITEM( weights_min_stake ) // FIXME: this storage is used interchangably for alpha/tao + /// Minimum stake required to set weights; units are alpha or TAO depending on call path (see FIXME). #[pallet::storage] pub type StakeThreshold = StorageValue<_, u64, ValueQuery, DefaultStakeThreshold>; @@ -2622,8 +2624,7 @@ pub mod pallet { OptionQuery, >; - /// MAP (netuid, epoch) → VecDeque<(who, commit_block, ciphertext, reveal_round)> - /// Stores a queue of weight commits for an account on a given subnet. + /// Commit-reveal queue keyed by `(netuid, epoch)` holding ciphertext until reveal round. #[pallet::storage] pub type TimelockedWeightCommits = StorageDoubleMap< _, @@ -2640,8 +2641,7 @@ pub mod pallet { ValueQuery, >; - /// MAP (netuid, epoch) → VecDeque<(who, ciphertext, reveal_round)> - /// Deprecated: superseded by `CRV3WeightCommitsV2`. + /// Commit-reveal v3 queue keyed by `(netuid, epoch)` (legacy shape without commit_block). #[pallet::storage] pub type CRV3WeightCommits = StorageDoubleMap< _, @@ -2657,8 +2657,7 @@ pub mod pallet { ValueQuery, >; - /// MAP (netuid, epoch) → VecDeque<(who, commit_block, ciphertext, reveal_round)> - /// Deprecated: superseded by `TimelockedWeightCommits`. + /// Commit-reveal v3 queue keyed by `(netuid, epoch)` including commit_block for timelock checks. #[pallet::storage] pub type CRV3WeightCommitsV2 = StorageDoubleMap< _, @@ -2706,7 +2705,7 @@ pub mod pallet { DefaultRootClaimable, >; - // Already claimed root alpha. + /// Cumulative root alpha already claimed for `(netuid, hotkey, coldkey)`, in alpha fixed-point units (`u128`). #[pallet::storage] pub type RootClaimed = StorageNMap< _, @@ -2773,7 +2772,7 @@ pub mod pallet { pub type SubnetUidToLeaseId = StorageMap<_, Twox64Concat, NetUid, LeaseId, OptionQuery>; - /// ITEM ( next_lease_id ) | The next lease id. + /// Monotonic counter for the next subnet `LeaseId` to allocate. #[pallet::storage] pub type NextSubnetLeaseId = StorageValue<_, LeaseId, ValueQuery, ConstU32<0>>; @@ -2782,17 +2781,17 @@ pub mod pallet { pub type AccumulatedLeaseDividends = StorageMap<_, Twox64Concat, LeaseId, AlphaBalance, ValueQuery, DefaultZeroAlpha>; - /// ITEM ( CommitRevealWeightsVersion ) + /// Active commit-reveal weights protocol version (`u16`) enforced by weight extrinsics. #[pallet::storage] pub type CommitRevealWeightsVersion = StorageValue<_, u16, ValueQuery, DefaultCommitRevealWeightsVersion>; - /// ITEM( NetworkRegistrationStartBlock ) + /// Earliest block at which queued network registrations may execute. #[pallet::storage] pub type NetworkRegistrationStartBlock = StorageValue<_, u64, ValueQuery, DefaultNetworkRegistrationStartBlock>; - /// ITEM( TaoInRefundDeploymentBlock ) + /// Runtime deployment block used as the origin for TAO-in refund eligibility checks. #[pallet::storage] pub type TaoInRefundDeploymentBlock = StorageValue<_, u64, ValueQuery, DefaultTaoInRefundDeploymentBlock>; @@ -2815,7 +2814,7 @@ pub mod pallet { MechId::from(2) } - /// ITEM( max_mechanism_count ) + /// Global maximum mechanisms a subnet may configure (`MechId`). #[pallet::storage] pub type MaxMechanismCount = StorageValue<_, MechId, ValueQuery, DefaultMaxMechanismCount>; @@ -2832,7 +2831,7 @@ pub mod pallet { prod_or_fast!(7_200, 1) } - /// MAP ( netuid ) --> Current number of subnet mechanisms + /// Current mechanism count configured on the subnet (`MechId`). #[pallet::storage] pub type MechanismCountCurrent = StorageMap<_, Twox64Concat, NetUid, MechId, ValueQuery, DefaultMechanismCount>; @@ -2842,12 +2841,12 @@ pub mod pallet { pub type MechanismEmissionSplit = StorageMap<_, Twox64Concat, NetUid, Vec, OptionQuery>; - /// MAP ( netuid ) --> BurnHalfLife (blocks) + /// Burn dynamic half-life for the subnet, in blocks. #[pallet::storage] pub type BurnHalfLife = StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultBurnHalfLife>; - /// MAP ( netuid ) --> BurnIncreaseMult + /// Multiplier (`U64F64`) applied when increasing burn after excess registrations. #[pallet::storage] pub type BurnIncreaseMult = StorageMap<_, Identity, NetUid, U64F64, ValueQuery, DefaultBurnIncreaseMult>; @@ -2995,7 +2994,7 @@ pub mod pallet { if netuid.is_root() { return false; } - if !Self::if_subnet_exist(netuid) { + if !Self::subnet_exists(netuid) { return false; } if !Self::get_network_registration_allowed(netuid) { @@ -3079,7 +3078,7 @@ impl> { #![deny(clippy::expect_used)] fn exists(netuid: NetUid) -> bool { - Self::if_subnet_exist(netuid) + Self::subnet_exists(netuid) } fn mechanism(netuid: NetUid) -> u16 { diff --git a/pallets/subtensor/src/macros/config.rs b/pallets/subtensor/src/macros/config.rs index 6f6718a15c..4c9300c321 100644 --- a/pallets/subtensor/src/macros/config.rs +++ b/pallets/subtensor/src/macros/config.rs @@ -1,8 +1,11 @@ #![allow(clippy::crate_in_macro_def)] use frame_support::pallet_macros::pallet_section; -/// A [`pallet_section`] that defines the errors for a pallet. -/// This can later be imported into the pallet using [`import_section`]. + +/// [`pallet_section`] defining [`Config`] for the subtensor pallet (imported via [`import_section`]). +/// +/// Associated type **names** are wired through the runtime; prefer definition-site docs over renames. +/// `#[pallet::constant]` items seed genesis / defaults (often mirrored by storage). #[pallet_section] mod config { @@ -14,7 +17,10 @@ mod config { use subtensor_runtime_common::AuthorshipInfo; use subtensor_swap_interface::{SwapEngine, SwapHandler}; - /// Configure the pallet by specifying the parameters and types on which it depends. + /// Runtime dependencies for SubtensorModule: currency, swap, commitments, scheduling, and genesis constants. + /// + /// Implemented by the node runtime; associated types and constants below are the wiring surface + /// agents should search when tracing a Config bound or an `Initial*` default. #[pallet::config] pub trait Config: frame_system::Config @@ -22,24 +28,24 @@ mod config { + pallet_crowdloan::Config + pallet_scheduler::Config { - /// call type + /// Runtime call type that can encode SubtensorModule calls (used by scheduler / proxy paths). type RuntimeCall: Parameter + Dispatchable> + From> + IsType<::RuntimeCall> + From>; - /// A sudo-able call. + /// Call type that may be dispatched without origin filters (sudo / privileged schedules). type SudoRuntimeCall: Parameter + UnfilteredDispatchable> + GetDispatchInfo; - /// Currency type that will be used to place deposits on neurons + /// Fungible TAO currency used for neuron deposits, locks, and transfers (`TaoBalance` units = rao). type Currency: fungible::Balanced + fungible::Mutate + LockableCurrency; - /// The scheduler type used for scheduling delayed calls. + /// Anonymous scheduler used for delayed dissolve / coldkey-swap style call dispatch. type Scheduler: ScheduleAnon< BlockNumberFor, LocalCallOf, @@ -47,247 +53,245 @@ mod config { Hasher = Self::Hashing, >; - /// the preimage to store the call data. + /// Preimage store for scheduled call payloads (hash lookup + store). type Preimages: QueryPreimage + StorePreimage; - /// Implementor of `SwapHandler` interface from `subtensor_swap_interface` + /// TAO↔alpha AMM: implements `SwapHandler` plus both directional `SwapEngine` adapters. type SwapInterface: SwapHandler + SwapEngine> + SwapEngine>; - /// Interface to allow interacting with the proxy pallet. + /// Proxy pallet bridge for filtered proxy-call dispatch into subtensor. type ProxyInterface: crate::ProxyInterface; - /// Interface to get commitments. + /// Read path for on-chain commitments (weights / mechanism commit-reveal). type GetCommitments: GetCommitments; - /// Interface to clean commitments on network dissolution. + /// Purge commitments when a subnet is dissolved. type CommitmentsInterface: CommitmentsInterface; - /// Interface to mint, burn, and recycle subnet alpha. + /// Mint, burn, and recycle subnet alpha via the alpha-assets pallet. type AlphaAssets: AlphaAssetsInterface; - /// Rate limit for associating an EVM key. + /// Minimum blocks between EVM key associations for the same coldkey. type EvmKeyAssociateRateLimit: Get; - /// Provider of current block author + /// Current block author account (used for authorship-gated rewards / accounting). type AuthorshipProvider: AuthorshipInfo; - /// Weight information for extrinsics in this pallet. + /// Extrinsic weight implementations; method names must match call names (Tier D). type WeightInfo: crate::weights::WeightInfo; // Initial Value Constants - /// Initial currency issuance. + /// Genesis default for total TAO issuance seed, in rao. #[pallet::constant] type InitialIssuance: Get; - /// Initial min allowed weights setting. + /// Genesis default for per-subnet minimum non-zero weight count. #[pallet::constant] type InitialMinAllowedWeights: Get; - /// Initial Emission Ratio. + /// Genesis default emission-share parameter (u16 fixed-point used by early emission math). #[pallet::constant] type InitialEmissionValue: Get; - /// Tempo for each network. + /// Genesis default tempo (blocks per epoch) for new subnets. #[pallet::constant] type InitialTempo: Get; - /// Initial Difficulty. + /// Genesis default PoW registration difficulty. #[pallet::constant] type InitialDifficulty: Get; - /// Initial Max Difficulty. + /// Genesis default upper bound for adaptive PoW difficulty. #[pallet::constant] type InitialMaxDifficulty: Get; - /// Initial Min Difficulty. + /// Genesis default lower bound for adaptive PoW difficulty. #[pallet::constant] type InitialMinDifficulty: Get; - /// Initial RAO Recycled. + /// Genesis default RAO recycled into the network on registration, in rao. #[pallet::constant] type InitialRAORecycledForRegistration: Get; - /// Initial Burn. + /// Genesis default registration burn cost, in rao. #[pallet::constant] type InitialBurn: Get; - /// Initial Max Burn. + /// Genesis default upper bound for adaptive registration burn, in rao. #[pallet::constant] type InitialMaxBurn: Get; - /// Initial Min Burn. + /// Genesis default lower bound for adaptive registration burn, in rao. #[pallet::constant] type InitialMinBurn: Get; - /// Initial minimum stake. + /// Genesis default minimum stake required for weight-setting eligibility, in rao. #[pallet::constant] type InitialMinStake: Get; - /// Initial minimum stake transfer amount. + /// Genesis default minimum stake transfer / move amount, in rao. #[pallet::constant] type InitialMinTransfer: Get; - /// Min burn upper bound. + /// Hard upper bound owners may set for min burn, in rao. #[pallet::constant] type MinBurnUpperBound: Get; - /// Max burn lower bound. + /// Hard lower bound owners may set for max burn, in rao. #[pallet::constant] type MaxBurnLowerBound: Get; - /// Lower bound for owner-set tempo. + /// Hard lower bound for owner-set tempo (blocks per epoch). #[pallet::constant] type MinTempo: Get; - /// Upper bound for owner-set tempo. + /// Hard upper bound for owner-set tempo (blocks per epoch). #[pallet::constant] type MaxTempo: Get; - /// Lower bound for the activity-cutoff factor (per-mille). + /// Hard lower bound for activity-cutoff factor, in per-mille (‰). #[pallet::constant] type MinActivityCutoffFactorMilli: Get; - /// Upper bound for the activity-cutoff factor (per-mille). + /// Hard upper bound for activity-cutoff factor, in per-mille (‰). #[pallet::constant] type MaxActivityCutoffFactorMilli: Get; - /// Initial adjustment interval. + /// Genesis default difficulty/burn adjustment interval, in blocks. #[pallet::constant] type InitialAdjustmentInterval: Get; - /// Initial bonds moving average. + /// Genesis default bonds EMA moving-average parameter. #[pallet::constant] type InitialBondsMovingAverage: Get; - /// Initial bonds penalty. + /// Genesis default bonds penalty applied during consensus. #[pallet::constant] type InitialBondsPenalty: Get; - /// Initial bonds reset. + /// Genesis default for whether bonds reset each epoch. #[pallet::constant] type InitialBondsResetOn: Get; - /// Initial target registrations per interval. + /// Genesis default target registrations per adjustment interval. #[pallet::constant] type InitialTargetRegistrationsPerInterval: Get; - /// Rho constant. + /// Genesis default Yuma consensus `rho` constant. #[pallet::constant] type InitialRho: Get; - /// AlphaSigmoidSteepness constant. + /// Genesis default steepness for the alpha sigmoid in consensus. #[pallet::constant] type InitialAlphaSigmoidSteepness: Get; - /// Kappa constant. + /// Genesis default Yuma consensus `kappa` constant. #[pallet::constant] type InitialKappa: Get; - /// Initial minimum allowed network UIDs + /// Genesis default minimum allowed UIDs on a subnet. #[pallet::constant] type InitialMinAllowedUids: Get; - /// Initial maximum allowed network UIDs + /// Genesis default maximum allowed UIDs on a subnet. #[pallet::constant] type InitialMaxAllowedUids: Get; - /// Initial validator context pruning length. + /// Genesis default validator context pruning length (blocks / epochs retained). #[pallet::constant] type InitialValidatorPruneLen: Get; - /// Initial scaling law power. + /// Genesis default scaling-law power for emission distribution. #[pallet::constant] type InitialScalingLawPower: Get; - /// Immunity Period Constant. + /// Genesis default neuron immunity period, in blocks. #[pallet::constant] type InitialImmunityPeriod: Get; - /// Activity constant. + /// Genesis default activity cutoff, in blocks (pruning inactivity window). #[pallet::constant] type InitialActivityCutoff: Get; - /// Initial max registrations per block. + /// Genesis default per-block registration cap per subnet. #[pallet::constant] type InitialMaxRegistrationsPerBlock: Get; - /// Initial pruning score for each neuron. + /// Genesis default pruning score assigned to new neurons. #[pallet::constant] type InitialPruningScore: Get; - /// Initial maximum allowed validators per network. + /// Genesis default maximum validators allowed per subnet. #[pallet::constant] type InitialMaxAllowedValidators: Get; - /// Initial default delegation take. + /// Genesis default (max) validator delegate take as u16 (`PerU16` scale). #[pallet::constant] type InitialDefaultDelegateTake: Get; - /// Initial minimum delegation take. + /// Genesis default minimum validator delegate take as u16 (`PerU16` scale). #[pallet::constant] type InitialMinDelegateTake: Get; - /// Initial default childkey take. + /// Genesis default (max) childkey take as u16 (`PerU16` scale). #[pallet::constant] type InitialDefaultChildKeyTake: Get; - /// Initial minimum childkey take. + /// Genesis default minimum childkey take as u16 (`PerU16` scale). #[pallet::constant] type InitialMinChildKeyTake: Get; - /// Initial maximum childkey take. + /// Genesis default maximum childkey take as u16 (`PerU16` scale). #[pallet::constant] type InitialMaxChildKeyTake: Get; - /// Initial weights version key. + /// Genesis default weights version key required for `set_weights`. #[pallet::constant] type InitialWeightsVersionKey: Get; - /// Initial serving rate limit. + /// Genesis default axon/prometheus serving rate limit, in blocks. #[pallet::constant] type InitialServingRateLimit: Get; - /// Initial transaction rate limit. + /// Genesis default general transaction rate limit, in blocks. #[pallet::constant] type InitialTxRateLimit: Get; - /// Initial delegate take transaction rate limit. + /// Genesis default rate limit for delegate-take updates, in blocks. #[pallet::constant] type InitialTxDelegateTakeRateLimit: Get; - /// Initial childkey take transaction rate limit. + /// Genesis default rate limit for childkey-take updates, in blocks. #[pallet::constant] type InitialTxChildKeyTakeRateLimit: Get; - /// Initial adjustment alpha on burn and pow. + /// Genesis default adjustment alpha for burn and PoW difficulty EMA. #[pallet::constant] type InitialAdjustmentAlpha: Get; - /// Initial network immunity period + /// Genesis default immunity period for newly registered subnets, in blocks. #[pallet::constant] type InitialNetworkImmunityPeriod: Get; - /// Initial network minimum burn cost + /// Genesis default floor for subnet registration lock cost, in rao. #[pallet::constant] type InitialNetworkMinLockCost: Get; - /// Initial network subnet cut. + /// Genesis default subnet-owner emission cut as u16 (`PerU16` scale). #[pallet::constant] type InitialSubnetOwnerCut: Get; - /// Initial lock reduction interval. + /// Genesis default interval over which subnet lock cost decays, in blocks. #[pallet::constant] type InitialNetworkLockReductionInterval: Get; - /// Initial network creation rate limit + /// Genesis default rate limit between subnet creations, in blocks. #[pallet::constant] type InitialNetworkRateLimit: Get; - /// Cost of swapping a hotkey. + /// Fee charged for a global hotkey swap, in rao. #[pallet::constant] type KeySwapCost: Get; - /// The upper bound for the alpha parameter. Used for Liquid Alpha. + /// Upper bound for Liquid Alpha parameter (u16 scale). #[pallet::constant] type AlphaHigh: Get; - /// The lower bound for the alpha parameter. Used for Liquid Alpha. + /// Lower bound for Liquid Alpha parameter (u16 scale). #[pallet::constant] type AlphaLow: Get; - /// A flag to indicate if Liquid Alpha is enabled. + /// Genesis default for whether Liquid Alpha consensus is enabled. #[pallet::constant] type LiquidAlphaOn: Get; - /// A flag to indicate if Yuma3 is enabled. + /// Genesis default for whether Yuma3 consensus is enabled. #[pallet::constant] type Yuma3On: Get; - /// Coldkey swap announcement delay. + /// Delay after announcing a coldkey swap before it may execute, in blocks. #[pallet::constant] type InitialColdkeySwapAnnouncementDelay: Get>; - /// Coldkey swap reannouncement delay. + /// Minimum delay before re-announcing a coldkey swap, in blocks. #[pallet::constant] type InitialColdkeySwapReannouncementDelay: Get>; - /// Dissolve network schedule duration + /// Scheduled delay before a dissolve-network call runs, in blocks. #[pallet::constant] type InitialDissolveNetworkScheduleDuration: Get>; - /// Initial TAO weight. + /// Genesis default TAO weight used in root / dual-token emission math (u64 fixed-point). #[pallet::constant] type InitialTaoWeight: Get; - /// Initial EMA price halving period + /// Genesis default EMA price halving period, in blocks. #[pallet::constant] type InitialEmaPriceHalvingPeriod: Get; - /// Delay after which a new subnet can dispatch start call extrinsic. + /// Delay after subnet creation before `start_call` may enable emissions, in blocks. #[pallet::constant] type InitialStartCallDelay: Get; - /// Cost of swapping a hotkey in a subnet. + /// Fee charged for a subnet-scoped hotkey swap, in rao. #[pallet::constant] type KeySwapOnSubnetCost: Get; - /// Block number for a coldkey swap the hotkey in specific subnet. + /// Interval (blocks) governing subnet-scoped hotkey-swap rate limits / cleanup slots. #[pallet::constant] type HotkeySwapOnSubnetInterval: Get; - /// Number of blocks between dividends distribution. + /// Blocks between lease dividend distribution runs. #[pallet::constant] type LeaseDividendsDistributionInterval: Get>; - /// Maximum percentage of immune UIDs. + /// Maximum share of UIDs that may be immune from pruning on a subnet. #[pallet::constant] type MaxImmuneUidsPercentage: Get; - /// Pallet account ID + /// Pallet account id used as the SubtensorModule sovereign account. #[pallet::constant] type SubtensorPalletId: Get; - /// Burn account ID + /// Pallet id of the burn sink account for recycled / burned TAO. #[pallet::constant] type BurnAccountId: Get; - /// Initial default per-block cap on number of subnet epochs that may - /// execute in a single `block_step`; the rest are deferred 1 block forward via - /// `PendingEpochAt`. + /// Cap on subnet epochs executed in one `block_step`; overflow deferred via `PendingEpochAt`. #[pallet::constant] type InitialMaxEpochsPerBlock: Get; } diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 4ce0c69be6..0fd3799467 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -1,8 +1,10 @@ #![allow(clippy::crate_in_macro_def)] use frame_support::pallet_macros::pallet_section; -/// A [`pallet_section`] that defines the errors for a pallet. -/// This can later be imported into the pallet using [`import_section`]. +/// A [`pallet_section`] that defines SubtensorModule dispatchables (extrinsics). +/// +/// Imported into the pallet via [`import_section`]. Call names and `call_index` +/// values are frozen for client/SDK compatibility — edit definition-site docs only. #[pallet_section] mod dispatches { use frame_support::pallet_prelude::DispatchResultWithPostInfo; @@ -16,20 +18,19 @@ mod dispatches { use crate::MAX_ROOT_CLAIM_THRESHOLD; use crate::MAX_SUBNET_CLAIMS; - /// Dispatchable functions allow users to interact with the pallet and invoke state changes. - /// These functions materialize as "extrinsics", which are often compared to transactions. - /// Dispatchable functions must be annotated with a weight and must return a DispatchResult. + /// SubtensorModule extrinsics: staking, weights, registration, identity, admin, and related state changes. + /// + /// Each call is weight-annotated and returns `DispatchResult` / `DispatchResultWithPostInfo`. + /// Prefer the opening doc sentence for origin, units, and invariants agents cannot infer from the signature. #[pallet::call] impl Pallet { #![deny(clippy::expect_used)] - /// Sets the caller weights for the incentive mechanism. The call can be - /// made from the hotkey account so is potentially insecure, however, the damage - /// of changing weights is minimal if caught early. This function includes all the - /// checks that the passed weights meet the requirements. Stored weights are u16s - /// max-upscaled by the pallet, so the largest non-zero supplied weight is stored - /// as `u16::MAX`. The weights determine how inflation propagates outward - /// from this peer. + /// Sets validator weights on a subnet (hotkey origin); rejected when commit-reveal is enabled for `netuid`. + /// + /// Stored weights are u16s max-upscaled so the largest non-zero input becomes `u16::MAX`. + /// Inputs are relative and need not sum to a fixed total. Hotkey-signed, so treat as + /// low-trust until observed on-chain. /// /// # Note /// Input weights are relative. They do not need to sum to a particular @@ -81,13 +82,9 @@ mod dispatches { } } - /// Sets the caller weights for the incentive mechanism for mechanisms. The call - /// can be made from the hotkey account so is potentially insecure, however, the damage - /// of changing weights is minimal if caught early. This function includes all the - /// checks that the passed weights meet the requirements. Stored weights are u16s - /// max-upscaled by the pallet, so the largest non-zero supplied weight is stored - /// as `u16::MAX`. The weights determine how inflation propagates outward - /// from this peer. + /// Sets validator weights for mechanism `mecid` on `netuid` (hotkey origin); rejected when commit-reveal is enabled. + /// + /// Same storage/upscaling rules as `set_weights`, scoped to a single mechanism id. /// /// # Note /// Input weights are relative. They do not need to sum to a particular @@ -142,7 +139,7 @@ mod dispatches { } } - /// Allows a hotkey to set weights for multiple netuids as a batch. + /// Batch `set_weights` across many netuids in one extrinsic; per-item failures emit batch error events without reverting the whole call. /// /// # Arguments /// * `origin`: The caller, a hotkey who wishes to set their weights. @@ -170,7 +167,7 @@ mod dispatches { Self::do_batch_set_weights(origin, netuids, weights, version_keys) } - /// Used to commit a hash of your weight values to later be revealed. + /// Commits a Blake2 hash of upcoming weights for later `reveal_weights` (hotkey origin; commit-reveal must be enabled). /// /// # Arguments /// * `origin`: The signature of the committing hotkey. @@ -194,7 +191,7 @@ mod dispatches { Self::do_commit_weights(origin, netuid, commit_hash) } - /// Used to commit a hash of your weight values to later be revealed for mechanisms. + /// Commits a weight hash for mechanism `mecid` on `netuid` for later `reveal_mechanism_weights`. /// /// # Arguments /// * `origin`: The signature of the committing hotkey. @@ -221,7 +218,7 @@ mod dispatches { Self::do_commit_mechanism_weights(origin, netuid, mecid, commit_hash) } - /// Allows a hotkey to commit weight hashes for multiple netuids as a batch. + /// Batch `commit_weights` across many netuids; individual commit failures are reported via batch events. /// /// # Arguments /// * `origin`: The caller, a hotkey who wishes to set their weights. @@ -246,7 +243,7 @@ mod dispatches { Self::do_batch_commit_weights(origin, netuids, commit_hashes) } - /// Used to reveal the weights for a previously committed hash. + /// Reveals previously `commit_weights`-hashed weights (hotkey origin); hash must match `uids`/`values`/`salt`/`version_key`. /// /// # Arguments /// * `origin`: The signature of the revealing hotkey. @@ -285,7 +282,7 @@ mod dispatches { Self::do_reveal_weights(origin, netuid, uids, values, salt, version_key) } - /// Used to reveal the weights for a previously committed hash for mechanisms. + /// Reveals mechanism-scoped weights committed via `commit_mechanism_weights` (hotkey origin). /// /// # Arguments /// * `origin`: The signature of the revealing hotkey. @@ -371,7 +368,7 @@ mod dispatches { // Self::do_commit_timelocked_weights(origin, netuid, commit, reveal_round, 4) // } - /// Used to commit encrypted commit-reveal v3 weight values to later be revealed for mechanisms. + /// Commits CRV3 timelock-encrypted weights for mechanism `mecid` (hotkey origin; payload size capped by `MAX_CRV3_COMMIT_SIZE_BYTES`). /// /// # Arguments /// * `origin`: The committing hotkey. @@ -415,7 +412,7 @@ mod dispatches { ) } - /// The implementation for batch revealing committed weights. + /// Batch `reveal_weights` across many commits; per-item failures emit batch events without reverting successes. /// /// # Arguments /// * `origin`: The signature of the revealing hotkey. @@ -462,7 +459,7 @@ mod dispatches { ) } - /// Allows delegates to decrease its take value. + /// Decreases a delegate's take (hotkey origin); not rate-limited unlike `increase_take`. /// /// # Arguments /// * `origin`: The signature of the caller's coldkey. @@ -497,7 +494,7 @@ mod dispatches { Self::do_decrease_take(origin, hotkey, take) } - /// Allows delegates to increase its take value. This call is rate-limited. + /// Increases a delegate's take (hotkey origin); rate-limited and bounded by max take. /// /// # Arguments /// * `origin`: The signature of the caller's coldkey. @@ -530,8 +527,9 @@ mod dispatches { Self::do_increase_take(origin, hotkey, take) } - /// Adds stake to a hotkey. The call is made from a coldkey account. - /// This delegates stake to the hotkey. + /// Stakes TAO from the signing coldkey onto `hotkey` for `netuid` (buys alpha via the subnet AMM). + /// + /// `amount_staked` is TAO (Rao). Coldkey may own the hotkey (self-stake) or delegate to another. /// /// # Note /// The coldkey account may own the hotkey, in which case they are @@ -567,9 +565,9 @@ mod dispatches { Self::do_add_stake(origin, hotkey, netuid, amount_staked).map(|_| ()) } - /// Remove stake from the staking account. The call must be made - /// from the coldkey account attached to the neuron metadata. Only this key - /// has permission to make staking and unstaking requests. + /// Unstakes alpha from `(coldkey, hotkey, netuid)` back to TAO on the signing coldkey (AMM sell). + /// + /// Only the coldkey associated with the hotkey may withdraw; `amount_unstaked` is alpha. /// /// # Arguments /// * `origin`: The signature of the caller's coldkey. @@ -601,8 +599,7 @@ mod dispatches { Self::do_remove_stake(origin, hotkey, netuid, amount_unstaked) } - /// Serves or updates axon /prometheus information for the neuron associated with the caller. If the caller is - /// already registered the metadata is updated. If the caller is not registered this call throws NotRegistered. + /// Serves or updates axon endpoint metadata for a registered neuron (hotkey origin); errors with `NotRegistered` if absent. /// /// # Arguments /// * `origin`: The signature of the caller. @@ -664,9 +661,7 @@ mod dispatches { ) } - /// Same as `serve_axon` but takes a certificate as an extra optional argument. - /// Serves or updates axon /prometheus information for the neuron associated with the caller. If the caller is - /// already registered the metadata is updated. If the caller is not registered this call throws NotRegistered. + /// Like `serve_axon`, but also accepts an optional TLS certificate for inter-neuron communication (hotkey origin). /// /// # Arguments /// * `origin`: The signature of the caller. @@ -731,20 +726,15 @@ mod dispatches { ) } - /// Set prometheus information for the neuron. - /// # Arguments - /// * `origin`: The signature of the calling hotkey. - /// - /// * `netuid`: The u16 network identifier. - /// - /// * `version`: The bittensor version identifier. - /// - /// * `ip`: The prometheus ip information as a u128 encoded integer. - /// - /// * `port`: The prometheus port information as a u16 encoded integer. - /// - /// * `ip_type`: The ip type v4 or v6. + /// Publishes Prometheus scrape endpoint metadata for a registered neuron (hotkey origin). /// + /// # Arguments + /// * `origin`: Hotkey of the neuron. + /// * `netuid`: Subnet of the neuron. + /// * `version`: Protocol/version identifier. + /// * `ip`: Endpoint IP as a `u128`. + /// * `port`: Scrape port. + /// * `ip_type`: `4` or `6`. #[pallet::call_index(5)] #[pallet::weight((::WeightInfo::serve_prometheus(), DispatchClass::Normal, Pays::No))] pub fn serve_prometheus( @@ -758,7 +748,7 @@ mod dispatches { Self::do_serve_prometheus(origin, netuid, version, ip, port, ip_type) } - /// Registers a new neuron to the subnetwork. + /// Legacy registration entrypoint; PoW args (`_block_number`/`_nonce`/`_work`/`_coldkey`) are unused and ignored. /// /// # Arguments /// * `origin`: The signature of the calling hotkey. @@ -805,14 +795,14 @@ mod dispatches { Self::do_register(origin, netuid, hotkey) } - /// Register the hotkey to root network + /// Registers `hotkey` on the root subnet; origin must be the owning coldkey. #[pallet::call_index(62)] #[pallet::weight(::WeightInfo::root_register())] pub fn root_register(origin: OriginFor, hotkey: T::AccountId) -> DispatchResult { Self::do_root_register(origin, hotkey) } - /// User register a new subnetwork via burning token + /// Registers `hotkey` on an existing subnet by paying the current burn (not PoW); coldkey origin. #[pallet::call_index(7)] #[pallet::weight(::WeightInfo::burned_register())] pub fn burned_register( @@ -823,13 +813,15 @@ mod dispatches { Self::do_register(origin, netuid, hotkey) } - /// The extrinsic for user to change its hotkey in subnet or all subnets. + /// Deprecated: swaps `hotkey` → `new_hotkey` on one subnet or all (`netuid = None`); coldkey origin, `keep_stake = false`. + /// + /// Prefer `swap_hotkey_v2`. Retained for call-index / client compatibility until removed after June 2026. /// /// # Arguments - /// * `origin`: The origin of the transaction (must be signed by the coldkey). - /// * `hotkey`: The old hotkey to be swapped. - /// * `new_hotkey`: The new hotkey to replace the old one. - /// * `netuid`: Optional subnet ID. If `Some`, swap only on that subnet; if `None`, swap on all subnets. + /// * `origin`: Coldkey that owns `hotkey`. + /// * `hotkey`: Existing hotkey to replace. + /// * `new_hotkey`: Replacement hotkey. + /// * `netuid`: `Some` for one subnet; `None` for all subnets. #[deprecated( note = "Please use swap_hotkey_v2 instead. This extrinsic will be removed some time after June 2026." )] @@ -845,24 +837,24 @@ mod dispatches { new_hotkey: T::AccountId, netuid: Option, ) -> DispatchResultWithPostInfo { - Self::do_swap_hotkey(origin, &hotkey, &new_hotkey, netuid, false) + Self::perform_hotkey_swap(origin, &hotkey, &new_hotkey, netuid, false) } - /// The extrinsic for user to change its hotkey in subnet or all subnets. This extrinsic is - /// similar to swap_hotkey, but with keep_stake parameter bo be able to keep the stake when swapping - /// a root key to a child key + /// Swaps `hotkey` → `new_hotkey` on one subnet or all; coldkey origin. + /// + /// When `keep_stake` is true, stake stays on the old hotkey while other metadata moves + /// (used when promoting a root key to a child key). Prefer this over deprecated `swap_hotkey`. /// /// # Arguments - /// * `origin`: The origin of the transaction (must be signed by the coldkey). - /// * `hotkey`: The old hotkey to be swapped. - /// * `new_hotkey`: The new hotkey to replace the old one. - /// * `netuid`: Optional subnet ID. If `Some`, swap only on that subnet; if `None`, swap on all subnets. - /// * `keep_stake`: If `true`, stake remains on the old hotkey and the rest metadata - /// is transferred to the new hotkey. + /// * `origin`: Coldkey that owns `hotkey`. + /// * `hotkey`: Existing hotkey to replace. + /// * `new_hotkey`: Replacement hotkey. + /// * `netuid`: `Some` for one subnet; `None` for all subnets. + /// * `keep_stake`: If true, leave stake on `hotkey` and transfer remaining metadata only. #[allow(unknown_lints, benchmarked_weight_not_plugged)] #[pallet::call_index(72)] #[pallet::weight(( - crate::Pallet::::swap_hotkey_v2_dispatch_weight(netuid, *keep_stake), + crate::Pallet::::hotkey_swap_dispatch_weight(netuid, *keep_stake), DispatchClass::Normal, Pays::Yes ))] @@ -873,12 +865,12 @@ mod dispatches { netuid: Option, keep_stake: bool, ) -> DispatchResultWithPostInfo { - Self::do_swap_hotkey(origin, &hotkey, &new_hotkey, netuid, keep_stake) + Self::perform_hotkey_swap(origin, &hotkey, &new_hotkey, netuid, keep_stake) } - /// Performs an arbitrary coldkey swap for any coldkey. + /// Root-only immediate coldkey swap (`old_coldkey` → `new_coldkey`) without announcement; optionally charges `swap_cost` TAO. /// - /// Only callable by root as it doesn't require an announcement and can be used to swap any coldkey. + /// Also clears any pending coldkey-swap announcement/dispute for `old_coldkey`. #[pallet::call_index(71)] #[pallet::weight(::WeightInfo::swap_coldkey())] pub fn swap_coldkey( @@ -890,9 +882,9 @@ mod dispatches { ensure_root(origin)?; if !swap_cost.is_zero() { - Self::charge_swap_cost(&old_coldkey, swap_cost)?; + Self::charge_coldkey_swap_cost(&old_coldkey, swap_cost)?; } - Self::do_swap_coldkey(&old_coldkey, &new_coldkey)?; + Self::perform_coldkey_swap(&old_coldkey, &new_coldkey)?; // We also clear any announcement or dispute for security reasons ColdkeySwapAnnouncements::::remove(&old_coldkey); @@ -901,7 +893,7 @@ mod dispatches { Ok(()) } - /// Sets the childkey take for a given hotkey. + /// Sets per-subnet childkey take for `hotkey` (`PerU16`, 65535 = 100%); coldkey origin, rate-limited. /// /// This function allows a coldkey to set the childkey take for a given hotkey. /// The childkey take determines the proportion of stake that the hotkey keeps for itself @@ -939,7 +931,7 @@ mod dispatches { // ---- SUDO ONLY FUNCTIONS ------------------------------------------------------------ - /// Sets the transaction rate limit for changing childkey take. + /// Root-only: sets blocks between childkey-take updates (`tx_rate_limit`). /// /// This function can only be called by the root origin. /// @@ -961,7 +953,7 @@ mod dispatches { Ok(()) } - /// Sets the minimum allowed childkey take. + /// Root-only: sets the global minimum childkey take (`PerU16`). /// /// This function can only be called by the root origin. /// @@ -980,7 +972,7 @@ mod dispatches { Ok(()) } - /// Sets the maximum allowed childkey take. + /// Root-only: sets the global maximum childkey take (`PerU16`). /// /// This function can only be called by the root origin. /// @@ -999,16 +991,14 @@ mod dispatches { Ok(()) } - /// User register a new subnetwork + /// Creates a new subnet; signing coldkey becomes owner and pays the network lock cost, associating `hotkey`. #[pallet::call_index(59)] #[pallet::weight(::WeightInfo::register_network())] pub fn register_network(origin: OriginFor, hotkey: T::AccountId) -> DispatchResult { Self::do_register_network(origin, &hotkey, 1, None) } - /// Facility extrinsic for user to get taken from faucet - /// It is only available when pow-faucet feature enabled - /// Just deployed in testnet and devnet for testing purpose + /// Test-only PoW faucet mint (`pow-faucet` feature); unavailable / errors with `FaucetDisabled` in production builds. #[pallet::call_index(60)] #[pallet::weight((Weight::from_parts(91_000_000, 0) .saturating_add(T::DbWeight::get().reads(27)) @@ -1027,8 +1017,7 @@ mod dispatches { Err(Error::::FaucetDisabled.into()) } - /// Remove a user's subnetwork - /// The caller must be the owner of the network + /// Root-only dissolve of subnet `netuid`. The `_coldkey` argument is unused (kept for call-signature stability). #[pallet::call_index(61)] #[pallet::weight(::WeightInfo::dissolve_network())] pub fn dissolve_network( @@ -1040,7 +1029,7 @@ mod dispatches { Self::do_dissolve_network(netuid) } - /// Set a single child for a given hotkey on a specified network. + /// Sets the childkey proportion map for `hotkey` on `netuid` (coldkey origin); root subnet disallowed. /// /// This function allows a coldkey to set a single child for a given hotkey on a specified network. /// The proportion of the hotkey's stake to be allocated to the child is also specified. @@ -1087,11 +1076,7 @@ mod dispatches { Ok(().into()) } - /// Schedules a coldkey swap operation to be executed at a future block. - /// - /// # Note - /// This function is deprecated; please migrate to - /// `announce_coldkey_swap` / `coldkey_swap`. + /// Deprecated: always returns `Error::Deprecated`. Use `announce_coldkey_swap` / `swap_coldkey_announced`. #[pallet::call_index(73)] #[pallet::weight(::WeightInfo::schedule_swap_coldkey())] #[deprecated(note = "Deprecated, please migrate to `announce_coldkey_swap`/`coldkey_swap`")] @@ -1102,20 +1087,13 @@ mod dispatches { Err(Error::::Deprecated.into()) } - /// Set prometheus information for the neuron. - /// # Arguments - /// * `origin`: The signature of the calling hotkey. - /// - /// * `netuid`: The u16 network identifier. + /// Sets coldkey chain-identity fields (name, url, github, image, discord, description, additional). /// - /// * `version`: The bittensor version identifier. - /// - /// * `ip`: The prometheus ip information as a u128 encoded integer. - /// - /// * `port`: The prometheus port information as a u16 encoded integer. - /// - /// * `ip_type`: The ip type v4 or v6. + /// Signed by the coldkey; distinct from axon/prometheus serving and from `set_subnet_identity`. /// + /// # Arguments + /// * `origin`: Coldkey whose identity is updated. + /// * `name` / `url` / `github_repo` / `image` / `discord` / `description` / `additional`: UTF-8 byte fields. #[pallet::call_index(68)] #[pallet::weight(::WeightInfo::set_identity())] pub fn set_identity( @@ -1140,7 +1118,7 @@ mod dispatches { ) } - /// Set the identity information for a subnet. + /// Sets subnet-owner identity metadata for `netuid` (name, contacts, urls); coldkey must own the subnet. /// # Arguments /// * `origin`: The signature of the calling coldkey, which must be the owner of the subnet. /// @@ -1179,7 +1157,7 @@ mod dispatches { ) } - /// User register a new subnetwork + /// Creates a new subnet like `register_network`, optionally attaching `SubnetIdentityOfV3` at registration. #[pallet::call_index(79)] #[pallet::weight(::WeightInfo::register_network_with_identity())] pub fn register_network_with_identity( @@ -1190,55 +1168,41 @@ mod dispatches { Self::do_register_network(origin, &hotkey, 1, identity) } - /// The implementation for the extrinsic unstake_all: Removes all stake from a hotkey account across all subnets and adds it onto a coldkey. + /// Unstakes all positions for `hotkey` across every subnet onto the signing coldkey (TAO proceeds). /// /// # Arguments - /// * `origin`: The signature of the caller's coldkey. - /// - /// * `hotkey`: The associated hotkey account. + /// * `origin`: Coldkey that owns `hotkey`. + /// * `hotkey`: Hotkey whose stake is fully withdrawn. /// /// # Events - /// * `StakeRemoved`: On the successfully removing stake from the hotkey account. + /// * `StakeRemoved`: On successfully removing stake from the hotkey account. /// /// # Errors - /// * `NotRegistered`: Thrown if the account we are attempting to unstake from is non existent. - /// - /// * `NonAssociatedColdKey`: Thrown if the coldkey does not own the hotkey we are unstaking from. - /// - /// * `NotEnoughStakeToWithdraw`: Thrown if there is not enough stake on the hotkey to withdraw this amount. - /// - /// * `TxRateLimitExceeded`: Thrown if key has hit transaction rate limit. + /// * `NotRegistered` / `NonAssociatedColdKey` / `NotEnoughStakeToWithdraw` / `TxRateLimitExceeded`. #[pallet::call_index(83)] #[pallet::weight(::WeightInfo::unstake_all())] pub fn unstake_all(origin: OriginFor, hotkey: T::AccountId) -> DispatchResult { Self::do_unstake_all(origin, hotkey) } - /// The implementation for the extrinsic unstake_all: Removes all stake from a hotkey account across all subnets and adds it onto a coldkey. + /// Unstakes all non-root alpha for `hotkey` across subnets onto the signing coldkey; root stake is left untouched. /// /// # Arguments - /// * `origin`: The signature of the caller's coldkey. - /// - /// * `hotkey`: The associated hotkey account. + /// * `origin`: Coldkey that owns `hotkey`. + /// * `hotkey`: Hotkey whose alpha stake is fully withdrawn. /// /// # Events - /// * `StakeRemoved`: On the successfully removing stake from the hotkey account. + /// * `StakeRemoved`: On successfully removing stake from the hotkey account. /// /// # Errors - /// * `NotRegistered`: Thrown if the account we are attempting to unstake from is non existent. - /// - /// * `NonAssociatedColdKey`: Thrown if the coldkey does not own the hotkey we are unstaking from. - /// - /// * `NotEnoughStakeToWithdraw`: Thrown if there is not enough stake on the hotkey to withdraw this amount. - /// - /// * `TxRateLimitExceeded`: Thrown if key has hit transaction rate limit. + /// * `NotRegistered` / `NonAssociatedColdKey` / `NotEnoughStakeToWithdraw` / `TxRateLimitExceeded`. #[pallet::call_index(84)] #[pallet::weight(::WeightInfo::unstake_all_alpha())] pub fn unstake_all_alpha(origin: OriginFor, hotkey: T::AccountId) -> DispatchResult { Self::do_unstake_all_alpha(origin, hotkey) } - /// The implementation for the extrinsic move_stake: Moves specified amount of stake from a hotkey to another across subnets. + /// Moves alpha stake between hotkeys and/or subnets while keeping the same coldkey ownership. /// /// # Arguments /// * `origin`: The signature of the caller's coldkey. @@ -1273,8 +1237,7 @@ mod dispatches { ) } - /// Transfers a specified amount of stake from one coldkey to another, optionally across subnets, - /// while keeping the same hotkey. + /// Transfers alpha stake to another coldkey on the same hotkey, optionally across subnets. /// /// # Arguments /// * `origin`: The origin of the transaction, which must be signed by the `origin_coldkey`. @@ -1317,25 +1280,17 @@ mod dispatches { ) } - /// Swaps a specified amount of stake from one subnet to another, while keeping the same coldkey and hotkey. + /// Swaps alpha stake from `origin_netuid` to `destination_netuid` for the same coldkey/hotkey (AMM path). /// /// # Arguments - /// * `origin`: The origin of the transaction, which must be signed by the coldkey that owns the `hotkey`. - /// * `hotkey`: The hotkey whose stake is being swapped. - /// * `origin_netuid`: The network/subnet ID from which stake is removed. - /// * `destination_netuid`: The network/subnet ID to which stake is added. - /// * `alpha_amount`: The amount of stake to swap. + /// * `origin`: Coldkey that owns `hotkey`. + /// * `hotkey`: Hotkey whose stake is swapped. + /// * `origin_netuid` / `destination_netuid`: Distinct subnets. + /// * `alpha_amount`: Alpha to sell on origin and rebuy on destination. /// /// # Errors - /// * `BadOrigin`: The transaction is not signed. - /// * `SameNetuid`: `origin_netuid` and `destination_netuid` are the same. - /// * `SubnetNotExists`: Either `origin_netuid` or `destination_netuid` does not exist. - /// * `SubtokenDisabled`: The subtoken is disabled on the origin or destination subnet. - /// * `HotKeyAccountNotExists`: The `hotkey` account does not exist. - /// * `NotEnoughStakeToWithdraw`: The `(coldkey, hotkey, origin_netuid)` position has less stake than `alpha_amount`. - /// * `InsufficientLiquidity`: The swap simulation on the origin subnet fails. - /// * `AmountTooLow`: The TAO-equivalent of the swap is below the minimum stake requirement. - /// * `StakeUnavailable`: The remaining stake would not cover the locked amount on the origin subnet. + /// * `BadOrigin` / `SameNetuid` / `SubnetNotExists` / `SubtokenDisabled` / `HotKeyAccountNotExists` / + /// `NotEnoughStakeToWithdraw` / `InsufficientLiquidity` / `AmountTooLow` / `StakeUnavailable`. /// /// # Events /// May emit a `StakeSwapped` event on success. @@ -1357,9 +1312,7 @@ mod dispatches { ) } - /// Adds stake to a hotkey on a subnet with a price limit. - /// This extrinsic allows to specify the limit price for alpha token - /// at which or better (lower) the staking should execute. + /// Like `add_stake`, but with alpha price limit (`limit_price` RAO/alpha) and optional partial fill. /// /// In case if slippage occurs and the price shall move beyond the limit /// price, the staking order may execute only partially or not execute @@ -1410,9 +1363,7 @@ mod dispatches { .map(|_| ()) } - /// Removes stake from a hotkey on a subnet with a price limit. - /// This extrinsic allows to specify the limit price for alpha token - /// at which or better (higher) the staking should execute. + /// Like `remove_stake`, but with alpha price floor (`limit_price` RAO/alpha) and optional partial fill. /// /// In case if slippage occurs and the price shall move beyond the limit /// price, the staking order may execute only partially or not execute @@ -1462,28 +1413,20 @@ mod dispatches { ) } - /// Swaps a specified amount of stake from one subnet to another, while keeping the same coldkey and hotkey. + /// Like `swap_stake`, with `limit_price` (RAO/alpha) and optional partial fill on the AMM legs. /// /// # Arguments - /// * `origin`: The origin of the transaction, which must be signed by the coldkey that owns the `hotkey`. - /// * `hotkey`: The hotkey whose stake is being swapped. - /// * `origin_netuid`: The network/subnet ID from which stake is removed. - /// * `destination_netuid`: The network/subnet ID to which stake is added. - /// * `alpha_amount`: The amount of stake to swap. - /// * `limit_price`: The limit price expressed in units of RAO per one Alpha. - /// * `allow_partial`: Allows partial execution of the amount. If set to false, this becomes fill or kill type of order. + /// * `origin`: Coldkey that owns `hotkey`. + /// * `hotkey`: Hotkey whose stake is swapped. + /// * `origin_netuid` / `destination_netuid`: Distinct subnets. + /// * `alpha_amount`: Alpha to sell on origin and rebuy on destination. + /// * `limit_price`: Limit price in RAO per alpha. + /// * `allow_partial`: If false, fill-or-kill when the limit would be crossed. /// /// # Errors - /// * `BadOrigin`: The transaction is not signed. - /// * `SameNetuid`: `origin_netuid` and `destination_netuid` are the same. - /// * `SubnetNotExists`: Either `origin_netuid` or `destination_netuid` does not exist. - /// * `SubtokenDisabled`: The subtoken is disabled on the origin or destination subnet. - /// * `HotKeyAccountNotExists`: The `hotkey` account does not exist. - /// * `NotEnoughStakeToWithdraw`: The `(coldkey, hotkey, origin_netuid)` position has less stake than `alpha_amount`. - /// * `InsufficientLiquidity`: The swap simulation on the origin subnet fails. - /// * `AmountTooLow`: The TAO-equivalent of the swap is below the minimum stake requirement. - /// * `SlippageTooHigh`: `allow_partial` is false and the amount would cross the limit price. - /// * `StakeUnavailable`: The remaining stake would not cover the locked amount on the origin subnet. + /// * `BadOrigin` / `SameNetuid` / `SubnetNotExists` / `SubtokenDisabled` / `HotKeyAccountNotExists` / + /// `NotEnoughStakeToWithdraw` / `InsufficientLiquidity` / `AmountTooLow` / `SlippageTooHigh` / + /// `StakeUnavailable`. /// /// # Events /// May emit a `StakeSwapped` event on success. @@ -1509,7 +1452,7 @@ mod dispatches { ) } - /// Attempts to associate a hotkey with a coldkey. + /// Associates `hotkey` with the signing coldkey if unbound; no-op / errors if already owned elsewhere. /// /// # Arguments /// * `origin`: The origin of the transaction, which must be signed by the coldkey that owns the `hotkey`. @@ -1527,7 +1470,7 @@ mod dispatches { Ok(()) } - /// Initiates a call on a subnet. + /// Subnet-owner starts emissions for `netuid` (sets first emission block); owner-signed. /// /// # Arguments /// * `origin`: The origin of the call, which must be signed by the subnet owner. @@ -1542,7 +1485,7 @@ mod dispatches { Ok(()) } - /// Attempts to associate a hotkey with an EVM key. + /// Associates an EVM address with a registered hotkey after ECDSA recovery of the expected signed payload. /// /// The signature will be checked to see if the recovered public key matches the `evm_key` provided. /// @@ -1587,7 +1530,7 @@ mod dispatches { Self::do_associate_evm_key(origin, netuid, evm_key, block_number, signature) } - /// Recycles alpha from a cold/hot key pair, reducing AlphaOut on a subnet + /// Recycles alpha from `(coldkey, hotkey)` on `netuid`, decreasing `AlphaOut` (coldkey origin). /// /// # Arguments /// * `origin`: The origin of the call (must be signed by the coldkey) @@ -1608,7 +1551,7 @@ mod dispatches { Self::do_recycle_alpha(origin, hotkey, amount, netuid).map(|_| ()) } - /// Burns alpha from a cold/hot key pair without reducing `AlphaOut` + /// Burns alpha from `(coldkey, hotkey)` on `netuid` without decreasing `AlphaOut` (coldkey origin). /// /// # Arguments /// * `origin`: The origin of the call (must be signed by the coldkey) @@ -1629,7 +1572,7 @@ mod dispatches { Self::do_burn_alpha(origin, hotkey, amount, netuid).map(|_| ()) } - /// Sets the pending childkey cooldown (in blocks). Root only. + /// Root-only: sets `PendingChildKeyCooldown` (blocks) before a pending childkey assignment takes effect. #[pallet::call_index(109)] #[pallet::weight(::WeightInfo::set_pending_childkey_cooldown())] pub fn set_pending_childkey_cooldown( @@ -1641,10 +1584,7 @@ mod dispatches { Ok(()) } - /// Removes all stake from a hotkey on a subnet with a price limit. - /// This extrinsic allows to specify the limit price for alpha token - /// at which or better (higher) the staking should execute. - /// Without limit_price it remove all the stake similar to `remove_stake` extrinsic + /// Removes the full `(coldkey, hotkey, netuid)` position, optionally with a sell price floor; `None` limit matches full `remove_stake`. #[pallet::call_index(103)] #[pallet::weight(::WeightInfo::remove_stake_full_limit())] pub fn remove_stake_full_limit( @@ -1656,7 +1596,7 @@ mod dispatches { Self::do_remove_stake_full_limit(origin, hotkey, netuid, limit_price) } - /// Register a new leased network. + /// Registers a leased subnet; origin pays lease terms and designates beneficiary/hotkey per lease config. /// /// The crowdloan's contributions are used to compute the share of the emissions that the contributors /// will receive as dividends. @@ -1679,7 +1619,7 @@ mod dispatches { Self::do_register_leased_network(origin, emissions_share, end_block) } - /// Terminate a lease. + /// Terminates an active subnet lease early when caller permissions and lease terms allow. /// /// The beneficiary can terminate the lease after the end block has passed and get the subnet ownership. /// The subnet is transferred to the beneficiary and the lease is removed from storage. @@ -1702,7 +1642,7 @@ mod dispatches { Self::do_terminate_lease(origin, lease_id, hotkey) } - /// Updates the symbol for a subnet. + /// Sets `TokenSymbol` for `netuid` (sudo or subnet owner); symbol must exist and be unused. /// /// # Arguments /// * `origin`: The origin of the call, which must be the subnet owner or root. @@ -1725,7 +1665,7 @@ mod dispatches { symbol: Vec, ) -> DispatchResult { Self::ensure_subnet_owner_or_root(origin, netuid)?; - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); Self::ensure_symbol_exists(&symbol)?; Self::ensure_symbol_available(&symbol)?; @@ -1736,7 +1676,7 @@ mod dispatches { Ok(()) } - /// Used to commit timelock encrypted commit-reveal weight values to later be revealed. + /// Commits CRV3 timelock-encrypted weights for later reveal (hotkey origin; size capped by `MAX_CRV3_COMMIT_SIZE_BYTES`). /// /// # Arguments /// * `origin`: The committing hotkey. @@ -1773,7 +1713,7 @@ mod dispatches { ) } - /// Set the autostake destination hotkey for a coldkey. + /// Sets which `hotkey` receives auto-staked emissions for the signing coldkey on `netuid`. /// /// The caller selects a hotkey where all future rewards /// will be automatically staked. @@ -1790,7 +1730,7 @@ mod dispatches { hotkey: T::AccountId, ) -> DispatchResult { let coldkey = ensure_signed(origin)?; - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); ensure!( Uids::::contains_key(netuid, &hotkey), Error::::HotKeyNotRegisteredInSubNet @@ -1826,8 +1766,7 @@ mod dispatches { Ok(()) } - /// Used to commit timelock encrypted commit-reveal weight values to later be revealed for - /// a mechanism. + /// Commits CRV3 timelock-encrypted weights for mechanism `mecid` (hotkey origin; size capped by `MAX_CRV3_COMMIT_SIZE_BYTES`). /// /// # Arguments /// * `origin`: The committing hotkey. @@ -1868,8 +1807,7 @@ mod dispatches { ) } - /// Remove a subnetwork - /// The caller must be root + /// Root-only dissolve of subnet `netuid` (same effect as `dissolve_network`, without unused coldkey arg). #[pallet::call_index(120)] #[pallet::weight(::WeightInfo::root_dissolve_network())] pub fn root_dissolve_network(origin: OriginFor, netuid: NetUid) -> DispatchResult { @@ -1877,7 +1815,7 @@ mod dispatches { Self::do_dissolve_network(netuid) } - /// Claims the root emissions for a coldkey. + /// Claims root emissions for the signing coldkey on the given subnet set (non-empty, <= `MAX_SUBNET_CLAIMS`). /// # Arguments /// * `origin`: The signature of the caller's coldkey. /// @@ -1907,7 +1845,7 @@ mod dispatches { Ok((Some(weight), Pays::Yes).into()) } - /// Sets the root claim type for the coldkey. + /// Sets how the signing coldkey claims root emissions (`RootClaimTypeEnum`, including keep-subnet filters). /// # Arguments /// * `origin`: The signature of the caller's coldkey. /// @@ -1932,7 +1870,7 @@ mod dispatches { Ok(()) } - /// Sets root claim number (sudo extrinsic). Zero disables auto-claim. + /// Root-only: sets how many coldkeys auto-claim root emissions per block; `0` disables (max `MAX_NUM_ROOT_CLAIMS`). #[pallet::call_index(123)] #[pallet::weight(::WeightInfo::sudo_set_num_root_claims())] pub fn sudo_set_num_root_claims(origin: OriginFor, new_value: u64) -> DispatchResult { @@ -1948,7 +1886,7 @@ mod dispatches { Ok(()) } - /// Sets root claim threshold for subnet (sudo or owner origin). + /// Sets per-subnet `RootClaimableThreshold` (sudo or subnet owner); claims only above this threshold. #[pallet::call_index(124)] #[pallet::weight(::WeightInfo::sudo_set_root_claim_threshold())] pub fn sudo_set_root_claim_threshold( @@ -1957,7 +1895,7 @@ mod dispatches { new_value: u64, ) -> DispatchResult { Self::ensure_subnet_owner_or_root(origin, netuid)?; - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); ensure!( new_value <= I96F32::from(MAX_ROOT_CLAIM_THRESHOLD), @@ -1969,7 +1907,7 @@ mod dispatches { Ok(()) } - /// Announces a coldkey swap using BlakeTwo256 hash of the new coldkey. + /// Announces a pending coldkey swap by committing `BlakeTwo256` of the new coldkey; starts the dispute window. /// /// This is required before the coldkey swap can be performed /// after the delay period. @@ -1999,7 +1937,7 @@ mod dispatches { } else { // Only charge the swap cost on the first announcement let swap_cost = Self::get_key_swap_cost(); - Self::charge_swap_cost(&who, swap_cost)?; + Self::charge_coldkey_swap_cost(&who, swap_cost)?; } let delay = ColdkeySwapAnnouncementDelay::::get(); @@ -2013,7 +1951,7 @@ mod dispatches { Ok(()) } - /// Performs a coldkey swap if an announcement has been made. + /// Completes an announced coldkey swap after the delay if undisputed; reveals the new coldkey matching the hash. /// /// The dispatch origin of this call must be the original coldkey that made the announcement. /// @@ -2040,12 +1978,12 @@ mod dispatches { let now = >::block_number(); ensure!(now >= when, Error::::ColdkeySwapTooEarly); - Self::do_swap_coldkey(&who, &new_coldkey)?; + Self::perform_coldkey_swap(&who, &new_coldkey)?; Ok(()) } - /// Dispute a coldkey swap. + /// Disputes a pending coldkey-swap announcement for the signing coldkey, blocking completion until reset. /// /// This will prevent any further actions on the coldkey swap /// until triumvirate step in to resolve the issue. @@ -2073,7 +2011,7 @@ mod dispatches { Ok(()) } - /// Reset a coldkey swap by clearing the announcement and dispute status. + /// Clears announcement and dispute state for the signing coldkey so a new swap can be announced. /// /// The dispatch origin of this call must be root. /// @@ -2091,7 +2029,7 @@ mod dispatches { Ok(()) } - /// Enables voting power tracking for a subnet. + /// Enables voting-power tracking on `netuid` (sudo or owner); required before EMA voting-power features apply. /// /// This function can be called by the subnet owner or root. /// When enabled, voting power EMA is updated every epoch for all validators. @@ -2114,7 +2052,7 @@ mod dispatches { Self::do_enable_voting_power_tracking(netuid) } - /// Schedules disabling of voting power tracking for a subnet. + /// Schedules disable of voting-power tracking on `netuid` (sudo or owner); not an instantaneous clear. /// /// This function can be called by the subnet owner or root. /// Voting power tracking will continue for 14 days (grace period) after this call, @@ -2138,7 +2076,7 @@ mod dispatches { Self::do_disable_voting_power_tracking(netuid) } - /// Sets the EMA alpha value for voting power calculation on a subnet. + /// Root-only: sets voting-power EMA alpha for `netuid` (`u64` with 1e18 = 1.0; must be <= 1e18). /// /// This function can only be called by root (sudo). /// Higher alpha = faster response to stake changes. @@ -2164,8 +2102,7 @@ mod dispatches { Self::do_set_voting_power_ema_alpha(netuid, alpha) } - /// The extrinsic is a combination of add_stake(add_stake_limit) and burn_alpha. We buy - /// alpha token first and immediately burn the acquired amount of alpha (aka Subnet buyback). + /// Subnet buyback: buy alpha with TAO (optional price `limit`) then immediately `burn_alpha` the acquired amount. #[pallet::call_index(132)] #[pallet::weight(::WeightInfo::add_stake_burn())] pub fn add_stake_burn( @@ -2201,10 +2138,9 @@ mod dispatches { Ok(()) } - /// User register a new subnetwork via burning token, but only if the - /// on-chain burn price for this block is <= `limit_price`. + /// Registers `hotkey` via burn only when this block's burn price is <= `limit_price` (same units as `Burn`). /// - /// `limit_price` is expressed in the same TaoCurrency/u64 units as `Burn`. + /// Fill-or-kill relative to burn; coldkey origin. Distinct from creating a subnet. #[pallet::call_index(134)] #[pallet::weight((::WeightInfo::register_limit(), DispatchClass::Normal, Pays::Yes))] pub fn register_limit( @@ -2216,8 +2152,7 @@ mod dispatches { Self::do_register_limit(origin, netuid, hotkey, limit_price) } - /// Allows a root validator to toggle auto parent delegation - /// for new subnets owner hotkey + /// Coldkey owning a root-registered `hotkey` toggles auto parent-delegation to new subnet owner hotkeys. #[pallet::call_index(135)] #[pallet::weight((::WeightInfo::set_auto_parent_delegation_enabled(), DispatchClass::Normal, Pays::Yes))] pub fn set_auto_parent_delegation_enabled( @@ -2337,8 +2272,7 @@ mod dispatches { Ok(()) } - /// Owner-side `trigger_epoch`. Schedules an epoch to fire after `AdminFreezeWindow` - /// blocks. Rate-limited via the existing `OwnerHyperparamUpdate` pattern. + /// Subnet-owner schedules an epoch after `AdminFreezeWindow` blocks; rate-limited like other owner hyperparam updates. #[pallet::call_index(141)] #[pallet::weight(::WeightInfo::trigger_epoch())] pub fn trigger_epoch(origin: OriginFor, netuid: NetUid) -> DispatchResult { @@ -2359,8 +2293,7 @@ mod dispatches { Ok(()) } - /// Transfers a specified amount of stake from one coldkey to another, landing it - /// on a different hotkey, optionally across subnets. + /// Transfers alpha stake to another coldkey and hotkey in one call, optionally across subnets. /// /// This is `transfer_stake` generalized to a destination hotkey: it transfers /// ownership of the position and re-delegates it in one atomic call. Use diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index b009236cb6..c18b1b3494 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -1,350 +1,328 @@ use frame_support::pallet_macros::pallet_section; -/// A [`pallet_section`] that defines the errors for a pallet. -/// This can later be imported into the pallet using [`import_section`]. +/// [`pallet_section`] defining [`Error`] for the subtensor pallet (imported via [`import_section`]). +/// +/// Variant **names and declaration order are frozen** (Tier B/C metadata). Edit docs only — +/// never rename, reorder, insert, or remove variants. #[pallet_section] mod errors { #[derive(PartialEq)] #[pallet::error] pub enum Error { - /// The root network does not exist. + /// Root network (netuid 0) is missing from chain state (`NetworksAdded`). RootNetworkDoesNotExist, - /// The user is trying to serve an axon which is not of type 4 (IPv4) or 6 (IPv6). + /// `serve_axon` / `serve_prometheus` `ip_type` is not 4 (IPv4) or 6 (IPv6). InvalidIpType, - /// An invalid IP address is passed to the serve function. + /// `serve_axon` / `serve_prometheus` `ip` is invalid for the declared `ip_type`. InvalidIpAddress, - /// An invalid port is passed to the serve function. + /// `serve_axon` / `serve_prometheus` `port` is zero (rejected). InvalidPort, - /// The hotkey is not registered in subnet + /// Hotkey has no UID on the given netuid (`Uids`); weight/commit/UID paths. HotKeyNotRegisteredInSubNet, - /// The hotkey does not exists + /// Hotkey has no on-chain account (`Owner` missing) — never registered. HotKeyAccountNotExists, - /// The hotkey is not registered in any subnet. + /// Hotkey is not registered on any subnet (or the serving netuid for axon/prometheus). HotKeyNotRegisteredInNetwork, - /// Request to stake, unstake or subscribe is made by a coldkey that is not associated with - /// the hotkey account. + /// Signing coldkey does not own the target hotkey (`Owner`); stake/swap/serve/children. NonAssociatedColdKey, // StakeToWithdrawIsZero (deprecated, kept commented out for historical reference). - /// The caller does not have enought stake to perform this action. + /// Generic insufficient-stake failure: hotkey stake below what the action requires. NotEnoughStake, - /// The caller is requesting removing more stake than there exists in the staking account. - /// See: "[remove_stake()]". + /// Unstake/move/swap/transfer requested more alpha than the coldkey–hotkey pair holds. NotEnoughStakeToWithdraw, - /// The caller is requesting to set weights but the caller has less than minimum stake - /// required to set weights (less than WeightsMinStake). + /// Hotkey stake weight on the subnet is below `StakeThreshold` (owner hotkey exempt). NotEnoughStakeToSetWeights, - /// The parent hotkey doesn't have enough own stake to set childkeys. + /// `set_children`: parent hotkey total stake below `StakeThreshold` (owner exempt). NotEnoughStakeToSetChildkeys, - /// The caller is requesting adding more stake than there exists in the coldkey account. - /// See: "[add_stake()]" + /// Coldkey free balance below TAO needed for `add_stake` or registration burn. NotEnoughBalanceToStake, - /// The caller is trying to add stake, but for some reason the requested amount could not be - /// withdrawn from the coldkey account. + /// Could not withdraw the requested TAO from the coldkey (balance / ED / freeze). BalanceWithdrawalError, - /// Unsuccessfully withdraw, balance could be zero (can not make account exist) after - /// withdrawal. + /// Withdrawal would leave the coldkey below existential deposit (account would vanish). ZeroBalanceAfterWithdrawn, - /// The caller is attempting to set non-self weights without being a permitted validator. + /// Setting non-self weights without a validator permit on that subnet. NeuronNoValidatorPermit, - /// The caller is attempting to set the weight keys and values but these vectors have - /// different size. + /// Weight `uids` and `values` vectors have different lengths. WeightVecNotEqualSize, - /// The caller is attempting to set weights with duplicate UIDs in the weight matrix. + /// Weight `uids` vector contains the same UID more than once. DuplicateUids, - /// The caller is attempting to set weight to at least one UID that does not exist in the - /// metagraph. + /// At least one weight target UID is not in the subnet metagraph. UidVecContainInvalidOne, - /// The dispatch is attempting to set weights on chain with fewer elements than are allowed. + /// Weight vector has fewer elements than the subnet minimum allows. WeightVecLengthIsLow, - /// Number of registrations in this block exceeds the allowed number (i.e., exceeds the - /// subnet hyperparameter "max_regs_per_block"). + /// Registrations this block exceed subnet hyperparameter `max_regs_per_block`. TooManyRegistrationsThisBlock, - /// The caller is requesting registering a neuron which already exists in the active set. + /// Hotkey already holds a UID on the target subnet (or any subnet for some swaps). HotKeyAlreadyRegisteredInSubNet, - /// The new hotkey is the same as old one + /// `swap_hotkey`: `new_hotkey` equals the current hotkey (no-op). NewHotKeyIsSameWithOld, - /// The new hotkey has outstanding root claimable or non-zero root stake, - /// so the root rate-book cannot be merged without misallocating dividends. + /// Destination hotkey has root claimable/stake/history; root rate-book cannot merge safely. NewHotKeyNotCleanForRootSwap, - /// The supplied PoW hash block is in the future or negative. + /// PoW `block_number` is in the future or too far in the past (stale work). InvalidWorkBlock, - /// The supplied PoW hash block does not meet the network difficulty. + /// PoW hash does not meet required difficulty (faucet fixed or subnet `Difficulty`). InvalidDifficulty, - /// The supplied PoW hash seal does not match the supplied work. + /// PoW seal recomputed from block/nonce/key does not match submitted `work`. InvalidSeal, - /// The dispatch is attempting to set weights on chain with weight value exceeding the - /// configured max weight limit (currently `u16::MAX`). + /// After normalization, a weight exceeds the subnet max weight limit (self-weight exempt). MaxWeightExceeded, - /// The hotkey is attempting to become a delegate when the hotkey is already a delegate. + /// `become_delegate`: hotkey is already a delegate (`Delegates`). HotKeyAlreadyDelegate, - /// A transactor exceeded the rate limit for setting weights. + /// Weights set again before `WeightsSetRateLimit` blocks since this neuron's last update. SettingWeightsTooFast, - /// A validator is attempting to set weights from a validator with incorrect weight version. + /// `version_key` is older than the subnet's required `WeightsVersionKey`. IncorrectWeightVersionKey, - /// An axon or prometheus serving exceeded the rate limit for a registered neuron. + /// `serve_axon` / `serve_prometheus` before `ServingRateLimit` since last serve update. ServingRateLimitExceeded, - /// The caller is attempting to set weights with more UIDs than allowed. + /// Weight `uids` length exceeds the number of UIDs in the subnet. UidsLengthExceedUidsInSubNet, // 32 - /// A transactor exceeded the rate limit for add network transaction. + /// Coldkey `register_network` again before `NetworkRateLimit` elapsed. NetworkTxRateLimitExceeded, - /// A transactor exceeded the rate limit for delegate transaction. + /// Delegate take change before `TxDelegateTakeRateLimit` since last take tx. DelegateTxRateLimitExceeded, - /// A transactor exceeded the rate limit for setting or swapping hotkey. + /// Hotkey set/swap before `TxRateLimit` since the coldkey's last such transaction. HotKeySetTxRateLimitExceeded, - /// A transactor exceeded the rate limit for staking. + /// Staking extrinsic exceeded the staking rate limit for this coldkey. StakingRateLimitExceeded, - /// Registration is disabled. + /// Neuron registration is disabled on this subnet. SubNetRegistrationDisabled, - /// The number of registration attempts exceeded the allowed number in the interval. + /// Registration attempts this interval exceed the subnet allowed count. TooManyRegistrationsThisInterval, - /// The hotkey is required to be the origin. + /// Extrinsic requires the origin to be the hotkey account itself. TransactorAccountShouldBeHotKey, - /// Faucet is disabled. + /// `faucet` called on a runtime without the pow-faucet feature (real networks). FaucetDisabled, - /// Not a subnet owner. + /// Signing coldkey is not `SubnetOwner` for the target netuid. NotSubnetOwner, - /// Operation is not permitted on the root subnet. + /// Neuron registration / `set_children` is not allowed on the root subnet (use `root_register`). RegistrationNotPermittedOnRootSubnet, - /// A hotkey with too little stake is attempting to join the root subnet. + /// Hotkey stake too low to join the root subnet. StakeTooLowForRoot, - /// All subnets are in the immunity period. + /// New subnet would need a prune, but every candidate is still in network immunity. AllNetworksInImmunity, - /// Not enough balance to pay swapping hotkey. + /// Coldkey free TAO below the hotkey-swap cost. NotEnoughBalanceToPaySwapHotKey, - /// Netuid does not match for setting root network weights. + /// Call that only operates on root was given a non-root netuid (must be 0). NotRootSubnet, - /// Can not set weights for the root network. + /// `set_weights` is not allowed on the root network (netuid 0). CanNotSetRootNetworkWeights, - /// No neuron ID is available. + /// No UID available: `MaxAllowedUids` is 0, or subnet full and every neuron is immune. NoNeuronIdAvailable, - /// Delegate take is too low. + /// Delegate `take` below `MinDelegateTake`, or take change not strictly mono vs current. DelegateTakeTooLow, - /// Delegate take is too high. + /// Delegate `take` exceeds `MaxDelegateTake`. DelegateTakeTooHigh, - /// No commit found for the provided hotkey+netuid combination when attempting to reveal the - /// weights. + /// Reveal found no pending non-expired weight commit for this hotkey+netuid. NoWeightsCommitFound, - /// Committed hash does not equal the hashed reveal data. + /// Revealed uids/values/salt/version_key hash matches none of the pending commits. InvalidRevealCommitHashNotMatch, - /// Attempting to call set_weights when commit/reveal is enabled + /// Plain `set_weights` while commit-reveal is enabled; use commit/reveal instead. CommitRevealEnabled, - /// Attemtping to commit/reveal weights when disabled. + /// Commit/reveal submitted while commit-reveal is disabled on the subnet. CommitRevealDisabled, - /// Attempting to set alpha high/low while disabled + /// Setting liquid-alpha values while `LiquidAlphaOn` is false for the subnet. LiquidAlphaDisabled, - /// Alpha high is too low: alpha_high > 0.8 + /// `alpha_high` below the liquid-alpha minimum (`u16::MAX / 40` ≈ 1638). AlphaHighTooLow, - /// Alpha low is out of range: alpha_low > 0 && alpha_low < 0.8 + /// `alpha_low` below `u16::MAX / 40` or greater than `alpha_high`. AlphaLowOutOfRange, - /// The coldkey has already been swapped + /// Coldkey-swap destination already has associated staking hotkeys. ColdKeyAlreadyAssociated, - /// The coldkey balance is not enough to pay for the swap + /// Coldkey free TAO cannot cover the coldkey-swap cost. NotEnoughBalanceToPaySwapColdKey, - /// Attempting to set an invalid child for a hotkey on a network. + /// Children/parents list includes a self-loop or invalid child for this hotkey. InvalidChild, - /// Duplicate child when setting children. + /// `set_children`: the same child hotkey appears more than once. DuplicateChild, - /// Proportion overflow when setting children. + /// `set_children`: child proportions sum overflows u64. ProportionOverflow, - /// Too many children MAX 5. + /// `set_children`: more than the maximum of 5 children. TooManyChildren, - /// Default transaction rate limit exceeded. + /// Default transaction rate limit exceeded for this coldkey. TxRateLimitExceeded, - /// Coldkey swap announcement not found + /// No pending entry in `ColdkeySwapAnnouncements` for this coldkey. ColdkeySwapAnnouncementNotFound, - /// Coldkey swap too early. + /// `coldkey_swap` before announcement delay (`ColdkeySwapAnnouncementDelay`) elapsed. ColdkeySwapTooEarly, - /// Coldkey swap reannounced too early. + /// `announce_coldkey_swap` again before `ColdkeySwapReannouncementDelay` elapsed. ColdkeySwapReannouncedTooEarly, - /// The announced coldkey hash does not match the new coldkey hash. + /// `new_coldkey` hash does not match the hash in `ColdkeySwapAnnouncements`. AnnouncedColdkeyHashDoesNotMatch, - /// Coldkey swap already disputed + /// `dispute_coldkey_swap` when the announcement is already disputed. ColdkeySwapAlreadyDisputed, - /// New coldkey is hotkey + /// Proposed new coldkey is already an existing hotkey (`Owner`). NewColdKeyIsHotkey, - /// Childkey take is invalid. + /// Childkey take outside `[MinChildkeyTake, MaxChildkeyTake]` for the subnet. InvalidChildkeyTake, - /// Childkey take rate limit exceeded. + /// Childkey-take change exceeded its per-hotkey rate limit. TxChildkeyTakeRateLimitExceeded, - /// Invalid identity. + /// Coldkey or subnet identity failed validation (field length / malformed data). InvalidIdentity, - /// Subnet mechanism does not exist. + /// Target subnet or sub-mechanism missing (`mechid` ≥ `MechanismCountCurrent`, etc.). MechanismDoesNotExist, - /// Trying to unstake or re-lock the locked amount. + /// Alpha is locked/unavailable for unstake, transfer, or re-lock at the requested amount. StakeUnavailable, - /// Trying to perform action on non-existent subnet. + /// Operation targeted a netuid that is not an existing subnet. SubnetNotExists, - /// Maximum commit limit reached + /// Hotkey has too many unrevealed weight commits on this subnet. TooManyUnrevealedCommits, - /// Attempted to reveal weights that are expired. + /// Reveal after the commit's reveal window expired (`commit_reveal_period`). ExpiredWeightCommit, - /// Attempted to reveal weights too early. + /// Reveal before commit epoch + reveal period (`RevealPeriodEpochs`). RevealTooEarly, - /// Attempted to batch reveal weights with mismatched vector input lenghts. + /// Batch weights call: parallel input vectors have unequal lengths. InputLengthsUnequal, - /// A transactor exceeded the rate limit for setting weights. + /// Weight commit again before per-UID `weights_rate_limit` since last commit. CommittingWeightsTooFast, - /// Stake amount is too low. + /// Stake/unstake/move/swap amount is zero or below `DefaultMinStake` after fees/slippage. AmountTooLow, - /// Not enough liquidity. + /// Pool cannot absorb the swap/stake (simulation failed or reserves too small). InsufficientLiquidity, - /// Slippage is too high for the transaction. + /// Slippage / price impact exceeds the caller-supplied max amount. SlippageTooHigh, - /// Subnet disallows transfer. + /// Subnet disallows the requested stake/alpha transfer. TransferDisallowed, - /// Activity cutoff is being set too low. + /// Admin tried to set activity cutoff below the chain-wide minimum. ActivityCutoffTooLow, - /// Call is disabled + /// Extrinsic is switched off in this runtime (no active raise site in current code). CallDisabled, - /// FirstEmissionBlockNumber is already set. + /// `start_call`: `FirstEmissionBlockNumber` already set; subnet already emitting. FirstEmissionBlockNumberAlreadySet, - /// need wait for more blocks to accept the start call extrinsic. + /// Legacy start-call delay error (superseded in paths by `StartCallNotReady`). NeedWaitingMoreBlocksToStarCall, - /// Not enough AlphaOut on the subnet to recycle + /// Recycle/burn amount exceeds subnet outstanding alpha (`SubnetAlphaOut`). NotEnoughAlphaOutToRecycle, - /// Cannot burn or recycle TAO from root subnet + /// `recycle_alpha` / `burn_alpha` is not allowed on the root subnet. CannotBurnOrRecycleOnRootSubnet, - /// Public key cannot be recovered. + /// EVM association signature could not recover a public key. UnableToRecoverPublicKey, - /// Recovered public key is invalid. + /// Recovered EVM pubkey keccak hash does not match the claimed `evm_key`. InvalidRecoveredPublicKey, - /// SubToken disabled now + /// Subtoken / alpha staking path disabled for this subnet (`SubtokenEnabled`). SubtokenDisabled, - /// Too frequent hotkey swap on subnet + /// Hotkey swap on subnet before `HotkeySwapOnSubnetInterval` since last swap on that netuid. HotKeySwapOnSubnetIntervalNotPassed, - /// `keep_stake` hotkey swap refused because the old hotkey still has - /// standing miner collateral. Stake would stay on the old key while - /// the UID moves, stranding the bond. Swap with `keep_stake=false` so - /// collateral migrates with the UID (lineage maps track the rename). + /// `keep_stake=true` refused: old hotkey still has miner collateral (would strand the bond). KeepStakeBlockedByCollateral, - /// Invalid netuid duplication + /// Stake move/swap where origin and destination netuid (and keys) leave nothing to change. SameNetuid, - /// The caller does not have enough TAO balance for the operation. + /// Coldkey free TAO below amount needed for transfer, burn/recycle, or registration lock. InsufficientTaoBalance, - /// Invalid lease beneficiary to register the leased network. + /// Leased-network registrant is not the crowdloan creator (beneficiary mismatch). InvalidLeaseBeneficiary, - /// Lease cannot end in the past. + /// Leased-network `end_block` is not after the current block. LeaseCannotEndInThePast, - /// Couldn't find the lease netuid. + /// After leased registration, no subnet owned by the lease coldkey was found. LeaseNetuidNotFound, - /// Lease does not exist. + /// `lease_id` has no entry in `SubnetLeases`. LeaseDoesNotExist, - /// Lease has no end block. + /// Lease is perpetual (`end_block` is `None`) and cannot be ended this way. LeaseHasNoEndBlock, - /// Lease has not ended. + /// Lease termination before stored `end_block`. LeaseHasNotEnded, - /// An overflow occurred. + /// Checked arithmetic overflow (e.g. `NextSubnetLeaseId` or crowdloan counters). Overflow, - /// Beneficiary does not own hotkey. + /// Lease end: handover hotkey is not owned by the lease beneficiary coldkey. BeneficiaryDoesNotOwnHotkey, - /// Expected beneficiary origin. + /// Lease operation signed by someone other than the lease beneficiary coldkey. ExpectedBeneficiaryOrigin, - /// Admin operation is prohibited during the protected weights window + /// Owner/admin hyperparameter change inside the pre-epoch admin freeze window. AdminActionProhibitedDuringWeightsWindow, - /// Symbol does not exist. + /// Requested subnet symbol is not in the allowed symbol set. SymbolDoesNotExist, - /// Symbol already in use. + /// Requested subnet symbol is already assigned to another subnet. SymbolAlreadyInUse, - /// Incorrect commit-reveal version. + /// `commit_reveal_version` does not match `CommitRevealWeightsVersion`. IncorrectCommitRevealVersion, - /// Reveal round is older than the most recently stored DRAND round. + /// Timelocked commit `reveal_round` older than drand `LastStoredRound` (would decrypt now). InvalidRevealRound, - /// Reveal period is too large. + /// `set_reveal_period`: period above the compiled-in maximum epochs. RevealPeriodTooLarge, - /// Reveal period is too small. + /// `set_reveal_period`: period below the compiled-in minimum epochs. RevealPeriodTooSmall, - /// Generic error for out-of-range parameter value + /// Generic out-of-range admin/sudo parameter (mechanism counts, splits, UID bounds, etc.). InvalidValue, - /// Subnet limit reached & there is no eligible subnet to prune + /// Subnet limit reached and no eligible subnet can be pruned. SubnetLimitReached, - /// Insufficient funds to meet the subnet lock cost + /// Coldkey free balance cannot cover the dynamic subnet-creation lock cost. CannotAffordLockCost, - /// exceeded the rate limit for associating an EVM key. + /// `associate_evm_key` before `EvmKeyAssociateRateLimit` since last association for this UID. EvmKeyAssociateRateLimitExceeded, - /// The EVM address already has the maximum number of associated UIDs on this subnet. + /// EVM address already at max associated UIDs on this subnet. EvmKeyAssociationLimitExceeded, - /// Same auto stake hotkey already set + /// Auto-stake destination already set to this same hotkey for the coldkey+netuid. SameAutoStakeHotkeyAlreadySet, - /// The UID map for the subnet could not be cleared + /// Subnet UID map could not be cleared (inconsistent UID state). UidMapCouldNotBeCleared, - /// Trimming would exceed the max immune neurons percentage + /// Pruning/trimming would push immune neurons above the max immune percentage. TrimmingWouldExceedMaxImmunePercentage, - /// Violating the rules of Childkey-Parentkey consistency + /// `set_children` would make a hotkey both child and parent, or reference a missing child. ChildParentInconsistency, - /// Invalid number of root claims + /// `sudo_set_num_root_claims` exceeds compile-time `MAX_NUM_ROOT_CLAIMS`. InvalidNumRootClaim, - /// Invalid value of root claim threshold + /// Root claim threshold exceeds `MAX_ROOT_CLAIM_THRESHOLD`. InvalidRootClaimThreshold, - /// Exceeded subnet limit number or zero. + /// Root-claim subnet set empty or larger than `MAX_SUBNET_CLAIMS`. InvalidSubnetNumber, - /// The maximum allowed UIDs times mechanism count should not exceed 256. + /// `MaxAllowedUids` × mechanism count would exceed 256. TooManyUIDsPerMechanism, - /// Voting power tracking is not enabled for this subnet. + /// Voting-power tracking is not enabled for this subnet. VotingPowerTrackingNotEnabled, - /// Invalid voting power EMA alpha value (must be <= 10^18). + /// Voting-power EMA alpha > 10^18 (must be ≤ 1.0 in fixed-point). InvalidVotingPowerEmaAlpha, - /// Deprecated call. + /// Extrinsic removed and always fails (e.g. legacy coldkey-swap schedule path). Deprecated, - /// Subnet buyback exceeded the operation rate limit + /// Subnet buyback exceeded its operation rate limit. SubnetBuybackRateLimitExceeded, - /// Network already in dissolved queue + /// Subnet already queued in `DissolveCleanupQueue`. NetworkDissolveAlreadyQueued, - /// "Add stake and burn" exceeded the operation rate limit + /// Add-stake-and-burn exceeded its per-key rate limit. AddStakeBurnRateLimitExceeded, - /// A coldkey swap has been announced for this account. + /// Coldkey has a pending swap announcement; most extrinsics are blocked until clear/swap. ColdkeySwapAnnounced, - /// A coldkey swap for this account is under dispute. + /// Coldkey swap is under dispute; extrinsics blocked until root resolves. ColdkeySwapDisputed, - /// Coldkey swap clear too early. + /// Clear announcement before reannouncement delay after the execution block. ColdkeySwapClearTooEarly, - /// Disabled temporarily. + /// Operation temporarily disabled in runtime (hotfix switch; no active raise site now). DisabledTemporarily, - /// Registration Price Limit Exceeded + /// `burned_register` price limit below current subnet registration burn (`Burn`). RegistrationPriceLimitExceeded, - /// Lock hotkey mismatch: existing lock is for a different hotkey. + /// Existing conviction lock on this coldkey+netuid is bound to a different hotkey. LockHotkeyMismatch, - /// Insufficient stake on subnet to cover the lock amount. + /// Lock amount exceeds the coldkey's total alpha stake on that subnet (incl. locked mass). InsufficientStakeForLock, - /// No existing lock found for the given coldkey and subnet. + /// No conviction lock for this coldkey on the given subnet. NoExistingLock, - /// There is already an active lock for the given coldkey. + /// Coldkey already has an active nonzero lock on that subnet; cannot create another. ActiveLockExists, - /// A system account cannot be used in this operation + /// Hotkey is a reserved subnet system account (`netuid_for_subnet_account`); use a user key. CannotUseSystemAccount, - /// Trying to unlock more than locked + /// Unlock requested more alpha than is currently locked. UnlockAmountTooHigh, - /// Waiting for dissolved subnet cleanup. + /// Intended guard while a dissolved netuid is still cleaning up (declared; not wired). WaitingForDissolvedSubnetCleanup, - /// The supplied tempo is outside the allowed range. + /// Supplied tempo outside the allowed range for the subnet. TempoOutOfBounds, - /// The supplied activity-cutoff factor is outside the allowed range. + /// Activity-cutoff factor outside the allowed per-mille range (1000–50000). ActivityCutoffFactorMilliOutOfBounds, - /// An epoch trigger is already pending for this subnet; wait for it to fire - /// before triggering again. + /// `trigger_epoch`: a previous manual epoch is still pending (`PendingEpochAt`). EpochTriggerAlreadyPending, - /// The next automatic epoch is already imminent; a manual trigger would have - /// no effect. + /// `trigger_epoch`: next automatic epoch is already within `AdminFreezeWindow`. AutoEpochAlreadyImminent, - /// `trigger_epoch` is blocked because commit-reveal is enabled for this subnet: - /// an out-of-band epoch would desync the CRv3 reveal window from the wall-clock - /// Drand schedule and silently drop committed weights. + /// `trigger_epoch` blocked while commit-reveal is on (would desync CRv3 from Drand). DynamicTempoBlockedByCommitReveal, - /// The destination coldkey rejects incoming locked alpha. + /// Destination coldkey `AccountFlags` reject incoming locked alpha. AccountRejectsLockedAlpha, - /// The coldkey has already registered too many subnets + /// Network-registration lock-id counter hit `u32::MAX` while queueing a registration. LockIdOverFlow, - /// Need to wait more blocks to do the start call. + /// `start_call` before `NetworkRegisteredAt` + `StartCallDelay` blocks have passed. StartCallNotReady, - /// The caller does not have enough Alpha stake for the operation. + /// Stake decrease would debit more alpha than the coldkey–hotkey pair holds on the subnet. InsufficientAlphaBalance, - /// Coldkey swap could not fully migrate miner collateral: the old - /// coldkey's [`ColdkeyMinerCollateral`] aggregate remained non-zero - /// after migrating every indexed collateral hotkey. Failing closed - /// avoids under-locking the destination unstake guard. + /// Coldkey swap could not fully migrate miner collateral (`ColdkeyMinerCollateral` nonzero). ColdkeyCollateralIncomplete, - /// This coldkey already has the maximum number of distinct hotkeys - /// with miner collateral on the subnet - /// ([`crate::MAX_COLDKEY_COLLATERAL_HOTKEYS`]). + /// Coldkey already at [`crate::MAX_COLDKEY_COLLATERAL_HOTKEYS`] collateral hotkeys on subnet. ColdkeyCollateralPositionsFull, } } diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index 5391c2a027..bf1674adb3 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -1,19 +1,32 @@ use frame_support::pallet_macros::pallet_section; -/// A [`pallet_section`] that defines the events for a pallet. -/// This can later be imported into the pallet using [`import_section`]. +/// `pallet_section` defining [`Event`] for the subtensor pallet (`SubtensorModule` in runtime metadata). +/// +/// Imported into the pallet via [`import_section`]. Variant **names and order are frozen** for +/// metadata / client compatibility — docs only may change. #[pallet_section] mod events { use codec::Compact; + /// On-chain events emitted by `SubtensorModule`. + /// + /// Prefer searching variant names (e.g. `StakeAdded`, `WeightsSet`) from deposit sites or + /// explorers; field order for tuple variants is documented on each variant. #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { - /// a new network is added. + /// A subnet was registered and added to the active set. + /// + /// Fields: `(netuid, mechid)`. NetworkAdded(NetUid, u16), - /// a network is removed. + /// A subnet was dissolved / removed from the active set. + /// + /// Fields: `(netuid)`. NetworkRemoved(NetUid), - /// stake has been transferred from the a coldkey account onto the hotkey staking account. + /// Stake was added: TAO from a coldkey was swapped into alpha and credited to a hotkey on a subnet. + /// + /// Fields: `(coldkey, hotkey, tao, alpha, netuid, fee)`. + /// `tao` / `alpha` are the amounts involved in the stake add; `fee` is the swap fee paid (rao). StakeAdded( T::AccountId, T::AccountId, @@ -22,7 +35,9 @@ mod events { NetUid, u64, ), - /// stake has been removed from the hotkey staking account onto the coldkey account. + /// Stake was removed: alpha on a hotkey/subnet was swapped back to TAO and paid to the coldkey. + /// + /// Fields: `(coldkey, hotkey, tao, alpha, netuid, fee)`. StakeRemoved( T::AccountId, T::AccountId, @@ -31,7 +46,9 @@ mod events { NetUid, u64, ), - /// stake has been moved from origin (hotkey, subnet ID) to destination (hotkey, subnet ID) of this amount (in TAO). + /// Stake was moved between hotkeys and/or subnets for the same coldkey (TAO-equivalent amount). + /// + /// Fields: `(coldkey, origin_hotkey, origin_netuid, destination_hotkey, destination_netuid, tao)`. StakeMoved( T::AccountId, T::AccountId, @@ -40,257 +57,389 @@ mod events { NetUid, TaoBalance, ), - /// a caller successfully sets their weights on a subnetwork. + /// A neuron successfully set weights on a subnet (or mechanism index). + /// + /// Fields: `(netuid_index, uid)`. WeightsSet(NetUidStorageIndex, u16), - /// a new neuron account has been registered to the chain. + /// A hotkey was registered as a neuron on a subnet and assigned a uid. + /// + /// Fields: `(netuid, uid, hotkey)`. NeuronRegistered(NetUid, u16, T::AccountId), - /// multiple uids have been concurrently registered. + /// Multiple neurons were registered in one bulk operation. + /// + /// Fields: `(u16, u16)` — historically subnet / count style args; no active deposit site today. BulkNeuronsRegistered(u16, u16), // FIXME: Not used yet. - /// bulk balances have been set (placeholder: this event is currently unused). + /// Placeholder for a bulk-balance-set path; currently unused. + /// + /// Fields: `(u16, u16)`. BulkBalancesSet(u16, u16), - /// max allowed uids has been set for a subnetwork. + /// Max allowed uids (`MaxAllowedUids`) was set for a subnet. + /// + /// Fields: `(netuid, max_allowed_uids)`. MaxAllowedUidsSet(NetUid, u16), - /// max weight limit has been set for a subnet (deprecated: this event is unused). + /// Max weight limit was set for a subnet (deprecated: limit is now constant; event unused). + /// + /// Fields: `(netuid, max_weight_limit)`. #[deprecated(note = "Max weight limit is now a constant and this event is unused")] MaxWeightLimitSet(NetUid, u16), - /// the difficulty has been set for a subnet. + /// PoW registration difficulty was set for a subnet. + /// + /// Fields: `(netuid, difficulty)`. DifficultySet(NetUid, u64), - /// the adjustment interval is set for a subnet. + /// Difficulty adjustment interval (blocks) was set for a subnet. + /// + /// Fields: `(netuid, adjustment_interval)`. AdjustmentIntervalSet(NetUid, u16), - /// registration per interval is set for a subnet. + /// Target registrations per adjustment interval was set for a subnet. + /// + /// Fields: `(netuid, target_registrations)`. RegistrationPerIntervalSet(NetUid, u16), - /// we set max registrations per block. + /// Max neuron registrations allowed per block was set for a subnet. + /// + /// Fields: `(netuid, max_registrations_per_block)`. MaxRegistrationsPerBlockSet(NetUid, u16), - /// an activity cutoff is set for a subnet. + /// Activity cutoff (blocks without update before a neuron is inactive) was set for a subnet. + /// + /// Fields: `(netuid, activity_cutoff)`. ActivityCutoffSet(NetUid, u16), - /// Rho value is set. + /// Consensus hyperparameter Rho was set for a subnet. + /// + /// Fields: `(netuid, rho)`. RhoSet(NetUid, u16), - /// steepness of the sigmoid used to compute alpha values. + /// Steepness of the sigmoid used when computing alpha values was set for a subnet. + /// + /// Fields: `(netuid, steepness)`. AlphaSigmoidSteepnessSet(NetUid, i16), - /// Kappa is set for a subnet. + /// Consensus hyperparameter Kappa was set for a subnet. + /// + /// Fields: `(netuid, kappa)`. KappaSet(NetUid, u16), - /// minimum allowed weight is set for a subnet. + /// Minimum allowed weight value was set for a subnet. + /// + /// Fields: `(netuid, min_allowed_weight)`. MinAllowedWeightSet(NetUid, u16), - /// the validator pruning length has been set. + /// Validator pruning length was set for a subnet. + /// + /// Fields: `(netuid, validator_prune_len)`. ValidatorPruneLenSet(NetUid, u64), - /// the scaling law power has been set for a subnet. + /// Scaling-law power hyperparameter was set for a subnet. + /// + /// Fields: `(netuid, scaling_law_power)`. ScalingLawPowerSet(NetUid, u16), - /// weights set rate limit has been set for a subnet. + /// Rate limit (blocks) between weight-set extrinsics was set for a subnet. + /// + /// Fields: `(netuid, rate_limit)`. WeightsSetRateLimitSet(NetUid, u64), - /// immunity period is set for a subnet. + /// Immunity period (blocks) for newly registered neurons was set for a subnet. + /// + /// Fields: `(netuid, immunity_period)`. ImmunityPeriodSet(NetUid, u16), - /// bonds moving average is set for a subnet. + /// Bonds moving-average hyperparameter was set for a subnet. + /// + /// Fields: `(netuid, bonds_moving_average)`. BondsMovingAverageSet(NetUid, u64), - /// bonds penalty is set for a subnet. + /// Bonds penalty hyperparameter was set for a subnet. + /// + /// Fields: `(netuid, bonds_penalty)`. BondsPenaltySet(NetUid, u16), - /// bonds reset is set for a subnet. + /// Whether bonds reset on weight set was configured for a subnet. + /// + /// Fields: `(netuid, bonds_reset_on)`. BondsResetOnSet(NetUid, bool), - /// setting the max number of allowed validators on a subnet. + /// Max allowed validators on a subnet was set. + /// + /// Fields: `(netuid, max_allowed_validators)`. MaxAllowedValidatorsSet(NetUid, u16), - /// the axon server information is added to the network. + /// Axon (serve) endpoint metadata was published for a hotkey on a subnet. + /// + /// Fields: `(netuid, hotkey)`. AxonServed(NetUid, T::AccountId), - /// the prometheus server information is added to the network. + /// Prometheus endpoint metadata was published for a hotkey on a subnet. + /// + /// Fields: `(netuid, hotkey)`. PrometheusServed(NetUid, T::AccountId), - /// a hotkey has become a delegate. + /// A hotkey became a delegate (nominatable) with the given take. + /// + /// Fields: `(coldkey, hotkey, take)`. DelegateAdded(T::AccountId, T::AccountId, PerU16), - /// the default take is set. + /// Global default delegate take was set. + /// + /// Fields: `(default_take)`. DefaultTakeSet(PerU16), - /// weights version key is set for a network. + /// Weights version key required by a subnet was set. + /// + /// Fields: `(netuid, weights_version_key)`. WeightsVersionKeySet(NetUid, u64), - /// setting min difficulty on a network. + /// Minimum PoW difficulty for a subnet was set. + /// + /// Fields: `(netuid, min_difficulty)`. MinDifficultySet(NetUid, u64), - /// setting max difficulty on a network. + /// Maximum PoW difficulty for a subnet was set. + /// + /// Fields: `(netuid, max_difficulty)`. MaxDifficultySet(NetUid, u64), - /// setting the prometheus serving rate limit. + /// Rate limit for axon/prometheus serve updates was set for a subnet. + /// + /// Fields: `(netuid, serving_rate_limit)`. ServingRateLimitSet(NetUid, u64), - /// setting burn on a network. + /// Current registration burn (TAO) was set for a subnet. + /// + /// Fields: `(netuid, burn)`. BurnSet(NetUid, TaoBalance), - /// setting max burn on a network. + /// Maximum registration burn (TAO) was set for a subnet. + /// + /// Fields: `(netuid, max_burn)`. MaxBurnSet(NetUid, TaoBalance), - /// setting min burn on a network. + /// Minimum registration burn (TAO) was set for a subnet. + /// + /// Fields: `(netuid, min_burn)`. MinBurnSet(NetUid, TaoBalance), - /// setting the per-block epoch cap (dynamic tempo throttle). + /// Per-block cap on how many subnet epochs may run (dynamic tempo throttle) was set. + /// + /// Fields: `(max_epochs_per_block)`. MaxEpochsPerBlockSet(u8), - /// setting the transaction rate limit. + /// Global transaction rate limit was set. + /// + /// Fields: `(tx_rate_limit)`. TxRateLimitSet(u64), - /// setting the delegate take transaction rate limit. + /// Rate limit for delegate-take changes was set. + /// + /// Fields: `(tx_delegate_take_rate_limit)`. TxDelegateTakeRateLimitSet(u64), - /// setting the childkey take transaction rate limit. + /// Rate limit for childkey-take changes was set. + /// + /// Fields: `(tx_childkey_take_rate_limit)`. TxChildKeyTakeRateLimitSet(u64), - /// setting the admin freeze window length (last N blocks of tempo) + /// Admin freeze window length (last N blocks of a tempo where owner admin calls are frozen) was set. + /// + /// Fields: `(admin_freeze_window)`. AdminFreezeWindowSet(u16), - /// setting the owner hyperparameter rate limit in epochs + /// Owner hyperparameter rate limit, measured in epochs, was set. + /// + /// Fields: `(owner_hyperparam_rate_limit_epochs)`. OwnerHyperparamRateLimitSet(u16), - /// minimum childkey take set + /// Global minimum childkey take was set. + /// + /// Fields: `(min_childkey_take)`. MinChildKeyTakeSet(PerU16), - /// subnet-specific minimum childkey take set + /// Per-subnet minimum childkey take was set. + /// + /// Fields: `(netuid, min_childkey_take)`. MinChildKeyTakePerSubnetSet(NetUid, PerU16), - /// maximum childkey take set + /// Global maximum childkey take was set. + /// + /// Fields: `(max_childkey_take)`. MaxChildKeyTakeSet(PerU16), - /// childkey take set + /// Childkey take for a specific hotkey was set. + /// + /// Fields: `(hotkey, childkey_take)`. ChildKeyTakeSet(T::AccountId, PerU16), - /// a sudo call is done. + /// A privileged sudo call finished with the given dispatch result. + /// + /// Fields: `(result)`. Sudid(DispatchResult), - /// registration is allowed/disallowed for a subnet. + /// Whether normal (non-PoW) registration is allowed was toggled for a subnet. + /// + /// Fields: `(netuid, registration_allowed)`. RegistrationAllowed(NetUid, bool), - /// POW registration is allowed/disallowed for a subnet. + /// Whether PoW registration is allowed was toggled for a subnet. + /// + /// Fields: `(netuid, pow_registration_allowed)`. PowRegistrationAllowed(NetUid, bool), - /// setting tempo on a network + /// Subnet tempo (blocks per epoch) was set. + /// + /// Fields: `(netuid, tempo)`. TempoSet(NetUid, u16), - /// setting the RAO recycled for registration. + /// RAO recycled into the subnet pool on registration was set. + /// + /// Fields: `(netuid, rao_recycled)`. RAORecycledForRegistrationSet(NetUid, TaoBalance), - /// min stake is set for validators to set weights. + /// Minimum stake threshold required for validators to set weights was set. + /// + /// Fields: `(stake_threshold)`. StakeThresholdSet(u64), - /// setting the adjustment alpha on a subnet. + /// Difficulty adjustment alpha was set for a subnet. + /// + /// Fields: `(netuid, adjustment_alpha)`. AdjustmentAlphaSet(NetUid, u64), - /// the faucet it called on the test net. + /// Testnet faucet credited free balance to an account. + /// + /// Fields: `(coldkey, balance_added)`. Faucet(T::AccountId, u64), - /// the subnet owner cut is set. + /// Global subnet-owner cut of emissions was set. + /// + /// Fields: `(subnet_owner_cut)`. SubnetOwnerCutSet(u16), - /// the network creation rate limit is set. + /// Minimum blocks between network registrations was set. + /// + /// Fields: `(network_rate_limit)`. NetworkRateLimitSet(u64), - /// the network immunity period is set. + /// Network immunity period (blocks a new subnet is immune from deregistration) was set. + /// + /// Fields: `(network_immunity_period)`. NetworkImmunityPeriodSet(u64), - /// the start call delay is set. + /// Delay before `start_call` may enable emissions on a new subnet was set. + /// + /// Fields: `(start_call_delay)`. StartCallDelaySet(u64), - /// the network minimum locking cost is set. + /// Minimum TAO lock cost to register a new subnet was set. + /// + /// Fields: `(network_min_lock_cost)`. NetworkMinLockCostSet(TaoBalance), - /// the maximum number of subnets is set + /// Maximum number of subnets was set. + /// + /// Fields: `(subnet_limit)`. SubnetLimitSet(u16), - /// the lock cost reduction is set + /// Interval over which network lock cost decays was set. + /// + /// Fields: `(lock_cost_reduction_interval)`. NetworkLockCostReductionIntervalSet(u64), - /// the take for a delegate is decreased. + /// A delegate decreased its take. + /// + /// Fields: `(coldkey, hotkey, take)`. TakeDecreased(T::AccountId, T::AccountId, PerU16), - /// the take for a delegate is increased. + /// A delegate increased its take. + /// + /// Fields: `(coldkey, hotkey, take)`. TakeIncreased(T::AccountId, T::AccountId, PerU16), - /// the hotkey is swapped + /// A coldkey swapped its associated hotkey globally. HotkeySwapped { - /// the account ID of coldkey + /// Coldkey that owns the hotkey association. coldkey: T::AccountId, - /// the account ID of old hotkey + /// Hotkey being replaced. old_hotkey: T::AccountId, - /// the account ID of new hotkey + /// Hotkey that replaces `old_hotkey`. new_hotkey: T::AccountId, }, - /// maximum delegate take is set by sudo/admin transaction + /// Maximum delegate take was set via sudo/admin. + /// + /// Fields: `(max_delegate_take)`. MaxDelegateTakeSet(PerU16), - /// minimum delegate take is set by sudo/admin transaction + /// Minimum delegate take was set via sudo/admin. + /// + /// Fields: `(min_delegate_take)`. MinDelegateTakeSet(PerU16), - /// A coldkey swap announcement has been made. + /// A coldkey announced an intent to swap to a new coldkey (commitment by hash). ColdkeySwapAnnounced { - /// The account ID of the coldkey that made the announcement. + /// Coldkey that made the announcement. who: T::AccountId, - /// The hash of the new coldkey. + /// Hash commitment of the new coldkey. new_coldkey_hash: T::Hash, }, - /// A coldkey swap has been reset. + /// A pending coldkey swap announcement was reset for an account. ColdkeySwapReset { - /// The account ID of the coldkey for which the swap has been reset. + /// Coldkey whose swap announcement was cleared/reset. who: T::AccountId, }, - /// A coldkey has been swapped. + /// A coldkey swap completed; ownership moved from `old_coldkey` to `new_coldkey`. ColdkeySwapped { - /// The account ID of old coldkey. + /// Previous coldkey. old_coldkey: T::AccountId, - /// The account ID of new coldkey. + /// New coldkey that now owns the accounts/stake. new_coldkey: T::AccountId, }, - /// A coldkey swap has been disputed. + /// A coldkey swap was disputed during the arbitration window. ColdkeySwapDisputed { - /// The account ID of the coldkey that was disputed. + /// Coldkey whose swap was disputed. coldkey: T::AccountId, }, - /// All balance of a hotkey has been unstaked and transferred to a new coldkey + /// All balance of a hotkey was unstaked and transferred to a new coldkey during a swap path. AllBalanceUnstakedAndTransferredToNewColdkey { - /// The account ID of the current coldkey + /// Coldkey that previously owned the funds. current_coldkey: T::AccountId, - /// The account ID of the new coldkey + /// Coldkey that received the unstaked balance. new_coldkey: T::AccountId, - /// The total balance of the hotkey + /// Total free balance transferred. total_balance: <::Currency as fungible::Inspect< ::AccountId, >>::Balance, }, - /// The arbitration period has been extended + /// The arbitration period for a coldkey swap was extended. ArbitrationPeriodExtended { - /// The account ID of the coldkey + /// Coldkey whose arbitration window was extended. coldkey: T::AccountId, }, - /// Setting of children of a hotkey have been scheduled + /// Setting children of a parent hotkey was scheduled (cooldown before it takes effect). + /// + /// Fields: `(hotkey, netuid, cooldown_block, children)` where each child is `(proportion, child_hotkey)`. SetChildrenScheduled(T::AccountId, NetUid, u64, Vec<(u64, T::AccountId)>), - /// The children of a hotkey have been set + /// Children of a parent hotkey were applied on a subnet. + /// + /// Fields: `(hotkey, netuid, children)` where each child is `(proportion, child_hotkey)`. SetChildren(T::AccountId, NetUid, Vec<(u64, T::AccountId)>), // /// The hotkey emission tempo has been set // HotkeyEmissionTempoSet(u64), // /// The network maximum stake has been set // NetworkMaxStakeSet(u16, u64), - /// The identity of a coldkey has been set + /// On-chain identity for a coldkey was set or updated. + /// + /// Fields: `(coldkey)`. ChainIdentitySet(T::AccountId), - /// The identity of a subnet has been set + /// On-chain identity metadata for a subnet was set or updated. + /// + /// Fields: `(netuid)`. SubnetIdentitySet(NetUid), - /// The identity of a subnet has been removed + /// On-chain identity metadata for a subnet was removed. + /// + /// Fields: `(netuid)`. SubnetIdentityRemoved(NetUid), - /// A dissolve network extrinsic scheduled. + /// Dissolving a subnet was scheduled for a future block. DissolveNetworkScheduled { - /// The account ID schedule the dissolve network extrinsic + /// Account that scheduled the dissolve. account: T::AccountId, - /// network ID will be dissolved + /// Subnet that will be dissolved. netuid: NetUid, - /// extrinsic execution block number + /// Block at which the dissolve extrinsic executes. execution_block: BlockNumberFor, }, - /// The coldkey swap announcement delay has been set. + /// Delay between coldkey-swap announcement and execution was set. + /// + /// Fields: `(announcement_delay)`. ColdkeySwapAnnouncementDelaySet(BlockNumberFor), - /// The coldkey swap reannouncement delay has been set. + /// Delay required before re-announcing a coldkey swap was set. + /// + /// Fields: `(reannouncement_delay)`. ColdkeySwapReannouncementDelaySet(BlockNumberFor), - /// The duration of dissolve network has been set + /// Duration used when scheduling network dissolve was set. + /// + /// Fields: `(schedule_duration)`. DissolveNetworkScheduleDurationSet(BlockNumberFor), - /// Commit-reveal v3 weights have been successfully committed. + /// Commit-reveal v3 weights were committed (hash only; reveal comes later). /// - /// * **who**: The account ID of the user committing the weights. - /// * **netuid**: The network identifier. - /// * **commit_hash**: The hash representing the committed weights. + /// Fields: `(who, netuid_index, commit_hash)`. CRV3WeightsCommitted(T::AccountId, NetUidStorageIndex, H256), - /// Weights have been successfully committed. + /// Weights were committed under the commit-reveal flow (hash only). /// - /// * **who**: The account ID of the user committing the weights. - /// * **netuid**: The network identifier. - /// * **commit_hash**: The hash representing the committed weights. + /// Fields: `(who, netuid_index, commit_hash)`. WeightsCommitted(T::AccountId, NetUidStorageIndex, H256), - /// Weights have been successfully revealed. + /// Previously committed weights were revealed on-chain. /// - /// * **who**: The account ID of the user revealing the weights. - /// * **netuid**: The network identifier. - /// * **commit_hash**: The hash of the revealed weights. + /// Fields: `(who, netuid_index, commit_hash)`. WeightsRevealed(T::AccountId, NetUidStorageIndex, H256), - /// Weights have been successfully batch revealed. + /// Multiple previously committed weight sets were revealed in one batch. /// - /// * **who**: The account ID of the user revealing the weights. - /// * **netuid**: The network identifier. - /// * **revealed_hashes**: A vector of hashes representing each revealed weight set. + /// Fields: `(who, netuid, revealed_hashes)`. WeightsBatchRevealed(T::AccountId, NetUid, Vec), - /// A batch of weights (or commits) have been force-set. + /// A batch of weight sets / commits completed successfully for the listed netuids. /// - /// * **netuids**: The netuids these weights were successfully set/committed for. - /// * **who**: The hotkey that set this batch. + /// Fields: `(netuids, hotkey)`. BatchWeightsCompleted(Vec>, T::AccountId), - /// A batch extrinsic completed but with some errors. + /// A batch weight extrinsic finished but at least one item failed (see `BatchWeightItemFailed`). BatchCompletedWithErrors(), - /// A weight set among a batch of weights failed. + /// One item inside a batch weight set/commit failed. /// - /// * **netuid**: The netuid of the batch item that failed. - /// * **error**: The dispatch error emitted by the failed item. + /// Fields: `(netuid, error)`. BatchWeightItemFailed(NetUid, sp_runtime::DispatchError), - /// Stake has been transferred from one coldkey to another on the same subnet. - /// Parameters: - /// (origin_coldkey, destination_coldkey, hotkey, origin_netuid, destination_netuid, amount) + /// Stake was transferred from one coldkey to another (same hotkey; subnets may differ). + /// + /// Fields: `(origin_coldkey, destination_coldkey, hotkey, origin_netuid, destination_netuid, tao)`. StakeTransferred( T::AccountId, T::AccountId, @@ -300,348 +449,329 @@ mod events { TaoBalance, ), - /// Stake has been swapped from one subnet to another for the same coldkey-hotkey pair. + /// Stake was swapped from one subnet to another for the same coldkey–hotkey pair. /// - /// Parameters: - /// (coldkey, hotkey, origin_netuid, destination_netuid, amount) + /// Fields: `(coldkey, hotkey, origin_netuid, destination_netuid, tao)`. StakeSwapped(T::AccountId, T::AccountId, NetUid, NetUid, TaoBalance), - /// Event called when transfer is toggled on a subnet. + /// Stake transfer to/from a subnet was enabled or disabled. /// - /// Parameters: - /// (netuid, bool) + /// Fields: `(netuid, transfers_enabled)`. TransferToggle(NetUid, bool), - /// The owner hotkey for a subnet has been set. + /// Owner hotkey for a subnet was set (hotkey authorized for owner actions). /// - /// Parameters: - /// (netuid, new_hotkey) + /// Fields: `(netuid, owner_hotkey)`. SubnetOwnerHotkeySet(NetUid, T::AccountId), - /// FirstEmissionBlockNumber is set via start call extrinsic + /// First block at which a subnet may emit was set (typically via `start_call`). /// - /// Parameters: - /// netuid - /// block number + /// Fields: `(netuid, first_emission_block)`. FirstEmissionBlockNumberSet(NetUid, u64), - /// Alpha has been recycled, reducing AlphaOut on a subnet. + /// Alpha was recycled from a stake position, reducing `AlphaOut` on the subnet. /// - /// Parameters: - /// (coldkey, hotkey, amount, subnet_id) + /// Fields: `(coldkey, hotkey, alpha, netuid)`. AlphaRecycled(T::AccountId, T::AccountId, AlphaBalance, NetUid), - /// Alpha have been burned without reducing AlphaOut. + /// Alpha was burned from a stake position without reducing `AlphaOut`. /// - /// Parameters: - /// (coldkey, hotkey, amount, subnet_id) + /// Fields: `(coldkey, hotkey, alpha, netuid)`. AlphaBurned(T::AccountId, T::AccountId, AlphaBalance, NetUid), - /// An EVM key has been associated with a hotkey. + /// An EVM address was associated with a hotkey on a subnet. EvmKeyAssociated { - /// The subnet that the hotkey belongs to. + /// Subnet the hotkey belongs to. netuid: NetUid, - /// The hotkey associated with the EVM key. + /// Hotkey associated with the EVM key. hotkey: T::AccountId, - /// The EVM key being associated with the hotkey. + /// EVM address being associated. evm_key: H160, - /// The block where the association happened. + /// Block at which the association was recorded. block_associated: u64, }, - /// CRV3 Weights have been successfully revealed. + /// Commit-reveal v3 weights were revealed for a hotkey on a subnet. /// - /// * **netuid**: The network identifier. - /// * **who**: The account ID of the user revealing the weights. + /// Fields: `(netuid, who)`. CRV3WeightsRevealed(NetUid, T::AccountId), - /// Commit-Reveal periods has been successfully set. + /// Commit-reveal reveal period (epochs) was set for a subnet. /// - /// * **netuid**: The network identifier. - /// * **periods**: The number of epochs before the reveal. + /// Fields: `(netuid, periods)`. CommitRevealPeriodsSet(NetUid, u64), - /// Commit-Reveal has been successfully toggled. + /// Commit-reveal weight setting was enabled or disabled for a subnet. /// - /// * **netuid**: The network identifier. - /// * **Enabled**: Is Commit-Reveal enabled. + /// Fields: `(netuid, enabled)`. CommitRevealEnabled(NetUid, bool), - /// the hotkey is swapped + /// A coldkey swapped its hotkey association on a single subnet only. HotkeySwappedOnSubnet { - /// the account ID of coldkey + /// Coldkey that owns the association. coldkey: T::AccountId, - /// the account ID of old hotkey + /// Hotkey being replaced on this subnet. old_hotkey: T::AccountId, - /// the account ID of new hotkey + /// Replacement hotkey on this subnet. new_hotkey: T::AccountId, - /// the subnet ID + /// Subnet where the hotkey association changed. netuid: NetUid, }, - /// A subnet lease has been created. + /// A subnet lease was created for a beneficiary. SubnetLeaseCreated { - /// The beneficiary of the lease. + /// Beneficiary of the lease. beneficiary: T::AccountId, - /// The lease ID + /// Lease identifier. lease_id: LeaseId, - /// The subnet ID + /// Leased subnet. netuid: NetUid, - /// The end block of the lease + /// Optional end block; `None` means open-ended until terminated. end_block: Option>, }, - /// A subnet lease has been terminated. + /// A subnet lease was terminated. SubnetLeaseTerminated { - /// The beneficiary of the lease. + /// Former beneficiary of the lease. beneficiary: T::AccountId, - /// The subnet ID + /// Subnet whose lease ended. netuid: NetUid, }, - /// The symbol for a subnet has been updated. + /// Token symbol metadata for a subnet was updated. SymbolUpdated { - /// The subnet ID + /// Subnet whose symbol changed. netuid: NetUid, - /// The symbol that has been updated. + /// New symbol bytes. symbol: Vec, }, - /// Commit Reveal Weights version has been updated. + /// Required commit-reveal protocol version was set globally. /// - /// * **version**: The required version. + /// Fields: `(version)`. CommitRevealVersionSet(u16), - /// Timelocked weights have been successfully committed. + /// Timelocked weights were committed (reveal allowed at `reveal_round`). /// - /// * **who**: The account ID of the user committing the weights. - /// * **netuid**: The network identifier. - /// * **commit_hash**: The hash representing the committed weights. - /// * **reveal_round**: The round at which weights can be revealed. + /// Fields: `(who, netuid_index, commit_hash, reveal_round)`. TimelockedWeightsCommitted(T::AccountId, NetUidStorageIndex, H256, u64), - /// Timelocked Weights have been successfully revealed. + /// Timelocked weights were revealed. /// - /// * **netuid**: The network identifier. - /// * **who**: The account ID of the user revealing the weights. + /// Fields: `(netuid_index, who)`. TimelockedWeightsRevealed(NetUidStorageIndex, T::AccountId), - /// Auto-staking hotkey received stake + /// Auto-staking path credited alpha incentive to a destination hotkey. AutoStakeAdded { - /// Subnet identifier. + /// Subnet of the auto-stake. netuid: NetUid, /// Destination account that received the auto-staked funds. destination: T::AccountId, - /// Hotkey account whose stake was auto-staked. + /// Hotkey whose stake was auto-staked. hotkey: T::AccountId, - /// Owner (coldkey) account associated with the hotkey. + /// Owner coldkey associated with the hotkey. owner: T::AccountId, - /// Amount of alpha auto-staked. + /// Amount of alpha auto-staked (incentive). incentive: AlphaBalance, }, - /// End-of-epoch miner incentive alpha by UID + /// End-of-epoch miner incentive alpha was emitted, indexed by uid. IncentiveAlphaEmittedToMiners { - /// Subnet identifier. + /// Subnet (mechanism) index for this emission. netuid: NetUidStorageIndex, - /// UID-indexed array of miner incentive alpha; index equals UID. + /// UID-indexed miner incentive alpha; vector index equals uid. emissions: Vec, }, - /// The minimum allowed UIDs for a subnet have been set. + /// Minimum allowed uids for a subnet was set. + /// + /// Fields: `(netuid, min_allowed_uids)`. MinAllowedUidsSet(NetUid, u16), - /// The auto stake destination has been set. - /// - /// * **coldkey**: The account ID of the coldkey. - /// * **netuid**: The network identifier. - /// * **hotkey**: The account ID of the hotkey. + /// Auto-stake destination hotkey was set for a coldkey on a subnet. AutoStakeDestinationSet { - /// The account ID of the coldkey. + /// Coldkey configuring the destination. coldkey: T::AccountId, - /// The network identifier. + /// Subnet the destination applies to. netuid: NetUid, - /// The account ID of the hotkey. + /// Hotkey that will receive auto-staked funds. hotkey: T::AccountId, }, - /// The minimum allowed non-Immune UIDs has been set. + /// Minimum number of non-immune uids required on a subnet was set. + /// + /// Fields: `(netuid, min_non_immune_uids)`. MinNonImmuneUidsSet(NetUid, u16), - /// Root emissions have been claimed for a coldkey on all subnets and hotkeys. - /// Parameters: - /// (coldkey) + /// Root emissions were claimed for a coldkey across its subnets/hotkeys. RootClaimed { - /// Claim coldkey + /// Coldkey that claimed root emissions. coldkey: T::AccountId, }, - /// Root claim type for a coldkey has been set. - /// Parameters: - /// (coldkey, u8) + /// Root claim type for a coldkey was configured. RootClaimTypeSet { - /// Claim coldkey + /// Coldkey whose claim type changed. coldkey: T::AccountId, - /// Claim type + /// Selected root claim type. root_claim_type: RootClaimTypeEnum, }, - /// Voting power tracking has been enabled for a subnet. + /// Voting-power tracking was enabled for a subnet. VotingPowerTrackingEnabled { - /// The subnet ID + /// Subnet where tracking started. netuid: NetUid, }, - /// Voting power tracking has been scheduled for disabling. - /// Tracking will continue until disable_at_block, then stop and clear entries. + /// Voting-power tracking disable was scheduled; tracking continues until `disable_at_block`. VotingPowerTrackingDisableScheduled { - /// The subnet ID + /// Subnet whose tracking will stop. netuid: NetUid, - /// Block at which tracking will be disabled + /// Block at which tracking disables and entries clear. disable_at_block: u64, }, - /// Voting power tracking has been fully disabled and entries cleared. + /// Voting-power tracking was fully disabled and entries cleared for a subnet. VotingPowerTrackingDisabled { - /// The subnet ID + /// Subnet where tracking stopped. netuid: NetUid, }, - /// Voting power EMA alpha has been set for a subnet. + /// Voting-power EMA alpha was set for a subnet (`u64` with 18-decimal fixed-point precision). VotingPowerEmaAlphaSet { - /// The subnet ID + /// Subnet whose EMA alpha changed. netuid: NetUid, - /// The new alpha value (u64 with 18 decimal precision) + /// New alpha value (u64 with 18 decimal precision). alpha: u64, }, - /// Subnet lease dividends have been distributed. + /// Subnet lease dividends (alpha) were distributed to a contributor. SubnetLeaseDividendsDistributed { - /// The lease ID + /// Lease that paid the dividend. lease_id: LeaseId, - /// The contributor + /// Contributor receiving alpha. contributor: T::AccountId, - /// The amount of alpha distributed + /// Alpha amount distributed. alpha: AlphaBalance, }, - /// "Add stake and burn" event: alpha token was purchased and burned. + /// Add-stake-and-burn: TAO was used to buy alpha that was then burned. AddStakeBurn { - /// The subnet ID + /// Subnet where alpha was purchased and burned. netuid: NetUid, - /// hotky account ID + /// Hotkey path used for the stake/burn. hotkey: T::AccountId, - /// Tao provided + /// TAO provided as input. amount: TaoBalance, - /// Alpha burned + /// Alpha that was burned. alpha: AlphaBalance, }, - /// data for a dissolved network has been cleaned up. + /// Deferred cleanup of storage for a dissolved subnet completed. NetworkDissolveCleanupCompleted { - /// The subnet ID + /// Dissolved subnet whose residual maps were cleaned. netuid: NetUid, }, - /// A coldkey swap announcement has been cleared. + /// A coldkey swap announcement was cleared without completing the swap. ColdkeySwapCleared { - /// The account ID of the coldkey that cleared the announcement. + /// Coldkey that cleared its announcement. who: T::AccountId, }, - /// Transaction fee was paid in Alpha. + /// A transaction fee was paid in alpha (in addition to any TAO fee accounting). /// - /// Emitted in addition to `TransactionFeePaid` when the fee payment path is Alpha. - /// `alpha_fee` is the exact Alpha amount deducted. + /// Emitted alongside fee payment when the fee path uses alpha; `alpha_fee` is the exact + /// alpha deducted and `tao_amount` is the TAO-equivalent from the swap. TransactionFeePaidWithAlpha { - /// Account that paid the transaction fee. + /// Account that paid the fee. who: T::AccountId, - /// Netuid + /// Subnet whose alpha was used to pay the fee. netuid: NetUid, - /// Exact fee deducted in Alpha units. + /// Exact fee deducted in alpha. alpha_fee: AlphaBalance, - /// Resulting swapped TAO amount + /// TAO amount obtained from swapping the alpha fee. tao_amount: TaoBalance, }, - /// Burn half-life set for neuron registration. + /// Registration burn half-life was set for a subnet. BurnHalfLifeSet { - /// The subnet identifier. + /// Subnet whose burn half-life changed. netuid: NetUid, - /// The burn half-life value for neuron registration. + /// Burn half-life used by the registration burn schedule. burn_half_life: u16, }, - /// Burn increase multiplier set for neuron registration. + /// Registration burn increase multiplier was set for a subnet. BurnIncreaseMultSet { - /// The subnet identifier. + /// Subnet whose burn increase multiplier changed. netuid: NetUid, - /// The burn increase multiplier value for neuron registration. + /// Multiplier applied when increasing registration burn. burn_increase_mult: U64F64, }, - /// A root validator toggled the "auto parent delegation" flag. + /// A root validator toggled auto parent-delegation. AutoParentDelegationEnabledSet { - /// The validator hotkey. + /// Validator hotkey whose flag changed. hotkey: T::AccountId, - /// Whether delegation is now enabled. + /// Whether auto parent-delegation is now enabled. enabled: bool, }, - /// Stake has been locked to a hotkey on a subnet. + /// Stake (alpha) was locked to a hotkey on a subnet. StakeLocked { - /// The coldkey that locked the stake. + /// Coldkey that locked the stake. coldkey: T::AccountId, - /// The hotkey the stake is locked to. + /// Hotkey the stake is locked to. hotkey: T::AccountId, - /// The subnet the stake is locked on. + /// Subnet the stake is locked on. netuid: NetUid, - /// The alpha amount locked. + /// Alpha amount locked. amount: AlphaBalance, }, - /// Stake has been unlocked from a hotkey on a subnet. + /// Previously locked stake (alpha) was unlocked from a hotkey on a subnet. StakeUnlocked { - /// The coldkey that unlocked the stake. + /// Coldkey that unlocked the stake. coldkey: T::AccountId, - /// The hotkey the stake was locked to. + /// Hotkey the stake was locked to. hotkey: T::AccountId, - /// The subnet the stake was locked on. + /// Subnet the stake was locked on. netuid: NetUid, - /// The alpha amount unlocked. + /// Alpha amount unlocked. amount: AlphaBalance, }, - /// Stake has been unlocked from a hotkey on a subnet. + /// A stake lock was moved from one hotkey to another on the same subnet (same coldkey). LockMoved { - /// The coldkey that moved the lock. + /// Coldkey that moved the lock. coldkey: T::AccountId, - /// The hotkey the lock was moved from. + /// Hotkey the lock was moved from. origin_hotkey: T::AccountId, - /// The hotkey the lock was moved to. + /// Hotkey the lock was moved to. destination_hotkey: T::AccountId, - /// The subnet the lock is on. + /// Subnet the lock remains on. netuid: NetUid, }, - /// Activity-cutoff factor (per-mille) set on a subnet by its owner. + /// Activity-cutoff factor (per-mille) was set on a subnet by its owner. ActivityCutoffFactorMilliSet { - /// The subnet identifier. + /// Subnet whose activity-cutoff factor changed. netuid: NetUid, - /// Factor (per-mille). + /// Factor in per-mille. factor_milli: u32, }, - /// Owner manually triggered an epoch for their subnet. + /// Subnet owner manually triggered an epoch; execution is deferred until `fires_at`. EpochTriggered { - /// The subnet identifier. + /// Subnet whose epoch was triggered. netuid: NetUid, - /// The account that triggered the epoch. + /// Account that triggered the epoch. by: T::AccountId, - /// The earliest block at which the triggered epoch may execute. + /// Earliest block at which the triggered epoch may execute. fires_at: u64, }, - /// An epoch slot was deferred to the next block due to the per-block epoch cap. + /// An epoch slot was deferred to a later block due to the per-block epoch cap. EpochDeferred { - /// The subnet identifier. + /// Subnet whose epoch was deferred. netuid: NetUid, /// Block at which the epoch was originally scheduled. from_block: u64, @@ -649,86 +779,86 @@ mod events { to_block: u64, }, - /// Epoch execution skipped by `is_epoch_input_state_consistent` returned false or other errors. + /// An epoch slot was skipped (e.g. inconsistent input state or other execution error). EpochSkipped { - /// The subnet identifier. + /// Subnet whose epoch was skipped. netuid: NetUid, - /// The block at which the slot was consumed. + /// Block at which the slot was consumed without running the epoch. block: u64, }, - /// Subnet ownership was reassigned by lock conviction. + /// Subnet ownership was reassigned (e.g. via lock conviction). SubnetOwnerChanged { - /// The subnet whose owner changed. + /// Subnet whose owner changed. netuid: NetUid, - /// The previous owner coldkey. + /// Previous owner coldkey. old_coldkey: T::AccountId, - /// The new owner coldkey. + /// New owner coldkey. new_coldkey: T::AccountId, }, - /// A coldkey's perpetual lock flag was updated. + /// A coldkey's perpetual-lock flag was updated for a subnet. PerpetualLockUpdated { - /// The coldkey whose flag changed. + /// Coldkey whose flag changed. coldkey: T::AccountId, - /// The subnet whose coldkey flag changed. + /// Subnet the flag applies to. netuid: NetUid, - /// Whether this coldkey's locks are now perpetual. + /// Whether this coldkey's locks on the subnet are now perpetual. enabled: bool, }, - /// A network registration cost has been queued. + /// A network registration was queued (pending activation / later materialization). NetworkRegistrationQueued { - /// The network registration information. + /// Coldkey that paid / owns the registration. coldkey: T::AccountId, - /// The hotkey that registered the network. + /// Hotkey supplied at registration. hotkey: T::AccountId, - /// The mechanism that registered the network. + /// Mechanism id used for the registration. mechid: u16, - /// The identity that registered the network. + /// Optional subnet identity attached at registration. identity: Option, - /// The lock amount that registered the network. + /// TAO locked for the registration. lock_amount: TaoBalance, - /// The median subnet alpha price that registered the network. + /// Median subnet alpha price snapshot used for pricing. median_subnet_alpha_price: U64F64, - /// The block at which the network was registered. + /// Block at which the registration was queued. registration_block: u64, }, - /// A coldkey's reject locked alpha account flag was updated. + /// A coldkey toggled whether it rejects incoming locked alpha. RejectLockedAlphaUpdated { - /// The coldkey whose flag changed. + /// Coldkey whose flag changed. coldkey: T::AccountId, /// Whether this coldkey rejects incoming locked alpha. enabled: bool, }, - /// Stake has been transferred from one coldkey to another, landing on a - /// different hotkey (and optionally a different subnet). + /// Stake was transferred from one coldkey to another, landing on a different hotkey + /// (and optionally a different subnet). StakeAndHotkeyTransferred { - /// The coldkey the stake left. + /// Coldkey the stake left. origin_coldkey: T::AccountId, - /// The coldkey that now owns the stake. + /// Coldkey that now owns the stake. destination_coldkey: T::AccountId, - /// The hotkey the stake left. + /// Hotkey the stake left. origin_hotkey: T::AccountId, - /// The hotkey the stake landed on. + /// Hotkey the stake landed on. destination_hotkey: T::AccountId, - /// The subnet the stake left. + /// Subnet the stake left. origin_netuid: NetUid, - /// The subnet the stake landed on. + /// Subnet the stake landed on. destination_netuid: NetUid, - /// The TAO-equivalent amount moved. + /// TAO-equivalent amount moved. amount: TaoBalance, }, - /// Miner collateral was staked and locked (at registration or via - /// `add_collateral`). Appended at the end of the enum to avoid - /// shifting existing event indices. + /// Miner collateral was staked and locked (at registration or via `add_collateral`). + /// + /// Appended at the end of the enum so existing event indices stay stable. CollateralLocked { /// Subnet identifier. netuid: NetUid, - /// The miner hotkey the collateral is attached to. + /// Miner hotkey the collateral is attached to. hotkey: T::AccountId, /// Alpha locked by this operation. locked: AlphaBalance, @@ -740,9 +870,9 @@ mod events { MinCollateralSet { /// Subnet identifier. netuid: NetUid, - /// The miner hotkey the floor applies to. + /// Miner hotkey the floor applies to. hotkey: T::AccountId, - /// The new floor; zero clears it. + /// New floor; zero clears it. min_locked: AlphaBalance, }, } diff --git a/pallets/subtensor/src/macros/genesis.rs b/pallets/subtensor/src/macros/genesis.rs index aba9d528db..dcbfd0f50c 100644 --- a/pallets/subtensor/src/macros/genesis.rs +++ b/pallets/subtensor/src/macros/genesis.rs @@ -1,12 +1,15 @@ use frame_support::pallet_macros::pallet_section; -/// A [`pallet_section`] that defines the errors for a pallet. -/// This can later be imported into the pallet using [`import_section`]. +/// [`pallet_section`] defining genesis build for the subtensor pallet (imported via [`import_section`]). +/// +/// Seeds root (`NetUid::ROOT`) and a bootstrap dynamic subnet (netuid 1) used by local/dev chains. +/// Production vs fast-runtime owner keys are selected via `prod_or_fast!`. #[pallet_section] mod genesis { use sp_core::crypto::Pair; use sp_core::sr25519::Pair as Sr25519Pair; + /// Applies [`GenesisConfig`] at chain start: issuance, optional `start_call` delay, root network, and netuid-1 pool. #[pallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { @@ -23,19 +26,20 @@ mod genesis { let alice_hk_account = T::AccountId::decode(&mut &alice_hk_bytes[..]) .expect("Alice hotkey account should decode"); + // Prod: `DefaultSubnetOwner`; fast/dev: Alice coldkey + `//Alice_hk` hotkey. let subnet_root_owner = prod_or_fast!(DefaultSubnetOwner::::get(), alice_account); let subnet_root_owner_hotkey = prod_or_fast!(DefaultSubnetOwner::::get(), alice_hk_account); - // Set initial total issuance from balances + // Align SubtensorModule issuance with the balances-pallet genesis figure (rao). TotalIssuance::::put(self.balances_issuance); - // Set start call delay if provided in genesis config + // Optional override for blocks before `start_call` may enable emissions. if let Some(delay) = self.start_call_delay { StartCallDelay::::put(delay); } - // Set the root network as added. + // --- Root network (netuid 0): senate-sized, open registration, no weight floor. --- NetworksAdded::::insert(NetUid::ROOT, true); // Increment the number of total networks. @@ -74,6 +78,7 @@ mod genesis { Pallet::::get_symbol_for_subnet(NetUid::ROOT), ); + // --- Bootstrap subnet netuid 1: dynamic mechanism, seeded AMM reserves, uid 0 = DefaultAccount. --- let netuid = NetUid::from(1); let hotkey = DefaultAccount::::get(); SubnetMechanism::::insert(netuid, 1); // Make dynamic. diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index 6d3692d9a2..25a8b68076 100644 --- a/pallets/subtensor/src/macros/hooks.rs +++ b/pallets/subtensor/src/macros/hooks.rs @@ -1,22 +1,25 @@ #![allow(clippy::crate_in_macro_def)] use frame_support::pallet_macros::pallet_section; -/// A [`pallet_section`] that defines the events for a pallet. -/// This can later be imported into the pallet using [`import_section`]. +/// [`pallet_section`] defining FRAME hooks for the subtensor pallet (imported via [`import_section`]). +/// +/// Owns `on_initialize` (block step), `on_runtime_upgrade` (migration chain), `on_idle`, and +/// try-runtime state checks. Migration **name strings** are frozen for idempotency (Tier D). #[pallet_section] mod hooks { - // ================ - // ==== Hooks ===== - // ================ + /// Block and runtime-upgrade hooks for SubtensorModule. + /// + /// `on_initialize` always charges `WeightInfo::block_step` plus hotkey-swap cleanup weight, + /// even when `block_step` returns an error (logged, not reverted). #[pallet::hooks] impl Hooks> for Pallet { - // ---- Called on the initialization of this pallet. (the order of on_finalize calls is determined in the runtime) - // - // # Args: - // * 'n': (BlockNumberFor): - // - The number of the block we are initializing. + /// Runs each block: purge stale subnet hotkey-swap records, then `block_step` (epochs / coinbase). + /// + /// # Args + /// * `block_number` — current block; selects which netuid slot is cleaned via + /// `HotkeySwapOnSubnetInterval` residue. fn on_initialize(block_number: BlockNumberFor) -> Weight { - let hotkey_swap_clean_up_weight = Self::clean_up_hotkey_swap_records(block_number); + let hotkey_swap_clean_up_weight = Self::purge_expired_hotkey_swap_on_netuid_records(block_number); let block_step_result = Self::block_step(); match block_step_result { @@ -35,6 +38,10 @@ mod hooks { } } + /// Chains historical storage migrations; each call is expected to be idempotent via its own guards. + /// + /// Order is load-bearing for chains that have not yet run older steps. Do not rename migration + /// identifiers that are recorded in `HasMigrationRun` / similar. fn on_runtime_upgrade() -> frame_support::weights::Weight { // --- Migrate storage let mut weight = frame_support::weights::Weight::from_parts(0, 0); @@ -187,6 +194,7 @@ mod hooks { weight } + /// Try-runtime invariant checks; stake total check is currently disabled (see PR #1166). #[cfg(feature = "try-runtime")] fn try_state(_n: BlockNumberFor) -> Result<(), sp_runtime::TryRuntimeError> { // Disabled: https://github.com/RaoFoundation/subtensor/pull/1166 @@ -194,6 +202,9 @@ mod hooks { Ok(()) } + /// Idle-time cleanup: dissolve leftover subnet data, then drain the network registration queue. + /// + /// Stops when the remaining weight budget would be exceeded. fn on_idle(_block: BlockNumberFor, limit: Weight) -> Weight { let mut weight = Self::remove_data_for_dissolved_networks(limit); @@ -206,9 +217,11 @@ mod hooks { } impl Pallet { - // This function is to clean up the old hotkey swap records - // It just clean up for one subnet at a time, according to the block number - pub(crate) fn clean_up_hotkey_swap_records(block_number: BlockNumberFor) -> Weight { + /// Removes expired `LastHotkeySwapOnNetuid` rows for the netuid slot matching this block. + /// + /// Each block only touches netuids where `netuid % HotkeySwapOnSubnetInterval` equals + /// `block_number % HotkeySwapOnSubnetInterval`, spreading cleanup across the interval. + pub(crate) fn purge_expired_hotkey_swap_on_netuid_records(block_number: BlockNumberFor) -> Weight { let mut weight = Weight::from_parts(0, 0); let hotkey_swap_on_subnet_interval = T::HotkeySwapOnSubnetInterval::get(); let block_number: u64 = TryInto::try_into(block_number) diff --git a/pallets/subtensor/src/macros/mod.rs b/pallets/subtensor/src/macros/mod.rs index e491ec8c40..d52b3bdf0b 100644 --- a/pallets/subtensor/src/macros/mod.rs +++ b/pallets/subtensor/src/macros/mod.rs @@ -1,3 +1,9 @@ +//! `pallet_section` modules composing the SubtensorModule pallet. +//! +//! Each child is imported into `pallet` via `import_section`. Dispatchables, events, and errors +//! live in sibling modules and are frozen for metadata; this module also owns `config`, +//! `genesis`, and `hooks`. + pub mod config; pub mod dispatches; pub mod errors; diff --git a/pallets/subtensor/src/migrations/migrate_associated_evm_address_index.rs b/pallets/subtensor/src/migrations/migrate_associated_evm_address_index.rs index 5de0ce0f85..84c1b86913 100644 --- a/pallets/subtensor/src/migrations/migrate_associated_evm_address_index.rs +++ b/pallets/subtensor/src/migrations/migrate_associated_evm_address_index.rs @@ -3,14 +3,9 @@ use frame_support::{traits::Get, weights::Weight}; use scale_info::prelude::string::String; use sp_std::vec::Vec; -/// Backfill the reverse index `AssociatedUidsByEvmAddress` from the existing -/// `AssociatedEvmAddress` forward map. One-time, idempotent, guarded by `HasMigrationRun`. +/// Backfill reverse index `AssociatedUidsByEvmAddress` from existing `AssociatedEvmAddress` rows. /// -/// This scans the whole forward map in a single block. That is safe because the map is tiny: it -/// grows only through `do_associate_evm_key`, an opt-in, signature-gated, rate-limited extrinsic. -/// Measured on 2026-07-07, the entire map holds **100 entries on Finney and 20 on testnet**, with a -/// largest single-`(netuid, evm_key)` bucket of **3** (against the cap of 32). There is no realistic -/// chain state in which this scan is expensive, so no chunked / multi-block migration is warranted. +/// Idempotency key (frozen): `migrate_associated_evm_address_index`. pub fn migrate_associated_evm_address_index() -> Weight { let migration_name = b"migrate_associated_evm_address_index".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_auto_stake_destination.rs b/pallets/subtensor/src/migrations/migrate_auto_stake_destination.rs index e478a3581a..98e703a881 100644 --- a/pallets/subtensor/src/migrations/migrate_auto_stake_destination.rs +++ b/pallets/subtensor/src/migrations/migrate_auto_stake_destination.rs @@ -18,7 +18,10 @@ pub mod deprecated_auto_stake_destination_format { StorageMap, Blake2_128Concat, AccountIdOf, AccountIdOf, OptionQuery>; } -/// Migrate the AutoStakeDestination map from single map to double map format +/// Rewrites `AutoStakeDestination` from a single map (coldkey → hotkey) into a double map keyed by (coldkey, netuid), +/// and backfills the inverse `AutoStakeDestinationColdkeys` index. Skips root (`NetUid::ROOT`). +/// +/// Idempotency key (frozen): `migrate_auto_stake_destination`. pub fn migrate_auto_stake_destination() -> Weight { use deprecated_auto_stake_destination_format as old; diff --git a/pallets/subtensor/src/migrations/migrate_cleanup_swap_v3.rs b/pallets/subtensor/src/migrations/migrate_cleanup_swap_v3.rs index cebbf373ec..7c96bc53b1 100644 --- a/pallets/subtensor/src/migrations/migrate_cleanup_swap_v3.rs +++ b/pallets/subtensor/src/migrations/migrate_cleanup_swap_v3.rs @@ -17,6 +17,10 @@ pub mod deprecated_swap_maps { StorageMap, Identity, NetUid, AlphaBalance, ValueQuery>; } +/// Folds deprecated user-provided swap liquidity (`SubnetTaoProvided` / `SubnetAlphaInProvided`) into +/// `SubnetTAO` / `SubnetAlphaIn` reserves, then clears those obsolete maps. +/// +/// Idempotency key (frozen): `migrate_cleanup_swap_v3`. pub fn migrate_cleanup_swap_v3() -> Weight { let migration_name = b"migrate_cleanup_swap_v3".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_clear_deprecated_registration_maps.rs b/pallets/subtensor/src/migrations/migrate_clear_deprecated_registration_maps.rs index 4d062699e6..a7c8f21cb2 100644 --- a/pallets/subtensor/src/migrations/migrate_clear_deprecated_registration_maps.rs +++ b/pallets/subtensor/src/migrations/migrate_clear_deprecated_registration_maps.rs @@ -2,6 +2,10 @@ use super::*; use frame_support::{traits::Get, weights::Weight}; use scale_info::prelude::string::String; +/// Clears deprecated registration tracking maps (`NetworkPowRegistrationAllowed`, +/// `POWRegistrationsThisInterval`, `BurnRegistrationsThisInterval`) without touching the new-model storage. +/// +/// Idempotency key (frozen): `migrate_clear_deprecated_registration_maps_v1`. pub fn migrate_clear_deprecated_registration_maps() -> Weight { let migration_name = b"migrate_clear_deprecated_registration_maps_v1".to_vec(); let mut weight: Weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_clear_orphan_subnet_identities_v3.rs b/pallets/subtensor/src/migrations/migrate_clear_orphan_subnet_identities_v3.rs index 293dd34833..3448bda415 100644 --- a/pallets/subtensor/src/migrations/migrate_clear_orphan_subnet_identities_v3.rs +++ b/pallets/subtensor/src/migrations/migrate_clear_orphan_subnet_identities_v3.rs @@ -4,20 +4,9 @@ use scale_info::prelude::string::String; use sp_std::vec::Vec; use subtensor_runtime_common::NetUid; -/// Remove `SubnetIdentitiesV3` entries that belong to netuids which are no -/// longer registered networks (`!NetworksAdded`). +/// Removes `SubnetIdentitiesV3` entries whose netuid is no longer in the active subnet set. /// -/// Such orphan identities accumulate when a subnet slot is recycled: a new -/// owner registers a subnet in a previously-used netuid without supplying an -/// identity, and the stale identity from the prior owner is left in place, -/// misleading participants about what the subnet is. The registration path -/// (`set_new_network_state`) only writes on `Some(identity)` and never clears a -/// pre-existing entry, and the historical `migrate_subnet_identities_to_v3` -/// migration copied V2 entries unconditionally. -/// -/// This mirrors the identity-clear in `do_dissolve_network` as a one-shot -/// upgrade migration: orphan entries are removed and a `SubnetIdentityRemoved` -/// event is emitted for each; identities of live subnets are untouched. +/// Idempotency key (frozen): `migrate_clear_orphan_subnet_identities_v3`. pub fn migrate_clear_orphan_subnet_identities_v3() -> Weight { let migration_name = b"migrate_clear_orphan_subnet_identities_v3".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_coldkey_collateral_hotkeys.rs b/pallets/subtensor/src/migrations/migrate_coldkey_collateral_hotkeys.rs index 3d3026a0f5..af25e69ffa 100644 --- a/pallets/subtensor/src/migrations/migrate_coldkey_collateral_hotkeys.rs +++ b/pallets/subtensor/src/migrations/migrate_coldkey_collateral_hotkeys.rs @@ -2,20 +2,10 @@ use super::*; use frame_support::{traits::Get, weights::Weight}; use scale_info::prelude::string::String; -/// Backfill [`ColdkeyCollateralHotkeys`] from existing [`MinerCollateral`] rows. +/// Backfills [`ColdkeyCollateralHotkeys`] from existing [`MinerCollateral`] rows so coldkeys can look up +/// their collateralized hotkeys without scanning the collateral map. /// -/// Collateral rows may exist before this index shipped (testnets / early -/// deploys). Without a backfill, lazy indexing can fill the 32-entry cap while -/// a legacy unindexed row remains, so a later hotkey swap fails after mutating -/// storage. This migration indexes every standing row up to -/// [`crate::MAX_COLDKEY_COLLATERAL_HOTKEYS`] per `(netuid, coldkey)`. -/// -/// Over-cap coldkeys keep their aggregate and MinerCollateral rows; only the -/// index stops growing. Coldkey swaps for those coldkeys fail closed with -/// [`Error::ColdkeyCollateralIncomplete`] until enough indexed positions drain. -/// That branch is unreachable for any realistic mainnet state at first deploy -/// (collateral ships with this index), but keeps the maps consistent under the -/// pallet's bound. +/// Idempotency key (frozen): `migrate_coldkey_collateral_hotkeys`. pub fn migrate_coldkey_collateral_hotkeys() -> Weight { let migration_name = b"migrate_coldkey_collateral_hotkeys".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_coldkey_swap_scheduled.rs b/pallets/subtensor/src/migrations/migrate_coldkey_swap_scheduled.rs index 243d953ac1..7a30ad5460 100644 --- a/pallets/subtensor/src/migrations/migrate_coldkey_swap_scheduled.rs +++ b/pallets/subtensor/src/migrations/migrate_coldkey_swap_scheduled.rs @@ -17,7 +17,9 @@ pub mod deprecated_coldkey_swap_scheduled_format { StorageMap, Blake2_128Concat, AccountIdOf, (), ValueQuery>; } -/// Migrate the ColdkeySwapScheduled map to the new storage format +/// Migrates `ColdkeySwapScheduled` into the post-schedule storage layout used by coldkey-swap scheduling. +/// +/// Idempotency key (frozen): `migrate_coldkey_swap_scheduled`. pub fn migrate_coldkey_swap_scheduled() -> Weight { use deprecated_coldkey_swap_scheduled_format as old; diff --git a/pallets/subtensor/src/migrations/migrate_coldkey_swap_scheduled_to_announcements.rs b/pallets/subtensor/src/migrations/migrate_coldkey_swap_scheduled_to_announcements.rs index 52b2a66e8f..dfc5cf7e4f 100644 --- a/pallets/subtensor/src/migrations/migrate_coldkey_swap_scheduled_to_announcements.rs +++ b/pallets/subtensor/src/migrations/migrate_coldkey_swap_scheduled_to_announcements.rs @@ -50,6 +50,11 @@ pub mod deprecated { } } +/// Migrates scheduled coldkey swaps into the announcement model: clears old schedule-duration values, +/// rewrites future `ColdkeySwapScheduled` entries into `ColdkeySwapAnnouncements`, and cancels matching +/// scheduler call entries for the old `swap_coldkey` extrinsic. +/// +/// Idempotency key (frozen): `migrate_coldkey_swap_scheduled_to_announcements`. pub fn migrate_coldkey_swap_scheduled_to_announcements() -> Weight { let migration_name = b"migrate_coldkey_swap_scheduled_to_announcements".to_vec(); diff --git a/pallets/subtensor/src/migrations/migrate_commit_reveal_settings.rs b/pallets/subtensor/src/migrations/migrate_commit_reveal_settings.rs index 54df469600..b3c5717f49 100644 --- a/pallets/subtensor/src/migrations/migrate_commit_reveal_settings.rs +++ b/pallets/subtensor/src/migrations/migrate_commit_reveal_settings.rs @@ -7,6 +7,10 @@ use subtensor_runtime_common::NetUid; use super::*; +/// Enables commit-reveal weights on all non-root subnets and ensures `RevealPeriodEpochs` is at least +/// `MIN_COMMIT_REVEAL_PEROIDS` when previously zero. +/// +/// Idempotency key (frozen): `migrate_commit_reveal_settings`. pub fn migrate_commit_reveal_settings() -> Weight { let migration_name = b"migrate_commit_reveal_settings".to_vec(); diff --git a/pallets/subtensor/src/migrations/migrate_commit_reveal_v2.rs b/pallets/subtensor/src/migrations/migrate_commit_reveal_v2.rs index bedc43d724..3ede2bee83 100644 --- a/pallets/subtensor/src/migrations/migrate_commit_reveal_v2.rs +++ b/pallets/subtensor/src/migrations/migrate_commit_reveal_v2.rs @@ -4,6 +4,10 @@ use frame_support::{traits::Get, weights::Weight}; use scale_info::prelude::string::String; use sp_io::{KillStorageResult, hashing::twox_128, storage::clear_prefix}; +/// Removes obsolete commit-reveal v1 storage prefixes `WeightCommitRevealInterval` and `WeightCommits` +/// under `SubtensorModule` ahead of the CRV2/CRV3 layouts. +/// +/// Idempotency key (frozen): `migrate_commit_reveal_2_v2`. pub fn migrate_commit_reveal_2() -> Weight { let migration_name = b"migrate_commit_reveal_2_v2".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_create_root_network.rs b/pallets/subtensor/src/migrations/migrate_create_root_network.rs index 6cca34f815..def5ce3efe 100644 --- a/pallets/subtensor/src/migrations/migrate_create_root_network.rs +++ b/pallets/subtensor/src/migrations/migrate_create_root_network.rs @@ -19,26 +19,10 @@ pub mod deprecated_loaded_emission_format { StorageMap, Identity, u16, Vec<(AccountIdOf, u64)>, OptionQuery>; } -/// Migrates the storage to create the root network +/// Creates the root subnet (`NetUid::ROOT`) with bootstrap hyperparameters if it is not already present. /// -/// This function performs the following steps: -/// 1. Checks if the root network already exists -/// 2. If not, creates the root network with default settings -/// 3. Removes all existing senate members -/// -/// # Arguments -/// -/// * `T` - The Config trait of the pallet -/// -/// # Returns -/// -/// * `Weight` - The computational weight of this operation -/// -/// # Example -/// -/// ```ignore -/// let weight = migrate_create_root_network::(); -/// ``` +/// Sets max uids/validators to 64, tempo 100, registration open, and target registrations to 1 per interval. +/// No-op when `NetworksAdded(ROOT)` is already true. Does not use [`HasMigrationRun`]. pub fn migrate_create_root_network() -> Weight { // Initialize weight counter let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_crv3_commits_add_block.rs b/pallets/subtensor/src/migrations/migrate_crv3_commits_add_block.rs index bf5a0bb2b5..51b9dedd12 100644 --- a/pallets/subtensor/src/migrations/migrate_crv3_commits_add_block.rs +++ b/pallets/subtensor/src/migrations/migrate_crv3_commits_add_block.rs @@ -4,9 +4,9 @@ use log; use scale_info::prelude::string::String; use sp_std::collections::vec_deque::VecDeque; -/// --------------- Migration ------------------------------------------ -/// Upgrades every entry to the new 4-tuple layout by inserting -/// `commit_block = first_block_of_epoch(netuid, epoch)`. +/// Upgrades CRV3 weight-commit queue entries to include the commit block number (`CRV3WeightCommitsV2`). +/// +/// Idempotency key (frozen): `crv3_commits_add_block_v1`. pub fn migrate_crv3_commits_add_block() -> Weight { let mig_name: Vec = b"crv3_commits_add_block_v1".to_vec(); let mut total_weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_crv3_v2_to_timelocked.rs b/pallets/subtensor/src/migrations/migrate_crv3_v2_to_timelocked.rs index 7ae5a2529c..c18b6c4614 100644 --- a/pallets/subtensor/src/migrations/migrate_crv3_v2_to_timelocked.rs +++ b/pallets/subtensor/src/migrations/migrate_crv3_v2_to_timelocked.rs @@ -5,8 +5,10 @@ use scale_info::prelude::string::String; use sp_std::vec::Vec; // --------------- Migration ------------------------------------------ -/// Moves every (netuid, epoch) queue from `CRV3WeightCommitsV2` into -/// `TimelockedWeightCommits`. Identical key/value layout → pure move. +/// Moves every (netuid, epoch) commit queue from `CRV3WeightCommitsV2` into `TimelockedWeightCommits`, +/// then clears the old map. Bridge from CRV3 v2 storage to the timelocked commit-reveal layout. +/// +/// Idempotency key (frozen): `crv3_v2_to_timelocked_v1`. pub fn migrate_crv3_v2_to_timelocked() -> Weight { let mig_name: Vec = b"crv3_v2_to_timelocked_v1".to_vec(); let mut total_weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_delete_subnet_21.rs b/pallets/subtensor/src/migrations/migrate_delete_subnet_21.rs index d5dadfc576..d78eba3df4 100644 --- a/pallets/subtensor/src/migrations/migrate_delete_subnet_21.rs +++ b/pallets/subtensor/src/migrations/migrate_delete_subnet_21.rs @@ -20,26 +20,9 @@ pub mod deprecated_loaded_emission_format { StorageMap, Identity, u16, Vec<(AccountIdOf, u64)>, OptionQuery>; } -/// Migrates the storage to delete subnet 21 +/// One-shot cleanup that removes all subnet-scoped storage for netuid 21 (historical subnet deletion). /// -/// This function performs the following steps: -/// 1. Checks if the migration is necessary -/// 2. Removes all storage related to subnet 21 -/// 3. Updates the storage version -/// -/// # Arguments -/// -/// * `T` - The Config trait of the pallet -/// -/// # Returns -/// -/// * `Weight` - The computational weight of this operation -/// -/// # Example -/// -/// ```ignore -/// let weight = migrate_delete_subnet_21::(); -/// ``` +/// Does not use [`HasMigrationRun`]; safe to re-run because deletes are idempotent on missing keys. pub fn migrate_delete_subnet_21() -> Weight { let new_storage_version = 4; @@ -50,7 +33,7 @@ pub fn migrate_delete_subnet_21() -> Weight { let onchain_version = Pallet::::on_chain_storage_version(); // Only runs if we haven't already updated version past above new_storage_version and subnet 21 exists. - if onchain_version < new_storage_version && Pallet::::if_subnet_exist(NetUid::from(21)) { + if onchain_version < new_storage_version && Pallet::::subnet_exists(NetUid::from(21)) { info!(target: LOG_TARGET, ">>> Removing subnet 21 {onchain_version:?}"); let netuid = NetUid::from(21); diff --git a/pallets/subtensor/src/migrations/migrate_delete_subnet_3.rs b/pallets/subtensor/src/migrations/migrate_delete_subnet_3.rs index 600bce38a9..94e645d126 100644 --- a/pallets/subtensor/src/migrations/migrate_delete_subnet_3.rs +++ b/pallets/subtensor/src/migrations/migrate_delete_subnet_3.rs @@ -20,26 +20,9 @@ pub mod deprecated_loaded_emission_format { StorageMap, Identity, u16, Vec<(AccountIdOf, u64)>, OptionQuery>; } -/// Migrates the storage to delete subnet 3 +/// One-shot cleanup that removes all subnet-scoped storage for netuid 3 (historical subnet deletion). /// -/// This function performs the following steps: -/// 1. Checks if the migration is necessary -/// 2. Removes all storage related to subnet 3 -/// 3. Updates the storage version -/// -/// # Arguments -/// -/// * `T` - The Config trait of the pallet -/// -/// # Returns -/// -/// * `Weight` - The computational weight of this operation -/// -/// # Example -/// -/// ```ignore -/// let weight = migrate_delete_subnet_3::(); -/// ``` +/// Does not use [`HasMigrationRun`]; safe to re-run because deletes are idempotent on missing keys. pub fn migrate_delete_subnet_3() -> Weight { let new_storage_version = 5; @@ -50,7 +33,7 @@ pub fn migrate_delete_subnet_3() -> Weight { let onchain_version = Pallet::::on_chain_storage_version(); // Only proceed if current version is less than the new version and subnet 3 exists - if onchain_version < new_storage_version && Pallet::::if_subnet_exist(3.into()) { + if onchain_version < new_storage_version && Pallet::::subnet_exists(3.into()) { info!( target: LOG_TARGET, "Removing subnet 3. Current version: {onchain_version:?}" diff --git a/pallets/subtensor/src/migrations/migrate_disable_commit_reveal.rs b/pallets/subtensor/src/migrations/migrate_disable_commit_reveal.rs index 5465adbdd1..da0417de30 100644 --- a/pallets/subtensor/src/migrations/migrate_disable_commit_reveal.rs +++ b/pallets/subtensor/src/migrations/migrate_disable_commit_reveal.rs @@ -3,6 +3,9 @@ use crate::HasMigrationRun; use frame_support::{traits::Get, weights::Weight}; use scale_info::prelude::string::String; +/// Force-disables `CommitRevealWeightsEnabled` for every subnet (emergency / cutover helper). +/// +/// Idempotency key (frozen): `disable_commit_reveal_v1`. pub fn migrate_disable_commit_reveal() -> Weight { const MIG_NAME: &[u8] = b"disable_commit_reveal_v1"; diff --git a/pallets/subtensor/src/migrations/migrate_dynamic_tempo.rs b/pallets/subtensor/src/migrations/migrate_dynamic_tempo.rs index c359b96c2f..b7b6d35a88 100644 --- a/pallets/subtensor/src/migrations/migrate_dynamic_tempo.rs +++ b/pallets/subtensor/src/migrations/migrate_dynamic_tempo.rs @@ -5,33 +5,10 @@ use scale_info::prelude::string::String; use sp_core::H256; use sp_std::collections::vec_deque::VecDeque; -/// One-shot migration for the dynamic-tempo / owner-triggered-epochs feature. +/// One-shot migration for dynamic-tempo / owner-triggered epochs: initializes `SubnetEpochIndex`, +/// `LastEpochBlock`, `ActivityCutoffFactorMilli`, and rewrites legacy `WeightCommits` layout as needed. /// -/// 1. Back-fills `LastEpochBlock[netuid]` for every existing subnet so the first -/// post-upgrade epoch lands on the same block as the legacy modulo formula -/// `(block + netuid + 1) % (tempo + 1) == 0`. The new scheduler period is -/// `tempo` (next firing at `LastEpochBlock + tempo`). -/// Existing `Tempo[netuid]` values are preserved as-is regardless of whether -/// they fall inside `[MIN_TEMPO, MAX_TEMPO]`. Owner-side `set_tempo` enforces -/// the bounds for new updates; root-side `sudo_set_tempo` can still write any -/// `u16`. Subnets with `Tempo == 0` are left as-is — the legacy short-circuit -/// keeps them dormant and matches their pre-upgrade behaviour. -/// 2. Converts each subnet's existing `ActivityCutoff[netuid]` (absolute block count) -/// into `ActivityCutoffFactorMilli[netuid]` (per-mille of `tempo`) so that -/// `factor * tempo / 1000 ≈ old_cutoff` post-upgrade. Production defaults -/// (`tempo=360`, `cutoff=5000`) round-trip to 5000 blocks exactly via ceiling -/// division. Out-of-range factors are clamped to -/// `[MIN_ACTIVITY_CUTOFF_FACTOR_MILLI, MAX_ACTIVITY_CUTOFF_FACTOR_MILLI]` — -/// extreme historical cutoffs may shift to the nearest representable factor. -/// 3. Seeds `SubnetEpochIndex[netuid]` (the new stateful epoch counter) with the -/// legacy modulo epoch index `(block + netuid + 1) / (tempo + 1)` so that -/// existing commit-reveal commit keys — `TimelockedWeightCommits` (CR-v4) keyed -/// by epoch, and `WeightCommits` (CR-v2) tagged with `commit_epoch` — stay -/// valid and continuous across the upgrade. -/// 4. Rewrites every CR-v2 `WeightCommits` entry to `(hash, commit_epoch, -/// commit_block, _)`: field 1 (previously the absolute `commit_block`) becomes -/// `commit_epoch` under the legacy modulo formula; field 2 keeps the absolute -/// `commit_block` (used by the epoch's commit-reveal weight column-mask). +/// Idempotency key (frozen): `dynamic_tempo_v1`. pub fn migrate_dynamic_tempo() -> Weight { let mig_name: Vec = b"dynamic_tempo_v1".to_vec(); let mig_name_str = String::from_utf8_lossy(&mig_name); diff --git a/pallets/subtensor/src/migrations/migrate_fix_bad_hk_swap.rs b/pallets/subtensor/src/migrations/migrate_fix_bad_hk_swap.rs index 380232e499..b6bbd7929b 100644 --- a/pallets/subtensor/src/migrations/migrate_fix_bad_hk_swap.rs +++ b/pallets/subtensor/src/migrations/migrate_fix_bad_hk_swap.rs @@ -258,6 +258,10 @@ pub fn try_restore_shares() -> Weight { weight } +/// Mainnet-only remediation for a bad hotkey-swap: restores alpha shares for a hardcoded +/// affected hotkey/netuid using a fixed coldkey delta table (`try_restore_shares`). +/// +/// No-ops on non-mainnet genesis. Idempotency key (frozen): `migrate_fix_bad_hk_swap`. pub fn migrate_fix_bad_hk_swap() -> Weight { let migration_name = b"migrate_fix_bad_hk_swap".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_fix_childkeys.rs b/pallets/subtensor/src/migrations/migrate_fix_childkeys.rs index cc0b6988e0..fe5116c991 100644 --- a/pallets/subtensor/src/migrations/migrate_fix_childkeys.rs +++ b/pallets/subtensor/src/migrations/migrate_fix_childkeys.rs @@ -1,6 +1,9 @@ use super::*; use alloc::string::String; +/// Repairs `ChildKeys` / `ParentKeys` linkage inconsistencies from an earlier childkey bug. +/// +/// Idempotency key (frozen): `migrate_fix_childkeys`. pub fn migrate_fix_childkeys() -> Weight { let migration_name = b"migrate_fix_childkeys".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_fix_is_network_member.rs b/pallets/subtensor/src/migrations/migrate_fix_is_network_member.rs index 0821c8b06a..b964dcc76f 100644 --- a/pallets/subtensor/src/migrations/migrate_fix_is_network_member.rs +++ b/pallets/subtensor/src/migrations/migrate_fix_is_network_member.rs @@ -3,6 +3,9 @@ use alloc::string::String; use frame_support::{traits::Get, weights::Weight}; use log; +/// Rebuilds the `IsNetworkMember` index so membership matches current subnet key maps. +/// +/// Idempotency key (frozen): `migrate_fix_is_network_member`. pub fn migrate_fix_is_network_member() -> Weight { let migration_name = b"migrate_fix_is_network_member".to_vec(); diff --git a/pallets/subtensor/src/migrations/migrate_fix_root_claimed_overclaim.rs b/pallets/subtensor/src/migrations/migrate_fix_root_claimed_overclaim.rs index 49109c2421..32a3e14f49 100644 --- a/pallets/subtensor/src/migrations/migrate_fix_root_claimed_overclaim.rs +++ b/pallets/subtensor/src/migrations/migrate_fix_root_claimed_overclaim.rs @@ -16,17 +16,9 @@ struct HotkeySwapFix { new_hotkey_ss58: &'static str, } -/// Cleans up leftover `RootClaimable` state on new hotkeys produced by the buggy -/// `perform_hotkey_swap_on_one_subnet`, which unconditionally moved the entire -/// `RootClaimable` map from the old hotkey to the new hotkey during a -/// single-subnet swap. +/// Cleans leftover `RootClaimable` state on hotkeys produced by a buggy root-claim overclaim path. /// -/// These new hotkeys have no root stake (root swaps are and were guarded), so the -/// transferred claimable state produces no legitimate yield and only blocks future -/// flows. For each affected new hotkey we check that it truly holds no root-subnet -/// alpha and, if so, remove its `RootClaimable` entry. `RootClaimed` watermarks -/// are intentionally left in place — scanning that map does not fit in a single -/// block. +/// Idempotency key (frozen): `migrate_fix_root_claimed_overclaim`. pub fn migrate_fix_root_claimed_overclaim() -> Weight { let migration_name = b"migrate_fix_root_claimed_overclaim".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_fix_root_subnet_tao.rs b/pallets/subtensor/src/migrations/migrate_fix_root_subnet_tao.rs index 982e071ea3..2792ee802f 100644 --- a/pallets/subtensor/src/migrations/migrate_fix_root_subnet_tao.rs +++ b/pallets/subtensor/src/migrations/migrate_fix_root_subnet_tao.rs @@ -2,6 +2,10 @@ use super::migrate_init_total_issuance::migrate_init_total_issuance; use super::*; use alloc::string::String; +/// Sets root `SubnetTAO` to the sum of all hotkeys' `TotalHotkeyAlpha` on `NetUid::ROOT`, then +/// re-runs total-issuance init so counters stay consistent. +/// +/// Idempotency key (frozen): `migrate_fix_root_subnet_tao`. pub fn migrate_fix_root_subnet_tao() -> Weight { let migration_name = b"migrate_fix_root_subnet_tao".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_fix_root_tao_and_alpha_in.rs b/pallets/subtensor/src/migrations/migrate_fix_root_tao_and_alpha_in.rs index 10898f9eb2..95141e18b6 100644 --- a/pallets/subtensor/src/migrations/migrate_fix_root_tao_and_alpha_in.rs +++ b/pallets/subtensor/src/migrations/migrate_fix_root_tao_and_alpha_in.rs @@ -2,6 +2,10 @@ use super::migrate_init_total_issuance::migrate_init_total_issuance; use super::*; use alloc::string::String; +/// Applies hardcoded root-pool corrections for an over-unstake incident: adjusts root +/// `SubnetTAO`, `SubnetAlphaIn`, `SubnetAlphaOut`, `SubnetVolume`, and `TotalStake` by fixed rao deltas. +/// +/// Idempotency key (frozen): `migrate_fix_root_tao_and_alpha_in`. pub fn migrate_fix_root_tao_and_alpha_in() -> Weight { let migration_name = b"migrate_fix_root_tao_and_alpha_in".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_fix_staking_hot_keys.rs b/pallets/subtensor/src/migrations/migrate_fix_staking_hot_keys.rs index 8c0358614d..4e3929e816 100644 --- a/pallets/subtensor/src/migrations/migrate_fix_staking_hot_keys.rs +++ b/pallets/subtensor/src/migrations/migrate_fix_staking_hot_keys.rs @@ -4,6 +4,9 @@ use log; use scale_info::prelude::string::String; use sp_std::collections::btree_map::BTreeMap; +/// Ensures every coldkey with non-zero `Alpha` stake lists the corresponding hotkey in `StakingHotkeys`. +/// +/// Idempotency key (frozen): `migrate_fix_staking_hot_keys`. pub fn migrate_fix_staking_hot_keys() -> Weight { let migration_name = b"migrate_fix_staking_hot_keys".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_fix_subnet_hotkey_lock_swaps.rs b/pallets/subtensor/src/migrations/migrate_fix_subnet_hotkey_lock_swaps.rs index 93af663092..046bceef1c 100644 --- a/pallets/subtensor/src/migrations/migrate_fix_subnet_hotkey_lock_swaps.rs +++ b/pallets/subtensor/src/migrations/migrate_fix_subnet_hotkey_lock_swaps.rs @@ -237,10 +237,10 @@ fn add_to_aggregate( } } -/// Fixes lock state left behind by subnet-scoped hotkey swaps. +/// Repairs conviction / hotkey lock state left inconsistent after subnet-scoped hotkey swaps +/// (`HotkeyLock`, `OwnerLock`, decaying variants, and `LockingColdkeys`). /// -/// If a destination lock already exists for the same coldkey, the old lock is -/// discarded instead of merged. +/// Idempotency key (frozen): `migrate_fix_subnet_hotkey_lock_swaps`. pub fn migrate_fix_subnet_hotkey_lock_swaps() -> Weight { let migration_name = b"migrate_fix_subnet_hotkey_lock_swaps".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_fix_total_issuance_evm_fees.rs b/pallets/subtensor/src/migrations/migrate_fix_total_issuance_evm_fees.rs index 476a9d8b4f..9aa5500596 100644 --- a/pallets/subtensor/src/migrations/migrate_fix_total_issuance_evm_fees.rs +++ b/pallets/subtensor/src/migrations/migrate_fix_total_issuance_evm_fees.rs @@ -2,6 +2,11 @@ use super::*; use frame_support::traits::fungible::Inspect; use frame_support::weights::Weight; +/// Resets `TotalIssuance` to the Balances pallet's authoritative `total_issuance` when either +/// frozen key has not yet run: `migrate_fix_total_issuance_evm_fees` or +/// `migrate_fix_total_issuance_after_dust_collection`. +/// +/// Both keys perform the same reset; the dual names allow re-applying after distinct incidents. pub fn migrate_fix_total_issuance_evm_fees() -> Weight { let migration_names: [&[u8]; 2] = [ // Fix testnet TotalIssuance after the earlier EVM fees issue caused the diff --git a/pallets/subtensor/src/migrations/migrate_init_tao_flow.rs b/pallets/subtensor/src/migrations/migrate_init_tao_flow.rs index 477410600f..47818bb558 100644 --- a/pallets/subtensor/src/migrations/migrate_init_tao_flow.rs +++ b/pallets/subtensor/src/migrations/migrate_init_tao_flow.rs @@ -4,6 +4,9 @@ use frame_support::{traits::Get, weights::Weight}; use super::*; +/// Clears all `SubnetEmaTaoFlow` entries so EMA tao-flow state restarts empty after the feature cutover. +/// +/// Idempotency key (frozen): `migrate_init_tao_flow`. pub fn migrate_init_tao_flow() -> Weight { let migration_name = b"migrate_init_tao_flow".to_vec(); diff --git a/pallets/subtensor/src/migrations/migrate_init_total_issuance.rs b/pallets/subtensor/src/migrations/migrate_init_total_issuance.rs index a4fa157f57..e1616a9d29 100644 --- a/pallets/subtensor/src/migrations/migrate_init_total_issuance.rs +++ b/pallets/subtensor/src/migrations/migrate_init_total_issuance.rs @@ -14,7 +14,9 @@ pub mod deprecated_loaded_emission_format { StorageMap, Identity, u16, Vec<(AccountIdOf, u64)>, OptionQuery>; } -/// This on-going migration is disabled as part of imbalances work. +/// Ongoing total-issuance sync migration — **disabled** (imbalances work); returns zero-weight no-op. +/// +/// Previously recomputed `TotalStake` / `TotalIssuance` from subnet TAO + balances; left as a stub so call sites stay stable. pub(crate) fn migrate_init_total_issuance() -> Weight { // let subnets_len = crate::NetworksAdded::::iter().count() as u64; @@ -58,7 +60,9 @@ pub(crate) fn migrate_init_total_issuance() -> Weight { T::DbWeight::get().reads(0) } -/// This on-going migration is disabled as part of imbalances work. +/// One-shot total-issuance initialization from account balances + stake (imbalances-era bootstrap). +/// +/// Idempotency key (frozen): `migrate_init_total_issuance_once`. pub(crate) fn migrate_init_total_issuance_once() -> Weight { let migration_name = b"migrate_init_total_issuance_once".to_vec(); let weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_kappa_map_to_default.rs b/pallets/subtensor/src/migrations/migrate_kappa_map_to_default.rs index 69b82cae0b..0a7dd9710a 100644 --- a/pallets/subtensor/src/migrations/migrate_kappa_map_to_default.rs +++ b/pallets/subtensor/src/migrations/migrate_kappa_map_to_default.rs @@ -3,6 +3,9 @@ use frame_support::{traits::Get, weights::Weight}; use log; use scale_info::prelude::string::String; +/// Resets per-subnet `Kappa` to the pallet default for every existing netuid. +/// +/// Idempotency key (frozen): `kappa_map_to_default`. pub fn migrate_kappa_map_to_default() -> Weight { let mig_name: Vec = b"kappa_map_to_default".to_vec(); let mig_name_str = String::from_utf8_lossy(&mig_name); diff --git a/pallets/subtensor/src/migrations/migrate_network_immunity_period.rs b/pallets/subtensor/src/migrations/migrate_network_immunity_period.rs index a9fcea21e3..f7b9853367 100644 --- a/pallets/subtensor/src/migrations/migrate_network_immunity_period.rs +++ b/pallets/subtensor/src/migrations/migrate_network_immunity_period.rs @@ -1,6 +1,9 @@ use crate::{Config, Event, HasMigrationRun, NetworkImmunityPeriod, Pallet, Weight}; use scale_info::prelude::string::String; +/// Sets the global `NetworkImmunityPeriod` hyperparameter to the cutover value used at migration time. +/// +/// Idempotency key (frozen): `migrate_network_immunity_period`. pub fn migrate_network_immunity_period() -> Weight { use frame_support::traits::Get; diff --git a/pallets/subtensor/src/migrations/migrate_network_lock_cost_2500.rs b/pallets/subtensor/src/migrations/migrate_network_lock_cost_2500.rs index 3206a27fbb..8826252d3b 100644 --- a/pallets/subtensor/src/migrations/migrate_network_lock_cost_2500.rs +++ b/pallets/subtensor/src/migrations/migrate_network_lock_cost_2500.rs @@ -3,6 +3,10 @@ use frame_support::{traits::Get, weights::Weight}; use log; use scale_info::prelude::string::String; +/// Sets network last-lock so `get_network_lock_cost()` evaluates to 2_500 TAO at the current block +/// (writes last-lock = 1_250 TAO rao and last-lock block = now). +/// +/// Idempotency key (frozen): `migrate_network_lock_cost_2500`. pub fn migrate_network_lock_cost_2500() -> Weight { const RAO_PER_TAO: u64 = 1_000_000_000; const TARGET_COST_TAO: u64 = 2_500; diff --git a/pallets/subtensor/src/migrations/migrate_network_lock_reduction_interval.rs b/pallets/subtensor/src/migrations/migrate_network_lock_reduction_interval.rs index 9b67dfd583..a72e2c13fa 100644 --- a/pallets/subtensor/src/migrations/migrate_network_lock_reduction_interval.rs +++ b/pallets/subtensor/src/migrations/migrate_network_lock_reduction_interval.rs @@ -3,6 +3,9 @@ use frame_support::{traits::Get, weights::Weight}; use log; use scale_info::prelude::string::String; +/// Initializes network lock-cost reduction schedule fields (`NetworkLockReductionInterval`, related rate-limit / start-block values). +/// +/// Idempotency key (frozen): `migrate_network_lock_reduction_interval`. pub fn migrate_network_lock_reduction_interval() -> Weight { const FOUR_DAYS: u64 = 28_800; const EIGHT_DAYS: u64 = 57_600; diff --git a/pallets/subtensor/src/migrations/migrate_orphaned_storage_items.rs b/pallets/subtensor/src/migrations/migrate_orphaned_storage_items.rs index db00a4c440..780ecef43f 100644 --- a/pallets/subtensor/src/migrations/migrate_orphaned_storage_items.rs +++ b/pallets/subtensor/src/migrations/migrate_orphaned_storage_items.rs @@ -1,6 +1,9 @@ use super::*; use frame_support::weights::Weight; +/// Batch-clears several obsolete `SubtensorModule` storage prefixes left after dynamic/feature refactors. +/// +/// Each helper below has its own frozen [`HasMigrationRun`] key via [`migrate_storage`]. pub fn migrate_orphaned_storage_items() -> Weight { remove_last_hotkey_coldkey_emission_on_netuid::() .saturating_add(remove_subnet_alpha_emission_sell::()) @@ -11,6 +14,9 @@ pub fn migrate_orphaned_storage_items() -> Weight { .saturating_add(remove_dynamic_block::()) } +/// Clears orphaned `LastHotkeyColdkeyEmissionOnNetuid` storage. +/// +/// Idempotency key (frozen): `migrate_remove_last_hotkey_coldkey_emission_on_netuid`. pub(crate) fn remove_last_hotkey_coldkey_emission_on_netuid() -> Weight { let migration_name = "migrate_remove_last_hotkey_coldkey_emission_on_netuid"; let pallet_name = "SubtensorModule"; @@ -19,6 +25,9 @@ pub(crate) fn remove_last_hotkey_coldkey_emission_on_netuid() -> Weig migrate_storage::(migration_name, pallet_name, storage_name) } +/// Clears orphaned `SubnetAlphaEmissionSell` storage. +/// +/// Idempotency key (frozen): `migrate_remove_subnet_alpha_emission_sell`. pub(crate) fn remove_subnet_alpha_emission_sell() -> Weight { let migration_name = "migrate_remove_subnet_alpha_emission_sell"; let pallet_name = "SubtensorModule"; @@ -27,6 +36,9 @@ pub(crate) fn remove_subnet_alpha_emission_sell() -> Weight { migrate_storage::(migration_name, pallet_name, storage_name) } +/// Clears orphaned `NeuronsToPruneAtNextEpoch` storage. +/// +/// Idempotency key (frozen): `migrate_remove_neurons_to_prune_at_next_epoch`. pub(crate) fn remove_neurons_to_prune_at_next_epoch() -> Weight { let migration_name = "migrate_remove_neurons_to_prune_at_next_epoch"; let pallet_name = "SubtensorModule"; @@ -35,6 +47,9 @@ pub(crate) fn remove_neurons_to_prune_at_next_epoch() -> Weight { migrate_storage::(migration_name, pallet_name, storage_name) } +/// Clears orphaned `TotalStakeAtDynamic` storage. +/// +/// Idempotency key (frozen): `migrate_remove_total_stake_at_dynamic`. pub(crate) fn remove_total_stake_at_dynamic() -> Weight { let migration_name = "migrate_remove_total_stake_at_dynamic"; let pallet_name = "SubtensorModule"; @@ -43,6 +58,9 @@ pub(crate) fn remove_total_stake_at_dynamic() -> Weight { migrate_storage::(migration_name, pallet_name, storage_name) } +/// Clears orphaned `SubnetName` storage (names moved to identity / other maps). +/// +/// Idempotency key (frozen): `migrate_remove_subnet_name`. pub(crate) fn remove_subnet_name() -> Weight { let migration_name = "migrate_remove_subnet_name"; let pallet_name = "SubtensorModule"; @@ -51,6 +69,9 @@ pub(crate) fn remove_subnet_name() -> Weight { migrate_storage::(migration_name, pallet_name, storage_name) } +/// Clears orphaned `NetworkMinAllowedUids` storage. +/// +/// Idempotency key (frozen): `migrate_remove_network_min_allowed_uids`. pub(crate) fn remove_network_min_allowed_uids() -> Weight { let migration_name = "migrate_remove_network_min_allowed_uids"; let pallet_name = "SubtensorModule"; @@ -59,6 +80,9 @@ pub(crate) fn remove_network_min_allowed_uids() -> Weight { migrate_storage::(migration_name, pallet_name, storage_name) } +/// Clears orphaned `DynamicBlock` storage. +/// +/// Idempotency key (frozen): `migrate_remove_dynamic_block`. pub(crate) fn remove_dynamic_block() -> Weight { let migration_name = "migrate_remove_dynamic_block"; let pallet_name = "SubtensorModule"; diff --git a/pallets/subtensor/src/migrations/migrate_pending_emissions.rs b/pallets/subtensor/src/migrations/migrate_pending_emissions.rs index d93dcc6949..89e625872d 100644 --- a/pallets/subtensor/src/migrations/migrate_pending_emissions.rs +++ b/pallets/subtensor/src/migrations/migrate_pending_emissions.rs @@ -10,6 +10,10 @@ pub mod deprecated_pending_emission_format { StorageMap, Identity, NetUid, AlphaBalance, ValueQuery>; } +/// Splits legacy `PendingEmission` into `PendingServerEmission` / `PendingValidatorEmission` +/// (50/50 with root-alpha adjustment), then clears the old map. +/// +/// Idempotency key (frozen): `migrate_pending_emissions`. pub fn migrate_pending_emissions() -> Weight { let migration_name = b"migrate_pending_emissions".to_vec(); let mut weight: Weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_populate_locking_coldkeys.rs b/pallets/subtensor/src/migrations/migrate_populate_locking_coldkeys.rs index c1220c2077..63d4f94f71 100644 --- a/pallets/subtensor/src/migrations/migrate_populate_locking_coldkeys.rs +++ b/pallets/subtensor/src/migrations/migrate_populate_locking_coldkeys.rs @@ -5,6 +5,9 @@ use crate::{Config, HasMigrationRun, Lock, Pallet as Subtensor}; const MIGRATION_NAME: &[u8] = b"migrate_populate_locking_coldkeys"; +/// Backfills `LockingColdkeys` from existing conviction `Lock` rows so coldkeys can be looked up from lock state. +/// +/// Idempotency key (frozen): `migrate_populate_locking_coldkeys`. pub fn migrate_populate_locking_coldkeys() -> Weight { let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_populate_owned_hotkeys.rs b/pallets/subtensor/src/migrations/migrate_populate_owned_hotkeys.rs index 26d35dbd09..00883c23ca 100644 --- a/pallets/subtensor/src/migrations/migrate_populate_owned_hotkeys.rs +++ b/pallets/subtensor/src/migrations/migrate_populate_owned_hotkeys.rs @@ -19,7 +19,10 @@ pub mod deprecated_loaded_emission_format { StorageMap, Identity, u16, Vec<(AccountIdOf, u64)>, OptionQuery>; } -/// Migrate the OwnedHotkeys map to the new storage format +/// Rebuilds `OwnedHotkeys` from the `Owner` map when `OwnedHotkeys` is empty. +/// +/// Gated by emptiness of `OwnedHotkeys` (not [`HasMigrationRun`]); the log label +/// `Populate OwnedHotkeys map` is for logging only. pub fn migrate_populate_owned() -> Weight { // Setup migration weight let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_rao.rs b/pallets/subtensor/src/migrations/migrate_rao.rs index e4d181e813..273646c6ed 100644 --- a/pallets/subtensor/src/migrations/migrate_rao.rs +++ b/pallets/subtensor/src/migrations/migrate_rao.rs @@ -6,6 +6,10 @@ use subtensor_runtime_common::{AlphaBalance, NetUid}; use super::*; +/// Dynamic-TAO / RAO cutover migration: initializes subnet mechanism, pool, lock, and related RAO-era fields. +/// The stake-to-root rewrite body is intentionally commented (already applied on live chains) and kept for reference. +/// +/// Idempotency key (frozen): `migrate_rao`. pub fn migrate_rao() -> Weight { let migration_name = b"migrate_rao".to_vec(); diff --git a/pallets/subtensor/src/migrations/migrate_rate_limit_keys.rs b/pallets/subtensor/src/migrations/migrate_rate_limit_keys.rs index e6e331fb63..42d2833c84 100644 --- a/pallets/subtensor/src/migrations/migrate_rate_limit_keys.rs +++ b/pallets/subtensor/src/migrations/migrate_rate_limit_keys.rs @@ -24,6 +24,9 @@ enum RateLimitKeyV0 { LastTxBlockDelegateTake(AccountId), } +/// Rewrites `LastRateLimitedBlock` keys into the typed `RateLimitKey` encoding used by current rate-limit checks. +/// +/// Idempotency key (frozen): `migrate_rate_limit_keys`. pub fn migrate_rate_limit_keys() -> Weight where T::AccountId: Ord + Clone, diff --git a/pallets/subtensor/src/migrations/migrate_rate_limiting_last_blocks.rs b/pallets/subtensor/src/migrations/migrate_rate_limiting_last_blocks.rs index 99ce1e3077..0a68539d6b 100644 --- a/pallets/subtensor/src/migrations/migrate_rate_limiting_last_blocks.rs +++ b/pallets/subtensor/src/migrations/migrate_rate_limiting_last_blocks.rs @@ -7,6 +7,8 @@ use frame_support::weights::Weight; use sp_io::hashing::twox_128; use sp_io::storage::{clear, get}; +/// Entry point that migrates obsolete last-block rate-limit storage values into the current maps +/// (`NetworkLastRegistered`, `LastTxBlock`, childkey/delegate-take variants). pub fn migrate_obsolete_rate_limiting_last_blocks_storage() -> Weight { migrate_network_last_registered::() .saturating_add(migrate_last_tx_block::()) @@ -14,6 +16,9 @@ pub fn migrate_obsolete_rate_limiting_last_blocks_storage() -> Weight .saturating_add(migrate_last_tx_block_delegate_take::()) } +/// Migrates `NetworkLastRegistered` last-block values into the current rate-limit storage shape. +/// +/// Idempotency key (frozen): `migrate_network_last_registered`. pub fn migrate_network_last_registered() -> Weight { let migration_name = b"migrate_network_last_registered".to_vec(); let pallet_name = "SubtensorModule"; @@ -25,6 +30,9 @@ pub fn migrate_network_last_registered() -> Weight { } #[allow(deprecated)] +/// Migrates per-account `LastTxBlock` rate-limit timestamps into the current storage shape. +/// +/// Idempotency key (frozen): `migrate_last_tx_block`. pub fn migrate_last_tx_block() -> Weight { let migration_name = b"migrate_last_tx_block".to_vec(); @@ -38,6 +46,9 @@ pub fn migrate_last_tx_block() -> Weight { } #[allow(deprecated)] +/// Migrates `LastTxBlockChildkeyTake` rate-limit timestamps into the current storage shape. +/// +/// Idempotency key (frozen): `migrate_last_tx_block_childkey_take`. pub fn migrate_last_tx_block_childkey_take() -> Weight { let migration_name = b"migrate_last_tx_block_childkey_take".to_vec(); @@ -51,6 +62,9 @@ pub fn migrate_last_tx_block_childkey_take() -> Weight { } #[allow(deprecated)] +/// Migrates `LastTxBlockDelegateTake` rate-limit timestamps into the current storage shape. +/// +/// Idempotency key (frozen): `migrate_last_tx_block_delegate_take`. pub fn migrate_last_tx_block_delegate_take() -> Weight { let migration_name = b"migrate_last_tx_block_delegate_take".to_vec(); diff --git a/pallets/subtensor/src/migrations/migrate_remove_add_stake_burn_rate_limit.rs b/pallets/subtensor/src/migrations/migrate_remove_add_stake_burn_rate_limit.rs index dcbf307855..b096b18b4b 100644 --- a/pallets/subtensor/src/migrations/migrate_remove_add_stake_burn_rate_limit.rs +++ b/pallets/subtensor/src/migrations/migrate_remove_add_stake_burn_rate_limit.rs @@ -6,6 +6,9 @@ use crate::{Config, HasMigrationRun, LastRateLimitedBlock, RateLimitKey}; const MIGRATION_NAME: &[u8] = b"migrate_remove_add_stake_burn_rate_limit"; +/// Removes obsolete `LastRateLimitedBlock` entries keyed for the retired add-stake-burn rate limit. +/// +/// Idempotency key (frozen): `migrate_remove_add_stake_burn_rate_limit`. pub fn migrate_remove_add_stake_burn_rate_limit() -> Weight { let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_remove_commitments_rate_limit.rs b/pallets/subtensor/src/migrations/migrate_remove_commitments_rate_limit.rs index 06fcad0a10..080aeabfe0 100644 --- a/pallets/subtensor/src/migrations/migrate_remove_commitments_rate_limit.rs +++ b/pallets/subtensor/src/migrations/migrate_remove_commitments_rate_limit.rs @@ -4,6 +4,9 @@ use frame_support::{traits::Get, weights::Weight}; use scale_info::prelude::string::String; use sp_io::{KillStorageResult, hashing::twox_128, storage::clear_prefix}; +/// Clears the obsolete commitments `RateLimit` storage prefix under `SubtensorModule`. +/// +/// Idempotency key (frozen): `migrate_remove_commitments_rate_limit`. pub fn migrate_remove_commitments_rate_limit() -> Weight { let migration_name = b"migrate_remove_commitments_rate_limit".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_remove_deprecated_conviction_maps.rs b/pallets/subtensor/src/migrations/migrate_remove_deprecated_conviction_maps.rs index cb6ca56f9b..216d44678c 100644 --- a/pallets/subtensor/src/migrations/migrate_remove_deprecated_conviction_maps.rs +++ b/pallets/subtensor/src/migrations/migrate_remove_deprecated_conviction_maps.rs @@ -56,9 +56,9 @@ pub mod deprecated { pub type UnlockRate = StorageValue, u64, ValueQuery>; } -/// This migration removes the conviction v1 maps that were deprecated before they were -/// deployed on mainnet. They existed briefly on testnet and contain some values that need -/// to be cleaned before deploying conviction v2. +/// Removes conviction v1 maps that were deprecated before conviction v2 shipped (`HotkeyLock`/`Lock`/`MaturityRate`/`UnlockRate` cleanup). +/// +/// Idempotency key (frozen): `migrate_remove_deprecated_conviction_maps`. pub fn migrate_remove_deprecated_conviction_maps() -> Weight { let migration_name = b"migrate_remove_deprecated_conviction_maps".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_remove_network_modality.rs b/pallets/subtensor/src/migrations/migrate_remove_network_modality.rs index c39291d3b4..943a905db8 100644 --- a/pallets/subtensor/src/migrations/migrate_remove_network_modality.rs +++ b/pallets/subtensor/src/migrations/migrate_remove_network_modality.rs @@ -13,6 +13,9 @@ pub mod deprecated_network_modality_format { StorageMap, Identity, NetUid, u16, ValueQuery>; } +/// Deletes deprecated `NetworkModality` per-netuid entries (feature removed). +/// +/// Idempotency key (frozen): `migrate_remove_network_modality`. pub fn migrate_remove_network_modality() -> Weight { const MIG_NAME: &[u8] = b"migrate_remove_network_modality"; diff --git a/pallets/subtensor/src/migrations/migrate_remove_old_identity_maps.rs b/pallets/subtensor/src/migrations/migrate_remove_old_identity_maps.rs index 96dc4de2b4..c5b1fe3a75 100644 --- a/pallets/subtensor/src/migrations/migrate_remove_old_identity_maps.rs +++ b/pallets/subtensor/src/migrations/migrate_remove_old_identity_maps.rs @@ -3,6 +3,10 @@ use crate::HasMigrationRun; use frame_support::{traits::Get, weights::Weight}; use scale_info::prelude::string::String; +/// Clears superseded identity maps `Identities`, `SubnetIdentities`, and `SubnetIdentitiesV2` +/// after the V3 identity layout took over. +/// +/// Idempotency key (frozen): `migrate_remove_old_identity_maps`. pub fn migrate_remove_old_identity_maps() -> Weight { let migration_name = b"migrate_remove_old_identity_maps".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_remove_stake_map.rs b/pallets/subtensor/src/migrations/migrate_remove_stake_map.rs index bffe7f8fe5..a132b813b2 100644 --- a/pallets/subtensor/src/migrations/migrate_remove_stake_map.rs +++ b/pallets/subtensor/src/migrations/migrate_remove_stake_map.rs @@ -4,6 +4,9 @@ use frame_support::{traits::Get, weights::Weight}; use scale_info::prelude::string::String; use sp_io::{KillStorageResult, hashing::twox_128, storage::clear_prefix}; +/// Clears the legacy `Stake` double map after stake moved to alpha/share-based storage. +/// +/// Idempotency key (frozen): `migrate_remove_stake_map`. pub fn migrate_remove_stake_map() -> Weight { let migration_name = b"migrate_remove_stake_map".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_remove_tao_dividends.rs b/pallets/subtensor/src/migrations/migrate_remove_tao_dividends.rs index b93df22339..2e627326de 100644 --- a/pallets/subtensor/src/migrations/migrate_remove_tao_dividends.rs +++ b/pallets/subtensor/src/migrations/migrate_remove_tao_dividends.rs @@ -26,6 +26,9 @@ fn remove_prefix(old_map: &str) -> Weight { T::DbWeight::get().writes(removed_entries_count) } +/// Clears obsolete dividend maps `TaoDividendsPerSubnet`, `PendingAlphaSwapped`, and `PendingRootDivs`. +/// +/// Idempotency key (frozen): `migrate_remove_tao_dividends`. pub fn migrate_remove_tao_dividends() -> Weight { let migration_name = b"migrate_remove_tao_dividends".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_remove_total_hotkey_coldkey_stakes_this_interval.rs b/pallets/subtensor/src/migrations/migrate_remove_total_hotkey_coldkey_stakes_this_interval.rs index 9d6b2d681f..f433ad63d5 100644 --- a/pallets/subtensor/src/migrations/migrate_remove_total_hotkey_coldkey_stakes_this_interval.rs +++ b/pallets/subtensor/src/migrations/migrate_remove_total_hotkey_coldkey_stakes_this_interval.rs @@ -3,6 +3,10 @@ use crate::HasMigrationRun; use frame_support::{traits::Get, weights::Weight}; use sp_io::{KillStorageResult, hashing::twox_128, storage::clear_prefix}; +/// Clears obsolete `TotalHotkeyColdkeyStakesThisInterval` storage under `SubtensorModule`. +/// +/// Idempotency key (frozen): `migrate_remove_total_hotkey_coldkey_stakes_this_interval`. +/// Marks complete only when `clear_prefix` reports `AllRemoved`. pub fn migrate_remove_total_hotkey_coldkey_stakes_this_interval() -> Weight { let migration_name = "migrate_remove_total_hotkey_coldkey_stakes_this_interval"; let migration_name_bytes = migration_name.as_bytes().to_vec(); diff --git a/pallets/subtensor/src/migrations/migrate_remove_unknown_neuron_axon_cert_prom.rs b/pallets/subtensor/src/migrations/migrate_remove_unknown_neuron_axon_cert_prom.rs index 4553333e65..5d9f7ae7a1 100644 --- a/pallets/subtensor/src/migrations/migrate_remove_unknown_neuron_axon_cert_prom.rs +++ b/pallets/subtensor/src/migrations/migrate_remove_unknown_neuron_axon_cert_prom.rs @@ -4,6 +4,9 @@ use frame_support::{traits::Get, weights::Weight}; use scale_info::prelude::string::String; use sp_std::collections::btree_set::BTreeSet; +/// Drops `Axons`, `NeuronCertificates`, and `Prometheus` rows whose hotkey is not a current subnet member. +/// +/// Idempotency key (frozen): `migrate_remove_neuron_axon_cert_prom`. pub fn migrate_remove_unknown_neuron_axon_cert_prom() -> Weight { let migration_name = b"migrate_remove_neuron_axon_cert_prom".to_vec(); let mut weight: Weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_remove_unused_maps_and_values.rs b/pallets/subtensor/src/migrations/migrate_remove_unused_maps_and_values.rs index df439eafe3..cc4c38a025 100644 --- a/pallets/subtensor/src/migrations/migrate_remove_unused_maps_and_values.rs +++ b/pallets/subtensor/src/migrations/migrate_remove_unused_maps_and_values.rs @@ -4,6 +4,9 @@ use frame_support::{traits::Get, weights::Weight}; use scale_info::prelude::string::String; use sp_io::storage::clear; +/// Removes unused maps/values `EmissionValues`, `NetworkMaxStake`, and the old `SubnetLimit` value key. +/// +/// Idempotency key (frozen): `migrate_remove_unused_maps_and_values`. pub fn migrate_remove_unused_maps_and_values() -> Weight { let migration_name = b"migrate_remove_unused_maps_and_values".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_remove_zero_total_hotkey_alpha.rs b/pallets/subtensor/src/migrations/migrate_remove_zero_total_hotkey_alpha.rs index a9e7bd24b3..2c619effa1 100644 --- a/pallets/subtensor/src/migrations/migrate_remove_zero_total_hotkey_alpha.rs +++ b/pallets/subtensor/src/migrations/migrate_remove_zero_total_hotkey_alpha.rs @@ -3,6 +3,9 @@ use frame_support::{traits::Get, weights::Weight}; use log; use scale_info::prelude::string::String; +/// Prunes zero-valued `TotalHotkeyAlpha` entries that waste storage without affecting balances. +/// +/// Idempotency key (frozen): `migrate_remove_zero_total_hotkey_alpha`. pub fn migrate_remove_zero_total_hotkey_alpha() -> Weight { let migration_name = b"migrate_remove_zero_total_hotkey_alpha".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_reset_bonds_moving_average.rs b/pallets/subtensor/src/migrations/migrate_reset_bonds_moving_average.rs index 9825aed391..5344457885 100644 --- a/pallets/subtensor/src/migrations/migrate_reset_bonds_moving_average.rs +++ b/pallets/subtensor/src/migrations/migrate_reset_bonds_moving_average.rs @@ -3,6 +3,9 @@ use frame_support::{traits::Get, weights::Weight}; use log; use scale_info::prelude::string::String; +/// Caps each subnet's `BondsMovingAverage` at 975_000 when the stored value exceeds that ceiling. +/// +/// Idempotency key (frozen): `migrate_reset_bonds_moving_average`. pub fn migrate_reset_bonds_moving_average() -> Weight { let migration_name = b"migrate_reset_bonds_moving_average".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_reset_max_burn.rs b/pallets/subtensor/src/migrations/migrate_reset_max_burn.rs index 8016cd83ff..1ea2893d0c 100644 --- a/pallets/subtensor/src/migrations/migrate_reset_max_burn.rs +++ b/pallets/subtensor/src/migrations/migrate_reset_max_burn.rs @@ -3,6 +3,9 @@ use frame_support::{traits::Get, weights::Weight}; use log; use scale_info::prelude::string::String; +/// Resets every subnet's `MaxBurn` to 100 TAO (rao units). +/// +/// Idempotency key (frozen): `migrate_reset_max_burn`. pub fn migrate_reset_max_burn() -> Weight { let migration_name = b"migrate_reset_max_burn".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_reset_tnet_conviction_locks.rs b/pallets/subtensor/src/migrations/migrate_reset_tnet_conviction_locks.rs index 277af2037e..2516c00d4a 100644 --- a/pallets/subtensor/src/migrations/migrate_reset_tnet_conviction_locks.rs +++ b/pallets/subtensor/src/migrations/migrate_reset_tnet_conviction_locks.rs @@ -2,13 +2,10 @@ use super::*; use frame_support::weights::Weight; use scale_info::prelude::string::String; -/// Clears conviction v2 lock state that only exists on testnet before this -/// conviction design is deployed more broadly. +/// Clears conviction v2 lock state that only exists on testnet before a conviction feature cutover +/// (`HotkeyLock`, `OwnerLock`, `Lock`, and decaying variants). /// -/// `devnet-ready` had `Lock`, `HotkeyLock`, `DecayingHotkeyLock`, `OwnerLock`, -/// and `DecayingLock`, but did not have `DecayingOwnerLock`. `OwnerLock` also -/// used the old owner-coldkey aggregate semantics. Clear these prefixes without -/// decoding values so old or incompatible aggregate bytes are removed safely. +/// Idempotency key (frozen): `migrate_reset_tnet_conviction_locks`. pub fn migrate_reset_tnet_conviction_locks() -> Weight { let migration_name = b"migrate_reset_tnet_conviction_locks".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_reset_unactive_sn.rs b/pallets/subtensor/src/migrations/migrate_reset_unactive_sn.rs index 8c24cb0100..afc2cc9ab0 100644 --- a/pallets/subtensor/src/migrations/migrate_reset_unactive_sn.rs +++ b/pallets/subtensor/src/migrations/migrate_reset_unactive_sn.rs @@ -23,6 +23,10 @@ pub fn get_unactive_sn_netuids( (unactive_netuids, weight) } +/// Clears pending emission counters for subnets that have alpha issuance but never received a first emission block +/// (`PendingServerEmission`, `PendingValidatorEmission`, root/owner pending cuts, and in/out emission tallies). +/// +/// Idempotency key (frozen): `migrate_reset_unactive_sn`. pub fn migrate_reset_unactive_sn() -> Weight { let migration_name = b"migrate_reset_unactive_sn".to_vec(); let mut weight: Weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_set_first_emission_block_number.rs b/pallets/subtensor/src/migrations/migrate_set_first_emission_block_number.rs index 2da7cef51a..36a4537844 100644 --- a/pallets/subtensor/src/migrations/migrate_set_first_emission_block_number.rs +++ b/pallets/subtensor/src/migrations/migrate_set_first_emission_block_number.rs @@ -3,6 +3,9 @@ use crate::HasMigrationRun; use frame_support::{traits::Get, weights::Weight}; use scale_info::prelude::string::String; +/// Writes `FirstEmissionBlockNumber` to the current block for every non-root subnet missing that marker. +/// +/// Idempotency key (frozen): `migrate_set_first_emission_block_number`. pub fn migrate_set_first_emission_block_number() -> Weight { let migration_name = b"migrate_set_first_emission_block_number".to_vec(); diff --git a/pallets/subtensor/src/migrations/migrate_set_min_burn.rs b/pallets/subtensor/src/migrations/migrate_set_min_burn.rs index 80518eb188..4f7e440bcf 100644 --- a/pallets/subtensor/src/migrations/migrate_set_min_burn.rs +++ b/pallets/subtensor/src/migrations/migrate_set_min_burn.rs @@ -6,6 +6,9 @@ use subtensor_runtime_common::NetUid; use super::*; +/// Sets each subnet's min-burn hyperparameter to the then-current initial min-burn default. +/// +/// Idempotency key (frozen): `migrate_set_min_burn_1`. pub fn migrate_set_min_burn() -> Weight { let migration_name = b"migrate_set_min_burn_1".to_vec(); diff --git a/pallets/subtensor/src/migrations/migrate_set_min_difficulty.rs b/pallets/subtensor/src/migrations/migrate_set_min_difficulty.rs index 80a7d5faad..428d4ef424 100644 --- a/pallets/subtensor/src/migrations/migrate_set_min_difficulty.rs +++ b/pallets/subtensor/src/migrations/migrate_set_min_difficulty.rs @@ -6,6 +6,9 @@ use subtensor_runtime_common::NetUid; use super::*; +/// Sets min PoW registration difficulty to 10_000_000 for all subnets. +/// +/// Idempotency key (frozen): `migrate_set_min_difficulty`. pub fn migrate_set_min_difficulty() -> Weight { let migration_name = b"migrate_set_min_difficulty".to_vec(); diff --git a/pallets/subtensor/src/migrations/migrate_set_nominator_min_stake.rs b/pallets/subtensor/src/migrations/migrate_set_nominator_min_stake.rs index d06d38bc7c..30df3dcdd5 100644 --- a/pallets/subtensor/src/migrations/migrate_set_nominator_min_stake.rs +++ b/pallets/subtensor/src/migrations/migrate_set_nominator_min_stake.rs @@ -2,6 +2,9 @@ use super::*; use alloc::string::String; use frame_support::{traits::Get, weights::Weight}; +/// Sets nominator min required stake to `10_000_000` rao via `set_nominator_min_required_stake`. +/// +/// Idempotency key (frozen): `migrate_set_nominator_min_stake`. pub fn migrate_set_nominator_min_stake() -> Weight { let migration_name = b"migrate_set_nominator_min_stake".to_vec(); diff --git a/pallets/subtensor/src/migrations/migrate_set_registration_enable.rs b/pallets/subtensor/src/migrations/migrate_set_registration_enable.rs index adb8209662..8491855902 100644 --- a/pallets/subtensor/src/migrations/migrate_set_registration_enable.rs +++ b/pallets/subtensor/src/migrations/migrate_set_registration_enable.rs @@ -5,6 +5,9 @@ use frame_support::{traits::Get, weights::Weight}; use super::*; +/// Enables network registration on every non-root subnet where it is currently disabled. +/// +/// Idempotency key (frozen): `migrate_set_registration_enable`. pub fn migrate_set_registration_enable() -> Weight { let migration_name = b"migrate_set_registration_enable".to_vec(); diff --git a/pallets/subtensor/src/migrations/migrate_set_subtoken_enabled.rs b/pallets/subtensor/src/migrations/migrate_set_subtoken_enabled.rs index 3b0d236f99..3a749fbff1 100644 --- a/pallets/subtensor/src/migrations/migrate_set_subtoken_enabled.rs +++ b/pallets/subtensor/src/migrations/migrate_set_subtoken_enabled.rs @@ -3,6 +3,9 @@ use crate::HasMigrationRun; use frame_support::{traits::Get, weights::Weight}; use scale_info::prelude::string::String; +/// Sets `SubtokenEnabled`: root always `true`; other subnets `true` iff `FirstEmissionBlockNumber` is set. +/// +/// Idempotency key (frozen): `migrate_set_subtoken_enabled`. pub fn migrate_set_subtoken_enabled() -> Weight { let migration_name = b"migrate_set_subtoken_enabled".to_vec(); diff --git a/pallets/subtensor/src/migrations/migrate_stake_threshold.rs b/pallets/subtensor/src/migrations/migrate_stake_threshold.rs index 0a7c578170..4c857b1495 100644 --- a/pallets/subtensor/src/migrations/migrate_stake_threshold.rs +++ b/pallets/subtensor/src/migrations/migrate_stake_threshold.rs @@ -13,6 +13,9 @@ pub mod deprecated_weights_min_stake { pub(super) type WeightsMinStake = StorageValue, u64, ValueQuery>; } +/// Copies deprecated `WeightsMinStake` into `StakeThreshold`, then kills the old storage value. +/// +/// Idempotency key (frozen): `migrate_stake_threshold`. pub fn migrate_stake_threshold() -> Weight { let migration_name = b"migrate_stake_threshold".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_subnet_balances.rs b/pallets/subtensor/src/migrations/migrate_subnet_balances.rs index b1c5b04202..5cc09c2080 100644 --- a/pallets/subtensor/src/migrations/migrate_subnet_balances.rs +++ b/pallets/subtensor/src/migrations/migrate_subnet_balances.rs @@ -4,14 +4,10 @@ use frame_support::{ weights::Weight, }; -/// Performs migration to mint SubnetTAO and subnet locked funds into subnet accounts. -/// -/// # Arguments -/// -/// # Returns -/// -/// * `Weight` - The computational weight of this operation. +/// Mints `SubnetTAO` and subnet-locked funds into each subnet account so on-chain balances match +/// reserve accounting, then adjusts `TotalIssuance` accordingly. /// +/// Idempotency key (frozen): `migrate_subnet_balances`. pub fn migrate_subnet_balances() -> Weight { let migration_name = b"migrate_subnet_balances".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_subnet_limit_to_default.rs b/pallets/subtensor/src/migrations/migrate_subnet_limit_to_default.rs index 3d88337a24..7c44514627 100644 --- a/pallets/subtensor/src/migrations/migrate_subnet_limit_to_default.rs +++ b/pallets/subtensor/src/migrations/migrate_subnet_limit_to_default.rs @@ -3,6 +3,9 @@ use frame_support::{traits::Get, weights::Weight}; use log; use scale_info::prelude::string::String; +/// Resets global `SubnetLimit` to the pallet default value. +/// +/// Idempotency key (frozen): `subnet_limit_to_default`. pub fn migrate_subnet_limit_to_default() -> Weight { let mig_name: Vec = b"subnet_limit_to_default".to_vec(); diff --git a/pallets/subtensor/src/migrations/migrate_subnet_locked.rs b/pallets/subtensor/src/migrations/migrate_subnet_locked.rs index 73bb183e00..ca74fe6a47 100644 --- a/pallets/subtensor/src/migrations/migrate_subnet_locked.rs +++ b/pallets/subtensor/src/migrations/migrate_subnet_locked.rs @@ -5,6 +5,9 @@ use log; use scale_info::prelude::string::String; use subtensor_runtime_common::NetUid; +/// Restores `SubnetLocked` amounts for subnets after a lock-accounting regression. +/// +/// Idempotency key (frozen): `migrate_restore_subnet_locked`. pub fn migrate_restore_subnet_locked() -> Weight { // Track whether we've already run this migration let migration_name = b"migrate_restore_subnet_locked".to_vec(); diff --git a/pallets/subtensor/src/migrations/migrate_subnet_symbols.rs b/pallets/subtensor/src/migrations/migrate_subnet_symbols.rs index f4933dc5f2..ff478203bc 100644 --- a/pallets/subtensor/src/migrations/migrate_subnet_symbols.rs +++ b/pallets/subtensor/src/migrations/migrate_subnet_symbols.rs @@ -3,8 +3,9 @@ use alloc::string::String; use frame_support::IterableStorageMap; use frame_support::{traits::Get, weights::Weight}; -/// Migrates the subnet symbols to their correct values because some shift is present -/// after subnet 81. +/// Rewrites `TokenSymbol` for each subnet to the canonical symbol table after a historical shift bug. +/// +/// Idempotency key (frozen): `migrate_subnet_symbols`. pub fn migrate_subnet_symbols() -> Weight { let migration_name = b"migrate_subnet_symbols".to_vec(); diff --git a/pallets/subtensor/src/migrations/migrate_subnet_volume.rs b/pallets/subtensor/src/migrations/migrate_subnet_volume.rs index eb763e68e0..ee780a50f9 100644 --- a/pallets/subtensor/src/migrations/migrate_subnet_volume.rs +++ b/pallets/subtensor/src/migrations/migrate_subnet_volume.rs @@ -2,6 +2,9 @@ use super::*; use alloc::string::String; use frame_support::{traits::Get, weights::Weight}; +/// Widens every `SubnetVolume` value from `u64` to `u128` in place via `translate`. +/// +/// Idempotency key (frozen): `migrate_subnet_volume`. pub fn migrate_subnet_volume() -> Weight { let migration_name = b"migrate_subnet_volume".to_vec(); diff --git a/pallets/subtensor/src/migrations/migrate_tao_in_refund_deployment_block.rs b/pallets/subtensor/src/migrations/migrate_tao_in_refund_deployment_block.rs index 984060590e..c434486e06 100644 --- a/pallets/subtensor/src/migrations/migrate_tao_in_refund_deployment_block.rs +++ b/pallets/subtensor/src/migrations/migrate_tao_in_refund_deployment_block.rs @@ -4,7 +4,9 @@ use frame_support::{traits::Get, weights::Weight}; use scale_info::prelude::string::String; use sp_runtime::traits::SaturatedConversion; -/// Captures the runtime-upgrade block used as the TAO-in refund behavior cutover. +/// Records the runtime-upgrade block used as the TAO-in refund behavior cutover in `TaoInRefundDeploymentBlock`. +/// +/// Idempotency key (frozen): `migrate_tao_in_refund_deployment_block`. pub fn migrate_tao_in_refund_deployment_block() -> Weight { let migration_name = b"migrate_tao_in_refund_deployment_block".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/migrate_to_v1_separate_emission.rs b/pallets/subtensor/src/migrations/migrate_to_v1_separate_emission.rs index f6816b291d..a657a673cb 100644 --- a/pallets/subtensor/src/migrations/migrate_to_v1_separate_emission.rs +++ b/pallets/subtensor/src/migrations/migrate_to_v1_separate_emission.rs @@ -23,21 +23,9 @@ pub mod deprecated_loaded_emission_format { StorageMap, Identity, u16, Vec<(AccountIdOf, u64)>, OptionQuery>; } -/// Migrates the LoadedEmission storage to a new format +/// Migrates legacy `LoadedEmission` into the separated emission storage format (storage version bump era). /// -/// # Arguments -/// -/// * `T` - The runtime configuration trait -/// -/// # Returns -/// -/// * `Weight` - The computational weight of this operation -/// -/// # Example -/// -/// ```ignore -/// let weight = migrate_to_v1_separate_emission::(); -/// ``` +/// Does not use [`HasMigrationRun`]; gated by pallet storage version in the body. pub fn migrate_to_v1_separate_emission() -> Weight { use deprecated_loaded_emission_format as old; diff --git a/pallets/subtensor/src/migrations/migrate_to_v2_fixed_total_stake.rs b/pallets/subtensor/src/migrations/migrate_to_v2_fixed_total_stake.rs index c8ea6a33af..3ed1dce1a6 100644 --- a/pallets/subtensor/src/migrations/migrate_to_v2_fixed_total_stake.rs +++ b/pallets/subtensor/src/migrations/migrate_to_v2_fixed_total_stake.rs @@ -19,26 +19,9 @@ pub mod deprecated_loaded_emission_format { StorageMap, Identity, u16, Vec<(AccountIdOf, u64)>, OptionQuery>; } -/// Migrates the storage to fix TotalStake and TotalColdkeyStake +/// Resets and recalculates `TotalStake` and `TotalColdkeyStake` from the (then-current) `Stake` map. /// -/// This function performs the following steps: -/// 1. Resets TotalStake to 0 -/// 2. Resets all TotalColdkeyStake entries to 0 -/// 3. Recalculates TotalStake and TotalColdkeyStake based on the Stake map -/// -/// # Arguments -/// -/// * `T` - The Config trait of the pallet -/// -/// # Returns -/// -/// * `Weight` - The computational weight of this operation -/// -/// # Example -/// -/// ```ignore -/// let weight = migrate_to_v2_fixed_total_stake::(); -/// ``` +/// Does not use [`HasMigrationRun`]; intended as a storage-version migration. pub fn migrate_to_v2_fixed_total_stake() -> Weight { let new_storage_version = 2; diff --git a/pallets/subtensor/src/migrations/migrate_total_issuance.rs b/pallets/subtensor/src/migrations/migrate_total_issuance.rs index ba11ce363c..bb0e8a689f 100644 --- a/pallets/subtensor/src/migrations/migrate_total_issuance.rs +++ b/pallets/subtensor/src/migrations/migrate_total_issuance.rs @@ -22,9 +22,9 @@ pub mod deprecated_loaded_emission_format { /// Note: This migration is now disabled. We needed it to sync up two different total issuance counters: /// 1. Balances pallet /// 2. Subtensor pallet -/// Now that two total issuances are naturally synched, it is not needed anymore, and it will lead to an +/// Now that two total issuances are naturally synched, it is not needed anymore, and it will lead to an /// incorrect state if it runs. -/// +/// /// Performs migration to update the total issuance based on the sum of stakes and total balances. /// /// This migration is applicable only if the current storage version is 5, after which it updates the storage version to 6. diff --git a/pallets/subtensor/src/migrations/migrate_transfer_ownership_to_foundation.rs b/pallets/subtensor/src/migrations/migrate_transfer_ownership_to_foundation.rs index 11711a9184..43a0dbaf80 100644 --- a/pallets/subtensor/src/migrations/migrate_transfer_ownership_to_foundation.rs +++ b/pallets/subtensor/src/migrations/migrate_transfer_ownership_to_foundation.rs @@ -22,22 +22,9 @@ pub mod deprecated_loaded_emission_format { StorageMap, Identity, u16, Vec<(AccountIdOf, u64)>, OptionQuery>; } -/// Migrates subnet ownership to the foundation and updates related storage +/// Transfers subnet ownership (`SubnetOwner`) to the foundation coldkey and updates related registration timestamps. /// -/// # Arguments -/// -/// * `coldkey` - 32-byte array representing the foundation's coldkey -/// -/// # Returns -/// -/// * `Weight` - The computational weight of this operation -/// -/// # Example -/// -/// ```ignore -/// let foundation_coldkey = [0u8; 32]; // Replace with actual foundation coldkey -/// let weight = migrate_transfer_ownership_to_foundation::(foundation_coldkey); -/// ``` +/// Does not use [`HasMigrationRun`]; caller supplies the 32-byte foundation coldkey. pub fn migrate_transfer_ownership_to_foundation(coldkey: [u8; 32]) -> Weight { let new_storage_version = 3; diff --git a/pallets/subtensor/src/migrations/migrate_upgrade_revealed_commitments.rs b/pallets/subtensor/src/migrations/migrate_upgrade_revealed_commitments.rs index 6c10bf24e3..89396ee8c6 100644 --- a/pallets/subtensor/src/migrations/migrate_upgrade_revealed_commitments.rs +++ b/pallets/subtensor/src/migrations/migrate_upgrade_revealed_commitments.rs @@ -4,6 +4,9 @@ use frame_support::{traits::Get, weights::Weight}; use scale_info::prelude::string::String; use sp_io::{KillStorageResult, hashing::twox_128, storage::clear_prefix}; +/// Clears obsolete `RevealedCommitments` storage under the `Commitments` pallet prefix. +/// +/// Idempotency key (frozen): `migrate_revealed_commitments_v2`. pub fn migrate_upgrade_revealed_commitments() -> Weight { let migration_name = b"migrate_revealed_commitments_v2".to_vec(); let mut weight = T::DbWeight::get().reads(1); diff --git a/pallets/subtensor/src/migrations/mod.rs b/pallets/subtensor/src/migrations/mod.rs index 63a7ec4439..6840b9931e 100644 --- a/pallets/subtensor/src/migrations/mod.rs +++ b/pallets/subtensor/src/migrations/mod.rs @@ -1,3 +1,13 @@ +//! Storage migrations for `SubtensorModule`. +//! +//! Each `migrate_*` module is typically a one-shot runtime upgrade step. Prefer searching the frozen +//! [`HasMigrationRun`] key string (e.g. `rg 'migrate_remove_stake_map'`) — those byte strings must never be renamed +//! once applied on a live chain, or the migration would re-run. +//! +//! Helpers in this file: +//! - [`migrate_storage`]: clear a pallet storage prefix and mark a migration name complete +//! - [`remove_prefix`]: clear a prefix without touching [`HasMigrationRun`] + use super::*; use alloc::string::String; use frame_support::pallet_prelude::Weight; @@ -76,6 +86,11 @@ pub mod migrate_to_v2_fixed_total_stake; pub mod migrate_transfer_ownership_to_foundation; pub mod migrate_upgrade_revealed_commitments; +/// Clears an entire storage prefix under a pallet and records completion in [`HasMigrationRun`]. +/// +/// `migration_name` is the frozen idempotency key (must never be renamed once applied on a live chain). +/// `pallet_name` / `storage_name` are Twox128-hashed to form the clear_prefix key. +/// If `clear_prefix` leaves entries (`SomeRemaining`), the migration is **not** marked complete so it can retry. pub(crate) fn migrate_storage( migration_name: &'static str, pallet_name: &'static str, @@ -124,6 +139,9 @@ pub(crate) fn migrate_storage( weight } +/// Clears all keys under `module`/`old_map` (Twox128 prefix) and accrues write weight for removed entries. +/// +/// Unlike [`migrate_storage`], this helper does **not** consult or update [`HasMigrationRun`]; callers own idempotency. pub(crate) fn remove_prefix(module: &str, old_map: &str, weight: &mut Weight) { let mut prefix = Vec::new(); prefix.extend_from_slice(&twox_128(module.as_bytes())); diff --git a/pallets/subtensor/src/rpc_info/delegate_info.rs b/pallets/subtensor/src/rpc_info/delegate_info.rs index 0efb5e8d62..c3bb3d1939 100644 --- a/pallets/subtensor/src/rpc_info/delegate_info.rs +++ b/pallets/subtensor/src/rpc_info/delegate_info.rs @@ -1,3 +1,5 @@ +//! Delegate (validator hotkey) RPC views: take, nominators, registrations, returns. + use super::*; use frame_support::IterableStorageMap; use frame_support::pallet_prelude::{Decode, Encode}; @@ -9,21 +11,36 @@ use codec::Compact; use sp_runtime::PerU16; use subtensor_runtime_common::{AlphaBalance, NetUid}; -#[freeze_struct("bfb7d342e9ede512")] +/// RPC view of a delegate hotkey: take, nominators, registrations, and estimated returns. +/// +/// `return_per_1000` / `total_daily_return` are estimates from current emission × epochs/day, +/// not settled payouts. Amounts are in rao (1e9 rao = 1 TAO) unless noted. +#[freeze_struct("4545e1c058fbd2fe")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] pub struct DelegateInfo { + /// Delegate hotkey. pub delegate_ss58: AccountId, + /// Validator take as [`PerU16`]. pub take: Compact, - pub nominators: Vec<(AccountId, Vec<(Compact, Compact)>)>, // map of nominator_ss58 to netuid and stake amount + /// Nominators: coldkey → list of `(netuid, alpha stake)` pairs. + pub nominators: Vec<(AccountId, Vec<(Compact, Compact)>)>, + /// Coldkey that owns this hotkey. pub owner_ss58: AccountId, - pub registrations: Vec>, // Vec of netuid this delegate is registered on - pub validator_permits: Vec>, // Vec of netuid this delegate has validator permit on - pub return_per_1000: Compact, // Delegators current daily return per 1000 TAO staked minus take fee - pub total_daily_return: Compact, // Delegators current daily return + /// Subnets where this hotkey is registered. + pub registrations: Vec>, + /// Subnets where this hotkey currently holds a validator permit. + pub validator_permits: Vec>, + /// Estimated daily return per 1000 TAO staked, after take (rao). + pub return_per_1000: Compact, + /// Estimated total daily emission attributed to this delegate (rao, before take split). + pub total_daily_return: Compact, } impl Pallet { - fn return_per_1000_tao( + /// Estimated daily return (rao) per 1000 TAO of root stake after deducting `take`. + /// + /// Uses `emissions_per_day` as the pre-take emission total. Returns 0 when `total_stake` is 0. + fn delegator_return_per_1000_tao( take: Compact, total_stake: U64F64, emissions_per_day: U64F64, @@ -44,15 +61,19 @@ impl Pallet { } #[cfg(test)] - pub fn return_per_1000_tao_test( + pub fn delegator_return_per_1000_tao_test( take: Compact, total_stake: U64F64, emissions_per_day: U64F64, ) -> U64F64 { - Self::return_per_1000_tao(take, total_stake, emissions_per_day) + Self::delegator_return_per_1000_tao(take, total_stake, emissions_per_day) } - fn get_delegate_by_existing_account( + /// Build [`DelegateInfo`] for a hotkey already present in [`Delegates`]. + /// + /// When `skip_nominators` is true, `nominators` is left empty (used by + /// [`Self::get_delegated`] to avoid rebuilding the full nominator map per row). + fn build_delegate_info_for_hotkey( delegate: AccountIdOf, skip_nominators: bool, ) -> DelegateInfo { @@ -118,7 +139,7 @@ impl Pallet { .into(); let return_per_1000: U64F64 = - Self::return_per_1000_tao(take, total_stake, emissions_per_day); + Self::delegator_return_per_1000_tao(take, total_stake, emissions_per_day); DelegateInfo { delegate_ss58: delegate.clone(), @@ -132,30 +153,31 @@ impl Pallet { } } + /// [`DelegateInfo`] for one hotkey, or `None` if it is not in [`Delegates`]. pub fn get_delegate(delegate: T::AccountId) -> Option> { // Check delegate exists if !>::contains_key(delegate.clone()) { return None; } - let delegate_info = Self::get_delegate_by_existing_account(delegate.clone(), false); + let delegate_info = Self::build_delegate_info_for_hotkey(delegate.clone(), false); Some(delegate_info) } - /// get all delegates info from storage - /// + /// All delegates currently in [`Delegates`], each with a full nominator list. pub fn get_delegates() -> Vec> { let mut delegates = Vec::>::new(); for delegate in as IterableStorageMap>::iter_keys() { - let delegate_info = Self::get_delegate_by_existing_account(delegate.clone(), false); + let delegate_info = Self::build_delegate_info_for_hotkey(delegate.clone(), false); delegates.push(delegate_info); } delegates } - /// get all delegate info and staked token amount for a given delegatee account + /// Delegates that `delegatee` (coldkey) has stake on, with `(netuid, alpha)` per position. /// + /// Nominator lists inside each [`DelegateInfo`] are omitted (`skip_nominators`). pub fn get_delegated( delegatee: T::AccountId, ) -> Vec<( @@ -169,7 +191,7 @@ impl Pallet { for delegate in as IterableStorageMap>::iter_keys() { // Staked to this delegate, so add to list for (netuid, _) in Self::alpha_iter_prefix((&delegate, &delegatee)) { - let delegate_info = Self::get_delegate_by_existing_account(delegate.clone(), true); + let delegate_info = Self::build_delegate_info_for_hotkey(delegate.clone(), true); delegates.push(( delegate_info, ( @@ -186,12 +208,13 @@ impl Pallet { delegates } - // Helper function to get the coldkey associated with a hotkey + /// Owning coldkey for `hotkey` via [`Owner`] (default account if unset). pub fn get_coldkey_for_hotkey(hotkey: &T::AccountId) -> T::AccountId { Owner::::get(hotkey) } - pub fn maybe_coldkey_for_hotkey(hotkey: &T::AccountId) -> Option { + /// Owning coldkey for `hotkey` when [`Owner`] has an entry. + pub fn owning_coldkey_for_hotkey_if_set(hotkey: &T::AccountId) -> Option { Owner::::try_get(hotkey).ok() } } diff --git a/pallets/subtensor/src/rpc_info/dynamic_info.rs b/pallets/subtensor/src/rpc_info/dynamic_info.rs index b143295425..4ca6cacd64 100644 --- a/pallets/subtensor/src/rpc_info/dynamic_info.rs +++ b/pallets/subtensor/src/rpc_info/dynamic_info.rs @@ -1,3 +1,5 @@ +//! Per-subnet dynamic pool / emission RPC view (`DynamicInfo`). + use super::*; extern crate alloc; use codec::Compact; @@ -6,7 +8,11 @@ use substrate_fixed::types::I96F32; use subtensor_macros::freeze_struct; use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance}; -#[freeze_struct("cf677afa654c96a6")] +/// Live subnet pool and emission snapshot for dynamic (non-root) subnets. +/// +/// Built for RPC; `emission` is currently always zero (legacy field). Pool balances +/// come from `SubnetAlphaIn` / `SubnetAlphaOut` / `SubnetTAO` and related emission maps. +#[freeze_struct("82f520639ff75c3d")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] pub struct DynamicInfo { netuid: Compact, @@ -17,6 +23,7 @@ pub struct DynamicInfo { tempo: Compact, last_step: Compact, blocks_since_last_step: Compact, + /// Legacy; always encoded as 0. emission: Compact, alpha_in: Compact, alpha_out: Compact, @@ -25,6 +32,7 @@ pub struct DynamicInfo { alpha_in_emission: Compact, tao_in_emission: Compact, pending_alpha_emission: Compact, + /// Root TAO dividends removed; always zero. pending_root_emission: Compact, subnet_volume: Compact, network_registered_at: Compact, @@ -33,8 +41,9 @@ pub struct DynamicInfo { } impl Pallet { + /// [`DynamicInfo`] for `netuid`, or `None` if the subnet does not exist. pub fn get_dynamic_info(netuid: NetUid) -> Option> { - if !Self::if_subnet_exist(netuid) { + if !Self::subnet_exists(netuid) { return None; } let last_step: u64 = LastMechansimStepBlock::::get(netuid); @@ -72,6 +81,8 @@ impl Pallet { moving_price: SubnetMovingPrice::::get(netuid), }) } + + /// [`DynamicInfo`] for every subnet netuid, in [`Self::get_all_subnet_netuids`] order. pub fn get_all_dynamic_info() -> Vec>> { let netuids = Self::get_all_subnet_netuids(); let mut dynamic_info = Vec::>>::new(); diff --git a/pallets/subtensor/src/rpc_info/metagraph.rs b/pallets/subtensor/src/rpc_info/metagraph.rs index a627a4eff3..9d07f72a10 100644 --- a/pallets/subtensor/src/rpc_info/metagraph.rs +++ b/pallets/subtensor/src/rpc_info/metagraph.rs @@ -1,3 +1,8 @@ +//! Full and selective metagraph RPC views (`Metagraph`, `SelectiveMetagraph`). +//! +//! Field indices for selective queries are defined by [`SelectiveMetagraphIndex`] and must +//! stay aligned with client index constants (append-only). + use super::*; extern crate alloc; use crate::epoch::math::*; @@ -11,7 +16,11 @@ use substrate_fixed::types::I96F32; use subtensor_macros::freeze_struct; use subtensor_runtime_common::{AlphaBalance, MechId, NetUid, NetUidStorageIndex, TaoBalance}; -#[freeze_struct("93460b9b3cbf6d4e")] +/// Full metagraph RPC DTO: subnet hyperparams plus per-uid consensus/stake/collateral vectors. +/// +/// `subnet_emission` is deprecated (always 0). `pruning_score`, `trust`, and `rank` are empty. +/// `tao_dividends_per_hotkey` values are always zero (root TAO dividends removed). +#[freeze_struct("98bcc63bc4564059")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] pub struct Metagraph { // Subnet index @@ -34,17 +43,17 @@ pub struct Metagraph { blocks_since_last_step: Compact, // blocks since last epoch. // Subnet emission terms - subnet_emission: Compact, // subnet emission via stao - alpha_in: Compact, // amount of alpha in reserve - alpha_out: Compact, // amount of alpha outstanding - tao_in: Compact, // amount of tao injected per block + subnet_emission: Compact, // deprecated; always 0 + alpha_in: Compact, // amount of alpha in reserve + alpha_out: Compact, // amount of alpha outstanding + tao_in: Compact, // amount of tao in the pool alpha_out_emission: Compact, // amount injected in alpha reserves per block - alpha_in_emission: Compact, // amount injected outstanding per block - tao_in_emission: Compact, // amount of tao injected per block + alpha_in_emission: Compact, // amount injected outstanding per block + tao_in_emission: Compact, // amount of tao injected per block pending_alpha_emission: Compact, // pending alpha to be distributed - pending_root_emission: Compact, // pending tao for root divs to be distributed - subnet_volume: Compact, // volume of the subnet in TAO - moving_price: I96F32, // subnet moving price. + pending_root_emission: Compact, // root TAO dividends removed; always 0 + subnet_volume: Compact, // volume of the subnet in TAO + moving_price: I96F32, // subnet moving price. // Hparams for epoch rho: Compact, // subnet rho param @@ -91,24 +100,24 @@ pub struct Metagraph { coldkeys: Vec, // coldkey per UID identities: Vec>, // coldkeys identities axons: Vec, // UID axons - active: Vec, // Avtive per UID + active: Vec, // active per UID validator_permit: Vec, // Val permit per UID - pruning_score: Vec>, // Pruning per UID + pruning_score: Vec>, // deprecated; empty last_update: Vec>, // Last update per UID emission: Vec>, // Emission per UID dividends: Vec>, // Dividends per UID incentives: Vec>, // Mining incentives per UID consensus: Vec>, // Consensus per UID - trust: Vec>, // Trust per UID - rank: Vec>, // Rank per UID + trust: Vec>, // deprecated; empty + rank: Vec>, // deprecated; empty block_at_registration: Vec>, // Reg block per UID alpha_stake: Vec>, // Alpha staked per UID tao_stake: Vec>, // TAO staked per UID total_stake: Vec>, // Total stake per UID // Dividend break down. - tao_dividends_per_hotkey: Vec<(AccountId, Compact)>, // List of dividend payouts in tao via root. - alpha_dividends_per_hotkey: Vec<(AccountId, Compact)>, // List of dividend payout in alpha via subnet. + tao_dividends_per_hotkey: Vec<(AccountId, Compact)>, // always zero amounts + alpha_dividends_per_hotkey: Vec<(AccountId, Compact)>, // alpha dividends per hotkey // Miner collateral (per UID; zero when the hotkey has no collateral entry). collateral_locked: Vec>, // Locked collateral per UID @@ -116,7 +125,11 @@ pub struct Metagraph { collateral_earned: Vec>, // Lifetime emission earned per UID (since collateral existed) } -#[freeze_struct("bb7420226d39c0eb")] +/// Sparse metagraph: each field is `None` unless requested by [`SelectiveMetagraphIndex`]. +/// +/// Clients pass a list of field indices; [`Self::merge_selective_metagraph_field`] overlays +/// each requested field onto a default instance. +#[freeze_struct("201e5d736d38f056")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] pub struct SelectiveMetagraph { // Subnet index @@ -147,9 +160,9 @@ pub struct SelectiveMetagraph { alpha_in_emission: Option>, // amount injected outstanding per block tao_in_emission: Option>, // amount of tao injected per block pending_alpha_emission: Option>, // pending alpha to be distributed - pending_root_emission: Option>, // panding tao for root divs to be distributed - subnet_volume: Option>, // volume of the subnet in TAO - moving_price: Option, // subnet moving price. + pending_root_emission: Option>, // root TAO dividends removed; always 0 + subnet_volume: Option>, // volume of the subnet in TAO + moving_price: Option, // subnet moving price. // Hparams for epoch rho: Option>, // subnet rho param @@ -196,29 +209,29 @@ pub struct SelectiveMetagraph { coldkeys: Option>, // coldkey per UID identities: Option>>, // coldkeys identities axons: Option>, // UID axons. - active: Option>, // Avtive per UID + active: Option>, // active per UID validator_permit: Option>, // Val permit per UID - pruning_score: Option>>, // Pruning per UID + pruning_score: Option>>, // deprecated; empty when requested last_update: Option>>, // Last update per UID emission: Option>>, // Emission per UID dividends: Option>>, // Dividends per UID incentives: Option>>, // Mining incentives per UID consensus: Option>>, // Consensus per UID - trust: Option>>, // Trust per UID - rank: Option>>, // Rank per UID + trust: Option>>, // deprecated; empty when requested + rank: Option>>, // deprecated; empty when requested block_at_registration: Option>>, // Reg block per UID alpha_stake: Option>>, // Alpha staked per UID tao_stake: Option>>, // TAO staked per UID total_stake: Option>>, // Total stake per UID // Dividend break down. - tao_dividends_per_hotkey: Option)>>, // List of dividend payouts in tao via root - alpha_dividends_per_hotkey: Option)>>, // List of dividend payout in alpha via subnet + tao_dividends_per_hotkey: Option)>>, // always zero amounts + alpha_dividends_per_hotkey: Option)>>, // alpha dividends per hotkey // validators - validators: Option>>, // List of validators + validators: Option>>, // validator uids above stake threshold // commitments - commitments: Option>)>>, // List of commitments + commitments: Option>)>>, // hotkey commitments // Miner collateral (per UID; zero when the hotkey has no collateral entry). collateral_locked: Option>>, // Locked collateral per UID @@ -230,8 +243,9 @@ impl SelectiveMetagraph where AccountId: TypeInfo + Encode + Decode + Clone, { - pub fn merge_value(&mut self, other: &Self, metagraph_index: usize) { - match SelectiveMetagraphIndex::from_index(metagraph_index) { + /// Overlay the field selected by `metagraph_index` from `other` onto `self`. + pub fn merge_selective_metagraph_field(&mut self, other: &Self, metagraph_index: usize) { + match SelectiveMetagraphIndex::from_field_index(metagraph_index) { Some(SelectiveMetagraphIndex::Netuid) => self.netuid = other.netuid, Some(SelectiveMetagraphIndex::Name) => self.name = other.name.clone(), Some(SelectiveMetagraphIndex::Symbol) => self.symbol = other.symbol.clone(), @@ -485,6 +499,9 @@ where } } +/// Field index for [`SelectiveMetagraph`] RPCs (0 = `netuid` … 76 = `collateral_earned`). +/// +/// Indices are append-only wire constants shared with clients; do not reorder variants. pub enum SelectiveMetagraphIndex { Netuid, Name, @@ -566,7 +583,8 @@ pub enum SelectiveMetagraphIndex { } impl SelectiveMetagraphIndex { - fn from_index(index: usize) -> Option { + /// Map a client field index to a variant; unknown indices yield `None`. + fn from_field_index(index: usize) -> Option { match index { 0 => Some(SelectiveMetagraphIndex::Netuid), 1 => Some(SelectiveMetagraphIndex::Name), @@ -650,8 +668,9 @@ impl SelectiveMetagraphIndex { } } impl Pallet { + /// Full [`Metagraph`] for `netuid`, or `None` if the subnet does not exist. pub fn get_metagraph(netuid: NetUid) -> Option> { - if !Self::if_subnet_exist(netuid) { + if !Self::subnet_exists(netuid) { return None; } @@ -690,7 +709,7 @@ impl Pallet { let subnet_volume = SubnetVolume::::get(netuid); let (collateral_locked, collateral_min, collateral_earned) = - Self::collateral_vectors(netuid); + Self::miner_collateral_vectors_for_netuid(netuid); Some(Metagraph { // Subnet index netuid: netuid.into(), // subnet index. @@ -826,6 +845,7 @@ impl Pallet { collateral_earned, }) } + /// [`Metagraph`] for every subnet netuid, in [`Self::get_all_subnet_netuids`] order. pub fn get_all_metagraphs() -> Vec>> { let netuids = Self::get_all_subnet_netuids(); let mut metagraphs = Vec::>>::new(); @@ -835,6 +855,9 @@ impl Pallet { metagraphs } + /// [`Metagraph`] for a mechanism: subnet view with mechanism-scoped last_update/incentives. + /// + /// Encodes the mechanism storage index into `netuid` for clients that key by that index. pub fn get_mechagraph(netuid: NetUid, mecid: MechId) -> Option> { if Self::ensure_mechanism_exists(netuid, mecid).is_err() { return None; @@ -862,6 +885,7 @@ impl Pallet { } } + /// All mechanism metagraphs across subnets (nested `netuid` × `mecid` expansion). pub fn get_all_mechagraphs() -> Vec>> { let netuids = Self::get_all_subnet_netuids(); let mut metagraphs = Vec::>>::new(); @@ -874,35 +898,37 @@ impl Pallet { metagraphs } + /// Sparse [`SelectiveMetagraph`] containing only fields listed in `metagraph_indexes`. pub fn get_selective_metagraph( netuid: NetUid, metagraph_indexes: Vec, ) -> Option> { - if !Self::if_subnet_exist(netuid) { + if !Self::subnet_exists(netuid) { None } else { let mut result = SelectiveMetagraph::default(); for index in metagraph_indexes.iter() { - let value = Self::get_single_selective_metagraph(netuid, *index); - result.merge_value(&value, *index as usize); + let value = Self::selective_metagraph_field_for_netuid(netuid, *index); + result.merge_selective_metagraph_field(&value, *index as usize); } Some(result) } } + /// Selective metagraph for a mechanism; always sets `netuid` even if not requested. pub fn get_selective_mechagraph( netuid: NetUid, mecid: MechId, metagraph_indexes: Vec, ) -> Option> { - if !Self::if_subnet_exist(netuid) { + if !Self::subnet_exists(netuid) { None } else { let mut result = SelectiveMetagraph::default(); for index in metagraph_indexes.iter() { - let value = Self::get_single_selective_mechagraph(netuid, mecid, *index); - result.merge_value(&value, *index as usize); + let value = Self::selective_metagraph_field_for_mechanism(netuid, mecid, *index); + result.merge_selective_metagraph_field(&value, *index as usize); } // always include netuid even the metagraph_indexes doesn't contain it result.netuid = netuid.into(); @@ -911,11 +937,12 @@ impl Pallet { } } - fn get_single_selective_metagraph( + /// Single-field [`SelectiveMetagraph`] for `metagraph_index` on `netuid`. + fn selective_metagraph_field_for_netuid( netuid: NetUid, metagraph_index: u16, ) -> SelectiveMetagraph { - match SelectiveMetagraphIndex::from_index(metagraph_index as usize) { + match SelectiveMetagraphIndex::from_field_index(metagraph_index as usize) { // Name and symbol Some(SelectiveMetagraphIndex::Netuid) => SelectiveMetagraph { netuid: netuid.into(), @@ -1454,10 +1481,14 @@ impl Pallet { ..Default::default() } } - Some(SelectiveMetagraphIndex::Validators) => Self::get_validators(netuid), - Some(SelectiveMetagraphIndex::Commitments) => Self::get_commitments(netuid), + Some(SelectiveMetagraphIndex::Validators) => { + Self::validator_uids_above_stake_threshold(netuid) + } + Some(SelectiveMetagraphIndex::Commitments) => { + Self::hotkey_commitments_for_selective_metagraph(netuid) + } Some(SelectiveMetagraphIndex::CollateralLocked) => { - let (locked, _, _) = Self::collateral_vectors(netuid); + let (locked, _, _) = Self::miner_collateral_vectors_for_netuid(netuid); SelectiveMetagraph { netuid: netuid.into(), collateral_locked: Some(locked), @@ -1465,7 +1496,7 @@ impl Pallet { } } Some(SelectiveMetagraphIndex::CollateralMin) => { - let (_, min, _) = Self::collateral_vectors(netuid); + let (_, min, _) = Self::miner_collateral_vectors_for_netuid(netuid); SelectiveMetagraph { netuid: netuid.into(), collateral_min: Some(min), @@ -1473,7 +1504,7 @@ impl Pallet { } } Some(SelectiveMetagraphIndex::CollateralEarned) => { - let (_, _, earned) = Self::collateral_vectors(netuid); + let (_, _, earned) = Self::miner_collateral_vectors_for_netuid(netuid); SelectiveMetagraph { netuid: netuid.into(), collateral_earned: Some(earned), @@ -1490,7 +1521,7 @@ impl Pallet { /// Per-UID miner-collateral vectors: (locked, floor, lifetime earned). /// Zero entries for hotkeys without a collateral entry. - fn collateral_vectors( + fn miner_collateral_vectors_for_netuid( netuid: NetUid, ) -> ( Vec>, @@ -1515,7 +1546,8 @@ impl Pallet { (locked_vec, min_vec, earned_vec) } - fn get_single_selective_mechagraph( + /// Single-field selective view for a mechanism (overrides incentives/last_update storage index). + fn selective_metagraph_field_for_mechanism( netuid: NetUid, mecid: MechId, metagraph_index: u16, @@ -1523,7 +1555,7 @@ impl Pallet { let netuid_index = Self::get_mechanism_storage_index(netuid, mecid); // Default to netuid, replace as needed for mecid - match SelectiveMetagraphIndex::from_index(metagraph_index as usize) { + match SelectiveMetagraphIndex::from_field_index(metagraph_index as usize) { Some(SelectiveMetagraphIndex::Incentives) => SelectiveMetagraph { netuid: netuid.into(), incentives: Some( @@ -1547,7 +1579,7 @@ impl Pallet { }, _ => { - let mut meta = Self::get_single_selective_metagraph(netuid, metagraph_index); + let mut meta = Self::selective_metagraph_field_for_netuid(netuid, metagraph_index); // Replace netuid with index meta.netuid = NetUid::from(u16::from(netuid_index)).into(); meta @@ -1555,7 +1587,8 @@ impl Pallet { } } - fn get_validators(netuid: NetUid) -> SelectiveMetagraph { + /// Validator uids with permit and stake above [`Self::get_stake_threshold`], sorted by stake. + fn validator_uids_above_stake_threshold(netuid: NetUid) -> SelectiveMetagraph { let stake_threshold = Self::get_stake_threshold(); let hotkeys: Vec<(u16, T::AccountId)> = as IterableStorageDoubleMap>::iter_prefix(netuid) @@ -1594,7 +1627,10 @@ impl Pallet { } } - fn get_commitments(netuid: NetUid) -> SelectiveMetagraph { + /// Hotkey commitments from the commitments pallet, packed for selective metagraph index 73. + fn hotkey_commitments_for_selective_metagraph( + netuid: NetUid, + ) -> SelectiveMetagraph { let commitments = ::GetCommitments::get_commitments(netuid); let commitments: Vec<(T::AccountId, Vec>)> = commitments .iter() @@ -1708,11 +1744,11 @@ fn test_selective_metagraph() { }; // test merge function - metagraph.merge_value(&metagraph_name, wrong_index); + metagraph.merge_selective_metagraph_field(&metagraph_name, wrong_index); assert!(metagraph.name.is_none()); let name_index: usize = 1; - metagraph.merge_value(&metagraph_name, name_index); + metagraph.merge_selective_metagraph_field(&metagraph_name, name_index); assert!(metagraph.name.is_some()); let alph_low_index: usize = 50; @@ -1722,6 +1758,6 @@ fn test_selective_metagraph() { ..Default::default() }; assert!(metagraph.alpha_low.is_none()); - metagraph.merge_value(&metagraph_alpha_low, alph_low_index); + metagraph.merge_selective_metagraph_field(&metagraph_alpha_low, alph_low_index); assert!(metagraph.alpha_low.is_some()); } diff --git a/pallets/subtensor/src/rpc_info/mod.rs b/pallets/subtensor/src/rpc_info/mod.rs index 13b2e9cbb4..5104db37b0 100644 --- a/pallets/subtensor/src/rpc_info/mod.rs +++ b/pallets/subtensor/src/rpc_info/mod.rs @@ -1,3 +1,10 @@ +//! Runtime-API / RPC view builders for Subtensor state. +//! +//! Each submodule assembles a frozen SCALE DTO (`DelegateInfo`, `Metagraph`, +//! `StakeInfo`, …) from pallet storage for custom RPCs. Public method names here +//! are wired through `runtime-api` and must stay stable; private helpers in these +//! files are fair game for clearer naming. + use super::*; pub mod delegate_info; pub mod dynamic_info; diff --git a/pallets/subtensor/src/rpc_info/neuron_info.rs b/pallets/subtensor/src/rpc_info/neuron_info.rs index fe9f0f2e52..15a053f07c 100644 --- a/pallets/subtensor/src/rpc_info/neuron_info.rs +++ b/pallets/subtensor/src/rpc_info/neuron_info.rs @@ -1,3 +1,5 @@ +//! Per-uid neuron RPC views (`NeuronInfo` / `NeuronInfoLite`). + use super::*; use frame_support::pallet_prelude::{Decode, Encode}; extern crate alloc; @@ -5,7 +7,12 @@ use codec::Compact; use sp_runtime::PerU16; use subtensor_runtime_common::{AlphaBalance, NetUid, NetUidStorageIndex}; -#[freeze_struct("77daa43c02912b80")] +/// Full per-uid neuron view including sparse weights and bonds matrices. +/// +/// `rank`, `trust`, and `pruning_score` are legacy fields (zeros / max); they are no longer +/// computed on-chain. `stake` currently carries a single owner-coldkey total, not a full +/// coldkey→stake map. +#[freeze_struct("23b656b0f34441f5")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] pub struct NeuronInfo { hotkey: AccountId, @@ -15,22 +22,29 @@ pub struct NeuronInfo { active: bool, axon_info: AxonInfo, prometheus_info: PrometheusInfo, - stake: Vec<(AccountId, Compact)>, // map of coldkey to stake on this neuron/hotkey (includes delegations) + /// Owner coldkey paired with total hotkey alpha on this subnet (not a full nominator map). + stake: Vec<(AccountId, Compact)>, + /// Deprecated: always 0. rank: Compact, emission: Compact, incentive: Compact, consensus: Compact, + /// Deprecated: always 0. trust: Compact, validator_trust: Compact, dividends: Compact, last_update: Compact, validator_permit: bool, - weights: Vec<(Compact, Compact)>, // Vec of (uid, weight) - bonds: Vec<(Compact, Compact)>, // Vec of (uid, bond) + /// Sparse `(target_uid, weight)` pairs with weight > 0. + weights: Vec<(Compact, Compact)>, + /// Sparse `(target_uid, bond)` pairs with bond > 0. + bonds: Vec<(Compact, Compact)>, + /// Deprecated: always `u16::MAX`. pruning_score: Compact, } -#[freeze_struct("1d61b46ca68a1e02")] +/// [`NeuronInfo`] without weights/bonds — cheaper for list endpoints. +#[freeze_struct("8bd1725e22406377")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] pub struct NeuronInfoLite { hotkey: AccountId, @@ -40,30 +54,34 @@ pub struct NeuronInfoLite { active: bool, axon_info: AxonInfo, prometheus_info: PrometheusInfo, - stake: Vec<(AccountId, Compact)>, // map of coldkey to stake on this neuron/hotkey (includes delegations) + /// Owner coldkey paired with total hotkey alpha on this subnet (not a full nominator map). + stake: Vec<(AccountId, Compact)>, + /// Deprecated: always 0. rank: Compact, emission: Compact, incentive: Compact, consensus: Compact, + /// Deprecated: always 0. trust: Compact, validator_trust: Compact, dividends: Compact, last_update: Compact, validator_permit: bool, - // has no weights or bonds + /// Deprecated: always `u16::MAX`. pruning_score: Compact, } impl Pallet { + /// All neurons on `netuid`, or empty if the subnet does not exist. pub fn get_neurons(netuid: NetUid) -> Vec> { - if !Self::if_subnet_exist(netuid) { + if !Self::subnet_exists(netuid) { return Vec::new(); } let mut neurons = Vec::new(); let n = Self::get_subnetwork_n(netuid); for uid in 0..n { - let neuron = match Self::get_neuron_subnet_exists(netuid, uid) { + let neuron = match Self::neuron_info_for_existing_subnet_uid(netuid, uid) { Some(n) => n, None => break, // No more neurons }; @@ -73,7 +91,11 @@ impl Pallet { neurons } - fn get_neuron_subnet_exists(netuid: NetUid, uid: u16) -> Option> { + /// Full [`NeuronInfo`] for `uid` on an existing subnet; `None` if the uid has no hotkey. + fn neuron_info_for_existing_subnet_uid( + netuid: NetUid, + uid: u16, + ) -> Option> { let hotkey = match Self::get_hotkey_for_net_and_uid(netuid, uid) { Ok(h) => h, Err(_) => return None, @@ -145,15 +167,17 @@ impl Pallet { Some(neuron) } + /// One full neuron, or `None` if the subnet or uid is missing. pub fn get_neuron(netuid: NetUid, uid: u16) -> Option> { - if !Self::if_subnet_exist(netuid) { + if !Self::subnet_exists(netuid) { return None; } - Self::get_neuron_subnet_exists(netuid, uid) + Self::neuron_info_for_existing_subnet_uid(netuid, uid) } - fn get_neuron_lite_subnet_exists( + /// Lite neuron for `uid` on an existing subnet; `None` if the uid has no hotkey. + fn neuron_info_lite_for_existing_subnet_uid( netuid: NetUid, uid: u16, ) -> Option> { @@ -206,15 +230,16 @@ impl Pallet { Some(neuron) } + /// Lite neurons for every uid on `netuid`, or empty if the subnet does not exist. pub fn get_neurons_lite(netuid: NetUid) -> Vec> { - if !Self::if_subnet_exist(netuid) { + if !Self::subnet_exists(netuid) { return Vec::new(); } let mut neurons: Vec> = Vec::new(); let n = Self::get_subnetwork_n(netuid); for uid in 0..n { - let neuron = match Self::get_neuron_lite_subnet_exists(netuid, uid) { + let neuron = match Self::neuron_info_lite_for_existing_subnet_uid(netuid, uid) { Some(n) => n, None => break, // No more neurons }; @@ -224,11 +249,12 @@ impl Pallet { neurons } + /// One lite neuron, or `None` if the subnet or uid is missing. pub fn get_neuron_lite(netuid: NetUid, uid: u16) -> Option> { - if !Self::if_subnet_exist(netuid) { + if !Self::subnet_exists(netuid) { return None; } - Self::get_neuron_lite_subnet_exists(netuid, uid) + Self::neuron_info_lite_for_existing_subnet_uid(netuid, uid) } } diff --git a/pallets/subtensor/src/rpc_info/show_subnet.rs b/pallets/subtensor/src/rpc_info/show_subnet.rs index 18adfe4bc4..3bdc81a745 100644 --- a/pallets/subtensor/src/rpc_info/show_subnet.rs +++ b/pallets/subtensor/src/rpc_info/show_subnet.rs @@ -1,3 +1,5 @@ +//! Compact per-uid subnet state RPC view (`SubnetState`). + use super::*; extern crate alloc; use crate::epoch::math::*; @@ -7,7 +9,12 @@ use sp_runtime::PerU16; use substrate_fixed::types::I64F64; use subtensor_runtime_common::{AlphaBalance, NetUid, NetUidStorageIndex, TaoBalance}; -#[freeze_struct("b48b8accfd0ac902")] +/// Per-uid vectors for a subnet: keys, consensus scores, stakes, and emission history. +/// +/// `pruning_score`, `trust`, and `rank` are deprecated empty vectors. `emission_history` is +/// last emission per hotkey across all subnets (outer index = subnet order from +/// [`Pallet::get_all_subnet_netuids`]). +#[freeze_struct("8a7c09d1eba9df6a")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] pub struct SubnetState { netuid: Compact, @@ -15,44 +22,32 @@ pub struct SubnetState { coldkeys: Vec, active: Vec, validator_permit: Vec, + /// Deprecated: always empty. pruning_score: Vec>, last_update: Vec>, emission: Vec>, dividends: Vec>, incentives: Vec>, consensus: Vec>, + /// Deprecated: always empty. trust: Vec>, + /// Deprecated: always empty. rank: Vec>, block_at_registration: Vec>, alpha_stake: Vec>, tao_stake: Vec>, total_stake: Vec>, emission_history: Vec>>, - // identities: Vec, - // tao_stake: Compact, - // incentive: Compact, - // consensus: Compact, - // trust: Compact, - // validator_trust: Compact, - // dividends: Compact, - // // has no weights or bonds } impl Pallet { - /// Retrieves the emission history for a list of hotkeys across all subnets. - /// - /// This function iterates over all subnets and collects the last emission value - /// for each hotkey in the provided list. The result is a vector of vectors, where - /// each inner vector contains the emission values for a specific subnet. - /// - /// # Arguments - /// - /// * `hotkeys`: A vector of hotkeys (account IDs) for which the emission history is to be retrieved. - /// - /// # Returns + /// Last hotkey emission on each subnet for the given hotkeys. /// - /// * `Vec>>`: A vector of vectors containing the emission history for each hotkey across all subnets. - pub fn get_emissions_history(hotkeys: Vec) -> Vec>> { + /// Outer vector follows [`Self::get_all_subnet_netuids`]; each inner vector aligns with + /// `hotkeys`. + fn last_emission_history_across_subnets( + hotkeys: Vec, + ) -> Vec>> { let mut result: Vec>> = vec![]; for netuid in Self::get_all_subnet_netuids() { let mut hotkeys_emissions: Vec> = vec![]; @@ -66,37 +61,21 @@ impl Pallet { result } - /// Retrieves the state of a specific subnet. - /// - /// This function gathers various metrics and data points for a given subnet, identified by its `netuid`. - /// It collects information such as hotkeys, coldkeys, block at registration, active status, validator permits, - /// pruning scores, last updates, emissions, dividends, incentives, consensus, trust, rank, local stake, global stake, - /// stake weight, and emission history. - /// - /// # Arguments - /// - /// * `netuid`: The unique identifier of the subnet for which the state is to be retrieved. - /// - /// # Returns - /// - /// * `Option>`: An optional `SubnetState` struct containing the collected data for the subnet. - /// Returns `None` if the subnet does not exist. + /// [`SubnetState`] for `netuid`, or `None` if the subnet does not exist. pub fn get_subnet_state(netuid: NetUid) -> Option> { - if !Self::if_subnet_exist(netuid) { + if !Self::subnet_exists(netuid) { return None; } let n: u16 = Self::get_subnetwork_n(netuid); let mut hotkeys: Vec = vec![]; let mut coldkeys: Vec = vec![]; let mut block_at_registration: Vec> = vec![]; - // let mut identities: Vec = vec![]; for uid in 0..n { let hotkey = Keys::::get(netuid, uid); let coldkey = Owner::::get(hotkey.clone()); hotkeys.push(hotkey); coldkeys.push(coldkey); block_at_registration.push(BlockAtRegistration::::get(netuid, uid).into()); - // identities.push( Identities::::get( coldkey.clone() ) ); } let active: Vec = Active::::get(netuid); let validator_permit: Vec = ValidatorPermit::::get(netuid); @@ -141,7 +120,7 @@ impl Pallet { .iter() .map(|xi| Compact::from(TaoBalance::from(fixed64_to_u64(*xi)))) .collect(); - let emission_history = Self::get_emissions_history(hotkeys.clone()); + let emission_history = Self::last_emission_history_across_subnets(hotkeys.clone()); Some(SubnetState { netuid: netuid.into(), hotkeys, diff --git a/pallets/subtensor/src/rpc_info/stake_info.rs b/pallets/subtensor/src/rpc_info/stake_info.rs index 2d3316f34d..34b3c1df93 100644 --- a/pallets/subtensor/src/rpc_info/stake_info.rs +++ b/pallets/subtensor/src/rpc_info/stake_info.rs @@ -1,3 +1,5 @@ +//! Stake position and unstake-availability RPC views. + extern crate alloc; use codec::Compact; @@ -8,21 +10,29 @@ use subtensor_swap_interface::SwapHandler; use super::*; -#[freeze_struct("8cef3fae262a623e")] +/// One hotkey/coldkey stake position on a subnet for stake-info RPCs. +/// +/// `locked` and `drain` are legacy placeholders (always 0). `tao_emission` is always zero +/// (root TAO dividends removed). Prefer [`StakeAvailability`] for lock-aware unstake amounts. +#[freeze_struct("8dea8cf5d0699319")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] pub struct StakeInfo { hotkey: AccountId, coldkey: AccountId, netuid: Compact, stake: Compact, + /// Legacy; always 0. locked: Compact, emission: Compact, + /// Root TAO dividends removed; always zero. tao_emission: Compact, + /// Legacy; always 0. drain: Compact, is_registered: bool, } -#[freeze_struct("2d52e2de04425fb6")] +/// Per-subnet stake breakdown: total alpha, locked mass, and free-to-unstake alpha. +#[freeze_struct("b70cfdd87c474405")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] pub struct StakeAvailability { total: Compact, @@ -30,7 +40,6 @@ pub struct StakeAvailability { available: Compact, } -// Per-subnet stake breakdown: total alpha, locked mass, and what is free to unstake. impl StakeAvailability { pub fn total(&self) -> AlphaBalance { self.total.into() @@ -46,7 +55,8 @@ impl StakeAvailability { } impl Pallet { - fn _get_stake_info_for_coldkeys( + /// Stake rows for each coldkey across all subnets / associated hotkeys (skips zero alpha). + fn stake_info_rows_for_coldkeys( coldkeys: Vec, ) -> Vec<(T::AccountId, Vec>)> { if coldkeys.is_empty() { @@ -89,6 +99,7 @@ impl Pallet { stake_info } + /// Batch [`StakeInfo`] rows keyed by coldkey. pub fn get_stake_info_for_coldkeys( coldkey_accounts: Vec, ) -> Vec<(T::AccountId, Vec>)> { @@ -96,13 +107,14 @@ impl Pallet { return Vec::new(); // Empty coldkeys } - Self::_get_stake_info_for_coldkeys(coldkey_accounts) + Self::stake_info_rows_for_coldkeys(coldkey_accounts) } + /// [`StakeInfo`] rows for a single coldkey. pub fn get_stake_info_for_coldkey( coldkey_account: T::AccountId, ) -> Vec> { - let stake_info = Self::_get_stake_info_for_coldkeys(vec![coldkey_account]); + let stake_info = Self::stake_info_rows_for_coldkeys(vec![coldkey_account]); if stake_info.is_empty() { Vec::new() // Invalid coldkey @@ -115,6 +127,7 @@ impl Pallet { } } + /// Single stake position for `(hotkey, coldkey, netuid)` (includes zero-stake rows). pub fn get_stake_info_for_hotkey_coldkey_netuid( hotkey_account: T::AccountId, coldkey_account: T::AccountId, @@ -172,7 +185,7 @@ impl Pallet { .map(|coldkey| (coldkey, BTreeMap::new())) .collect(); } - requested.retain(|n| Self::if_subnet_exist(*n)); + requested.retain(|n| Self::subnet_exists(*n)); requested } }; @@ -206,6 +219,10 @@ impl Pallet { .collect() } + /// Approximate swap fee (rao) for moving `amount` between stake endpoints. + /// + /// Same origin and destination → 0. Otherwise fee is taken from the destination + /// subnet when present, else the origin subnet, else root. pub fn get_stake_fee( origin: Option<(T::AccountId, NetUid)>, _origin_coldkey_account: T::AccountId, diff --git a/pallets/subtensor/src/rpc_info/subnet_info.rs b/pallets/subtensor/src/rpc_info/subnet_info.rs index 073b1f0885..a1a14e5126 100644 --- a/pallets/subtensor/src/rpc_info/subnet_info.rs +++ b/pallets/subtensor/src/rpc_info/subnet_info.rs @@ -1,3 +1,5 @@ +//! Subnet hyperparameter and identity RPC views (`SubnetInfo`, `SubnetHyperparams*`). + use super::*; use frame_support::pallet_prelude::{Decode, Encode}; use frame_support::storage::IterableStorageMap; @@ -6,7 +8,10 @@ use codec::Compact; use substrate_fixed::types::{I32F32, U64F64}; use subtensor_runtime_common::{NetUid, TaoBalance}; -#[freeze_struct("f691073111c39620")] +/// Legacy subnet summary RPC DTO (v1). +/// +/// `network_connect`, `network_modality`, and `emission_values` are deprecated placeholders. +#[freeze_struct("5f8e9855861b246b")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] pub struct SubnetInfo { netuid: Compact, @@ -29,7 +34,10 @@ pub struct SubnetInfo { owner: AccountId, } -#[freeze_struct("e8e028bf4fbc6741")] +/// Subnet summary RPC DTO with optional [`SubnetIdentityV3`]. +/// +/// Wire name stays `SubnetInfov2` (clients decode by metadata type name). +#[freeze_struct("55639426c896e495")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] pub struct SubnetInfov2 { netuid: Compact, @@ -53,7 +61,8 @@ pub struct SubnetInfov2 { identity: Option, } -#[freeze_struct("5a0830a4518a7325")] +/// Fixed-shape subnet hyperparameters (v1 RPC). Prefer [`SubnetHyperparamsV2`] / V3 for new clients. +#[freeze_struct("72176dc3987ea812")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] pub struct SubnetHyperparams { rho: Compact, @@ -85,7 +94,11 @@ pub struct SubnetHyperparams { liquid_alpha_enabled: bool, } -#[freeze_struct("336a6658e70b5554")] +/// Fixed-shape subnet hyperparameters including Yuma/transfer/bonds toggles. +/// +/// `user_liquidity_enabled` is currently always `false` in the getter. New params should +/// prefer [`SubnetHyperparamsV3`] so the wire shape stays additive. +#[freeze_struct("f7bae491246ac4d9")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] pub struct SubnetHyperparamsV2 { rho: Compact, @@ -173,8 +186,9 @@ impl From<(&str, HyperparamValue)> for HyperparamEntry { pub type SubnetHyperparamsV3 = Vec; impl Pallet { + /// Legacy [`SubnetInfo`] for `netuid`, or `None` if the subnet does not exist. pub fn get_subnet_info(netuid: NetUid) -> Option> { - if !Self::if_subnet_exist(netuid) { + if !Self::subnet_exists(netuid) { return None; } @@ -219,6 +233,7 @@ impl Pallet { }) } + /// Dense `0..=max_netuid` list of optional [`SubnetInfo`] (gaps are omitted, not `None`). pub fn get_subnets_info() -> Vec>> { let mut subnet_netuids = Vec::::new(); let mut max_netuid: u16 = 0; @@ -241,8 +256,9 @@ impl Pallet { subnets_info } + /// [`SubnetInfov2`] for `netuid`, or `None` if the subnet does not exist. pub fn get_subnet_info_v2(netuid: NetUid) -> Option> { - if !Self::if_subnet_exist(netuid) { + if !Self::subnet_exists(netuid) { return None; } @@ -290,6 +306,7 @@ impl Pallet { }) } + /// Dense list of optional [`SubnetInfov2`] for every added subnet (same packing as v1). pub fn get_subnets_info_v2() -> Vec>> { let mut subnet_netuids = Vec::::new(); let mut max_netuid: u16 = 0; @@ -312,8 +329,9 @@ impl Pallet { subnets_info } + /// [`SubnetHyperparams`] for `netuid`, or `None` if the subnet does not exist. pub fn get_subnet_hyperparams(netuid: NetUid) -> Option { - if !Self::if_subnet_exist(netuid) { + if !Self::subnet_exists(netuid) { return None; } @@ -375,8 +393,9 @@ impl Pallet { }) } + /// [`SubnetHyperparamsV2`] for `netuid`, or `None` if the subnet does not exist. pub fn get_subnet_hyperparams_v2(netuid: NetUid) -> Option { - if !Self::if_subnet_exist(netuid) { + if !Self::subnet_exists(netuid) { return None; } @@ -452,6 +471,7 @@ impl Pallet { }) } + /// Auto-stake destination hotkey for `coldkey` on `netuid`, if configured. pub fn get_coldkey_auto_stake_hotkey( coldkey: T::AccountId, netuid: NetUid, @@ -466,7 +486,7 @@ impl Pallet { /// below — no struct edit and no V4 required, provided the value's type /// already has a [`HyperparamValue`] variant. pub fn get_subnet_hyperparams_v3(netuid: NetUid) -> Option { - if !Self::if_subnet_exist(netuid) { + if !Self::subnet_exists(netuid) { return None; } diff --git a/pallets/subtensor/src/staking/account.rs b/pallets/subtensor/src/staking/account.rs index 3252c0836f..227a1cbf04 100644 --- a/pallets/subtensor/src/staking/account.rs +++ b/pallets/subtensor/src/staking/account.rs @@ -1,3 +1,4 @@ +//! Associate a hotkey with a coldkey when the hotkey account does not yet exist. use super::*; impl Pallet { diff --git a/pallets/subtensor/src/staking/add_stake.rs b/pallets/subtensor/src/staking/add_stake.rs index 607c53731d..48084e2bbc 100644 --- a/pallets/subtensor/src/staking/add_stake.rs +++ b/pallets/subtensor/src/staking/add_stake.rs @@ -1,3 +1,4 @@ +//! Extrinsic bodies for `add_stake` and price-limited `add_stake_limit`. use subtensor_runtime_common::{NetUid, TaoBalance}; use subtensor_swap_interface::{Order, SwapHandler}; diff --git a/pallets/subtensor/src/staking/claim_root.rs b/pallets/subtensor/src/staking/claim_root.rs index c8b95bcb29..8f9e20047d 100644 --- a/pallets/subtensor/src/staking/claim_root.rs +++ b/pallets/subtensor/src/staking/claim_root.rs @@ -1,3 +1,4 @@ +//! Root-subnet claimable dividends: accrue, claim, and auto-claim indexing. use super::*; use frame_support::dispatch::DispatchResult; use frame_support::storage::{TransactionOutcome, with_transaction}; @@ -11,6 +12,10 @@ use subtensor_runtime_common::clear_prefix_with_meter; use subtensor_swap_interface::SwapHandler; impl Pallet { + /// Derive up to `k` distinct coldkey-index samples in `0..n` from `block_hash`. + /// + /// Used by auto-claim to pick which indexed coldkeys receive root dividends + /// this block without scanning the full index. pub fn block_hash_to_indices(block_hash: T::Hash, k: u64, n: u64) -> Vec { let block_hash_bytes = block_hash.as_ref(); let mut indices: BTreeSet = BTreeSet::new(); @@ -476,7 +481,7 @@ impl Pallet { // // GHSA-2026-010 (a *stale residual* watermark on new_hotkey inflating this sum // in the hotkey-swap path) is prevented upstream by the root-swap cleanliness - // gate in `do_swap_hotkey`, which now also requires RootClaimed to be empty on + // gate in `perform_hotkey_swap`, which now also requires RootClaimed to be empty on // new_hotkey (see `test_do_swap_hotkey_err_new_hotkey_not_clean_for_root`). With // that gate the destination is always clean (new == 0) in the swap path, so the // sum cannot be inflated there. @@ -516,7 +521,8 @@ impl Pallet { None => RootClaimable::::iter(), }; - fn filter_claimable( + /// Drop claimable entries for `netuid` from a hotkey's root-claimable map. + fn filter_root_claimable_for_netuid( claimable: &BTreeMap, netuid: NetUid, ) -> BTreeMap { @@ -533,7 +539,10 @@ impl Pallet { |(_, _)| true, |(hotkey, claimable)| (hotkey.clone(), claimable.clone()), |(hotkey, claimable)| { - RootClaimable::::insert(hotkey, filter_claimable(claimable, netuid)) + RootClaimable::::insert( + hotkey, + filter_root_claimable_for_netuid(claimable, netuid), + ) }, 1, ); diff --git a/pallets/subtensor/src/staking/decrease_take.rs b/pallets/subtensor/src/staking/decrease_take.rs index cf16a27f47..6b57751903 100644 --- a/pallets/subtensor/src/staking/decrease_take.rs +++ b/pallets/subtensor/src/staking/decrease_take.rs @@ -1,3 +1,4 @@ +//! Extrinsic body for decreasing a delegate hotkey's take. use sp_runtime::PerU16; use super::*; diff --git a/pallets/subtensor/src/staking/helpers.rs b/pallets/subtensor/src/staking/helpers.rs index 96b1d6a391..d8ac4705ff 100644 --- a/pallets/subtensor/src/staking/helpers.rs +++ b/pallets/subtensor/src/staking/helpers.rs @@ -1,3 +1,4 @@ +//! Stake totals, hotkey ownership, delegate flags, and nomination dust cleanup. use alloc::collections::BTreeMap; use safe_math::*; use share_pool::SafeFloat; @@ -9,32 +10,27 @@ use subtensor_swap_interface::{Order, SwapHandler}; use super::*; impl Pallet { - // Returns true if the passed hotkey allow delegative staking. - // + /// True if this hotkey is registered as a delegate (accepts nominator stake). pub fn hotkey_is_delegate(hotkey: &T::AccountId) -> bool { Delegates::::contains_key(hotkey) } - // Sets the hotkey as a delegate with take. - // + /// Mark `hotkey` as a delegate with the given take (parts-per-`u16::MAX`). pub fn delegate_hotkey(hotkey: &T::AccountId, take: u16) { Delegates::::insert(hotkey, PerU16::from_parts(take)); } - // Returns the total amount of stake in the staking table. - // + /// Network-wide total stake in TAO (`TotalStake` storage). pub fn get_total_stake() -> TaoBalance { TotalStake::::get() } - // Increases the total amount of stake by the passed amount. - // + /// Increase [`TotalStake`] by `increment` (saturating). pub fn increase_total_stake(increment: TaoBalance) { TotalStake::::put(Self::get_total_stake().saturating_add(increment)); } - // Decreases the total amount of stake by the passed amount. - // + /// Decrease [`TotalStake`] by `decrement` (saturating). pub fn decrease_total_stake(decrement: TaoBalance) { TotalStake::::put(Self::get_total_stake().saturating_sub(decrement)); } @@ -130,7 +126,7 @@ impl Pallet { ) -> DispatchResult { // Only allow to register non-system hotkeys ensure!( - Self::is_subnet_account_id(hotkey).is_none(), + Self::netuid_for_subnet_account(hotkey).is_none(), Error::::CannotUseSystemAccount ); @@ -157,7 +153,7 @@ impl Pallet { pub fn set_hotkey_owner(coldkey: &T::AccountId, hotkey: &T::AccountId) -> DispatchResult { // Only allow to register non-system hotkeys ensure!( - Self::is_subnet_account_id(hotkey).is_none(), + Self::netuid_for_subnet_account(hotkey).is_none(), Error::::CannotUseSystemAccount ); Owner::::insert(hotkey, coldkey); diff --git a/pallets/subtensor/src/staking/increase_take.rs b/pallets/subtensor/src/staking/increase_take.rs index e88a871559..3b62a7e267 100644 --- a/pallets/subtensor/src/staking/increase_take.rs +++ b/pallets/subtensor/src/staking/increase_take.rs @@ -1,3 +1,4 @@ +//! Extrinsic body for increasing a delegate hotkey's take (rate-limited). use sp_runtime::PerU16; use super::*; diff --git a/pallets/subtensor/src/staking/lock.rs b/pallets/subtensor/src/staking/lock.rs deleted file mode 100644 index 6a02d13942..0000000000 --- a/pallets/subtensor/src/staking/lock.rs +++ /dev/null @@ -1,2054 +0,0 @@ -use super::*; -use codec::{Decode, DecodeWithMemTracking, Encode}; -use frame_support::weights::WeightMeter; -use safe_math::FixedExt; -use scale_info::TypeInfo; -use sp_std::collections::btree_map::BTreeMap; -use sp_std::ops::Neg; -use substrate_fixed::transcendental::exp; -use substrate_fixed::types::{I64F64, U64F64}; -use subtensor_runtime_common::NetUid; - -pub const ONE_YEAR: u64 = 7200 * 365 + 1800; -pub const LOCK_STATE_ZERO_THRESHOLD: u64 = 100; - -/// Exponential lock state for a coldkey on a subnet. -#[crate::freeze_struct("1f6be20a66128b8d")] -#[derive(Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug, TypeInfo)] -pub struct LockState { - /// Exponentially decaying locked amount. - pub locked_mass: AlphaBalance, - /// Matured decaying score (integral of locked_mass over time). - pub conviction: U64F64, - /// Block number of last roll-forward. - pub last_update: u64, -} - -impl LockState { - pub fn is_zero(&self) -> bool { - self.locked_mass < AlphaBalance::from(LOCK_STATE_ZERO_THRESHOLD) - && self.conviction < U64F64::saturating_from_num(LOCK_STATE_ZERO_THRESHOLD) - } -} - -/// Change produced by rolling a lock forward. Locked mass only ever -/// decreases, but conviction can move either way (it matures upward from -/// locked mass and decays downward once the mass is gone), so its change is -/// carried as separate unsigned growth/decay components. -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct RollDelta { - pub locked_mass_delta: AlphaBalance, - pub conviction_decay: U64F64, - pub conviction_growth: U64F64, -} - -impl RollDelta { - pub fn zero() -> Self { - Self { - locked_mass_delta: AlphaBalance::ZERO, - conviction_decay: U64F64::saturating_from_num(0), - conviction_growth: U64F64::saturating_from_num(0), - } - } - - pub fn is_zero(&self) -> bool { - self.locked_mass_delta.is_zero() - && self.conviction_decay == U64F64::saturating_from_num(0) - && self.conviction_growth == U64F64::saturating_from_num(0) - } -} - -/// A struct that incapsulates Lock primitives such as adding, removing, -/// rolling, and updating aggregates. -/// -/// This model has one individual lock state, which relates to the stake owner -/// (locking coldkey) lock and 4 aggregates that are maintained in operations. -pub struct ConvictionModel { - /// Whether this model's individual lock targets the subnet owner hotkey. - owner_lock: bool, - /// Whether this model's individual lock uses the non-decaying lock mode. - perpetual_lock: bool, - /// Individual stake owner coldkey lock - individual_lock: LockState, - individual_lock_dirty: bool, - /// Perpetual non-owner aggregate - agg_perpetual_general: LockState, - agg_perpetual_general_dirty: bool, - /// Decaying non-owner aggregate - agg_decaying_general: LockState, - agg_decaying_general_dirty: bool, - /// Perpetual owner aggregate - agg_perpetual_owner: LockState, - agg_perpetual_owner_dirty: bool, - /// Decaying owner aggregate - agg_decaying_owner: LockState, - agg_decaying_owner_dirty: bool, -} - -impl ConvictionModel { - pub fn new( - owner_lock: bool, - perpetual_lock: bool, - individual_lock: LockState, - agg_perpetual_general: LockState, - agg_decaying_general: LockState, - agg_perpetual_owner: LockState, - agg_decaying_owner: LockState, - ) -> Self { - Self { - owner_lock, - perpetual_lock, - individual_lock, - individual_lock_dirty: false, - agg_perpetual_general, - agg_perpetual_general_dirty: false, - agg_decaying_general, - agg_decaying_general_dirty: false, - agg_perpetual_owner, - agg_perpetual_owner_dirty: false, - agg_decaying_owner, - agg_decaying_owner_dirty: false, - } - } - - pub fn individual_lock(&self) -> &LockState { - &self.individual_lock - } - - pub fn agg_perpetual_general(&self) -> &LockState { - &self.agg_perpetual_general - } - - pub fn agg_decaying_general(&self) -> &LockState { - &self.agg_decaying_general - } - - pub fn agg_perpetual_owner(&self) -> &LockState { - &self.agg_perpetual_owner - } - - pub fn agg_decaying_owner(&self) -> &LockState { - &self.agg_decaying_owner - } - - pub fn aggregate_lock(&self) -> &LockState { - if self.owner_lock && self.perpetual_lock { - &self.agg_perpetual_owner - } else if self.owner_lock { - &self.agg_decaying_owner - } else if self.perpetual_lock { - &self.agg_perpetual_general - } else { - &self.agg_decaying_general - } - } - - pub fn individual_lock_dirty(&self) -> bool { - self.individual_lock_dirty - } - - pub fn agg_perpetual_general_dirty(&self) -> bool { - self.agg_perpetual_general_dirty - } - - pub fn agg_decaying_general_dirty(&self) -> bool { - self.agg_decaying_general_dirty - } - - pub fn agg_perpetual_owner_dirty(&self) -> bool { - self.agg_perpetual_owner_dirty - } - - pub fn agg_decaying_owner_dirty(&self) -> bool { - self.agg_decaying_owner_dirty - } - - pub fn merge(&mut self, conv: &ConvictionModel) { - self.individual_lock = Self::merge_lock(&self.individual_lock, &conv.individual_lock); - self.individual_lock_dirty = true; - self.agg_perpetual_general = - Self::merge_lock(&self.agg_perpetual_general, &conv.agg_perpetual_general); - self.agg_perpetual_general_dirty = true; - self.agg_decaying_general = - Self::merge_lock(&self.agg_decaying_general, &conv.agg_decaying_general); - self.agg_decaying_general_dirty = true; - self.agg_perpetual_owner = - Self::merge_lock(&self.agg_perpetual_owner, &conv.agg_perpetual_owner); - self.agg_perpetual_owner_dirty = true; - self.agg_decaying_owner = - Self::merge_lock(&self.agg_decaying_owner, &conv.agg_decaying_owner); - self.agg_decaying_owner_dirty = true; - } - - pub fn set_individual_lock(&mut self, lock: LockState) { - self.individual_lock = lock; - self.individual_lock_dirty = true; - } - - pub fn set_rolled_individual_lock( - &mut self, - lock: LockState, - now: u64, - unlock_rate: u64, - maturity_rate: u64, - ) { - self.individual_lock = Self::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - self.owner_lock, - self.perpetual_lock, - ) - .0; - self.individual_lock_dirty = true; - } - - pub fn roll_forward(&mut self, now: u64, unlock_rate: u64, maturity_rate: u64) { - let (rolled_individual_lock, roll_delta) = Self::roll_forward_lock( - self.individual_lock.clone(), - now, - unlock_rate, - maturity_rate, - self.owner_lock, - self.perpetual_lock, - ); - self.individual_lock = rolled_individual_lock; - self.individual_lock_dirty = true; - if !roll_delta.is_zero() { - self.apply_roll_delta_to_aggregate(roll_delta, now); - } else { - self.roll_forward_aggregate(now, unlock_rate, maturity_rate); - } - } - - pub fn roll_forward_aggregate(&mut self, now: u64, unlock_rate: u64, maturity_rate: u64) { - let owner_lock = self.owner_lock; - let perpetual_lock = self.perpetual_lock; - let (aggregate, aggregate_dirty) = self.aggregate_mut(); - *aggregate = Self::roll_forward_lock( - aggregate.clone(), - now, - unlock_rate, - maturity_rate, - owner_lock, - perpetual_lock, - ) - .0; - *aggregate_dirty = true; - } - - pub fn add_to_aggregate(&mut self, added: &LockState) { - let (aggregate, aggregate_dirty) = self.aggregate_mut(); - *aggregate = Self::merge_lock(aggregate, added); - *aggregate_dirty = true; - } - - pub fn reduce_aggregate(&mut self, locked_mass: AlphaBalance, conviction: U64F64) { - let (aggregate, aggregate_dirty) = self.aggregate_mut(); - *aggregate = Self::reduce_lock(aggregate, locked_mass, conviction); - *aggregate_dirty = true; - } - - fn apply_roll_delta_to_aggregate(&mut self, roll_delta: RollDelta, now: u64) { - let (aggregate, aggregate_dirty) = self.aggregate_mut(); - *aggregate = Self::reduce_lock( - aggregate, - roll_delta.locked_mass_delta, - roll_delta.conviction_decay, - ); - // Conviction matured by the individual lock must be credited to the - // aggregate here: bumping last_update below means the aggregate's own - // roll-forward will never cover this window, so dropping the growth - // (as a saturating decrease-only delta used to) permanently - // understates aggregate conviction. - aggregate.conviction = aggregate - .conviction - .saturating_add(roll_delta.conviction_growth); - aggregate.last_update = now; - *aggregate_dirty = true; - } - - pub fn reduce(&mut self, locked_mass: AlphaBalance, conviction: U64F64) { - self.individual_lock = Self::reduce_lock(&self.individual_lock, locked_mass, conviction); - self.individual_lock_dirty = true; - - let (aggregate, aggregate_dirty) = self.aggregate_mut(); - *aggregate = Self::reduce_lock(aggregate, locked_mass, conviction); - *aggregate_dirty = true; - } - - pub fn force_reduce_individual(&mut self, amount: AlphaBalance, now: u64) { - let rolled = self.individual_lock.clone(); - let new_locked_mass = rolled.locked_mass.saturating_sub(amount); - let locked_mass_diff = rolled.locked_mass.saturating_sub(new_locked_mass); - - let conviction_diff = if new_locked_mass.is_zero() { - self.individual_lock = LockState { - locked_mass: AlphaBalance::ZERO, - conviction: U64F64::saturating_from_num(0), - last_update: now, - }; - rolled.conviction - } else { - let removed_proportion = U64F64::saturating_from_num(u64::from(amount)) - .safe_div(U64F64::saturating_from_num(u64::from(rolled.locked_mass))); - let new_conviction = rolled - .conviction - .saturating_mul(U64F64::saturating_from_num(1).saturating_sub(removed_proportion)); - self.individual_lock = LockState { - locked_mass: new_locked_mass, - conviction: new_conviction, - last_update: now, - }; - rolled.conviction.saturating_sub(new_conviction) - }; - self.individual_lock_dirty = true; - - self.reduce_aggregate(locked_mass_diff, conviction_diff); - } - - fn aggregate_mut(&mut self) -> (&mut LockState, &mut bool) { - if self.owner_lock && self.perpetual_lock { - ( - &mut self.agg_perpetual_owner, - &mut self.agg_perpetual_owner_dirty, - ) - } else if self.owner_lock { - ( - &mut self.agg_decaying_owner, - &mut self.agg_decaying_owner_dirty, - ) - } else if self.perpetual_lock { - ( - &mut self.agg_perpetual_general, - &mut self.agg_perpetual_general_dirty, - ) - } else { - ( - &mut self.agg_decaying_general, - &mut self.agg_decaying_general_dirty, - ) - } - } - - fn merge_lock(lhs: &LockState, rhs: &LockState) -> LockState { - LockState { - locked_mass: lhs.locked_mass.saturating_add(rhs.locked_mass), - conviction: lhs.conviction.saturating_add(rhs.conviction), - last_update: lhs.last_update.max(rhs.last_update), - } - } - - fn reduce_lock(lock: &LockState, locked_mass: AlphaBalance, conviction: U64F64) -> LockState { - LockState { - locked_mass: lock.locked_mass.saturating_sub(locked_mass), - conviction: lock.conviction.saturating_sub(conviction), - last_update: lock.last_update, - } - } - - pub fn exp_decay(dt: u64, tau: u64) -> U64F64 { - if tau == 0 || dt == 0 { - if dt == 0 { - return U64F64::saturating_from_num(1); - } - return U64F64::saturating_from_num(0); - } - let min_ratio = I64F64::saturating_from_num(-40); - let neg_ratio = I64F64::saturating_from_num((dt as i128).neg()) - .checked_div(I64F64::saturating_from_num(tau)) - .unwrap_or(min_ratio); - let clamped = neg_ratio.max(min_ratio); - let decay: I64F64 = exp(clamped).unwrap_or(I64F64::saturating_from_num(0)); - if decay < I64F64::saturating_from_num(0) { - U64F64::saturating_from_num(0) - } else { - U64F64::saturating_from_num(decay) - } - } - - fn calculate_decayed_mass_and_conviction( - locked_mass: AlphaBalance, - conviction: U64F64, - dt: u64, - unlock_rate: u64, - maturity_rate: u64, - perpetual_lock: bool, - ) -> (AlphaBalance, U64F64) { - let unlock_decay = Self::exp_decay(dt, unlock_rate); - let maturity_decay = Self::exp_decay(dt, maturity_rate); - let mass_fixed = U64F64::saturating_from_num(locked_mass); - let new_locked_mass = if perpetual_lock { - locked_mass - } else { - unlock_decay - .saturating_mul(mass_fixed) - .saturating_to_num::() - .into() - }; - - let conviction_from_existing = maturity_decay.saturating_mul(conviction); - let conviction_from_mass = if perpetual_lock { - mass_fixed.saturating_mul(U64F64::saturating_from_num(1).saturating_sub(maturity_decay)) - } else if unlock_rate == maturity_rate { - let dt_fixed = U64F64::saturating_from_num(dt); - let maturity_rate_fixed = U64F64::saturating_from_num(maturity_rate); - mass_fixed.saturating_mul( - dt_fixed - .safe_div(maturity_rate_fixed) - .saturating_mul(maturity_decay), - ) - } else if unlock_rate == 0 || maturity_rate == 0 { - U64F64::saturating_from_num(0) - } else { - let tau_x = I64F64::saturating_from_num(unlock_rate); - let tau_delta = I64F64::saturating_from_num( - (unlock_rate as i128).saturating_sub(maturity_rate as i128), - ); - let decay_delta = I64F64::saturating_from_num(unlock_decay) - .saturating_sub(I64F64::saturating_from_num(maturity_decay)); - let gamma = tau_x - .saturating_mul(decay_delta) - .checked_div(tau_delta) - .unwrap_or(I64F64::saturating_from_num(0)); - if gamma <= I64F64::saturating_from_num(0) { - U64F64::saturating_from_num(0) - } else { - mass_fixed.saturating_mul(U64F64::saturating_from_num(gamma)) - } - }; - let new_conviction = conviction_from_existing.saturating_add(conviction_from_mass); - (new_locked_mass, new_conviction) - } - - pub fn roll_forward_lock( - lock: LockState, - now: u64, - unlock_rate: u64, - maturity_rate: u64, - owner_lock: bool, - perpetual_lock: bool, - ) -> (LockState, RollDelta) { - let previous_locked_mass = lock.locked_mass; - let previous_conviction = lock.conviction; - let mut rolled = if now > lock.last_update { - let dt = now.saturating_sub(lock.last_update); - let (new_locked_mass, new_conviction) = Self::calculate_decayed_mass_and_conviction( - lock.locked_mass, - lock.conviction, - dt, - unlock_rate, - maturity_rate, - perpetual_lock, - ); - - LockState { - locked_mass: new_locked_mass, - conviction: new_conviction, - last_update: now, - } - } else { - lock - }; - - if owner_lock { - rolled.conviction = U64F64::saturating_from_num(u64::from(rolled.locked_mass)); - } - - if rolled.is_zero() { - rolled.locked_mass = AlphaBalance::ZERO; - rolled.conviction = U64F64::saturating_from_num(0); - } - - let roll_delta = RollDelta { - locked_mass_delta: previous_locked_mass.saturating_sub(rolled.locked_mass), - conviction_decay: previous_conviction.saturating_sub(rolled.conviction), - conviction_growth: rolled.conviction.saturating_sub(previous_conviction), - }; - - (rolled, roll_delta) - } -} - -impl Pallet { - pub fn add_locking_coldkey(hotkey: &T::AccountId, netuid: NetUid, coldkey: &T::AccountId) { - LockingColdkeys::::insert((netuid, hotkey, coldkey), ()); - } - - pub fn maybe_remove_locking_coldkey( - hotkey: &T::AccountId, - netuid: NetUid, - coldkey: &T::AccountId, - ) { - LockingColdkeys::::remove((netuid, hotkey, coldkey)); - } - - pub fn account_rejects_locked_alpha(coldkey: &T::AccountId) -> bool { - AccountFlags::::get(coldkey) & crate::ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA != 1 - } - - pub fn set_accept_locked_alpha(coldkey: &T::AccountId, enabled: bool) { - AccountFlags::::mutate_exists(coldkey, |maybe_flags| { - let mut flags = maybe_flags.unwrap_or_default(); - if enabled { - flags |= crate::ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA; - } else { - flags &= !crate::ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA; - } - *maybe_flags = if flags == 0 { None } else { Some(flags) }; - }); - } - - pub fn ensure_can_receive_locked_alpha( - coldkey: &T::AccountId, - amount: AlphaBalance, - ) -> DispatchResult { - let rejects_locked_alpha = Self::account_rejects_locked_alpha(coldkey); - Self::ensure_can_receive_locked_alpha_with_flag(rejects_locked_alpha, amount) - } - - fn ensure_can_receive_locked_alpha_with_flag( - rejects_locked_alpha: bool, - amount: AlphaBalance, - ) -> DispatchResult { - if amount.is_zero() { - return Ok(()); - } - ensure!(!rejects_locked_alpha, Error::::AccountRejectsLockedAlpha); - Ok(()) - } - - pub fn insert_lock_state( - coldkey: &T::AccountId, - netuid: NetUid, - hotkey: &T::AccountId, - lock_state: LockState, - ) { - if lock_state.is_zero() { - Self::maybe_remove_locking_coldkey(hotkey, netuid, coldkey); - // If there is no record previously, this is a no-op - Lock::::remove((coldkey, netuid, hotkey)); - } else { - Self::add_locking_coldkey(hotkey, netuid, coldkey); - Lock::::insert((coldkey, netuid, hotkey), lock_state); - } - } - - pub fn insert_hotkey_lock_state(netuid: NetUid, hotkey: &T::AccountId, lock_state: LockState) { - if !lock_state.locked_mass.is_zero() - || lock_state.conviction > U64F64::saturating_from_num(0) - { - HotkeyLock::::insert(netuid, hotkey, lock_state); - } else { - HotkeyLock::::remove(netuid, hotkey); - } - } - - pub fn insert_decaying_hotkey_lock_state( - netuid: NetUid, - hotkey: &T::AccountId, - lock_state: LockState, - ) { - if !lock_state.locked_mass.is_zero() - || lock_state.conviction > U64F64::saturating_from_num(0) - { - DecayingHotkeyLock::::insert(netuid, hotkey, lock_state); - } else { - DecayingHotkeyLock::::remove(netuid, hotkey); - } - } - - pub fn insert_owner_lock_state(netuid: NetUid, lock_state: LockState) { - if !lock_state.locked_mass.is_zero() - || lock_state.conviction > U64F64::saturating_from_num(0) - { - OwnerLock::::insert(netuid, lock_state); - } else { - OwnerLock::::remove(netuid); - } - } - - pub fn insert_decaying_owner_lock_state(netuid: NetUid, lock_state: LockState) { - if !lock_state.locked_mass.is_zero() - || lock_state.conviction > U64F64::saturating_from_num(0) - { - DecayingOwnerLock::::insert(netuid, lock_state); - } else { - DecayingOwnerLock::::remove(netuid); - } - } - - pub(crate) fn is_subnet_owner_hotkey(netuid: NetUid, hotkey: &T::AccountId) -> bool { - hotkey == &SubnetOwnerHotkey::::get(netuid) - } - - pub(crate) fn is_perpetual_lock(coldkey: &T::AccountId, netuid: NetUid) -> bool { - DecayingLock::::get(coldkey, netuid) == Some(false) - } - - fn empty_lock(now: u64) -> LockState { - LockState { - locked_mass: AlphaBalance::ZERO, - conviction: U64F64::saturating_from_num(0), - last_update: now, - } - } - - pub(crate) fn read_conviction_model_for_hotkey( - coldkey: &T::AccountId, - netuid: NetUid, - hotkey: &T::AccountId, - now: u64, - ) -> ConvictionModel { - ConvictionModel::new( - Self::is_subnet_owner_hotkey(netuid, hotkey), - Self::is_perpetual_lock(coldkey, netuid), - Lock::::get((coldkey, netuid, hotkey)).unwrap_or_else(|| Self::empty_lock(now)), - HotkeyLock::::get(netuid, hotkey).unwrap_or_else(|| Self::empty_lock(now)), - DecayingHotkeyLock::::get(netuid, hotkey).unwrap_or_else(|| Self::empty_lock(now)), - OwnerLock::::get(netuid).unwrap_or_else(|| Self::empty_lock(now)), - DecayingOwnerLock::::get(netuid).unwrap_or_else(|| Self::empty_lock(now)), - ) - } - - fn read_conviction_model( - coldkey: &T::AccountId, - netuid: NetUid, - now: u64, - ) -> Option<(T::AccountId, ConvictionModel)> { - Lock::::iter_prefix((coldkey, netuid)) - .next() - .map(|(hotkey, _lock)| { - let model = Self::read_conviction_model_for_hotkey(coldkey, netuid, &hotkey, now); - (hotkey, model) - }) - } - - pub(crate) fn save_conviction_model( - coldkey: &T::AccountId, - netuid: NetUid, - hotkey: &T::AccountId, - model: ConvictionModel, - ) { - if model.individual_lock_dirty() { - Self::insert_lock_state(coldkey, netuid, hotkey, model.individual_lock().clone()); - } - if model.agg_perpetual_general_dirty() { - Self::insert_hotkey_lock_state(netuid, hotkey, model.agg_perpetual_general().clone()); - } - if model.agg_decaying_general_dirty() { - Self::insert_decaying_hotkey_lock_state( - netuid, - hotkey, - model.agg_decaying_general().clone(), - ); - } - if model.agg_perpetual_owner_dirty() { - Self::insert_owner_lock_state(netuid, model.agg_perpetual_owner().clone()); - } - if model.agg_decaying_owner_dirty() { - Self::insert_decaying_owner_lock_state(netuid, model.agg_decaying_owner().clone()); - } - } - - pub fn do_set_perpetual_lock( - coldkey: &T::AccountId, - netuid: NetUid, - enabled: bool, - ) -> DispatchResult { - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); - - let now = Self::get_current_block_as_u64(); - let current_enabled = Self::is_perpetual_lock(coldkey, netuid); - - if let Some((hotkey, mut model)) = Self::read_conviction_model(coldkey, netuid, now) { - model.roll_forward(now, UnlockRate::::get(), MaturityRate::::get()); - let rolled = model.individual_lock().clone(); - Self::save_conviction_model(coldkey, netuid, &hotkey, model); - - if current_enabled != enabled { - Self::reduce_aggregate_lock( - coldkey, - &hotkey, - netuid, - rolled.locked_mass, - rolled.conviction, - ); - } - } - - if enabled { - DecayingLock::::insert(coldkey, netuid, false); - } else { - DecayingLock::::remove(coldkey, netuid); - } - - if current_enabled != enabled - && let Some((hotkey, model)) = Self::read_conviction_model(coldkey, netuid, now) - { - Self::add_aggregate_lock(coldkey, &hotkey, netuid, model.individual_lock().clone()); - } - Self::deposit_event(Event::PerpetualLockUpdated { - coldkey: coldkey.clone(), - netuid, - enabled, - }); - Ok(()) - } - - /// Returns the sum of raw alpha shares for a coldkey across all hotkeys on a given subnet. - pub fn total_coldkey_alpha_on_subnet(coldkey: &T::AccountId, netuid: NetUid) -> AlphaBalance { - StakingHotkeys::::get(coldkey) - .into_iter() - .map(|hotkey| { - Self::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, coldkey, netuid) - }) - .fold(AlphaBalance::ZERO, |acc, stake| acc.saturating_add(stake)) - } - - /// Returns the current locked amount for a coldkey on a subnet. - pub fn get_current_locked(coldkey: &T::AccountId, netuid: NetUid) -> AlphaBalance { - let now = Self::get_current_block_as_u64(); - Self::read_conviction_model(coldkey, netuid, now) - .map(|(_hotkey, mut model)| { - model.roll_forward(now, UnlockRate::::get(), MaturityRate::::get()); - model.individual_lock().locked_mass - }) - .unwrap_or(AlphaBalance::ZERO) - } - - /// Returns the current conviction for a coldkey on a subnet (rolled forward to now). - pub fn get_conviction(coldkey: &T::AccountId, netuid: NetUid) -> U64F64 { - let now = Self::get_current_block_as_u64(); - Self::read_conviction_model(coldkey, netuid, now) - .map(|(_hotkey, mut model)| { - model.roll_forward(now, UnlockRate::::get(), MaturityRate::::get()); - model.individual_lock().conviction - }) - .unwrap_or_else(|| U64F64::saturating_from_num(0)) - } - - /// Returns the current lock for a coldkey on a subnet, rolled forward to now. - pub fn get_coldkey_lock(coldkey: &T::AccountId, netuid: NetUid) -> Option { - let now = Self::get_current_block_as_u64(); - Self::read_conviction_model(coldkey, netuid, now).map(|(_hotkey, mut model)| { - model.roll_forward(now, UnlockRate::::get(), MaturityRate::::get()); - model.individual_lock().clone() - }) - } - - /// (total_stake, locked_mass, available_to_unstake) for a coldkey on one subnet. - /// - /// The conviction lock is subnet-wide: it blocks unstaking from any hotkey on - /// that subnet, not from a single hotkey position. Miner registration - /// collateral is also subtracted here as a coldkey-wide residual; call sites - /// that know the origin hotkey must additionally call - /// `ensure_hotkey_covers_collateral` so the bond cannot be covered by free - /// stake on a sibling hotkey. - pub fn stake_availability( - coldkey: &T::AccountId, - netuid: NetUid, - ) -> (AlphaBalance, AlphaBalance, AlphaBalance) { - let total = Self::total_coldkey_alpha_on_subnet(coldkey, netuid); - let locked = Self::get_current_locked(coldkey, netuid); - let collateral = Self::total_miner_collateral_for_coldkey(coldkey, netuid); - let available = total.saturating_sub(locked).saturating_sub(collateral); - (total, locked, available) - } - - /// Alpha the coldkey can still unstake on this subnet right now. - pub fn available_to_unstake(coldkey: &T::AccountId, netuid: NetUid) -> AlphaBalance { - let (_, _, available) = Self::stake_availability(coldkey, netuid); - available - } - - /// Ensures that the amount can be unstaked - pub fn ensure_available_to_unstake( - coldkey: &T::AccountId, - netuid: NetUid, - amount: AlphaBalance, - ) -> Result<(), Error> { - let alpha_available = Self::available_to_unstake(coldkey, netuid); - ensure!(alpha_available >= amount, Error::::StakeUnavailable); - Ok(()) - } - - /// Locks stake for a coldkey on a subnet to a specific hotkey. - /// If no lock exists, creates one. If one exists, the hotkey must match. - /// Top-up adds to locked_mass after rolling forward. - pub fn do_lock_stake( - coldkey: &T::AccountId, - netuid: NetUid, - hotkey: &T::AccountId, - amount: AlphaBalance, - ) -> dispatch::DispatchResult { - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); - ensure!(!amount.is_zero(), Error::::AmountTooLow); - ensure!( - Self::hotkey_account_exists(hotkey), - Error::::HotKeyAccountNotExists - ); - - let total = Self::total_coldkey_alpha_on_subnet(coldkey, netuid); - let now = Self::get_current_block_as_u64(); - - let mut model = match Self::read_conviction_model(coldkey, netuid, now) { - Some((existing_hotkey, model)) => { - ensure!(*hotkey == existing_hotkey, Error::::LockHotkeyMismatch); - model - } - None => Self::read_conviction_model_for_hotkey(coldkey, netuid, hotkey, now), - }; - model.roll_forward(now, UnlockRate::::get(), MaturityRate::::get()); - - if model.individual_lock().locked_mass.is_zero() - && model.individual_lock().conviction == U64F64::saturating_from_num(0) - { - ensure!(total >= amount, Error::::InsufficientStakeForLock); - - model.set_rolled_individual_lock( - LockState { - locked_mass: amount, - conviction: U64F64::saturating_from_num(0), - last_update: now, - }, - now, - UnlockRate::::get(), - MaturityRate::::get(), - ); - } else { - let mut lock = model.individual_lock().clone(); - lock.locked_mass = lock.locked_mass.saturating_add(amount); - ensure!( - total >= lock.locked_mass, - Error::::InsufficientStakeForLock - ); - model.set_rolled_individual_lock( - lock, - now, - UnlockRate::::get(), - MaturityRate::::get(), - ); - } - - model.roll_forward_aggregate(now, UnlockRate::::get(), MaturityRate::::get()); - model.add_to_aggregate(&LockState { - locked_mass: amount, - conviction: U64F64::saturating_from_num(0), - last_update: now, - }); - model.roll_forward_aggregate(now, UnlockRate::::get(), MaturityRate::::get()); - Self::save_conviction_model(coldkey, netuid, hotkey, model); - - Self::deposit_event(Event::StakeLocked { - coldkey: coldkey.clone(), - hotkey: hotkey.clone(), - netuid, - amount, - }); - - Ok(()) - } - - /// Reduces the coldkey lock by a specified alpha amount and the coldkey conviction - /// proportionally. - pub fn force_reduce_lock(coldkey: &T::AccountId, netuid: NetUid, amount: AlphaBalance) { - let now = Self::get_current_block_as_u64(); - if let Some((hotkey, mut model)) = Self::read_conviction_model(coldkey, netuid, now) { - model.roll_forward(now, UnlockRate::::get(), MaturityRate::::get()); - model.roll_forward_aggregate(now, UnlockRate::::get(), MaturityRate::::get()); - model.force_reduce_individual(amount, now); - Self::save_conviction_model(coldkey, netuid, &hotkey, model); - } - } - - /// Rolls the lock forward to now and persists it if the locked mass is zero. This is used when we want to - /// update the lock when a user stakes or unstakes. - pub fn cleanup_lock_if_zero(coldkey: &T::AccountId, netuid: NetUid) { - let now = Self::get_current_block_as_u64(); - - // Cleanup locks for the specific coldkey and hotkey - if let Some((hotkey, mut model)) = Self::read_conviction_model(coldkey, netuid, now) { - model.roll_forward(now, UnlockRate::::get(), MaturityRate::::get()); - Self::save_conviction_model(coldkey, netuid, &hotkey, model); - } - } - - /// Update the total lock for a hotkey on a subnet or create one if - /// it doesn't exist. - /// - /// Roll the existing hotkey lock forward to now, then add the - /// latest conviction and locked mass. - pub fn upsert_aggregate_lock( - coldkey: &T::AccountId, - hotkey: &T::AccountId, - netuid: NetUid, - amount: AlphaBalance, - ) { - let now = Self::get_current_block_as_u64(); - Self::add_aggregate_lock( - coldkey, - hotkey, - netuid, - LockState { - locked_mass: amount, - conviction: U64F64::saturating_from_num(0), - last_update: now, - }, - ); - } - - /// Merges an already-existing lock state into the aggregate lock bucket. - /// - /// This is used when lock state moves between keys, such as lock moves, stake - /// transfers, or coldkey swaps. Unlike `upsert_aggregate_lock`, this preserves - /// both locked mass and conviction from the moved lock because that conviction - /// was already earned before the aggregate bucket changed. - /// - /// Locks to the subnet owner hotkey are merged into `OwnerLock`; all other - /// locks are merged into the destination hotkey's perpetual or decaying bucket. - fn add_aggregate_lock( - coldkey: &T::AccountId, - hotkey: &T::AccountId, - netuid: NetUid, - added: LockState, - ) { - let now = Self::get_current_block_as_u64(); - let mut model = Self::read_conviction_model_for_hotkey(coldkey, netuid, hotkey, now); - model.roll_forward_aggregate(now, UnlockRate::::get(), MaturityRate::::get()); - model.add_to_aggregate(&added); - model.roll_forward_aggregate(now, UnlockRate::::get(), MaturityRate::::get()); - Self::save_conviction_model(coldkey, netuid, hotkey, model); - } - - /// Reduces locked mass and conviction from exactly one aggregate bucket. - fn reduce_aggregate_lock( - coldkey: &T::AccountId, - hotkey: &T::AccountId, - netuid: NetUid, - amount: AlphaBalance, - conviction: U64F64, - ) { - let now = Self::get_current_block_as_u64(); - let mut model = Self::read_conviction_model_for_hotkey(coldkey, netuid, hotkey, now); - model.roll_forward_aggregate(now, UnlockRate::::get(), MaturityRate::::get()); - model.reduce_aggregate(amount, conviction); - Self::save_conviction_model(coldkey, netuid, hotkey, model); - } - - /// Returns the total conviction for a hotkey on a subnet, - /// summed over all coldkeys that have locked to this hotkey. - pub fn hotkey_conviction(hotkey: &T::AccountId, netuid: NetUid) -> U64F64 { - let now = Self::get_current_block_as_u64(); - let unlock_rate = UnlockRate::::get(); - let maturity_rate = MaturityRate::::get(); - let perpetual_conviction = HotkeyLock::::get(netuid, hotkey) - .map(|lock| { - ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - false, - true, - ) - .0 - .conviction - }) - .unwrap_or_else(|| U64F64::saturating_from_num(0)); - let decaying_conviction = DecayingHotkeyLock::::get(netuid, hotkey) - .map(|lock| { - ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - false, - false, - ) - .0 - .conviction - }) - .unwrap_or_else(|| U64F64::saturating_from_num(0)); - let hotkey_conviction = perpetual_conviction.saturating_add(decaying_conviction); - if hotkey == &SubnetOwnerHotkey::::get(netuid) { - let owner_conviction = OwnerLock::::get(netuid) - .map(|lock| { - ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - true, - true, - ) - .0 - .conviction - }) - .unwrap_or_else(|| U64F64::saturating_from_num(0)); - let decaying_owner_conviction = DecayingOwnerLock::::get(netuid) - .map(|lock| { - ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - true, - false, - ) - .0 - .conviction - }) - .unwrap_or_else(|| U64F64::saturating_from_num(0)); - hotkey_conviction - .saturating_add(owner_conviction) - .saturating_add(decaying_owner_conviction) - } else { - hotkey_conviction - } - } - - /// Returns total rolled aggregate conviction across all hotkey and owner locks on a subnet. - pub fn get_total_conviction(netuid: NetUid) -> U64F64 { - let now = Self::get_current_block_as_u64(); - let unlock_rate = UnlockRate::::get(); - let maturity_rate = MaturityRate::::get(); - let hotkey_conviction = HotkeyLock::::iter_prefix(netuid) - .map(|(_hotkey, lock)| { - ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - false, - true, - ) - .0 - .conviction - }) - .fold(U64F64::saturating_from_num(0), |acc, conviction| { - acc.saturating_add(conviction) - }); - let decaying_hotkey_conviction = DecayingHotkeyLock::::iter_prefix(netuid) - .map(|(_hotkey, lock)| { - ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - false, - false, - ) - .0 - .conviction - }) - .fold(U64F64::saturating_from_num(0), |acc, conviction| { - acc.saturating_add(conviction) - }); - let owner_conviction = OwnerLock::::get(netuid) - .map(|lock| { - ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - true, - true, - ) - .0 - .conviction - }) - .unwrap_or_else(|| U64F64::saturating_from_num(0)); - let decaying_owner_conviction = DecayingOwnerLock::::get(netuid) - .map(|lock| { - ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - true, - false, - ) - .0 - .conviction - }) - .unwrap_or_else(|| U64F64::saturating_from_num(0)); - - hotkey_conviction - .saturating_add(decaying_hotkey_conviction) - .saturating_add(owner_conviction) - .saturating_add(decaying_owner_conviction) - } - - /// Finds the hotkey with the highest conviction on a given subnet. - pub fn subnet_king(netuid: NetUid) -> Option { - let now = Self::get_current_block_as_u64(); - let unlock_rate = UnlockRate::::get(); - let maturity_rate = MaturityRate::::get(); - let mut scores: BTreeMap = BTreeMap::new(); - - HotkeyLock::::iter_prefix(netuid).for_each(|(hotkey, lock)| { - let rolled = ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - false, - true, - ); - let entry = scores - .entry(hotkey) - .or_insert_with(|| U64F64::saturating_from_num(0)); - *entry = entry.saturating_add(rolled.0.conviction); - }); - DecayingHotkeyLock::::iter_prefix(netuid).for_each(|(hotkey, lock)| { - let rolled = ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - false, - false, - ); - let entry = scores - .entry(hotkey) - .or_insert_with(|| U64F64::saturating_from_num(0)); - *entry = entry.saturating_add(rolled.0.conviction); - }); - if let Some(lock) = OwnerLock::::get(netuid) { - let owner_hotkey = SubnetOwnerHotkey::::get(netuid); - let rolled = ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - true, - true, - ); - let entry = scores - .entry(owner_hotkey) - .or_insert_with(|| U64F64::saturating_from_num(0)); - *entry = entry.saturating_add(rolled.0.conviction); - } - if let Some(lock) = DecayingOwnerLock::::get(netuid) { - let owner_hotkey = SubnetOwnerHotkey::::get(netuid); - let rolled = ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - true, - false, - ); - let entry = scores - .entry(owner_hotkey) - .or_insert_with(|| U64F64::saturating_from_num(0)); - *entry = entry.saturating_add(rolled.0.conviction); - } - - scores - .into_iter() - .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(core::cmp::Ordering::Equal)) - .map(|(hotkey, _)| hotkey) - } - - /// Reassigns subnet ownership to the current lock-conviction leader when the subnet - /// is mature enough and enough conviction has accumulated. - /// - /// Ownership can change only after the subnet is at least [`ONE_YEAR`] old and the - /// total rolled aggregate conviction on the subnet is at least 10% of `SubnetAlphaOut`. - /// If those gates pass, the hotkey with the highest rolled aggregate conviction - /// becomes the subnet owner hotkey, and that hotkey's owning coldkey becomes the - /// subnet owner coldkey. The new owner hotkey's conviction is then progressed to - /// its current locked mass so the new owner starts with full owner conviction. - pub fn change_subnet_owner_if_needed(netuid: NetUid) { - // No outstanding alpha means there is no meaningful 10% conviction threshold. - let subnet_alpha_out = SubnetAlphaOut::::get(netuid); - if subnet_alpha_out.is_zero() { - return; - } - - // Ownership can only be reassigned after the subnet has aged for one year. - let now = Self::get_current_block_as_u64(); - let registered_at = NetworkRegisteredAt::::get(netuid); - if now < registered_at.saturating_add(ONE_YEAR) { - return; - } - - // Require total rolled aggregate conviction to be at least 10% of subnet alpha out. - let total_conviction = Self::get_total_conviction(netuid); - if total_conviction.saturating_mul(U64F64::saturating_from_num(10)) - < U64F64::saturating_from_num(u64::from(subnet_alpha_out)) - { - return; - } - - // Pick the hotkey with the highest rolled aggregate conviction. - let Some(king_hotkey) = Self::subnet_king(netuid) else { - return; - }; - - // The king hotkey must resolve to a real coldkey owner. - let new_owner_coldkey = Self::get_owning_coldkey_for_hotkey(&king_hotkey); - if new_owner_coldkey == DefaultAccount::::get() { - return; - } - - // If the winning hotkey already belongs to the current owner, nothing changes. - let current_owner_coldkey = SubnetOwner::::get(netuid); - if new_owner_coldkey == current_owner_coldkey { - return; - } - let old_owner_hotkey = SubnetOwnerHotkey::::get(netuid); - let unlock_rate = UnlockRate::::get(); - let maturity_rate = MaturityRate::::get(); - - // Register new owner as a neuron if not yet registered. - if Self::get_uid_for_net_and_hotkey(netuid, &king_hotkey).is_err() - && Self::register_neuron(netuid, &king_hotkey).is_err() - { - return; - } - - // Move aggregate buckets using the hotkey's new role. - if let Some(owner_lock) = OwnerLock::::take(netuid) { - let moved_owner_lock = ConvictionModel::roll_forward_lock( - owner_lock, - now, - unlock_rate, - maturity_rate, - true, - true, - ); - let current = HotkeyLock::::get(netuid, &old_owner_hotkey) - .map(|lock| { - ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - false, - true, - ) - .0 - }) - .unwrap_or_else(|| Self::empty_lock(now)); - Self::insert_hotkey_lock_state( - netuid, - &old_owner_hotkey, - LockState { - locked_mass: current - .locked_mass - .saturating_add(moved_owner_lock.0.locked_mass), - conviction: current - .conviction - .saturating_add(moved_owner_lock.0.conviction), - last_update: now, - }, - ); - } - if let Some(owner_lock) = DecayingOwnerLock::::take(netuid) { - let moved_owner_lock = ConvictionModel::roll_forward_lock( - owner_lock, - now, - unlock_rate, - maturity_rate, - true, - false, - ); - let current = DecayingHotkeyLock::::get(netuid, &old_owner_hotkey) - .map(|lock| { - ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - false, - false, - ) - .0 - }) - .unwrap_or_else(|| Self::empty_lock(now)); - Self::insert_decaying_hotkey_lock_state( - netuid, - &old_owner_hotkey, - LockState { - locked_mass: current - .locked_mass - .saturating_add(moved_owner_lock.0.locked_mass), - conviction: current - .conviction - .saturating_add(moved_owner_lock.0.conviction), - last_update: now, - }, - ); - } - if let Some(king_lock) = HotkeyLock::::take(netuid, &king_hotkey) { - let moved_king_lock = ConvictionModel::roll_forward_lock( - king_lock, - now, - unlock_rate, - maturity_rate, - false, - true, - ); - let current = OwnerLock::::get(netuid) - .map(|lock| { - ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - true, - true, - ) - .0 - }) - .unwrap_or_else(|| Self::empty_lock(now)); - Self::insert_owner_lock_state( - netuid, - ConvictionModel::roll_forward_lock( - LockState { - locked_mass: current - .locked_mass - .saturating_add(moved_king_lock.0.locked_mass), - conviction: current - .conviction - .saturating_add(moved_king_lock.0.conviction), - last_update: now, - }, - now, - unlock_rate, - maturity_rate, - true, - true, - ) - .0, - ); - } - if let Some(king_lock) = DecayingHotkeyLock::::take(netuid, &king_hotkey) { - let moved_king_lock = ConvictionModel::roll_forward_lock( - king_lock, - now, - unlock_rate, - maturity_rate, - false, - false, - ); - let current = DecayingOwnerLock::::get(netuid) - .map(|lock| { - ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - true, - false, - ) - .0 - }) - .unwrap_or_else(|| Self::empty_lock(now)); - Self::insert_decaying_owner_lock_state( - netuid, - ConvictionModel::roll_forward_lock( - LockState { - locked_mass: current - .locked_mass - .saturating_add(moved_king_lock.0.locked_mass), - conviction: current - .conviction - .saturating_add(moved_king_lock.0.conviction), - last_update: now, - }, - now, - unlock_rate, - maturity_rate, - true, - false, - ) - .0, - ); - } - - // Reassign subnet owner coldkey and owner hotkey. - SubnetOwner::::insert(netuid, new_owner_coldkey.clone()); - SubnetOwnerHotkey::::insert(netuid, king_hotkey.clone()); - Self::deposit_event(Event::SubnetOwnerChanged { - netuid, - old_coldkey: current_owner_coldkey, - new_coldkey: new_owner_coldkey, - }); - } - - /// Ensure the coldkey does not have an active lock on any subnets. - pub fn ensure_no_active_locks(coldkey: &T::AccountId) -> Result<(), Error> { - let now = Self::get_current_block_as_u64(); - let unlock_rate = UnlockRate::::get(); - let maturity_rate = MaturityRate::::get(); - - for ((netuid, hotkey), lock) in Lock::::iter_prefix((coldkey,)) { - let rolled = ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - Self::is_subnet_owner_hotkey(netuid, &hotkey), - Self::is_perpetual_lock(coldkey, netuid), - ); - if rolled.0.locked_mass > AlphaBalance::ZERO { - return Err(Error::::ActiveLockExists); - } - } - - Ok(()) - } - - /// Transfers the lock from one coldkey to another for all subnets. This is used when a - /// user swaps their coldkey and we want to preserve their locks. - /// - /// The hotkey and netuid remain the same, only the coldkey changes. - /// - /// The new coldkey must have no active locks, so we can transfer the locks - /// "as is" without rolling them forward and the - /// HotkeyLock map does not change (because it only contains totals, not individual coldkey locks). - pub fn swap_coldkey_locks( - old_coldkey: &T::AccountId, - new_coldkey: &T::AccountId, - ) -> DispatchResult { - Self::ensure_no_active_locks(new_coldkey)?; - - let mut locks_to_transfer: Vec<(NetUid, T::AccountId, LockState)> = Vec::new(); - let now = Self::get_current_block_as_u64(); - let unlock_rate = UnlockRate::::get(); - let maturity_rate = MaturityRate::::get(); - let new_coldkey_rejects_locked_alpha = Self::account_rejects_locked_alpha(new_coldkey); - let decaying_locks_to_transfer: Vec<(NetUid, bool)> = - DecayingLock::::iter_prefix(old_coldkey).collect(); - - // Gather locks for old coldkey - for ((netuid, hotkey), lock) in Lock::::iter_prefix((old_coldkey,)) { - locks_to_transfer.push((netuid, hotkey, lock)); - } - - let mut rolled_locks_to_transfer: Vec<(NetUid, T::AccountId, LockState, bool)> = Vec::new(); - for (netuid, hotkey, lock) in locks_to_transfer { - let perpetual_lock = decaying_locks_to_transfer - .iter() - .any(|(decaying_netuid, decaying)| *decaying_netuid == netuid && !*decaying); - let (old_lock, _) = ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - Self::is_subnet_owner_hotkey(netuid, &hotkey), - perpetual_lock, - ); - Self::ensure_can_receive_locked_alpha_with_flag( - new_coldkey_rejects_locked_alpha, - old_lock.locked_mass, - )?; - rolled_locks_to_transfer.push((netuid, hotkey, old_lock, perpetual_lock)); - } - - // Remove old locks and reduce old aggregate buckets before moving the - // perpetual-lock flags; aggregate selection depends on the old flag. - for (netuid, hotkey, old_lock, _) in rolled_locks_to_transfer.iter() { - Lock::::remove((old_coldkey.clone(), *netuid, hotkey.clone())); - Self::maybe_remove_locking_coldkey(hotkey, *netuid, old_coldkey); - Self::reduce_aggregate_lock( - old_coldkey, - hotkey, - *netuid, - old_lock.locked_mass, - old_lock.conviction, - ); - } - - for (netuid, _) in decaying_locks_to_transfer { - if let Some(decaying) = DecayingLock::::take(old_coldkey, netuid) { - DecayingLock::::insert(new_coldkey, netuid, decaying); - } - } - - let flags = AccountFlags::::get(old_coldkey); - AccountFlags::::remove(old_coldkey); - if flags != 0 { - AccountFlags::::insert(new_coldkey, flags); - } else { - AccountFlags::::remove(new_coldkey); - } - - // Insert locks for the new coldkey and add to the destination aggregate - // buckets after the flags have moved. - for (netuid, hotkey, old_lock, perpetual_lock) in rolled_locks_to_transfer { - let new_lock = ConvictionModel::roll_forward_lock( - old_lock.clone(), - now, - unlock_rate, - maturity_rate, - Self::is_subnet_owner_hotkey(netuid, &hotkey), - perpetual_lock, - ) - .0; - Self::insert_lock_state(new_coldkey, netuid, &hotkey, new_lock.clone()); - Self::add_aggregate_lock(new_coldkey, &hotkey, netuid, new_lock); - } - - Ok(()) - } - - /// Swap all locks made to the old_hotkey to new_hotkey on all netuids - /// - /// There is no need to roll the locks, they can be just copied "as is": - /// The lock relation between coldkeys and hotkey is 1:1, so if old hotkey has a - /// coldkey locking to it, then the same coldkey cannot lock to the new hotkey. - /// And in reverse: If a coldkey is locking to the new hotkey, it will not appear - /// in the transfer list because it does not lock to the old hotkey. - /// - /// Conviction is not reset because the hotkey ownership does not change, it's still - /// the same hotkey owner who will own the new hotkey. - pub fn swap_hotkey_locks(old_hotkey: &T::AccountId, new_hotkey: &T::AccountId) -> (u64, u64) { - Self::swap_hotkey_locks_for_netuids(old_hotkey, new_hotkey, Self::get_all_subnet_netuids()) - } - - /// Swap locks made to the old_hotkey to new_hotkey on one netuid. - pub fn swap_hotkey_locks_on_subnet( - old_hotkey: &T::AccountId, - new_hotkey: &T::AccountId, - netuid: NetUid, - ) -> (u64, u64) { - Self::swap_hotkey_locks_for_netuids(old_hotkey, new_hotkey, vec![netuid]) - } - - fn swap_hotkey_locks_for_netuids( - old_hotkey: &T::AccountId, - new_hotkey: &T::AccountId, - netuids: Vec, - ) -> (u64, u64) { - let mut locks_to_transfer: Vec<(T::AccountId, NetUid, LockState)> = Vec::new(); - let mut netuids_to_transfer: Vec<(NetUid, bool, bool)> = Vec::new(); - let mut reads: u64 = 0; - let mut writes: u64 = 0; - - for netuid in netuids.iter().copied() { - let old_is_owner_hotkey = Self::is_subnet_owner_hotkey(netuid, old_hotkey); - let new_is_owner_hotkey = Self::is_subnet_owner_hotkey(netuid, new_hotkey); - let has_hotkey_lock = HotkeyLock::::contains_key(netuid, old_hotkey); - let has_decaying_hotkey_lock = - DecayingHotkeyLock::::contains_key(netuid, old_hotkey); - let has_owner_lock = old_is_owner_hotkey && OwnerLock::::contains_key(netuid); - let has_decaying_owner_lock = - old_is_owner_hotkey && DecayingOwnerLock::::contains_key(netuid); - - if old_is_owner_hotkey - || new_is_owner_hotkey - || has_hotkey_lock - || has_decaying_hotkey_lock - || has_owner_lock - || has_decaying_owner_lock - { - netuids_to_transfer.push(( - netuid, - old_is_owner_hotkey, - old_is_owner_hotkey || new_is_owner_hotkey, - )); - } - reads = reads.saturating_add(5); - } - - // Build a concrete transfer list from the hotkey-to-coldkey index. - // The index can contain stale coldkeys, so only locks that still exist - // are carried forward; missing locks are pruned from the index. - for (netuid, _, _) in &netuids_to_transfer { - for (coldkey, _) in LockingColdkeys::::iter_prefix((*netuid, old_hotkey)) { - if let Some(lock) = Lock::::get((coldkey.clone(), *netuid, old_hotkey.clone())) { - locks_to_transfer.push((coldkey, *netuid, lock)); - } else { - Self::maybe_remove_locking_coldkey(old_hotkey, *netuid, &coldkey); - writes = writes.saturating_add(1); - } - reads = reads.saturating_add(1); - } - } - - for (coldkey, netuid, lock) in locks_to_transfer { - let now = Self::get_current_block_as_u64(); - let unlock_rate = UnlockRate::::get(); - let maturity_rate = MaturityRate::::get(); - let old_owner_lock = netuids_to_transfer - .iter() - .any(|(rebuild_netuid, is_owner, _)| *rebuild_netuid == netuid && *is_owner); - let new_owner_lock = netuids_to_transfer - .iter() - .any(|(rebuild_netuid, _, is_owner)| *rebuild_netuid == netuid && *is_owner); - let perpetual_lock = Self::is_perpetual_lock(&coldkey, netuid); - let rolled = ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - old_owner_lock, - perpetual_lock, - ) - .0; - let moved = ConvictionModel::roll_forward_lock( - rolled, - now, - unlock_rate, - maturity_rate, - new_owner_lock, - perpetual_lock, - ) - .0; - Lock::::remove((coldkey.clone(), netuid, old_hotkey.clone())); - Self::maybe_remove_locking_coldkey(old_hotkey, netuid, &coldkey); - Self::insert_lock_state(&coldkey, netuid, new_hotkey, moved); - writes = writes.saturating_add(2); - } - - for (netuid, old_was_owner, new_is_owner) in netuids_to_transfer { - let now = Self::get_current_block_as_u64(); - let unlock_rate = UnlockRate::::get(); - let maturity_rate = MaturityRate::::get(); - let moved_perpetual_lock = if old_was_owner { - OwnerLock::::take(netuid).map(|lock| { - ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - true, - true, - ) - .0 - }) - } else { - HotkeyLock::::take(netuid, old_hotkey).map(|lock| { - ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - false, - true, - ) - .0 - }) - }; - let moved_decaying_lock = if old_was_owner { - DecayingOwnerLock::::take(netuid).map(|lock| { - ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - true, - false, - ) - .0 - }) - } else { - DecayingHotkeyLock::::take(netuid, old_hotkey).map(|lock| { - ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - false, - false, - ) - .0 - }) - }; - - if let Some(lock) = moved_perpetual_lock { - if new_is_owner { - Self::insert_owner_lock_state( - netuid, - ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - true, - true, - ) - .0, - ); - } else { - Self::insert_hotkey_lock_state( - netuid, - new_hotkey, - ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - false, - true, - ) - .0, - ); - } - } - if let Some(lock) = moved_decaying_lock { - if new_is_owner { - Self::insert_decaying_owner_lock_state( - netuid, - ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - true, - false, - ) - .0, - ); - } else { - Self::insert_decaying_hotkey_lock_state( - netuid, - new_hotkey, - ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - false, - false, - ) - .0, - ); - } - } - writes = writes.saturating_add(6); - } - (reads, writes) - } - - /// Conviction is only preserved when a lock moves between hotkeys owned by - /// the same coldkey; moving it to a differently owned hotkey forfeits it. - /// Shared by `do_move_lock` and `transfer_lock`. - fn conviction_survives_hotkey_change( - source_hotkey: &T::AccountId, - destination_hotkey: &T::AccountId, - ) -> bool { - Self::get_owning_coldkey_for_hotkey(source_hotkey) - == Self::get_owning_coldkey_for_hotkey(destination_hotkey) - } - - /// Moves lock from one hotkey to another and clears conviction - /// - /// The lock is rolled forward to the current block before switching the - /// associated hotkey so that the lock stays mathematically correct and - /// preserves current decayed locked mass. - /// - /// The conviction is reset to zero if the destination and source hotkeys - /// are owned by different coldkeys, otherwise it is preserved. - pub fn do_move_lock( - coldkey: &T::AccountId, - destination_hotkey: &T::AccountId, - netuid: NetUid, - ) -> DispatchResult { - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); - ensure!( - Self::hotkey_account_exists(destination_hotkey), - Error::::HotKeyAccountNotExists - ); - let now = Self::get_current_block_as_u64(); - - match Self::read_conviction_model(coldkey, netuid, now) { - Some((origin_hotkey, mut model)) => { - let unlock_rate = UnlockRate::::get(); - let maturity_rate = MaturityRate::::get(); - model.roll_forward(now, unlock_rate, maturity_rate); - let mut lock = model.individual_lock().clone(); - let removed = lock.clone(); - - if !Self::conviction_survives_hotkey_change(&origin_hotkey, destination_hotkey) { - lock.conviction = U64F64::saturating_from_num(0); - } - lock = ConvictionModel::roll_forward_lock( - lock, - now, - unlock_rate, - maturity_rate, - Self::is_subnet_owner_hotkey(netuid, destination_hotkey), - Self::is_perpetual_lock(coldkey, netuid), - ) - .0; - - Lock::::remove((coldkey.clone(), netuid, origin_hotkey.clone())); - Self::maybe_remove_locking_coldkey(&origin_hotkey, netuid, coldkey); - Self::insert_lock_state(coldkey, netuid, destination_hotkey, lock.clone()); - Self::reduce_aggregate_lock( - coldkey, - &origin_hotkey, - netuid, - removed.locked_mass, - removed.conviction, - ); - Self::add_aggregate_lock(coldkey, destination_hotkey, netuid, lock); - - Self::deposit_event(Event::LockMoved { - coldkey: coldkey.clone(), - origin_hotkey, - destination_hotkey: destination_hotkey.clone(), - netuid, - }); - Ok(()) - } - None => Err(Error::::NoExistingLock.into()), - } - } - - pub fn auto_lock_owner_cut(netuid: NetUid, amount: AlphaBalance) { - if !OwnerCutAutoLockEnabled::::get(netuid) { - return; - } - - let subnet_owner_coldkey = Self::get_subnet_owner(netuid); - - // Determine the lock hotkey. If no locks exist, assign subnet owner's hotkey, otherwise - // auto-lock to existing lock hotkey - let lock_hotkey = if let Some((existing_hotkey, _model)) = Self::read_conviction_model( - &subnet_owner_coldkey, - netuid, - Self::get_current_block_as_u64(), - ) { - existing_hotkey - } else { - SubnetOwnerHotkey::::get(netuid) - }; - - // Ignore the result. It may only fail if amount is zero, which is OK to ignore because nothing - // needs to happen in that case - let _ = Self::do_lock_stake(&subnet_owner_coldkey, netuid, &lock_hotkey, amount); - } - - /// When locked stake is transfered, the lock should follow the stake - /// - /// First, this function rolls the lock forward and checks if amount is over available - /// stake and if it is, the stake that's over the available amount on the destination - /// coldkey is locked in the same way as the original stake: the lock follows the stake - /// to `destination_hotkey` (which, for plain stake transfers, is the same hotkey the - /// stake was locked to). Conviction is moved proportionally to the moved locked amount - /// of alpha. For example, if 20% of locked alpha is moved, then also 20% of conviction - /// is moved. If the source and destination hotkeys are owned by different coldkeys, - /// the moved conviction is reset to zero, mirroring `do_move_lock`. - pub fn transfer_lock( - origin_coldkey: &T::AccountId, - destination_coldkey: &T::AccountId, - destination_hotkey: &T::AccountId, - netuid: NetUid, - amount: AlphaBalance, - ) -> DispatchResult { - let now = Self::get_current_block_as_u64(); - - // If no actual transfer happens, this is ok - if origin_coldkey == destination_coldkey || amount.is_zero() { - return Ok(()); - } - - // Read total alpha of the coldkey on this netuid. Do not check if total alpha is - // lower than amount transferred, this is responsibility of a higher level, this - // function needs to act protectively. - let total_alpha = Self::total_coldkey_alpha_on_subnet(origin_coldkey, netuid); - let mut remaining_to_transfer = amount; - - // Read the locks for source and destination coldkey (if exist) and roll forward - let Some((source_hotkey, mut source_model)) = - Self::read_conviction_model(origin_coldkey, netuid, now) - else { - return Ok(()); - }; - - let unlock_rate = UnlockRate::::get(); - let maturity_rate = MaturityRate::::get(); - source_model.roll_forward(now, unlock_rate, maturity_rate); - let mut source_lock = source_model.individual_lock().clone(); - let maybe_destination_lock = Self::read_conviction_model(destination_coldkey, netuid, now) - .map(|(hotkey, mut model)| { - model.roll_forward(now, unlock_rate, maturity_rate); - (hotkey, model.individual_lock().clone()) - }); - - let destination_lock_hotkey = maybe_destination_lock - .as_ref() - .map(|(hotkey, _)| hotkey.clone()) - .unwrap_or_else(|| destination_hotkey.clone()); - let mut destination_lock = maybe_destination_lock - .as_ref() - .map(|(_, lock)| lock.clone()) - .unwrap_or(LockState { - locked_mass: AlphaBalance::ZERO, - conviction: U64F64::saturating_from_num(0), - last_update: now, - }); - - // Calculate available stake by subtracting locked_mass from total alpha. - let unavailable = source_lock.locked_mass; - let available_stake = total_alpha.saturating_sub(unavailable); - - // Reduce remaining_to_transfer by min(remaining_to_transfer, available stake) - let available_transfer = remaining_to_transfer.min(available_stake); - remaining_to_transfer = remaining_to_transfer.saturating_sub(available_transfer); - - // If result is non-zero, check the hotkey match between source and destination coldkey locks - // (if destination coldkey lock exists). If no match, error out with LockHotkeyMismatch, otherwise, - // reduce remaining_to_transfer by min(remaining_to_transfer, locked_mass), reduce locked_mass on - // the source coldkey by the same amount, increase locked_mass on the destination coldkey by the - // same amount, reduce conviction on the source coldkey proportionally, and increase conviction - // on the destination coldkey proportionally. - let mut locked_transfer = AlphaBalance::ZERO; - let mut conviction_transfer = U64F64::saturating_from_num(0); - let mut received_conviction = U64F64::saturating_from_num(0); - if !remaining_to_transfer.is_zero() { - if let Some((existing_hotkey, _)) = maybe_destination_lock.as_ref() { - ensure!( - existing_hotkey == destination_hotkey, - Error::::LockHotkeyMismatch - ); - } - - locked_transfer = remaining_to_transfer.min(source_lock.locked_mass); - conviction_transfer = if locked_transfer.is_zero() || source_lock.locked_mass.is_zero() - { - U64F64::saturating_from_num(0) - } else { - let locked_transfer = U64F64::saturating_from_num(locked_transfer.to_u64()); - let source_locked = U64F64::saturating_from_num(source_lock.locked_mass.to_u64()); - let transferred_proportion = locked_transfer.safe_div(source_locked); - source_lock - .conviction - .saturating_mul(transferred_proportion) - }; - - // Conviction only follows the lock when the destination hotkey is owned - // by the same coldkey as the source hotkey; otherwise it is forfeited, - // mirroring `do_move_lock`. - received_conviction = if Self::conviction_survives_hotkey_change( - &source_hotkey, - &destination_lock_hotkey, - ) { - conviction_transfer - } else { - U64F64::saturating_from_num(0) - }; - - source_lock.locked_mass = source_lock.locked_mass.saturating_sub(locked_transfer); - source_lock.conviction = source_lock.conviction.saturating_sub(conviction_transfer); - destination_lock.locked_mass = - destination_lock.locked_mass.saturating_add(locked_transfer); - destination_lock.conviction = destination_lock - .conviction - .saturating_add(received_conviction); - } - Self::ensure_can_receive_locked_alpha(destination_coldkey, locked_transfer)?; - - source_lock = ConvictionModel::roll_forward_lock( - source_lock, - now, - unlock_rate, - maturity_rate, - Self::is_subnet_owner_hotkey(netuid, &source_hotkey), - Self::is_perpetual_lock(origin_coldkey, netuid), - ) - .0; - destination_lock = ConvictionModel::roll_forward_lock( - destination_lock, - now, - unlock_rate, - maturity_rate, - Self::is_subnet_owner_hotkey(netuid, &destination_lock_hotkey), - Self::is_perpetual_lock(destination_coldkey, netuid), - ) - .0; - - // Upsert updated locks (only once per this fn) even if there were no updates because - // of roll-forward - Self::insert_lock_state(origin_coldkey, netuid, &source_hotkey, source_lock); - Self::insert_lock_state( - destination_coldkey, - netuid, - &destination_lock_hotkey, - destination_lock, - ); - if !locked_transfer.is_zero() { - Self::reduce_aggregate_lock( - origin_coldkey, - &source_hotkey, - netuid, - locked_transfer, - conviction_transfer, - ); - Self::add_aggregate_lock( - destination_coldkey, - &destination_lock_hotkey, - netuid, - LockState { - locked_mass: locked_transfer, - conviction: received_conviction, - last_update: now, - }, - ); - } - - Ok(()) - } - - /// Removes `Lock` entries for `netuid`, resuming from `LastKeptRawKey` when weight is limited. - pub fn remove_network_lock( - netuid: NetUid, - weight_meter: &mut WeightMeter, - last_key: Option>, - ) -> (bool, Option>) { - let iter = match last_key { - Some(key) => Lock::::iter_from(key), - None => Lock::::iter(), - }; - - let (read_all, last_item) = Self::remove_storage_entries_for_netuid( - weight_meter, - iter, - |((_, this_netuid, _), _)| *this_netuid == netuid, - |((coldkey, _this_netuid, hotkey), _)| (coldkey, hotkey), - |(coldkey, hotkey)| Lock::::remove((coldkey.clone(), netuid, hotkey.clone())), - 1, - ); - - ( - read_all, - last_item.map(|((coldkey, _, hotkey), _)| { - Lock::::hashed_key_for((&coldkey, netuid, &hotkey)) - }), - ) - } - - /// Removes `DecayingLock` entries for `netuid`, resuming from `LastKeptRawKey` when weight is limited. - pub fn remove_network_decaying_lock( - netuid: NetUid, - weight_meter: &mut WeightMeter, - last_key: Option>, - ) -> (bool, Option>) { - let iter = match last_key { - Some(raw_key) => DecayingLock::::iter_from(raw_key), - None => DecayingLock::::iter(), - }; - - let (read_all, last_item) = Self::remove_storage_entries_for_netuid( - weight_meter, - iter, - |(_, nu, _)| *nu == netuid, - |(cold, nu, _)| (cold, nu), - |(cold, netuid)| DecayingLock::::remove(cold, netuid), - 1, - ); - - ( - read_all, - last_item.map(|(cold, nu, _)| DecayingLock::::hashed_key_for(&cold, nu)), - ) - } -} diff --git a/pallets/subtensor/src/staking/lock/conviction_model.rs b/pallets/subtensor/src/staking/lock/conviction_model.rs new file mode 100644 index 0000000000..2ea5bc57f7 --- /dev/null +++ b/pallets/subtensor/src/staking/lock/conviction_model.rs @@ -0,0 +1,474 @@ +//! Conviction lock types: [`LockState`], [`RollDelta`], and [`ConvictionModel`]. +//! +//! These types model exponentially decaying locked alpha and its matured +//! conviction score. Aggregate buckets (owner vs general, perpetual vs decaying) +//! are updated together so hotkey / owner totals stay consistent with individuals. +use super::*; +use codec::{Decode, DecodeWithMemTracking, Encode}; +use safe_math::FixedExt; +use scale_info::TypeInfo; +use sp_std::ops::Neg; +use substrate_fixed::transcendental::exp; +use substrate_fixed::types::{I64F64, U64F64}; + +pub const ONE_YEAR: u64 = 7200 * 365 + 1800; +pub const LOCK_STATE_ZERO_THRESHOLD: u64 = 100; + +/// Exponential lock state for a coldkey on a subnet. +#[crate::freeze_struct("1f6be20a66128b8d")] +#[derive(Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug, TypeInfo)] +pub struct LockState { + /// Exponentially decaying locked amount. + pub locked_mass: AlphaBalance, + /// Matured decaying score (integral of locked_mass over time). + pub conviction: U64F64, + /// Block number of last roll-forward. + pub last_update: u64, +} + +impl LockState { + pub fn is_zero(&self) -> bool { + self.locked_mass < AlphaBalance::from(LOCK_STATE_ZERO_THRESHOLD) + && self.conviction < U64F64::saturating_from_num(LOCK_STATE_ZERO_THRESHOLD) + } +} + +/// Change produced by rolling a lock forward. Locked mass only ever +/// decreases, but conviction can move either way (it matures upward from +/// locked mass and decays downward once the mass is gone), so its change is +/// carried as separate unsigned growth/decay components. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct RollDelta { + pub locked_mass_delta: AlphaBalance, + pub conviction_decay: U64F64, + pub conviction_growth: U64F64, +} + +impl RollDelta { + pub fn zero() -> Self { + Self { + locked_mass_delta: AlphaBalance::ZERO, + conviction_decay: U64F64::saturating_from_num(0), + conviction_growth: U64F64::saturating_from_num(0), + } + } + + pub fn is_zero(&self) -> bool { + self.locked_mass_delta.is_zero() + && self.conviction_decay == U64F64::saturating_from_num(0) + && self.conviction_growth == U64F64::saturating_from_num(0) + } +} + +/// In-memory conviction lock: individual coldkey state plus four aggregate buckets +/// (owner/general × perpetual/decaying), with roll/add/reduce primitives. +/// +/// This model has one individual lock state, which relates to the stake owner +/// (locking coldkey) lock and 4 aggregates that are maintained in operations. +pub struct ConvictionModel { + /// Whether this model's individual lock targets the subnet owner hotkey. + owner_lock: bool, + /// Whether this model's individual lock uses the non-decaying lock mode. + perpetual_lock: bool, + /// Individual stake owner coldkey lock + individual_lock: LockState, + individual_lock_dirty: bool, + /// Perpetual non-owner aggregate + agg_perpetual_general: LockState, + agg_perpetual_general_dirty: bool, + /// Decaying non-owner aggregate + agg_decaying_general: LockState, + agg_decaying_general_dirty: bool, + /// Perpetual owner aggregate + agg_perpetual_owner: LockState, + agg_perpetual_owner_dirty: bool, + /// Decaying owner aggregate + agg_decaying_owner: LockState, + agg_decaying_owner_dirty: bool, +} + +impl ConvictionModel { + pub fn new( + owner_lock: bool, + perpetual_lock: bool, + individual_lock: LockState, + agg_perpetual_general: LockState, + agg_decaying_general: LockState, + agg_perpetual_owner: LockState, + agg_decaying_owner: LockState, + ) -> Self { + Self { + owner_lock, + perpetual_lock, + individual_lock, + individual_lock_dirty: false, + agg_perpetual_general, + agg_perpetual_general_dirty: false, + agg_decaying_general, + agg_decaying_general_dirty: false, + agg_perpetual_owner, + agg_perpetual_owner_dirty: false, + agg_decaying_owner, + agg_decaying_owner_dirty: false, + } + } + + pub fn individual_lock(&self) -> &LockState { + &self.individual_lock + } + + pub fn agg_perpetual_general(&self) -> &LockState { + &self.agg_perpetual_general + } + + pub fn agg_decaying_general(&self) -> &LockState { + &self.agg_decaying_general + } + + pub fn agg_perpetual_owner(&self) -> &LockState { + &self.agg_perpetual_owner + } + + pub fn agg_decaying_owner(&self) -> &LockState { + &self.agg_decaying_owner + } + + pub fn aggregate_lock(&self) -> &LockState { + if self.owner_lock && self.perpetual_lock { + &self.agg_perpetual_owner + } else if self.owner_lock { + &self.agg_decaying_owner + } else if self.perpetual_lock { + &self.agg_perpetual_general + } else { + &self.agg_decaying_general + } + } + + pub fn individual_lock_dirty(&self) -> bool { + self.individual_lock_dirty + } + + pub fn agg_perpetual_general_dirty(&self) -> bool { + self.agg_perpetual_general_dirty + } + + pub fn agg_decaying_general_dirty(&self) -> bool { + self.agg_decaying_general_dirty + } + + pub fn agg_perpetual_owner_dirty(&self) -> bool { + self.agg_perpetual_owner_dirty + } + + pub fn agg_decaying_owner_dirty(&self) -> bool { + self.agg_decaying_owner_dirty + } + + pub fn merge(&mut self, conv: &ConvictionModel) { + self.individual_lock = Self::merge_lock(&self.individual_lock, &conv.individual_lock); + self.individual_lock_dirty = true; + self.agg_perpetual_general = + Self::merge_lock(&self.agg_perpetual_general, &conv.agg_perpetual_general); + self.agg_perpetual_general_dirty = true; + self.agg_decaying_general = + Self::merge_lock(&self.agg_decaying_general, &conv.agg_decaying_general); + self.agg_decaying_general_dirty = true; + self.agg_perpetual_owner = + Self::merge_lock(&self.agg_perpetual_owner, &conv.agg_perpetual_owner); + self.agg_perpetual_owner_dirty = true; + self.agg_decaying_owner = + Self::merge_lock(&self.agg_decaying_owner, &conv.agg_decaying_owner); + self.agg_decaying_owner_dirty = true; + } + + pub fn set_individual_lock(&mut self, lock: LockState) { + self.individual_lock = lock; + self.individual_lock_dirty = true; + } + + pub fn set_rolled_individual_lock( + &mut self, + lock: LockState, + now: u64, + unlock_rate: u64, + maturity_rate: u64, + ) { + self.individual_lock = Self::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + self.owner_lock, + self.perpetual_lock, + ) + .0; + self.individual_lock_dirty = true; + } + + pub fn roll_forward(&mut self, now: u64, unlock_rate: u64, maturity_rate: u64) { + let (rolled_individual_lock, roll_delta) = Self::roll_forward_lock( + self.individual_lock.clone(), + now, + unlock_rate, + maturity_rate, + self.owner_lock, + self.perpetual_lock, + ); + self.individual_lock = rolled_individual_lock; + self.individual_lock_dirty = true; + if !roll_delta.is_zero() { + self.apply_roll_delta_to_aggregate(roll_delta, now); + } else { + self.roll_forward_aggregate(now, unlock_rate, maturity_rate); + } + } + + pub fn roll_forward_aggregate(&mut self, now: u64, unlock_rate: u64, maturity_rate: u64) { + let owner_lock = self.owner_lock; + let perpetual_lock = self.perpetual_lock; + let (aggregate, aggregate_dirty) = self.aggregate_mut(); + *aggregate = Self::roll_forward_lock( + aggregate.clone(), + now, + unlock_rate, + maturity_rate, + owner_lock, + perpetual_lock, + ) + .0; + *aggregate_dirty = true; + } + + pub fn add_to_aggregate(&mut self, added: &LockState) { + let (aggregate, aggregate_dirty) = self.aggregate_mut(); + *aggregate = Self::merge_lock(aggregate, added); + *aggregate_dirty = true; + } + + pub fn reduce_aggregate(&mut self, locked_mass: AlphaBalance, conviction: U64F64) { + let (aggregate, aggregate_dirty) = self.aggregate_mut(); + *aggregate = Self::reduce_lock(aggregate, locked_mass, conviction); + *aggregate_dirty = true; + } + + fn apply_roll_delta_to_aggregate(&mut self, roll_delta: RollDelta, now: u64) { + let (aggregate, aggregate_dirty) = self.aggregate_mut(); + *aggregate = Self::reduce_lock( + aggregate, + roll_delta.locked_mass_delta, + roll_delta.conviction_decay, + ); + // Conviction matured by the individual lock must be credited to the + // aggregate here: bumping last_update below means the aggregate's own + // roll-forward will never cover this window, so dropping the growth + // (as a saturating decrease-only delta used to) permanently + // understates aggregate conviction. + aggregate.conviction = aggregate + .conviction + .saturating_add(roll_delta.conviction_growth); + aggregate.last_update = now; + *aggregate_dirty = true; + } + + pub fn reduce(&mut self, locked_mass: AlphaBalance, conviction: U64F64) { + self.individual_lock = Self::reduce_lock(&self.individual_lock, locked_mass, conviction); + self.individual_lock_dirty = true; + + let (aggregate, aggregate_dirty) = self.aggregate_mut(); + *aggregate = Self::reduce_lock(aggregate, locked_mass, conviction); + *aggregate_dirty = true; + } + + pub fn force_reduce_individual(&mut self, amount: AlphaBalance, now: u64) { + let rolled = self.individual_lock.clone(); + let new_locked_mass = rolled.locked_mass.saturating_sub(amount); + let locked_mass_diff = rolled.locked_mass.saturating_sub(new_locked_mass); + + let conviction_diff = if new_locked_mass.is_zero() { + self.individual_lock = LockState { + locked_mass: AlphaBalance::ZERO, + conviction: U64F64::saturating_from_num(0), + last_update: now, + }; + rolled.conviction + } else { + let removed_proportion = U64F64::saturating_from_num(u64::from(amount)) + .safe_div(U64F64::saturating_from_num(u64::from(rolled.locked_mass))); + let new_conviction = rolled + .conviction + .saturating_mul(U64F64::saturating_from_num(1).saturating_sub(removed_proportion)); + self.individual_lock = LockState { + locked_mass: new_locked_mass, + conviction: new_conviction, + last_update: now, + }; + rolled.conviction.saturating_sub(new_conviction) + }; + self.individual_lock_dirty = true; + + self.reduce_aggregate(locked_mass_diff, conviction_diff); + } + + fn aggregate_mut(&mut self) -> (&mut LockState, &mut bool) { + if self.owner_lock && self.perpetual_lock { + ( + &mut self.agg_perpetual_owner, + &mut self.agg_perpetual_owner_dirty, + ) + } else if self.owner_lock { + ( + &mut self.agg_decaying_owner, + &mut self.agg_decaying_owner_dirty, + ) + } else if self.perpetual_lock { + ( + &mut self.agg_perpetual_general, + &mut self.agg_perpetual_general_dirty, + ) + } else { + ( + &mut self.agg_decaying_general, + &mut self.agg_decaying_general_dirty, + ) + } + } + + fn merge_lock(lhs: &LockState, rhs: &LockState) -> LockState { + LockState { + locked_mass: lhs.locked_mass.saturating_add(rhs.locked_mass), + conviction: lhs.conviction.saturating_add(rhs.conviction), + last_update: lhs.last_update.max(rhs.last_update), + } + } + + fn reduce_lock(lock: &LockState, locked_mass: AlphaBalance, conviction: U64F64) -> LockState { + LockState { + locked_mass: lock.locked_mass.saturating_sub(locked_mass), + conviction: lock.conviction.saturating_sub(conviction), + last_update: lock.last_update, + } + } + + pub fn exp_decay(dt: u64, tau: u64) -> U64F64 { + if tau == 0 || dt == 0 { + if dt == 0 { + return U64F64::saturating_from_num(1); + } + return U64F64::saturating_from_num(0); + } + let min_ratio = I64F64::saturating_from_num(-40); + let neg_ratio = I64F64::saturating_from_num((dt as i128).neg()) + .checked_div(I64F64::saturating_from_num(tau)) + .unwrap_or(min_ratio); + let clamped = neg_ratio.max(min_ratio); + let decay: I64F64 = exp(clamped).unwrap_or(I64F64::saturating_from_num(0)); + if decay < I64F64::saturating_from_num(0) { + U64F64::saturating_from_num(0) + } else { + U64F64::saturating_from_num(decay) + } + } + + fn calculate_decayed_mass_and_conviction( + locked_mass: AlphaBalance, + conviction: U64F64, + dt: u64, + unlock_rate: u64, + maturity_rate: u64, + perpetual_lock: bool, + ) -> (AlphaBalance, U64F64) { + let unlock_decay = Self::exp_decay(dt, unlock_rate); + let maturity_decay = Self::exp_decay(dt, maturity_rate); + let mass_fixed = U64F64::saturating_from_num(locked_mass); + let new_locked_mass = if perpetual_lock { + locked_mass + } else { + unlock_decay + .saturating_mul(mass_fixed) + .saturating_to_num::() + .into() + }; + + let conviction_from_existing = maturity_decay.saturating_mul(conviction); + let conviction_from_mass = if perpetual_lock { + mass_fixed.saturating_mul(U64F64::saturating_from_num(1).saturating_sub(maturity_decay)) + } else if unlock_rate == maturity_rate { + let dt_fixed = U64F64::saturating_from_num(dt); + let maturity_rate_fixed = U64F64::saturating_from_num(maturity_rate); + mass_fixed.saturating_mul( + dt_fixed + .safe_div(maturity_rate_fixed) + .saturating_mul(maturity_decay), + ) + } else if unlock_rate == 0 || maturity_rate == 0 { + U64F64::saturating_from_num(0) + } else { + let tau_x = I64F64::saturating_from_num(unlock_rate); + let tau_delta = I64F64::saturating_from_num( + (unlock_rate as i128).saturating_sub(maturity_rate as i128), + ); + let decay_delta = I64F64::saturating_from_num(unlock_decay) + .saturating_sub(I64F64::saturating_from_num(maturity_decay)); + let gamma = tau_x + .saturating_mul(decay_delta) + .checked_div(tau_delta) + .unwrap_or(I64F64::saturating_from_num(0)); + if gamma <= I64F64::saturating_from_num(0) { + U64F64::saturating_from_num(0) + } else { + mass_fixed.saturating_mul(U64F64::saturating_from_num(gamma)) + } + }; + let new_conviction = conviction_from_existing.saturating_add(conviction_from_mass); + (new_locked_mass, new_conviction) + } + + pub fn roll_forward_lock( + lock: LockState, + now: u64, + unlock_rate: u64, + maturity_rate: u64, + owner_lock: bool, + perpetual_lock: bool, + ) -> (LockState, RollDelta) { + let previous_locked_mass = lock.locked_mass; + let previous_conviction = lock.conviction; + let mut rolled = if now > lock.last_update { + let dt = now.saturating_sub(lock.last_update); + let (new_locked_mass, new_conviction) = Self::calculate_decayed_mass_and_conviction( + lock.locked_mass, + lock.conviction, + dt, + unlock_rate, + maturity_rate, + perpetual_lock, + ); + + LockState { + locked_mass: new_locked_mass, + conviction: new_conviction, + last_update: now, + } + } else { + lock + }; + + if owner_lock { + rolled.conviction = U64F64::saturating_from_num(u64::from(rolled.locked_mass)); + } + + if rolled.is_zero() { + rolled.locked_mass = AlphaBalance::ZERO; + rolled.conviction = U64F64::saturating_from_num(0); + } + + let roll_delta = RollDelta { + locked_mass_delta: previous_locked_mass.saturating_sub(rolled.locked_mass), + conviction_decay: previous_conviction.saturating_sub(rolled.conviction), + conviction_growth: rolled.conviction.saturating_sub(previous_conviction), + }; + + (rolled, roll_delta) + } +} diff --git a/pallets/subtensor/src/staking/lock/lock_availability.rs b/pallets/subtensor/src/staking/lock/lock_availability.rs new file mode 100644 index 0000000000..700472ac32 --- /dev/null +++ b/pallets/subtensor/src/staking/lock/lock_availability.rs @@ -0,0 +1,86 @@ +//! Coldkey lock queries and unstake availability checks. +//! +//! Conviction locks are subnet-wide: they block unstaking from any hotkey on the +//! subnet, not a single hotkey position. +use super::*; +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::NetUid; + +impl Pallet { + /// Returns the sum of raw alpha shares for a coldkey across all hotkeys on a given subnet. + pub fn total_coldkey_alpha_on_subnet(coldkey: &T::AccountId, netuid: NetUid) -> AlphaBalance { + StakingHotkeys::::get(coldkey) + .into_iter() + .map(|hotkey| { + Self::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, coldkey, netuid) + }) + .fold(AlphaBalance::ZERO, |acc, stake| acc.saturating_add(stake)) + } + + /// Returns the current locked amount for a coldkey on a subnet. + pub fn get_current_locked(coldkey: &T::AccountId, netuid: NetUid) -> AlphaBalance { + let now = Self::get_current_block_as_u64(); + Self::read_conviction_model(coldkey, netuid, now) + .map(|(_hotkey, mut model)| { + model.roll_forward(now, UnlockRate::::get(), MaturityRate::::get()); + model.individual_lock().locked_mass + }) + .unwrap_or(AlphaBalance::ZERO) + } + + /// Returns the current conviction for a coldkey on a subnet (rolled forward to now). + pub fn get_conviction(coldkey: &T::AccountId, netuid: NetUid) -> U64F64 { + let now = Self::get_current_block_as_u64(); + Self::read_conviction_model(coldkey, netuid, now) + .map(|(_hotkey, mut model)| { + model.roll_forward(now, UnlockRate::::get(), MaturityRate::::get()); + model.individual_lock().conviction + }) + .unwrap_or_else(|| U64F64::saturating_from_num(0)) + } + + /// Returns the current lock for a coldkey on a subnet, rolled forward to now. + pub fn get_coldkey_lock(coldkey: &T::AccountId, netuid: NetUid) -> Option { + let now = Self::get_current_block_as_u64(); + Self::read_conviction_model(coldkey, netuid, now).map(|(_hotkey, mut model)| { + model.roll_forward(now, UnlockRate::::get(), MaturityRate::::get()); + model.individual_lock().clone() + }) + } + + /// (total_stake, locked_mass, available_to_unstake) for a coldkey on one subnet. + /// + /// The conviction lock is subnet-wide: it blocks unstaking from any hotkey on + /// that subnet, not from a single hotkey position. Miner registration + /// collateral is also subtracted here as a coldkey-wide residual; call sites + /// that know the origin hotkey must additionally call + /// `ensure_hotkey_covers_collateral` so the bond cannot be covered by free + /// stake on a sibling hotkey. + pub fn stake_availability( + coldkey: &T::AccountId, + netuid: NetUid, + ) -> (AlphaBalance, AlphaBalance, AlphaBalance) { + let total = Self::total_coldkey_alpha_on_subnet(coldkey, netuid); + let locked = Self::get_current_locked(coldkey, netuid); + let collateral = Self::total_miner_collateral_for_coldkey(coldkey, netuid); + let available = total.saturating_sub(locked).saturating_sub(collateral); + (total, locked, available) + } + + /// Alpha the coldkey can still unstake on this subnet right now. + pub fn available_to_unstake(coldkey: &T::AccountId, netuid: NetUid) -> AlphaBalance { + let (_, _, available) = Self::stake_availability(coldkey, netuid); + available + } + + /// Ensures that the amount can be unstaked + pub fn ensure_available_to_unstake( + coldkey: &T::AccountId, + netuid: NetUid, + amount: AlphaBalance, + ) -> Result<(), Error> { + let alpha_available = Self::available_to_unstake(coldkey, netuid); + ensure!(alpha_available >= amount, Error::::StakeUnavailable); + Ok(()) + } +} diff --git a/pallets/subtensor/src/staking/lock/lock_key_swaps.rs b/pallets/subtensor/src/staking/lock/lock_key_swaps.rs new file mode 100644 index 0000000000..b4168f943e --- /dev/null +++ b/pallets/subtensor/src/staking/lock/lock_key_swaps.rs @@ -0,0 +1,362 @@ +//! Migrate conviction locks across coldkey or hotkey swaps. +use super::*; +use subtensor_runtime_common::NetUid; + +impl Pallet { + /// Ensure the coldkey does not have an active lock on any subnets. + pub fn ensure_no_active_locks(coldkey: &T::AccountId) -> Result<(), Error> { + let now = Self::get_current_block_as_u64(); + let unlock_rate = UnlockRate::::get(); + let maturity_rate = MaturityRate::::get(); + + for ((netuid, hotkey), lock) in Lock::::iter_prefix((coldkey,)) { + let rolled = ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + Self::is_subnet_owner_hotkey(netuid, &hotkey), + Self::is_perpetual_lock(coldkey, netuid), + ); + if rolled.0.locked_mass > AlphaBalance::ZERO { + return Err(Error::::ActiveLockExists); + } + } + + Ok(()) + } + + /// Transfers the lock from one coldkey to another for all subnets. This is used when a + /// user swaps their coldkey and we want to preserve their locks. + /// + /// The hotkey and netuid remain the same, only the coldkey changes. + /// + /// The new coldkey must have no active locks, so we can transfer the locks + /// "as is" without rolling them forward and the + /// HotkeyLock map does not change (because it only contains totals, not individual coldkey locks). + pub fn swap_coldkey_locks( + old_coldkey: &T::AccountId, + new_coldkey: &T::AccountId, + ) -> DispatchResult { + Self::ensure_no_active_locks(new_coldkey)?; + + let mut locks_to_transfer: Vec<(NetUid, T::AccountId, LockState)> = Vec::new(); + let now = Self::get_current_block_as_u64(); + let unlock_rate = UnlockRate::::get(); + let maturity_rate = MaturityRate::::get(); + let new_coldkey_rejects_locked_alpha = Self::account_rejects_locked_alpha(new_coldkey); + let decaying_locks_to_transfer: Vec<(NetUid, bool)> = + DecayingLock::::iter_prefix(old_coldkey).collect(); + + // Gather locks for old coldkey + for ((netuid, hotkey), lock) in Lock::::iter_prefix((old_coldkey,)) { + locks_to_transfer.push((netuid, hotkey, lock)); + } + + let mut rolled_locks_to_transfer: Vec<(NetUid, T::AccountId, LockState, bool)> = Vec::new(); + for (netuid, hotkey, lock) in locks_to_transfer { + let perpetual_lock = decaying_locks_to_transfer + .iter() + .any(|(decaying_netuid, decaying)| *decaying_netuid == netuid && !*decaying); + let (old_lock, _) = ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + Self::is_subnet_owner_hotkey(netuid, &hotkey), + perpetual_lock, + ); + Self::ensure_can_receive_locked_alpha_with_flag( + new_coldkey_rejects_locked_alpha, + old_lock.locked_mass, + )?; + rolled_locks_to_transfer.push((netuid, hotkey, old_lock, perpetual_lock)); + } + + // Remove old locks and reduce old aggregate buckets before moving the + // perpetual-lock flags; aggregate selection depends on the old flag. + for (netuid, hotkey, old_lock, _) in rolled_locks_to_transfer.iter() { + Lock::::remove((old_coldkey.clone(), *netuid, hotkey.clone())); + Self::maybe_remove_locking_coldkey(hotkey, *netuid, old_coldkey); + Self::reduce_aggregate_lock( + old_coldkey, + hotkey, + *netuid, + old_lock.locked_mass, + old_lock.conviction, + ); + } + + for (netuid, _) in decaying_locks_to_transfer { + if let Some(decaying) = DecayingLock::::take(old_coldkey, netuid) { + DecayingLock::::insert(new_coldkey, netuid, decaying); + } + } + + let flags = AccountFlags::::get(old_coldkey); + AccountFlags::::remove(old_coldkey); + if flags != 0 { + AccountFlags::::insert(new_coldkey, flags); + } else { + AccountFlags::::remove(new_coldkey); + } + + // Insert locks for the new coldkey and add to the destination aggregate + // buckets after the flags have moved. + for (netuid, hotkey, old_lock, perpetual_lock) in rolled_locks_to_transfer { + let new_lock = ConvictionModel::roll_forward_lock( + old_lock.clone(), + now, + unlock_rate, + maturity_rate, + Self::is_subnet_owner_hotkey(netuid, &hotkey), + perpetual_lock, + ) + .0; + Self::insert_lock_state(new_coldkey, netuid, &hotkey, new_lock.clone()); + Self::add_aggregate_lock(new_coldkey, &hotkey, netuid, new_lock); + } + + Ok(()) + } + + /// Swap all locks made to the old_hotkey to new_hotkey on all netuids + /// + /// There is no need to roll the locks, they can be just copied "as is": + /// The lock relation between coldkeys and hotkey is 1:1, so if old hotkey has a + /// coldkey locking to it, then the same coldkey cannot lock to the new hotkey. + /// And in reverse: If a coldkey is locking to the new hotkey, it will not appear + /// in the transfer list because it does not lock to the old hotkey. + /// + /// Conviction is not reset because the hotkey ownership does not change, it's still + /// the same hotkey owner who will own the new hotkey. + pub fn swap_hotkey_locks(old_hotkey: &T::AccountId, new_hotkey: &T::AccountId) -> (u64, u64) { + Self::swap_hotkey_locks_for_netuids(old_hotkey, new_hotkey, Self::get_all_subnet_netuids()) + } + + /// Swap locks made to the old_hotkey to new_hotkey on one netuid. + pub fn swap_hotkey_locks_on_subnet( + old_hotkey: &T::AccountId, + new_hotkey: &T::AccountId, + netuid: NetUid, + ) -> (u64, u64) { + Self::swap_hotkey_locks_for_netuids(old_hotkey, new_hotkey, vec![netuid]) + } + + pub(crate) fn swap_hotkey_locks_for_netuids( + old_hotkey: &T::AccountId, + new_hotkey: &T::AccountId, + netuids: Vec, + ) -> (u64, u64) { + let mut locks_to_transfer: Vec<(T::AccountId, NetUid, LockState)> = Vec::new(); + let mut netuids_to_transfer: Vec<(NetUid, bool, bool)> = Vec::new(); + let mut reads: u64 = 0; + let mut writes: u64 = 0; + + for netuid in netuids.iter().copied() { + let old_is_owner_hotkey = Self::is_subnet_owner_hotkey(netuid, old_hotkey); + let new_is_owner_hotkey = Self::is_subnet_owner_hotkey(netuid, new_hotkey); + let has_hotkey_lock = HotkeyLock::::contains_key(netuid, old_hotkey); + let has_decaying_hotkey_lock = + DecayingHotkeyLock::::contains_key(netuid, old_hotkey); + let has_owner_lock = old_is_owner_hotkey && OwnerLock::::contains_key(netuid); + let has_decaying_owner_lock = + old_is_owner_hotkey && DecayingOwnerLock::::contains_key(netuid); + + if old_is_owner_hotkey + || new_is_owner_hotkey + || has_hotkey_lock + || has_decaying_hotkey_lock + || has_owner_lock + || has_decaying_owner_lock + { + netuids_to_transfer.push(( + netuid, + old_is_owner_hotkey, + old_is_owner_hotkey || new_is_owner_hotkey, + )); + } + reads = reads.saturating_add(5); + } + + // Build a concrete transfer list from the hotkey-to-coldkey index. + // The index can contain stale coldkeys, so only locks that still exist + // are carried forward; missing locks are pruned from the index. + for (netuid, _, _) in &netuids_to_transfer { + for (coldkey, _) in LockingColdkeys::::iter_prefix((*netuid, old_hotkey)) { + if let Some(lock) = Lock::::get((coldkey.clone(), *netuid, old_hotkey.clone())) { + locks_to_transfer.push((coldkey, *netuid, lock)); + } else { + Self::maybe_remove_locking_coldkey(old_hotkey, *netuid, &coldkey); + writes = writes.saturating_add(1); + } + reads = reads.saturating_add(1); + } + } + + for (coldkey, netuid, lock) in locks_to_transfer { + let now = Self::get_current_block_as_u64(); + let unlock_rate = UnlockRate::::get(); + let maturity_rate = MaturityRate::::get(); + let old_owner_lock = netuids_to_transfer + .iter() + .any(|(rebuild_netuid, is_owner, _)| *rebuild_netuid == netuid && *is_owner); + let new_owner_lock = netuids_to_transfer + .iter() + .any(|(rebuild_netuid, _, is_owner)| *rebuild_netuid == netuid && *is_owner); + let perpetual_lock = Self::is_perpetual_lock(&coldkey, netuid); + let rolled = ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + old_owner_lock, + perpetual_lock, + ) + .0; + let moved = ConvictionModel::roll_forward_lock( + rolled, + now, + unlock_rate, + maturity_rate, + new_owner_lock, + perpetual_lock, + ) + .0; + Lock::::remove((coldkey.clone(), netuid, old_hotkey.clone())); + Self::maybe_remove_locking_coldkey(old_hotkey, netuid, &coldkey); + Self::insert_lock_state(&coldkey, netuid, new_hotkey, moved); + writes = writes.saturating_add(2); + } + + for (netuid, old_was_owner, new_is_owner) in netuids_to_transfer { + let now = Self::get_current_block_as_u64(); + let unlock_rate = UnlockRate::::get(); + let maturity_rate = MaturityRate::::get(); + let moved_perpetual_lock = if old_was_owner { + OwnerLock::::take(netuid).map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + true, + ) + .0 + }) + } else { + HotkeyLock::::take(netuid, old_hotkey).map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + true, + ) + .0 + }) + }; + let moved_decaying_lock = if old_was_owner { + DecayingOwnerLock::::take(netuid).map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + false, + ) + .0 + }) + } else { + DecayingHotkeyLock::::take(netuid, old_hotkey).map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + false, + ) + .0 + }) + }; + + if let Some(lock) = moved_perpetual_lock { + if new_is_owner { + Self::insert_owner_lock_state( + netuid, + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + true, + ) + .0, + ); + } else { + Self::insert_hotkey_lock_state( + netuid, + new_hotkey, + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + true, + ) + .0, + ); + } + } + if let Some(lock) = moved_decaying_lock { + if new_is_owner { + Self::insert_decaying_owner_lock_state( + netuid, + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + false, + ) + .0, + ); + } else { + Self::insert_decaying_hotkey_lock_state( + netuid, + new_hotkey, + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + false, + ) + .0, + ); + } + } + writes = writes.saturating_add(6); + } + (reads, writes) + } + + /// Conviction is only preserved when a lock moves between hotkeys owned by + /// the same coldkey; moving it to a differently owned hotkey forfeits it. + /// Shared by `do_move_lock` and `transfer_lock`. + pub(crate) fn conviction_survives_hotkey_change( + source_hotkey: &T::AccountId, + destination_hotkey: &T::AccountId, + ) -> bool { + Self::get_owning_coldkey_for_hotkey(source_hotkey) + == Self::get_owning_coldkey_for_hotkey(destination_hotkey) + } +} diff --git a/pallets/subtensor/src/staking/lock/lock_operations.rs b/pallets/subtensor/src/staking/lock/lock_operations.rs new file mode 100644 index 0000000000..70e91c9869 --- /dev/null +++ b/pallets/subtensor/src/staking/lock/lock_operations.rs @@ -0,0 +1,169 @@ +//! Create, top up, reduce, and aggregate conviction locks for a coldkey. +use super::*; +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::NetUid; + +impl Pallet { + /// Locks stake for a coldkey on a subnet to a specific hotkey. + /// If no lock exists, creates one. If one exists, the hotkey must match. + /// Top-up adds to locked_mass after rolling forward. + pub fn do_lock_stake( + coldkey: &T::AccountId, + netuid: NetUid, + hotkey: &T::AccountId, + amount: AlphaBalance, + ) -> dispatch::DispatchResult { + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); + ensure!(!amount.is_zero(), Error::::AmountTooLow); + ensure!( + Self::hotkey_account_exists(hotkey), + Error::::HotKeyAccountNotExists + ); + + let total = Self::total_coldkey_alpha_on_subnet(coldkey, netuid); + let now = Self::get_current_block_as_u64(); + + let mut model = match Self::read_conviction_model(coldkey, netuid, now) { + Some((existing_hotkey, model)) => { + ensure!(*hotkey == existing_hotkey, Error::::LockHotkeyMismatch); + model + } + None => Self::read_conviction_model_for_hotkey(coldkey, netuid, hotkey, now), + }; + model.roll_forward(now, UnlockRate::::get(), MaturityRate::::get()); + + if model.individual_lock().locked_mass.is_zero() + && model.individual_lock().conviction == U64F64::saturating_from_num(0) + { + ensure!(total >= amount, Error::::InsufficientStakeForLock); + + model.set_rolled_individual_lock( + LockState { + locked_mass: amount, + conviction: U64F64::saturating_from_num(0), + last_update: now, + }, + now, + UnlockRate::::get(), + MaturityRate::::get(), + ); + } else { + let mut lock = model.individual_lock().clone(); + lock.locked_mass = lock.locked_mass.saturating_add(amount); + ensure!( + total >= lock.locked_mass, + Error::::InsufficientStakeForLock + ); + model.set_rolled_individual_lock( + lock, + now, + UnlockRate::::get(), + MaturityRate::::get(), + ); + } + + model.roll_forward_aggregate(now, UnlockRate::::get(), MaturityRate::::get()); + model.add_to_aggregate(&LockState { + locked_mass: amount, + conviction: U64F64::saturating_from_num(0), + last_update: now, + }); + model.roll_forward_aggregate(now, UnlockRate::::get(), MaturityRate::::get()); + Self::save_conviction_model(coldkey, netuid, hotkey, model); + + Self::deposit_event(Event::StakeLocked { + coldkey: coldkey.clone(), + hotkey: hotkey.clone(), + netuid, + amount, + }); + + Ok(()) + } + + /// Reduces the coldkey lock by a specified alpha amount and the coldkey conviction + /// proportionally. + pub fn force_reduce_lock(coldkey: &T::AccountId, netuid: NetUid, amount: AlphaBalance) { + let now = Self::get_current_block_as_u64(); + if let Some((hotkey, mut model)) = Self::read_conviction_model(coldkey, netuid, now) { + model.roll_forward(now, UnlockRate::::get(), MaturityRate::::get()); + model.roll_forward_aggregate(now, UnlockRate::::get(), MaturityRate::::get()); + model.force_reduce_individual(amount, now); + Self::save_conviction_model(coldkey, netuid, &hotkey, model); + } + } + + /// Rolls the lock forward to now and persists it if the locked mass is zero. This is used when we want to + /// update the lock when a user stakes or unstakes. + pub fn cleanup_lock_if_zero(coldkey: &T::AccountId, netuid: NetUid) { + let now = Self::get_current_block_as_u64(); + + // Cleanup locks for the specific coldkey and hotkey + if let Some((hotkey, mut model)) = Self::read_conviction_model(coldkey, netuid, now) { + model.roll_forward(now, UnlockRate::::get(), MaturityRate::::get()); + Self::save_conviction_model(coldkey, netuid, &hotkey, model); + } + } + + /// Update the total lock for a hotkey on a subnet or create one if + /// it doesn't exist. + /// + /// Roll the existing hotkey lock forward to now, then add the + /// latest conviction and locked mass. + pub fn upsert_aggregate_lock( + coldkey: &T::AccountId, + hotkey: &T::AccountId, + netuid: NetUid, + amount: AlphaBalance, + ) { + let now = Self::get_current_block_as_u64(); + Self::add_aggregate_lock( + coldkey, + hotkey, + netuid, + LockState { + locked_mass: amount, + conviction: U64F64::saturating_from_num(0), + last_update: now, + }, + ); + } + + /// Merges an already-existing lock state into the aggregate lock bucket. + /// + /// This is used when lock state moves between keys, such as lock moves, stake + /// transfers, or coldkey swaps. Unlike `upsert_aggregate_lock`, this preserves + /// both locked mass and conviction from the moved lock because that conviction + /// was already earned before the aggregate bucket changed. + /// + /// Locks to the subnet owner hotkey are merged into `OwnerLock`; all other + /// locks are merged into the destination hotkey's perpetual or decaying bucket. + pub(crate) fn add_aggregate_lock( + coldkey: &T::AccountId, + hotkey: &T::AccountId, + netuid: NetUid, + added: LockState, + ) { + let now = Self::get_current_block_as_u64(); + let mut model = Self::read_conviction_model_for_hotkey(coldkey, netuid, hotkey, now); + model.roll_forward_aggregate(now, UnlockRate::::get(), MaturityRate::::get()); + model.add_to_aggregate(&added); + model.roll_forward_aggregate(now, UnlockRate::::get(), MaturityRate::::get()); + Self::save_conviction_model(coldkey, netuid, hotkey, model); + } + + /// Reduces locked mass and conviction from exactly one aggregate bucket. + pub(crate) fn reduce_aggregate_lock( + coldkey: &T::AccountId, + hotkey: &T::AccountId, + netuid: NetUid, + amount: AlphaBalance, + conviction: U64F64, + ) { + let now = Self::get_current_block_as_u64(); + let mut model = Self::read_conviction_model_for_hotkey(coldkey, netuid, hotkey, now); + model.roll_forward_aggregate(now, UnlockRate::::get(), MaturityRate::::get()); + model.reduce_aggregate(amount, conviction); + Self::save_conviction_model(coldkey, netuid, hotkey, model); + } +} diff --git a/pallets/subtensor/src/staking/lock/lock_storage.rs b/pallets/subtensor/src/staking/lock/lock_storage.rs new file mode 100644 index 0000000000..49df854fa3 --- /dev/null +++ b/pallets/subtensor/src/staking/lock/lock_storage.rs @@ -0,0 +1,232 @@ +//! Lock storage helpers: locking-coldkey index, accept-locked-alpha flags, and +//! read/write of [`super::ConvictionModel`] into `Lock` / aggregate maps. +use super::*; +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::NetUid; + +impl Pallet { + pub fn add_locking_coldkey(hotkey: &T::AccountId, netuid: NetUid, coldkey: &T::AccountId) { + LockingColdkeys::::insert((netuid, hotkey, coldkey), ()); + } + + pub fn maybe_remove_locking_coldkey( + hotkey: &T::AccountId, + netuid: NetUid, + coldkey: &T::AccountId, + ) { + LockingColdkeys::::remove((netuid, hotkey, coldkey)); + } + + pub fn account_rejects_locked_alpha(coldkey: &T::AccountId) -> bool { + AccountFlags::::get(coldkey) & crate::ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA != 1 + } + + pub fn set_accept_locked_alpha(coldkey: &T::AccountId, enabled: bool) { + AccountFlags::::mutate_exists(coldkey, |maybe_flags| { + let mut flags = maybe_flags.unwrap_or_default(); + if enabled { + flags |= crate::ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA; + } else { + flags &= !crate::ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA; + } + *maybe_flags = if flags == 0 { None } else { Some(flags) }; + }); + } + + pub fn ensure_can_receive_locked_alpha( + coldkey: &T::AccountId, + amount: AlphaBalance, + ) -> DispatchResult { + let rejects_locked_alpha = Self::account_rejects_locked_alpha(coldkey); + Self::ensure_can_receive_locked_alpha_with_flag(rejects_locked_alpha, amount) + } + + pub(crate) fn ensure_can_receive_locked_alpha_with_flag( + rejects_locked_alpha: bool, + amount: AlphaBalance, + ) -> DispatchResult { + if amount.is_zero() { + return Ok(()); + } + ensure!(!rejects_locked_alpha, Error::::AccountRejectsLockedAlpha); + Ok(()) + } + + pub fn insert_lock_state( + coldkey: &T::AccountId, + netuid: NetUid, + hotkey: &T::AccountId, + lock_state: LockState, + ) { + if lock_state.is_zero() { + Self::maybe_remove_locking_coldkey(hotkey, netuid, coldkey); + // If there is no record previously, this is a no-op + Lock::::remove((coldkey, netuid, hotkey)); + } else { + Self::add_locking_coldkey(hotkey, netuid, coldkey); + Lock::::insert((coldkey, netuid, hotkey), lock_state); + } + } + + pub fn insert_hotkey_lock_state(netuid: NetUid, hotkey: &T::AccountId, lock_state: LockState) { + if !lock_state.locked_mass.is_zero() + || lock_state.conviction > U64F64::saturating_from_num(0) + { + HotkeyLock::::insert(netuid, hotkey, lock_state); + } else { + HotkeyLock::::remove(netuid, hotkey); + } + } + + pub fn insert_decaying_hotkey_lock_state( + netuid: NetUid, + hotkey: &T::AccountId, + lock_state: LockState, + ) { + if !lock_state.locked_mass.is_zero() + || lock_state.conviction > U64F64::saturating_from_num(0) + { + DecayingHotkeyLock::::insert(netuid, hotkey, lock_state); + } else { + DecayingHotkeyLock::::remove(netuid, hotkey); + } + } + + pub fn insert_owner_lock_state(netuid: NetUid, lock_state: LockState) { + if !lock_state.locked_mass.is_zero() + || lock_state.conviction > U64F64::saturating_from_num(0) + { + OwnerLock::::insert(netuid, lock_state); + } else { + OwnerLock::::remove(netuid); + } + } + + pub fn insert_decaying_owner_lock_state(netuid: NetUid, lock_state: LockState) { + if !lock_state.locked_mass.is_zero() + || lock_state.conviction > U64F64::saturating_from_num(0) + { + DecayingOwnerLock::::insert(netuid, lock_state); + } else { + DecayingOwnerLock::::remove(netuid); + } + } + + pub(crate) fn is_subnet_owner_hotkey(netuid: NetUid, hotkey: &T::AccountId) -> bool { + hotkey == &SubnetOwnerHotkey::::get(netuid) + } + + pub(crate) fn is_perpetual_lock(coldkey: &T::AccountId, netuid: NetUid) -> bool { + DecayingLock::::get(coldkey, netuid) == Some(false) + } + + pub(crate) fn empty_lock(now: u64) -> LockState { + LockState { + locked_mass: AlphaBalance::ZERO, + conviction: U64F64::saturating_from_num(0), + last_update: now, + } + } + + pub(crate) fn read_conviction_model_for_hotkey( + coldkey: &T::AccountId, + netuid: NetUid, + hotkey: &T::AccountId, + now: u64, + ) -> ConvictionModel { + ConvictionModel::new( + Self::is_subnet_owner_hotkey(netuid, hotkey), + Self::is_perpetual_lock(coldkey, netuid), + Lock::::get((coldkey, netuid, hotkey)).unwrap_or_else(|| Self::empty_lock(now)), + HotkeyLock::::get(netuid, hotkey).unwrap_or_else(|| Self::empty_lock(now)), + DecayingHotkeyLock::::get(netuid, hotkey).unwrap_or_else(|| Self::empty_lock(now)), + OwnerLock::::get(netuid).unwrap_or_else(|| Self::empty_lock(now)), + DecayingOwnerLock::::get(netuid).unwrap_or_else(|| Self::empty_lock(now)), + ) + } + + pub(crate) fn read_conviction_model( + coldkey: &T::AccountId, + netuid: NetUid, + now: u64, + ) -> Option<(T::AccountId, ConvictionModel)> { + Lock::::iter_prefix((coldkey, netuid)) + .next() + .map(|(hotkey, _lock)| { + let model = Self::read_conviction_model_for_hotkey(coldkey, netuid, &hotkey, now); + (hotkey, model) + }) + } + + pub(crate) fn save_conviction_model( + coldkey: &T::AccountId, + netuid: NetUid, + hotkey: &T::AccountId, + model: ConvictionModel, + ) { + if model.individual_lock_dirty() { + Self::insert_lock_state(coldkey, netuid, hotkey, model.individual_lock().clone()); + } + if model.agg_perpetual_general_dirty() { + Self::insert_hotkey_lock_state(netuid, hotkey, model.agg_perpetual_general().clone()); + } + if model.agg_decaying_general_dirty() { + Self::insert_decaying_hotkey_lock_state( + netuid, + hotkey, + model.agg_decaying_general().clone(), + ); + } + if model.agg_perpetual_owner_dirty() { + Self::insert_owner_lock_state(netuid, model.agg_perpetual_owner().clone()); + } + if model.agg_decaying_owner_dirty() { + Self::insert_decaying_owner_lock_state(netuid, model.agg_decaying_owner().clone()); + } + } + + pub fn do_set_perpetual_lock( + coldkey: &T::AccountId, + netuid: NetUid, + enabled: bool, + ) -> DispatchResult { + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); + + let now = Self::get_current_block_as_u64(); + let current_enabled = Self::is_perpetual_lock(coldkey, netuid); + + if let Some((hotkey, mut model)) = Self::read_conviction_model(coldkey, netuid, now) { + model.roll_forward(now, UnlockRate::::get(), MaturityRate::::get()); + let rolled = model.individual_lock().clone(); + Self::save_conviction_model(coldkey, netuid, &hotkey, model); + + if current_enabled != enabled { + Self::reduce_aggregate_lock( + coldkey, + &hotkey, + netuid, + rolled.locked_mass, + rolled.conviction, + ); + } + } + + if enabled { + DecayingLock::::insert(coldkey, netuid, false); + } else { + DecayingLock::::remove(coldkey, netuid); + } + + if current_enabled != enabled + && let Some((hotkey, model)) = Self::read_conviction_model(coldkey, netuid, now) + { + Self::add_aggregate_lock(coldkey, &hotkey, netuid, model.individual_lock().clone()); + } + Self::deposit_event(Event::PerpetualLockUpdated { + coldkey: coldkey.clone(), + netuid, + enabled, + }); + Ok(()) + } +} diff --git a/pallets/subtensor/src/staking/lock/lock_transfer.rs b/pallets/subtensor/src/staking/lock/lock_transfer.rs new file mode 100644 index 0000000000..140274b793 --- /dev/null +++ b/pallets/subtensor/src/staking/lock/lock_transfer.rs @@ -0,0 +1,322 @@ +//! Move or transfer locks between hotkeys, auto-lock owner cuts, and wipe locks on network removal. +use super::*; +use frame_support::weights::WeightMeter; +use safe_math::FixedExt; +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::NetUid; + +impl Pallet { + /// Moves lock from one hotkey to another and clears conviction + /// + /// The lock is rolled forward to the current block before switching the + /// associated hotkey so that the lock stays mathematically correct and + /// preserves current decayed locked mass. + /// + /// The conviction is reset to zero if the destination and source hotkeys + /// are owned by different coldkeys, otherwise it is preserved. + pub fn do_move_lock( + coldkey: &T::AccountId, + destination_hotkey: &T::AccountId, + netuid: NetUid, + ) -> DispatchResult { + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); + ensure!( + Self::hotkey_account_exists(destination_hotkey), + Error::::HotKeyAccountNotExists + ); + let now = Self::get_current_block_as_u64(); + + match Self::read_conviction_model(coldkey, netuid, now) { + Some((origin_hotkey, mut model)) => { + let unlock_rate = UnlockRate::::get(); + let maturity_rate = MaturityRate::::get(); + model.roll_forward(now, unlock_rate, maturity_rate); + let mut lock = model.individual_lock().clone(); + let removed = lock.clone(); + + if !Self::conviction_survives_hotkey_change(&origin_hotkey, destination_hotkey) { + lock.conviction = U64F64::saturating_from_num(0); + } + lock = ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + Self::is_subnet_owner_hotkey(netuid, destination_hotkey), + Self::is_perpetual_lock(coldkey, netuid), + ) + .0; + + Lock::::remove((coldkey.clone(), netuid, origin_hotkey.clone())); + Self::maybe_remove_locking_coldkey(&origin_hotkey, netuid, coldkey); + Self::insert_lock_state(coldkey, netuid, destination_hotkey, lock.clone()); + Self::reduce_aggregate_lock( + coldkey, + &origin_hotkey, + netuid, + removed.locked_mass, + removed.conviction, + ); + Self::add_aggregate_lock(coldkey, destination_hotkey, netuid, lock); + + Self::deposit_event(Event::LockMoved { + coldkey: coldkey.clone(), + origin_hotkey, + destination_hotkey: destination_hotkey.clone(), + netuid, + }); + Ok(()) + } + None => Err(Error::::NoExistingLock.into()), + } + } + + pub fn auto_lock_owner_cut(netuid: NetUid, amount: AlphaBalance) { + if !OwnerCutAutoLockEnabled::::get(netuid) { + return; + } + + let subnet_owner_coldkey = Self::get_subnet_owner(netuid); + + // Determine the lock hotkey. If no locks exist, assign subnet owner's hotkey, otherwise + // auto-lock to existing lock hotkey + let lock_hotkey = if let Some((existing_hotkey, _model)) = Self::read_conviction_model( + &subnet_owner_coldkey, + netuid, + Self::get_current_block_as_u64(), + ) { + existing_hotkey + } else { + SubnetOwnerHotkey::::get(netuid) + }; + + // Ignore the result. It may only fail if amount is zero, which is OK to ignore because nothing + // needs to happen in that case + let _ = Self::do_lock_stake(&subnet_owner_coldkey, netuid, &lock_hotkey, amount); + } + + /// When locked stake is transfered, the lock should follow the stake + /// + /// First, this function rolls the lock forward and checks if amount is over available + /// stake and if it is, the stake that's over the available amount on the destination + /// coldkey is locked in the same way as the original stake: the lock follows the stake + /// to `destination_hotkey` (which, for plain stake transfers, is the same hotkey the + /// stake was locked to). Conviction is moved proportionally to the moved locked amount + /// of alpha. For example, if 20% of locked alpha is moved, then also 20% of conviction + /// is moved. If the source and destination hotkeys are owned by different coldkeys, + /// the moved conviction is reset to zero, mirroring `do_move_lock`. + pub fn transfer_lock( + origin_coldkey: &T::AccountId, + destination_coldkey: &T::AccountId, + destination_hotkey: &T::AccountId, + netuid: NetUid, + amount: AlphaBalance, + ) -> DispatchResult { + let now = Self::get_current_block_as_u64(); + + // If no actual transfer happens, this is ok + if origin_coldkey == destination_coldkey || amount.is_zero() { + return Ok(()); + } + + // Read total alpha of the coldkey on this netuid. Do not check if total alpha is + // lower than amount transferred, this is responsibility of a higher level, this + // function needs to act protectively. + let total_alpha = Self::total_coldkey_alpha_on_subnet(origin_coldkey, netuid); + let mut remaining_to_transfer = amount; + + // Read the locks for source and destination coldkey (if exist) and roll forward + let Some((source_hotkey, mut source_model)) = + Self::read_conviction_model(origin_coldkey, netuid, now) + else { + return Ok(()); + }; + + let unlock_rate = UnlockRate::::get(); + let maturity_rate = MaturityRate::::get(); + source_model.roll_forward(now, unlock_rate, maturity_rate); + let mut source_lock = source_model.individual_lock().clone(); + let maybe_destination_lock = Self::read_conviction_model(destination_coldkey, netuid, now) + .map(|(hotkey, mut model)| { + model.roll_forward(now, unlock_rate, maturity_rate); + (hotkey, model.individual_lock().clone()) + }); + + let destination_lock_hotkey = maybe_destination_lock + .as_ref() + .map(|(hotkey, _)| hotkey.clone()) + .unwrap_or_else(|| destination_hotkey.clone()); + let mut destination_lock = maybe_destination_lock + .as_ref() + .map(|(_, lock)| lock.clone()) + .unwrap_or(LockState { + locked_mass: AlphaBalance::ZERO, + conviction: U64F64::saturating_from_num(0), + last_update: now, + }); + + // Calculate available stake by subtracting locked_mass from total alpha. + let unavailable = source_lock.locked_mass; + let available_stake = total_alpha.saturating_sub(unavailable); + + // Reduce remaining_to_transfer by min(remaining_to_transfer, available stake) + let available_transfer = remaining_to_transfer.min(available_stake); + remaining_to_transfer = remaining_to_transfer.saturating_sub(available_transfer); + + // If result is non-zero, check the hotkey match between source and destination coldkey locks + // (if destination coldkey lock exists). If no match, error out with LockHotkeyMismatch, otherwise, + // reduce remaining_to_transfer by min(remaining_to_transfer, locked_mass), reduce locked_mass on + // the source coldkey by the same amount, increase locked_mass on the destination coldkey by the + // same amount, reduce conviction on the source coldkey proportionally, and increase conviction + // on the destination coldkey proportionally. + let mut locked_transfer = AlphaBalance::ZERO; + let mut conviction_transfer = U64F64::saturating_from_num(0); + let mut received_conviction = U64F64::saturating_from_num(0); + if !remaining_to_transfer.is_zero() { + if let Some((existing_hotkey, _)) = maybe_destination_lock.as_ref() { + ensure!( + existing_hotkey == destination_hotkey, + Error::::LockHotkeyMismatch + ); + } + + locked_transfer = remaining_to_transfer.min(source_lock.locked_mass); + conviction_transfer = if locked_transfer.is_zero() || source_lock.locked_mass.is_zero() + { + U64F64::saturating_from_num(0) + } else { + let locked_transfer = U64F64::saturating_from_num(locked_transfer.to_u64()); + let source_locked = U64F64::saturating_from_num(source_lock.locked_mass.to_u64()); + let transferred_proportion = locked_transfer.safe_div(source_locked); + source_lock + .conviction + .saturating_mul(transferred_proportion) + }; + + // Conviction only follows the lock when the destination hotkey is owned + // by the same coldkey as the source hotkey; otherwise it is forfeited, + // mirroring `do_move_lock`. + received_conviction = if Self::conviction_survives_hotkey_change( + &source_hotkey, + &destination_lock_hotkey, + ) { + conviction_transfer + } else { + U64F64::saturating_from_num(0) + }; + + source_lock.locked_mass = source_lock.locked_mass.saturating_sub(locked_transfer); + source_lock.conviction = source_lock.conviction.saturating_sub(conviction_transfer); + destination_lock.locked_mass = + destination_lock.locked_mass.saturating_add(locked_transfer); + destination_lock.conviction = destination_lock + .conviction + .saturating_add(received_conviction); + } + Self::ensure_can_receive_locked_alpha(destination_coldkey, locked_transfer)?; + + source_lock = ConvictionModel::roll_forward_lock( + source_lock, + now, + unlock_rate, + maturity_rate, + Self::is_subnet_owner_hotkey(netuid, &source_hotkey), + Self::is_perpetual_lock(origin_coldkey, netuid), + ) + .0; + destination_lock = ConvictionModel::roll_forward_lock( + destination_lock, + now, + unlock_rate, + maturity_rate, + Self::is_subnet_owner_hotkey(netuid, &destination_lock_hotkey), + Self::is_perpetual_lock(destination_coldkey, netuid), + ) + .0; + + // Upsert updated locks (only once per this fn) even if there were no updates because + // of roll-forward + Self::insert_lock_state(origin_coldkey, netuid, &source_hotkey, source_lock); + Self::insert_lock_state( + destination_coldkey, + netuid, + &destination_lock_hotkey, + destination_lock, + ); + if !locked_transfer.is_zero() { + Self::reduce_aggregate_lock( + origin_coldkey, + &source_hotkey, + netuid, + locked_transfer, + conviction_transfer, + ); + Self::add_aggregate_lock( + destination_coldkey, + &destination_lock_hotkey, + netuid, + LockState { + locked_mass: locked_transfer, + conviction: received_conviction, + last_update: now, + }, + ); + } + + Ok(()) + } + + /// Removes `Lock` entries for `netuid`, resuming from `LastKeptRawKey` when weight is limited. + pub fn remove_network_lock( + netuid: NetUid, + weight_meter: &mut WeightMeter, + last_key: Option>, + ) -> (bool, Option>) { + let iter = match last_key { + Some(key) => Lock::::iter_from(key), + None => Lock::::iter(), + }; + + let (read_all, last_item) = Self::remove_storage_entries_for_netuid( + weight_meter, + iter, + |((_, this_netuid, _), _)| *this_netuid == netuid, + |((coldkey, _this_netuid, hotkey), _)| (coldkey, hotkey), + |(coldkey, hotkey)| Lock::::remove((coldkey.clone(), netuid, hotkey.clone())), + 1, + ); + + ( + read_all, + last_item.map(|((coldkey, _, hotkey), _)| { + Lock::::hashed_key_for((&coldkey, netuid, &hotkey)) + }), + ) + } + + /// Removes `DecayingLock` entries for `netuid`, resuming from `LastKeptRawKey` when weight is limited. + pub fn remove_network_decaying_lock( + netuid: NetUid, + weight_meter: &mut WeightMeter, + last_key: Option>, + ) -> (bool, Option>) { + let iter = match last_key { + Some(raw_key) => DecayingLock::::iter_from(raw_key), + None => DecayingLock::::iter(), + }; + + let (read_all, last_item) = Self::remove_storage_entries_for_netuid( + weight_meter, + iter, + |(_, nu, _)| *nu == netuid, + |(cold, nu, _)| (cold, nu), + |(cold, netuid)| DecayingLock::::remove(cold, netuid), + 1, + ); + + ( + read_all, + last_item.map(|(cold, nu, _)| DecayingLock::::hashed_key_for(&cold, nu)), + ) + } +} diff --git a/pallets/subtensor/src/staking/lock/mod.rs b/pallets/subtensor/src/staking/lock/mod.rs new file mode 100644 index 0000000000..6f3313c73c --- /dev/null +++ b/pallets/subtensor/src/staking/lock/mod.rs @@ -0,0 +1,30 @@ +//! Conviction / exponential stake locks. +//! +//! Locked alpha decays over time; matured conviction is the integral of locked +//! mass and feeds subnet-king selection and unstake availability. +//! +//! ## Search anchors +//! +//! | Module | Owns | +//! |--------|------| +//! | [`conviction_model`] | [`LockState`], [`RollDelta`], [`ConvictionModel`] math | +//! | [`lock_storage`] | Persist / load models, locking-coldkey index, perpetual flag | +//! | [`lock_availability`] | Locked / conviction getters, `available_to_unstake` | +//! | [`lock_operations`] | `do_lock_stake`, aggregate upserts / reductions | +//! | [`subnet_conviction`] | Hotkey totals, `subnet_king`, owner rotation | +//! | [`lock_key_swaps`] | Coldkey / hotkey swap lock migration | +//! | [`lock_transfer`] | `do_move_lock`, `transfer_lock`, network lock wipe | + +use super::*; + +pub mod conviction_model; +pub mod lock_availability; +pub mod lock_key_swaps; +pub mod lock_operations; +pub mod lock_storage; +pub mod lock_transfer; +pub mod subnet_conviction; + +pub use conviction_model::{ + ConvictionModel, LOCK_STATE_ZERO_THRESHOLD, LockState, ONE_YEAR, RollDelta, +}; diff --git a/pallets/subtensor/src/staking/lock/subnet_conviction.rs b/pallets/subtensor/src/staking/lock/subnet_conviction.rs new file mode 100644 index 0000000000..0e209419ce --- /dev/null +++ b/pallets/subtensor/src/staking/lock/subnet_conviction.rs @@ -0,0 +1,451 @@ +//! Aggregate conviction queries and subnet-king (highest-conviction) owner rotation. +use super::*; +use sp_std::collections::btree_map::BTreeMap; +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::NetUid; + +impl Pallet { + /// Returns the total conviction for a hotkey on a subnet, + /// summed over all coldkeys that have locked to this hotkey. + pub fn hotkey_conviction(hotkey: &T::AccountId, netuid: NetUid) -> U64F64 { + let now = Self::get_current_block_as_u64(); + let unlock_rate = UnlockRate::::get(); + let maturity_rate = MaturityRate::::get(); + let perpetual_conviction = HotkeyLock::::get(netuid, hotkey) + .map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + true, + ) + .0 + .conviction + }) + .unwrap_or_else(|| U64F64::saturating_from_num(0)); + let decaying_conviction = DecayingHotkeyLock::::get(netuid, hotkey) + .map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + false, + ) + .0 + .conviction + }) + .unwrap_or_else(|| U64F64::saturating_from_num(0)); + let hotkey_conviction = perpetual_conviction.saturating_add(decaying_conviction); + if hotkey == &SubnetOwnerHotkey::::get(netuid) { + let owner_conviction = OwnerLock::::get(netuid) + .map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + true, + ) + .0 + .conviction + }) + .unwrap_or_else(|| U64F64::saturating_from_num(0)); + let decaying_owner_conviction = DecayingOwnerLock::::get(netuid) + .map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + false, + ) + .0 + .conviction + }) + .unwrap_or_else(|| U64F64::saturating_from_num(0)); + hotkey_conviction + .saturating_add(owner_conviction) + .saturating_add(decaying_owner_conviction) + } else { + hotkey_conviction + } + } + + /// Returns total rolled aggregate conviction across all hotkey and owner locks on a subnet. + pub fn get_total_conviction(netuid: NetUid) -> U64F64 { + let now = Self::get_current_block_as_u64(); + let unlock_rate = UnlockRate::::get(); + let maturity_rate = MaturityRate::::get(); + let hotkey_conviction = HotkeyLock::::iter_prefix(netuid) + .map(|(_hotkey, lock)| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + true, + ) + .0 + .conviction + }) + .fold(U64F64::saturating_from_num(0), |acc, conviction| { + acc.saturating_add(conviction) + }); + let decaying_hotkey_conviction = DecayingHotkeyLock::::iter_prefix(netuid) + .map(|(_hotkey, lock)| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + false, + ) + .0 + .conviction + }) + .fold(U64F64::saturating_from_num(0), |acc, conviction| { + acc.saturating_add(conviction) + }); + let owner_conviction = OwnerLock::::get(netuid) + .map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + true, + ) + .0 + .conviction + }) + .unwrap_or_else(|| U64F64::saturating_from_num(0)); + let decaying_owner_conviction = DecayingOwnerLock::::get(netuid) + .map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + false, + ) + .0 + .conviction + }) + .unwrap_or_else(|| U64F64::saturating_from_num(0)); + + hotkey_conviction + .saturating_add(decaying_hotkey_conviction) + .saturating_add(owner_conviction) + .saturating_add(decaying_owner_conviction) + } + + /// Finds the hotkey with the highest conviction on a given subnet. + pub fn subnet_king(netuid: NetUid) -> Option { + let now = Self::get_current_block_as_u64(); + let unlock_rate = UnlockRate::::get(); + let maturity_rate = MaturityRate::::get(); + let mut scores: BTreeMap = BTreeMap::new(); + + HotkeyLock::::iter_prefix(netuid).for_each(|(hotkey, lock)| { + let rolled = ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + true, + ); + let entry = scores + .entry(hotkey) + .or_insert_with(|| U64F64::saturating_from_num(0)); + *entry = entry.saturating_add(rolled.0.conviction); + }); + DecayingHotkeyLock::::iter_prefix(netuid).for_each(|(hotkey, lock)| { + let rolled = ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + false, + ); + let entry = scores + .entry(hotkey) + .or_insert_with(|| U64F64::saturating_from_num(0)); + *entry = entry.saturating_add(rolled.0.conviction); + }); + if let Some(lock) = OwnerLock::::get(netuid) { + let owner_hotkey = SubnetOwnerHotkey::::get(netuid); + let rolled = ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + true, + ); + let entry = scores + .entry(owner_hotkey) + .or_insert_with(|| U64F64::saturating_from_num(0)); + *entry = entry.saturating_add(rolled.0.conviction); + } + if let Some(lock) = DecayingOwnerLock::::get(netuid) { + let owner_hotkey = SubnetOwnerHotkey::::get(netuid); + let rolled = ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + false, + ); + let entry = scores + .entry(owner_hotkey) + .or_insert_with(|| U64F64::saturating_from_num(0)); + *entry = entry.saturating_add(rolled.0.conviction); + } + + scores + .into_iter() + .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(core::cmp::Ordering::Equal)) + .map(|(hotkey, _)| hotkey) + } + + /// Reassigns subnet ownership to the current lock-conviction leader when the subnet + /// is mature enough and enough conviction has accumulated. + /// + /// Ownership can change only after the subnet is at least [`ONE_YEAR`] old and the + /// total rolled aggregate conviction on the subnet is at least 10% of `SubnetAlphaOut`. + /// If those gates pass, the hotkey with the highest rolled aggregate conviction + /// becomes the subnet owner hotkey, and that hotkey's owning coldkey becomes the + /// subnet owner coldkey. The new owner hotkey's conviction is then progressed to + /// its current locked mass so the new owner starts with full owner conviction. + pub fn change_subnet_owner_if_needed(netuid: NetUid) { + // No outstanding alpha means there is no meaningful 10% conviction threshold. + let subnet_alpha_out = SubnetAlphaOut::::get(netuid); + if subnet_alpha_out.is_zero() { + return; + } + + // Ownership can only be reassigned after the subnet has aged for one year. + let now = Self::get_current_block_as_u64(); + let registered_at = NetworkRegisteredAt::::get(netuid); + if now < registered_at.saturating_add(ONE_YEAR) { + return; + } + + // Require total rolled aggregate conviction to be at least 10% of subnet alpha out. + let total_conviction = Self::get_total_conviction(netuid); + if total_conviction.saturating_mul(U64F64::saturating_from_num(10)) + < U64F64::saturating_from_num(u64::from(subnet_alpha_out)) + { + return; + } + + // Pick the hotkey with the highest rolled aggregate conviction. + let Some(king_hotkey) = Self::subnet_king(netuid) else { + return; + }; + + // The king hotkey must resolve to a real coldkey owner. + let new_owner_coldkey = Self::get_owning_coldkey_for_hotkey(&king_hotkey); + if new_owner_coldkey == DefaultAccount::::get() { + return; + } + + // If the winning hotkey already belongs to the current owner, nothing changes. + let current_owner_coldkey = SubnetOwner::::get(netuid); + if new_owner_coldkey == current_owner_coldkey { + return; + } + let old_owner_hotkey = SubnetOwnerHotkey::::get(netuid); + let unlock_rate = UnlockRate::::get(); + let maturity_rate = MaturityRate::::get(); + + // Register new owner as a neuron if not yet registered. + if Self::get_uid_for_net_and_hotkey(netuid, &king_hotkey).is_err() + && Self::register_neuron(netuid, &king_hotkey).is_err() + { + return; + } + + // Move aggregate buckets using the hotkey's new role. + if let Some(owner_lock) = OwnerLock::::take(netuid) { + let moved_owner_lock = ConvictionModel::roll_forward_lock( + owner_lock, + now, + unlock_rate, + maturity_rate, + true, + true, + ); + let current = HotkeyLock::::get(netuid, &old_owner_hotkey) + .map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + true, + ) + .0 + }) + .unwrap_or_else(|| Self::empty_lock(now)); + Self::insert_hotkey_lock_state( + netuid, + &old_owner_hotkey, + LockState { + locked_mass: current + .locked_mass + .saturating_add(moved_owner_lock.0.locked_mass), + conviction: current + .conviction + .saturating_add(moved_owner_lock.0.conviction), + last_update: now, + }, + ); + } + if let Some(owner_lock) = DecayingOwnerLock::::take(netuid) { + let moved_owner_lock = ConvictionModel::roll_forward_lock( + owner_lock, + now, + unlock_rate, + maturity_rate, + true, + false, + ); + let current = DecayingHotkeyLock::::get(netuid, &old_owner_hotkey) + .map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + false, + false, + ) + .0 + }) + .unwrap_or_else(|| Self::empty_lock(now)); + Self::insert_decaying_hotkey_lock_state( + netuid, + &old_owner_hotkey, + LockState { + locked_mass: current + .locked_mass + .saturating_add(moved_owner_lock.0.locked_mass), + conviction: current + .conviction + .saturating_add(moved_owner_lock.0.conviction), + last_update: now, + }, + ); + } + if let Some(king_lock) = HotkeyLock::::take(netuid, &king_hotkey) { + let moved_king_lock = ConvictionModel::roll_forward_lock( + king_lock, + now, + unlock_rate, + maturity_rate, + false, + true, + ); + let current = OwnerLock::::get(netuid) + .map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + true, + ) + .0 + }) + .unwrap_or_else(|| Self::empty_lock(now)); + Self::insert_owner_lock_state( + netuid, + ConvictionModel::roll_forward_lock( + LockState { + locked_mass: current + .locked_mass + .saturating_add(moved_king_lock.0.locked_mass), + conviction: current + .conviction + .saturating_add(moved_king_lock.0.conviction), + last_update: now, + }, + now, + unlock_rate, + maturity_rate, + true, + true, + ) + .0, + ); + } + if let Some(king_lock) = DecayingHotkeyLock::::take(netuid, &king_hotkey) { + let moved_king_lock = ConvictionModel::roll_forward_lock( + king_lock, + now, + unlock_rate, + maturity_rate, + false, + false, + ); + let current = DecayingOwnerLock::::get(netuid) + .map(|lock| { + ConvictionModel::roll_forward_lock( + lock, + now, + unlock_rate, + maturity_rate, + true, + false, + ) + .0 + }) + .unwrap_or_else(|| Self::empty_lock(now)); + Self::insert_decaying_owner_lock_state( + netuid, + ConvictionModel::roll_forward_lock( + LockState { + locked_mass: current + .locked_mass + .saturating_add(moved_king_lock.0.locked_mass), + conviction: current + .conviction + .saturating_add(moved_king_lock.0.conviction), + last_update: now, + }, + now, + unlock_rate, + maturity_rate, + true, + false, + ) + .0, + ); + } + + // Reassign subnet owner coldkey and owner hotkey. + SubnetOwner::::insert(netuid, new_owner_coldkey.clone()); + SubnetOwnerHotkey::::insert(netuid, king_hotkey.clone()); + Self::deposit_event(Event::SubnetOwnerChanged { + netuid, + old_coldkey: current_owner_coldkey, + new_coldkey: new_owner_coldkey, + }); + } +} diff --git a/pallets/subtensor/src/staking/mod.rs b/pallets/subtensor/src/staking/mod.rs index cf8ac006ac..ea6b5e34ad 100644 --- a/pallets/subtensor/src/staking/mod.rs +++ b/pallets/subtensor/src/staking/mod.rs @@ -1,4 +1,24 @@ +//! Staking: add/remove/move stake, conviction locks, childkeys, and root claims. +//! +//! ## Search anchors +//! +//! | Module | Owns | +//! |--------|------| +//! | [`account`] | Hotkey↔coldkey association (`do_try_associate_hotkey`) | +//! | [`add_stake`] | `do_add_stake` / limit variants | +//! | [`remove_stake`] | `do_remove_stake`, unstake-all, dissolve alpha wipe | +//! | [`move_stake`] | Move / transfer / swap stake between hotkeys or subnets | +//! | [`lock`] | Conviction locks, availability, subnet-king | +//! | [`set_children`] | Parent/child hotkey graphs and childkey take | +//! | [`stake_utils`] | Prices, share pools, swaps, stake validation | +//! | [`helpers`] | Stake totals, ownership, nomination cleanup | +//! | [`claim_root`] | Root claimable dividends and auto-claim | +//! | [`increase_take`] / [`decrease_take`] | Delegate take changes | +//! | [`recycle_alpha`] | Recycle / burn alpha into subnet reserves | +//! | [`order_swap`] | Benchmark helpers for stake AMM orders | + use super::*; + pub mod account; pub mod add_stake; mod claim_root; diff --git a/pallets/subtensor/src/staking/move_stake.rs b/pallets/subtensor/src/staking/move_stake.rs index 4b984ca806..77b5deee64 100644 --- a/pallets/subtensor/src/staking/move_stake.rs +++ b/pallets/subtensor/src/staking/move_stake.rs @@ -1,3 +1,4 @@ +//! Move, transfer, and swap stake across hotkeys and/or subnets. use super::*; use safe_math::*; use sp_core::Get; @@ -39,7 +40,7 @@ impl Pallet { let coldkey = ensure_signed(origin)?; // Validate input and move stake - let tao_moved = Self::transition_stake_internal( + let tao_moved = Self::transition_stake_across_positions( &coldkey, &coldkey, &origin_hotkey, @@ -129,7 +130,7 @@ impl Pallet { let coldkey = ensure_signed(origin)?; // Validate input and move stake - let tao_moved = Self::transition_stake_internal( + let tao_moved = Self::transition_stake_across_positions( &coldkey, &destination_coldkey, &hotkey, @@ -203,7 +204,7 @@ impl Pallet { let coldkey = ensure_signed(origin)?; // Validate input and move stake - let tao_moved = Self::transition_stake_internal( + let tao_moved = Self::transition_stake_across_positions( &coldkey, &destination_coldkey, &origin_hotkey, @@ -268,7 +269,7 @@ impl Pallet { let coldkey = ensure_signed(origin)?; // Validate input and move stake - let tao_moved = Self::transition_stake_internal( + let tao_moved = Self::transition_stake_across_positions( &coldkey, &coldkey, &hotkey, @@ -335,7 +336,7 @@ impl Pallet { let coldkey = ensure_signed(origin)?; // Validate input and move stake - let tao_moved = Self::transition_stake_internal( + let tao_moved = Self::transition_stake_across_positions( &coldkey, &coldkey, &hotkey, @@ -366,7 +367,12 @@ impl Pallet { // If limit_price is None, this is a regular operation, otherwise, it is slippage-protected // by setting limit price between origin_netuid and destination_netuid token - fn transition_stake_internal( + /// Unstake `alpha_amount` from the origin position and restake into the destination. + /// + /// Shared core for move / transfer / swap stake extrinsics. When origin and + /// destination netuids differ, alpha is sold to TAO then bought on the destination + /// subnet (subject to lock and transfer toggles). + fn transition_stake_across_positions( origin_coldkey: &T::AccountId, destination_coldkey: &T::AccountId, origin_hotkey: &T::AccountId, diff --git a/pallets/subtensor/src/staking/order_swap.rs b/pallets/subtensor/src/staking/order_swap.rs index 54c2be4a50..8562c6e209 100644 --- a/pallets/subtensor/src/staking/order_swap.rs +++ b/pallets/subtensor/src/staking/order_swap.rs @@ -1,3 +1,4 @@ +//! Benchmark-only helpers for staking AMM buy/sell order paths. use super::*; use frame_support::transactional; use substrate_fixed::types::U64F64; @@ -14,7 +15,7 @@ impl OrderSwapInterface for Pallet { limit_price: TaoBalance, validate: bool, ) -> Result { - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); Self::ensure_subtoken_enabled(netuid)?; if validate { ensure!( @@ -58,7 +59,7 @@ impl OrderSwapInterface for Pallet { limit_price: TaoBalance, validate: bool, ) -> Result { - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); Self::ensure_subtoken_enabled(netuid)?; if validate { Self::validate_remove_stake( @@ -115,7 +116,7 @@ impl OrderSwapInterface for Pallet { validate_sender: bool, validate_receiver: bool, ) -> DispatchResult { - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); Self::ensure_subtoken_enabled(netuid)?; if validate_sender { ensure!( @@ -171,7 +172,7 @@ impl OrderSwapInterface for Pallet { #[cfg(feature = "runtime-benchmarks")] fn set_up_netuid_for_benchmark(netuid: NetUid) { - if !Self::if_subnet_exist(netuid) { + if !Self::subnet_exists(netuid) { Self::init_new_network(netuid, 100); } SubtokenEnabled::::insert(netuid, true); diff --git a/pallets/subtensor/src/staking/recycle_alpha.rs b/pallets/subtensor/src/staking/recycle_alpha.rs index d640e92b77..2880a608a7 100644 --- a/pallets/subtensor/src/staking/recycle_alpha.rs +++ b/pallets/subtensor/src/staking/recycle_alpha.rs @@ -1,3 +1,4 @@ +//! Recycle or burn alpha back into subnet reserves / protocol sinks. use super::*; use crate::{Error, system::ensure_signed}; use frame_support::storage::{TransactionOutcome, with_transaction}; @@ -24,7 +25,7 @@ impl Pallet { ) -> Result { let coldkey: T::AccountId = ensure_signed(origin)?; - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); ensure!( !netuid.is_root(), @@ -86,7 +87,7 @@ impl Pallet { ) -> Result { let coldkey = ensure_signed(origin)?; - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); ensure!( !netuid.is_root(), @@ -135,7 +136,7 @@ impl Pallet { amount: TaoBalance, limit: Option, ) -> DispatchResult { - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); with_transaction(|| { let result = (|| { let alpha = if let Some(limit) = limit { diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake/destroy_alpha.rs similarity index 59% rename from pallets/subtensor/src/staking/remove_stake.rs rename to pallets/subtensor/src/staking/remove_stake/destroy_alpha.rs index 0c8ed6f6cc..8c54cc0c59 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake/destroy_alpha.rs @@ -1,3 +1,4 @@ +//! Dissolve-path cleanup: destroy alpha in/out stakes, settle, and clear locks. use super::*; use crate::subnets::dissolution::DissolveCleanupStatus; use frame_support::weights::WeightMeter; @@ -5,413 +6,10 @@ use num_traits::ToPrimitive; use sp_std::collections::btree_map::BTreeMap; use sp_std::collections::btree_set::BTreeSet; use substrate_fixed::types::U96F32; -use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; -use subtensor_swap_interface::{Order, SwapHandler}; +use subtensor_runtime_common::{NetUid, TaoBalance, Token}; +use subtensor_swap_interface::SwapHandler; impl Pallet { - /// The implementation for the extrinsic remove_stake: Removes stake from a hotkey account and adds it onto a coldkey. - /// - /// # Arguments - /// * `origin`: The signature of the caller's coldkey. - /// - /// * `hotkey`: The associated hotkey account. - /// - /// * `netuid`: Subnetwork UID. - /// - /// * `alpha_unstaked`: The amount of stake to be removed from the staking account. - /// - /// # Events - /// * `StakeRemoved`: On the successfully removing stake from the hotkey account. - /// - /// # Errors - /// * `NotRegistered`: Thrown if the account we are attempting to unstake from is non existent. - /// - /// * `NonAssociatedColdKey`: Thrown if the coldkey does not own the hotkey we are unstaking from. - /// - /// * `NotEnoughStakeToWithdraw`: Thrown if there is not enough stake on the hotkey to withdwraw this amount. - /// - /// * `TxRateLimitExceeded`: Thrown if key has hit transaction rate limit. - /// - pub fn do_remove_stake( - origin: OriginFor, - hotkey: T::AccountId, - netuid: NetUid, - alpha_unstaked: AlphaBalance, - ) -> dispatch::DispatchResult { - // 1. We check the transaction is signed by the caller and retrieve the T::AccountId coldkey information. - let coldkey = ensure_signed(origin)?; - log::debug!( - "do_remove_stake( origin:{coldkey:?} hotkey:{hotkey:?}, netuid: {netuid:?}, alpha_unstaked:{alpha_unstaked:?} )" - ); - - Self::ensure_subtoken_enabled(netuid)?; - - // 1.1. Cap the alpha_unstaked at available Alpha because user might be paying transaxtion fees - // in Alpha and their total is already reduced by now. - let alpha_available = - Self::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); - let alpha_unstaked = alpha_unstaked.min(alpha_available); - Self::ensure_remove_stake_input_within_swap_limit(netuid, alpha_unstaked)?; - - // 2. Validate the user input - Self::validate_remove_stake( - &coldkey, - &hotkey, - netuid, - alpha_unstaked, - alpha_unstaked, - false, - )?; - - // 3. Swap the alpba to tao and update counters for this subnet. - Self::unstake_from_subnet( - &hotkey, - &coldkey, - &coldkey, - netuid, - alpha_unstaked, - T::SwapInterface::min_price(), - false, - )?; - - // 5. If the stake is below the minimum, we clear the nomination from storage. - Self::clear_small_nomination_if_required(&hotkey, &coldkey, netuid); - - // 6. Check if stake lowered below MinStake and remove Pending children if it did - if Self::get_total_stake_for_hotkey(&hotkey) < StakeThreshold::::get().into() { - Self::get_all_subnet_netuids().iter().for_each(|netuid| { - PendingChildKeys::::remove(netuid, &hotkey); - }) - } - - // Done and ok. - Ok(()) - } - - /// The implementation for the extrinsic unstake_all: Removes all stake from a hotkey account across all subnets and adds it onto a coldkey. - /// - /// # Arguments - /// * `origin`: The signature of the caller's coldkey. - /// - /// * `hotkey`: The associated hotkey account. - /// - /// # Events - /// * `StakeRemoved`: On the successfully removing stake from the hotkey account. - /// - /// # Errors - /// * `NotRegistered`: Thrown if the account we are attempting to unstake from is non existent. - /// - /// * `NonAssociatedColdKey`: Thrown if the coldkey does not own the hotkey we are unstaking from. - /// - /// * `NotEnoughStakeToWithdraw`: Thrown if there is not enough stake on the hotkey to withdraw this amount. - /// - /// * `TxRateLimitExceeded`: Thrown if key has hit transaction rate limit. - /// - pub fn do_unstake_all(origin: OriginFor, hotkey: T::AccountId) -> dispatch::DispatchResult { - // 1. We check the transaction is signed by the caller and retrieve the T::AccountId coldkey information. - let coldkey = ensure_signed(origin)?; - log::debug!("do_unstake_all( origin:{coldkey:?} hotkey:{hotkey:?} )"); - - // 2. Ensure that the hotkey account exists this is only possible through registration. - ensure!( - Self::hotkey_account_exists(&hotkey), - Error::::HotKeyAccountNotExists - ); - - // 3. Get all netuids. - let netuids = Self::get_all_subnet_netuids(); - log::debug!("All subnet netuids: {netuids:?}"); - - // 4. Iterate through all subnets and remove stake. - for netuid in netuids.into_iter() { - if !SubtokenEnabled::::get(netuid) { - continue; - } - // Ensure that the hotkey has enough stake to withdraw. - let alpha_unstaked = - Self::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); - - if Self::validate_remove_stake( - &coldkey, - &hotkey, - netuid, - alpha_unstaked, - alpha_unstaked, - false, - ) - .is_err() - { - // Don't unstake from this netuid - continue; - } - - if !alpha_unstaked.is_zero() { - // Swap the alpha to tao and update counters for this subnet. - Self::unstake_from_subnet( - &hotkey, - &coldkey, - &coldkey, - netuid, - alpha_unstaked, - T::SwapInterface::min_price(), - false, - )?; - - // If the stake is below the minimum, we clear the nomination from storage. - Self::clear_small_nomination_if_required(&hotkey, &coldkey, netuid); - } - } - - // 5. Done and ok. - Ok(()) - } - - /// The implementation for the extrinsic unstake_all: Removes all stake from a hotkey account across all subnets and adds it onto a coldkey. - /// - /// # Arguments - /// * `origin`: The signature of the caller's coldkey. - /// - /// * `hotkey`: The associated hotkey account. - /// - /// # Events - /// * `StakeRemoved`: On the successfully removing stake from the hotkey account. - /// - /// # Errors - /// * `NotRegistered`: Thrown if the account we are attempting to unstake from is non existent. - /// - /// * `NonAssociatedColdKey`: Thrown if the coldkey does not own the hotkey we are unstaking from. - /// - /// * `NotEnoughStakeToWithdraw`: Thrown if there is not enough stake on the hotkey to withdraw this amount. - /// - /// * `TxRateLimitExceeded`: Thrown if key has hit transaction rate limit. - /// - pub fn do_unstake_all_alpha( - origin: OriginFor, - hotkey: T::AccountId, - ) -> dispatch::DispatchResult { - // 1. We check the transaction is signed by the caller and retrieve the T::AccountId coldkey information. - let coldkey = ensure_signed(origin)?; - log::debug!("do_unstake_all( origin:{coldkey:?} hotkey:{hotkey:?} )"); - - // 2. Ensure that the hotkey account exists this is only possible through registration. - ensure!( - Self::hotkey_account_exists(&hotkey), - Error::::HotKeyAccountNotExists - ); - - // 3. Get all netuids. - let netuids = Self::get_all_subnet_netuids(); - log::debug!("All subnet netuids: {netuids:?}"); - - // 4. Iterate through all subnets and remove stake. - let mut total_tao_unstaked = TaoBalance::ZERO; - for netuid in netuids.into_iter() { - if !SubtokenEnabled::::get(netuid) { - continue; - } - // If not Root network. - if !netuid.is_root() { - // Ensure that the hotkey has enough stake to withdraw. - let alpha_unstaked = - Self::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); - - if Self::validate_remove_stake( - &coldkey, - &hotkey, - netuid, - alpha_unstaked, - alpha_unstaked, - false, - ) - .is_err() - { - // Don't unstake from this netuid - continue; - } - - if !alpha_unstaked.is_zero() { - // Swap the alpha to tao and update counters for this subnet. - let tao_unstaked = Self::unstake_from_subnet( - &hotkey, - &coldkey, - &coldkey, - netuid, - alpha_unstaked, - T::SwapInterface::min_price(), - false, - )?; - - // Increment total - total_tao_unstaked = total_tao_unstaked.saturating_add(tao_unstaked); - - // If the stake is below the minimum, we clear the nomination from storage. - Self::clear_small_nomination_if_required(&hotkey, &coldkey, netuid); - } - } - } - - // Stake into root. - Self::stake_into_subnet( - &hotkey, - &coldkey, - NetUid::ROOT, - total_tao_unstaked, - T::SwapInterface::max_price(), - false, - )?; - - // 5. Done and ok. - Ok(()) - } - - /// The implementation for the extrinsic remove_stake_limit: Removes stake from - /// a hotkey on a subnet with a price limit. - /// - /// In case if slippage occurs and the price shall move beyond the limit - /// price, the staking order may execute only partially or not execute - /// at all. - /// - /// # Arguments - /// * `origin`: The signature of the caller's coldkey. - /// - /// * `hotkey`: The associated hotkey account. - /// - /// * `netuid`: Subnetwork UID. - /// - /// * `amount_unstaked`: The amount of stake to be added to the hotkey staking account. - /// - /// * `limit_price`: The limit price expressed in units of RAO per one Alpha. - /// - /// * `allow_partial`: Allows partial execution of the amount. If set to false, this becomes - /// fill or kill type of order. - /// - /// # Events - /// * `StakeRemoved`: On the successfully removing stake from the hotkey account. - /// - /// # Errors - /// * `NotRegistered`: Thrown if the account we are attempting to unstake from is non existent. - /// - /// * `NonAssociatedColdKey`: Thrown if the coldkey does not own the hotkey we are unstaking from. - /// - /// * `NotEnoughStakeToWithdraw`: Thrown if there is not enough stake on the hotkey to withdwraw this amount. - /// - pub fn do_remove_stake_limit( - origin: OriginFor, - hotkey: T::AccountId, - netuid: NetUid, - alpha_unstaked: AlphaBalance, - limit_price: TaoBalance, - allow_partial: bool, - ) -> dispatch::DispatchResult { - // 1. We check the transaction is signed by the caller and retrieve the T::AccountId coldkey information. - let coldkey = ensure_signed(origin)?; - log::debug!( - "do_remove_stake( origin:{coldkey:?} hotkey:{hotkey:?}, netuid: {netuid:?}, alpha_unstaked:{alpha_unstaked:?} )" - ); - - Self::ensure_remove_stake_input_within_swap_limit(netuid, alpha_unstaked)?; - - // 2. Calculate the maximum amount that can be executed with price limit - let max_amount = Self::get_max_amount_remove(netuid, limit_price)?; - let mut possible_alpha = alpha_unstaked; - if possible_alpha > max_amount { - possible_alpha = max_amount; - } - - // 3. Validate the user input - Self::validate_remove_stake( - &coldkey, - &hotkey, - netuid, - alpha_unstaked, - max_amount, - allow_partial, - )?; - - // 4. Swap the alpha to tao and update counters for this subnet. - Self::unstake_from_subnet( - &hotkey, - &coldkey, - &coldkey, - netuid, - possible_alpha, - limit_price, - false, - )?; - - // 5. If the stake is below the minimum, we clear the nomination from storage. - Self::clear_small_nomination_if_required(&hotkey, &coldkey, netuid); - - // 6. Check if stake lowered below MinStake and remove Pending children if it did - if Self::get_total_stake_for_hotkey(&hotkey) < StakeThreshold::::get().into() { - Self::get_all_subnet_netuids().iter().for_each(|netuid| { - PendingChildKeys::::remove(netuid, &hotkey); - }) - } - - // Done and ok. - Ok(()) - } - - // Returns the maximum amount of RAO that can be executed with price limit - pub fn get_max_amount_remove( - netuid: NetUid, - limit_price: TaoBalance, - ) -> Result { - // Corner case: root and stao - // There's no slippage for root or stable subnets, so if limit price is 1e9 rao or - // lower, then max_amount equals u64::MAX, otherwise it is 0. - if netuid.is_root() || SubnetMechanism::::get(netuid) == 0 { - if limit_price <= 1_000_000_000.into() { - return Ok(AlphaBalance::MAX); - } else { - return Ok(AlphaBalance::ZERO); - } - } - - // Use the largest supported input instead of probing the swap path with u64::MAX. - let max_supported_input = SubnetAlphaIn::::get(netuid).saturating_mul(1_000.into()); - let order = GetTaoForAlpha::::with_amount(max_supported_input); - let result = T::SwapInterface::swap(netuid.into(), order, limit_price.into(), false, true) - .map(|r| r.amount_paid_in.saturating_add(r.fee_paid))?; - - Ok(result) - } - - fn ensure_remove_stake_input_within_swap_limit( - netuid: NetUid, - amount: AlphaBalance, - ) -> Result<(), Error> { - if !netuid.is_root() && SubnetMechanism::::get(netuid) == 1 { - let max_supported_input = SubnetAlphaIn::::get(netuid).saturating_mul(1_000.into()); - ensure!( - amount <= max_supported_input, - Error::::InsufficientLiquidity - ); - } - - Ok(()) - } - - pub fn do_remove_stake_full_limit( - origin: OriginFor, - hotkey: T::AccountId, - netuid: NetUid, - limit_price: Option, - ) -> DispatchResult { - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); - let coldkey = ensure_signed(origin.clone())?; - - let alpha_unstaked = - Self::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); - - if let Some(limit_price) = limit_price { - Self::do_remove_stake_limit(origin, hotkey, netuid, alpha_unstaked, limit_price, false) - } else { - Self::do_remove_stake(origin, hotkey, netuid, alpha_unstaked) - } - } - pub fn destroy_alpha_in_out_stakes( netuid: NetUid, weight_meter: &mut WeightMeter, diff --git a/pallets/subtensor/src/staking/remove_stake/mod.rs b/pallets/subtensor/src/staking/remove_stake/mod.rs new file mode 100644 index 0000000000..3d906a6f3b --- /dev/null +++ b/pallets/subtensor/src/staking/remove_stake/mod.rs @@ -0,0 +1,13 @@ +//! Remove / unstake alpha and dissolve-time alpha destruction. +//! +//! ## Search anchors +//! +//! | Module | Owns | +//! |--------|------| +//! | [`remove_stake_ops`] | `do_remove_stake`, `do_unstake_all`, limit helpers | +//! | [`destroy_alpha`] | `destroy_alpha_in_out_stakes` dissolve pipeline | + +use super::*; + +pub mod destroy_alpha; +pub mod remove_stake_ops; diff --git a/pallets/subtensor/src/staking/remove_stake/remove_stake_ops.rs b/pallets/subtensor/src/staking/remove_stake/remove_stake_ops.rs new file mode 100644 index 0000000000..4b28e59edc --- /dev/null +++ b/pallets/subtensor/src/staking/remove_stake/remove_stake_ops.rs @@ -0,0 +1,409 @@ +//! Extrinsic bodies for `remove_stake`, `unstake_all`, and limit variants. +use super::*; +use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; +use subtensor_swap_interface::{Order, SwapHandler}; + +impl Pallet { + /// The implementation for the extrinsic remove_stake: Removes stake from a hotkey account and adds it onto a coldkey. + /// + /// # Arguments + /// * `origin`: The signature of the caller's coldkey. + /// + /// * `hotkey`: The associated hotkey account. + /// + /// * `netuid`: Subnetwork UID. + /// + /// * `alpha_unstaked`: The amount of stake to be removed from the staking account. + /// + /// # Events + /// * `StakeRemoved`: On the successfully removing stake from the hotkey account. + /// + /// # Errors + /// * `NotRegistered`: Thrown if the account we are attempting to unstake from is non existent. + /// + /// * `NonAssociatedColdKey`: Thrown if the coldkey does not own the hotkey we are unstaking from. + /// + /// * `NotEnoughStakeToWithdraw`: Thrown if there is not enough stake on the hotkey to withdwraw this amount. + /// + /// * `TxRateLimitExceeded`: Thrown if key has hit transaction rate limit. + /// + pub fn do_remove_stake( + origin: OriginFor, + hotkey: T::AccountId, + netuid: NetUid, + alpha_unstaked: AlphaBalance, + ) -> dispatch::DispatchResult { + // 1. We check the transaction is signed by the caller and retrieve the T::AccountId coldkey information. + let coldkey = ensure_signed(origin)?; + log::debug!( + "do_remove_stake( origin:{coldkey:?} hotkey:{hotkey:?}, netuid: {netuid:?}, alpha_unstaked:{alpha_unstaked:?} )" + ); + + Self::ensure_subtoken_enabled(netuid)?; + + // 1.1. Cap the alpha_unstaked at available Alpha because user might be paying transaxtion fees + // in Alpha and their total is already reduced by now. + let alpha_available = + Self::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + let alpha_unstaked = alpha_unstaked.min(alpha_available); + Self::ensure_remove_stake_input_within_swap_limit(netuid, alpha_unstaked)?; + + // 2. Validate the user input + Self::validate_remove_stake( + &coldkey, + &hotkey, + netuid, + alpha_unstaked, + alpha_unstaked, + false, + )?; + + // 3. Swap the alpba to tao and update counters for this subnet. + Self::unstake_from_subnet( + &hotkey, + &coldkey, + &coldkey, + netuid, + alpha_unstaked, + T::SwapInterface::min_price(), + false, + )?; + + // 5. If the stake is below the minimum, we clear the nomination from storage. + Self::clear_small_nomination_if_required(&hotkey, &coldkey, netuid); + + // 6. Check if stake lowered below MinStake and remove Pending children if it did + if Self::get_total_stake_for_hotkey(&hotkey) < StakeThreshold::::get().into() { + Self::get_all_subnet_netuids().iter().for_each(|netuid| { + PendingChildKeys::::remove(netuid, &hotkey); + }) + } + + // Done and ok. + Ok(()) + } + + /// The implementation for the extrinsic unstake_all: Removes all stake from a hotkey account across all subnets and adds it onto a coldkey. + /// + /// # Arguments + /// * `origin`: The signature of the caller's coldkey. + /// + /// * `hotkey`: The associated hotkey account. + /// + /// # Events + /// * `StakeRemoved`: On the successfully removing stake from the hotkey account. + /// + /// # Errors + /// * `NotRegistered`: Thrown if the account we are attempting to unstake from is non existent. + /// + /// * `NonAssociatedColdKey`: Thrown if the coldkey does not own the hotkey we are unstaking from. + /// + /// * `NotEnoughStakeToWithdraw`: Thrown if there is not enough stake on the hotkey to withdraw this amount. + /// + /// * `TxRateLimitExceeded`: Thrown if key has hit transaction rate limit. + /// + pub fn do_unstake_all(origin: OriginFor, hotkey: T::AccountId) -> dispatch::DispatchResult { + // 1. We check the transaction is signed by the caller and retrieve the T::AccountId coldkey information. + let coldkey = ensure_signed(origin)?; + log::debug!("do_unstake_all( origin:{coldkey:?} hotkey:{hotkey:?} )"); + + // 2. Ensure that the hotkey account exists this is only possible through registration. + ensure!( + Self::hotkey_account_exists(&hotkey), + Error::::HotKeyAccountNotExists + ); + + // 3. Get all netuids. + let netuids = Self::get_all_subnet_netuids(); + log::debug!("All subnet netuids: {netuids:?}"); + + // 4. Iterate through all subnets and remove stake. + for netuid in netuids.into_iter() { + if !SubtokenEnabled::::get(netuid) { + continue; + } + // Ensure that the hotkey has enough stake to withdraw. + let alpha_unstaked = + Self::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + + if Self::validate_remove_stake( + &coldkey, + &hotkey, + netuid, + alpha_unstaked, + alpha_unstaked, + false, + ) + .is_err() + { + // Don't unstake from this netuid + continue; + } + + if !alpha_unstaked.is_zero() { + // Swap the alpha to tao and update counters for this subnet. + Self::unstake_from_subnet( + &hotkey, + &coldkey, + &coldkey, + netuid, + alpha_unstaked, + T::SwapInterface::min_price(), + false, + )?; + + // If the stake is below the minimum, we clear the nomination from storage. + Self::clear_small_nomination_if_required(&hotkey, &coldkey, netuid); + } + } + + // 5. Done and ok. + Ok(()) + } + + /// The implementation for the extrinsic unstake_all: Removes all stake from a hotkey account across all subnets and adds it onto a coldkey. + /// + /// # Arguments + /// * `origin`: The signature of the caller's coldkey. + /// + /// * `hotkey`: The associated hotkey account. + /// + /// # Events + /// * `StakeRemoved`: On the successfully removing stake from the hotkey account. + /// + /// # Errors + /// * `NotRegistered`: Thrown if the account we are attempting to unstake from is non existent. + /// + /// * `NonAssociatedColdKey`: Thrown if the coldkey does not own the hotkey we are unstaking from. + /// + /// * `NotEnoughStakeToWithdraw`: Thrown if there is not enough stake on the hotkey to withdraw this amount. + /// + /// * `TxRateLimitExceeded`: Thrown if key has hit transaction rate limit. + /// + pub fn do_unstake_all_alpha( + origin: OriginFor, + hotkey: T::AccountId, + ) -> dispatch::DispatchResult { + // 1. We check the transaction is signed by the caller and retrieve the T::AccountId coldkey information. + let coldkey = ensure_signed(origin)?; + log::debug!("do_unstake_all( origin:{coldkey:?} hotkey:{hotkey:?} )"); + + // 2. Ensure that the hotkey account exists this is only possible through registration. + ensure!( + Self::hotkey_account_exists(&hotkey), + Error::::HotKeyAccountNotExists + ); + + // 3. Get all netuids. + let netuids = Self::get_all_subnet_netuids(); + log::debug!("All subnet netuids: {netuids:?}"); + + // 4. Iterate through all subnets and remove stake. + let mut total_tao_unstaked = TaoBalance::ZERO; + for netuid in netuids.into_iter() { + if !SubtokenEnabled::::get(netuid) { + continue; + } + // If not Root network. + if !netuid.is_root() { + // Ensure that the hotkey has enough stake to withdraw. + let alpha_unstaked = + Self::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + + if Self::validate_remove_stake( + &coldkey, + &hotkey, + netuid, + alpha_unstaked, + alpha_unstaked, + false, + ) + .is_err() + { + // Don't unstake from this netuid + continue; + } + + if !alpha_unstaked.is_zero() { + // Swap the alpha to tao and update counters for this subnet. + let tao_unstaked = Self::unstake_from_subnet( + &hotkey, + &coldkey, + &coldkey, + netuid, + alpha_unstaked, + T::SwapInterface::min_price(), + false, + )?; + + // Increment total + total_tao_unstaked = total_tao_unstaked.saturating_add(tao_unstaked); + + // If the stake is below the minimum, we clear the nomination from storage. + Self::clear_small_nomination_if_required(&hotkey, &coldkey, netuid); + } + } + } + + // Stake into root. + Self::stake_into_subnet( + &hotkey, + &coldkey, + NetUid::ROOT, + total_tao_unstaked, + T::SwapInterface::max_price(), + false, + )?; + + // 5. Done and ok. + Ok(()) + } + + /// The implementation for the extrinsic remove_stake_limit: Removes stake from + /// a hotkey on a subnet with a price limit. + /// + /// In case if slippage occurs and the price shall move beyond the limit + /// price, the staking order may execute only partially or not execute + /// at all. + /// + /// # Arguments + /// * `origin`: The signature of the caller's coldkey. + /// + /// * `hotkey`: The associated hotkey account. + /// + /// * `netuid`: Subnetwork UID. + /// + /// * `amount_unstaked`: The amount of stake to be added to the hotkey staking account. + /// + /// * `limit_price`: The limit price expressed in units of RAO per one Alpha. + /// + /// * `allow_partial`: Allows partial execution of the amount. If set to false, this becomes + /// fill or kill type of order. + /// + /// # Events + /// * `StakeRemoved`: On the successfully removing stake from the hotkey account. + /// + /// # Errors + /// * `NotRegistered`: Thrown if the account we are attempting to unstake from is non existent. + /// + /// * `NonAssociatedColdKey`: Thrown if the coldkey does not own the hotkey we are unstaking from. + /// + /// * `NotEnoughStakeToWithdraw`: Thrown if there is not enough stake on the hotkey to withdwraw this amount. + /// + pub fn do_remove_stake_limit( + origin: OriginFor, + hotkey: T::AccountId, + netuid: NetUid, + alpha_unstaked: AlphaBalance, + limit_price: TaoBalance, + allow_partial: bool, + ) -> dispatch::DispatchResult { + // 1. We check the transaction is signed by the caller and retrieve the T::AccountId coldkey information. + let coldkey = ensure_signed(origin)?; + log::debug!( + "do_remove_stake( origin:{coldkey:?} hotkey:{hotkey:?}, netuid: {netuid:?}, alpha_unstaked:{alpha_unstaked:?} )" + ); + + Self::ensure_remove_stake_input_within_swap_limit(netuid, alpha_unstaked)?; + + // 2. Calculate the maximum amount that can be executed with price limit + let max_amount = Self::get_max_amount_remove(netuid, limit_price)?; + let mut possible_alpha = alpha_unstaked; + if possible_alpha > max_amount { + possible_alpha = max_amount; + } + + // 3. Validate the user input + Self::validate_remove_stake( + &coldkey, + &hotkey, + netuid, + alpha_unstaked, + max_amount, + allow_partial, + )?; + + // 4. Swap the alpha to tao and update counters for this subnet. + Self::unstake_from_subnet( + &hotkey, + &coldkey, + &coldkey, + netuid, + possible_alpha, + limit_price, + false, + )?; + + // 5. If the stake is below the minimum, we clear the nomination from storage. + Self::clear_small_nomination_if_required(&hotkey, &coldkey, netuid); + + // 6. Check if stake lowered below MinStake and remove Pending children if it did + if Self::get_total_stake_for_hotkey(&hotkey) < StakeThreshold::::get().into() { + Self::get_all_subnet_netuids().iter().for_each(|netuid| { + PendingChildKeys::::remove(netuid, &hotkey); + }) + } + + // Done and ok. + Ok(()) + } + + // Returns the maximum amount of RAO that can be executed with price limit + pub fn get_max_amount_remove( + netuid: NetUid, + limit_price: TaoBalance, + ) -> Result { + // Corner case: root and stao + // There's no slippage for root or stable subnets, so if limit price is 1e9 rao or + // lower, then max_amount equals u64::MAX, otherwise it is 0. + if netuid.is_root() || SubnetMechanism::::get(netuid) == 0 { + if limit_price <= 1_000_000_000.into() { + return Ok(AlphaBalance::MAX); + } else { + return Ok(AlphaBalance::ZERO); + } + } + + // Use the largest supported input instead of probing the swap path with u64::MAX. + let max_supported_input = SubnetAlphaIn::::get(netuid).saturating_mul(1_000.into()); + let order = GetTaoForAlpha::::with_amount(max_supported_input); + let result = T::SwapInterface::swap(netuid.into(), order, limit_price.into(), false, true) + .map(|r| r.amount_paid_in.saturating_add(r.fee_paid))?; + + Ok(result) + } + + fn ensure_remove_stake_input_within_swap_limit( + netuid: NetUid, + amount: AlphaBalance, + ) -> Result<(), Error> { + if !netuid.is_root() && SubnetMechanism::::get(netuid) == 1 { + let max_supported_input = SubnetAlphaIn::::get(netuid).saturating_mul(1_000.into()); + ensure!( + amount <= max_supported_input, + Error::::InsufficientLiquidity + ); + } + + Ok(()) + } + + pub fn do_remove_stake_full_limit( + origin: OriginFor, + hotkey: T::AccountId, + netuid: NetUid, + limit_price: Option, + ) -> DispatchResult { + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); + let coldkey = ensure_signed(origin.clone())?; + + let alpha_unstaked = + Self::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + + if let Some(limit_price) = limit_price { + Self::do_remove_stake_limit(origin, hotkey, netuid, alpha_unstaked, limit_price, false) + } else { + Self::do_remove_stake(origin, hotkey, netuid, alpha_unstaked) + } + } +} diff --git a/pallets/subtensor/src/staking/set_children.rs b/pallets/subtensor/src/staking/set_children.rs deleted file mode 100644 index 2c516eda5d..0000000000 --- a/pallets/subtensor/src/staking/set_children.rs +++ /dev/null @@ -1,1114 +0,0 @@ -use super::*; - -use sp_runtime::PerU16; -use sp_std::collections::{btree_map::BTreeMap, btree_set::BTreeSet}; -use subtensor_runtime_common::NetUid; - -pub struct PCRelations { - /// The distinguished `hotkey` this structure is built around. - pivot: T::AccountId, - children: BTreeMap, - parents: BTreeMap, -} - -impl PCRelations { - /// Create empty relations for a given pivot. - pub fn new(hotkey: T::AccountId) -> Self { - Self { - pivot: hotkey, - children: BTreeMap::new(), - parents: BTreeMap::new(), - } - } - - //////////////////////////////////////////////////////////// - // Constraint checkers - - /// Ensures sum(proportions) <= u64::MAX - pub fn ensure_total_proportions(children: &BTreeMap) -> DispatchResult { - let total: u128 = children - .values() - .fold(0u128, |acc, &w| acc.saturating_add(w as u128)); - ensure!(total <= u64::MAX as u128, Error::::ProportionOverflow); - Ok(()) - } - - /// Ensure that the number of children does not exceed 5 - pub fn ensure_childkey_count(children: &BTreeMap) -> DispatchResult { - ensure!(children.len() <= 5, Error::::TooManyChildren); - - Ok(()) - } - - /// Ensures the given children or parent set doesn't contain pivot - pub fn ensure_no_self_loop( - pivot: &T::AccountId, - hotkey_set: &BTreeMap, - ) -> DispatchResult { - ensure!(!hotkey_set.contains_key(pivot), Error::::InvalidChild); - Ok(()) - } - - /// Ensures that children and parents sets do not have any overlap - pub fn ensure_bipartite_separation( - children: &BTreeMap, - parents: &BTreeMap, - ) -> DispatchResult { - let has_overlap = children.keys().any(|c| parents.contains_key(c)); - ensure!(!has_overlap, Error::::ChildParentInconsistency); - Ok(()) - } - - /// Validate that applying `pending_children_vec` to `relations` (as the new - /// pivot->children mapping) preserves all invariants. - /// - /// Checks: - /// 1) No self-loop: pivot must not appear among children. - /// 2) Sum of child proportions fits in `u64`. - /// 3) Bipartite role separation: no child may also be a parent. - pub fn ensure_pending_consistency( - &self, - pending_children_vec: &Vec<(u64, T::AccountId)>, - ) -> DispatchResult { - // Build a deduped children map (last proportion wins if duplicates present). - let mut new_children: BTreeMap = BTreeMap::new(); - for (prop, child) in pending_children_vec { - new_children.insert(child.clone(), *prop); - } - - // Check constraints - Self::ensure_no_self_loop(&self.pivot, &new_children)?; - Self::ensure_childkey_count(&new_children)?; - Self::ensure_total_proportions(&new_children)?; - Self::ensure_bipartite_separation(&new_children, &self.parents)?; - - Ok(()) - } - - //////////////////////////////////////////////////////////// - // Getters - - #[inline] - pub fn pivot(&self) -> &T::AccountId { - &self.pivot - } - #[inline] - pub fn children(&self) -> &BTreeMap { - &self.children - } - #[inline] - pub fn parents(&self) -> &BTreeMap { - &self.parents - } - - //////////////////////////////////////////////////////////// - // Safe updaters - - /// Replace the pivot->children mapping after validating invariants. - /// - /// Invariants: - /// * No self-loop: child != pivot - /// * sum(proportions) fits in u64 (checked as u128 to avoid overflow mid-sum) - pub fn link_children(&mut self, new_children: BTreeMap) -> DispatchResult { - // Check constraints - Self::ensure_no_self_loop(&self.pivot, &new_children)?; - Self::ensure_total_proportions(&new_children)?; - Self::ensure_bipartite_separation(&new_children, &self.parents)?; - - self.children = new_children; - Ok(()) - } - - pub fn link_parents(&mut self, new_parents: BTreeMap) -> DispatchResult { - // Check constraints - Self::ensure_no_self_loop(&self.pivot, &new_parents)?; - Self::ensure_bipartite_separation(&self.children, &new_parents)?; - - self.parents = new_parents; - Ok(()) - } - - #[inline] - fn upsert_edge(list: &mut Vec<(u64, T::AccountId)>, proportion: u64, id: &T::AccountId) { - for (p, who) in list.iter_mut() { - if who == id { - *p = proportion; - return; - } - } - list.push((proportion, id.clone())); - } - - #[inline] - fn remove_edge(list: &mut Vec<(u64, T::AccountId)>, id: &T::AccountId) { - list.retain(|(_, who)| who != id); - } - - /// Change the pivot hotkey for these relations. - /// Ensures there are no self-loops with the new pivot. - pub fn rebind_pivot(&mut self, new_pivot: T::AccountId) -> DispatchResult { - // No self-loop via children or parents for the new pivot. - Self::ensure_no_self_loop(&new_pivot, &self.children)?; - Self::ensure_no_self_loop(&new_pivot, &self.parents)?; - - self.pivot = new_pivot; - Ok(()) - } -} - -impl Pallet { - /// Set childkeys vector making sure there are no empty vectors in the state - fn set_childkeys(parent: T::AccountId, netuid: NetUid, childkey_vec: Vec<(u64, T::AccountId)>) { - if childkey_vec.is_empty() { - ChildKeys::::remove(parent, netuid); - } else { - ChildKeys::::insert(parent, netuid, childkey_vec); - } - } - - /// Set parentkeys vector making sure there are no empty vectors in the state - fn set_parentkeys( - child: T::AccountId, - netuid: NetUid, - parentkey_vec: Vec<(u64, T::AccountId)>, - ) { - if parentkey_vec.is_empty() { - ParentKeys::::remove(child, netuid); - } else { - ParentKeys::::insert(child, netuid, parentkey_vec); - } - } - - /// Loads all records from ChildKeys and ParentKeys where (hotkey, netuid) is the key. - /// Produces a parent->(child->prop) adjacency map that **cannot violate** - /// the required consistency because all inserts go through `link`. - fn load_child_parent_relations( - hotkey: &T::AccountId, - netuid: NetUid, - ) -> Result, DispatchError> { - let mut rel = PCRelations::::new(hotkey.clone()); - - // Load children: (prop, child) from ChildKeys(hotkey, netuid) - let child_links = ChildKeys::::get(hotkey, netuid); - let mut children = BTreeMap::::new(); - for (prop, child) in child_links { - // Ignore any accidental self-loop in storage - if child != *hotkey { - children.insert(child, prop); - } - } - // Validate & set (enforce no self-loop and sum limit) - rel.link_children(children)?; - - // Load parents: (prop, parent) from ParentKeys(hotkey, netuid) - let parent_links = ParentKeys::::get(hotkey, netuid); - let mut parents = BTreeMap::::new(); - for (prop, parent) in parent_links { - if parent != *hotkey { - parents.insert(parent, prop); - } - } - // Keep the same validation rules for parents (no self-loop, bounded sum). - rel.link_parents(parents)?; - - Ok(rel) - } - - /// Build a `PCRelations` for `pivot` (parent) from the `PendingChildKeys` queue, - /// preserving the current `ParentKeys(pivot, netuid)` so `persist_child_parent_relations` - /// won’t accidentally clear existing parents. - /// - /// PendingChildKeys layout: - /// (netuid, pivot) -> (Vec<(proportion, child)>) - pub fn load_relations_from_pending( - pivot: T::AccountId, - pending_children_vec: &Vec<(u64, T::AccountId)>, - netuid: NetUid, - ) -> Result, DispatchError> { - let mut rel = PCRelations::::new(pivot.clone()); - - // Deduplicate into a BTreeMap (last wins if duplicates). - let mut children: BTreeMap = BTreeMap::new(); - for (prop, child) in pending_children_vec { - if *child != pivot { - children.insert(child.clone(), *prop); - } - } - - // Enforce invariants (no self-loop, total weight <= u64::MAX) - rel.link_children(children)?; - - // Preserve the current parents of the pivot so `persist_child_parent_relations` - // won’t clear them when we only intend to update children. - let existing_parents_vec = ParentKeys::::get(pivot.clone(), netuid); - let mut parents: BTreeMap = BTreeMap::new(); - for (w, parent) in existing_parents_vec { - if parent != pivot { - parents.insert(parent, w); - } - } - // This uses the same basic checks (no self-loop, bounded sum). - // If you didn't expose link_parents, inline the simple validations here. - rel.link_parents(parents)?; - - Ok(rel) - } - - /// Persist the `relations` around `hotkey` to storage, updating both directions: - /// * Writes ChildKeys(hotkey, netuid) = children - /// and synchronizes ParentKeys(child, netuid) entries accordingly. - /// * Writes ParentKeys(hotkey, netuid) = parents - /// and synchronizes ChildKeys(parent, netuid) entries accordingly. - /// - /// This is a **diff-based** update that only touches affected neighbors. - pub fn persist_child_parent_relations( - relations: PCRelations, - netuid: NetUid, - weight: &mut Weight, - ) -> DispatchResult { - let pivot = relations.pivot().clone(); - - // --------------------------- - // 1) Pivot -> Children side - // --------------------------- - let new_children_map = relations.children(); - let new_children_vec: Vec<(u64, T::AccountId)> = new_children_map - .iter() - .map(|(c, p)| (*p, c.clone())) - .collect(); - - let prev_children_vec = ChildKeys::::get(&pivot, netuid); - weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 0)); - - // Overwrite pivot's children vector - Self::set_childkeys(pivot.clone(), netuid, new_children_vec.clone()); - weight.saturating_accrue(T::DbWeight::get().reads_writes(0, 1)); - - // Build quick-lookup sets for diffing - let prev_children_set: BTreeSet = - prev_children_vec.iter().map(|(_, c)| c.clone()).collect(); - let new_children_set: BTreeSet = new_children_map.keys().cloned().collect(); - - // Added children = new / prev - for added in new_children_set - .iter() - .filter(|c| !prev_children_set.contains(*c)) - { - let p = match new_children_map.get(added) { - Some(p) => *p, - None => return Err(Error::::ChildParentInconsistency.into()), - }; - let mut pk = ParentKeys::::get(added.clone(), netuid); - PCRelations::::upsert_edge(&mut pk, p, &pivot); - Self::set_parentkeys(added.clone(), netuid, pk); - weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); - } - - // Updated children = intersection where proportion changed - for common in new_children_set.intersection(&prev_children_set) { - let new_p = match new_children_map.get(common) { - Some(p) => *p, - None => return Err(Error::::ChildParentInconsistency.into()), - }; - let mut pk = ParentKeys::::get(common.clone(), netuid); - PCRelations::::upsert_edge(&mut pk, new_p, &pivot); - Self::set_parentkeys(common.clone(), netuid, pk); - weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); - } - - // Removed children = prev \ new => remove (pivot) from ParentKeys(child) - for removed in prev_children_set - .iter() - .filter(|c| !new_children_set.contains(*c)) - { - let mut pk = ParentKeys::::get(removed.clone(), netuid); - PCRelations::::remove_edge(&mut pk, &pivot); - Self::set_parentkeys(removed.clone(), netuid, pk); - weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); - } - - // --------------------------- - // 2) Parents -> Pivot side - // --------------------------- - let new_parents_map = relations.parents(); - let new_parents_vec: Vec<(u64, T::AccountId)> = new_parents_map - .iter() - .map(|(p, pr)| (*pr, p.clone())) - .collect(); - - let prev_parents_vec = ParentKeys::::get(&pivot, netuid); - - // Overwrite pivot's parents vector - Self::set_parentkeys(pivot.clone(), netuid, new_parents_vec.clone()); - - let prev_parents_set: BTreeSet = - prev_parents_vec.into_iter().map(|(_, p)| p).collect(); - let new_parents_set: BTreeSet = new_parents_map.keys().cloned().collect(); - - // Added parents = new / prev => ensure ChildKeys(parent) has (p, pivot) - for added in new_parents_set - .iter() - .filter(|p| !prev_parents_set.contains(*p)) - { - let p_val = match new_parents_map.get(added) { - Some(p) => *p, - None => return Err(Error::::ChildParentInconsistency.into()), - }; - let mut ck = ChildKeys::::get(added.clone(), netuid); - PCRelations::::upsert_edge(&mut ck, p_val, &pivot); - Self::set_childkeys(added.clone(), netuid, ck); - weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); - } - - // Updated parents = intersection where proportion changed - for common in new_parents_set.intersection(&prev_parents_set) { - let new_p = new_parents_map - .get(common) - .ok_or(Error::::ChildParentInconsistency)?; - let mut ck = ChildKeys::::get(common.clone(), netuid); - PCRelations::::upsert_edge(&mut ck, *new_p, &pivot); - Self::set_childkeys(common.clone(), netuid, ck); - weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); - } - - // Removed parents = prev \ new => remove (pivot) from ChildKeys(parent) - for removed in prev_parents_set - .iter() - .filter(|p| !new_parents_set.contains(*p)) - { - let mut ck = ChildKeys::::get(removed.clone(), netuid); - PCRelations::::remove_edge(&mut ck, &pivot); - Self::set_childkeys(removed.clone(), netuid, ck); - weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); - } - - Ok(()) - } - - /// Swap all parent/child relations from `old_hotkey` to `new_hotkey` on `netuid`. - /// Steps: - /// 1) Load relations around `old_hotkey` - /// 2) Clean up storage references to `old_hotkey` (both directions) - /// 3) Rebind pivot to `new_hotkey` - /// 4) Persist relations around `new_hotkey` - pub fn parent_child_swap_hotkey( - old_hotkey: &T::AccountId, - new_hotkey: &T::AccountId, - netuid: NetUid, - weight: &mut Weight, - ) -> DispatchResult { - // 1) Load the current relations around old_hotkey - let mut relations = Self::load_child_parent_relations(old_hotkey, netuid)?; - weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 0)); - - // 2) Clean up all storage entries that reference old_hotkey - // 2a) For each child of old_hotkey: remove old_hotkey from ParentKeys(child, netuid) - for (child, _) in relations.children().iter() { - let mut pk = ParentKeys::::get(child.clone(), netuid); - PCRelations::::remove_edge(&mut pk, old_hotkey); - Self::set_parentkeys(child.clone(), netuid, pk.clone()); - weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); - } - // 2b) For each parent of old_hotkey: remove old_hotkey from ChildKeys(parent, netuid) - for (parent, _) in relations.parents().iter() { - let mut ck = ChildKeys::::get(parent.clone(), netuid); - PCRelations::::remove_edge(&mut ck, old_hotkey); - ChildKeys::::insert(parent.clone(), netuid, ck); - weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); - } - // 2c) Clear direct maps of old_hotkey - ChildKeys::::insert( - old_hotkey.clone(), - netuid, - Vec::<(u64, T::AccountId)>::new(), - ); - Self::set_parentkeys( - old_hotkey.clone(), - netuid, - Vec::<(u64, T::AccountId)>::new(), - ); - weight.saturating_accrue(T::DbWeight::get().reads_writes(0, 2)); - - // 3) Rebind pivot to new_hotkey (validate no self-loop with existing maps) - relations.rebind_pivot(new_hotkey.clone())?; - - // 4) Swap PendingChildKeys( netuid, parent ) --> Vec<(proportion,child), cool_down_block> - // Fail if consistency breaks - if PendingChildKeys::::contains_key(netuid, old_hotkey) { - let (children, cool_down_block) = PendingChildKeys::::get(netuid, old_hotkey); - relations.ensure_pending_consistency(&children)?; - - PendingChildKeys::::remove(netuid, old_hotkey); - PendingChildKeys::::insert(netuid, new_hotkey, (children, cool_down_block)); - weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 2)); - } - - // 5) Persist relations under the new pivot (diffs vs existing state at new_hotkey) - Self::persist_child_parent_relations(relations, netuid, weight) - } - - /// The implementation for the extrinsic do_set_child_singular: Sets a single child. - /// This function allows a coldkey to set children keys. - /// - /// Adds a childkey vector to the PendingChildKeys map and performs a few checks: - /// **Signature Verification**: Ensures that the caller has signed the transaction, verifying the coldkey. - /// **Root Network Check**: Ensures that the delegation is not on the root network, as child hotkeys are not valid on the root. - /// **Network Existence Check**: Ensures that the specified network exists. - /// **Ownership Verification**: Ensures that the coldkey owns the hotkey. - /// **Hotkey Account Existence Check**: Ensures that the hotkey account already exists. - /// **Child count**: Only allow to add up to 5 children per parent - /// **Child-Hotkey Distinction**: Ensures that the child is not the same as the hotkey. - /// **Minimum stake**: Ensures that the parent key has at least the minimum stake. - /// **Proportion check**: Ensure that the sum of the proportions does not exceed u64::MAX. - /// **Duplicate check**: Ensure there are no duplicates in the list of children. - /// - /// # Events - /// * `SetChildrenScheduled`: If all checks pass and setting the childkeys is scheduled. - /// - /// # Errors - /// * `MechanismDoesNotExist`: Attempting to register to a non-existent network. - /// * `RegistrationNotPermittedOnRootSubnet`: Attempting to register a child on the root network. - /// * `NonAssociatedColdKey`: The coldkey does not own the hotkey or the child is the same as the hotkey. - /// * `HotKeyAccountNotExists`: The hotkey account does not exist. - /// * `TooManyChildren`: Too many children in request. - /// - pub fn do_schedule_children( - origin: OriginFor, - hotkey: T::AccountId, - netuid: NetUid, - children: Vec<(u64, T::AccountId)>, - ) -> DispatchResult { - // Check that the caller has signed the transaction. (the coldkey of the pairing) - let coldkey = ensure_signed(origin)?; - log::trace!( - "do_set_children( coldkey:{coldkey:?} hotkey:{netuid:?} netuid:{hotkey:?} children:{children:?} )" - ); - - // Ensure the hotkey passes the rate limit. - ensure!( - TransactionType::SetChildren.passes_rate_limit_on_subnet::( - &hotkey, // Specific to a hotkey. - netuid, // Specific to a subnet. - ), - Error::::TxRateLimitExceeded - ); - - // Check that this delegation is not on the root network. Child hotkeys are not valid on root. - ensure!( - !netuid.is_root(), - Error::::RegistrationNotPermittedOnRootSubnet - ); - - // Check that the network we are trying to create the child on exists. - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); - - // Check that the coldkey owns the hotkey. - ensure!( - Self::coldkey_owns_hotkey(&coldkey, &hotkey), - Error::::NonAssociatedColdKey - ); - - // Ensure there are no duplicates in the list of children. - let mut unique_children = Vec::new(); - for (_, child_i) in &children { - ensure!( - !unique_children.contains(child_i), - Error::::DuplicateChild - ); - unique_children.push(child_i.clone()); - } - - // Ensure we don't break consistency when these new childkeys are set: - // - Ensure that the number of children does not exceed 5 - // - Each child is not the hotkey. - // - The sum of the proportions does not exceed u64::MAX. - // - Bipartite separation (no A <-> B relations) - let relations = Self::load_child_parent_relations(&hotkey, netuid)?; - relations.ensure_pending_consistency(&children)?; - - // Check that the parent key has at least the minimum own stake - // if children vector is not empty - // (checking with check_weights_min_stake wouldn't work because it considers - // grandparent stake in this case) - ensure!( - children.is_empty() - || Self::get_total_stake_for_hotkey(&hotkey) >= StakeThreshold::::get().into() - || SubnetOwnerHotkey::::try_get(netuid) - .is_ok_and(|owner_hotkey| owner_hotkey.eq(&hotkey)), - Error::::NotEnoughStakeToSetChildkeys - ); - - // Set last transaction block - let current_block = Self::get_current_block_as_u64(); - TransactionType::SetChildren.set_last_block_on_subnet::(&hotkey, netuid, current_block); - - // Schedule or immediately apply CK - Self::schedule_or_apply_ck(netuid, hotkey, children) - } - - /// If the start call occured, schedule children, otherwise, - /// apply immediately - fn schedule_or_apply_ck( - netuid: NetUid, - hotkey: T::AccountId, - children: Vec<(u64, T::AccountId)>, - ) -> DispatchResult { - if !SubtokenEnabled::::get(netuid) { - Self::persist_pending_chidren_ok(netuid, &hotkey, &children); - return Ok(()); - } - - // Calculate cool-down block - let cooldown_block = - Self::get_current_block_as_u64().saturating_add(PendingChildKeyCooldown::::get()); - - // Insert or update PendingChildKeys - PendingChildKeys::::insert(netuid, hotkey.clone(), (children.clone(), cooldown_block)); - - // Log and return. - log::trace!( - "SetChildrenScheduled( netuid:{:?}, cooldown_block:{:?}, hotkey:{:?}, children:{:?} )", - cooldown_block, - hotkey, - netuid, - children.clone() - ); - Self::deposit_event(Event::SetChildrenScheduled( - hotkey, - netuid, - cooldown_block, - children, - )); - - // Ok and return. - Ok(()) - } - - /// This function executes setting children keys when called during hotkey draining. - /// - /// * `netuid`: The u16 network identifier where the child keys will exist. - /// - /// # Events - /// * `SetChildren`: On successfully registering children to a hotkey. - /// - /// # Errors - /// * `MechanismDoesNotExist`: Attempting to register to a non-existent network. - /// * `RegistrationNotPermittedOnRootSubnet`: Attempting to register a child on the root network. - /// * `NonAssociatedColdKey`: The coldkey does not own the hotkey or the child is the same as the hotkey. - /// * `HotKeyAccountNotExists`: The hotkey account does not exist. - /// - /// # Note - /// 1. **Old Children Cleanup**: Removes the hotkey from the parent list of its old children. - /// 2. **New Children Assignment**: Assigns the new child to the hotkey and updates the parent list for the new child. - /// - pub fn do_set_pending_children(netuid: NetUid) { - let current_block = Self::get_current_block_as_u64(); - - // If the childkey cools down before the subnet start call + PendingChildKeyCooldown: - // - If Start call happened: Normal track - // - If Start call didn't happen: Apply immediately - // TODO: This check may be removed after all ck are applied after the runtime upgrade - let start_call_occured = SubtokenEnabled::::get(netuid); - - // Iterate over all pending children of this subnet and set as needed - let mut to_remove: Vec = Vec::new(); - - PendingChildKeys::::iter_prefix(netuid).for_each( - |(hotkey, (children, cool_down_block))| { - if (cool_down_block < current_block) || !start_call_occured { - Self::persist_pending_chidren_ok(netuid, &hotkey, &children); - to_remove.push(hotkey); - } - }, - ); - - for hotkey in to_remove { - PendingChildKeys::::remove(netuid, hotkey); - } - } - - // If child-parent consistency is broken, fail setting new children silently - fn persist_pending_chidren_ok( - netuid: NetUid, - hotkey: &T::AccountId, - children: &Vec<(u64, T::AccountId)>, - ) { - let maybe_relations = Self::load_relations_from_pending(hotkey.clone(), children, netuid); - if let Ok(relations) = maybe_relations { - let mut _weight: Weight = T::DbWeight::get().reads(0); - if let Ok(()) = Self::persist_child_parent_relations(relations, netuid, &mut _weight) { - // Log and emit event. - log::trace!( - "SetChildren( netuid:{:?}, hotkey:{:?}, children:{:?} )", - hotkey, - netuid, - children.clone() - ); - Self::deposit_event(Event::SetChildren(hotkey.clone(), netuid, children.clone())); - } - } - } - - /* Retrieves the list of children for a given hotkey and network. - /// - /// # Arguments - /// * `hotkey`: The hotkey whose children are to be retrieved. - /// * `netuid`: The network identifier. - /// - /// # Returns - /// * `Vec<(u64, T::AccountId)>`: A vector of tuples containing the proportion and child account ID. - /// - /// # Example - /// ``` - /// let children = SubtensorModule::get_children(&hotkey, netuid); - */ - pub fn get_children(hotkey: &T::AccountId, netuid: NetUid) -> Vec<(u64, T::AccountId)> { - ChildKeys::::get(hotkey, netuid) - } - - /* Retrieves the list of parents for a given child and network. - /// - /// # Arguments - /// * `child`: The child whose parents are to be retrieved. - /// * `netuid`: The network identifier. - /// - /// # Returns - /// * `Vec<(u64, T::AccountId)>`: A vector of tuples containing the proportion and parent account ID. - /// - /// # Example - /// ``` - /// let parents = SubtensorModule::get_parents(&child, netuid); - */ - pub fn get_parents(child: &T::AccountId, netuid: NetUid) -> Vec<(u64, T::AccountId)> { - ParentKeys::::get(child, netuid) - } - - /// Sets the childkey take for a given hotkey. - /// - /// This function allows a coldkey to set the childkey take for a given hotkey. - /// The childkey take determines the proportion of stake that the hotkey keeps for itself - /// when distributing stake to its children. - /// - /// # Arguments - /// * `coldkey`: The coldkey that owns the hotkey. - /// - /// * `hotkey`: The hotkey for which the childkey take will be set. - /// - /// * `take`: The new childkey take value. This is a ratio represented in parts per 65535, - /// where 65535 represents 100%. - /// - /// # Returns - /// * `DispatchResult`: The result of the operation. - /// - /// # Errors - /// * `NonAssociatedColdKey`: The coldkey does not own the hotkey. - /// * `InvalidChildkeyTake`: The provided take value is invalid (greater than the maximum allowed take). - /// * `TxChildkeyTakeRateLimitExceeded`: The rate limit for changing childkey take has been exceeded. - pub fn do_set_childkey_take( - coldkey: T::AccountId, - hotkey: T::AccountId, - netuid: NetUid, - take: PerU16, - ) -> DispatchResult { - // Ensure the coldkey owns the hotkey - ensure!( - Self::coldkey_owns_hotkey(&coldkey, &hotkey), - Error::::NonAssociatedColdKey - ); - - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); - - // Ensure the take value is valid - ensure!( - take.deconstruct() >= Self::get_effective_min_childkey_take(netuid) - && take.deconstruct() <= Self::get_max_childkey_take(), - Error::::InvalidChildkeyTake - ); - - let current_take = Self::get_childkey_take(&hotkey, netuid); - // Check the rate limit for increasing childkey take case - if take.deconstruct() > current_take { - // Ensure the hotkey passes the rate limit. - ensure!( - TransactionType::SetChildkeyTake.passes_rate_limit_on_subnet::( - &hotkey, // Specific to a hotkey. - netuid, // Specific to a subnet. - ), - Error::::TxChildkeyTakeRateLimitExceeded - ); - } - - // Set last transaction block - let current_block = Self::get_current_block_as_u64(); - TransactionType::SetChildkeyTake.set_last_block_on_subnet::( - &hotkey, - netuid, - current_block, - ); - - // Set the new childkey take value for the given hotkey and network - ChildkeyTake::::insert(hotkey.clone(), netuid, take); - - // Update the last transaction block - TransactionType::SetChildkeyTake.set_last_block_on_subnet::( - &hotkey, - netuid, - current_block, - ); - - // Emit the event - Self::deposit_event(Event::ChildKeyTakeSet(hotkey.clone(), take)); - log::debug!("Childkey take set for hotkey: {hotkey:?} and take: {take:?}"); - Ok(()) - } - - /// Gets the childkey take for a given hotkey. - /// - /// This function retrieves the current childkey take value for a specified hotkey. - /// If no specific take value has been set, it returns the default childkey take. - /// - /// # Arguments - /// * `hotkey` (&T::AccountId): The hotkey for which to retrieve the childkey take. - /// - /// # Returns - /// * `u16`: The childkey take value, scaled so `u16::MAX` represents 100%. - pub fn get_childkey_take(hotkey: &T::AccountId, netuid: NetUid) -> u16 { - ChildkeyTake::::get(hotkey, netuid) - .deconstruct() - .max(Self::get_effective_min_childkey_take(netuid)) - } - - pub fn get_auto_parent_delegation_enabled(root_validator_hotkey: &T::AccountId) -> bool { - AutoParentDelegationEnabled::::get(root_validator_hotkey) - } - - //////////////////////////////////////////////////////////// - // State cleaners (for use in migration) - // TODO: Deprecate when the state is clean for a while - - /// Establishes parent-child relationships between all root validators and - /// a subnet owner's hotkey on the specified subnet. - /// - /// For each validator on the root network (netuid 0), this function calls - /// `do_schedule_children` to schedule the subnet owner hotkey as a child - /// of that root validator on the given subnet, with full proportion (u64::MAX). - /// - /// # Arguments - /// * `netuid`: The subnet on which to establish relationships. - /// - /// # Returns - /// * `DispatchResult`: Ok if at least the setup completes; individual - /// scheduling failures per validator are logged but do not abort the loop. - pub fn do_set_root_validators_for_subnet(netuid: NetUid) -> DispatchResult { - // Cannot set children on root network itself. - ensure!( - !netuid.is_root(), - Error::::RegistrationNotPermittedOnRootSubnet - ); - - // Subnet must exist. - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); - - // Get the subnet owner hotkey. - let subnet_owner_hotkey = - SubnetOwnerHotkey::::try_get(netuid).map_err(|_| Error::::SubnetNotExists)?; - - // Iterate over all root validators and schedule each one as a parent - // of the subnet owner hotkey. - for (_uid, root_validator_hotkey) in Keys::::iter_prefix(NetUid::ROOT) { - // Skip if the root validator is the subnet owner hotkey itself - // (cannot be both parent and child). - if root_validator_hotkey == subnet_owner_hotkey { - continue; - } - - // Skip if root validator disabled auto parent delegation via AutoParentDelegationEnabled flag - if !Self::get_auto_parent_delegation_enabled(&root_validator_hotkey) { - continue; - } - - // Look up the coldkey that owns this root validator hotkey. - let coldkey = Self::get_owning_coldkey_for_hotkey(&root_validator_hotkey); - - // Build a signed origin from the coldkey. - let origin: ::RuntimeOrigin = - frame_system::RawOrigin::Signed(coldkey).into(); - - // Schedule the subnet owner hotkey as a child with full proportion. - let children = vec![(u64::MAX, subnet_owner_hotkey.clone())]; - - if let Err(e) = - Self::do_schedule_children(origin, root_validator_hotkey.clone(), netuid, children) - { - log::warn!( - "Failed to schedule children for root validator {:?} on netuid {:?}: {:?}", - root_validator_hotkey, - netuid, - e - ); - } - } - - Ok(()) - } - - pub fn clean_zero_childkey_vectors(weight: &mut Weight) { - // Collect keys to delete first to avoid mutating while iterating. - let mut to_remove: Vec<(T::AccountId, NetUid)> = Vec::new(); - - for (parent, netuid, children) in ChildKeys::::iter() { - // Account for the read - *weight = weight.saturating_add(T::DbWeight::get().reads(1)); - - if children.is_empty() { - to_remove.push((parent, netuid)); - } - } - - // Remove all empty entries - for (parent, netuid) in &to_remove { - ChildKeys::::remove(parent, netuid); - // Account for the write - *weight = weight.saturating_add(T::DbWeight::get().writes(1)); - } - log::info!( - target: "runtime", - "Removed {} empty childkey vectors.", - to_remove.len() - ); - } - - /// Remove self-loops in `ChildKeys` and `ParentKeys`. - /// If, after removal, a value-vector becomes empty, the storage key is removed. - pub fn clean_self_loops(weight: &mut Weight) { - // ------------------------------- - // 1) ChildKeys: (parent, netuid) -> Vec<(w, child)> - // Remove any entries where child == parent. - // ------------------------------- - let mut to_update_ck: Vec<((T::AccountId, NetUid), Vec<(u64, T::AccountId)>)> = Vec::new(); - let mut to_remove_ck: Vec<(T::AccountId, NetUid)> = Vec::new(); - - for (parent, netuid, children) in ChildKeys::::iter() { - *weight = weight.saturating_add(T::DbWeight::get().reads(1)); - - // Filter out self-loops - let filtered: Vec<(u64, T::AccountId)> = children - .clone() - .into_iter() - .filter(|(_, c)| *c != parent) - .collect(); - - // If nothing changed, skip - // (we can detect by comparing lengths; safer is to re-check if any removed existed) - // For simplicity, just compare lengths: - // If len unchanged and the previous vector had no self-loop, skip. - // If there *was* a self-loop and filtered is empty, we'll remove the key. - if filtered.len() == children.len() { - // No change -> continue - continue; - } - - if filtered.is_empty() { - to_remove_ck.push((parent, netuid)); - } else { - to_update_ck.push(((parent, netuid), filtered)); - } - } - - // Apply ChildKeys updates/removals - for ((parent, netuid), new_vec) in &to_update_ck { - Self::set_childkeys(parent.clone(), *netuid, new_vec.clone()); - *weight = weight.saturating_add(T::DbWeight::get().writes(1)); - } - for (parent, netuid) in &to_remove_ck { - ChildKeys::::remove(parent, netuid); - *weight = weight.saturating_add(T::DbWeight::get().writes(1)); - } - log::info!( - target: "runtime", - "Removed {} self-looping childkeys.", - to_update_ck.len().saturating_add(to_remove_ck.len()) - ); - - // ------------------------------- - // 2) ParentKeys: (child, netuid) -> Vec<(w, parent)> - // Remove any entries where parent == child. - // ------------------------------- - let mut to_update_pk: Vec<((T::AccountId, NetUid), Vec<(u64, T::AccountId)>)> = Vec::new(); - let mut to_remove_pk: Vec<(T::AccountId, NetUid)> = Vec::new(); - - for (child, netuid, parents) in ParentKeys::::iter() { - *weight = weight.saturating_add(T::DbWeight::get().reads(1)); - - // Filter out self-loops - let filtered: Vec<(u64, T::AccountId)> = parents - .clone() - .into_iter() - .filter(|(_, p)| *p != child) - .collect(); - - // If unchanged, skip - if filtered.len() == parents.len() { - continue; - } - - if filtered.is_empty() { - to_remove_pk.push((child, netuid)); - } else { - to_update_pk.push(((child, netuid), filtered)); - } - } - - // Apply ParentKeys updates/removals - for ((child, netuid), new_vec) in &to_update_pk { - Self::set_parentkeys(child.clone(), *netuid, new_vec.clone()); - *weight = weight.saturating_add(T::DbWeight::get().writes(1)); - } - for (child, netuid) in &to_remove_pk { - ParentKeys::::remove(child, netuid); - *weight = weight.saturating_add(T::DbWeight::get().writes(1)); - } - log::info!( - target: "runtime", - "Removed {} self-looping parentkeys.", - to_update_pk.len().saturating_add(to_remove_pk.len()) - ); - } - - pub fn clean_zero_parentkey_vectors(weight: &mut Weight) { - // Collect keys to delete first to avoid mutating while iterating. - let mut to_remove: Vec<(T::AccountId, NetUid)> = Vec::new(); - - for (parent, netuid, children) in ParentKeys::::iter() { - // Account for the read - *weight = weight.saturating_add(T::DbWeight::get().reads(1)); - - if children.is_empty() { - to_remove.push((parent, netuid)); - } - } - - // Remove all empty entries - for (parent, netuid) in &to_remove { - ParentKeys::::remove(parent, netuid); - // Account for the write - *weight = weight.saturating_add(T::DbWeight::get().writes(1)); - } - log::info!( - target: "runtime", - "Removed {} empty parentkey vectors.", - to_remove.len() - ); - } - - /// Make ChildKeys and ParentKeys bidirectionally consistent by - /// **removing** entries that don't have a matching counterpart. - /// A match means the exact tuple `(p, other_id)` is present on the opposite map. - /// - /// Rules: - /// * For each (parent, netuid) -> [(p, child)...] in ChildKeys: - /// keep only those (p, child) that appear in ParentKeys(child, netuid) as (p, parent). - /// If resulting list is empty, remove the key. - /// * For each (child, netuid) -> [(p, parent)...] in ParentKeys: - /// keep only those (p, parent) that appear in ChildKeys(parent, netuid) as (p, child). - /// If resulting list is empty, remove the key. - pub fn repair_child_parent_consistency(weight: &mut Weight) { - // ------------------------------- - // 1) Prune ChildKeys by checking ParentKeys - // ------------------------------- - let mut ck_updates: Vec<((T::AccountId, NetUid), Vec<(u64, T::AccountId)>)> = Vec::new(); - let mut ck_removes: Vec<(T::AccountId, NetUid)> = Vec::new(); - - for (parent, netuid, children) in ChildKeys::::iter() { - *weight = weight.saturating_add(T::DbWeight::get().reads(1)); - - // Keep (p, child) only if ParentKeys(child, netuid) contains (p, parent) - let mut filtered: Vec<(u64, T::AccountId)> = Vec::with_capacity(children.len()); - for (p, child) in children.clone().into_iter() { - let rev = ParentKeys::::get(&child, netuid); - *weight = weight.saturating_add(T::DbWeight::get().reads(1)); - let has_match = rev.iter().any(|(pr, pa)| *pr == p && *pa == parent); - if has_match { - filtered.push((p, child)); - } - } - - if filtered.is_empty() { - ck_removes.push((parent, netuid)); - } else { - // Only write if changed - if children != filtered { - ck_updates.push(((parent, netuid), filtered)); - } - } - } - - for ((parent, netuid), new_vec) in &ck_updates { - Self::set_childkeys(parent.clone(), *netuid, new_vec.clone()); - *weight = weight.saturating_add(T::DbWeight::get().writes(1)); - } - for (parent, netuid) in &ck_removes { - ChildKeys::::remove(parent, netuid); - *weight = weight.saturating_add(T::DbWeight::get().writes(1)); - } - log::info!( - target: "runtime", - "Updated {} childkey inconsistent records.", - ck_updates.len() - ); - log::info!( - target: "runtime", - "Removed {} childkey inconsistent records.", - ck_removes.len() - ); - - // ------------------------------- - // 2) Prune ParentKeys by checking ChildKeys - // ------------------------------- - let mut pk_updates: Vec<((T::AccountId, NetUid), Vec<(u64, T::AccountId)>)> = Vec::new(); - let mut pk_removes: Vec<(T::AccountId, NetUid)> = Vec::new(); - - for (child, netuid, parents) in ParentKeys::::iter() { - *weight = weight.saturating_add(T::DbWeight::get().reads(1)); - - // Keep (p, parent) only if ChildKeys(parent, netuid) contains (p, child) - let mut filtered: Vec<(u64, T::AccountId)> = Vec::with_capacity(parents.len()); - for (p, parent) in parents.clone().into_iter() { - let fwd = ChildKeys::::get(&parent, netuid); - *weight = weight.saturating_add(T::DbWeight::get().reads(1)); - let has_match = fwd.iter().any(|(pr, ch)| *pr == p && *ch == child); - if has_match { - filtered.push((p, parent)); - } - } - - if filtered.is_empty() { - pk_removes.push((child, netuid)); - } else { - // Only write if changed - if parents != filtered { - pk_updates.push(((child, netuid), filtered)); - } - } - } - - for ((child, netuid), new_vec) in &pk_updates { - Self::set_parentkeys(child.clone(), *netuid, new_vec.clone()); - *weight = weight.saturating_add(T::DbWeight::get().writes(1)); - } - for (child, netuid) in &pk_removes { - ParentKeys::::remove(child, netuid); - *weight = weight.saturating_add(T::DbWeight::get().writes(1)); - } - log::info!( - target: "runtime", - "Updated {} parentkey inconsistent records.", - pk_updates.len() - ); - log::info!( - target: "runtime", - "Removed {} parentkey inconsistent records.", - pk_removes.len() - ); - } -} diff --git a/pallets/subtensor/src/staking/set_children/childkey_take.rs b/pallets/subtensor/src/staking/set_children/childkey_take.rs new file mode 100644 index 0000000000..129c4f2477 --- /dev/null +++ b/pallets/subtensor/src/staking/set_children/childkey_take.rs @@ -0,0 +1,113 @@ +//! Childkey take (fee) getters/setters and auto-parent-delegation flag. +use super::*; +use sp_runtime::PerU16; +use subtensor_runtime_common::NetUid; + +impl Pallet { + pub fn get_children(hotkey: &T::AccountId, netuid: NetUid) -> Vec<(u64, T::AccountId)> { + ChildKeys::::get(hotkey, netuid) + } + + pub fn get_parents(child: &T::AccountId, netuid: NetUid) -> Vec<(u64, T::AccountId)> { + ParentKeys::::get(child, netuid) + } + + /// Sets the childkey take for a given hotkey. + /// + /// This function allows a coldkey to set the childkey take for a given hotkey. + /// The childkey take determines the proportion of stake that the hotkey keeps for itself + /// when distributing stake to its children. + /// + /// # Arguments + /// * `coldkey`: The coldkey that owns the hotkey. + /// + /// * `hotkey`: The hotkey for which the childkey take will be set. + /// + /// * `take`: The new childkey take value. This is a ratio represented in parts per 65535, + /// where 65535 represents 100%. + /// + /// # Returns + /// * `DispatchResult`: The result of the operation. + /// + /// # Errors + /// * `NonAssociatedColdKey`: The coldkey does not own the hotkey. + /// * `InvalidChildkeyTake`: The provided take value is invalid (greater than the maximum allowed take). + /// * `TxChildkeyTakeRateLimitExceeded`: The rate limit for changing childkey take has been exceeded. + pub fn do_set_childkey_take( + coldkey: T::AccountId, + hotkey: T::AccountId, + netuid: NetUid, + take: PerU16, + ) -> DispatchResult { + // Ensure the coldkey owns the hotkey + ensure!( + Self::coldkey_owns_hotkey(&coldkey, &hotkey), + Error::::NonAssociatedColdKey + ); + + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); + + // Ensure the take value is valid + ensure!( + take.deconstruct() >= Self::get_effective_min_childkey_take(netuid) + && take.deconstruct() <= Self::get_max_childkey_take(), + Error::::InvalidChildkeyTake + ); + + let current_take = Self::get_childkey_take(&hotkey, netuid); + // Check the rate limit for increasing childkey take case + if take.deconstruct() > current_take { + // Ensure the hotkey passes the rate limit. + ensure!( + TransactionType::SetChildkeyTake.passes_rate_limit_on_subnet::( + &hotkey, // Specific to a hotkey. + netuid, // Specific to a subnet. + ), + Error::::TxChildkeyTakeRateLimitExceeded + ); + } + + // Set last transaction block + let current_block = Self::get_current_block_as_u64(); + TransactionType::SetChildkeyTake.set_last_block_on_subnet::( + &hotkey, + netuid, + current_block, + ); + + // Set the new childkey take value for the given hotkey and network + ChildkeyTake::::insert(hotkey.clone(), netuid, take); + + // Update the last transaction block + TransactionType::SetChildkeyTake.set_last_block_on_subnet::( + &hotkey, + netuid, + current_block, + ); + + // Emit the event + Self::deposit_event(Event::ChildKeyTakeSet(hotkey.clone(), take)); + log::debug!("Childkey take set for hotkey: {hotkey:?} and take: {take:?}"); + Ok(()) + } + + /// Gets the childkey take for a given hotkey. + /// + /// This function retrieves the current childkey take value for a specified hotkey. + /// If no specific take value has been set, it returns the default childkey take. + /// + /// # Arguments + /// * `hotkey` (&T::AccountId): The hotkey for which to retrieve the childkey take. + /// + /// # Returns + /// * `u16`: The childkey take value, scaled so `u16::MAX` represents 100%. + pub fn get_childkey_take(hotkey: &T::AccountId, netuid: NetUid) -> u16 { + ChildkeyTake::::get(hotkey, netuid) + .deconstruct() + .max(Self::get_effective_min_childkey_take(netuid)) + } + + pub fn get_auto_parent_delegation_enabled(root_validator_hotkey: &T::AccountId) -> bool { + AutoParentDelegationEnabled::::get(root_validator_hotkey) + } +} diff --git a/pallets/subtensor/src/staking/set_children/mod.rs b/pallets/subtensor/src/staking/set_children/mod.rs new file mode 100644 index 0000000000..5dcfeb6149 --- /dev/null +++ b/pallets/subtensor/src/staking/set_children/mod.rs @@ -0,0 +1,23 @@ +//! Parent/child hotkey delegation (childkeys) and childkey take. +//! +//! ## Search anchors +//! +//! | Module | Owns | +//! |--------|------| +//! | [`parent_child_relations`] | [`ParentChildRelations`] in-memory graph + invariants | +//! | [`parent_child_storage`] | Load/persist edges, `parent_child_swap_hotkey` | +//! | [`schedule_children`] | `do_schedule_children`, pending apply | +//! | [`childkey_take`] | `do_set_childkey_take` / getters | +//! | [`root_validators`] | `do_set_root_validators_for_subnet` | +//! | [`repair_children`] | Empty-vector / self-loop / consistency repair | + +use super::*; + +pub mod childkey_take; +pub mod parent_child_relations; +pub mod parent_child_storage; +pub mod repair_children; +pub mod root_validators; +pub mod schedule_children; + +pub use parent_child_relations::ParentChildRelations; diff --git a/pallets/subtensor/src/staking/set_children/parent_child_relations.rs b/pallets/subtensor/src/staking/set_children/parent_child_relations.rs new file mode 100644 index 0000000000..392fc23797 --- /dev/null +++ b/pallets/subtensor/src/staking/set_children/parent_child_relations.rs @@ -0,0 +1,162 @@ +//! Parent/child hotkey relation graph ([`ParentChildRelations`]). +//! +//! Maintains bipartite parent↔child edges with proportions, load/persist diffs +//! against `ChildKeys` / `ParentKeys`, and hotkey-swap rebinding. +use super::*; +use sp_std::collections::btree_map::BTreeMap; + +pub struct ParentChildRelations { + /// The distinguished `hotkey` this structure is built around. + pivot: T::AccountId, + children: BTreeMap, + parents: BTreeMap, +} + +impl ParentChildRelations { + /// Create empty relations for a given pivot. + pub fn new(hotkey: T::AccountId) -> Self { + Self { + pivot: hotkey, + children: BTreeMap::new(), + parents: BTreeMap::new(), + } + } + + //////////////////////////////////////////////////////////// + // Constraint checkers + + /// Ensures sum(proportions) <= u64::MAX + pub fn ensure_total_proportions(children: &BTreeMap) -> DispatchResult { + let total: u128 = children + .values() + .fold(0u128, |acc, &w| acc.saturating_add(w as u128)); + ensure!(total <= u64::MAX as u128, Error::::ProportionOverflow); + Ok(()) + } + + /// Ensure that the number of children does not exceed 5 + pub fn ensure_childkey_count(children: &BTreeMap) -> DispatchResult { + ensure!(children.len() <= 5, Error::::TooManyChildren); + + Ok(()) + } + + /// Ensures the given children or parent set doesn't contain pivot + pub fn ensure_no_self_loop( + pivot: &T::AccountId, + hotkey_set: &BTreeMap, + ) -> DispatchResult { + ensure!(!hotkey_set.contains_key(pivot), Error::::InvalidChild); + Ok(()) + } + + /// Ensures that children and parents sets do not have any overlap + pub fn ensure_bipartite_separation( + children: &BTreeMap, + parents: &BTreeMap, + ) -> DispatchResult { + let has_overlap = children.keys().any(|c| parents.contains_key(c)); + ensure!(!has_overlap, Error::::ChildParentInconsistency); + Ok(()) + } + + /// Validate that applying `pending_children_vec` to `relations` (as the new + /// pivot->children mapping) preserves all invariants. + /// + /// Checks: + /// 1) No self-loop: pivot must not appear among children. + /// 2) Sum of child proportions fits in `u64`. + /// 3) Bipartite role separation: no child may also be a parent. + pub fn ensure_pending_consistency( + &self, + pending_children_vec: &Vec<(u64, T::AccountId)>, + ) -> DispatchResult { + // Build a deduped children map (last proportion wins if duplicates present). + let mut new_children: BTreeMap = BTreeMap::new(); + for (prop, child) in pending_children_vec { + new_children.insert(child.clone(), *prop); + } + + // Check constraints + Self::ensure_no_self_loop(&self.pivot, &new_children)?; + Self::ensure_childkey_count(&new_children)?; + Self::ensure_total_proportions(&new_children)?; + Self::ensure_bipartite_separation(&new_children, &self.parents)?; + + Ok(()) + } + + //////////////////////////////////////////////////////////// + // Getters + + #[inline] + pub fn pivot(&self) -> &T::AccountId { + &self.pivot + } + #[inline] + pub fn children(&self) -> &BTreeMap { + &self.children + } + #[inline] + pub fn parents(&self) -> &BTreeMap { + &self.parents + } + + //////////////////////////////////////////////////////////// + // Safe updaters + + /// Replace the pivot->children mapping after validating invariants. + /// + /// Invariants: + /// * No self-loop: child != pivot + /// * sum(proportions) fits in u64 (checked as u128 to avoid overflow mid-sum) + pub fn link_children(&mut self, new_children: BTreeMap) -> DispatchResult { + // Check constraints + Self::ensure_no_self_loop(&self.pivot, &new_children)?; + Self::ensure_total_proportions(&new_children)?; + Self::ensure_bipartite_separation(&new_children, &self.parents)?; + + self.children = new_children; + Ok(()) + } + + pub fn link_parents(&mut self, new_parents: BTreeMap) -> DispatchResult { + // Check constraints + Self::ensure_no_self_loop(&self.pivot, &new_parents)?; + Self::ensure_bipartite_separation(&self.children, &new_parents)?; + + self.parents = new_parents; + Ok(()) + } + + #[inline] + pub(crate) fn upsert_edge( + list: &mut Vec<(u64, T::AccountId)>, + proportion: u64, + id: &T::AccountId, + ) { + for (p, who) in list.iter_mut() { + if who == id { + *p = proportion; + return; + } + } + list.push((proportion, id.clone())); + } + + #[inline] + pub(crate) fn remove_edge(list: &mut Vec<(u64, T::AccountId)>, id: &T::AccountId) { + list.retain(|(_, who)| who != id); + } + + /// Change the pivot hotkey for these relations. + /// Ensures there are no self-loops with the new pivot. + pub fn rebind_pivot(&mut self, new_pivot: T::AccountId) -> DispatchResult { + // No self-loop via children or parents for the new pivot. + Self::ensure_no_self_loop(&new_pivot, &self.children)?; + Self::ensure_no_self_loop(&new_pivot, &self.parents)?; + + self.pivot = new_pivot; + Ok(()) + } +} diff --git a/pallets/subtensor/src/staking/set_children/parent_child_storage.rs b/pallets/subtensor/src/staking/set_children/parent_child_storage.rs new file mode 100644 index 0000000000..1d6f7a7127 --- /dev/null +++ b/pallets/subtensor/src/staking/set_children/parent_child_storage.rs @@ -0,0 +1,300 @@ +//! Load/persist parent–child edges and swap them across a hotkey change. +use super::*; +use sp_std::collections::{btree_map::BTreeMap, btree_set::BTreeSet}; +use subtensor_runtime_common::NetUid; + +impl Pallet { + /// Set childkeys vector making sure there are no empty vectors in the state + pub(crate) fn set_childkeys( + parent: T::AccountId, + netuid: NetUid, + childkey_vec: Vec<(u64, T::AccountId)>, + ) { + if childkey_vec.is_empty() { + ChildKeys::::remove(parent, netuid); + } else { + ChildKeys::::insert(parent, netuid, childkey_vec); + } + } + + /// Set parentkeys vector making sure there are no empty vectors in the state + pub(crate) fn set_parentkeys( + child: T::AccountId, + netuid: NetUid, + parentkey_vec: Vec<(u64, T::AccountId)>, + ) { + if parentkey_vec.is_empty() { + ParentKeys::::remove(child, netuid); + } else { + ParentKeys::::insert(child, netuid, parentkey_vec); + } + } + + /// Loads all records from ChildKeys and ParentKeys where (hotkey, netuid) is the key. + /// Produces a parent->(child->prop) adjacency map that **cannot violate** + /// the required consistency because all inserts go through `link`. + pub(crate) fn load_child_parent_relations( + hotkey: &T::AccountId, + netuid: NetUid, + ) -> Result, DispatchError> { + let mut rel = ParentChildRelations::::new(hotkey.clone()); + + // Load children: (prop, child) from ChildKeys(hotkey, netuid) + let child_links = ChildKeys::::get(hotkey, netuid); + let mut children = BTreeMap::::new(); + for (prop, child) in child_links { + // Ignore any accidental self-loop in storage + if child != *hotkey { + children.insert(child, prop); + } + } + // Validate & set (enforce no self-loop and sum limit) + rel.link_children(children)?; + + // Load parents: (prop, parent) from ParentKeys(hotkey, netuid) + let parent_links = ParentKeys::::get(hotkey, netuid); + let mut parents = BTreeMap::::new(); + for (prop, parent) in parent_links { + if parent != *hotkey { + parents.insert(parent, prop); + } + } + // Keep the same validation rules for parents (no self-loop, bounded sum). + rel.link_parents(parents)?; + + Ok(rel) + } + + /// Build a `ParentChildRelations` for `pivot` (parent) from the `PendingChildKeys` queue, + /// preserving the current `ParentKeys(pivot, netuid)` so `persist_child_parent_relations` + /// won’t accidentally clear existing parents. + /// + /// PendingChildKeys layout: + /// (netuid, pivot) -> (Vec<(proportion, child)>) + pub fn load_relations_from_pending( + pivot: T::AccountId, + pending_children_vec: &Vec<(u64, T::AccountId)>, + netuid: NetUid, + ) -> Result, DispatchError> { + let mut rel = ParentChildRelations::::new(pivot.clone()); + + // Deduplicate into a BTreeMap (last wins if duplicates). + let mut children: BTreeMap = BTreeMap::new(); + for (prop, child) in pending_children_vec { + if *child != pivot { + children.insert(child.clone(), *prop); + } + } + + // Enforce invariants (no self-loop, total weight <= u64::MAX) + rel.link_children(children)?; + + // Preserve the current parents of the pivot so `persist_child_parent_relations` + // won’t clear them when we only intend to update children. + let existing_parents_vec = ParentKeys::::get(pivot.clone(), netuid); + let mut parents: BTreeMap = BTreeMap::new(); + for (w, parent) in existing_parents_vec { + if parent != pivot { + parents.insert(parent, w); + } + } + // This uses the same basic checks (no self-loop, bounded sum). + // If you didn't expose link_parents, inline the simple validations here. + rel.link_parents(parents)?; + + Ok(rel) + } + + /// Persist the `relations` around `hotkey` to storage, updating both directions: + /// * Writes ChildKeys(hotkey, netuid) = children + /// and synchronizes ParentKeys(child, netuid) entries accordingly. + /// * Writes ParentKeys(hotkey, netuid) = parents + /// and synchronizes ChildKeys(parent, netuid) entries accordingly. + /// + /// This is a **diff-based** update that only touches affected neighbors. + pub fn persist_child_parent_relations( + relations: ParentChildRelations, + netuid: NetUid, + weight: &mut Weight, + ) -> DispatchResult { + let pivot = relations.pivot().clone(); + + // --------------------------- + // 1) Pivot -> Children side + // --------------------------- + let new_children_map = relations.children(); + let new_children_vec: Vec<(u64, T::AccountId)> = new_children_map + .iter() + .map(|(c, p)| (*p, c.clone())) + .collect(); + + let prev_children_vec = ChildKeys::::get(&pivot, netuid); + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 0)); + + // Overwrite pivot's children vector + Self::set_childkeys(pivot.clone(), netuid, new_children_vec.clone()); + weight.saturating_accrue(T::DbWeight::get().reads_writes(0, 1)); + + // Build quick-lookup sets for diffing + let prev_children_set: BTreeSet = + prev_children_vec.iter().map(|(_, c)| c.clone()).collect(); + let new_children_set: BTreeSet = new_children_map.keys().cloned().collect(); + + // Added children = new / prev + for added in new_children_set + .iter() + .filter(|c| !prev_children_set.contains(*c)) + { + let p = match new_children_map.get(added) { + Some(p) => *p, + None => return Err(Error::::ChildParentInconsistency.into()), + }; + let mut pk = ParentKeys::::get(added.clone(), netuid); + ParentChildRelations::::upsert_edge(&mut pk, p, &pivot); + Self::set_parentkeys(added.clone(), netuid, pk); + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + } + + // Updated children = intersection where proportion changed + for common in new_children_set.intersection(&prev_children_set) { + let new_p = match new_children_map.get(common) { + Some(p) => *p, + None => return Err(Error::::ChildParentInconsistency.into()), + }; + let mut pk = ParentKeys::::get(common.clone(), netuid); + ParentChildRelations::::upsert_edge(&mut pk, new_p, &pivot); + Self::set_parentkeys(common.clone(), netuid, pk); + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + } + + // Removed children = prev \ new => remove (pivot) from ParentKeys(child) + for removed in prev_children_set + .iter() + .filter(|c| !new_children_set.contains(*c)) + { + let mut pk = ParentKeys::::get(removed.clone(), netuid); + ParentChildRelations::::remove_edge(&mut pk, &pivot); + Self::set_parentkeys(removed.clone(), netuid, pk); + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + } + + // --------------------------- + // 2) Parents -> Pivot side + // --------------------------- + let new_parents_map = relations.parents(); + let new_parents_vec: Vec<(u64, T::AccountId)> = new_parents_map + .iter() + .map(|(p, pr)| (*pr, p.clone())) + .collect(); + + let prev_parents_vec = ParentKeys::::get(&pivot, netuid); + + // Overwrite pivot's parents vector + Self::set_parentkeys(pivot.clone(), netuid, new_parents_vec.clone()); + + let prev_parents_set: BTreeSet = + prev_parents_vec.into_iter().map(|(_, p)| p).collect(); + let new_parents_set: BTreeSet = new_parents_map.keys().cloned().collect(); + + // Added parents = new / prev => ensure ChildKeys(parent) has (p, pivot) + for added in new_parents_set + .iter() + .filter(|p| !prev_parents_set.contains(*p)) + { + let p_val = match new_parents_map.get(added) { + Some(p) => *p, + None => return Err(Error::::ChildParentInconsistency.into()), + }; + let mut ck = ChildKeys::::get(added.clone(), netuid); + ParentChildRelations::::upsert_edge(&mut ck, p_val, &pivot); + Self::set_childkeys(added.clone(), netuid, ck); + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + } + + // Updated parents = intersection where proportion changed + for common in new_parents_set.intersection(&prev_parents_set) { + let new_p = new_parents_map + .get(common) + .ok_or(Error::::ChildParentInconsistency)?; + let mut ck = ChildKeys::::get(common.clone(), netuid); + ParentChildRelations::::upsert_edge(&mut ck, *new_p, &pivot); + Self::set_childkeys(common.clone(), netuid, ck); + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + } + + // Removed parents = prev \ new => remove (pivot) from ChildKeys(parent) + for removed in prev_parents_set + .iter() + .filter(|p| !new_parents_set.contains(*p)) + { + let mut ck = ChildKeys::::get(removed.clone(), netuid); + ParentChildRelations::::remove_edge(&mut ck, &pivot); + Self::set_childkeys(removed.clone(), netuid, ck); + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + } + + Ok(()) + } + + /// Swap all parent/child relations from `old_hotkey` to `new_hotkey` on `netuid`. + /// Steps: + /// 1) Load relations around `old_hotkey` + /// 2) Clean up storage references to `old_hotkey` (both directions) + /// 3) Rebind pivot to `new_hotkey` + /// 4) Persist relations around `new_hotkey` + pub fn parent_child_swap_hotkey( + old_hotkey: &T::AccountId, + new_hotkey: &T::AccountId, + netuid: NetUid, + weight: &mut Weight, + ) -> DispatchResult { + // 1) Load the current relations around old_hotkey + let mut relations = Self::load_child_parent_relations(old_hotkey, netuid)?; + weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 0)); + + // 2) Clean up all storage entries that reference old_hotkey + // 2a) For each child of old_hotkey: remove old_hotkey from ParentKeys(child, netuid) + for (child, _) in relations.children().iter() { + let mut pk = ParentKeys::::get(child.clone(), netuid); + ParentChildRelations::::remove_edge(&mut pk, old_hotkey); + Self::set_parentkeys(child.clone(), netuid, pk.clone()); + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + } + // 2b) For each parent of old_hotkey: remove old_hotkey from ChildKeys(parent, netuid) + for (parent, _) in relations.parents().iter() { + let mut ck = ChildKeys::::get(parent.clone(), netuid); + ParentChildRelations::::remove_edge(&mut ck, old_hotkey); + ChildKeys::::insert(parent.clone(), netuid, ck); + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + } + // 2c) Clear direct maps of old_hotkey + ChildKeys::::insert( + old_hotkey.clone(), + netuid, + Vec::<(u64, T::AccountId)>::new(), + ); + Self::set_parentkeys( + old_hotkey.clone(), + netuid, + Vec::<(u64, T::AccountId)>::new(), + ); + weight.saturating_accrue(T::DbWeight::get().reads_writes(0, 2)); + + // 3) Rebind pivot to new_hotkey (validate no self-loop with existing maps) + relations.rebind_pivot(new_hotkey.clone())?; + + // 4) Swap PendingChildKeys( netuid, parent ) --> Vec<(proportion,child), cool_down_block> + // Fail if consistency breaks + if PendingChildKeys::::contains_key(netuid, old_hotkey) { + let (children, cool_down_block) = PendingChildKeys::::get(netuid, old_hotkey); + relations.ensure_pending_consistency(&children)?; + + PendingChildKeys::::remove(netuid, old_hotkey); + PendingChildKeys::::insert(netuid, new_hotkey, (children, cool_down_block)); + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 2)); + } + + // 5) Persist relations under the new pivot (diffs vs existing state at new_hotkey) + Self::persist_child_parent_relations(relations, netuid, weight) + } +} diff --git a/pallets/subtensor/src/staking/set_children/repair_children.rs b/pallets/subtensor/src/staking/set_children/repair_children.rs new file mode 100644 index 0000000000..550eb46c24 --- /dev/null +++ b/pallets/subtensor/src/staking/set_children/repair_children.rs @@ -0,0 +1,265 @@ +//! Weight-metered cleanup / repair of child–parent storage inconsistencies. +use super::*; +use subtensor_runtime_common::NetUid; + +impl Pallet { + pub fn clean_zero_childkey_vectors(weight: &mut Weight) { + // Collect keys to delete first to avoid mutating while iterating. + let mut to_remove: Vec<(T::AccountId, NetUid)> = Vec::new(); + + for (parent, netuid, children) in ChildKeys::::iter() { + // Account for the read + *weight = weight.saturating_add(T::DbWeight::get().reads(1)); + + if children.is_empty() { + to_remove.push((parent, netuid)); + } + } + + // Remove all empty entries + for (parent, netuid) in &to_remove { + ChildKeys::::remove(parent, netuid); + // Account for the write + *weight = weight.saturating_add(T::DbWeight::get().writes(1)); + } + log::info!( + target: "runtime", + "Removed {} empty childkey vectors.", + to_remove.len() + ); + } + + /// Remove self-loops in `ChildKeys` and `ParentKeys`. + /// If, after removal, a value-vector becomes empty, the storage key is removed. + pub fn clean_self_loops(weight: &mut Weight) { + // ------------------------------- + // 1) ChildKeys: (parent, netuid) -> Vec<(w, child)> + // Remove any entries where child == parent. + // ------------------------------- + let mut to_update_ck: Vec<((T::AccountId, NetUid), Vec<(u64, T::AccountId)>)> = Vec::new(); + let mut to_remove_ck: Vec<(T::AccountId, NetUid)> = Vec::new(); + + for (parent, netuid, children) in ChildKeys::::iter() { + *weight = weight.saturating_add(T::DbWeight::get().reads(1)); + + // Filter out self-loops + let filtered: Vec<(u64, T::AccountId)> = children + .clone() + .into_iter() + .filter(|(_, c)| *c != parent) + .collect(); + + // If nothing changed, skip + // (we can detect by comparing lengths; safer is to re-check if any removed existed) + // For simplicity, just compare lengths: + // If len unchanged and the previous vector had no self-loop, skip. + // If there *was* a self-loop and filtered is empty, we'll remove the key. + if filtered.len() == children.len() { + // No change -> continue + continue; + } + + if filtered.is_empty() { + to_remove_ck.push((parent, netuid)); + } else { + to_update_ck.push(((parent, netuid), filtered)); + } + } + + // Apply ChildKeys updates/removals + for ((parent, netuid), new_vec) in &to_update_ck { + Self::set_childkeys(parent.clone(), *netuid, new_vec.clone()); + *weight = weight.saturating_add(T::DbWeight::get().writes(1)); + } + for (parent, netuid) in &to_remove_ck { + ChildKeys::::remove(parent, netuid); + *weight = weight.saturating_add(T::DbWeight::get().writes(1)); + } + log::info!( + target: "runtime", + "Removed {} self-looping childkeys.", + to_update_ck.len().saturating_add(to_remove_ck.len()) + ); + + // ------------------------------- + // 2) ParentKeys: (child, netuid) -> Vec<(w, parent)> + // Remove any entries where parent == child. + // ------------------------------- + let mut to_update_pk: Vec<((T::AccountId, NetUid), Vec<(u64, T::AccountId)>)> = Vec::new(); + let mut to_remove_pk: Vec<(T::AccountId, NetUid)> = Vec::new(); + + for (child, netuid, parents) in ParentKeys::::iter() { + *weight = weight.saturating_add(T::DbWeight::get().reads(1)); + + // Filter out self-loops + let filtered: Vec<(u64, T::AccountId)> = parents + .clone() + .into_iter() + .filter(|(_, p)| *p != child) + .collect(); + + // If unchanged, skip + if filtered.len() == parents.len() { + continue; + } + + if filtered.is_empty() { + to_remove_pk.push((child, netuid)); + } else { + to_update_pk.push(((child, netuid), filtered)); + } + } + + // Apply ParentKeys updates/removals + for ((child, netuid), new_vec) in &to_update_pk { + Self::set_parentkeys(child.clone(), *netuid, new_vec.clone()); + *weight = weight.saturating_add(T::DbWeight::get().writes(1)); + } + for (child, netuid) in &to_remove_pk { + ParentKeys::::remove(child, netuid); + *weight = weight.saturating_add(T::DbWeight::get().writes(1)); + } + log::info!( + target: "runtime", + "Removed {} self-looping parentkeys.", + to_update_pk.len().saturating_add(to_remove_pk.len()) + ); + } + + pub fn clean_zero_parentkey_vectors(weight: &mut Weight) { + // Collect keys to delete first to avoid mutating while iterating. + let mut to_remove: Vec<(T::AccountId, NetUid)> = Vec::new(); + + for (parent, netuid, children) in ParentKeys::::iter() { + // Account for the read + *weight = weight.saturating_add(T::DbWeight::get().reads(1)); + + if children.is_empty() { + to_remove.push((parent, netuid)); + } + } + + // Remove all empty entries + for (parent, netuid) in &to_remove { + ParentKeys::::remove(parent, netuid); + // Account for the write + *weight = weight.saturating_add(T::DbWeight::get().writes(1)); + } + log::info!( + target: "runtime", + "Removed {} empty parentkey vectors.", + to_remove.len() + ); + } + + /// Make ChildKeys and ParentKeys bidirectionally consistent by + /// **removing** entries that don't have a matching counterpart. + /// A match means the exact tuple `(p, other_id)` is present on the opposite map. + /// + /// Rules: + /// * For each (parent, netuid) -> [(p, child)...] in ChildKeys: + /// keep only those (p, child) that appear in ParentKeys(child, netuid) as (p, parent). + /// If resulting list is empty, remove the key. + /// * For each (child, netuid) -> [(p, parent)...] in ParentKeys: + /// keep only those (p, parent) that appear in ChildKeys(parent, netuid) as (p, child). + /// If resulting list is empty, remove the key. + pub fn repair_child_parent_consistency(weight: &mut Weight) { + // ------------------------------- + // 1) Prune ChildKeys by checking ParentKeys + // ------------------------------- + let mut ck_updates: Vec<((T::AccountId, NetUid), Vec<(u64, T::AccountId)>)> = Vec::new(); + let mut ck_removes: Vec<(T::AccountId, NetUid)> = Vec::new(); + + for (parent, netuid, children) in ChildKeys::::iter() { + *weight = weight.saturating_add(T::DbWeight::get().reads(1)); + + // Keep (p, child) only if ParentKeys(child, netuid) contains (p, parent) + let mut filtered: Vec<(u64, T::AccountId)> = Vec::with_capacity(children.len()); + for (p, child) in children.clone().into_iter() { + let rev = ParentKeys::::get(&child, netuid); + *weight = weight.saturating_add(T::DbWeight::get().reads(1)); + let has_match = rev.iter().any(|(pr, pa)| *pr == p && *pa == parent); + if has_match { + filtered.push((p, child)); + } + } + + if filtered.is_empty() { + ck_removes.push((parent, netuid)); + } else { + // Only write if changed + if children != filtered { + ck_updates.push(((parent, netuid), filtered)); + } + } + } + + for ((parent, netuid), new_vec) in &ck_updates { + Self::set_childkeys(parent.clone(), *netuid, new_vec.clone()); + *weight = weight.saturating_add(T::DbWeight::get().writes(1)); + } + for (parent, netuid) in &ck_removes { + ChildKeys::::remove(parent, netuid); + *weight = weight.saturating_add(T::DbWeight::get().writes(1)); + } + log::info!( + target: "runtime", + "Updated {} childkey inconsistent records.", + ck_updates.len() + ); + log::info!( + target: "runtime", + "Removed {} childkey inconsistent records.", + ck_removes.len() + ); + + // ------------------------------- + // 2) Prune ParentKeys by checking ChildKeys + // ------------------------------- + let mut pk_updates: Vec<((T::AccountId, NetUid), Vec<(u64, T::AccountId)>)> = Vec::new(); + let mut pk_removes: Vec<(T::AccountId, NetUid)> = Vec::new(); + + for (child, netuid, parents) in ParentKeys::::iter() { + *weight = weight.saturating_add(T::DbWeight::get().reads(1)); + + // Keep (p, parent) only if ChildKeys(parent, netuid) contains (p, child) + let mut filtered: Vec<(u64, T::AccountId)> = Vec::with_capacity(parents.len()); + for (p, parent) in parents.clone().into_iter() { + let fwd = ChildKeys::::get(&parent, netuid); + *weight = weight.saturating_add(T::DbWeight::get().reads(1)); + let has_match = fwd.iter().any(|(pr, ch)| *pr == p && *ch == child); + if has_match { + filtered.push((p, parent)); + } + } + + if filtered.is_empty() { + pk_removes.push((child, netuid)); + } else { + // Only write if changed + if parents != filtered { + pk_updates.push(((child, netuid), filtered)); + } + } + } + + for ((child, netuid), new_vec) in &pk_updates { + Self::set_parentkeys(child.clone(), *netuid, new_vec.clone()); + *weight = weight.saturating_add(T::DbWeight::get().writes(1)); + } + for (child, netuid) in &pk_removes { + ParentKeys::::remove(child, netuid); + *weight = weight.saturating_add(T::DbWeight::get().writes(1)); + } + log::info!( + target: "runtime", + "Updated {} parentkey inconsistent records.", + pk_updates.len() + ); + log::info!( + target: "runtime", + "Removed {} parentkey inconsistent records.", + pk_removes.len() + ); + } +} diff --git a/pallets/subtensor/src/staking/set_children/root_validators.rs b/pallets/subtensor/src/staking/set_children/root_validators.rs new file mode 100644 index 0000000000..5e71bc05a4 --- /dev/null +++ b/pallets/subtensor/src/staking/set_children/root_validators.rs @@ -0,0 +1,75 @@ +//! Auto-schedule childkeys for root validators onto a non-root subnet. +use super::*; +use subtensor_runtime_common::NetUid; + +impl Pallet { + //////////////////////////////////////////////////////////// + // State cleaners (for use in migration) + // TODO: Deprecate when the state is clean for a while + + /// Establishes parent-child relationships between all root validators and + /// a subnet owner's hotkey on the specified subnet. + /// + /// For each validator on the root network (netuid 0), this function calls + /// `do_schedule_children` to schedule the subnet owner hotkey as a child + /// of that root validator on the given subnet, with full proportion (u64::MAX). + /// + /// # Arguments + /// * `netuid`: The subnet on which to establish relationships. + /// + /// # Returns + /// * `DispatchResult`: Ok if at least the setup completes; individual + /// scheduling failures per validator are logged but do not abort the loop. + pub fn do_set_root_validators_for_subnet(netuid: NetUid) -> DispatchResult { + // Cannot set children on root network itself. + ensure!( + !netuid.is_root(), + Error::::RegistrationNotPermittedOnRootSubnet + ); + + // Subnet must exist. + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); + + // Get the subnet owner hotkey. + let subnet_owner_hotkey = + SubnetOwnerHotkey::::try_get(netuid).map_err(|_| Error::::SubnetNotExists)?; + + // Iterate over all root validators and schedule each one as a parent + // of the subnet owner hotkey. + for (_uid, root_validator_hotkey) in Keys::::iter_prefix(NetUid::ROOT) { + // Skip if the root validator is the subnet owner hotkey itself + // (cannot be both parent and child). + if root_validator_hotkey == subnet_owner_hotkey { + continue; + } + + // Skip if root validator disabled auto parent delegation via AutoParentDelegationEnabled flag + if !Self::get_auto_parent_delegation_enabled(&root_validator_hotkey) { + continue; + } + + // Look up the coldkey that owns this root validator hotkey. + let coldkey = Self::get_owning_coldkey_for_hotkey(&root_validator_hotkey); + + // Build a signed origin from the coldkey. + let origin: ::RuntimeOrigin = + frame_system::RawOrigin::Signed(coldkey).into(); + + // Schedule the subnet owner hotkey as a child with full proportion. + let children = vec![(u64::MAX, subnet_owner_hotkey.clone())]; + + if let Err(e) = + Self::do_schedule_children(origin, root_validator_hotkey.clone(), netuid, children) + { + log::warn!( + "Failed to schedule children for root validator {:?} on netuid {:?}: {:?}", + root_validator_hotkey, + netuid, + e + ); + } + } + + Ok(()) + } +} diff --git a/pallets/subtensor/src/staking/set_children/schedule_children.rs b/pallets/subtensor/src/staking/set_children/schedule_children.rs new file mode 100644 index 0000000000..08d19794b2 --- /dev/null +++ b/pallets/subtensor/src/staking/set_children/schedule_children.rs @@ -0,0 +1,207 @@ +//! Schedule and apply pending child-key sets (`PendingChildKeys`). +use super::*; +use subtensor_runtime_common::NetUid; + +impl Pallet { + /// The implementation for the extrinsic do_set_child_singular: Sets a single child. + /// This function allows a coldkey to set children keys. + /// + /// Adds a childkey vector to the PendingChildKeys map and performs a few checks: + /// **Signature Verification**: Ensures that the caller has signed the transaction, verifying the coldkey. + /// **Root Network Check**: Ensures that the delegation is not on the root network, as child hotkeys are not valid on the root. + /// **Network Existence Check**: Ensures that the specified network exists. + /// **Ownership Verification**: Ensures that the coldkey owns the hotkey. + /// **Hotkey Account Existence Check**: Ensures that the hotkey account already exists. + /// **Child count**: Only allow to add up to 5 children per parent + /// **Child-Hotkey Distinction**: Ensures that the child is not the same as the hotkey. + /// **Minimum stake**: Ensures that the parent key has at least the minimum stake. + /// **Proportion check**: Ensure that the sum of the proportions does not exceed u64::MAX. + /// **Duplicate check**: Ensure there are no duplicates in the list of children. + /// + /// # Events + /// * `SetChildrenScheduled`: If all checks pass and setting the childkeys is scheduled. + /// + /// # Errors + /// * `MechanismDoesNotExist`: Attempting to register to a non-existent network. + /// * `RegistrationNotPermittedOnRootSubnet`: Attempting to register a child on the root network. + /// * `NonAssociatedColdKey`: The coldkey does not own the hotkey or the child is the same as the hotkey. + /// * `HotKeyAccountNotExists`: The hotkey account does not exist. + /// * `TooManyChildren`: Too many children in request. + /// + pub fn do_schedule_children( + origin: OriginFor, + hotkey: T::AccountId, + netuid: NetUid, + children: Vec<(u64, T::AccountId)>, + ) -> DispatchResult { + // Check that the caller has signed the transaction. (the coldkey of the pairing) + let coldkey = ensure_signed(origin)?; + log::trace!( + "do_set_children( coldkey:{coldkey:?} hotkey:{netuid:?} netuid:{hotkey:?} children:{children:?} )" + ); + + // Ensure the hotkey passes the rate limit. + ensure!( + TransactionType::SetChildren.passes_rate_limit_on_subnet::( + &hotkey, // Specific to a hotkey. + netuid, // Specific to a subnet. + ), + Error::::TxRateLimitExceeded + ); + + // Check that this delegation is not on the root network. Child hotkeys are not valid on root. + ensure!( + !netuid.is_root(), + Error::::RegistrationNotPermittedOnRootSubnet + ); + + // Check that the network we are trying to create the child on exists. + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); + + // Check that the coldkey owns the hotkey. + ensure!( + Self::coldkey_owns_hotkey(&coldkey, &hotkey), + Error::::NonAssociatedColdKey + ); + + // Ensure there are no duplicates in the list of children. + let mut unique_children = Vec::new(); + for (_, child_i) in &children { + ensure!( + !unique_children.contains(child_i), + Error::::DuplicateChild + ); + unique_children.push(child_i.clone()); + } + + // Ensure we don't break consistency when these new childkeys are set: + // - Ensure that the number of children does not exceed 5 + // - Each child is not the hotkey. + // - The sum of the proportions does not exceed u64::MAX. + // - Bipartite separation (no A <-> B relations) + let relations = Self::load_child_parent_relations(&hotkey, netuid)?; + relations.ensure_pending_consistency(&children)?; + + // Check that the parent key has at least the minimum own stake + // if children vector is not empty + // (checking with check_weights_min_stake wouldn't work because it considers + // grandparent stake in this case) + ensure!( + children.is_empty() + || Self::get_total_stake_for_hotkey(&hotkey) >= StakeThreshold::::get().into() + || SubnetOwnerHotkey::::try_get(netuid) + .is_ok_and(|owner_hotkey| owner_hotkey.eq(&hotkey)), + Error::::NotEnoughStakeToSetChildkeys + ); + + // Set last transaction block + let current_block = Self::get_current_block_as_u64(); + TransactionType::SetChildren.set_last_block_on_subnet::(&hotkey, netuid, current_block); + + // Schedule or immediately apply CK + Self::schedule_or_apply_ck(netuid, hotkey, children) + } + + /// If the start call occured, schedule children, otherwise, + /// apply immediately + pub(crate) fn schedule_or_apply_ck( + netuid: NetUid, + hotkey: T::AccountId, + children: Vec<(u64, T::AccountId)>, + ) -> DispatchResult { + if !SubtokenEnabled::::get(netuid) { + Self::persist_pending_children_ok(netuid, &hotkey, &children); + return Ok(()); + } + + // Calculate cool-down block + let cooldown_block = + Self::get_current_block_as_u64().saturating_add(PendingChildKeyCooldown::::get()); + + // Insert or update PendingChildKeys + PendingChildKeys::::insert(netuid, hotkey.clone(), (children.clone(), cooldown_block)); + + // Log and return. + log::trace!( + "SetChildrenScheduled( netuid:{:?}, cooldown_block:{:?}, hotkey:{:?}, children:{:?} )", + cooldown_block, + hotkey, + netuid, + children.clone() + ); + Self::deposit_event(Event::SetChildrenScheduled( + hotkey, + netuid, + cooldown_block, + children, + )); + + // Ok and return. + Ok(()) + } + + /// This function executes setting children keys when called during hotkey draining. + /// + /// * `netuid`: The u16 network identifier where the child keys will exist. + /// + /// # Events + /// * `SetChildren`: On successfully registering children to a hotkey. + /// + /// # Errors + /// * `MechanismDoesNotExist`: Attempting to register to a non-existent network. + /// * `RegistrationNotPermittedOnRootSubnet`: Attempting to register a child on the root network. + /// * `NonAssociatedColdKey`: The coldkey does not own the hotkey or the child is the same as the hotkey. + /// * `HotKeyAccountNotExists`: The hotkey account does not exist. + /// + /// # Note + /// 1. **Old Children Cleanup**: Removes the hotkey from the parent list of its old children. + /// 2. **New Children Assignment**: Assigns the new child to the hotkey and updates the parent list for the new child. + /// + pub fn do_set_pending_children(netuid: NetUid) { + let current_block = Self::get_current_block_as_u64(); + + // If the childkey cools down before the subnet start call + PendingChildKeyCooldown: + // - If Start call happened: Normal track + // - If Start call didn't happen: Apply immediately + // TODO: This check may be removed after all ck are applied after the runtime upgrade + let start_call_occured = SubtokenEnabled::::get(netuid); + + // Iterate over all pending children of this subnet and set as needed + let mut to_remove: Vec = Vec::new(); + + PendingChildKeys::::iter_prefix(netuid).for_each( + |(hotkey, (children, cool_down_block))| { + if (cool_down_block < current_block) || !start_call_occured { + Self::persist_pending_children_ok(netuid, &hotkey, &children); + to_remove.push(hotkey); + } + }, + ); + + for hotkey in to_remove { + PendingChildKeys::::remove(netuid, hotkey); + } + } + + // If child-parent consistency is broken, fail setting new children silently + pub(crate) fn persist_pending_children_ok( + netuid: NetUid, + hotkey: &T::AccountId, + children: &Vec<(u64, T::AccountId)>, + ) { + let maybe_relations = Self::load_relations_from_pending(hotkey.clone(), children, netuid); + if let Ok(relations) = maybe_relations { + let mut _weight: Weight = T::DbWeight::get().reads(0); + if let Ok(()) = Self::persist_child_parent_relations(relations, netuid, &mut _weight) { + // Log and emit event. + log::trace!( + "SetChildren( netuid:{:?}, hotkey:{:?}, children:{:?} )", + hotkey, + netuid, + children.clone() + ); + Self::deposit_event(Event::SetChildren(hotkey.clone(), netuid, children.clone())); + } + } + } +} diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs deleted file mode 100644 index fe5037eb98..0000000000 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ /dev/null @@ -1,1532 +0,0 @@ -use super::*; -use safe_math::*; -use share_pool::{SafeFloat, SharePool, SharePoolDataOperations}; -use sp_std::{collections::btree_map::BTreeMap, ops::Neg}; -use substrate_fixed::types::{I64F64, I96F32, U64F64, U96F32}; -use subtensor_runtime_common::{AlphaBalance, AuthorshipInfo, NetUid, TaoBalance, Token}; -use subtensor_swap_interface::{Order, SwapHandler, SwapResult}; - -impl Pallet { - /// Retrieves the total alpha issuance for a given subnet. - /// - /// This function calculates the total alpha issuance by summing the alpha - /// values from `SubnetAlphaIn` and `SubnetAlphaOut` for the specified subnet. - /// - /// # Arguments - /// * `netuid`: The unique identifier of the subnet. - /// - /// # Returns - /// * `u64`: The total alpha issuance for the specified subnet. - pub fn get_alpha_issuance(netuid: NetUid) -> AlphaBalance { - SubnetAlphaIn::::get(netuid) - .saturating_add(SubnetAlphaOut::::get(netuid)) - .saturating_add(T::SwapInterface::protocol_alpha_reservoir(netuid)) - } - - pub fn get_moving_alpha_price(netuid: NetUid) -> U64F64 { - let one = U64F64::saturating_from_num(1.0); - if netuid.is_root() { - // Root. - one - } else if SubnetMechanism::::get(netuid) == 0 { - // Stable - one - } else { - U64F64::saturating_from_num(SubnetMovingPrice::::get(netuid)) - } - } - - pub fn update_moving_price(netuid: NetUid) { - let blocks_since_start_call = U64F64::saturating_from_num({ - // We expect FirstEmissionBlockNumber to be set earlier, and we take the block when - // `start_call` was called (first block before FirstEmissionBlockNumber). - let start_call_block = FirstEmissionBlockNumber::::get(netuid) - .unwrap_or_default() - .saturating_sub(1); - - Self::get_current_block_as_u64().saturating_sub(start_call_block) - }); - - // Use halving time hyperparameter. The meaning of this parameter can be best explained under - // the assumption of a constant price and SubnetMovingAlpha == 0.5: It is how many blocks it - // will take in order for the distance between current EMA of price and current price to shorten - // by half. - let halving_time = EMAPriceHalvingBlocks::::get(netuid); - let current_ma_unsigned = U64F64::saturating_from_num(SubnetMovingAlpha::::get()); - let alpha: U64F64 = current_ma_unsigned.saturating_mul(blocks_since_start_call.safe_div( - blocks_since_start_call.saturating_add(U64F64::saturating_from_num(halving_time)), - )); - // Because alpha = b / (b + h), where b and h > 0, alpha < 1, so 1 - alpha > 0. - // We can use unsigned type here: U96F32 - let one_minus_alpha: U64F64 = U64F64::saturating_from_num(1.0).saturating_sub(alpha); - let current_price: U64F64 = alpha.saturating_mul(U64F64::saturating_from_num( - T::SwapInterface::current_alpha_price(netuid.into()) - .min(U64F64::saturating_from_num(1.0)), - )); - let current_moving: U64F64 = one_minus_alpha.saturating_mul(U64F64::saturating_from_num( - Self::get_moving_alpha_price(netuid), - )); - // Convert batch to signed I96F32 to avoid migration of SubnetMovingPrice for now`` - let new_moving: I96F32 = - I96F32::saturating_from_num(current_price.saturating_add(current_moving)); - SubnetMovingPrice::::insert(netuid, new_moving); - } - - /// Gets the Median Subnet Alpha Price - pub fn get_median_subnet_alpha_price() -> U64F64 { - let default_price = U64F64::saturating_from_num(1_u64); - let zero_price = U64F64::saturating_from_num(0_u64); - let two = U64F64::saturating_from_num(2_u64); - - let mut price_counts: BTreeMap = BTreeMap::new(); - let mut total_prices: usize = 0; - - for (netuid, added) in NetworksAdded::::iter() { - if !added || netuid == NetUid::ROOT { - continue; - } - - let price = T::SwapInterface::current_alpha_price(netuid); - if price <= zero_price { - continue; - } - - total_prices = total_prices.saturating_add(1); - - if let Some(count) = price_counts.get_mut(&price) { - *count = count.saturating_add(1); - } else { - price_counts.insert(price, 1usize); - } - } - - if total_prices == 0 { - return default_price; - } - - let Some(last_index) = total_prices.checked_sub(1) else { - return default_price; - }; - let Some(lower_target) = last_index.checked_div(2) else { - return default_price; - }; - let Some(upper_target) = total_prices.checked_div(2) else { - return default_price; - }; - - let mut cumulative: usize = 0; - let mut lower_price: Option = None; - let mut upper_price: Option = None; - - for (price, count) in price_counts.into_iter() { - let next_cumulative = cumulative.saturating_add(count); - - if lower_price.is_none() && lower_target < next_cumulative { - lower_price = Some(price); - } - - if upper_price.is_none() && upper_target < next_cumulative { - upper_price = Some(price); - } - - if lower_price.is_some() && upper_price.is_some() { - break; - } - - cumulative = next_cumulative; - } - - match (lower_price, upper_price) { - (Some(_), Some(upper)) if lower_target == upper_target => upper, - (Some(lower), Some(upper)) => lower.saturating_add(upper).safe_div(two), - _ => default_price, - } - } - - /// Retrieves the TAO weight as a normalized value between 0 and 1. - /// - /// This function performs the following steps: - /// 1. Fetches the TAO weight from storage using the TaoWeight storage item. - /// 2. Converts the retrieved u64 value to a fixed-point number (U96F32). - /// 3. Normalizes the weight by dividing it by the maximum possible u64 value. - /// 4. Returns the normalized weight as an U96F32 fixed-point number. - /// - /// The normalization ensures that the returned value is always between 0 and 1, - /// regardless of the actual stored weight value. - /// - /// # Returns - /// * `U96F32`: The normalized TAO weight as a fixed-point number between 0 and 1. - /// - /// # Note - /// This function uses saturating division to prevent potential overflow errors. - pub fn get_tao_weight() -> U96F32 { - // Step 1: Fetch the TAO weight from storage - let stored_weight = TaoWeight::::get(); - - // Step 2: Convert the u64 weight to U96F32 - let weight_fixed = U96F32::saturating_from_num(stored_weight); - - // Step 3: Normalize the weight by dividing by u64::MAX - // This ensures the result is always between 0 and 1 - weight_fixed.safe_div(U96F32::saturating_from_num(u64::MAX)) - } - pub fn get_ck_burn() -> U96F32 { - let stored_weight = CKBurn::::get(); - let weight_fixed = U96F32::saturating_from_num(stored_weight); - weight_fixed.safe_div(U96F32::saturating_from_num(u64::MAX)) - } - - /// Sets the TAO weight in storage. - /// - /// This function performs the following steps: - /// 1. Takes the provided weight value as a u64. - /// 2. Updates the TaoWeight storage item with the new value. - /// - /// # Arguments - /// * `weight`: The new TAO weight value to be set, as a u64. - /// - /// # Effects - /// This function modifies the following storage item: - /// * `TaoWeight`: Updates it with the new weight value. - /// - /// # Note - /// The weight is stored as a raw u64 value. To get the normalized weight between 0 and 1, - /// use the `get_tao_weight()` function. - pub fn set_tao_weight(weight: u64) { - // Update the TaoWeight storage with the new weight value - TaoWeight::::set(weight); - } - // Set the amount burned on non owned CK - pub fn set_ck_burn(weight: u64) { - // Update the ck burn value. - CKBurn::::set(weight); - } - - /// Calculates the weighted combination of alpha and TAO stake for a single hotkey on a subnet. - /// - pub fn get_stake_weights_for_hotkey_on_subnet( - hotkey: &T::AccountId, - netuid: NetUid, - ) -> (I64F64, I64F64, I64F64) { - // Retrieve the TAO weight. - let tao_weight = I64F64::saturating_from_num(Self::get_tao_weight()); - log::debug!("tao_weight: {tao_weight:?}"); - - // Step 1: Get stake of hotkey (neuron) - let alpha_stake = - I64F64::saturating_from_num(Self::get_inherited_for_hotkey_on_subnet(hotkey, netuid)); - log::debug!("alpha_stake: {alpha_stake:?}"); - - // Step 2: Get the TAO stake for the hotkey - let tao_stake = I64F64::saturating_from_num(Self::get_tao_inherited_for_hotkey_on_subnet( - hotkey, netuid, - )); - log::debug!("tao_stake: {tao_stake:?}"); - - // Step 3: Combine alpha and tao stakes - let total_stake = alpha_stake.saturating_add(tao_stake.saturating_mul(tao_weight)); - log::debug!("total_stake: {total_stake:?}"); - - (total_stake, alpha_stake, tao_stake) - } - - /// Calculates the weighted combination of alpha and TAO stake for hotkeys on a subnet. - /// - pub fn get_stake_weights_for_network( - netuid: NetUid, - ) -> (Vec, Vec, Vec) { - // Retrieve the TAO weight. - let tao_weight: I64F64 = I64F64::saturating_from_num(Self::get_tao_weight()); - log::debug!("tao_weight: {tao_weight:?}"); - - // Step 1: Get subnetwork size - let n: u16 = Self::get_subnetwork_n(netuid); - - // Step 2: Get stake of all hotkeys (neurons) ordered by uid - let alpha_stake: Vec = (0..n) - .map(|uid| { - if Keys::::contains_key(netuid, uid) { - let hotkey: T::AccountId = Keys::::get(netuid, uid); - I64F64::saturating_from_num(Self::get_inherited_for_hotkey_on_subnet( - &hotkey, netuid, - )) - } else { - I64F64::saturating_from_num(0) - } - }) - .collect(); - log::debug!("alpha_stake: {alpha_stake:?}"); - - // Step 3: Calculate the TAO stake vector. - // Initialize a vector to store TAO stakes for each neuron. - let tao_stake: Vec = (0..n) - .map(|uid| { - if Keys::::contains_key(netuid, uid) { - let hotkey: T::AccountId = Keys::::get(netuid, uid); - I64F64::saturating_from_num(Self::get_tao_inherited_for_hotkey_on_subnet( - &hotkey, netuid, - )) - } else { - I64F64::saturating_from_num(0) - } - }) - .collect(); - log::trace!("tao_stake: {tao_stake:?}"); - - // Step 4: Combine alpha and TAO stakes. - // Calculate the weighted average of alpha and TAO stakes for each neuron. - let total_stake: Vec = alpha_stake - .iter() - .zip(tao_stake.iter()) - .map(|(alpha_i, tao_i)| alpha_i.saturating_add(tao_i.saturating_mul(tao_weight))) - .collect(); - log::trace!("total_stake: {total_stake:?}"); - - (total_stake, alpha_stake, tao_stake) - } - - /// Calculates the total inherited stake (alpha) held by a hotkey on a network, considering child/parent relationships. - /// - /// This function performs the following steps: - /// 1. Retrieves the initial alpha (stake) for the hotkey on the specified subnet. - /// 2. Retrieves the list of children and parents for the hotkey on the subnet. - /// 3. Calculates the alpha allocated to children: - /// a. For each child, computes the proportion of alpha to be allocated. - /// b. Accumulates the total alpha allocated to all children. - /// 4. Calculates the alpha received from parents: - /// a. For each parent, retrieves the parent's stake on the subnet. - /// b. Computes the proportion of the parent's stake to be inherited. - /// c. Accumulates the total alpha inherited from all parents. - /// 5. Computes the final inherited alpha by adjusting the initial alpha: - /// a. Subtracts the alpha allocated to children. - /// b. Adds the alpha inherited from parents. - /// 6. Returns the final inherited alpha value. - /// - /// # Arguments - /// * `hotkey`: AccountId of the hotkey whose total inherited stake is to be calculated. - /// * `netuid`: Network unique identifier specifying the subnet context. - /// - /// # Returns - /// * `u64`: The total inherited alpha for the hotkey on the subnet after considering the - /// stakes allocated to children and inherited from parents. - /// - /// # Note - /// This function uses saturating arithmetic to prevent overflows. - pub fn get_tao_inherited_for_hotkey_on_subnet( - hotkey: &T::AccountId, - netuid: NetUid, - ) -> TaoBalance { - let initial_tao: U96F32 = - U96F32::saturating_from_num(Self::get_stake_for_hotkey_on_subnet(hotkey, NetUid::ROOT)); - - // Initialize variables to track alpha allocated to children and inherited from parents. - let mut tao_to_children: U96F32 = U96F32::saturating_from_num(0); - let mut tao_from_parents: U96F32 = U96F32::saturating_from_num(0); - - // Step 2: Retrieve the lists of parents and children for the hotkey on the subnet. - let parents: Vec<(u64, T::AccountId)> = Self::get_parents(hotkey, netuid); - let children: Vec<(u64, T::AccountId)> = Self::get_children(hotkey, netuid); - log::trace!("Parents for hotkey {hotkey:?} on subnet {netuid}: {parents:?}"); - log::trace!("Children for hotkey {hotkey:?} on subnet {netuid}: {children:?}"); - - // Step 3: Calculate the total tao allocated to children. - for (proportion, _) in children { - // Convert the proportion to a normalized value between 0 and 1. - let normalized_proportion: U96F32 = U96F32::saturating_from_num(proportion) - .safe_div(U96F32::saturating_from_num(u64::MAX)); - log::trace!("Normalized proportion for child: {normalized_proportion:?}"); - - // Calculate the amount of tao to be allocated to this child. - let tao_proportion_to_child: U96F32 = - U96F32::saturating_from_num(initial_tao).saturating_mul(normalized_proportion); - log::trace!("Tao proportion to child: {tao_proportion_to_child:?}"); - - // Add this child's allocation to the total tao allocated to children. - tao_to_children = tao_to_children.saturating_add(tao_proportion_to_child); - } - log::trace!("Total tao allocated to children: {tao_to_children:?}"); - - // Step 4: Calculate the total tao inherited from parents. - for (proportion, parent) in parents { - // Retrieve the parent's total stake on this subnet. - let parent_tao = U96F32::saturating_from_num(Self::get_stake_for_hotkey_on_subnet( - &parent, - NetUid::ROOT, - )); - log::trace!("Parent tao for parent {parent:?} on subnet {netuid}: {parent_tao:?}"); - - // Convert the proportion to a normalized value between 0 and 1. - let normalized_proportion = U96F32::saturating_from_num(proportion) - .safe_div(U96F32::saturating_from_num(u64::MAX)); - log::trace!("Normalized proportion from parent: {normalized_proportion:?}"); - - // Calculate the amount of tao to be inherited from this parent. - let tao_proportion_from_parent: U96F32 = - U96F32::saturating_from_num(parent_tao).saturating_mul(normalized_proportion); - log::trace!("Tao proportion from parent: {tao_proportion_from_parent:?}"); - - // Add this parent's contribution to the total tao inherited from parents. - tao_from_parents = tao_from_parents.saturating_add(tao_proportion_from_parent); - } - log::trace!("Total tao inherited from parents: {tao_from_parents:?}"); - - // Step 5: Calculate the final inherited tao for the hotkey. - let finalized_tao: U96F32 = initial_tao - .saturating_sub(tao_to_children) // Subtract tao allocated to children - .saturating_add(tao_from_parents); // Add tao inherited from parents - log::trace!("Finalized tao for hotkey {hotkey:?} on subnet {netuid}: {finalized_tao:?}"); - - // Step 6: Return the final inherited tao value. - finalized_tao.saturating_to_num::().into() - } - - pub fn get_inherited_for_hotkey_on_subnet( - hotkey: &T::AccountId, - netuid: NetUid, - ) -> AlphaBalance { - // Step 1: Retrieve the initial total stake (alpha) for the hotkey on the specified subnet. - let initial_alpha: U96F32 = - U96F32::saturating_from_num(Self::get_stake_for_hotkey_on_subnet(hotkey, netuid)); - log::debug!("Initial alpha for hotkey {hotkey:?} on subnet {netuid}: {initial_alpha:?}"); - if netuid.is_root() { - return initial_alpha.saturating_to_num::().into(); - } - - // Initialize variables to track alpha allocated to children and inherited from parents. - let mut alpha_to_children: U96F32 = U96F32::saturating_from_num(0); - let mut alpha_from_parents: U96F32 = U96F32::saturating_from_num(0); - - // Step 2: Retrieve the lists of parents and children for the hotkey on the subnet. - let parents: Vec<(u64, T::AccountId)> = Self::get_parents(hotkey, netuid); - let children: Vec<(u64, T::AccountId)> = Self::get_children(hotkey, netuid); - log::debug!("Parents for hotkey {hotkey:?} on subnet {netuid}: {parents:?}"); - log::debug!("Children for hotkey {hotkey:?} on subnet {netuid}: {children:?}"); - - // Step 3: Calculate the total alpha allocated to children. - for (proportion, _) in children { - // Convert the proportion to a normalized value between 0 and 1. - let normalized_proportion: U96F32 = U96F32::saturating_from_num(proportion) - .safe_div(U96F32::saturating_from_num(u64::MAX)); - log::trace!("Normalized proportion for child: {normalized_proportion:?}"); - - // Calculate the amount of alpha to be allocated to this child. - let alpha_proportion_to_child: U96F32 = - U96F32::saturating_from_num(initial_alpha).saturating_mul(normalized_proportion); - log::trace!("Alpha proportion to child: {alpha_proportion_to_child:?}"); - - // Add this child's allocation to the total alpha allocated to children. - alpha_to_children = alpha_to_children.saturating_add(alpha_proportion_to_child); - } - log::debug!("Total alpha allocated to children: {alpha_to_children:?}"); - - // Step 4: Calculate the total alpha inherited from parents. - for (proportion, parent) in parents { - // Retrieve the parent's total stake on this subnet. - let parent_alpha: U96F32 = - U96F32::saturating_from_num(Self::get_stake_for_hotkey_on_subnet(&parent, netuid)); - log::trace!("Parent alpha for parent {parent:?} on subnet {netuid}: {parent_alpha:?}"); - - // Convert the proportion to a normalized value between 0 and 1. - let normalized_proportion: U96F32 = U96F32::saturating_from_num(proportion) - .safe_div(U96F32::saturating_from_num(u64::MAX)); - log::trace!("Normalized proportion from parent: {normalized_proportion:?}"); - - // Calculate the amount of alpha to be inherited from this parent. - let alpha_proportion_from_parent: U96F32 = - U96F32::saturating_from_num(parent_alpha).saturating_mul(normalized_proportion); - log::trace!("Alpha proportion from parent: {alpha_proportion_from_parent:?}"); - - // Add this parent's contribution to the total alpha inherited from parents. - alpha_from_parents = alpha_from_parents.saturating_add(alpha_proportion_from_parent); - } - log::debug!("Total alpha inherited from parents: {alpha_from_parents:?}"); - - // Step 5: Calculate the final inherited alpha for the hotkey. - let finalized_alpha: U96F32 = initial_alpha - .saturating_sub(alpha_to_children) // Subtract alpha allocated to children - .saturating_add(alpha_from_parents); // Add alpha inherited from parents - log::trace!( - "Finalized alpha for hotkey {hotkey:?} on subnet {netuid}: {finalized_alpha:?}" - ); - - // Step 6: Return the final inherited alpha value. - finalized_alpha.saturating_to_num::().into() - } - - /// Checks if a specific hotkey-coldkey pair has enough stake on a subnet to fulfill a given decrement. - /// - /// This function performs the following steps: - /// 1. Retrieves the current stake for the hotkey-coldkey pair on the specified subnet. - /// 2. Compares this stake with the requested decrement amount. - /// - /// # Arguments - /// * `hotkey`: The account ID of the hotkey. - /// * `coldkey`: The account ID of the coldkey. - /// * `netuid`: The unique identifier of the subnet. - /// * `decrement`: The amount of stake to be potentially decremented. - /// - /// # Returns - /// * `bool`: True if the account has enough stake to fulfill the decrement, false otherwise. - /// - /// # Note - /// This function only checks the stake for the specific hotkey-coldkey pair, not the total stake of the hotkey or coldkey individually. - pub fn calculate_reduced_stake_on_subnet( - hotkey: &T::AccountId, - coldkey: &T::AccountId, - netuid: NetUid, - decrement: AlphaBalance, - ) -> Result> { - // Retrieve the current stake for this hotkey-coldkey pair on the subnet - let current_stake = - Self::get_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid); - - // Compare the current stake with the requested decrement - // Return true if the current stake is greater than or equal to the decrement - if current_stake >= decrement { - Ok(current_stake.saturating_sub(decrement)) - } else { - Err(Error::::NotEnoughStakeToWithdraw) - } - } - - /// Retrieves the alpha (stake) value for a given hotkey and coldkey pair on a specific subnet. - /// - /// This function performs the following steps: - /// 1. Takes the hotkey, coldkey, and subnet ID as input parameters. - /// 2. Accesses the Alpha storage map to retrieve the stake value. - /// 3. Returns the retrieved stake value as a u64. - /// - /// # Arguments - /// * `hotkey`: The account ID of the hotkey (neuron). - /// * `coldkey`: The account ID of the coldkey (owner). - /// * `netuid`: The unique identifier of the subnet. - /// - /// # Returns - /// * `u64`: The alpha (stake) value for the specified hotkey-coldkey pair on the given subnet. - /// - /// # Note - /// This function retrieves the stake specific to the hotkey-coldkey pair, not the total stake of the hotkey or coldkey individually. - pub fn get_stake_for_hotkey_and_coldkey_on_subnet( - hotkey: &T::AccountId, - coldkey: &T::AccountId, - netuid: NetUid, - ) -> AlphaBalance { - let alpha_share_pool = Self::get_alpha_share_pool(hotkey.clone(), netuid); - alpha_share_pool.try_get_value(coldkey).unwrap_or(0).into() - } - - /// Retrieves the total stake (alpha) for a given hotkey on a specific subnet. - /// - /// This function performs the following step: - /// 1. Retrieves and returns the total alpha value associated with the hotkey on the specified subnet. - /// - /// # Arguments - /// * `hotkey`: The account ID of the hotkey. - /// * `netuid`: The unique identifier of the subnet. - /// - /// # Returns - /// * `u64`: The total alpha value for the hotkey on the specified subnet. - /// - /// # Note - /// This function returns the cumulative stake across all coldkeys associated with this hotkey on the subnet. - pub fn get_stake_for_hotkey_on_subnet(hotkey: &T::AccountId, netuid: NetUid) -> AlphaBalance { - // Retrieve and return the total alpha this hotkey owns on this subnet. - // This value represents the sum of stakes from all coldkeys associated with this hotkey. - TotalHotkeyAlpha::::get(hotkey, netuid) - } - - /// Increase hotkey stake on a subnet. - /// - /// The function updates share totals given current prices. - /// - /// # Arguments - /// * `hotkey`: The account ID of the hotkey. - /// * `netuid`: The unique identifier of the subnet. - /// * `amount`: The amount of alpha to be added. - /// - pub fn increase_stake_for_hotkey_on_subnet( - hotkey: &T::AccountId, - netuid: NetUid, - amount: AlphaBalance, - ) { - let mut alpha_share_pool = Self::get_alpha_share_pool(hotkey.clone(), netuid); - alpha_share_pool.update_value_for_all(amount.to_u64() as i64); - } - - /// Decrease hotkey stake on a subnet. - /// - /// The function updates share totals given current prices. - /// - /// # Arguments - /// * `hotkey`: The account ID of the hotkey. - /// * `netuid`: The unique identifier of the subnet. - /// * `amount`: The amount of alpha to be added. - /// - pub fn decrease_stake_for_hotkey_on_subnet(hotkey: &T::AccountId, netuid: NetUid, amount: u64) { - let mut alpha_share_pool = Self::get_alpha_share_pool(hotkey.clone(), netuid); - alpha_share_pool.update_value_for_all((amount as i64).neg()); - } - - /// Buys shares in the hotkey on a given subnet - /// - /// The function updates share totals given current prices. - /// - /// # Arguments - /// * `hotkey`: The account ID of the hotkey. - /// * `coldkey`: The account ID of the coldkey (owner). - /// * `netuid`: The unique identifier of the subnet. - /// * `amount`: The amount of alpha to be added. - /// - pub fn increase_stake_for_hotkey_and_coldkey_on_subnet( - hotkey: &T::AccountId, - coldkey: &T::AccountId, - netuid: NetUid, - amount: AlphaBalance, - ) { - if !amount.is_zero() { - let mut staking_hotkeys = StakingHotkeys::::get(coldkey); - if !staking_hotkeys.contains(hotkey) { - staking_hotkeys.push(hotkey.clone()); - StakingHotkeys::::insert(coldkey, staking_hotkeys.clone()); - } - } - - let mut alpha_share_pool = Self::get_alpha_share_pool(hotkey.clone(), netuid); - // We expect to add a positive amount here. - let amount = amount.to_u64() as i64; - alpha_share_pool.update_value_for_one(coldkey, amount); - } - - pub fn try_increase_stake_for_hotkey_and_coldkey_on_subnet( - hotkey: &T::AccountId, - netuid: NetUid, - amount: AlphaBalance, - ) -> bool { - let mut alpha_share_pool = Self::get_alpha_share_pool(hotkey.clone(), netuid); - let amount = amount.to_u64() as i64; - alpha_share_pool.sim_update_value_for_one(amount) - } - - /// Sell shares in the hotkey on a given subnet - /// - /// The function updates share totals given current prices. - /// - /// # Arguments - /// * `hotkey`: The account ID of the hotkey. - /// * `coldkey`: The account ID of the coldkey (owner). - /// * `netuid`: The unique identifier of the subnet. - /// * `amount`: The amount of alpha to be added. - /// - pub fn decrease_stake_for_hotkey_and_coldkey_on_subnet( - hotkey: &T::AccountId, - coldkey: &T::AccountId, - netuid: NetUid, - amount: AlphaBalance, - ) { - let mut alpha_share_pool = Self::get_alpha_share_pool(hotkey.clone(), netuid); - let amount = amount.to_u64(); - - // We expect a negative value here - if let Ok(value) = alpha_share_pool.try_get_value(coldkey) - && value >= amount - { - alpha_share_pool.update_value_for_one(coldkey, (amount as i64).neg()); - } - } - - /// Swaps TAO for the alpha token on the subnet. - /// - /// Updates TaoIn, AlphaIn, and AlphaOut - pub fn swap_tao_for_alpha( - netuid: NetUid, - tao: TaoBalance, - price_limit: TaoBalance, - drop_fees: bool, - ) -> Result, DispatchError> { - // Step 1: Get the mechanism type for the subnet (0 for Stable, 1 for Dynamic) - let mechanism_id: u16 = SubnetMechanism::::get(netuid); - let swap_result = if mechanism_id == 1 { - let order = GetAlphaForTao::::with_amount(tao); - T::SwapInterface::swap(netuid.into(), order, price_limit.into(), drop_fees, false)? - } else { - // Step 3.b.1: Stable mechanism, just return the value 1:1 - SwapResult { - amount_paid_in: tao, - amount_paid_out: tao.to_u64().into(), - fee_paid: TaoBalance::ZERO, - fee_to_block_author: TaoBalance::ZERO, - } - }; - - let alpha_decrease = swap_result.paid_out_reserve_delta_i64().unsigned_abs(); - - // Decrease Alpha reserves. - Self::decrease_provided_alpha_reserve(netuid.into(), alpha_decrease.into()); - - // Increase Alpha outstanding. - SubnetAlphaOut::::mutate(netuid, |total| { - *total = total.saturating_add(swap_result.amount_paid_out.into()); - }); - - // Increase the protocol TAO reserve - SubnetTAO::::mutate(netuid, |total| { - let delta = swap_result.paid_in_reserve_delta_i64().unsigned_abs(); - *total = total.saturating_add(delta.into()); - }); - - // Increase Total Tao reserves. - TotalStake::::mutate(|total| *total = total.saturating_add(tao)); - - // Increase total subnet TAO volume. - SubnetVolume::::mutate(netuid, |total| { - *total = total.saturating_add(tao.to_u64() as u128); - }); - - Ok(swap_result) - } - - /// Swaps a subnet's Alpha token for TAO. - /// - /// Updates TaoIn, AlphaIn, and AlphaOut - pub fn swap_alpha_for_tao( - netuid: NetUid, - alpha: AlphaBalance, - price_limit: TaoBalance, - drop_fees: bool, - ) -> Result, DispatchError> { - // Step 1: Get the mechanism type for the subnet (0 for Stable, 1 for Dynamic) - let mechanism_id: u16 = SubnetMechanism::::get(netuid); - // Step 2: Swap alpha and attain tao - let swap_result = if mechanism_id == 1 { - let order = GetTaoForAlpha::::with_amount(alpha); - T::SwapInterface::swap(netuid.into(), order, price_limit.into(), drop_fees, false)? - } else { - // Step 3.b.1: Stable mechanism, just return the value 1:1 - SwapResult { - amount_paid_in: alpha, - amount_paid_out: alpha.to_u64().into(), - fee_paid: AlphaBalance::ZERO, - fee_to_block_author: AlphaBalance::ZERO, - } - }; - - // Increase only the protocol Alpha reserve - let alpha_delta = swap_result.paid_in_reserve_delta_i64().unsigned_abs(); - SubnetAlphaIn::::mutate(netuid, |total| { - *total = total.saturating_add(alpha_delta.into()); - }); - - // Decrease Alpha outstanding. - SubnetAlphaOut::::mutate(netuid, |total| { - *total = total.saturating_sub(alpha_delta.into()); - }); - - // Decrease tao reserves. - let tao_delta = swap_result.paid_out_reserve_delta_i64().unsigned_abs(); - Self::decrease_provided_tao_reserve(netuid.into(), tao_delta.into()); - - // Reduce total TAO reserves. - TotalStake::::mutate(|total| *total = total.saturating_sub(swap_result.amount_paid_out)); - - // Increase total subnet TAO volume. - SubnetVolume::::mutate(netuid, |total| { - *total = total.saturating_add(swap_result.amount_paid_out.to_u64() as u128) - }); - - // Return the tao received. - Ok(swap_result) - } - - /// Unstakes alpha from a subnet for a given hotkey and coldkey pair. - /// - /// We update the pools associated with a subnet as well as update hotkey alpha shares. - /// Credits the unstaked TAO to the beneficiary account - pub fn unstake_from_subnet( - hotkey: &T::AccountId, - coldkey: &T::AccountId, - beneficiary: &T::AccountId, - netuid: NetUid, - alpha: AlphaBalance, - price_limit: TaoBalance, - drop_fees: bool, - ) -> Result { - // Refuse to strip conviction-locked or collateral-bonded alpha even when - // callers (e.g. alpha fee withdrawal) skip the remove-stake validators. - Self::ensure_available_to_unstake(coldkey, netuid, alpha)?; - Self::ensure_hotkey_covers_collateral(coldkey, hotkey, netuid, alpha)?; - - // Decrease alpha on subnet - Self::decrease_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid, alpha); - - // Swap the alpha for TAO. - let swap_result = Self::swap_alpha_for_tao(netuid, alpha, price_limit, drop_fees)?; - - // Refund the unused alpha (in case if limit price is hit) - let refund = alpha.saturating_sub( - swap_result - .amount_paid_in - .saturating_add(swap_result.fee_paid) - .into(), - ); - if !refund.is_zero() { - Self::increase_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid, refund); - } - - // Transfer unstaked TAO from subnet account to the coldkey. - Self::transfer_tao_from_subnet(netuid, beneficiary, swap_result.amount_paid_out.into())?; - - // Swap (in a fee-less way) the block builder alpha fee - let mut fee_outflow = 0_u64; - let maybe_block_author_coldkey = T::AuthorshipProvider::author(); - if let Some(block_author_coldkey) = maybe_block_author_coldkey { - let bb_swap_result = Self::swap_alpha_for_tao( - netuid, - swap_result.fee_to_block_author, - T::SwapInterface::min_price::(), - true, - )?; - Self::transfer_tao_from_subnet( - netuid, - &block_author_coldkey, - bb_swap_result.amount_paid_out.into(), - )?; - fee_outflow = bb_swap_result.amount_paid_out.into(); - } else { - // block author is not found, burn this alpha - Self::burn_subnet_alpha(netuid, swap_result.fee_to_block_author); - } - - // If this is a root-stake - if netuid == NetUid::ROOT { - // Adjust root claimed value for this hotkey and coldkey. - Self::remove_stake_adjust_root_claimed_for_hotkey_and_coldkey(hotkey, coldkey, alpha); - - // If the coldkey no longer holds any root stake, remove it from the - // auto-claim staking-coldkey index so dead entries do not accumulate. - if !Self::coldkey_has_root_stake(coldkey) { - Self::maybe_remove_coldkey_index(coldkey); - } - } - - // Step 3: Update StakingHotkeys if the hotkey's total alpha, across all subnets, is zero - // TODO const: fix. - // if Self::get_stake(hotkey, coldkey) == 0 { - // StakingHotkeys::::mutate(coldkey, |hotkeys| { - // hotkeys.retain(|k| k != hotkey); - // }); - // } - - // Record TAO outflow - Self::record_tao_outflow( - netuid, - swap_result - .amount_paid_out - .saturating_add(fee_outflow.into()), - ); - - // Cleanup locks if needed - Self::cleanup_lock_if_zero(coldkey, netuid); - - LastColdkeyHotkeyStakeBlock::::insert(coldkey, hotkey, Self::get_current_block_as_u64()); - - // Deposit and log the unstaking event. - Self::deposit_event(Event::StakeRemoved( - coldkey.clone(), - hotkey.clone(), - swap_result.amount_paid_out.into(), - swap_result.amount_paid_in.into(), - netuid, - swap_result.fee_paid.to_u64(), - )); - - log::debug!( - "StakeRemoved( coldkey: {:?}, hotkey:{:?}, tao: {:?}, alpha:{:?}, netuid: {:?}, fee {} )", - coldkey.clone(), - hotkey.clone(), - swap_result.amount_paid_out, - swap_result.amount_paid_in, - netuid, - swap_result.fee_paid - ); - - Ok(swap_result.amount_paid_out.into()) - } - - /// Stakes TAO into a subnet for a given hotkey and coldkey pair. - /// - /// We update the pools associated with a subnet as well as update hotkey alpha shares. - pub(crate) fn stake_into_subnet( - hotkey: &T::AccountId, - coldkey: &T::AccountId, - netuid: NetUid, - tao: TaoBalance, - price_limit: TaoBalance, - drop_fees: bool, - ) -> Result { - // Transfer TAO from coldkey to the subnet account. - // Actual transfered may be different within ED amount. - let tao_staked = Self::transfer_tao_to_subnet(netuid, coldkey, tao)?; - - // Swap the tao to alpha. - let swap_result = Self::swap_tao_for_alpha(netuid, tao_staked, price_limit, drop_fees)?; - - ensure!( - !swap_result.amount_paid_out.is_zero(), - Error::::AmountTooLow - ); - - ensure!( - Self::try_increase_stake_for_hotkey_and_coldkey_on_subnet( - hotkey, - netuid, - swap_result.amount_paid_out.into(), - ), - Error::::InsufficientLiquidity - ); - - // Increase the alpha on the hotkey account. - Self::increase_stake_for_hotkey_and_coldkey_on_subnet( - hotkey, - coldkey, - netuid, - swap_result.amount_paid_out.into(), - ); - - // Step 4: Update the list of hotkeys staking for this coldkey - let mut staking_hotkeys = StakingHotkeys::::get(coldkey); - if !staking_hotkeys.contains(hotkey) { - staking_hotkeys.push(hotkey.clone()); - StakingHotkeys::::insert(coldkey, staking_hotkeys.clone()); - } - - // Increase the balance of the block author - let maybe_block_author_coldkey = T::AuthorshipProvider::author(); - if let Some(block_author_coldkey) = maybe_block_author_coldkey { - // TAO was transferred to subnet account in the beginning of this fn - // swap_tao_for_alpha guarantees that input amount of TAO was split into - // reserve delta + fee_to_block_author. - // Now transfer the fee from subnet account to block builder. - Self::transfer_tao_from_subnet( - netuid, - &block_author_coldkey, - swap_result.fee_to_block_author.into(), - )?; - } else { - // Block author is not found - burn this TAO - if let Some(subnet_account_id) = Self::get_subnet_account_id(netuid) { - let _ = Self::burn_tao(&subnet_account_id, swap_result.fee_to_block_author.into()); - } - } - - // Refund the TAO the AMM could not consume (e.g. when the user-supplied - // price limit is hit before the full `tao_staked` is swapped). Without - // this, the unswapped remainder is stranded on the subnet PalletId - // account. Mirrors the alpha refund in `unstake_from_subnet`. - let consumed_tao = swap_result - .amount_paid_in - .saturating_add(swap_result.fee_paid); - let refund_tao = tao_staked.saturating_sub(consumed_tao); - if !refund_tao.is_zero() { - Self::transfer_tao_from_subnet(netuid, coldkey, refund_tao)?; - // `swap_tao_for_alpha` bumped `TotalStake` by the full `tao_staked`; - // only `consumed_tao` actually became stake, so back out the refund. - TotalStake::::mutate(|total| *total = total.saturating_sub(refund_tao)); - } - - // Record TAO inflow - Self::record_tao_inflow(netuid, swap_result.amount_paid_in.into()); - - // Cleanup locks if needed - Self::cleanup_lock_if_zero(coldkey, netuid); - - LastColdkeyHotkeyStakeBlock::::insert(coldkey, hotkey, Self::get_current_block_as_u64()); - - // If this is a root-stake - if netuid == NetUid::ROOT { - // Adjust root claimed for this hotkey and coldkey. - let alpha = swap_result.amount_paid_out.into(); - Self::add_stake_adjust_root_claimed_for_hotkey_and_coldkey(hotkey, coldkey, alpha); - Self::maybe_add_coldkey_index(coldkey); - } - - // Deposit and log the staking event. - Self::deposit_event(Event::StakeAdded( - coldkey.clone(), - hotkey.clone(), - tao_staked, - swap_result.amount_paid_out.into(), - netuid, - swap_result.fee_paid.to_u64(), - )); - - log::debug!( - "StakeAdded( coldkey: {:?}, hotkey:{:?}, tao: {:?}, alpha:{:?}, netuid: {:?}, fee {} )", - coldkey.clone(), - hotkey.clone(), - tao_staked, - swap_result.amount_paid_out, - netuid, - swap_result.fee_paid, - ); - - Ok(swap_result.amount_paid_out.into()) - } - - /// Transfers stake between coldkeys and/or hotkey within one subnet without running it - /// through swap. - /// - /// Does not incur any swapping nor fees - pub fn transfer_stake_within_subnet( - origin_coldkey: &T::AccountId, - origin_hotkey: &T::AccountId, - destination_coldkey: &T::AccountId, - destination_hotkey: &T::AccountId, - netuid: NetUid, - alpha: AlphaBalance, - ) -> Result { - // Transfer lock (may fail if destination coldkey has a conflicting lock). - // The lock must follow the stake to the destination hotkey, otherwise a - // hotkey-changing transfer would leave the recipient's lock and conviction - // stranded on the origin hotkey. - Self::transfer_lock( - origin_coldkey, - destination_coldkey, - destination_hotkey, - netuid, - alpha, - )?; - - // Decrease alpha on origin keys - Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( - origin_hotkey, - origin_coldkey, - netuid, - alpha, - ); - if netuid == NetUid::ROOT { - Self::remove_stake_adjust_root_claimed_for_hotkey_and_coldkey( - origin_hotkey, - origin_coldkey, - alpha, - ); - } - - // If the destination coldkey does not own the destination hotkey, make the - // hotkey a delegate, matching the cross-subnet transfer path. - if Self::get_owning_coldkey_for_hotkey(destination_hotkey) != *destination_coldkey { - Self::maybe_become_delegate(destination_hotkey); - } - - // Increase alpha on destination keys - Self::increase_stake_for_hotkey_and_coldkey_on_subnet( - destination_hotkey, - destination_coldkey, - netuid, - alpha, - ); - if netuid == NetUid::ROOT { - Self::add_stake_adjust_root_claimed_for_hotkey_and_coldkey( - destination_hotkey, - destination_coldkey, - u64::from(alpha).into(), - ); - } - - // Calculate TAO equivalent based on current price (it is accurate because - // there's no slippage in this move) - let current_price = - ::SwapInterface::current_alpha_price(netuid.into()); - let tao_equivalent: TaoBalance = current_price - .saturating_mul(U64F64::saturating_from_num(alpha)) - .saturating_to_num::() - .into(); - - // Ensure tao_equivalent is above the minimum transfer amount - ensure!( - tao_equivalent >= DefaultMinTransfer::::get(), - Error::::AmountTooLow - ); - - // Step 3: Update StakingHotkeys if the hotkey's total alpha, across all subnets, is zero - // TODO: fix. - // if Self::get_stake(hotkey, coldkey) == 0 { - // StakingHotkeys::::mutate(coldkey, |hotkeys| { - // hotkeys.retain(|k| k != hotkey); - // }); - // } - - LastColdkeyHotkeyStakeBlock::::insert( - destination_coldkey, - destination_hotkey, - Self::get_current_block_as_u64(), - ); - - // Deposit and log the unstaking event. - Self::deposit_event(Event::StakeRemoved( - origin_coldkey.clone(), - origin_hotkey.clone(), - tao_equivalent, - alpha, - netuid, - 0_u64, // 0 fee - )); - Self::deposit_event(Event::StakeAdded( - destination_coldkey.clone(), - destination_hotkey.clone(), - tao_equivalent, - alpha, - netuid, - 0_u64, // 0 fee - )); - - Ok(tao_equivalent) - } - - pub fn get_alpha_share_pool( - hotkey: ::AccountId, - netuid: NetUid, - ) -> SharePool, HotkeyAlphaSharePoolDataOperations> { - let ops = HotkeyAlphaSharePoolDataOperations::new(hotkey, netuid); - SharePool::, HotkeyAlphaSharePoolDataOperations>::new(ops) - } - - /// Validate add_stake user input - pub fn validate_add_stake( - coldkey: &T::AccountId, - hotkey: &T::AccountId, - netuid: NetUid, - mut stake_to_be_added: TaoBalance, - max_amount: TaoBalance, - allow_partial: bool, - ) -> Result<(), Error> { - // Ensure that the subnet exists. - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); - - // Ensure that the subnet is enabled. - Self::ensure_subtoken_enabled(netuid)?; - - // Get the minimum balance (and amount) that satisfies the transaction - let min_stake = DefaultMinStake::::get(); - let min_amount = { - let order = GetAlphaForTao::::with_amount(min_stake); - let fee = T::SwapInterface::sim_swap(netuid.into(), order) - .map(|res| res.fee_paid) - .unwrap_or(T::SwapInterface::approx_fee_amount( - netuid.into(), - min_stake.into(), - )); - min_stake.saturating_add(fee.into()) - }; - - // Ensure that the stake_to_be_added is at least the min_amount - ensure!(stake_to_be_added >= min_amount, Error::::AmountTooLow); - - // Ensure that if partial execution is not allowed, the amount will not cause - // slippage over desired - if !allow_partial { - ensure!(stake_to_be_added <= max_amount, Error::::SlippageTooHigh); - } else { - stake_to_be_added = max_amount.min(stake_to_be_added); - } - - // Ensure the callers coldkey has enough stake to perform the transaction. - ensure!( - Self::can_remove_balance_from_coldkey_account(coldkey, stake_to_be_added.into()), - Error::::NotEnoughBalanceToStake - ); - - // Ensure that the hotkey account exists this is only possible through registration. - ensure!( - Self::hotkey_account_exists(hotkey), - Error::::HotKeyAccountNotExists - ); - - let order = GetAlphaForTao::::with_amount(stake_to_be_added); - let swap_result = T::SwapInterface::sim_swap(netuid.into(), order) - .map_err(|_| Error::::InsufficientLiquidity)?; - - // Check that actual withdrawn TAO amount is not lower than the minimum stake - ensure!( - swap_result.amount_paid_in >= min_stake, - Error::::AmountTooLow - ); - - ensure!( - !swap_result.amount_paid_out.is_zero(), - Error::::InsufficientLiquidity - ); - - // Ensure hotkey pool is precise enough - let try_stake_result = Self::try_increase_stake_for_hotkey_and_coldkey_on_subnet( - hotkey, - netuid, - swap_result.amount_paid_out.into(), - ); - ensure!(try_stake_result, Error::::InsufficientLiquidity); - - Ok(()) - } - - /// Validate remove_stake user input - /// - pub fn validate_remove_stake( - coldkey: &T::AccountId, - hotkey: &T::AccountId, - netuid: NetUid, - alpha_unstaked: AlphaBalance, - max_amount: AlphaBalance, - allow_partial: bool, - ) -> Result<(), Error> { - // Ensure that the subnet exists. - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); - - // Ensure that the subnet is enabled. - // Self::ensure_subtoken_enabled(netuid)?; - - // Do not allow zero unstake amount - ensure!(!alpha_unstaked.is_zero(), Error::::AmountTooLow); - - // Ensure that the stake amount to be removed is above the minimum in tao equivalent. - // Bypass this check if the user unstakes full amount - let remaining_alpha_stake = - Self::calculate_reduced_stake_on_subnet(hotkey, coldkey, netuid, alpha_unstaked)?; - let order = GetTaoForAlpha::::with_amount(alpha_unstaked); - match T::SwapInterface::sim_swap(netuid.into(), order) { - Ok(res) => { - if !remaining_alpha_stake.is_zero() { - ensure!( - res.amount_paid_out >= DefaultMinStake::::get(), - Error::::AmountTooLow - ); - } - } - Err(_) => return Err(Error::::InsufficientLiquidity), - } - - // Ensure that if partial execution is not allowed, the amount will not cause - // slippage over desired - if !allow_partial { - ensure!(alpha_unstaked <= max_amount, Error::::SlippageTooHigh); - } - - // Ensure that the hotkey account exists this is only possible through registration. - ensure!( - Self::hotkey_account_exists(hotkey), - Error::::HotKeyAccountNotExists - ); - - // Ensure that unstaked amount is not greater than available to unstake (due to locks) - Self::ensure_available_to_unstake(coldkey, netuid, alpha_unstaked)?; - // Collateral is per-hotkey: free stake on a sibling hotkey must not cover - // stripping the bonded position. - Self::ensure_hotkey_covers_collateral(coldkey, hotkey, netuid, alpha_unstaked)?; - - Ok(()) - } - - /// Validate if unstake_all can be executed - /// - pub fn validate_unstake_all( - coldkey: &T::AccountId, - hotkey: &T::AccountId, - only_alpha: bool, - ) -> Result<(), Error> { - // Get all netuids (filter out root) - let subnets = Self::get_all_subnet_netuids(); - - // Ensure that the hotkey account exists this is only possible through registration. - ensure!( - Self::hotkey_account_exists(hotkey), - Error::::HotKeyAccountNotExists - ); - - let mut unstaking_any = false; - for netuid in subnets.iter() { - if only_alpha && netuid.is_root() { - continue; - } - - // Get user's stake in this subnet - let alpha = Self::get_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, *netuid); - - // Ensure that unstaked amount is not greater than available to unstake (due to locks) - // for this subnet. - Self::ensure_available_to_unstake(coldkey, *netuid, alpha)?; - - if Self::validate_remove_stake(coldkey, hotkey, *netuid, alpha, alpha, false).is_ok() { - unstaking_any = true; - } - } - - // If no unstaking happens, return error - ensure!(unstaking_any, Error::::AmountTooLow); - - Ok(()) - } - - /// Validate stake transition user input - /// That works for move_stake, transfer_stake, and swap_stake - /// - pub fn validate_stake_transition( - origin_coldkey: &T::AccountId, - destination_coldkey: &T::AccountId, - origin_hotkey: &T::AccountId, - destination_hotkey: &T::AccountId, - origin_netuid: NetUid, - destination_netuid: NetUid, - alpha_amount: AlphaBalance, - max_amount: AlphaBalance, - maybe_allow_partial: Option, - check_transfer_toggle: bool, - ) -> Result<(), Error> { - // Ensure stake transition is actually happening - if origin_coldkey == destination_coldkey && origin_hotkey == destination_hotkey { - ensure!(origin_netuid != destination_netuid, Error::::SameNetuid); - } - - // Ensure that both subnets exist. - ensure!( - Self::if_subnet_exist(origin_netuid), - Error::::SubnetNotExists - ); - if origin_netuid != destination_netuid { - ensure!( - Self::if_subnet_exist(destination_netuid), - Error::::SubnetNotExists - ); - } - - ensure!( - SubtokenEnabled::::get(origin_netuid), - Error::::SubtokenDisabled - ); - - ensure!( - SubtokenEnabled::::get(destination_netuid), - Error::::SubtokenDisabled - ); - - // Ensure that the origin hotkey account exists - ensure!( - Self::hotkey_account_exists(origin_hotkey), - Error::::HotKeyAccountNotExists - ); - - // Ensure that the destination hotkey account exists - ensure!( - Self::hotkey_account_exists(destination_hotkey), - Error::::HotKeyAccountNotExists - ); - - // Ensure there is enough stake in the origin subnet. - let origin_alpha = Self::get_stake_for_hotkey_and_coldkey_on_subnet( - origin_hotkey, - origin_coldkey, - origin_netuid, - ); - ensure!( - alpha_amount <= origin_alpha, - Error::::NotEnoughStakeToWithdraw - ); - - // If origin and destination netuid are different, do the swap-related checks - if origin_netuid != destination_netuid { - // Ensure that the stake amount to be removed is above the minimum in tao equivalent. - // Transfers (check_transfer_toggle == true) have their own minimum, detached from - // the staking minimum used by moves and swaps. - let min_amount = if check_transfer_toggle { - DefaultMinTransfer::::get() - } else { - DefaultMinStake::::get() - }; - let order = GetTaoForAlpha::::with_amount(alpha_amount); - let tao_equivalent = T::SwapInterface::sim_swap(origin_netuid.into(), order) - .map(|res| res.amount_paid_out) - .map_err(|_| Error::::InsufficientLiquidity)?; - ensure!(tao_equivalent > min_amount, Error::::AmountTooLow); - - // Ensure that if partial execution is not allowed, the amount will not cause - // slippage over desired - if let Some(allow_partial) = maybe_allow_partial - && !allow_partial - { - ensure!(alpha_amount <= max_amount, Error::::SlippageTooHigh); - } - } - - if check_transfer_toggle { - // Ensure transfer is toggled. - ensure!( - TransferToggle::::get(origin_netuid), - Error::::TransferDisallowed - ); - if origin_netuid != destination_netuid { - ensure!( - TransferToggle::::get(destination_netuid), - Error::::TransferDisallowed - ); - } - } - - // Enforce lock invariant: if the is cross-subnet move, the remaining amount must - // cover the lock. - if origin_netuid != destination_netuid { - Self::ensure_available_to_unstake(origin_coldkey, origin_netuid, alpha_amount)?; - } else if origin_coldkey != destination_coldkey { - // Same-subnet, ownership-changing transfer. Conviction locks follow the - // stake to the destination coldkey via `transfer_lock`, but miner - // registration collateral has no transfer exit and does not follow — its - // `MinerCollateral(netuid, hotkey, coldkey)` stays on the origin. Without - // this check, a coldkey could liberate locked collateral by transferring the - // staked alpha to a second coldkey. Require the origin coldkey to retain - // enough alpha on the subnet to still cover its collateral. - Self::ensure_transfer_respects_collateral(origin_coldkey, origin_netuid, alpha_amount)?; - } - // Always keep bonded alpha on the origin hotkey itself (same-subnet moves - // to a sibling hotkey would otherwise leave a ghost metagraph bond). - Self::ensure_hotkey_covers_collateral( - origin_coldkey, - origin_hotkey, - origin_netuid, - alpha_amount, - )?; - - Ok(()) - } - - pub fn increase_provided_tao_reserve(netuid: NetUid, tao: TaoBalance) { - if !tao.is_zero() { - SubnetTAO::::mutate(netuid, |total| { - *total = total.saturating_add(tao); - }); - } - } - - pub fn decrease_provided_tao_reserve(netuid: NetUid, tao: TaoBalance) { - if !tao.is_zero() { - SubnetTAO::::mutate(netuid, |total| { - *total = total.saturating_sub(tao); - }); - } - } - - pub fn increase_provided_alpha_reserve(netuid: NetUid, alpha: AlphaBalance) { - if !alpha.is_zero() { - SubnetAlphaIn::::mutate(netuid, |total| { - *total = total.saturating_add(alpha); - }); - } - } - - pub fn decrease_provided_alpha_reserve(netuid: NetUid, alpha: AlphaBalance) { - if !alpha.is_zero() { - SubnetAlphaIn::::mutate(netuid, |total| { - *total = total.saturating_sub(alpha); - }); - } - } -} - -/////////////////////////////////////////// -// Alpha share pool chain data layer - -#[derive(Debug)] -pub struct HotkeyAlphaSharePoolDataOperations { - netuid: NetUid, - hotkey: ::AccountId, - _marker: sp_std::marker::PhantomData, -} - -impl HotkeyAlphaSharePoolDataOperations { - fn new(hotkey: ::AccountId, netuid: NetUid) -> Self { - HotkeyAlphaSharePoolDataOperations { - netuid, - hotkey, - _marker: sp_std::marker::PhantomData, - } - } -} - -// Alpha share key is coldkey because the HotkeyAlphaSharePoolDataOperations struct already has hotkey and netuid -type AlphaShareKey = ::AccountId; - -impl SharePoolDataOperations> - for HotkeyAlphaSharePoolDataOperations -{ - fn get_shared_value(&self) -> u64 { - u64::from(TotalHotkeyAlpha::::get(&self.hotkey, self.netuid)) - } - - fn get_share(&self, key: &AlphaShareKey) -> SafeFloat { - // Read the deprecated Alpha map first and, if value is not available, try new AlphaV2 - let maybe_share_v1 = Alpha::::try_get((&(self.hotkey), key, self.netuid)); - if let Ok(share_v1) = maybe_share_v1 { - return SafeFloat::from(share_v1); - } - - AlphaV2::::get((&(self.hotkey), key, self.netuid)) - } - - fn try_get_share(&self, key: &AlphaShareKey) -> Result { - // Read the deprecated Alpha map first and, if value is not available, try new AlphaV2 - let maybe_share_v1 = Alpha::::try_get((&(self.hotkey), key, self.netuid)); - if let Ok(share_v1) = maybe_share_v1 { - return Ok(SafeFloat::from(share_v1)); - } - - let maybe_share = AlphaV2::::try_get((&(self.hotkey), key, self.netuid)); - if let Ok(share) = maybe_share { - Ok(share) - } else { - Err(()) - } - } - - fn get_denominator(&self) -> SafeFloat { - // Read the deprecated TotalHotkeyShares map first and, if value is not available, try new TotalHotkeySharesV2 - let maybe_denomnator_v1 = TotalHotkeyShares::::try_get(&(self.hotkey), self.netuid); - if let Ok(denomnator_v1) = maybe_denomnator_v1 { - return SafeFloat::from(denomnator_v1); - } - - TotalHotkeySharesV2::::get(&(self.hotkey), self.netuid) - } - - fn set_shared_value(&mut self, value: u64) { - if value != 0 { - TotalHotkeyAlpha::::insert(&(self.hotkey), self.netuid, AlphaBalance::from(value)); - } else { - TotalHotkeyAlpha::::remove(&(self.hotkey), self.netuid); - } - } - - fn set_share(&mut self, key: &AlphaShareKey, share: SafeFloat) { - // Lazy Alpha -> AlphaV2 migration happens right here - // Delete the Alpha entry, insert into AlphaV2 - let maybe_share_v1 = Alpha::::try_get((&(self.hotkey), key, self.netuid)); - if maybe_share_v1.is_ok() { - Alpha::::remove((&self.hotkey, key, self.netuid)); - } - - if !share.is_zero() { - AlphaV2::::insert((&self.hotkey, key, self.netuid), share); - } else { - AlphaV2::::remove((&self.hotkey, key, self.netuid)); - } - } - - fn set_denominator(&mut self, update: SafeFloat) { - // Lazy TotalHotkeyShares -> TotalHotkeySharesV2 migration happens right here - // Delete the TotalHotkeyShares entry, insert into TotalHotkeySharesV2 - let maybe_denominator_v1 = TotalHotkeyShares::::try_get(&(self.hotkey), self.netuid); - if maybe_denominator_v1.is_ok() { - TotalHotkeyShares::::remove(&self.hotkey, self.netuid); - } - - if !update.is_zero() { - TotalHotkeySharesV2::::insert(&self.hotkey, self.netuid, update); - } else { - TotalHotkeySharesV2::::remove(&self.hotkey, self.netuid); - } - } -} diff --git a/pallets/subtensor/src/staking/stake_utils/alpha_price.rs b/pallets/subtensor/src/staking/stake_utils/alpha_price.rs new file mode 100644 index 0000000000..15f298a3f0 --- /dev/null +++ b/pallets/subtensor/src/staking/stake_utils/alpha_price.rs @@ -0,0 +1,206 @@ +//! Alpha issuance, moving price EMA, median price, and TAO-weight / childkey-burn knobs. +use super::*; +use safe_math::*; +use sp_std::collections::btree_map::BTreeMap; +use substrate_fixed::types::{I96F32, U64F64, U96F32}; +use subtensor_runtime_common::{AlphaBalance, NetUid, Token}; +use subtensor_swap_interface::SwapHandler; + +impl Pallet { + /// Retrieves the total alpha issuance for a given subnet. + /// + /// This function calculates the total alpha issuance by summing the alpha + /// values from `SubnetAlphaIn` and `SubnetAlphaOut` for the specified subnet. + /// + /// # Arguments + /// * `netuid`: The unique identifier of the subnet. + /// + /// # Returns + /// * `u64`: The total alpha issuance for the specified subnet. + pub fn get_alpha_issuance(netuid: NetUid) -> AlphaBalance { + SubnetAlphaIn::::get(netuid) + .saturating_add(SubnetAlphaOut::::get(netuid)) + .saturating_add(T::SwapInterface::protocol_alpha_reservoir(netuid)) + } + + pub fn get_moving_alpha_price(netuid: NetUid) -> U64F64 { + let one = U64F64::saturating_from_num(1.0); + if netuid.is_root() { + // Root. + one + } else if SubnetMechanism::::get(netuid) == 0 { + // Stable + one + } else { + U64F64::saturating_from_num(SubnetMovingPrice::::get(netuid)) + } + } + + pub fn update_moving_price(netuid: NetUid) { + let blocks_since_start_call = U64F64::saturating_from_num({ + // We expect FirstEmissionBlockNumber to be set earlier, and we take the block when + // `start_call` was called (first block before FirstEmissionBlockNumber). + let start_call_block = FirstEmissionBlockNumber::::get(netuid) + .unwrap_or_default() + .saturating_sub(1); + + Self::get_current_block_as_u64().saturating_sub(start_call_block) + }); + + // Use halving time hyperparameter. The meaning of this parameter can be best explained under + // the assumption of a constant price and SubnetMovingAlpha == 0.5: It is how many blocks it + // will take in order for the distance between current EMA of price and current price to shorten + // by half. + let halving_time = EMAPriceHalvingBlocks::::get(netuid); + let current_ma_unsigned = U64F64::saturating_from_num(SubnetMovingAlpha::::get()); + let alpha: U64F64 = current_ma_unsigned.saturating_mul(blocks_since_start_call.safe_div( + blocks_since_start_call.saturating_add(U64F64::saturating_from_num(halving_time)), + )); + // Because alpha = b / (b + h), where b and h > 0, alpha < 1, so 1 - alpha > 0. + // We can use unsigned type here: U96F32 + let one_minus_alpha: U64F64 = U64F64::saturating_from_num(1.0).saturating_sub(alpha); + let current_price: U64F64 = alpha.saturating_mul(U64F64::saturating_from_num( + T::SwapInterface::current_alpha_price(netuid.into()) + .min(U64F64::saturating_from_num(1.0)), + )); + let current_moving: U64F64 = one_minus_alpha.saturating_mul(U64F64::saturating_from_num( + Self::get_moving_alpha_price(netuid), + )); + // Convert batch to signed I96F32 to avoid migration of SubnetMovingPrice for now`` + let new_moving: I96F32 = + I96F32::saturating_from_num(current_price.saturating_add(current_moving)); + SubnetMovingPrice::::insert(netuid, new_moving); + } + + /// Gets the Median Subnet Alpha Price + pub fn get_median_subnet_alpha_price() -> U64F64 { + let default_price = U64F64::saturating_from_num(1_u64); + let zero_price = U64F64::saturating_from_num(0_u64); + let two = U64F64::saturating_from_num(2_u64); + + let mut price_counts: BTreeMap = BTreeMap::new(); + let mut total_prices: usize = 0; + + for (netuid, added) in NetworksAdded::::iter() { + if !added || netuid == NetUid::ROOT { + continue; + } + + let price = T::SwapInterface::current_alpha_price(netuid); + if price <= zero_price { + continue; + } + + total_prices = total_prices.saturating_add(1); + + if let Some(count) = price_counts.get_mut(&price) { + *count = count.saturating_add(1); + } else { + price_counts.insert(price, 1usize); + } + } + + if total_prices == 0 { + return default_price; + } + + let Some(last_index) = total_prices.checked_sub(1) else { + return default_price; + }; + let Some(lower_target) = last_index.checked_div(2) else { + return default_price; + }; + let Some(upper_target) = total_prices.checked_div(2) else { + return default_price; + }; + + let mut cumulative: usize = 0; + let mut lower_price: Option = None; + let mut upper_price: Option = None; + + for (price, count) in price_counts.into_iter() { + let next_cumulative = cumulative.saturating_add(count); + + if lower_price.is_none() && lower_target < next_cumulative { + lower_price = Some(price); + } + + if upper_price.is_none() && upper_target < next_cumulative { + upper_price = Some(price); + } + + if lower_price.is_some() && upper_price.is_some() { + break; + } + + cumulative = next_cumulative; + } + + match (lower_price, upper_price) { + (Some(_), Some(upper)) if lower_target == upper_target => upper, + (Some(lower), Some(upper)) => lower.saturating_add(upper).safe_div(two), + _ => default_price, + } + } + + /// Retrieves the TAO weight as a normalized value between 0 and 1. + /// + /// This function performs the following steps: + /// 1. Fetches the TAO weight from storage using the TaoWeight storage item. + /// 2. Converts the retrieved u64 value to a fixed-point number (U96F32). + /// 3. Normalizes the weight by dividing it by the maximum possible u64 value. + /// 4. Returns the normalized weight as an U96F32 fixed-point number. + /// + /// The normalization ensures that the returned value is always between 0 and 1, + /// regardless of the actual stored weight value. + /// + /// # Returns + /// * `U96F32`: The normalized TAO weight as a fixed-point number between 0 and 1. + /// + /// # Note + /// This function uses saturating division to prevent potential overflow errors. + pub fn get_tao_weight() -> U96F32 { + // Step 1: Fetch the TAO weight from storage + let stored_weight = TaoWeight::::get(); + + // Step 2: Convert the u64 weight to U96F32 + let weight_fixed = U96F32::saturating_from_num(stored_weight); + + // Step 3: Normalize the weight by dividing by u64::MAX + // This ensures the result is always between 0 and 1 + weight_fixed.safe_div(U96F32::saturating_from_num(u64::MAX)) + } + + pub fn get_ck_burn() -> U96F32 { + let stored_weight = CKBurn::::get(); + let weight_fixed = U96F32::saturating_from_num(stored_weight); + weight_fixed.safe_div(U96F32::saturating_from_num(u64::MAX)) + } + + /// Sets the TAO weight in storage. + /// + /// This function performs the following steps: + /// 1. Takes the provided weight value as a u64. + /// 2. Updates the TaoWeight storage item with the new value. + /// + /// # Arguments + /// * `weight`: The new TAO weight value to be set, as a u64. + /// + /// # Effects + /// This function modifies the following storage item: + /// * `TaoWeight`: Updates it with the new weight value. + /// + /// # Note + /// The weight is stored as a raw u64 value. To get the normalized weight between 0 and 1, + /// use the `get_tao_weight()` function. + pub fn set_tao_weight(weight: u64) { + // Update the TaoWeight storage with the new weight value + TaoWeight::::set(weight); + } + + // Set the amount burned on non owned CK + pub fn set_ck_burn(weight: u64) { + // Update the ck burn value. + CKBurn::::set(weight); + } +} diff --git a/pallets/subtensor/src/staking/stake_utils/alpha_share_pool.rs b/pallets/subtensor/src/staking/stake_utils/alpha_share_pool.rs new file mode 100644 index 0000000000..9fda7ebb9f --- /dev/null +++ b/pallets/subtensor/src/staking/stake_utils/alpha_share_pool.rs @@ -0,0 +1,121 @@ +//! Hotkey alpha share-pool storage adapter ([`HotkeyAlphaSharePoolDataOperations`]). +//! +//! Backs `SharePool` so each coldkey's alpha share on a `(hotkey, netuid)` is +//! stored in the `Alpha` map with a shared denominator. +use super::*; +use share_pool::{SafeFloat, SharePool, SharePoolDataOperations}; +use subtensor_runtime_common::NetUid; + +/////////////////////////////////////////// +// Alpha share pool chain data layer + +#[derive(Debug)] +pub struct HotkeyAlphaSharePoolDataOperations { + netuid: NetUid, + hotkey: ::AccountId, + _marker: sp_std::marker::PhantomData, +} + +impl HotkeyAlphaSharePoolDataOperations { + pub(crate) fn new(hotkey: ::AccountId, netuid: NetUid) -> Self { + HotkeyAlphaSharePoolDataOperations { + netuid, + hotkey, + _marker: sp_std::marker::PhantomData, + } + } +} + +// Alpha share key is coldkey because the HotkeyAlphaSharePoolDataOperations struct already has hotkey and netuid +pub(crate) type AlphaShareKey = ::AccountId; + +impl SharePoolDataOperations> + for HotkeyAlphaSharePoolDataOperations +{ + fn get_shared_value(&self) -> u64 { + u64::from(TotalHotkeyAlpha::::get(&self.hotkey, self.netuid)) + } + + fn get_share(&self, key: &AlphaShareKey) -> SafeFloat { + // Read the deprecated Alpha map first and, if value is not available, try new AlphaV2 + let maybe_share_v1 = Alpha::::try_get((&(self.hotkey), key, self.netuid)); + if let Ok(share_v1) = maybe_share_v1 { + return SafeFloat::from(share_v1); + } + + AlphaV2::::get((&(self.hotkey), key, self.netuid)) + } + + fn try_get_share(&self, key: &AlphaShareKey) -> Result { + // Read the deprecated Alpha map first and, if value is not available, try new AlphaV2 + let maybe_share_v1 = Alpha::::try_get((&(self.hotkey), key, self.netuid)); + if let Ok(share_v1) = maybe_share_v1 { + return Ok(SafeFloat::from(share_v1)); + } + + let maybe_share = AlphaV2::::try_get((&(self.hotkey), key, self.netuid)); + if let Ok(share) = maybe_share { + Ok(share) + } else { + Err(()) + } + } + + fn get_denominator(&self) -> SafeFloat { + // Read the deprecated TotalHotkeyShares map first and, if value is not available, try new TotalHotkeySharesV2 + let maybe_denomnator_v1 = TotalHotkeyShares::::try_get(&(self.hotkey), self.netuid); + if let Ok(denomnator_v1) = maybe_denomnator_v1 { + return SafeFloat::from(denomnator_v1); + } + + TotalHotkeySharesV2::::get(&(self.hotkey), self.netuid) + } + + fn set_shared_value(&mut self, value: u64) { + if value != 0 { + TotalHotkeyAlpha::::insert(&(self.hotkey), self.netuid, AlphaBalance::from(value)); + } else { + TotalHotkeyAlpha::::remove(&(self.hotkey), self.netuid); + } + } + + fn set_share(&mut self, key: &AlphaShareKey, share: SafeFloat) { + // Lazy Alpha -> AlphaV2 migration happens right here + // Delete the Alpha entry, insert into AlphaV2 + let maybe_share_v1 = Alpha::::try_get((&(self.hotkey), key, self.netuid)); + if maybe_share_v1.is_ok() { + Alpha::::remove((&self.hotkey, key, self.netuid)); + } + + if !share.is_zero() { + AlphaV2::::insert((&self.hotkey, key, self.netuid), share); + } else { + AlphaV2::::remove((&self.hotkey, key, self.netuid)); + } + } + + fn set_denominator(&mut self, update: SafeFloat) { + // Lazy TotalHotkeyShares -> TotalHotkeySharesV2 migration happens right here + // Delete the TotalHotkeyShares entry, insert into TotalHotkeySharesV2 + let maybe_denominator_v1 = TotalHotkeyShares::::try_get(&(self.hotkey), self.netuid); + if maybe_denominator_v1.is_ok() { + TotalHotkeyShares::::remove(&self.hotkey, self.netuid); + } + + if !update.is_zero() { + TotalHotkeySharesV2::::insert(&self.hotkey, self.netuid, update); + } else { + TotalHotkeySharesV2::::remove(&self.hotkey, self.netuid); + } + } +} + +impl Pallet { + pub fn get_alpha_share_pool( + hotkey: ::AccountId, + netuid: NetUid, + ) -> SharePool, HotkeyAlphaSharePoolDataOperations> { + let ops = HotkeyAlphaSharePoolDataOperations::new(hotkey, netuid); + SharePool::, HotkeyAlphaSharePoolDataOperations>::new(ops) + } +} diff --git a/pallets/subtensor/src/staking/stake_utils/inherited_stake.rs b/pallets/subtensor/src/staking/stake_utils/inherited_stake.rs new file mode 100644 index 0000000000..5ac11ae0b4 --- /dev/null +++ b/pallets/subtensor/src/staking/stake_utils/inherited_stake.rs @@ -0,0 +1,258 @@ +//! Stake weight vectors and parent/child inherited alpha for a hotkey on a subnet. +use super::*; +use safe_math::*; +use substrate_fixed::types::{I64F64, U96F32}; +use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance}; + +impl Pallet { + /// Calculates the weighted combination of alpha and TAO stake for a single hotkey on a subnet. + /// + pub fn get_stake_weights_for_hotkey_on_subnet( + hotkey: &T::AccountId, + netuid: NetUid, + ) -> (I64F64, I64F64, I64F64) { + // Retrieve the TAO weight. + let tao_weight = I64F64::saturating_from_num(Self::get_tao_weight()); + log::debug!("tao_weight: {tao_weight:?}"); + + // Step 1: Get stake of hotkey (neuron) + let alpha_stake = + I64F64::saturating_from_num(Self::get_inherited_for_hotkey_on_subnet(hotkey, netuid)); + log::debug!("alpha_stake: {alpha_stake:?}"); + + // Step 2: Get the TAO stake for the hotkey + let tao_stake = I64F64::saturating_from_num(Self::get_tao_inherited_for_hotkey_on_subnet( + hotkey, netuid, + )); + log::debug!("tao_stake: {tao_stake:?}"); + + // Step 3: Combine alpha and tao stakes + let total_stake = alpha_stake.saturating_add(tao_stake.saturating_mul(tao_weight)); + log::debug!("total_stake: {total_stake:?}"); + + (total_stake, alpha_stake, tao_stake) + } + + /// Calculates the weighted combination of alpha and TAO stake for hotkeys on a subnet. + /// + pub fn get_stake_weights_for_network( + netuid: NetUid, + ) -> (Vec, Vec, Vec) { + // Retrieve the TAO weight. + let tao_weight: I64F64 = I64F64::saturating_from_num(Self::get_tao_weight()); + log::debug!("tao_weight: {tao_weight:?}"); + + // Step 1: Get subnetwork size + let n: u16 = Self::get_subnetwork_n(netuid); + + // Step 2: Get stake of all hotkeys (neurons) ordered by uid + let alpha_stake: Vec = (0..n) + .map(|uid| { + if Keys::::contains_key(netuid, uid) { + let hotkey: T::AccountId = Keys::::get(netuid, uid); + I64F64::saturating_from_num(Self::get_inherited_for_hotkey_on_subnet( + &hotkey, netuid, + )) + } else { + I64F64::saturating_from_num(0) + } + }) + .collect(); + log::debug!("alpha_stake: {alpha_stake:?}"); + + // Step 3: Calculate the TAO stake vector. + // Initialize a vector to store TAO stakes for each neuron. + let tao_stake: Vec = (0..n) + .map(|uid| { + if Keys::::contains_key(netuid, uid) { + let hotkey: T::AccountId = Keys::::get(netuid, uid); + I64F64::saturating_from_num(Self::get_tao_inherited_for_hotkey_on_subnet( + &hotkey, netuid, + )) + } else { + I64F64::saturating_from_num(0) + } + }) + .collect(); + log::trace!("tao_stake: {tao_stake:?}"); + + // Step 4: Combine alpha and TAO stakes. + // Calculate the weighted average of alpha and TAO stakes for each neuron. + let total_stake: Vec = alpha_stake + .iter() + .zip(tao_stake.iter()) + .map(|(alpha_i, tao_i)| alpha_i.saturating_add(tao_i.saturating_mul(tao_weight))) + .collect(); + log::trace!("total_stake: {total_stake:?}"); + + (total_stake, alpha_stake, tao_stake) + } + + /// Calculates the total inherited stake (alpha) held by a hotkey on a network, considering child/parent relationships. + /// + /// This function performs the following steps: + /// 1. Retrieves the initial alpha (stake) for the hotkey on the specified subnet. + /// 2. Retrieves the list of children and parents for the hotkey on the subnet. + /// 3. Calculates the alpha allocated to children: + /// a. For each child, computes the proportion of alpha to be allocated. + /// b. Accumulates the total alpha allocated to all children. + /// 4. Calculates the alpha received from parents: + /// a. For each parent, retrieves the parent's stake on the subnet. + /// b. Computes the proportion of the parent's stake to be inherited. + /// c. Accumulates the total alpha inherited from all parents. + /// 5. Computes the final inherited alpha by adjusting the initial alpha: + /// a. Subtracts the alpha allocated to children. + /// b. Adds the alpha inherited from parents. + /// 6. Returns the final inherited alpha value. + /// + /// # Arguments + /// * `hotkey`: AccountId of the hotkey whose total inherited stake is to be calculated. + /// * `netuid`: Network unique identifier specifying the subnet context. + /// + /// # Returns + /// * `u64`: The total inherited alpha for the hotkey on the subnet after considering the + /// stakes allocated to children and inherited from parents. + /// + /// # Note + /// This function uses saturating arithmetic to prevent overflows. + pub fn get_tao_inherited_for_hotkey_on_subnet( + hotkey: &T::AccountId, + netuid: NetUid, + ) -> TaoBalance { + let initial_tao: U96F32 = + U96F32::saturating_from_num(Self::get_stake_for_hotkey_on_subnet(hotkey, NetUid::ROOT)); + + // Initialize variables to track alpha allocated to children and inherited from parents. + let mut tao_to_children: U96F32 = U96F32::saturating_from_num(0); + let mut tao_from_parents: U96F32 = U96F32::saturating_from_num(0); + + // Step 2: Retrieve the lists of parents and children for the hotkey on the subnet. + let parents: Vec<(u64, T::AccountId)> = Self::get_parents(hotkey, netuid); + let children: Vec<(u64, T::AccountId)> = Self::get_children(hotkey, netuid); + log::trace!("Parents for hotkey {hotkey:?} on subnet {netuid}: {parents:?}"); + log::trace!("Children for hotkey {hotkey:?} on subnet {netuid}: {children:?}"); + + // Step 3: Calculate the total tao allocated to children. + for (proportion, _) in children { + // Convert the proportion to a normalized value between 0 and 1. + let normalized_proportion: U96F32 = U96F32::saturating_from_num(proportion) + .safe_div(U96F32::saturating_from_num(u64::MAX)); + log::trace!("Normalized proportion for child: {normalized_proportion:?}"); + + // Calculate the amount of tao to be allocated to this child. + let tao_proportion_to_child: U96F32 = + U96F32::saturating_from_num(initial_tao).saturating_mul(normalized_proportion); + log::trace!("Tao proportion to child: {tao_proportion_to_child:?}"); + + // Add this child's allocation to the total tao allocated to children. + tao_to_children = tao_to_children.saturating_add(tao_proportion_to_child); + } + log::trace!("Total tao allocated to children: {tao_to_children:?}"); + + // Step 4: Calculate the total tao inherited from parents. + for (proportion, parent) in parents { + // Retrieve the parent's total stake on this subnet. + let parent_tao = U96F32::saturating_from_num(Self::get_stake_for_hotkey_on_subnet( + &parent, + NetUid::ROOT, + )); + log::trace!("Parent tao for parent {parent:?} on subnet {netuid}: {parent_tao:?}"); + + // Convert the proportion to a normalized value between 0 and 1. + let normalized_proportion = U96F32::saturating_from_num(proportion) + .safe_div(U96F32::saturating_from_num(u64::MAX)); + log::trace!("Normalized proportion from parent: {normalized_proportion:?}"); + + // Calculate the amount of tao to be inherited from this parent. + let tao_proportion_from_parent: U96F32 = + U96F32::saturating_from_num(parent_tao).saturating_mul(normalized_proportion); + log::trace!("Tao proportion from parent: {tao_proportion_from_parent:?}"); + + // Add this parent's contribution to the total tao inherited from parents. + tao_from_parents = tao_from_parents.saturating_add(tao_proportion_from_parent); + } + log::trace!("Total tao inherited from parents: {tao_from_parents:?}"); + + // Step 5: Calculate the final inherited tao for the hotkey. + let finalized_tao: U96F32 = initial_tao + .saturating_sub(tao_to_children) // Subtract tao allocated to children + .saturating_add(tao_from_parents); // Add tao inherited from parents + log::trace!("Finalized tao for hotkey {hotkey:?} on subnet {netuid}: {finalized_tao:?}"); + + // Step 6: Return the final inherited tao value. + finalized_tao.saturating_to_num::().into() + } + + pub fn get_inherited_for_hotkey_on_subnet( + hotkey: &T::AccountId, + netuid: NetUid, + ) -> AlphaBalance { + // Step 1: Retrieve the initial total stake (alpha) for the hotkey on the specified subnet. + let initial_alpha: U96F32 = + U96F32::saturating_from_num(Self::get_stake_for_hotkey_on_subnet(hotkey, netuid)); + log::debug!("Initial alpha for hotkey {hotkey:?} on subnet {netuid}: {initial_alpha:?}"); + if netuid.is_root() { + return initial_alpha.saturating_to_num::().into(); + } + + // Initialize variables to track alpha allocated to children and inherited from parents. + let mut alpha_to_children: U96F32 = U96F32::saturating_from_num(0); + let mut alpha_from_parents: U96F32 = U96F32::saturating_from_num(0); + + // Step 2: Retrieve the lists of parents and children for the hotkey on the subnet. + let parents: Vec<(u64, T::AccountId)> = Self::get_parents(hotkey, netuid); + let children: Vec<(u64, T::AccountId)> = Self::get_children(hotkey, netuid); + log::debug!("Parents for hotkey {hotkey:?} on subnet {netuid}: {parents:?}"); + log::debug!("Children for hotkey {hotkey:?} on subnet {netuid}: {children:?}"); + + // Step 3: Calculate the total alpha allocated to children. + for (proportion, _) in children { + // Convert the proportion to a normalized value between 0 and 1. + let normalized_proportion: U96F32 = U96F32::saturating_from_num(proportion) + .safe_div(U96F32::saturating_from_num(u64::MAX)); + log::trace!("Normalized proportion for child: {normalized_proportion:?}"); + + // Calculate the amount of alpha to be allocated to this child. + let alpha_proportion_to_child: U96F32 = + U96F32::saturating_from_num(initial_alpha).saturating_mul(normalized_proportion); + log::trace!("Alpha proportion to child: {alpha_proportion_to_child:?}"); + + // Add this child's allocation to the total alpha allocated to children. + alpha_to_children = alpha_to_children.saturating_add(alpha_proportion_to_child); + } + log::debug!("Total alpha allocated to children: {alpha_to_children:?}"); + + // Step 4: Calculate the total alpha inherited from parents. + for (proportion, parent) in parents { + // Retrieve the parent's total stake on this subnet. + let parent_alpha: U96F32 = + U96F32::saturating_from_num(Self::get_stake_for_hotkey_on_subnet(&parent, netuid)); + log::trace!("Parent alpha for parent {parent:?} on subnet {netuid}: {parent_alpha:?}"); + + // Convert the proportion to a normalized value between 0 and 1. + let normalized_proportion: U96F32 = U96F32::saturating_from_num(proportion) + .safe_div(U96F32::saturating_from_num(u64::MAX)); + log::trace!("Normalized proportion from parent: {normalized_proportion:?}"); + + // Calculate the amount of alpha to be inherited from this parent. + let alpha_proportion_from_parent: U96F32 = + U96F32::saturating_from_num(parent_alpha).saturating_mul(normalized_proportion); + log::trace!("Alpha proportion from parent: {alpha_proportion_from_parent:?}"); + + // Add this parent's contribution to the total alpha inherited from parents. + alpha_from_parents = alpha_from_parents.saturating_add(alpha_proportion_from_parent); + } + log::debug!("Total alpha inherited from parents: {alpha_from_parents:?}"); + + // Step 5: Calculate the final inherited alpha for the hotkey. + let finalized_alpha: U96F32 = initial_alpha + .saturating_sub(alpha_to_children) // Subtract alpha allocated to children + .saturating_add(alpha_from_parents); // Add alpha inherited from parents + log::trace!( + "Finalized alpha for hotkey {hotkey:?} on subnet {netuid}: {finalized_alpha:?}" + ); + + // Step 6: Return the final inherited alpha value. + finalized_alpha.saturating_to_num::().into() + } +} diff --git a/pallets/subtensor/src/staking/stake_utils/mod.rs b/pallets/subtensor/src/staking/stake_utils/mod.rs new file mode 100644 index 0000000000..f9a16a87e5 --- /dev/null +++ b/pallets/subtensor/src/staking/stake_utils/mod.rs @@ -0,0 +1,25 @@ +//! Stake math utilities: prices, share pools, swaps, and extrinsic validation. +//! +//! ## Search anchors +//! +//! | Module | Owns | +//! |--------|------| +//! | [`alpha_price`] | Issuance, moving/median alpha price, TAO weight | +//! | [`inherited_stake`] | Parent/child inherited stake and weight vectors | +//! | [`stake_balances`] | Get / increase / decrease hotkey–coldkey alpha | +//! | [`stake_swap`] | `stake_into_subnet`, `unstake_from_subnet`, AMM swaps | +//! | [`stake_validation`] | `validate_add_stake` / remove / transition | +//! | [`provided_reserves`] | Provided TAO/alpha reserve counters | +//! | [`alpha_share_pool`] | [`HotkeyAlphaSharePoolDataOperations`] | + +use super::*; + +pub mod alpha_price; +pub mod alpha_share_pool; +pub mod inherited_stake; +pub mod provided_reserves; +pub mod stake_balances; +pub mod stake_swap; +pub mod stake_validation; + +pub use alpha_share_pool::HotkeyAlphaSharePoolDataOperations; diff --git a/pallets/subtensor/src/staking/stake_utils/provided_reserves.rs b/pallets/subtensor/src/staking/stake_utils/provided_reserves.rs new file mode 100644 index 0000000000..3a180916c8 --- /dev/null +++ b/pallets/subtensor/src/staking/stake_utils/provided_reserves.rs @@ -0,0 +1,37 @@ +//! Mutators for subnet-provided TAO / alpha reserve counters (`SubnetTAO`, `SubnetAlphaIn`). +use super::*; +use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; + +impl Pallet { + pub fn increase_provided_tao_reserve(netuid: NetUid, tao: TaoBalance) { + if !tao.is_zero() { + SubnetTAO::::mutate(netuid, |total| { + *total = total.saturating_add(tao); + }); + } + } + + pub fn decrease_provided_tao_reserve(netuid: NetUid, tao: TaoBalance) { + if !tao.is_zero() { + SubnetTAO::::mutate(netuid, |total| { + *total = total.saturating_sub(tao); + }); + } + } + + pub fn increase_provided_alpha_reserve(netuid: NetUid, alpha: AlphaBalance) { + if !alpha.is_zero() { + SubnetAlphaIn::::mutate(netuid, |total| { + *total = total.saturating_add(alpha); + }); + } + } + + pub fn decrease_provided_alpha_reserve(netuid: NetUid, alpha: AlphaBalance) { + if !alpha.is_zero() { + SubnetAlphaIn::::mutate(netuid, |total| { + *total = total.saturating_sub(alpha); + }); + } + } +} diff --git a/pallets/subtensor/src/staking/stake_utils/stake_balances.rs b/pallets/subtensor/src/staking/stake_utils/stake_balances.rs new file mode 100644 index 0000000000..bf52cc0acc --- /dev/null +++ b/pallets/subtensor/src/staking/stake_utils/stake_balances.rs @@ -0,0 +1,187 @@ +//! Read and mutate alpha stake shares for hotkey/coldkey pairs on a subnet. +use super::*; +use sp_std::ops::Neg; +use subtensor_runtime_common::{AlphaBalance, NetUid, Token}; + +impl Pallet { + /// Checks if a specific hotkey-coldkey pair has enough stake on a subnet to fulfill a given decrement. + /// + /// This function performs the following steps: + /// 1. Retrieves the current stake for the hotkey-coldkey pair on the specified subnet. + /// 2. Compares this stake with the requested decrement amount. + /// + /// # Arguments + /// * `hotkey`: The account ID of the hotkey. + /// * `coldkey`: The account ID of the coldkey. + /// * `netuid`: The unique identifier of the subnet. + /// * `decrement`: The amount of stake to be potentially decremented. + /// + /// # Returns + /// * `bool`: True if the account has enough stake to fulfill the decrement, false otherwise. + /// + /// # Note + /// This function only checks the stake for the specific hotkey-coldkey pair, not the total stake of the hotkey or coldkey individually. + pub fn calculate_reduced_stake_on_subnet( + hotkey: &T::AccountId, + coldkey: &T::AccountId, + netuid: NetUid, + decrement: AlphaBalance, + ) -> Result> { + // Retrieve the current stake for this hotkey-coldkey pair on the subnet + let current_stake = + Self::get_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid); + + // Compare the current stake with the requested decrement + // Return true if the current stake is greater than or equal to the decrement + if current_stake >= decrement { + Ok(current_stake.saturating_sub(decrement)) + } else { + Err(Error::::NotEnoughStakeToWithdraw) + } + } + + /// Retrieves the alpha (stake) value for a given hotkey and coldkey pair on a specific subnet. + /// + /// This function performs the following steps: + /// 1. Takes the hotkey, coldkey, and subnet ID as input parameters. + /// 2. Accesses the Alpha storage map to retrieve the stake value. + /// 3. Returns the retrieved stake value as a u64. + /// + /// # Arguments + /// * `hotkey`: The account ID of the hotkey (neuron). + /// * `coldkey`: The account ID of the coldkey (owner). + /// * `netuid`: The unique identifier of the subnet. + /// + /// # Returns + /// * `u64`: The alpha (stake) value for the specified hotkey-coldkey pair on the given subnet. + /// + /// # Note + /// This function retrieves the stake specific to the hotkey-coldkey pair, not the total stake of the hotkey or coldkey individually. + pub fn get_stake_for_hotkey_and_coldkey_on_subnet( + hotkey: &T::AccountId, + coldkey: &T::AccountId, + netuid: NetUid, + ) -> AlphaBalance { + let alpha_share_pool = Self::get_alpha_share_pool(hotkey.clone(), netuid); + alpha_share_pool.try_get_value(coldkey).unwrap_or(0).into() + } + + /// Retrieves the total stake (alpha) for a given hotkey on a specific subnet. + /// + /// This function performs the following step: + /// 1. Retrieves and returns the total alpha value associated with the hotkey on the specified subnet. + /// + /// # Arguments + /// * `hotkey`: The account ID of the hotkey. + /// * `netuid`: The unique identifier of the subnet. + /// + /// # Returns + /// * `u64`: The total alpha value for the hotkey on the specified subnet. + /// + /// # Note + /// This function returns the cumulative stake across all coldkeys associated with this hotkey on the subnet. + pub fn get_stake_for_hotkey_on_subnet(hotkey: &T::AccountId, netuid: NetUid) -> AlphaBalance { + // Retrieve and return the total alpha this hotkey owns on this subnet. + // This value represents the sum of stakes from all coldkeys associated with this hotkey. + TotalHotkeyAlpha::::get(hotkey, netuid) + } + + /// Increase hotkey stake on a subnet. + /// + /// The function updates share totals given current prices. + /// + /// # Arguments + /// * `hotkey`: The account ID of the hotkey. + /// * `netuid`: The unique identifier of the subnet. + /// * `amount`: The amount of alpha to be added. + /// + pub fn increase_stake_for_hotkey_on_subnet( + hotkey: &T::AccountId, + netuid: NetUid, + amount: AlphaBalance, + ) { + let mut alpha_share_pool = Self::get_alpha_share_pool(hotkey.clone(), netuid); + alpha_share_pool.update_value_for_all(amount.to_u64() as i64); + } + + /// Decrease hotkey stake on a subnet. + /// + /// The function updates share totals given current prices. + /// + /// # Arguments + /// * `hotkey`: The account ID of the hotkey. + /// * `netuid`: The unique identifier of the subnet. + /// * `amount`: The amount of alpha to be added. + /// + pub fn decrease_stake_for_hotkey_on_subnet(hotkey: &T::AccountId, netuid: NetUid, amount: u64) { + let mut alpha_share_pool = Self::get_alpha_share_pool(hotkey.clone(), netuid); + alpha_share_pool.update_value_for_all((amount as i64).neg()); + } + + /// Buys shares in the hotkey on a given subnet + /// + /// The function updates share totals given current prices. + /// + /// # Arguments + /// * `hotkey`: The account ID of the hotkey. + /// * `coldkey`: The account ID of the coldkey (owner). + /// * `netuid`: The unique identifier of the subnet. + /// * `amount`: The amount of alpha to be added. + /// + pub fn increase_stake_for_hotkey_and_coldkey_on_subnet( + hotkey: &T::AccountId, + coldkey: &T::AccountId, + netuid: NetUid, + amount: AlphaBalance, + ) { + if !amount.is_zero() { + let mut staking_hotkeys = StakingHotkeys::::get(coldkey); + if !staking_hotkeys.contains(hotkey) { + staking_hotkeys.push(hotkey.clone()); + StakingHotkeys::::insert(coldkey, staking_hotkeys.clone()); + } + } + + let mut alpha_share_pool = Self::get_alpha_share_pool(hotkey.clone(), netuid); + // We expect to add a positive amount here. + let amount = amount.to_u64() as i64; + alpha_share_pool.update_value_for_one(coldkey, amount); + } + + pub fn try_increase_stake_for_hotkey_and_coldkey_on_subnet( + hotkey: &T::AccountId, + netuid: NetUid, + amount: AlphaBalance, + ) -> bool { + let mut alpha_share_pool = Self::get_alpha_share_pool(hotkey.clone(), netuid); + let amount = amount.to_u64() as i64; + alpha_share_pool.sim_update_value_for_one(amount) + } + + /// Sell shares in the hotkey on a given subnet + /// + /// The function updates share totals given current prices. + /// + /// # Arguments + /// * `hotkey`: The account ID of the hotkey. + /// * `coldkey`: The account ID of the coldkey (owner). + /// * `netuid`: The unique identifier of the subnet. + /// * `amount`: The amount of alpha to be added. + /// + pub fn decrease_stake_for_hotkey_and_coldkey_on_subnet( + hotkey: &T::AccountId, + coldkey: &T::AccountId, + netuid: NetUid, + amount: AlphaBalance, + ) { + let mut alpha_share_pool = Self::get_alpha_share_pool(hotkey.clone(), netuid); + let amount = amount.to_u64(); + + // We expect a negative value here + if let Ok(value) = alpha_share_pool.try_get_value(coldkey) + && value >= amount + { + alpha_share_pool.update_value_for_one(coldkey, (amount as i64).neg()); + } + } +} diff --git a/pallets/subtensor/src/staking/stake_utils/stake_swap.rs b/pallets/subtensor/src/staking/stake_utils/stake_swap.rs new file mode 100644 index 0000000000..b5055e12ae --- /dev/null +++ b/pallets/subtensor/src/staking/stake_utils/stake_swap.rs @@ -0,0 +1,455 @@ +//! AMM stake swaps (TAO↔alpha), `stake_into_subnet` / `unstake_from_subnet`, and within-subnet transfers. +use super::*; +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::{AlphaBalance, AuthorshipInfo, NetUid, TaoBalance, Token}; +use subtensor_swap_interface::{Order, SwapHandler, SwapResult}; + +impl Pallet { + /// Swaps TAO for the alpha token on the subnet. + /// + /// Updates TaoIn, AlphaIn, and AlphaOut + pub fn swap_tao_for_alpha( + netuid: NetUid, + tao: TaoBalance, + price_limit: TaoBalance, + drop_fees: bool, + ) -> Result, DispatchError> { + // Step 1: Get the mechanism type for the subnet (0 for Stable, 1 for Dynamic) + let mechanism_id: u16 = SubnetMechanism::::get(netuid); + let swap_result = if mechanism_id == 1 { + let order = GetAlphaForTao::::with_amount(tao); + T::SwapInterface::swap(netuid.into(), order, price_limit.into(), drop_fees, false)? + } else { + // Step 3.b.1: Stable mechanism, just return the value 1:1 + SwapResult { + amount_paid_in: tao, + amount_paid_out: tao.to_u64().into(), + fee_paid: TaoBalance::ZERO, + fee_to_block_author: TaoBalance::ZERO, + } + }; + + let alpha_decrease = swap_result.paid_out_reserve_delta_i64().unsigned_abs(); + + // Decrease Alpha reserves. + Self::decrease_provided_alpha_reserve(netuid.into(), alpha_decrease.into()); + + // Increase Alpha outstanding. + SubnetAlphaOut::::mutate(netuid, |total| { + *total = total.saturating_add(swap_result.amount_paid_out.into()); + }); + + // Increase the protocol TAO reserve + SubnetTAO::::mutate(netuid, |total| { + let delta = swap_result.paid_in_reserve_delta_i64().unsigned_abs(); + *total = total.saturating_add(delta.into()); + }); + + // Increase Total Tao reserves. + TotalStake::::mutate(|total| *total = total.saturating_add(tao)); + + // Increase total subnet TAO volume. + SubnetVolume::::mutate(netuid, |total| { + *total = total.saturating_add(tao.to_u64() as u128); + }); + + Ok(swap_result) + } + + /// Swaps a subnet's Alpha token for TAO. + /// + /// Updates TaoIn, AlphaIn, and AlphaOut + pub fn swap_alpha_for_tao( + netuid: NetUid, + alpha: AlphaBalance, + price_limit: TaoBalance, + drop_fees: bool, + ) -> Result, DispatchError> { + // Step 1: Get the mechanism type for the subnet (0 for Stable, 1 for Dynamic) + let mechanism_id: u16 = SubnetMechanism::::get(netuid); + // Step 2: Swap alpha and attain tao + let swap_result = if mechanism_id == 1 { + let order = GetTaoForAlpha::::with_amount(alpha); + T::SwapInterface::swap(netuid.into(), order, price_limit.into(), drop_fees, false)? + } else { + // Step 3.b.1: Stable mechanism, just return the value 1:1 + SwapResult { + amount_paid_in: alpha, + amount_paid_out: alpha.to_u64().into(), + fee_paid: AlphaBalance::ZERO, + fee_to_block_author: AlphaBalance::ZERO, + } + }; + + // Increase only the protocol Alpha reserve + let alpha_delta = swap_result.paid_in_reserve_delta_i64().unsigned_abs(); + SubnetAlphaIn::::mutate(netuid, |total| { + *total = total.saturating_add(alpha_delta.into()); + }); + + // Decrease Alpha outstanding. + SubnetAlphaOut::::mutate(netuid, |total| { + *total = total.saturating_sub(alpha_delta.into()); + }); + + // Decrease tao reserves. + let tao_delta = swap_result.paid_out_reserve_delta_i64().unsigned_abs(); + Self::decrease_provided_tao_reserve(netuid.into(), tao_delta.into()); + + // Reduce total TAO reserves. + TotalStake::::mutate(|total| *total = total.saturating_sub(swap_result.amount_paid_out)); + + // Increase total subnet TAO volume. + SubnetVolume::::mutate(netuid, |total| { + *total = total.saturating_add(swap_result.amount_paid_out.to_u64() as u128) + }); + + // Return the tao received. + Ok(swap_result) + } + + /// Unstakes alpha from a subnet for a given hotkey and coldkey pair. + /// + /// We update the pools associated with a subnet as well as update hotkey alpha shares. + /// Credits the unstaked TAO to the beneficiary account + pub fn unstake_from_subnet( + hotkey: &T::AccountId, + coldkey: &T::AccountId, + beneficiary: &T::AccountId, + netuid: NetUid, + alpha: AlphaBalance, + price_limit: TaoBalance, + drop_fees: bool, + ) -> Result { + // Refuse to strip conviction-locked or collateral-bonded alpha even when + // callers (e.g. alpha fee withdrawal) skip the remove-stake validators. + Self::ensure_available_to_unstake(coldkey, netuid, alpha)?; + Self::ensure_hotkey_covers_collateral(coldkey, hotkey, netuid, alpha)?; + + // Decrease alpha on subnet + Self::decrease_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid, alpha); + + // Swap the alpha for TAO. + let swap_result = Self::swap_alpha_for_tao(netuid, alpha, price_limit, drop_fees)?; + + // Refund the unused alpha (in case if limit price is hit) + let refund = alpha.saturating_sub( + swap_result + .amount_paid_in + .saturating_add(swap_result.fee_paid) + .into(), + ); + if !refund.is_zero() { + Self::increase_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid, refund); + } + + // Transfer unstaked TAO from subnet account to the coldkey. + Self::transfer_tao_from_subnet(netuid, beneficiary, swap_result.amount_paid_out.into())?; + + // Swap (in a fee-less way) the block builder alpha fee + let mut fee_outflow = 0_u64; + let maybe_block_author_coldkey = T::AuthorshipProvider::author(); + if let Some(block_author_coldkey) = maybe_block_author_coldkey { + let bb_swap_result = Self::swap_alpha_for_tao( + netuid, + swap_result.fee_to_block_author, + T::SwapInterface::min_price::(), + true, + )?; + Self::transfer_tao_from_subnet( + netuid, + &block_author_coldkey, + bb_swap_result.amount_paid_out.into(), + )?; + fee_outflow = bb_swap_result.amount_paid_out.into(); + } else { + // block author is not found, burn this alpha + Self::burn_subnet_alpha(netuid, swap_result.fee_to_block_author); + } + + // If this is a root-stake + if netuid == NetUid::ROOT { + // Adjust root claimed value for this hotkey and coldkey. + Self::remove_stake_adjust_root_claimed_for_hotkey_and_coldkey(hotkey, coldkey, alpha); + + // If the coldkey no longer holds any root stake, remove it from the + // auto-claim staking-coldkey index so dead entries do not accumulate. + if !Self::coldkey_has_root_stake(coldkey) { + Self::maybe_remove_coldkey_index(coldkey); + } + } + + // Step 3: Update StakingHotkeys if the hotkey's total alpha, across all subnets, is zero + // TODO const: fix. + // if Self::get_stake(hotkey, coldkey) == 0 { + // StakingHotkeys::::mutate(coldkey, |hotkeys| { + // hotkeys.retain(|k| k != hotkey); + // }); + // } + + // Record TAO outflow + Self::record_tao_outflow( + netuid, + swap_result + .amount_paid_out + .saturating_add(fee_outflow.into()), + ); + + // Cleanup locks if needed + Self::cleanup_lock_if_zero(coldkey, netuid); + + LastColdkeyHotkeyStakeBlock::::insert(coldkey, hotkey, Self::get_current_block_as_u64()); + + // Deposit and log the unstaking event. + Self::deposit_event(Event::StakeRemoved( + coldkey.clone(), + hotkey.clone(), + swap_result.amount_paid_out.into(), + swap_result.amount_paid_in.into(), + netuid, + swap_result.fee_paid.to_u64(), + )); + + log::debug!( + "StakeRemoved( coldkey: {:?}, hotkey:{:?}, tao: {:?}, alpha:{:?}, netuid: {:?}, fee {} )", + coldkey.clone(), + hotkey.clone(), + swap_result.amount_paid_out, + swap_result.amount_paid_in, + netuid, + swap_result.fee_paid + ); + + Ok(swap_result.amount_paid_out.into()) + } + + /// Stakes TAO into a subnet for a given hotkey and coldkey pair. + /// + /// We update the pools associated with a subnet as well as update hotkey alpha shares. + pub(crate) fn stake_into_subnet( + hotkey: &T::AccountId, + coldkey: &T::AccountId, + netuid: NetUid, + tao: TaoBalance, + price_limit: TaoBalance, + drop_fees: bool, + ) -> Result { + // Transfer TAO from coldkey to the subnet account. + // Actual transfered may be different within ED amount. + let tao_staked = Self::transfer_tao_to_subnet(netuid, coldkey, tao)?; + + // Swap the tao to alpha. + let swap_result = Self::swap_tao_for_alpha(netuid, tao_staked, price_limit, drop_fees)?; + + ensure!( + !swap_result.amount_paid_out.is_zero(), + Error::::AmountTooLow + ); + + ensure!( + Self::try_increase_stake_for_hotkey_and_coldkey_on_subnet( + hotkey, + netuid, + swap_result.amount_paid_out.into(), + ), + Error::::InsufficientLiquidity + ); + + // Increase the alpha on the hotkey account. + Self::increase_stake_for_hotkey_and_coldkey_on_subnet( + hotkey, + coldkey, + netuid, + swap_result.amount_paid_out.into(), + ); + + // Step 4: Update the list of hotkeys staking for this coldkey + let mut staking_hotkeys = StakingHotkeys::::get(coldkey); + if !staking_hotkeys.contains(hotkey) { + staking_hotkeys.push(hotkey.clone()); + StakingHotkeys::::insert(coldkey, staking_hotkeys.clone()); + } + + // Increase the balance of the block author + let maybe_block_author_coldkey = T::AuthorshipProvider::author(); + if let Some(block_author_coldkey) = maybe_block_author_coldkey { + // TAO was transferred to subnet account in the beginning of this fn + // swap_tao_for_alpha guarantees that input amount of TAO was split into + // reserve delta + fee_to_block_author. + // Now transfer the fee from subnet account to block builder. + Self::transfer_tao_from_subnet( + netuid, + &block_author_coldkey, + swap_result.fee_to_block_author.into(), + )?; + } else { + // Block author is not found - burn this TAO + if let Some(subnet_account_id) = Self::get_subnet_account_id(netuid) { + let _ = Self::burn_tao(&subnet_account_id, swap_result.fee_to_block_author.into()); + } + } + + // Refund the TAO the AMM could not consume (e.g. when the user-supplied + // price limit is hit before the full `tao_staked` is swapped). Without + // this, the unswapped remainder is stranded on the subnet PalletId + // account. Mirrors the alpha refund in `unstake_from_subnet`. + let consumed_tao = swap_result + .amount_paid_in + .saturating_add(swap_result.fee_paid); + let refund_tao = tao_staked.saturating_sub(consumed_tao); + if !refund_tao.is_zero() { + Self::transfer_tao_from_subnet(netuid, coldkey, refund_tao)?; + // `swap_tao_for_alpha` bumped `TotalStake` by the full `tao_staked`; + // only `consumed_tao` actually became stake, so back out the refund. + TotalStake::::mutate(|total| *total = total.saturating_sub(refund_tao)); + } + + // Record TAO inflow + Self::record_tao_inflow(netuid, swap_result.amount_paid_in.into()); + + // Cleanup locks if needed + Self::cleanup_lock_if_zero(coldkey, netuid); + + LastColdkeyHotkeyStakeBlock::::insert(coldkey, hotkey, Self::get_current_block_as_u64()); + + // If this is a root-stake + if netuid == NetUid::ROOT { + // Adjust root claimed for this hotkey and coldkey. + let alpha = swap_result.amount_paid_out.into(); + Self::add_stake_adjust_root_claimed_for_hotkey_and_coldkey(hotkey, coldkey, alpha); + Self::maybe_add_coldkey_index(coldkey); + } + + // Deposit and log the staking event. + Self::deposit_event(Event::StakeAdded( + coldkey.clone(), + hotkey.clone(), + tao_staked, + swap_result.amount_paid_out.into(), + netuid, + swap_result.fee_paid.to_u64(), + )); + + log::debug!( + "StakeAdded( coldkey: {:?}, hotkey:{:?}, tao: {:?}, alpha:{:?}, netuid: {:?}, fee {} )", + coldkey.clone(), + hotkey.clone(), + tao_staked, + swap_result.amount_paid_out, + netuid, + swap_result.fee_paid, + ); + + Ok(swap_result.amount_paid_out.into()) + } + + /// Transfers stake between coldkeys and/or hotkey within one subnet without running it + /// through swap. + /// + /// Does not incur any swapping nor fees + pub fn transfer_stake_within_subnet( + origin_coldkey: &T::AccountId, + origin_hotkey: &T::AccountId, + destination_coldkey: &T::AccountId, + destination_hotkey: &T::AccountId, + netuid: NetUid, + alpha: AlphaBalance, + ) -> Result { + // Transfer lock (may fail if destination coldkey has a conflicting lock). + // The lock must follow the stake to the destination hotkey, otherwise a + // hotkey-changing transfer would leave the recipient's lock and conviction + // stranded on the origin hotkey. + Self::transfer_lock( + origin_coldkey, + destination_coldkey, + destination_hotkey, + netuid, + alpha, + )?; + + // Decrease alpha on origin keys + Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( + origin_hotkey, + origin_coldkey, + netuid, + alpha, + ); + if netuid == NetUid::ROOT { + Self::remove_stake_adjust_root_claimed_for_hotkey_and_coldkey( + origin_hotkey, + origin_coldkey, + alpha, + ); + } + + // If the destination coldkey does not own the destination hotkey, make the + // hotkey a delegate, matching the cross-subnet transfer path. + if Self::get_owning_coldkey_for_hotkey(destination_hotkey) != *destination_coldkey { + Self::maybe_become_delegate(destination_hotkey); + } + + // Increase alpha on destination keys + Self::increase_stake_for_hotkey_and_coldkey_on_subnet( + destination_hotkey, + destination_coldkey, + netuid, + alpha, + ); + if netuid == NetUid::ROOT { + Self::add_stake_adjust_root_claimed_for_hotkey_and_coldkey( + destination_hotkey, + destination_coldkey, + u64::from(alpha).into(), + ); + } + + // Calculate TAO equivalent based on current price (it is accurate because + // there's no slippage in this move) + let current_price = + ::SwapInterface::current_alpha_price(netuid.into()); + let tao_equivalent: TaoBalance = current_price + .saturating_mul(U64F64::saturating_from_num(alpha)) + .saturating_to_num::() + .into(); + + // Ensure tao_equivalent is above the minimum transfer amount + ensure!( + tao_equivalent >= DefaultMinTransfer::::get(), + Error::::AmountTooLow + ); + + // Step 3: Update StakingHotkeys if the hotkey's total alpha, across all subnets, is zero + // TODO: fix. + // if Self::get_stake(hotkey, coldkey) == 0 { + // StakingHotkeys::::mutate(coldkey, |hotkeys| { + // hotkeys.retain(|k| k != hotkey); + // }); + // } + + LastColdkeyHotkeyStakeBlock::::insert( + destination_coldkey, + destination_hotkey, + Self::get_current_block_as_u64(), + ); + + // Deposit and log the unstaking event. + Self::deposit_event(Event::StakeRemoved( + origin_coldkey.clone(), + origin_hotkey.clone(), + tao_equivalent, + alpha, + netuid, + 0_u64, // 0 fee + )); + Self::deposit_event(Event::StakeAdded( + destination_coldkey.clone(), + destination_hotkey.clone(), + tao_equivalent, + alpha, + netuid, + 0_u64, // 0 fee + )); + + Ok(tao_equivalent) + } +} diff --git a/pallets/subtensor/src/staking/stake_utils/stake_validation.rs b/pallets/subtensor/src/staking/stake_utils/stake_validation.rs new file mode 100644 index 0000000000..4248ed88e4 --- /dev/null +++ b/pallets/subtensor/src/staking/stake_utils/stake_validation.rs @@ -0,0 +1,310 @@ +//! Pre-flight validation for add / remove / unstake-all / stake-transition extrinsics. +use super::*; +use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; +use subtensor_swap_interface::{Order, SwapHandler}; + +impl Pallet { + /// Validate add_stake user input + pub fn validate_add_stake( + coldkey: &T::AccountId, + hotkey: &T::AccountId, + netuid: NetUid, + mut stake_to_be_added: TaoBalance, + max_amount: TaoBalance, + allow_partial: bool, + ) -> Result<(), Error> { + // Ensure that the subnet exists. + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); + + // Ensure that the subnet is enabled. + Self::ensure_subtoken_enabled(netuid)?; + + // Get the minimum balance (and amount) that satisfies the transaction + let min_stake = DefaultMinStake::::get(); + let min_amount = { + let order = GetAlphaForTao::::with_amount(min_stake); + let fee = T::SwapInterface::sim_swap(netuid.into(), order) + .map(|res| res.fee_paid) + .unwrap_or(T::SwapInterface::approx_fee_amount( + netuid.into(), + min_stake.into(), + )); + min_stake.saturating_add(fee.into()) + }; + + // Ensure that the stake_to_be_added is at least the min_amount + ensure!(stake_to_be_added >= min_amount, Error::::AmountTooLow); + + // Ensure that if partial execution is not allowed, the amount will not cause + // slippage over desired + if !allow_partial { + ensure!(stake_to_be_added <= max_amount, Error::::SlippageTooHigh); + } else { + stake_to_be_added = max_amount.min(stake_to_be_added); + } + + // Ensure the callers coldkey has enough stake to perform the transaction. + ensure!( + Self::can_remove_balance_from_coldkey_account(coldkey, stake_to_be_added.into()), + Error::::NotEnoughBalanceToStake + ); + + // Ensure that the hotkey account exists this is only possible through registration. + ensure!( + Self::hotkey_account_exists(hotkey), + Error::::HotKeyAccountNotExists + ); + + let order = GetAlphaForTao::::with_amount(stake_to_be_added); + let swap_result = T::SwapInterface::sim_swap(netuid.into(), order) + .map_err(|_| Error::::InsufficientLiquidity)?; + + // Check that actual withdrawn TAO amount is not lower than the minimum stake + ensure!( + swap_result.amount_paid_in >= min_stake, + Error::::AmountTooLow + ); + + ensure!( + !swap_result.amount_paid_out.is_zero(), + Error::::InsufficientLiquidity + ); + + // Ensure hotkey pool is precise enough + let try_stake_result = Self::try_increase_stake_for_hotkey_and_coldkey_on_subnet( + hotkey, + netuid, + swap_result.amount_paid_out.into(), + ); + ensure!(try_stake_result, Error::::InsufficientLiquidity); + + Ok(()) + } + + /// Validate remove_stake user input + /// + pub fn validate_remove_stake( + coldkey: &T::AccountId, + hotkey: &T::AccountId, + netuid: NetUid, + alpha_unstaked: AlphaBalance, + max_amount: AlphaBalance, + allow_partial: bool, + ) -> Result<(), Error> { + // Ensure that the subnet exists. + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); + + // Ensure that the subnet is enabled. + // Self::ensure_subtoken_enabled(netuid)?; + + // Do not allow zero unstake amount + ensure!(!alpha_unstaked.is_zero(), Error::::AmountTooLow); + + // Ensure that the stake amount to be removed is above the minimum in tao equivalent. + // Bypass this check if the user unstakes full amount + let remaining_alpha_stake = + Self::calculate_reduced_stake_on_subnet(hotkey, coldkey, netuid, alpha_unstaked)?; + let order = GetTaoForAlpha::::with_amount(alpha_unstaked); + match T::SwapInterface::sim_swap(netuid.into(), order) { + Ok(res) => { + if !remaining_alpha_stake.is_zero() { + ensure!( + res.amount_paid_out >= DefaultMinStake::::get(), + Error::::AmountTooLow + ); + } + } + Err(_) => return Err(Error::::InsufficientLiquidity), + } + + // Ensure that if partial execution is not allowed, the amount will not cause + // slippage over desired + if !allow_partial { + ensure!(alpha_unstaked <= max_amount, Error::::SlippageTooHigh); + } + + // Ensure that the hotkey account exists this is only possible through registration. + ensure!( + Self::hotkey_account_exists(hotkey), + Error::::HotKeyAccountNotExists + ); + + // Ensure that unstaked amount is not greater than available to unstake (due to locks) + Self::ensure_available_to_unstake(coldkey, netuid, alpha_unstaked)?; + // Collateral is per-hotkey: free stake on a sibling hotkey must not cover + // stripping the bonded position. + Self::ensure_hotkey_covers_collateral(coldkey, hotkey, netuid, alpha_unstaked)?; + + Ok(()) + } + + /// Validate if unstake_all can be executed + /// + pub fn validate_unstake_all( + coldkey: &T::AccountId, + hotkey: &T::AccountId, + only_alpha: bool, + ) -> Result<(), Error> { + // Get all netuids (filter out root) + let subnets = Self::get_all_subnet_netuids(); + + // Ensure that the hotkey account exists this is only possible through registration. + ensure!( + Self::hotkey_account_exists(hotkey), + Error::::HotKeyAccountNotExists + ); + + let mut unstaking_any = false; + for netuid in subnets.iter() { + if only_alpha && netuid.is_root() { + continue; + } + + // Get user's stake in this subnet + let alpha = Self::get_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, *netuid); + + // Ensure that unstaked amount is not greater than available to unstake (due to locks) + // for this subnet. + Self::ensure_available_to_unstake(coldkey, *netuid, alpha)?; + + if Self::validate_remove_stake(coldkey, hotkey, *netuid, alpha, alpha, false).is_ok() { + unstaking_any = true; + } + } + + // If no unstaking happens, return error + ensure!(unstaking_any, Error::::AmountTooLow); + + Ok(()) + } + + /// Validate stake transition user input + /// That works for move_stake, transfer_stake, and swap_stake + /// + pub fn validate_stake_transition( + origin_coldkey: &T::AccountId, + destination_coldkey: &T::AccountId, + origin_hotkey: &T::AccountId, + destination_hotkey: &T::AccountId, + origin_netuid: NetUid, + destination_netuid: NetUid, + alpha_amount: AlphaBalance, + max_amount: AlphaBalance, + maybe_allow_partial: Option, + check_transfer_toggle: bool, + ) -> Result<(), Error> { + // Ensure stake transition is actually happening + if origin_coldkey == destination_coldkey && origin_hotkey == destination_hotkey { + ensure!(origin_netuid != destination_netuid, Error::::SameNetuid); + } + + // Ensure that both subnets exist. + ensure!( + Self::subnet_exists(origin_netuid), + Error::::SubnetNotExists + ); + if origin_netuid != destination_netuid { + ensure!( + Self::subnet_exists(destination_netuid), + Error::::SubnetNotExists + ); + } + + ensure!( + SubtokenEnabled::::get(origin_netuid), + Error::::SubtokenDisabled + ); + + ensure!( + SubtokenEnabled::::get(destination_netuid), + Error::::SubtokenDisabled + ); + + // Ensure that the origin hotkey account exists + ensure!( + Self::hotkey_account_exists(origin_hotkey), + Error::::HotKeyAccountNotExists + ); + + // Ensure that the destination hotkey account exists + ensure!( + Self::hotkey_account_exists(destination_hotkey), + Error::::HotKeyAccountNotExists + ); + + // Ensure there is enough stake in the origin subnet. + let origin_alpha = Self::get_stake_for_hotkey_and_coldkey_on_subnet( + origin_hotkey, + origin_coldkey, + origin_netuid, + ); + ensure!( + alpha_amount <= origin_alpha, + Error::::NotEnoughStakeToWithdraw + ); + + // If origin and destination netuid are different, do the swap-related checks + if origin_netuid != destination_netuid { + // Ensure that the stake amount to be removed is above the minimum in tao equivalent. + // Transfers (check_transfer_toggle == true) have their own minimum, detached from + // the staking minimum used by moves and swaps. + let min_amount = if check_transfer_toggle { + DefaultMinTransfer::::get() + } else { + DefaultMinStake::::get() + }; + let order = GetTaoForAlpha::::with_amount(alpha_amount); + let tao_equivalent = T::SwapInterface::sim_swap(origin_netuid.into(), order) + .map(|res| res.amount_paid_out) + .map_err(|_| Error::::InsufficientLiquidity)?; + ensure!(tao_equivalent > min_amount, Error::::AmountTooLow); + + // Ensure that if partial execution is not allowed, the amount will not cause + // slippage over desired + if let Some(allow_partial) = maybe_allow_partial + && !allow_partial + { + ensure!(alpha_amount <= max_amount, Error::::SlippageTooHigh); + } + } + + if check_transfer_toggle { + // Ensure transfer is toggled. + ensure!( + TransferToggle::::get(origin_netuid), + Error::::TransferDisallowed + ); + if origin_netuid != destination_netuid { + ensure!( + TransferToggle::::get(destination_netuid), + Error::::TransferDisallowed + ); + } + } + + // Enforce lock invariant: if the is cross-subnet move, the remaining amount must + // cover the lock. + if origin_netuid != destination_netuid { + Self::ensure_available_to_unstake(origin_coldkey, origin_netuid, alpha_amount)?; + } else if origin_coldkey != destination_coldkey { + // Same-subnet, ownership-changing transfer. Conviction locks follow the + // stake to the destination coldkey via `transfer_lock`, but miner + // registration collateral has no transfer exit and does not follow — its + // `MinerCollateral(netuid, hotkey, coldkey)` stays on the origin. Without + // this check, a coldkey could liberate locked collateral by transferring the + // staked alpha to a second coldkey. Require the origin coldkey to retain + // enough alpha on the subnet to still cover its collateral. + Self::ensure_transfer_respects_collateral(origin_coldkey, origin_netuid, alpha_amount)?; + } + // Always keep bonded alpha on the origin hotkey itself (same-subnet moves + // to a sibling hotkey would otherwise leave a ghost metagraph bond). + Self::ensure_hotkey_covers_collateral( + origin_coldkey, + origin_hotkey, + origin_netuid, + alpha_amount, + )?; + + Ok(()) + } +} diff --git a/pallets/subtensor/src/subnets/collateral.rs b/pallets/subtensor/src/subnets/collateral.rs index 93d60d9913..80040bc31a 100644 --- a/pallets/subtensor/src/subnets/collateral.rs +++ b/pallets/subtensor/src/subnets/collateral.rs @@ -271,7 +271,8 @@ impl Pallet { /// /// Callers that also mutate registration state should wrap this in /// `with_transaction` so a later failure rolls the payment back. - pub fn pay_registration( + /// Burn the non-collateral share and lock/top-up miner collateral for a neuron registration. + pub fn pay_neuron_registration( netuid: NetUid, hotkey: &T::AccountId, coldkey: &T::AccountId, @@ -280,7 +281,7 @@ impl Pallet { ) -> DispatchResult { let total_charge = burned_share.saturating_add(collateral_topup); if total_charge.is_zero() { - Self::resnapshot_collateral_drain(netuid, hotkey, coldkey); + Self::resnapshot_miner_collateral_after_drain(netuid, hotkey, coldkey); return Ok(()); } @@ -328,7 +329,7 @@ impl Pallet { if total_alpha.is_zero() { // Dust: payment already settled via the single swap; nothing to // stake or remove from AlphaOut. - Self::resnapshot_collateral_drain(netuid, hotkey, coldkey); + Self::resnapshot_miner_collateral_after_drain(netuid, hotkey, coldkey); return Ok(()); } @@ -356,7 +357,7 @@ impl Pallet { } if lock_alpha.is_zero() { - Self::resnapshot_collateral_drain(netuid, hotkey, coldkey); + Self::resnapshot_miner_collateral_after_drain(netuid, hotkey, coldkey); return Ok(()); } @@ -454,7 +455,12 @@ impl Pallet { /// Re-snapshot a standing collateral entry's drain ratio to the subnet's /// current `CollateralDrainRatio`. No-op when the position has no entry. - fn resnapshot_collateral_drain(netuid: NetUid, hotkey: &T::AccountId, coldkey: &T::AccountId) { + /// After alpha drain, rewrite collateral lock state so remaining locked alpha matches stake. + fn resnapshot_miner_collateral_after_drain( + netuid: NetUid, + hotkey: &T::AccountId, + coldkey: &T::AccountId, + ) { MinerCollateral::::mutate_exists((netuid, hotkey, coldkey), |maybe_state| { if let Some(state) = maybe_state { state.drain_ratio = CollateralDrainRatio::::get(netuid); @@ -554,7 +560,7 @@ impl Pallet { !netuid.is_root(), Error::::RegistrationNotPermittedOnRootSubnet ); - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); Self::ensure_subtoken_enabled(netuid)?; ensure!( Self::hotkey_account_exists(&hotkey), @@ -671,7 +677,7 @@ impl Pallet { !netuid.is_root(), Error::::RegistrationNotPermittedOnRootSubnet ); - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); ensure!( Self::hotkey_account_exists(&hotkey), Error::::HotKeyAccountNotExists diff --git a/pallets/subtensor/src/subnets/dissolution.rs b/pallets/subtensor/src/subnets/dissolution.rs deleted file mode 100644 index 760d24aaa5..0000000000 --- a/pallets/subtensor/src/subnets/dissolution.rs +++ /dev/null @@ -1,1027 +0,0 @@ -use super::*; -use frame_support::weights::WeightMeter; -use subtensor_runtime_common::{NetUid, NetUidStorageIndex, clear_prefix_with_meter}; -use subtensor_swap_interface::SwapHandler; -/// Enum for the dissolve cleanup phase. -#[derive(Encode, Decode, TypeInfo, Clone, PartialEq, Eq, Debug, DecodeWithMemTracking)] -pub enum DissolveCleanupPhase { - /// Phase 1.1: Remove root dividend claimable entries for the subnet. - SubnetRootDividendsRootClaimable, - /// Phase 1.2: Remove root dividend claimed entries for the subnet. - SubnetRootDividendsRootClaimed, - /// Phase 2.1: Get the total alpha value for the subnet. - AlphaInOutStakesGetTotalAlphaValue, - /// Phase 2.2: Destroy alpha in and out stakes for the subnet. - AlphaInOutStakesSettleStakes, - /// Phase 2.3: Clean alpha entries for the subnet. - AlphaInOutStakesAlpha, - /// Phase 2.4: Clear hotkey totals for the subnet. - AlphaInOutStakesHotkeyTotals, - /// Phase 2.5: Clear locks for the subnet. - AlphaInOutStakesLocks, - /// Phase 2.6: Clear locks for the subnet. - AlphaInOutStakesDecayingLocks, - /// Phase 2.7: Destroy alpha in and out stakes for the subnet. - AlphaInOutStakes, - /// Phase 3: Clear protocol liquidity for the subnet on the swap layer. - ProtocolLiquidity, - /// Phase 4: Remove scalar `Network*` parameters, then continue with map and index cleanup phases. - PurgeNetuid, - /// Phase 5.1: Remove is network member entries for the subnet. - NetworkIsNetworkMember, - /// Phase 5.2: Recovery / legacy: scalar `Network*` removal; the hook advances to map cleanup like `PurgeNetuid` after `remove_network_parameters` completes. - NetworkParameters, - /// Phase 5.3: Remove map-backed subnet storage (keys, axons, per-mechanism weights, etc.). - NetworkMapParameters, - /// Phase 5.4: Clear root-network weight entries referencing this netuid. - NetworkUpdateWeightsOnRoot, - /// Phase 5.5: Remove childkey take entries for this netuid. - NetworkChildkeyTake, - /// Phase 5.6: Remove child key bindings for this netuid. - NetworkChildkeys, - /// Phase 5.7: Remove parent key bindings for this netuid. - NetworkParentkeys, - /// Phase 5.8: Remove last hotkey emission records for this netuid. - NetworkLastHotkeyEmissionOnNetuid, - /// Phase 5.9: Remove total hotkey alpha last epoch entries for this netuid. - NetworkTotalHotkeyAlphaLastEpoch, - /// Phase 5.10: Remove transaction key last-block rate limit entries for this netuid. - NetworkTransactionKeyLastBlock, - /// Phase 5.11: Remove lock entries for this netuid. - NetworkLock, - /// Phase 5.12: Remove decaying lock entries for this netuid. - NetworkDecayingLock, -} - -impl Default for DissolveCleanupPhase { - fn default() -> Self { - Self::SubnetRootDividendsRootClaimable - } -} - -#[crate::freeze_struct("c524ea54893ae91a")] -#[derive(Encode, Decode, TypeInfo, Clone, PartialEq, Eq, Debug, DecodeWithMemTracking)] -pub struct DissolveCleanupStatus { - pub netuid: NetUid, - pub phase: DissolveCleanupPhase, - pub last_key: Option>, - pub subnet_total_alpha_value: Option, - pub subnet_distributed_tao: Option, -} - -impl DissolveCleanupStatus { - pub fn new(netuid: NetUid) -> Self { - Self { - netuid, - phase: DissolveCleanupPhase::default(), - last_key: None, - subnet_total_alpha_value: None, - subnet_distributed_tao: None, - } - } - - pub fn set_phase(&mut self, phase: DissolveCleanupPhase) { - self.phase = phase; - } -} - -impl Pallet { - /// Facilitates the removal of a user's subnetwork. - /// - /// # Arguments - /// * `origin`: ('T::RuntimeOrigin'): The calling origin. Must be signed. - /// * `netuid`: ('u16'): The unique identifier of the network to be removed. - /// - /// # Events - /// * `NetworkRemoved`: Emitted when a network is successfully removed. - /// - /// # Errors - /// * `MechanismDoesNotExist`: If the specified network does not exist. - /// * `NotSubnetOwner`: If the caller does not own the specified subnet. - /// - pub fn do_dissolve_network(netuid: NetUid) -> dispatch::DispatchResult { - // --- The network exists? - ensure!( - Self::if_subnet_exist(netuid) && netuid != NetUid::ROOT, - Error::::SubnetNotExists - ); - - // Since TotalStake is updated on this level, purge reservoirs here into reserves and TotalStake - let reservoir_tao = T::SwapInterface::protocol_tao_reservoir(netuid); - let reservoir_alpha = T::SwapInterface::protocol_alpha_reservoir(netuid); - T::SwapInterface::clear_protocol_liquidity_reservoirs(netuid); - Self::increase_provided_tao_reserve(netuid, reservoir_tao); - Self::increase_provided_alpha_reserve(netuid, reservoir_alpha); - if !reservoir_tao.is_zero() { - TotalStake::::mutate(|total| { - *total = total.saturating_add(reservoir_tao); - }); - } - - let mut dissolved_networks = DissolveCleanupQueue::::get(); - ensure!( - !dissolved_networks.contains(&netuid), - Error::::NetworkDissolveAlreadyQueued - ); - - // Just remove the network from the added networks, it is used to check if the network is existed. - NetworksAdded::::remove(netuid); - // Reduce the total networks count. - TotalNetworks::::mutate(|n: &mut u16| *n = n.saturating_sub(1)); - TotalStake::::mutate(|total| *total = total.saturating_sub(SubnetTAO::::get(netuid))); - - dissolved_networks.push(netuid); - DissolveCleanupQueue::::set(dissolved_networks); - - log::debug!("NetworkRemoved( netuid:{netuid:?} )"); - - // --- Emit the NetworkRemoved event - Self::deposit_event(Event::NetworkRemoved(netuid)); - - Ok(()) - } - - pub fn remove_network_map_parameters(netuid: NetUid, weight_meter: &mut WeightMeter) -> bool { - let write_weight = T::DbWeight::get().writes(1); - - let result = clear_prefix_with_meter(weight_meter, write_weight, |limit| { - Keys::::clear_prefix(netuid, limit, None) - }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { - Uids::::clear_prefix(netuid, limit, None) - }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { - BlockAtRegistration::::clear_prefix(netuid, limit, None) - }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { - Axons::::clear_prefix(netuid, limit, None) - }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { - NeuronCertificates::::clear_prefix(netuid, limit, None) - }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { - Prometheus::::clear_prefix(netuid, limit, None) - }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { - AlphaDividendsPerSubnet::::clear_prefix(netuid, limit, None) - }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { - PendingChildKeys::::clear_prefix(netuid, limit, None) - }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { - AssociatedEvmAddress::::clear_prefix(netuid, limit, None) - }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { - AssociatedUidsByEvmAddress::::clear_prefix(netuid, limit, None) - }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { - HotkeyLock::::clear_prefix(netuid, limit, None) - }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { - DecayingHotkeyLock::::clear_prefix(netuid, limit, None) - }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { - LockingColdkeys::::clear_prefix((netuid,), limit, None) - }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { - // Lock metadata only. Alpha (including collateral stake) was already - // pro-rata converted to coldkey free TAO in AlphaInOutStakesSettleStakes; - // unlocking here would double-pay. Clearing drops the now-meaningless - // MinerCollateral rows for the dissolved netuid. - MinerCollateral::::clear_prefix((netuid,), limit, None) - }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { - ColdkeyMinerCollateral::::clear_prefix(netuid, limit, None) - }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { - ColdkeyCollateralHotkeys::::clear_prefix(netuid, limit, None) - }); - - if !result { - return false; - } - - let read_weight = T::DbWeight::get().reads(1); - if !weight_meter.can_consume(read_weight) { - return false; - } - weight_meter.consume(read_weight); - let mechanisms: u8 = MechanismCountCurrent::::get(netuid).into(); - - for subid in 0..mechanisms { - let mechanism_weight = T::DbWeight::get().reads_writes(1, 2); - if !weight_meter.can_consume(mechanism_weight) { - return false; - } - weight_meter.consume(mechanism_weight); - let netuid_index = Self::get_mechanism_storage_index(netuid, subid.into()); - - LastUpdate::::remove(netuid_index); - Incentive::::remove(netuid_index); - - let result = clear_prefix_with_meter(weight_meter, write_weight, |limit| { - WeightCommits::::clear_prefix(netuid_index, limit, None) - }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { - TimelockedWeightCommits::::clear_prefix(netuid_index, limit, None) - }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { - CRV3WeightCommits::::clear_prefix(netuid_index, limit, None) - }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { - CRV3WeightCommitsV2::::clear_prefix(netuid_index, limit, None) - }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { - Bonds::::clear_prefix(netuid_index, limit, None) - }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { - Weights::::clear_prefix(netuid_index, limit, None) - }); - - if !result { - return false; - } - } - - let removal_weight = T::DbWeight::get().writes(3); - if !weight_meter.can_consume(removal_weight) { - return false; - } - weight_meter.consume(removal_weight); - RevealPeriodEpochs::::remove(netuid); - MechanismCountCurrent::::remove(netuid); - MechanismEmissionSplit::::remove(netuid); - - if !clear_prefix_with_meter(weight_meter, write_weight, |limit| { - LastHotkeySwapOnNetuid::::clear_prefix(netuid, limit, None) - }) || !clear_prefix_with_meter(weight_meter, write_weight, |limit| { - HotkeySuccessor::::clear_prefix(netuid, limit, None) - }) || !clear_prefix_with_meter(weight_meter, write_weight, |limit| { - HotkeyRoot::::clear_prefix(netuid, limit, None) - }) { - return false; - } - - if let Some(lease_id) = SubnetUidToLeaseId::::get(netuid) { - if !clear_prefix_with_meter(weight_meter, write_weight, |limit| { - SubnetLeaseShares::::clear_prefix(lease_id, limit, None) - }) { - return false; - } - let lease_weight = T::DbWeight::get().writes(3); - if !weight_meter.can_consume(lease_weight) { - return false; - } - weight_meter.consume(lease_weight); - SubnetLeases::::remove(lease_id); - AccumulatedLeaseDividends::::remove(lease_id); - SubnetUidToLeaseId::::remove(netuid); - } - - true - } - - pub fn remove_network_parameters(netuid: NetUid, weight_meter: &mut WeightMeter) -> bool { - // Flat write charge for the `::remove(netuid)` list below. Bump this when - // adding or removing entries from that list so the weight stays in step. - let removal_weight = T::DbWeight::get().writes(82); - if !weight_meter.can_consume(removal_weight) { - return false; - } - weight_meter.consume(removal_weight); - SubnetOwner::::remove(netuid); - SubnetworkN::::remove(netuid); - NetworkRegisteredAt::::remove(netuid); - Active::::remove(netuid); - Emission::::remove(netuid); - Consensus::::remove(netuid); - Dividends::::remove(netuid); - ValidatorPermit::::remove(netuid); - ValidatorTrust::::remove(netuid); - Tempo::::remove(netuid); - Kappa::::remove(netuid); - Difficulty::::remove(netuid); - MaxAllowedUids::::remove(netuid); - ImmunityPeriod::::remove(netuid); - ActivityCutoff::::remove(netuid); - MinAllowedWeights::::remove(netuid); - RegistrationsThisInterval::::remove(netuid); - POWRegistrationsThisInterval::::remove(netuid); - BurnRegistrationsThisInterval::::remove(netuid); - SubnetAlphaInEmission::::remove(netuid); - SubnetAlphaOutEmission::::remove(netuid); - SubnetTaoInEmission::::remove(netuid); - SubnetVolume::::remove(netuid); - SubnetMovingPrice::::remove(netuid); - SubnetTaoFlow::::remove(netuid); - SubnetEmaTaoFlow::::remove(netuid); - SubnetProtocolFlow::::remove(netuid); - SubnetEmaProtocolFlow::::remove(netuid); - SubnetExcessTao::::remove(netuid); - SubnetRootSellTao::::remove(netuid); - TokenSymbol::::remove(netuid); - SubnetMechanism::::remove(netuid); - SubnetOwnerHotkey::::remove(netuid); - NetworkRegistrationAllowed::::remove(netuid); - NetworkPowRegistrationAllowed::::remove(netuid); - TransferToggle::::remove(netuid); - SubnetLocked::::remove(netuid); - LargestLocked::::remove(netuid); - FirstEmissionBlockNumber::::remove(netuid); - PendingValidatorEmission::::remove(netuid); - PendingServerEmission::::remove(netuid); - PendingRootAlphaDivs::::remove(netuid); - PendingOwnerCut::::remove(netuid); - MinerBurned::::remove(netuid); - BlocksSinceLastStep::::remove(netuid); - LastMechansimStepBlock::::remove(netuid); - LastAdjustmentBlock::::remove(netuid); - ServingRateLimit::::remove(netuid); - Rho::::remove(netuid); - AlphaSigmoidSteepness::::remove(netuid); - MaxAllowedValidators::::remove(netuid); - BondsMovingAverage::::remove(netuid); - BondsPenalty::::remove(netuid); - BondsResetOn::::remove(netuid); - WeightsSetRateLimit::::remove(netuid); - ValidatorPruneLen::::remove(netuid); - ScalingLawPower::::remove(netuid); - TargetRegistrationsPerInterval::::remove(netuid); - CommitRevealWeightsEnabled::::remove(netuid); - BurnHalfLife::::remove(netuid); - BurnIncreaseMult::::remove(netuid); - CollateralLockShare::::remove(netuid); - CollateralDrainRatio::::remove(netuid); - Burn::::remove(netuid); - MinBurn::::remove(netuid); - MaxBurn::::remove(netuid); - MinDifficulty::::remove(netuid); - MaxDifficulty::::remove(netuid); - RegistrationsThisBlock::::remove(netuid); - EMAPriceHalvingBlocks::::remove(netuid); - RAORecycledForRegistration::::remove(netuid); - MaxRegistrationsPerBlock::::remove(netuid); - WeightsVersionKey::::remove(netuid); - LiquidAlphaOn::::remove(netuid); - Yuma3On::::remove(netuid); - AlphaValues::::remove(netuid); - SubtokenEnabled::::remove(netuid); - OwnerCutAutoLockEnabled::::remove(netuid); - ImmuneOwnerUidsLimit::::remove(netuid); - StakeWeight::::remove(netuid); - LoadedEmission::::remove(netuid); - OwnerLock::::remove(netuid); - DecayingOwnerLock::::remove(netuid); - ActivityCutoffFactorMilli::::remove(netuid); - LastEpochBlock::::remove(netuid); - PendingEpochAt::::remove(netuid); - SubnetEpochIndex::::remove(netuid); - - if SubnetIdentitiesV3::::contains_key(netuid) { - SubnetIdentitiesV3::::remove(netuid); - Self::deposit_event(Event::SubnetIdentityRemoved(netuid)); - } - true - } - - pub fn remove_network_is_network_member( - netuid: NetUid, - weight_meter: &mut WeightMeter, - last_key: Option>, - ) -> (bool, Option>) { - let iter = match last_key { - Some(raw_key) => Keys::::iter_from(raw_key), - None => Keys::::iter(), - }; - - let (read_all, last_item) = Self::remove_storage_entries_for_netuid( - weight_meter, - iter, - |(nu, _, _)| *nu == netuid, - |(_, _, hotkey)| hotkey, - |hotkey| IsNetworkMember::::remove(hotkey, netuid), - 1, - ); - - ( - read_all, - last_item.map(|(nu, uid, _)| Keys::::hashed_key_for(nu, uid)), - ) - } - - pub fn remove_network_update_weights_on_root( - netuid: NetUid, - weight_meter: &mut WeightMeter, - last_key: Option>, - ) -> (bool, Option>) { - let netuid_u16 = u16::from(netuid); - - let root = NetUidStorageIndex::ROOT; - let iter = match last_key { - Some(raw_key) => Weights::::iter_prefix_from(root, raw_key), - None => Weights::::iter_prefix(root), - }; - - fn filter_weights(netuid_u16: u16, weights: &[(u16, u16)]) -> (bool, Vec<(u16, u16)>) { - let mut need_update = false; - let mut filtered_weights = weights.to_vec(); - for (subnet_id, weight) in filtered_weights.iter_mut() { - if *subnet_id == netuid_u16 && *weight != 0 { - need_update = true; - *weight = 0; - } - } - (need_update, filtered_weights) - } - - let (read_all, last_item) = Self::remove_storage_entries_for_netuid( - weight_meter, - iter, - |_| true, - |(uid, weights)| (uid, weights), - |(uid, weights)| { - let (update, filtered_weights) = filter_weights(netuid_u16, weights); - if update { - Weights::::insert(root, *uid, filtered_weights); - } - }, - 1, - ); - - ( - read_all, - last_item.map(|key| Weights::::hashed_key_for(root, key.0)), - ) - } - - pub fn remove_network_childkey_take( - netuid: NetUid, - weight_meter: &mut WeightMeter, - last_key: Option>, - ) -> (bool, Option>) { - let iter = match last_key { - Some(raw_key) => ChildkeyTake::::iter_from(raw_key), - None => ChildkeyTake::::iter(), - }; - - let (read_all, last_item) = Self::remove_storage_entries_for_netuid( - weight_meter, - iter, - |(_, nu, _)| *nu == netuid, - |(hot, _, _)| hot, - |hot| ChildkeyTake::::remove(hot, netuid), - 1, - ); - - ( - read_all, - last_item.map(|(hot, nu, _)| ChildkeyTake::::hashed_key_for(&hot, nu)), - ) - } - - pub fn remove_network_childkeys( - netuid: NetUid, - weight_meter: &mut WeightMeter, - last_key: Option>, - ) -> (bool, Option>) { - let iter = match last_key { - Some(raw_key) => ChildKeys::::iter_from(raw_key), - None => ChildKeys::::iter(), - }; - - let (read_all, last_item) = Self::remove_storage_entries_for_netuid( - weight_meter, - iter, - |(_, nu, _)| *nu == netuid, - |(hot, _, _)| hot, - |hot| ChildKeys::::remove(hot, netuid), - 1, - ); - - ( - read_all, - last_item.map(|key| ChildKeys::::hashed_key_for(&key.0, key.1)), - ) - } - - pub fn remove_network_parentkeys( - netuid: NetUid, - weight_meter: &mut WeightMeter, - last_key: Option>, - ) -> (bool, Option>) { - let iter = match last_key { - Some(raw_key) => ParentKeys::::iter_from(raw_key), - None => ParentKeys::::iter(), - }; - - let (read_all, last_item) = Self::remove_storage_entries_for_netuid( - weight_meter, - iter, - |(_, nu, _)| *nu == netuid, - |(hot, _, _)| hot, - |hot| ParentKeys::::remove(hot, netuid), - 1, - ); - - ( - read_all, - last_item.map(|key| ParentKeys::::hashed_key_for(&key.0, key.1)), - ) - } - - pub fn remove_network_last_hotkey_emission_on_netuid( - netuid: NetUid, - weight_meter: &mut WeightMeter, - last_key: Option>, - ) -> (bool, Option>) { - let iter = match last_key { - Some(raw_key) => LastHotkeyEmissionOnNetuid::::iter_from(raw_key), - None => LastHotkeyEmissionOnNetuid::::iter(), - }; - - let (read_all, last_item) = Self::remove_storage_entries_for_netuid( - weight_meter, - iter, - |(_, nu, _)| *nu == netuid, - |(hot, _, _)| hot, - |hot| LastHotkeyEmissionOnNetuid::::remove(hot, netuid), - 1, - ); - - ( - read_all, - last_item.map(|key| LastHotkeyEmissionOnNetuid::::hashed_key_for(&key.0, key.1)), - ) - } - - pub fn remove_network_total_hotkey_alpha_last_epoch( - netuid: NetUid, - weight_meter: &mut WeightMeter, - last_key: Option>, - ) -> (bool, Option>) { - let iter = match last_key { - Some(raw_key) => TotalHotkeyAlphaLastEpoch::::iter_from(raw_key), - None => TotalHotkeyAlphaLastEpoch::::iter(), - }; - - let (read_all, last_item) = Self::remove_storage_entries_for_netuid( - weight_meter, - iter, - |(_, nu, _)| *nu == netuid, - |(hot, _, _)| hot, - |hot| TotalHotkeyAlphaLastEpoch::::remove(hot, netuid), - 1, - ); - - ( - read_all, - last_item.map(|(hot, nu, _)| TotalHotkeyAlphaLastEpoch::::hashed_key_for(&hot, nu)), - ) - } - - pub fn remove_network_transaction_key_last_block( - netuid: NetUid, - weight_meter: &mut WeightMeter, - last_key: Option>, - ) -> (bool, Option>) { - let iter = match last_key { - Some(raw_key) => TransactionKeyLastBlock::::iter_from(raw_key), - None => TransactionKeyLastBlock::::iter(), - }; - - let (read_all, last_item) = Self::remove_storage_entries_for_netuid( - weight_meter, - iter, - |((_, nu, _), _)| *nu == netuid, - |((hot, _, name), _)| (hot, name), - |(hot, name)| TransactionKeyLastBlock::::remove((hot.clone(), netuid, *name)), - 1, - ); - - ( - read_all, - last_item.map(|((hot, _, name), _)| { - TransactionKeyLastBlock::::hashed_key_for((&hot, netuid, name)) - }), - ) - } - - pub fn remove_data_for_dissolved_networks(remaining_weight: Weight) -> Weight { - let w = T::DbWeight::get().writes(1); - let r = T::DbWeight::get().reads(1); - let mut weight_meter = frame_support::weights::WeightMeter::with_limit(remaining_weight); - - // complete unfinished network cleanup at first if any - if let Some(mut status) = CurrentDissolveCleanupStatus::::get() { - let (cleanup_completed, weight) = - Self::clean_up_data_for_one_dissolved_network(&mut weight_meter, &mut status); - if cleanup_completed { - DissolveCleanupQueue::::mutate(|queue| { - queue.retain(|queued_netuid| *queued_netuid != status.netuid); - }); - CurrentDissolveCleanupStatus::::kill(); - return weight.saturating_add(T::DbWeight::get().writes(2)); - } - return weight; - } - - if !weight_meter.can_consume(r) { - return weight_meter.consumed(); - } - weight_meter.consume(r); - - let dissolved_networks = DissolveCleanupQueue::::get(); - if let Some(netuid) = dissolved_networks.first() { - if !weight_meter.can_consume(w) { - return weight_meter.consumed(); - } - weight_meter.consume(w); - - let mut status = DissolveCleanupStatus::new(*netuid); - CurrentDissolveCleanupStatus::::set(Some(status.clone())); - - let (cleanup_completed, _weight) = - Self::clean_up_data_for_one_dissolved_network(&mut weight_meter, &mut status); - - if cleanup_completed { - DissolveCleanupQueue::::mutate(|queue| { - queue.retain(|queued_netuid| *queued_netuid != status.netuid); - }); - CurrentDissolveCleanupStatus::::kill(); - weight_meter.consume(T::DbWeight::get().writes(2)); - } - } - - weight_meter.consumed() - } - - // try use all weight available to clean up data for one dissolved network based on the status - pub fn clean_up_data_for_one_dissolved_network( - weight_meter: &mut WeightMeter, - status: &mut DissolveCleanupStatus, - ) -> (bool, Weight) { - let r = T::DbWeight::get().reads(1); - - let netuid = status.netuid; - - if !weight_meter.can_consume(r) { - return (false, weight_meter.consumed()); - } - - // if one phase is done or exit because of weight limit - let mut phase_done = true; - let mut cleanup_completed = false; - // only reason for phase_done to be false is if the weight limit is reached - while phase_done { - // let phase = status.phase.clone(); - log::debug!( - "dissolved_networks phase: {:?} for netuid: {:?}", - &status.phase, - netuid - ); - - let done = match &status.phase { - DissolveCleanupPhase::SubnetRootDividendsRootClaimable => { - let (done, new_key) = Self::clean_up_root_claimable_for_subnet( - netuid, - weight_meter, - status.last_key.clone(), - ); - - if done { - status.set_phase(DissolveCleanupPhase::SubnetRootDividendsRootClaimed); - status.last_key = None; - } else { - status.last_key = new_key; - } - done - } - - DissolveCleanupPhase::SubnetRootDividendsRootClaimed => { - let done = Self::clean_up_root_claimed_for_subnet(netuid, weight_meter); - - if done { - status.set_phase(DissolveCleanupPhase::AlphaInOutStakesGetTotalAlphaValue); - status.last_key = None; - } - done - } - - DissolveCleanupPhase::AlphaInOutStakesGetTotalAlphaValue => { - let (done, new_key) = Self::destroy_alpha_in_out_stakes_get_total_alpha_value( - netuid, - weight_meter, - status.last_key.clone(), - status, - ); - if done { - status.subnet_distributed_tao = Some(0); - status.set_phase(DissolveCleanupPhase::AlphaInOutStakesSettleStakes); - status.last_key = None; - weight_meter.consume(T::DbWeight::get().writes(2)); - } else { - status.last_key = new_key; - } - done - } - - DissolveCleanupPhase::AlphaInOutStakesSettleStakes => { - let (done, new_key) = Self::destroy_alpha_in_out_stakes_settle_stakes( - netuid, - weight_meter, - status.last_key.clone(), - status, - ); - if done { - status.set_phase(DissolveCleanupPhase::AlphaInOutStakesAlpha); - status.last_key = None; - } else { - status.last_key = new_key; - } - done - } - - DissolveCleanupPhase::AlphaInOutStakesAlpha => { - let (done, new_key) = Self::destroy_alpha_in_out_stakes_clean_alpha( - netuid, - weight_meter, - status.last_key.clone(), - ); - if done { - status.set_phase(DissolveCleanupPhase::AlphaInOutStakesHotkeyTotals); - status.last_key = None; - } else { - status.last_key = new_key; - } - done - } - - DissolveCleanupPhase::AlphaInOutStakesHotkeyTotals => { - let (done, new_key) = Self::destroy_alpha_in_out_stakes_clear_hotkey_totals( - netuid, - weight_meter, - status.last_key.clone(), - ); - - if done { - status.set_phase(DissolveCleanupPhase::AlphaInOutStakesLocks); - status.last_key = None; - } else { - status.last_key = new_key; - } - done - } - - DissolveCleanupPhase::AlphaInOutStakesLocks => { - let (done, new_key) = Self::destroy_alpha_in_out_stakes_clear_locks( - netuid, - weight_meter, - status.last_key.clone(), - ); - if done { - status.set_phase(DissolveCleanupPhase::AlphaInOutStakesDecayingLocks); - status.last_key = None; - } else { - status.last_key = new_key; - } - done - } - DissolveCleanupPhase::AlphaInOutStakesDecayingLocks => { - let (done, new_key) = Self::destroy_alpha_in_out_stakes_clear_decaying_locks( - netuid, - weight_meter, - status.last_key.clone(), - ); - if done { - status.set_phase(DissolveCleanupPhase::AlphaInOutStakes); - status.last_key = None; - } else { - status.last_key = new_key; - } - done - } - - DissolveCleanupPhase::AlphaInOutStakes => { - let done = Self::destroy_alpha_in_out_stakes(netuid, weight_meter, status); - if done { - status.set_phase(DissolveCleanupPhase::ProtocolLiquidity); - status.last_key = None; - } - done - } - - DissolveCleanupPhase::ProtocolLiquidity => { - let done = T::SwapInterface::clear_protocol_liquidity(netuid, weight_meter); - - if done { - status.set_phase(DissolveCleanupPhase::PurgeNetuid); - status.last_key = None; - } - done - } - - DissolveCleanupPhase::PurgeNetuid => { - let done = T::CommitmentsInterface::purge_netuid(netuid, weight_meter); - - if done { - status.set_phase(DissolveCleanupPhase::NetworkIsNetworkMember); - status.last_key = None; - } - done - } - DissolveCleanupPhase::NetworkIsNetworkMember => { - let (done, new_key) = Self::remove_network_is_network_member( - netuid, - weight_meter, - status.last_key.clone(), - ); - - if done { - status.set_phase(DissolveCleanupPhase::NetworkParameters); - status.last_key = None; - } else { - status.last_key = new_key; - } - done - } - DissolveCleanupPhase::NetworkParameters => { - let done = Self::remove_network_parameters(netuid, weight_meter); - - if done { - status.set_phase(DissolveCleanupPhase::NetworkMapParameters); - status.last_key = None; - } - done - } - DissolveCleanupPhase::NetworkMapParameters => { - let done = Self::remove_network_map_parameters(netuid, weight_meter); - - if done { - status.set_phase(DissolveCleanupPhase::NetworkUpdateWeightsOnRoot); - status.last_key = None; - } - done - } - DissolveCleanupPhase::NetworkUpdateWeightsOnRoot => { - let (done, new_key) = Self::remove_network_update_weights_on_root( - netuid, - weight_meter, - status.last_key.clone(), - ); - - if done { - status.set_phase(DissolveCleanupPhase::NetworkChildkeyTake); - status.last_key = None; - } else { - status.last_key = new_key; - } - done - } - DissolveCleanupPhase::NetworkChildkeyTake => { - let (done, new_key) = Self::remove_network_childkey_take( - netuid, - weight_meter, - status.last_key.clone(), - ); - - if done { - status.set_phase(DissolveCleanupPhase::NetworkChildkeys); - status.last_key = None; - } else { - status.last_key = new_key; - } - done - } - DissolveCleanupPhase::NetworkChildkeys => { - let (done, new_key) = Self::remove_network_childkeys( - netuid, - weight_meter, - status.last_key.clone(), - ); - - if done { - status.set_phase(DissolveCleanupPhase::NetworkParentkeys); - status.last_key = None; - } else { - status.last_key = new_key; - } - done - } - DissolveCleanupPhase::NetworkParentkeys => { - let (done, new_key) = Self::remove_network_parentkeys( - netuid, - weight_meter, - status.last_key.clone(), - ); - - if done { - status.set_phase(DissolveCleanupPhase::NetworkLastHotkeyEmissionOnNetuid); - status.last_key = None; - } else { - status.last_key = new_key; - } - done - } - DissolveCleanupPhase::NetworkLastHotkeyEmissionOnNetuid => { - let (done, new_key) = Self::remove_network_last_hotkey_emission_on_netuid( - netuid, - weight_meter, - status.last_key.clone(), - ); - - if done { - status.set_phase(DissolveCleanupPhase::NetworkTotalHotkeyAlphaLastEpoch); - status.last_key = None; - } else { - status.last_key = new_key; - } - done - } - DissolveCleanupPhase::NetworkTotalHotkeyAlphaLastEpoch => { - let (done, new_key) = Self::remove_network_total_hotkey_alpha_last_epoch( - netuid, - weight_meter, - status.last_key.clone(), - ); - - if done { - status.set_phase(DissolveCleanupPhase::NetworkTransactionKeyLastBlock); - status.last_key = None; - } else { - status.last_key = new_key; - } - done - } - DissolveCleanupPhase::NetworkTransactionKeyLastBlock => { - let (done, new_key) = Self::remove_network_transaction_key_last_block( - netuid, - weight_meter, - status.last_key.clone(), - ); - if done { - status.set_phase(DissolveCleanupPhase::NetworkLock); - status.last_key = None; - } else { - status.last_key = new_key; - } - done - } - DissolveCleanupPhase::NetworkLock => { - let (done, new_key) = - Self::remove_network_lock(netuid, weight_meter, status.last_key.clone()); - - if done { - status.set_phase(DissolveCleanupPhase::NetworkDecayingLock); - status.last_key = None; - } else { - status.last_key = new_key; - } - done - } - DissolveCleanupPhase::NetworkDecayingLock => { - let (done, new_key) = Self::remove_network_decaying_lock( - netuid, - weight_meter, - status.last_key.clone(), - ); - - // if all phases are done, remove the network from the dissolved networks list and emit the event - if done { - cleanup_completed = true; - } else { - status.last_key = new_key; - } - done - } - }; - - phase_done = done; - - if cleanup_completed { - Self::deposit_event(Event::NetworkDissolveCleanupCompleted { netuid }); - break; - } - - CurrentDissolveCleanupStatus::::set(Some(status.clone())); - } - - (cleanup_completed, weight_meter.consumed()) - } - - pub fn process_network_registration_queue() -> Weight { - let db_weight = T::DbWeight::get(); - let queue = NetworkRegistrationQueue::::get(); - let mut weight = db_weight.reads(1); - - for (index, info) in queue.iter().enumerate() { - // just complete one registration at a time since on_idle just complete one network dissolve cleanup - // if one registration fails, then try next one. it could be not align with the order of registration in the queue - match Self::set_new_network_state( - &info.coldkey, - &info.hotkey, - info.mechid, - info.identity.clone(), - info.lock_amount, - info.median_subnet_alpha_price, - Some(info.lock_id), - ) { - Ok(post_info) => { - NetworkRegistrationQueue::::mutate(|queue| queue.remove(index)); - weight.saturating_accrue(db_weight.reads_writes(1, 1)); - weight.saturating_accrue(post_info.actual_weight.unwrap_or_else(Weight::zero)); - return weight; - } - Err(_) => { - log::error!( - "Failed to set new network state for coldkey: {:?}, hotkey: {:?}", - info.coldkey, - info.hotkey - ); - continue; - } - } - } - - weight - } -} diff --git a/pallets/subtensor/src/subnets/dissolution/cleanup_status.rs b/pallets/subtensor/src/subnets/dissolution/cleanup_status.rs new file mode 100644 index 0000000000..f0197b1284 --- /dev/null +++ b/pallets/subtensor/src/subnets/dissolution/cleanup_status.rs @@ -0,0 +1,95 @@ +//! Dissolve cleanup phase machine and persisted resume status. +//! +//! [`DissolveCleanupStatus`] is stored in [`CurrentDissolveCleanupStatus`] so +//! weight-metered dissolve cleanup can resume across blocks. + +use super::*; +use subtensor_runtime_common::NetUid; + +/// Ordered phases of multi-block dissolve cleanup for one `netuid`. +/// +/// Variant order is part of the on-chain encoding of [`DissolveCleanupStatus`]; +/// do not reorder. +#[derive(Encode, Decode, TypeInfo, Clone, PartialEq, Eq, Debug, DecodeWithMemTracking)] +pub enum DissolveCleanupPhase { + /// Phase 1.1: Remove root dividend claimable entries for the subnet. + SubnetRootDividendsRootClaimable, + /// Phase 1.2: Remove root dividend claimed entries for the subnet. + SubnetRootDividendsRootClaimed, + /// Phase 2.1: Get the total alpha value for the subnet. + AlphaInOutStakesGetTotalAlphaValue, + /// Phase 2.2: Destroy alpha in and out stakes for the subnet. + AlphaInOutStakesSettleStakes, + /// Phase 2.3: Clean alpha entries for the subnet. + AlphaInOutStakesAlpha, + /// Phase 2.4: Clear hotkey totals for the subnet. + AlphaInOutStakesHotkeyTotals, + /// Phase 2.5: Clear locks for the subnet. + AlphaInOutStakesLocks, + /// Phase 2.6: Clear locks for the subnet. + AlphaInOutStakesDecayingLocks, + /// Phase 2.7: Destroy alpha in and out stakes for the subnet. + AlphaInOutStakes, + /// Phase 3: Clear protocol liquidity for the subnet on the swap layer. + ProtocolLiquidity, + /// Phase 4: Remove scalar `Network*` parameters, then continue with map and index cleanup phases. + PurgeNetuid, + /// Phase 5.1: Remove is network member entries for the subnet. + NetworkIsNetworkMember, + /// Phase 5.2: Recovery / legacy: scalar `Network*` removal; the hook advances to map cleanup like `PurgeNetuid` after `remove_network_parameters` completes. + NetworkParameters, + /// Phase 5.3: Remove map-backed subnet storage (keys, axons, per-mechanism weights, etc.). + NetworkMapParameters, + /// Phase 5.4: Clear root-network weight entries referencing this netuid. + NetworkUpdateWeightsOnRoot, + /// Phase 5.5: Remove childkey take entries for this netuid. + NetworkChildkeyTake, + /// Phase 5.6: Remove child key bindings for this netuid. + NetworkChildkeys, + /// Phase 5.7: Remove parent key bindings for this netuid. + NetworkParentkeys, + /// Phase 5.8: Remove last hotkey emission records for this netuid. + NetworkLastHotkeyEmissionOnNetuid, + /// Phase 5.9: Remove total hotkey alpha last epoch entries for this netuid. + NetworkTotalHotkeyAlphaLastEpoch, + /// Phase 5.10: Remove transaction key last-block rate limit entries for this netuid. + NetworkTransactionKeyLastBlock, + /// Phase 5.11: Remove lock entries for this netuid. + NetworkLock, + /// Phase 5.12: Remove decaying lock entries for this netuid. + NetworkDecayingLock, +} + +impl Default for DissolveCleanupPhase { + fn default() -> Self { + Self::SubnetRootDividendsRootClaimable + } +} + +#[crate::freeze_struct("c524ea54893ae91a")] +#[derive(Encode, Decode, TypeInfo, Clone, PartialEq, Eq, Debug, DecodeWithMemTracking)] +pub struct DissolveCleanupStatus { + pub netuid: NetUid, + pub phase: DissolveCleanupPhase, + pub last_key: Option>, + pub subnet_total_alpha_value: Option, + pub subnet_distributed_tao: Option, +} + +impl DissolveCleanupStatus { + /// Start cleanup for `netuid` at the first phase with empty resume cursor. + pub fn new(netuid: NetUid) -> Self { + Self { + netuid, + phase: DissolveCleanupPhase::default(), + last_key: None, + subnet_total_alpha_value: None, + subnet_distributed_tao: None, + } + } + + /// Advance to `phase` (caller clears `last_key` when entering a new map walk). + pub fn set_phase(&mut self, phase: DissolveCleanupPhase) { + self.phase = phase; + } +} diff --git a/pallets/subtensor/src/subnets/dissolution/mod.rs b/pallets/subtensor/src/subnets/dissolution/mod.rs new file mode 100644 index 0000000000..e443cc639d --- /dev/null +++ b/pallets/subtensor/src/subnets/dissolution/mod.rs @@ -0,0 +1,159 @@ +//! Subnet dissolution and deferred network-registration queue. +//! +//! Search anchors: +//! - `do_dissolve_network` — queue a subnet for multi-block storage cleanup +//! - `remove_data_for_dissolved_networks` — `on_idle` orchestrator over the dissolve queue +//! - `clean_up_data_for_one_dissolved_network` — phase machine for one netuid +//! - `process_network_registration_queue` — register one queued network when a slot frees +//! - [`DissolveCleanupPhase`] / [`DissolveCleanupStatus`] — persisted resume state +//! - `remove_network_*` — weight-metered storage purge helpers + +mod cleanup_status; +mod phased_cleanup; +mod purge_network_storage; + +pub use cleanup_status::{DissolveCleanupPhase, DissolveCleanupStatus}; + +use super::*; +use subtensor_runtime_common::NetUid; +use subtensor_swap_interface::SwapHandler; + +impl Pallet { + /// Mark `netuid` dissolved: remove from [`NetworksAdded`] and enqueue cleanup. + /// + /// Storage wipe continues asynchronously via + /// [`Self::remove_data_for_dissolved_networks`] / [`Self::clean_up_data_for_one_dissolved_network`]. + /// Emits [`Event::NetworkRemoved`]. Fails if the subnet is missing, is root, or + /// is already queued. + pub fn do_dissolve_network(netuid: NetUid) -> dispatch::DispatchResult { + // --- The network exists? + ensure!( + Self::subnet_exists(netuid) && netuid != NetUid::ROOT, + Error::::SubnetNotExists + ); + + // Since TotalStake is updated on this level, purge reservoirs here into reserves and TotalStake + let reservoir_tao = T::SwapInterface::protocol_tao_reservoir(netuid); + let reservoir_alpha = T::SwapInterface::protocol_alpha_reservoir(netuid); + T::SwapInterface::clear_protocol_liquidity_reservoirs(netuid); + Self::increase_provided_tao_reserve(netuid, reservoir_tao); + Self::increase_provided_alpha_reserve(netuid, reservoir_alpha); + if !reservoir_tao.is_zero() { + TotalStake::::mutate(|total| { + *total = total.saturating_add(reservoir_tao); + }); + } + + let mut dissolved_networks = DissolveCleanupQueue::::get(); + ensure!( + !dissolved_networks.contains(&netuid), + Error::::NetworkDissolveAlreadyQueued + ); + + // Just remove the network from the added networks, it is used to check if the network is existed. + NetworksAdded::::remove(netuid); + // Reduce the total networks count. + TotalNetworks::::mutate(|n: &mut u16| *n = n.saturating_sub(1)); + TotalStake::::mutate(|total| *total = total.saturating_sub(SubnetTAO::::get(netuid))); + + dissolved_networks.push(netuid); + DissolveCleanupQueue::::set(dissolved_networks); + + log::debug!("NetworkRemoved( netuid:{netuid:?} )"); + + // --- Emit the NetworkRemoved event + Self::deposit_event(Event::NetworkRemoved(netuid)); + + Ok(()) + } + + /// `on_idle` entry: resume or start dissolve cleanup within `remaining_weight`. + pub fn remove_data_for_dissolved_networks(remaining_weight: Weight) -> Weight { + let w = T::DbWeight::get().writes(1); + let r = T::DbWeight::get().reads(1); + let mut weight_meter = frame_support::weights::WeightMeter::with_limit(remaining_weight); + + // complete unfinished network cleanup at first if any + if let Some(mut status) = CurrentDissolveCleanupStatus::::get() { + let (cleanup_completed, weight) = + Self::clean_up_data_for_one_dissolved_network(&mut weight_meter, &mut status); + if cleanup_completed { + DissolveCleanupQueue::::mutate(|queue| { + queue.retain(|queued_netuid| *queued_netuid != status.netuid); + }); + CurrentDissolveCleanupStatus::::kill(); + return weight.saturating_add(T::DbWeight::get().writes(2)); + } + return weight; + } + + if !weight_meter.can_consume(r) { + return weight_meter.consumed(); + } + weight_meter.consume(r); + + let dissolved_networks = DissolveCleanupQueue::::get(); + if let Some(netuid) = dissolved_networks.first() { + if !weight_meter.can_consume(w) { + return weight_meter.consumed(); + } + weight_meter.consume(w); + + let mut status = DissolveCleanupStatus::new(*netuid); + CurrentDissolveCleanupStatus::::set(Some(status.clone())); + + let (cleanup_completed, _weight) = + Self::clean_up_data_for_one_dissolved_network(&mut weight_meter, &mut status); + + if cleanup_completed { + DissolveCleanupQueue::::mutate(|queue| { + queue.retain(|queued_netuid| *queued_netuid != status.netuid); + }); + CurrentDissolveCleanupStatus::::kill(); + weight_meter.consume(T::DbWeight::get().writes(2)); + } + } + + weight_meter.consumed() + } + + // try use all weight available to clean up data for one dissolved network based on the status + + /// Try to finalize one queued [`NetworkRegistrationInfo`] after a dissolve frees a slot. + pub fn process_network_registration_queue() -> Weight { + let db_weight = T::DbWeight::get(); + let queue = NetworkRegistrationQueue::::get(); + let mut weight = db_weight.reads(1); + + for (index, info) in queue.iter().enumerate() { + // just complete one registration at a time since on_idle just complete one network dissolve cleanup + // if one registration fails, then try next one. it could be not align with the order of registration in the queue + match Self::set_new_network_state( + &info.coldkey, + &info.hotkey, + info.mechid, + info.identity.clone(), + info.lock_amount, + info.median_subnet_alpha_price, + Some(info.lock_id), + ) { + Ok(post_info) => { + NetworkRegistrationQueue::::mutate(|queue| queue.remove(index)); + weight.saturating_accrue(db_weight.reads_writes(1, 1)); + weight.saturating_accrue(post_info.actual_weight.unwrap_or_else(Weight::zero)); + return weight; + } + Err(_) => { + log::error!( + "Failed to set new network state for coldkey: {:?}, hotkey: {:?}", + info.coldkey, + info.hotkey + ); + continue; + } + } + } + + weight + } +} diff --git a/pallets/subtensor/src/subnets/dissolution/phased_cleanup.rs b/pallets/subtensor/src/subnets/dissolution/phased_cleanup.rs new file mode 100644 index 0000000000..ef59c7b1c5 --- /dev/null +++ b/pallets/subtensor/src/subnets/dissolution/phased_cleanup.rs @@ -0,0 +1,363 @@ +//! Phased dissolve cleanup for a single netuid. +//! +//! Advances [`DissolveCleanupStatus::phase`] until all subnet storage for that +//! netuid is gone or the weight meter is exhausted. + +use super::*; +use frame_support::weights::WeightMeter; +use subtensor_swap_interface::SwapHandler; + +impl Pallet { + /// Run dissolve cleanup phases for `status.netuid` until done or weight exhausted. + pub fn clean_up_data_for_one_dissolved_network( + weight_meter: &mut WeightMeter, + status: &mut DissolveCleanupStatus, + ) -> (bool, Weight) { + let r = T::DbWeight::get().reads(1); + + let netuid = status.netuid; + + if !weight_meter.can_consume(r) { + return (false, weight_meter.consumed()); + } + + // if one phase is done or exit because of weight limit + let mut phase_done = true; + let mut cleanup_completed = false; + // only reason for phase_done to be false is if the weight limit is reached + while phase_done { + // let phase = status.phase.clone(); + log::debug!( + "dissolved_networks phase: {:?} for netuid: {:?}", + &status.phase, + netuid + ); + + let done = match &status.phase { + DissolveCleanupPhase::SubnetRootDividendsRootClaimable => { + let (done, new_key) = Self::clean_up_root_claimable_for_subnet( + netuid, + weight_meter, + status.last_key.clone(), + ); + + if done { + status.set_phase(DissolveCleanupPhase::SubnetRootDividendsRootClaimed); + status.last_key = None; + } else { + status.last_key = new_key; + } + done + } + + DissolveCleanupPhase::SubnetRootDividendsRootClaimed => { + let done = Self::clean_up_root_claimed_for_subnet(netuid, weight_meter); + + if done { + status.set_phase(DissolveCleanupPhase::AlphaInOutStakesGetTotalAlphaValue); + status.last_key = None; + } + done + } + + DissolveCleanupPhase::AlphaInOutStakesGetTotalAlphaValue => { + let (done, new_key) = Self::destroy_alpha_in_out_stakes_get_total_alpha_value( + netuid, + weight_meter, + status.last_key.clone(), + status, + ); + if done { + status.subnet_distributed_tao = Some(0); + status.set_phase(DissolveCleanupPhase::AlphaInOutStakesSettleStakes); + status.last_key = None; + weight_meter.consume(T::DbWeight::get().writes(2)); + } else { + status.last_key = new_key; + } + done + } + + DissolveCleanupPhase::AlphaInOutStakesSettleStakes => { + let (done, new_key) = Self::destroy_alpha_in_out_stakes_settle_stakes( + netuid, + weight_meter, + status.last_key.clone(), + status, + ); + if done { + status.set_phase(DissolveCleanupPhase::AlphaInOutStakesAlpha); + status.last_key = None; + } else { + status.last_key = new_key; + } + done + } + + DissolveCleanupPhase::AlphaInOutStakesAlpha => { + let (done, new_key) = Self::destroy_alpha_in_out_stakes_clean_alpha( + netuid, + weight_meter, + status.last_key.clone(), + ); + if done { + status.set_phase(DissolveCleanupPhase::AlphaInOutStakesHotkeyTotals); + status.last_key = None; + } else { + status.last_key = new_key; + } + done + } + + DissolveCleanupPhase::AlphaInOutStakesHotkeyTotals => { + let (done, new_key) = Self::destroy_alpha_in_out_stakes_clear_hotkey_totals( + netuid, + weight_meter, + status.last_key.clone(), + ); + + if done { + status.set_phase(DissolveCleanupPhase::AlphaInOutStakesLocks); + status.last_key = None; + } else { + status.last_key = new_key; + } + done + } + + DissolveCleanupPhase::AlphaInOutStakesLocks => { + let (done, new_key) = Self::destroy_alpha_in_out_stakes_clear_locks( + netuid, + weight_meter, + status.last_key.clone(), + ); + if done { + status.set_phase(DissolveCleanupPhase::AlphaInOutStakesDecayingLocks); + status.last_key = None; + } else { + status.last_key = new_key; + } + done + } + DissolveCleanupPhase::AlphaInOutStakesDecayingLocks => { + let (done, new_key) = Self::destroy_alpha_in_out_stakes_clear_decaying_locks( + netuid, + weight_meter, + status.last_key.clone(), + ); + if done { + status.set_phase(DissolveCleanupPhase::AlphaInOutStakes); + status.last_key = None; + } else { + status.last_key = new_key; + } + done + } + + DissolveCleanupPhase::AlphaInOutStakes => { + let done = Self::destroy_alpha_in_out_stakes(netuid, weight_meter, status); + if done { + status.set_phase(DissolveCleanupPhase::ProtocolLiquidity); + status.last_key = None; + } + done + } + + DissolveCleanupPhase::ProtocolLiquidity => { + let done = T::SwapInterface::clear_protocol_liquidity(netuid, weight_meter); + + if done { + status.set_phase(DissolveCleanupPhase::PurgeNetuid); + status.last_key = None; + } + done + } + + DissolveCleanupPhase::PurgeNetuid => { + let done = T::CommitmentsInterface::purge_netuid(netuid, weight_meter); + + if done { + status.set_phase(DissolveCleanupPhase::NetworkIsNetworkMember); + status.last_key = None; + } + done + } + DissolveCleanupPhase::NetworkIsNetworkMember => { + let (done, new_key) = Self::remove_network_is_network_member( + netuid, + weight_meter, + status.last_key.clone(), + ); + + if done { + status.set_phase(DissolveCleanupPhase::NetworkParameters); + status.last_key = None; + } else { + status.last_key = new_key; + } + done + } + DissolveCleanupPhase::NetworkParameters => { + let done = Self::remove_network_parameters(netuid, weight_meter); + + if done { + status.set_phase(DissolveCleanupPhase::NetworkMapParameters); + status.last_key = None; + } + done + } + DissolveCleanupPhase::NetworkMapParameters => { + let done = Self::remove_network_map_parameters(netuid, weight_meter); + + if done { + status.set_phase(DissolveCleanupPhase::NetworkUpdateWeightsOnRoot); + status.last_key = None; + } + done + } + DissolveCleanupPhase::NetworkUpdateWeightsOnRoot => { + let (done, new_key) = Self::remove_network_update_weights_on_root( + netuid, + weight_meter, + status.last_key.clone(), + ); + + if done { + status.set_phase(DissolveCleanupPhase::NetworkChildkeyTake); + status.last_key = None; + } else { + status.last_key = new_key; + } + done + } + DissolveCleanupPhase::NetworkChildkeyTake => { + let (done, new_key) = Self::remove_network_childkey_take( + netuid, + weight_meter, + status.last_key.clone(), + ); + + if done { + status.set_phase(DissolveCleanupPhase::NetworkChildkeys); + status.last_key = None; + } else { + status.last_key = new_key; + } + done + } + DissolveCleanupPhase::NetworkChildkeys => { + let (done, new_key) = Self::remove_network_childkeys( + netuid, + weight_meter, + status.last_key.clone(), + ); + + if done { + status.set_phase(DissolveCleanupPhase::NetworkParentkeys); + status.last_key = None; + } else { + status.last_key = new_key; + } + done + } + DissolveCleanupPhase::NetworkParentkeys => { + let (done, new_key) = Self::remove_network_parentkeys( + netuid, + weight_meter, + status.last_key.clone(), + ); + + if done { + status.set_phase(DissolveCleanupPhase::NetworkLastHotkeyEmissionOnNetuid); + status.last_key = None; + } else { + status.last_key = new_key; + } + done + } + DissolveCleanupPhase::NetworkLastHotkeyEmissionOnNetuid => { + let (done, new_key) = Self::remove_network_last_hotkey_emission_on_netuid( + netuid, + weight_meter, + status.last_key.clone(), + ); + + if done { + status.set_phase(DissolveCleanupPhase::NetworkTotalHotkeyAlphaLastEpoch); + status.last_key = None; + } else { + status.last_key = new_key; + } + done + } + DissolveCleanupPhase::NetworkTotalHotkeyAlphaLastEpoch => { + let (done, new_key) = Self::remove_network_total_hotkey_alpha_last_epoch( + netuid, + weight_meter, + status.last_key.clone(), + ); + + if done { + status.set_phase(DissolveCleanupPhase::NetworkTransactionKeyLastBlock); + status.last_key = None; + } else { + status.last_key = new_key; + } + done + } + DissolveCleanupPhase::NetworkTransactionKeyLastBlock => { + let (done, new_key) = Self::remove_network_transaction_key_last_block( + netuid, + weight_meter, + status.last_key.clone(), + ); + if done { + status.set_phase(DissolveCleanupPhase::NetworkLock); + status.last_key = None; + } else { + status.last_key = new_key; + } + done + } + DissolveCleanupPhase::NetworkLock => { + let (done, new_key) = + Self::remove_network_lock(netuid, weight_meter, status.last_key.clone()); + + if done { + status.set_phase(DissolveCleanupPhase::NetworkDecayingLock); + status.last_key = None; + } else { + status.last_key = new_key; + } + done + } + DissolveCleanupPhase::NetworkDecayingLock => { + let (done, new_key) = Self::remove_network_decaying_lock( + netuid, + weight_meter, + status.last_key.clone(), + ); + + // if all phases are done, remove the network from the dissolved networks list and emit the event + if done { + cleanup_completed = true; + } else { + status.last_key = new_key; + } + done + } + }; + + phase_done = done; + + if cleanup_completed { + Self::deposit_event(Event::NetworkDissolveCleanupCompleted { netuid }); + break; + } + + CurrentDissolveCleanupStatus::::set(Some(status.clone())); + } + + (cleanup_completed, weight_meter.consumed()) + } +} diff --git a/pallets/subtensor/src/subnets/dissolution/purge_network_storage.rs b/pallets/subtensor/src/subnets/dissolution/purge_network_storage.rs new file mode 100644 index 0000000000..ae2fff78f2 --- /dev/null +++ b/pallets/subtensor/src/subnets/dissolution/purge_network_storage.rs @@ -0,0 +1,470 @@ +//! Weight-metered deletion of per-`netuid` storage during dissolve cleanup. +//! +//! Each helper returns `false` / incomplete when the [`WeightMeter`] cannot +//! cover another read or write so the phased cleanup can resume next block. + +use super::*; +use frame_support::weights::WeightMeter; +use subtensor_runtime_common::{NetUid, NetUidStorageIndex, clear_prefix_with_meter}; + +impl Pallet { + /// Clear map-backed subnet storage (keys, axons, mechanism weights, leases, …). + pub fn remove_network_map_parameters(netuid: NetUid, weight_meter: &mut WeightMeter) -> bool { + let write_weight = T::DbWeight::get().writes(1); + + let result = clear_prefix_with_meter(weight_meter, write_weight, |limit| { + Keys::::clear_prefix(netuid, limit, None) + }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { + Uids::::clear_prefix(netuid, limit, None) + }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { + BlockAtRegistration::::clear_prefix(netuid, limit, None) + }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { + Axons::::clear_prefix(netuid, limit, None) + }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { + NeuronCertificates::::clear_prefix(netuid, limit, None) + }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { + Prometheus::::clear_prefix(netuid, limit, None) + }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { + AlphaDividendsPerSubnet::::clear_prefix(netuid, limit, None) + }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { + PendingChildKeys::::clear_prefix(netuid, limit, None) + }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { + AssociatedEvmAddress::::clear_prefix(netuid, limit, None) + }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { + AssociatedUidsByEvmAddress::::clear_prefix(netuid, limit, None) + }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { + HotkeyLock::::clear_prefix(netuid, limit, None) + }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { + DecayingHotkeyLock::::clear_prefix(netuid, limit, None) + }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { + LockingColdkeys::::clear_prefix((netuid,), limit, None) + }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { + // Lock metadata only. Alpha (including collateral stake) was already + // pro-rata converted to coldkey free TAO in AlphaInOutStakesSettleStakes; + // unlocking here would double-pay. Clearing drops the now-meaningless + // MinerCollateral rows for the dissolved netuid. + MinerCollateral::::clear_prefix((netuid,), limit, None) + }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { + ColdkeyMinerCollateral::::clear_prefix(netuid, limit, None) + }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { + ColdkeyCollateralHotkeys::::clear_prefix(netuid, limit, None) + }); + + if !result { + return false; + } + + let read_weight = T::DbWeight::get().reads(1); + if !weight_meter.can_consume(read_weight) { + return false; + } + weight_meter.consume(read_weight); + let mechanisms: u8 = MechanismCountCurrent::::get(netuid).into(); + + for subid in 0..mechanisms { + let mechanism_weight = T::DbWeight::get().reads_writes(1, 2); + if !weight_meter.can_consume(mechanism_weight) { + return false; + } + weight_meter.consume(mechanism_weight); + let netuid_index = Self::get_mechanism_storage_index(netuid, subid.into()); + + LastUpdate::::remove(netuid_index); + Incentive::::remove(netuid_index); + + let result = clear_prefix_with_meter(weight_meter, write_weight, |limit| { + WeightCommits::::clear_prefix(netuid_index, limit, None) + }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { + TimelockedWeightCommits::::clear_prefix(netuid_index, limit, None) + }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { + CRV3WeightCommits::::clear_prefix(netuid_index, limit, None) + }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { + CRV3WeightCommitsV2::::clear_prefix(netuid_index, limit, None) + }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { + Bonds::::clear_prefix(netuid_index, limit, None) + }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { + Weights::::clear_prefix(netuid_index, limit, None) + }); + + if !result { + return false; + } + } + + let removal_weight = T::DbWeight::get().writes(3); + if !weight_meter.can_consume(removal_weight) { + return false; + } + weight_meter.consume(removal_weight); + RevealPeriodEpochs::::remove(netuid); + MechanismCountCurrent::::remove(netuid); + MechanismEmissionSplit::::remove(netuid); + + if !clear_prefix_with_meter(weight_meter, write_weight, |limit| { + LastHotkeySwapOnNetuid::::clear_prefix(netuid, limit, None) + }) || !clear_prefix_with_meter(weight_meter, write_weight, |limit| { + HotkeySuccessor::::clear_prefix(netuid, limit, None) + }) || !clear_prefix_with_meter(weight_meter, write_weight, |limit| { + HotkeyRoot::::clear_prefix(netuid, limit, None) + }) { + return false; + } + + if let Some(lease_id) = SubnetUidToLeaseId::::get(netuid) { + if !clear_prefix_with_meter(weight_meter, write_weight, |limit| { + SubnetLeaseShares::::clear_prefix(lease_id, limit, None) + }) { + return false; + } + let lease_weight = T::DbWeight::get().writes(3); + if !weight_meter.can_consume(lease_weight) { + return false; + } + weight_meter.consume(lease_weight); + SubnetLeases::::remove(lease_id); + AccumulatedLeaseDividends::::remove(lease_id); + SubnetUidToLeaseId::::remove(netuid); + } + + true + } + + /// Remove scalar per-netuid hyperparams and identity; returns false if weight insufficient. + pub fn remove_network_parameters(netuid: NetUid, weight_meter: &mut WeightMeter) -> bool { + // Flat write charge for the `::remove(netuid)` list below. Bump this when + // adding or removing entries from that list so the weight stays in step. + let removal_weight = T::DbWeight::get().writes(82); + if !weight_meter.can_consume(removal_weight) { + return false; + } + weight_meter.consume(removal_weight); + SubnetOwner::::remove(netuid); + SubnetworkN::::remove(netuid); + NetworkRegisteredAt::::remove(netuid); + Active::::remove(netuid); + Emission::::remove(netuid); + Consensus::::remove(netuid); + Dividends::::remove(netuid); + ValidatorPermit::::remove(netuid); + ValidatorTrust::::remove(netuid); + Tempo::::remove(netuid); + Kappa::::remove(netuid); + Difficulty::::remove(netuid); + MaxAllowedUids::::remove(netuid); + ImmunityPeriod::::remove(netuid); + ActivityCutoff::::remove(netuid); + MinAllowedWeights::::remove(netuid); + RegistrationsThisInterval::::remove(netuid); + POWRegistrationsThisInterval::::remove(netuid); + BurnRegistrationsThisInterval::::remove(netuid); + SubnetAlphaInEmission::::remove(netuid); + SubnetAlphaOutEmission::::remove(netuid); + SubnetTaoInEmission::::remove(netuid); + SubnetVolume::::remove(netuid); + SubnetMovingPrice::::remove(netuid); + SubnetTaoFlow::::remove(netuid); + SubnetEmaTaoFlow::::remove(netuid); + SubnetProtocolFlow::::remove(netuid); + SubnetEmaProtocolFlow::::remove(netuid); + SubnetExcessTao::::remove(netuid); + SubnetRootSellTao::::remove(netuid); + TokenSymbol::::remove(netuid); + SubnetMechanism::::remove(netuid); + SubnetOwnerHotkey::::remove(netuid); + NetworkRegistrationAllowed::::remove(netuid); + NetworkPowRegistrationAllowed::::remove(netuid); + TransferToggle::::remove(netuid); + SubnetLocked::::remove(netuid); + LargestLocked::::remove(netuid); + FirstEmissionBlockNumber::::remove(netuid); + PendingValidatorEmission::::remove(netuid); + PendingServerEmission::::remove(netuid); + PendingRootAlphaDivs::::remove(netuid); + PendingOwnerCut::::remove(netuid); + MinerBurned::::remove(netuid); + BlocksSinceLastStep::::remove(netuid); + LastMechansimStepBlock::::remove(netuid); + LastAdjustmentBlock::::remove(netuid); + ServingRateLimit::::remove(netuid); + Rho::::remove(netuid); + AlphaSigmoidSteepness::::remove(netuid); + MaxAllowedValidators::::remove(netuid); + BondsMovingAverage::::remove(netuid); + BondsPenalty::::remove(netuid); + BondsResetOn::::remove(netuid); + WeightsSetRateLimit::::remove(netuid); + ValidatorPruneLen::::remove(netuid); + ScalingLawPower::::remove(netuid); + TargetRegistrationsPerInterval::::remove(netuid); + CommitRevealWeightsEnabled::::remove(netuid); + BurnHalfLife::::remove(netuid); + BurnIncreaseMult::::remove(netuid); + CollateralLockShare::::remove(netuid); + CollateralDrainRatio::::remove(netuid); + Burn::::remove(netuid); + MinBurn::::remove(netuid); + MaxBurn::::remove(netuid); + MinDifficulty::::remove(netuid); + MaxDifficulty::::remove(netuid); + RegistrationsThisBlock::::remove(netuid); + EMAPriceHalvingBlocks::::remove(netuid); + RAORecycledForRegistration::::remove(netuid); + MaxRegistrationsPerBlock::::remove(netuid); + WeightsVersionKey::::remove(netuid); + LiquidAlphaOn::::remove(netuid); + Yuma3On::::remove(netuid); + AlphaValues::::remove(netuid); + SubtokenEnabled::::remove(netuid); + OwnerCutAutoLockEnabled::::remove(netuid); + ImmuneOwnerUidsLimit::::remove(netuid); + StakeWeight::::remove(netuid); + LoadedEmission::::remove(netuid); + OwnerLock::::remove(netuid); + DecayingOwnerLock::::remove(netuid); + ActivityCutoffFactorMilli::::remove(netuid); + LastEpochBlock::::remove(netuid); + PendingEpochAt::::remove(netuid); + SubnetEpochIndex::::remove(netuid); + + if SubnetIdentitiesV3::::contains_key(netuid) { + SubnetIdentitiesV3::::remove(netuid); + Self::deposit_event(Event::SubnetIdentityRemoved(netuid)); + } + true + } + + /// Clear [`IsNetworkMember`] rows for hotkeys that belonged to `netuid`. + pub fn remove_network_is_network_member( + netuid: NetUid, + weight_meter: &mut WeightMeter, + last_key: Option>, + ) -> (bool, Option>) { + let iter = match last_key { + Some(raw_key) => Keys::::iter_from(raw_key), + None => Keys::::iter(), + }; + + let (read_all, last_item) = Self::remove_storage_entries_for_netuid( + weight_meter, + iter, + |(nu, _, _)| *nu == netuid, + |(_, _, hotkey)| hotkey, + |hotkey| IsNetworkMember::::remove(hotkey, netuid), + 1, + ); + + ( + read_all, + last_item.map(|(nu, uid, _)| Keys::::hashed_key_for(nu, uid)), + ) + } + + /// Zero root-network weight entries that pointed at the dissolved `netuid`. + pub fn remove_network_update_weights_on_root( + netuid: NetUid, + weight_meter: &mut WeightMeter, + last_key: Option>, + ) -> (bool, Option>) { + let netuid_u16 = u16::from(netuid); + + let root = NetUidStorageIndex::ROOT; + let iter = match last_key { + Some(raw_key) => Weights::::iter_prefix_from(root, raw_key), + None => Weights::::iter_prefix(root), + }; + + /// Zero weight entries targeting `netuid_u16`; returns whether any entry changed. + fn filter_root_weights_excluding_netuid( + netuid_u16: u16, + weights: &[(u16, u16)], + ) -> (bool, Vec<(u16, u16)>) { + let mut need_update = false; + let mut filtered_weights = weights.to_vec(); + for (subnet_id, weight) in filtered_weights.iter_mut() { + if *subnet_id == netuid_u16 && *weight != 0 { + need_update = true; + *weight = 0; + } + } + (need_update, filtered_weights) + } + + let (read_all, last_item) = Self::remove_storage_entries_for_netuid( + weight_meter, + iter, + |_| true, + |(uid, weights)| (uid, weights), + |(uid, weights)| { + let (update, filtered_weights) = + filter_root_weights_excluding_netuid(netuid_u16, weights); + if update { + Weights::::insert(root, *uid, filtered_weights); + } + }, + 1, + ); + + ( + read_all, + last_item.map(|key| Weights::::hashed_key_for(root, key.0)), + ) + } + + /// Clear [`ChildkeyTake`] entries for `netuid`. + pub fn remove_network_childkey_take( + netuid: NetUid, + weight_meter: &mut WeightMeter, + last_key: Option>, + ) -> (bool, Option>) { + let iter = match last_key { + Some(raw_key) => ChildkeyTake::::iter_from(raw_key), + None => ChildkeyTake::::iter(), + }; + + let (read_all, last_item) = Self::remove_storage_entries_for_netuid( + weight_meter, + iter, + |(_, nu, _)| *nu == netuid, + |(hot, _, _)| hot, + |hot| ChildkeyTake::::remove(hot, netuid), + 1, + ); + + ( + read_all, + last_item.map(|(hot, nu, _)| ChildkeyTake::::hashed_key_for(&hot, nu)), + ) + } + + /// Clear child-key bindings for `netuid`. + pub fn remove_network_childkeys( + netuid: NetUid, + weight_meter: &mut WeightMeter, + last_key: Option>, + ) -> (bool, Option>) { + let iter = match last_key { + Some(raw_key) => ChildKeys::::iter_from(raw_key), + None => ChildKeys::::iter(), + }; + + let (read_all, last_item) = Self::remove_storage_entries_for_netuid( + weight_meter, + iter, + |(_, nu, _)| *nu == netuid, + |(hot, _, _)| hot, + |hot| ChildKeys::::remove(hot, netuid), + 1, + ); + + ( + read_all, + last_item.map(|key| ChildKeys::::hashed_key_for(&key.0, key.1)), + ) + } + + /// Clear parent-key bindings for `netuid`. + pub fn remove_network_parentkeys( + netuid: NetUid, + weight_meter: &mut WeightMeter, + last_key: Option>, + ) -> (bool, Option>) { + let iter = match last_key { + Some(raw_key) => ParentKeys::::iter_from(raw_key), + None => ParentKeys::::iter(), + }; + + let (read_all, last_item) = Self::remove_storage_entries_for_netuid( + weight_meter, + iter, + |(_, nu, _)| *nu == netuid, + |(hot, _, _)| hot, + |hot| ParentKeys::::remove(hot, netuid), + 1, + ); + + ( + read_all, + last_item.map(|key| ParentKeys::::hashed_key_for(&key.0, key.1)), + ) + } + + /// Clear last hotkey emission records for `netuid`. + pub fn remove_network_last_hotkey_emission_on_netuid( + netuid: NetUid, + weight_meter: &mut WeightMeter, + last_key: Option>, + ) -> (bool, Option>) { + let iter = match last_key { + Some(raw_key) => LastHotkeyEmissionOnNetuid::::iter_from(raw_key), + None => LastHotkeyEmissionOnNetuid::::iter(), + }; + + let (read_all, last_item) = Self::remove_storage_entries_for_netuid( + weight_meter, + iter, + |(_, nu, _)| *nu == netuid, + |(hot, _, _)| hot, + |hot| LastHotkeyEmissionOnNetuid::::remove(hot, netuid), + 1, + ); + + ( + read_all, + last_item.map(|key| LastHotkeyEmissionOnNetuid::::hashed_key_for(&key.0, key.1)), + ) + } + + /// Clear last-epoch total hotkey alpha for `netuid`. + pub fn remove_network_total_hotkey_alpha_last_epoch( + netuid: NetUid, + weight_meter: &mut WeightMeter, + last_key: Option>, + ) -> (bool, Option>) { + let iter = match last_key { + Some(raw_key) => TotalHotkeyAlphaLastEpoch::::iter_from(raw_key), + None => TotalHotkeyAlphaLastEpoch::::iter(), + }; + + let (read_all, last_item) = Self::remove_storage_entries_for_netuid( + weight_meter, + iter, + |(_, nu, _)| *nu == netuid, + |(hot, _, _)| hot, + |hot| TotalHotkeyAlphaLastEpoch::::remove(hot, netuid), + 1, + ); + + ( + read_all, + last_item.map(|(hot, nu, _)| TotalHotkeyAlphaLastEpoch::::hashed_key_for(&hot, nu)), + ) + } + + /// Clear per-hotkey transaction rate-limit stamps for `netuid`. + pub fn remove_network_transaction_key_last_block( + netuid: NetUid, + weight_meter: &mut WeightMeter, + last_key: Option>, + ) -> (bool, Option>) { + let iter = match last_key { + Some(raw_key) => TransactionKeyLastBlock::::iter_from(raw_key), + None => TransactionKeyLastBlock::::iter(), + }; + + let (read_all, last_item) = Self::remove_storage_entries_for_netuid( + weight_meter, + iter, + |((_, nu, _), _)| *nu == netuid, + |((hot, _, name), _)| (hot, name), + |(hot, name)| TransactionKeyLastBlock::::remove((hot.clone(), netuid, *name)), + 1, + ); + + ( + read_all, + last_item.map(|((hot, _, name), _)| { + TransactionKeyLastBlock::::hashed_key_for((&hot, netuid, name)) + }), + ) + } +} diff --git a/pallets/subtensor/src/subnets/leasing.rs b/pallets/subtensor/src/subnets/leasing.rs index 3aa08b6db1..0bcc73965d 100644 --- a/pallets/subtensor/src/subnets/leasing.rs +++ b/pallets/subtensor/src/subnets/leasing.rs @@ -1,19 +1,11 @@ -//! This file defines abstraction for subnet leasing. +//! Crowdloan-backed subnet leasing. //! -//! It is used to register a new leased network through a crowdloan using the `register_leased_network` extrinsic -//! as a call parameter to the crowdloan pallet `create` extrinsic. A new subnet will be registered -//! paying the lock cost using the crowdloan funds and a proxy will be created for the beneficiary -//! to operate the subnet. -//! -//! The crowdloan's contributions are used to compute the share of the emissions that the contributors -//! will receive as dividends. The leftover cap is refunded to the contributors and the beneficiary. -//! -//! The lease can have a defined end block, after which the lease will be terminated and the subnet -//! will be transferred to the beneficiary. In case the lease is perpetual, the lease will never be -//! terminated and emissions will continue to be distributed to the contributors. -//! -//! The lease can be terminated by the beneficiary after the end block has passed (if any) and the subnet -//! ownership will be transferred to the beneficiary. +//! `register_leased_network` is invoked as the crowdloan finalize call: raised +//! funds pay the network lock, a deterministic lease coldkey/hotkey pair owns +//! the subnet, and the beneficiary receives a proxy. Contributor shares in +//! [`SubnetLeaseShares`] receive emission dividends until the lease ends (or +//! forever if perpetual). [`do_terminate_lease`] transfers ownership to the +//! beneficiary after `end_block`. use super::*; use crate::weights::WeightInfo; @@ -76,7 +68,7 @@ impl Pallet { let now = frame_system::Pallet::::block_number(); // Ensure the origin is the creator of the crowdloan - let (crowdloan_id, crowdloan) = Self::get_crowdloan_being_finalized()?; + let (crowdloan_id, crowdloan) = Self::crowdloan_being_finalized()?; ensure!( who == crowdloan.creator, Error::::InvalidLeaseBeneficiary @@ -87,9 +79,9 @@ impl Pallet { } // Initialize the lease id, coldkey and hotkey and keep track of them - let lease_id = Self::get_next_lease_id()?; - let lease_coldkey = Self::lease_coldkey(lease_id)?; - let lease_hotkey = Self::lease_hotkey(lease_id)?; + let lease_id = Self::allocate_next_lease_id()?; + let lease_coldkey = Self::derive_lease_coldkey(lease_id)?; + let lease_hotkey = Self::derive_lease_hotkey(lease_id)?; frame_system::Pallet::::inc_providers(&lease_coldkey); frame_system::Pallet::::inc_providers(&lease_hotkey); @@ -102,8 +94,8 @@ impl Pallet { None, )?; - let netuid = - Self::find_lease_netuid(&lease_coldkey).ok_or(Error::::LeaseNetuidNotFound)?; + let netuid = Self::find_netuid_for_lease_coldkey(&lease_coldkey) + .ok_or(Error::::LeaseNetuidNotFound)?; // Enable the beneficiary to operate the subnet through a proxy T::ProxyInterface::add_lease_beneficiary_proxy(&lease_coldkey, &who)?; @@ -336,19 +328,22 @@ impl Pallet { }; } - fn lease_coldkey(lease_id: LeaseId) -> Result { + /// Deterministic coldkey account derived from `("leasing/coldkey", lease_id)`. + fn derive_lease_coldkey(lease_id: LeaseId) -> Result { let entropy = ("leasing/coldkey", lease_id).using_encoded(blake2_256); T::AccountId::decode(&mut TrailingZeroInput::new(entropy.as_ref())) .map_err(|_| Error::::InvalidValue.into()) } - fn lease_hotkey(lease_id: LeaseId) -> Result { + /// Deterministic hotkey account derived from `("leasing/hotkey", lease_id)`. + fn derive_lease_hotkey(lease_id: LeaseId) -> Result { let entropy = ("leasing/hotkey", lease_id).using_encoded(blake2_256); T::AccountId::decode(&mut TrailingZeroInput::new(entropy.as_ref())) .map_err(|_| Error::::InvalidValue.into()) } - fn get_next_lease_id() -> Result> { + /// Take the next [`NextSubnetLeaseId`] value and advance the counter. + fn allocate_next_lease_id() -> Result> { let lease_id = NextSubnetLeaseId::::get(); // Increment the lease id @@ -358,7 +353,8 @@ impl Pallet { Ok(lease_id) } - fn find_lease_netuid(lease_coldkey: &T::AccountId) -> Option { + /// Find the subnet whose [`SubnetOwner`] is the derived lease coldkey. + fn find_netuid_for_lease_coldkey(lease_coldkey: &T::AccountId) -> Option { SubnetOwner::::iter() .find(|(_, coldkey)| coldkey == lease_coldkey) .map(|(netuid, _)| netuid) @@ -366,7 +362,8 @@ impl Pallet { // Get the crowdloan being finalized from the crowdloan pallet when the call is executed, // and the current crowdloan ID is exposed to us. - fn get_crowdloan_being_finalized() -> Result< + /// Crowdloan currently being finalized (`CurrentCrowdloanId` in pallet-crowdloan). + fn crowdloan_being_finalized() -> Result< ( pallet_crowdloan::CrowdloanId, pallet_crowdloan::CrowdloanInfoOf, diff --git a/pallets/subtensor/src/subnets/mechanism.rs b/pallets/subtensor/src/subnets/mechanism.rs index 30c4e90086..c960bde2bb 100644 --- a/pallets/subtensor/src/subnets/mechanism.rs +++ b/pallets/subtensor/src/subnets/mechanism.rs @@ -1,5 +1,7 @@ -//! This file contains all tooling to work with sub-subnets +//! Sub-subnet (mechanism) indexing, emission splits, and multi-mechanism epoch. //! +//! Mechanisms share a parent `netuid` but store weights/bonds/incentive under a +//! packed [`NetUidStorageIndex`] = `netuid + mech_id * GLOBAL_MAX_SUBNET_COUNT`. use super::*; use crate::epoch::run_epoch::EpochTerms; @@ -8,13 +10,6 @@ use safe_math::*; use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaBalance, MechId, NetUid, NetUidStorageIndex}; -pub type LeaseId = u32; - -pub type CurrencyOf = ::Currency; - -pub type BalanceOf = - as fungible::Inspect<::AccountId>>::Balance; - /// Theoretical maximum of subnets on bittensor. This value is used in indexed /// storage of epoch values for sub-subnets as /// @@ -32,6 +27,7 @@ pub const GLOBAL_MAX_SUBNET_COUNT: u16 = 4096; pub const MAX_MECHANISM_COUNT_PER_SUBNET: u8 = 16; impl Pallet { + /// Pack `(netuid, mechanism_id)` into the storage index used by weights/bonds maps. pub fn get_mechanism_storage_index(netuid: NetUid, sub_id: MechId) -> NetUidStorageIndex { u16::from(sub_id) .saturating_mul(GLOBAL_MAX_SUBNET_COUNT) @@ -39,7 +35,8 @@ impl Pallet { .into() } - pub fn get_netuid(netuid_index: NetUidStorageIndex) -> NetUid { + /// Extract parent `netuid` from a mechanism storage index (`index % GLOBAL_MAX_SUBNET_COUNT`). + pub fn netuid_from_mechanism_storage_index(netuid_index: NetUidStorageIndex) -> NetUid { if let Some(netuid) = u16::from(netuid_index).checked_rem(GLOBAL_MAX_SUBNET_COUNT) { NetUid::from(netuid) } else { @@ -48,6 +45,7 @@ impl Pallet { } } + /// Decode `(netuid, mech_id)` from a storage index and ensure that mechanism exists. pub fn get_netuid_and_subid( netuid_index: NetUidStorageIndex, ) -> Result<(NetUid, MechId), Error> { @@ -57,7 +55,7 @@ impl Pallet { // Make sure the base subnet exists ensure!( - Self::if_subnet_exist(netuid), + Self::subnet_exists(netuid), Error::::MechanismDoesNotExist ); @@ -76,14 +74,16 @@ impl Pallet { } } + /// Current mechanism count for `netuid` (defaults via storage to at least one). pub fn get_current_mechanism_count(netuid: NetUid) -> MechId { MechanismCountCurrent::::get(netuid) } + /// Error if `netuid` is missing or `sub_id` is outside `[0, mechanism_count)`. pub fn ensure_mechanism_exists(netuid: NetUid, sub_id: MechId) -> DispatchResult { // Make sure the base subnet exists ensure!( - Self::if_subnet_exist(netuid), + Self::subnet_exists(netuid), Error::::MechanismDoesNotExist ); @@ -95,6 +95,7 @@ impl Pallet { Ok(()) } + /// Cap `max_uids * mechanism_count` so multi-mechanism subnets cannot bloat past default max UIDs. pub fn ensure_max_uids_over_all_mechanisms( max_uids: u16, mechanism_count: MechId, @@ -113,7 +114,7 @@ impl Pallet { pub fn do_set_mechanism_count(netuid: NetUid, mechanism_count: MechId) -> DispatchResult { // Make sure the subnet exists ensure!( - Self::if_subnet_exist(netuid), + Self::subnet_exists(netuid), Error::::MechanismDoesNotExist ); @@ -197,10 +198,11 @@ impl Pallet { } } + /// Set or clear the per-mechanism emission split; values must sum to `u16::MAX` when `Some`. pub fn do_set_emission_split(netuid: NetUid, maybe_split: Option>) -> DispatchResult { // Make sure the subnet exists ensure!( - Self::if_subnet_exist(netuid), + Self::subnet_exists(netuid), Error::::MechanismDoesNotExist ); @@ -258,13 +260,15 @@ impl Pallet { result } - fn weighted_acc_u16(existing: u16, added: u16, weight: U64F64) -> u16 { + /// `existing + added * weight` for consolidating u16 epoch terms across mechanisms. + fn weighted_accumulate_u16(existing: u16, added: u16, weight: U64F64) -> u16 { U64F64::saturating_from_num(existing) .saturating_add(U64F64::saturating_from_num(added).saturating_mul(weight)) .saturating_to_num::() } - fn weighted_acc_alpha( + /// `existing + added * weight` for consolidating alpha epoch terms across mechanisms. + fn weighted_accumulate_alpha( existing: AlphaBalance, added: AlphaBalance, weight: U64F64, @@ -324,28 +328,28 @@ impl Pallet { .saturating_add(terms.server_emission); // The rest of the terms need to be aggregated as weighted sum - acc_terms.dividend = Self::weighted_acc_u16( + acc_terms.dividend = Self::weighted_accumulate_u16( acc_terms.dividend, terms.dividend, sub_weight, ); - acc_terms.stake_weight = Self::weighted_acc_u16( + acc_terms.stake_weight = Self::weighted_accumulate_u16( acc_terms.stake_weight, terms.stake_weight, sub_weight, ); acc_terms.active |= terms.active; - acc_terms.emission = Self::weighted_acc_alpha( + acc_terms.emission = Self::weighted_accumulate_alpha( acc_terms.emission, terms.emission, sub_weight, ); - acc_terms.consensus = Self::weighted_acc_u16( + acc_terms.consensus = Self::weighted_accumulate_u16( acc_terms.consensus, terms.consensus, sub_weight, ); - acc_terms.validator_trust = Self::weighted_acc_u16( + acc_terms.validator_trust = Self::weighted_accumulate_u16( acc_terms.validator_trust, terms.validator_trust, sub_weight, @@ -357,23 +361,35 @@ impl Pallet { // weighted insert for the first sub-subnet seen for this hotkey EpochTerms { uid: terms.uid, - dividend: Self::weighted_acc_u16(0, terms.dividend, sub_weight), - incentive: Self::weighted_acc_u16(0, terms.incentive, sub_weight), + dividend: Self::weighted_accumulate_u16( + 0, + terms.dividend, + sub_weight, + ), + incentive: Self::weighted_accumulate_u16( + 0, + terms.incentive, + sub_weight, + ), validator_emission: terms.validator_emission, server_emission: terms.server_emission, - stake_weight: Self::weighted_acc_u16( + stake_weight: Self::weighted_accumulate_u16( 0, terms.stake_weight, sub_weight, ), active: terms.active, // booleans are ORed across subs - emission: Self::weighted_acc_alpha( + emission: Self::weighted_accumulate_alpha( 0u64.into(), terms.emission, sub_weight, ), - consensus: Self::weighted_acc_u16(0, terms.consensus, sub_weight), - validator_trust: Self::weighted_acc_u16( + consensus: Self::weighted_accumulate_u16( + 0, + terms.consensus, + sub_weight, + ), + validator_trust: Self::weighted_accumulate_u16( 0, terms.validator_trust, sub_weight, diff --git a/pallets/subtensor/src/subnets/mod.rs b/pallets/subtensor/src/subnets/mod.rs index cc654fcc44..5a12de6ce7 100644 --- a/pallets/subtensor/src/subnets/mod.rs +++ b/pallets/subtensor/src/subnets/mod.rs @@ -1,3 +1,17 @@ +//! Subnet lifecycle: registration, UIDs, serving, mechanisms, leasing, dissolve. +//! +//! Search anchors: +//! - [`subnet`] — create/init networks, subnet account IDs, owner-cut flags +//! - [`registration`] — neuron register / faucet / prune / POW helpers +//! - [`collateral`] — miner registration collateral lock and drain +//! - [`uids`] — append/replace/trim neurons and uid↔hotkey lookups +//! - [`serving`] — axon / prometheus endpoint publish + validation +//! - [`mechanism`] — sub-subnet storage index and multi-mechanism epoch +//! - [`leasing`] — crowdloan-backed leased subnet registration +//! - [`dissolution`] — dissolve queue + weight-metered cleanup phases +//! - [`symbols`] — default token symbol / name tables per netuid +//! - [`weights`] — commit/reveal/set weights (owned by a later shard; not edited here) + use super::*; pub mod collateral; pub mod dissolution; diff --git a/pallets/subtensor/src/subnets/registration.rs b/pallets/subtensor/src/subnets/registration.rs index 022bd955df..1cc8ac57ef 100644 --- a/pallets/subtensor/src/subnets/registration.rs +++ b/pallets/subtensor/src/subnets/registration.rs @@ -1,3 +1,9 @@ +//! Neuron registration, faucet minting, and prune / POW helpers. +//! +//! Burn registration splits cost via collateral (`pay_neuron_registration`); +//! when the subnet is full, [`get_neuron_to_prune`] picks a replaceable uid +//! while protecting immune owner UIDs. + use super::*; use frame_support::storage::{TransactionOutcome, with_transaction}; use sp_core::{H256, U256}; @@ -10,6 +16,7 @@ use system::pallet_prelude::BlockNumberFor; const LOG_TARGET: &str = "runtime::subtensor::registration"; impl Pallet { + /// Append a neuron or replace a pruned uid; returns the assigned uid. pub fn register_neuron(netuid: NetUid, hotkey: &T::AccountId) -> Result { let block_number: u64 = Self::get_current_block_as_u64(); let current_subnetwork_n: u16 = Self::get_subnetwork_n(netuid); @@ -35,6 +42,7 @@ impl Pallet { } } + /// Burn/collateral neuron registration path for a signed coldkey. pub fn do_register( origin: OriginFor, netuid: NetUid, @@ -49,7 +57,7 @@ impl Pallet { !netuid.is_root(), Error::::RegistrationNotPermittedOnRootSubnet ); - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); // 3) registrations allowed ensure!( @@ -114,7 +122,13 @@ impl Pallet { // after the swap must not leave a partial charge. with_transaction(|| { let result = (|| -> Result { - Self::pay_registration(netuid, &hotkey, &coldkey, burned_share, collateral_topup)?; + Self::pay_neuron_registration( + netuid, + &hotkey, + &coldkey, + burned_share, + collateral_topup, + )?; let neuron_uid = Self::register_neuron(netuid, &hotkey)?; @@ -136,6 +150,7 @@ impl Pallet { }) } + /// Like [`Self::do_register`] but rejects when the subnet is already at max UIDs (no prune). pub fn do_register_limit( origin: OriginFor, netuid: NetUid, @@ -152,7 +167,7 @@ impl Pallet { !netuid.is_root(), Error::::RegistrationNotPermittedOnRootSubnet ); - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); // Enforce caller limit before entering the shared registration path. let registration_cost: TaoBalance = Self::get_burn(netuid); @@ -167,6 +182,7 @@ impl Pallet { Self::do_register(origin, netuid, hotkey) } + /// Testnet-only faucet: mint TAO to the signer after validating POW work. pub fn do_faucet( origin: OriginFor, block_number: u64, @@ -194,7 +210,7 @@ impl Pallet { // --- 3. Ensure the supplied work passes the difficulty. let difficulty: U256 = U256::from(1_000_000); // Base faucet difficulty. - let work_hash: H256 = Self::vec_to_hash(work.clone()); + let work_hash: H256 = Self::registration_work_bytes_to_h256(work.clone()); ensure!( Self::hash_meets_difficulty(&work_hash, difficulty), Error::::InvalidDifficulty @@ -218,28 +234,35 @@ impl Pallet { Ok(()) } - pub fn vec_to_hash(vec_hash: Vec) -> H256 { + /// Decode a 32-byte POW work vector as [`H256`]. + pub fn registration_work_bytes_to_h256(vec_hash: Vec) -> H256 { let de_ref_hash = &vec_hash; // b: &Vec let de_de_ref_hash: &[u8] = de_ref_hash; // c: &[u8] let real_hash: H256 = H256::from_slice(de_de_ref_hash); real_hash } - fn get_immune_owner_hotkeys(netuid: NetUid, coldkey: &T::AccountId) -> Vec { - Self::get_immune_owner_tuples(netuid, coldkey) + /// Hotkeys of immune owner UIDs for `coldkey` on `netuid` (prune protection set). + fn immune_owner_hotkeys(netuid: NetUid, coldkey: &T::AccountId) -> Vec { + Self::immune_owner_uid_hotkey_pairs(netuid, coldkey) .into_iter() .map(|(_, hk)| hk) .collect() } + /// Uids of immune owner neurons for `coldkey` on `netuid`. pub fn get_immune_owner_uids(netuid: NetUid, coldkey: &T::AccountId) -> Vec { - Self::get_immune_owner_tuples(netuid, coldkey) + Self::immune_owner_uid_hotkey_pairs(netuid, coldkey) .into_iter() .map(|(uid, _)| uid) .collect() } - fn get_immune_owner_tuples(netuid: NetUid, coldkey: &T::AccountId) -> Vec<(u16, T::AccountId)> { + /// `(uid, hotkey)` pairs for owner-immune neurons, newest-first, capped by [`ImmuneOwnerUidsLimit`]. + fn immune_owner_uid_hotkey_pairs( + netuid: NetUid, + coldkey: &T::AccountId, + ) -> Vec<(u16, T::AccountId)> { // Gather (block, uid, hotkey) only for hotkeys that have a UID and a registration block. let mut triples: Vec<(u64, u16, T::AccountId)> = OwnedHotkeys::::get(coldkey) .into_iter() @@ -289,7 +312,7 @@ impl Pallet { } let owner_ck = SubnetOwner::::get(netuid); - let immortal_hotkeys = Self::get_immune_owner_hotkeys(netuid, &owner_ck); + let immortal_hotkeys = Self::immune_owner_hotkeys(netuid, &owner_ck); let emissions: Vec = Emission::::get(netuid); // Single pass: @@ -364,6 +387,7 @@ impl Pallet { /// The test is done by multiplying the two together. If the product /// overflows the bounds of U256, then the product (and thus the hash) /// was too high. + /// Whether `hash` as a big-endian integer is below `difficulty` (POW check). pub fn hash_meets_difficulty(hash: &H256, difficulty: U256) -> bool { let bytes: &[u8] = hash.as_bytes(); let num_hash: U256 = U256::from_little_endian(bytes); @@ -376,6 +400,7 @@ impl Pallet { !overflowed } + /// Block hash for `block_number`, or zero hash if the block is unknown. pub fn get_block_hash_from_u64(block_number: u64) -> H256 { let block_number: BlockNumberFor = TryInto::>::try_into(block_number) .ok() @@ -394,12 +419,14 @@ impl Pallet { real_hash } + /// Encode [`H256`] as a 32-byte vector for POW work payloads. pub fn hash_to_vec(hash: H256) -> Vec { let hash_as_bytes: &[u8] = hash.as_bytes(); let hash_as_vec: Vec = hash_as_bytes.to_vec(); hash_as_vec } + /// Keccak hash of `block_hash || hotkey` used in POW seal construction. pub fn hash_block_and_hotkey(block_hash_bytes: &[u8; 32], hotkey: &T::AccountId) -> H256 { let binding = hotkey.encode(); // Safe because Substrate guarantees that all AccountId types are at least 32 bytes @@ -413,6 +440,7 @@ impl Pallet { H256::from_slice(&keccak_256_seal_hash_vec) } + /// First 8 bytes of the hotkey blake2 hash as `u64` (faucet / POW helper). pub fn hash_hotkey_to_u64(hotkey: &T::AccountId) -> u64 { let binding = hotkey.encode(); let (hotkey_bytes, _) = binding.split_at(32); @@ -428,6 +456,7 @@ impl Pallet { hash_u64 } + /// POW seal hash for `(block, nonce, hotkey)`. pub fn create_seal_hash(block_number_u64: u64, nonce_u64: u64, hotkey: &T::AccountId) -> H256 { let nonce = nonce_u64.to_le_bytes(); let block_hash_at_number: H256 = Self::get_block_hash_from_u64(block_number_u64); @@ -451,6 +480,7 @@ impl Pallet { } /// Helper function for creating nonce and work. + /// Build the POW work vector for tests / faucet against `block_number`. pub fn create_work_for_block_number( netuid: NetUid, block_number: u64, @@ -475,6 +505,7 @@ impl Pallet { /// where `f ^ BurnHalfLife = 1/2`. /// * Burn is clamped to the configured [`MinBurn`, `MaxBurn`] range. /// + /// Decay burn prices for every subnet once per block (`on_initialize`). pub fn update_registration_prices_for_networks() { let current_block: u64 = Self::get_current_block_as_u64(); diff --git a/pallets/subtensor/src/subnets/serving.rs b/pallets/subtensor/src/subnets/serving.rs index 867c497710..61c3af1ed1 100644 --- a/pallets/subtensor/src/subnets/serving.rs +++ b/pallets/subtensor/src/subnets/serving.rs @@ -1,3 +1,8 @@ +//! Axon and prometheus endpoint serving for registered hotkeys. +//! +//! Extrinsic bodies validate IP/port, enforce the serving rate limit, and +//! persist [`Axons`] / [`Prometheus`] maps keyed by `(netuid, hotkey)`. + use super::*; use subtensor_runtime_common::NetUid; @@ -53,7 +58,7 @@ impl Pallet { ) -> dispatch::DispatchResult { // We check the callers (hotkey) signature. let hotkey_id = ensure_signed(origin)?; - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); // Validate user input Self::validate_serve_axon( @@ -142,7 +147,7 @@ impl Pallet { ) -> dispatch::DispatchResult { // We check the callers (hotkey) signature. let hotkey_id = ensure_signed(origin)?; - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); let updated_prometheus = Self::validate_serve_prometheus(&hotkey_id, netuid, version, ip, port, ip_type)?; @@ -162,7 +167,8 @@ impl Pallet { --==[[ Helper functions ]]==-- *********************************/ - pub fn axon_passes_rate_limit( + /// Whether enough blocks have elapsed since the last axon serve on this hotkey. + pub fn axon_serve_passes_rate_limit( netuid: NetUid, prev_axon_info: &AxonInfoOf, current_block: u64, @@ -172,7 +178,8 @@ impl Pallet { rate_limit == 0 || last_serve == 0 || current_block.saturating_sub(last_serve) >= rate_limit } - pub fn prometheus_passes_rate_limit( + /// Whether enough blocks have elapsed since the last prometheus serve on this hotkey. + pub fn prometheus_serve_passes_rate_limit( netuid: NetUid, prev_prometheus_info: &PrometheusInfoOf, current_block: u64, @@ -182,6 +189,7 @@ impl Pallet { rate_limit == 0 || last_serve == 0 || current_block.saturating_sub(last_serve) >= rate_limit } + /// Stored axon info, or a zeroed default if the hotkey has never served. pub fn get_axon_info(netuid: NetUid, hotkey: &T::AccountId) -> AxonInfoOf { if let Some(axons) = Axons::::get(netuid, hotkey) { axons @@ -199,6 +207,7 @@ impl Pallet { } } + /// Stored prometheus info, or a zeroed default if the hotkey has never served. pub fn get_prometheus_info(netuid: NetUid, hotkey: &T::AccountId) -> PrometheusInfoOf { if let Some(prometheus) = Prometheus::::get(netuid, hotkey) { prometheus @@ -213,12 +222,14 @@ impl Pallet { } } + /// Accept only IPv4 (`4`) or IPv6 (`6`). pub fn is_valid_ip_type(ip_type: u8) -> bool { let allowed_values = [4, 6]; allowed_values.contains(&ip_type) } // @todo (Parallax 2-1-2021) : Implement exclusion of private IP ranges + /// Basic IP sanity checks; deliberately does not exclude private ranges. pub fn is_valid_ip_address(ip_type: u8, addr: u128, allow_zero: bool) -> bool { if !allow_zero && addr == 0 { return false; @@ -242,6 +253,7 @@ impl Pallet { true } + /// Reject axon payloads with port `0`. pub fn validate_axon_data(axon_info: &AxonInfoOf) -> Result> { if axon_info.port.clamp(0, u16::MAX) == 0 { return Err(Error::::InvalidPort); @@ -250,6 +262,7 @@ impl Pallet { Ok(true) } + /// Reject prometheus payloads with port `0`. pub fn validate_prometheus_data( prom_info: &PrometheusInfoOf, ) -> Result> { @@ -260,6 +273,7 @@ impl Pallet { Ok(true) } + /// Full pre-insert checks for `serve_axon` (registration, rate limit, IP/port). pub fn validate_serve_axon( hotkey_id: &T::AccountId, netuid: NetUid, @@ -289,7 +303,7 @@ impl Pallet { let mut prev_axon = Self::get_axon_info(netuid, hotkey_id); let current_block: u64 = Self::get_current_block_as_u64(); ensure!( - Self::axon_passes_rate_limit(netuid, &prev_axon, current_block), + Self::axon_serve_passes_rate_limit(netuid, &prev_axon, current_block), Error::::ServingRateLimitExceeded ); @@ -313,6 +327,7 @@ impl Pallet { } /// Same checks as [`Self::do_serve_prometheus`] before storage writes (for transaction extension). + /// Full pre-insert checks for `serve_prometheus`. pub fn validate_serve_prometheus( hotkey_id: &T::AccountId, netuid: NetUid, @@ -335,7 +350,7 @@ impl Pallet { let mut prev_prometheus = Self::get_prometheus_info(netuid, hotkey_id); let current_block: u64 = Self::get_current_block_as_u64(); ensure!( - Self::prometheus_passes_rate_limit(netuid, &prev_prometheus, current_block), + Self::prometheus_serve_passes_rate_limit(netuid, &prev_prometheus, current_block), Error::::ServingRateLimitExceeded ); diff --git a/pallets/subtensor/src/subnets/subnet.rs b/pallets/subtensor/src/subnets/subnet.rs index 0d2e8ef52b..0a8c98540f 100644 --- a/pallets/subtensor/src/subnets/subnet.rs +++ b/pallets/subtensor/src/subnets/subnet.rs @@ -1,3 +1,9 @@ +//! Subnet creation, initialization, and owner/account helpers. +//! +//! [`do_register_network`] locks the network cost and either creates the subnet +//! immediately or enqueues [`NetworkRegistrationInfo`] when dissolve cleanup +//! must free a slot first. + use super::*; use frame_support::PalletId; use safe_math::FixedExt; @@ -6,37 +12,34 @@ use sp_runtime::{SaturatedConversion, traits::AccountIdConversion}; use substrate_fixed::types::U64F64; use subtensor_runtime_common::{NetUid, TaoBalance}; -/// Data structure for a pending network registration in the execution queue. +/// Queued network registration waiting for a free subnet slot after dissolve. #[crate::freeze_struct("c47fe93995c89025")] #[derive(Encode, Decode, Default, TypeInfo, Clone, PartialEq, Eq, Debug)] pub struct NetworkRegistrationInfo { - /// The account that registered the network. + /// Coldkey that paid / locked the network registration cost. pub coldkey: AccountId, - /// The account that registered the network. + /// First neuron hotkey for the new subnet. pub hotkey: AccountId, - /// The mechanism that registered the network. + /// Mechanism id requested at registration (currently must be dynamic=`1`). pub mechid: u16, - /// The identity that registered the network. + /// Optional subnet identity to set once the network is created. pub identity: Option, - /// The lock amount that registered the network. + /// TAO locked for the network registration (released or consumed on finalize). pub lock_amount: TaoBalance, - /// The median subnet alpha price that registered the network. + /// Median subnet alpha price snapshot taken when the registration was queued. pub median_subnet_alpha_price: U64F64, - /// The block at which the network was registered. + /// Block at which the registration was queued. pub registration_block: u64, - /// The lock id that registered the network. + /// [`NetworkRegistrationLockId`] used to lock `lock_amount` for this entry. pub lock_id: u32, } impl Pallet { - /// Returns true if the subnetwork exists. - /// - /// This function checks if a subnetwork with the given UID exists. + /// Whether `netuid` is currently in [`NetworksAdded`] (live subnet). /// - /// # Returns - /// * `bool`: Whether the subnet exists. - /// - pub fn if_subnet_exist(netuid: NetUid) -> bool { + /// Mid-dissolve subnets are removed from this map immediately by + /// [`Self::do_dissolve_network`] even while cleanup is still queued. + pub fn subnet_exists(netuid: NetUid) -> bool { NetworksAdded::::get(netuid) } @@ -98,19 +101,11 @@ impl Pallet { Self::deposit_event(Event::NetworkRateLimitSet(limit)); } - /// Checks if registrations are allowed for a given subnet. - /// - /// This function retrieves the subnet hyperparameters for the specified subnet and checks the - /// `registration_allowed` flag. If the subnet doesn't exist or doesn't have hyperparameters - /// defined, it returns `false`. - /// - /// # Arguments - /// - /// * `netuid`: The unique identifier of the subnet. - /// - /// # Returns + /// Registration-allowed flag via [`Self::get_subnet_hyperparams`]. /// - /// * `bool`: `true` if registrations are allowed for the subnet, `false` otherwise. + /// Prefer [`Self::get_network_registration_allowed`] at call sites; this helper + /// reads the same flag through the hyperparams bundle and defaults to `false` + /// when hyperparams are missing. pub fn is_registration_allowed(netuid: NetUid) -> bool { Self::get_subnet_hyperparams(netuid) .map(|params| params.registration_allowed) @@ -156,7 +151,7 @@ impl Pallet { // Ensure that hotkey is not a special account ensure!( - Self::is_subnet_account_id(hotkey).is_none(), + Self::netuid_for_subnet_account(hotkey).is_none(), Error::::CannotUseSystemAccount ); @@ -267,6 +262,7 @@ impl Pallet { .map_err(|e| e.error) } + /// Finish network registration: allocate netuid, lock cost, init hyperparams, emit events. pub fn set_new_network_state( coldkey: &T::AccountId, hotkey: &T::AccountId, @@ -529,7 +525,7 @@ impl Pallet { /// /// * `DispatchResult`: A result indicating the success or failure of the operation. pub fn do_start_call(origin: OriginFor, netuid: NetUid) -> DispatchResult { - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); Self::ensure_subnet_owner(origin, netuid)?; ensure!( FirstEmissionBlockNumber::::get(netuid).is_none(), @@ -594,7 +590,7 @@ impl Pallet { Self::ensure_subnet_owner_or_root(origin, netuid)?; // Ensure that the subnet exists. - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); // Rate limit: 1 call per week ensure!( @@ -620,10 +616,12 @@ impl Pallet { Ok(()) } + /// Whether the subnet has started emission (`FirstEmissionBlockNumber` is set). pub fn is_valid_subnet_for_emission(netuid: NetUid) -> bool { FirstEmissionBlockNumber::::get(netuid).is_some() } + /// Pallet sub-account for `netuid` when the subnet exists or is mid-dissolve. pub fn get_subnet_account_id(netuid: NetUid) -> Option { if NetworksAdded::::contains_key(netuid) || netuid == NetUid::ROOT @@ -635,7 +633,8 @@ impl Pallet { } } - pub fn is_subnet_account_id(account: &T::AccountId) -> Option { + /// Inverse of [`Self::get_subnet_account_id`]: decode netuid from a pallet sub-account. + pub fn netuid_for_subnet_account(account: &T::AccountId) -> Option { let pallet_id = T::SubtensorPalletId::get(); match PalletId::try_from_sub_account::(account) { diff --git a/pallets/subtensor/src/subnets/symbols.rs b/pallets/subtensor/src/subnets/symbols.rs index 6fd3e82f56..706244aca2 100644 --- a/pallets/subtensor/src/subnets/symbols.rs +++ b/pallets/subtensor/src/subnets/symbols.rs @@ -1,3 +1,8 @@ +//! Default subnet token symbols and human-readable name tables. +//! +//! [`SYMBOLS`] is indexed by netuid for the default glyph; [`get_name_for_subnet`] +//! maps netuid to a UTF-8 name used in RPC / UI surfaces. + use super::*; use sp_std::collections::btree_set::BTreeSet; use subtensor_runtime_common::NetUid; @@ -469,6 +474,7 @@ pub static SYMBOLS: [&[u8]; 439] = [ /// Returns the Unicode symbol as a Vec for a given netuid. impl Pallet { + /// Human-readable UTF-8 name for `netuid` from the static name table. pub fn get_name_for_subnet(netuid: NetUid) -> Vec { SubnetIdentitiesV3::::try_get(netuid) .and_then(|identity| { @@ -924,6 +930,7 @@ impl Pallet { }) } + /// Default token symbol bytes for `netuid` from [`SYMBOLS`] (falls back to root symbol). pub fn get_symbol_for_subnet(netuid: NetUid) -> Vec { SYMBOLS .get(u16::from(netuid) as usize) @@ -931,6 +938,7 @@ impl Pallet { .to_vec() } + /// First unused symbol from [`SYMBOLS`], preferring the netuid-default slot. pub fn get_next_available_symbol(netuid: NetUid) -> Vec { let used_symbols: BTreeSet> = TokenSymbol::::iter_values().collect(); @@ -957,6 +965,7 @@ impl Pallet { available_symbol.unwrap_or(DEFAULT_SYMBOL.to_vec()) } + /// Error unless `symbol` is in the static [`SYMBOLS`] table (excluding root). pub fn ensure_symbol_exists(symbol: &[u8]) -> DispatchResult { if !SYMBOLS.iter().skip(1).any(|s| s == &symbol) { return Err(Error::::SymbolDoesNotExist.into()); @@ -965,6 +974,7 @@ impl Pallet { Ok(()) } + /// Error if `symbol` is already assigned in [`TokenSymbol`]. pub fn ensure_symbol_available(symbol: &[u8]) -> DispatchResult { if TokenSymbol::::iter_values().any(|s| s == symbol) { return Err(Error::::SymbolAlreadyInUse.into()); diff --git a/pallets/subtensor/src/subnets/uids.rs b/pallets/subtensor/src/subnets/uids.rs index c48e10f4ad..5dd376cb64 100644 --- a/pallets/subtensor/src/subnets/uids.rs +++ b/pallets/subtensor/src/subnets/uids.rs @@ -1,3 +1,8 @@ +//! Per-subnet uid allocation: append, replace, trim, and hotkey lookups. +//! +//! [`Uids`] / [`Keys`] are the bidirectional hotkey↔uid maps; emission and +//! consensus vectors are kept aligned with [`SubnetworkN`]. + use super::*; use frame_support::storage::IterableStorageDoubleMap; use sp_runtime::{PerU16, Percent}; @@ -12,7 +17,7 @@ impl Pallet { } /// Sets value for the element at the given position if it exists. - pub fn set_element_at(vec: &mut [N], position: usize, value: N) { + pub fn set_vec_element_at(vec: &mut [N], position: usize, value: N) { if let Some(element) = vec.get_mut(position) { *element = value; } @@ -22,14 +27,14 @@ impl Pallet { /// the neuron to default pub fn clear_neuron(netuid: NetUid, neuron_uid: u16) { let neuron_index: usize = neuron_uid.into(); - Emission::::mutate(netuid, |v| Self::set_element_at(v, neuron_index, 0.into())); + Emission::::mutate(netuid, |v| Self::set_vec_element_at(v, neuron_index, 0.into())); Consensus::::mutate(netuid, |v| { - Self::set_element_at(v, neuron_index, PerU16::zero()) + Self::set_vec_element_at(v, neuron_index, PerU16::zero()) }); for mecid in 0..MechanismCountCurrent::::get(netuid).into() { let netuid_index = Self::get_mechanism_storage_index(netuid, mecid.into()); Incentive::::mutate(netuid_index, |v| { - Self::set_element_at(v, neuron_index, PerU16::zero()) + Self::set_vec_element_at(v, neuron_index, PerU16::zero()) }); Bonds::::remove(netuid_index, neuron_uid); // Remove bonds for Validator. @@ -49,13 +54,13 @@ impl Pallet { } } Dividends::::mutate(netuid, |v| { - Self::set_element_at(v, neuron_index, PerU16::zero()) + Self::set_vec_element_at(v, neuron_index, PerU16::zero()) }); - StakeWeight::::mutate(netuid, |v| Self::set_element_at(v, neuron_index, 0)); + StakeWeight::::mutate(netuid, |v| Self::set_vec_element_at(v, neuron_index, 0)); ValidatorTrust::::mutate(netuid, |v| { - Self::set_element_at(v, neuron_index, PerU16::zero()) + Self::set_vec_element_at(v, neuron_index, PerU16::zero()) }); - ValidatorPermit::::mutate(netuid, |v| Self::set_element_at(v, neuron_index, false)); + ValidatorPermit::::mutate(netuid, |v| Self::set_vec_element_at(v, neuron_index, false)); } /// Replace the neuron under this uid. @@ -148,9 +153,10 @@ impl Pallet { Self::clear_stale_hotkey_successor(netuid, new_hotkey); } + /// Prune lowest-emission non-immune neurons until `SubnetworkN` fits `max_n`. pub fn trim_to_max_allowed_uids(netuid: NetUid, max_n: u16) -> DispatchResult { // Reasonable limits - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); ensure!( max_n >= MinAllowedUids::::get(netuid), Error::::InvalidValue @@ -384,7 +390,7 @@ impl Pallet { /// Returns true if the uid is set on the network. /// - pub fn is_uid_exist_on_network(netuid: NetUid, uid: u16) -> bool { + pub fn uid_exists_on_network(netuid: NetUid, uid: u16) -> bool { Keys::::contains_key(netuid, uid) } diff --git a/pallets/subtensor/src/subnets/weights.rs b/pallets/subtensor/src/subnets/weights.rs index 345dd4aca4..12343525de 100644 --- a/pallets/subtensor/src/subnets/weights.rs +++ b/pallets/subtensor/src/subnets/weights.rs @@ -430,7 +430,7 @@ impl Pallet { salt: Vec, version_key: u64, ) -> DispatchResult { - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); // Calculate netuid storage index let netuid_index = Self::get_mechanism_storage_index(netuid, mecid); @@ -572,7 +572,7 @@ impl Pallet { salts_list: Vec>, version_keys: Vec, ) -> DispatchResult { - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); // Calculate netuid storage index let netuid_index = Self::get_mechanism_storage_index(netuid, MechId::MAIN); @@ -1027,7 +1027,7 @@ impl Pallet { ) -> bool { let maybe_netuid_and_subid = Self::get_netuid_and_subid(netuid_index); if let Ok((netuid, _)) = maybe_netuid_and_subid - && Self::is_uid_exist_on_network(netuid, neuron_uid) + && Self::uid_exists_on_network(netuid, neuron_uid) { // --- 1. Ensure that the diff between current and last_set weights is greater than limit. let last_set_weights: u64 = Self::get_last_update_for_uid(netuid_index, neuron_uid); @@ -1045,7 +1045,7 @@ impl Pallet { /// Checks for any invalid uids on this network. pub fn contains_invalid_uids(netuid: NetUid, uids: &[u16]) -> bool { for uid in uids { - if !Self::is_uid_exist_on_network(netuid, *uid) { + if !Self::uid_exists_on_network(netuid, *uid) { log::debug!( "contains_invalid_uids( netuid:{netuid:?}, uid:{uids:?} does not exist on network. )" ); diff --git a/pallets/subtensor/src/swap/coldkey_lineage.rs b/pallets/subtensor/src/swap/coldkey_lineage.rs index f3f005b145..8375d76e85 100644 --- a/pallets/subtensor/src/swap/coldkey_lineage.rs +++ b/pallets/subtensor/src/swap/coldkey_lineage.rs @@ -1,9 +1,9 @@ -//! Global coldkey swap lineage. +//! Global coldkey swap lineage (`ColdkeyRoot` / `ColdkeySuccessor`). //! -//! After a successful coldkey swap, owner-identity continuity is recorded so -//! indexers and policies can attribute stake/ownership to a stable root -//! without replaying archives. Unlike hotkey lineage, maps are global: a -//! coldkey swap moves economic identity across every subnet at once. +//! After a successful [`crate::swap::swap_coldkey`] rename, owner-identity +//! continuity is recorded so indexers and policies can attribute stake/ownership +//! to a stable root without replaying archives. Unlike hotkey lineage, maps are +//! global: a coldkey swap moves economic identity across every subnet at once. //! //! Prefer [`Self::coldkey_root`] / [`Self::same_coldkey_lineage`] for identity //! checks. [`Self::coldkey_lineage_tip`] is best-effort: successor edges are diff --git a/pallets/subtensor/src/swap/hotkey_lineage.rs b/pallets/subtensor/src/swap/hotkey_lineage.rs index 2fd14ead2d..2ed317d4eb 100644 --- a/pallets/subtensor/src/swap/hotkey_lineage.rs +++ b/pallets/subtensor/src/swap/hotkey_lineage.rs @@ -1,9 +1,9 @@ -//! Per-subnet hotkey swap lineage. +//! Per-subnet hotkey swap lineage (`HotkeyRoot` / `HotkeySuccessor`). //! -//! After a successful hotkey swap, identity continuity is recorded so -//! validators and indexers can ban/score a stable root without replaying -//! archives. Maps are keyed by netuid because a swap may move a UID on one -//! subnet while the old hotkey remains registered on others. +//! After a successful [`crate::swap::swap_hotkey`] rename, identity continuity +//! is recorded so validators and indexers can ban/score a stable root without +//! replaying archives. Maps are keyed by netuid because a swap may move a UID +//! on one subnet while the old hotkey remains registered on others. //! //! On-chain helpers use [`HotkeySuccessor`] (tip walk) and [`HotkeyRoot`] //! (O(1) identity compare). Reverse edges are reconstructible off-chain from @@ -13,6 +13,9 @@ //! [`Self::hotkey_lineage_tip`] is best-effort: successor edges are cleared when //! a hotkey becomes live again and when it is written as a swap destination, //! but consumers should still treat tip walks as advisory. +//! +//! [`Self::record_hotkey_swap_on_netuid`] also stamps [`LastHotkeySwapOnNetuid`] +//! for the per-coldkey subnet swap cooldown. use frame_support::weights::Weight; diff --git a/pallets/subtensor/src/swap/mod.rs b/pallets/subtensor/src/swap/mod.rs index 26de3f819a..24a309548d 100644 --- a/pallets/subtensor/src/swap/mod.rs +++ b/pallets/subtensor/src/swap/mod.rs @@ -1,3 +1,21 @@ +//! Hotkey / coldkey identity swap (lineage + migration), not the TAO↔alpha AMM. +//! +//! This module lives under `pallets/subtensor/src/swap` and renames SS58 identity +//! across stake, ownership, and subnet membership. The AMM / liquidity pallet is +//! `pallets/swap` (`pallet-swap`) — different crate, different concern. +//! +//! ## Search anchors +//! +//! | Module | Owns | +//! |--------|------| +//! | [`swap_coldkey`] | Coldkey rename: stake, subnet ownership, owned hotkeys, locks, TAO | +//! | [`swap_hotkey`] | Hotkey rename on one subnet or all subnets (`keep_stake` paths) | +//! | [`coldkey_lineage`] | Global [`ColdkeyRoot`] / [`ColdkeySuccessor`] continuity maps | +//! | [`hotkey_lineage`] | Per-netuid [`HotkeyRoot`] / [`HotkeySuccessor`] + swap cooldown stamp | +//! +//! Extrinsics call [`Pallet::perform_coldkey_swap`] / [`Pallet::perform_hotkey_swap`]; lineage +//! helpers are the O(1) identity surface for bans/indexers after a successful swap. + use super::*; pub mod coldkey_lineage; pub mod hotkey_lineage; diff --git a/pallets/subtensor/src/swap/swap_coldkey.rs b/pallets/subtensor/src/swap/swap_coldkey.rs index b887acc8ce..60d230a5da 100644 --- a/pallets/subtensor/src/swap/swap_coldkey.rs +++ b/pallets/subtensor/src/swap/swap_coldkey.rs @@ -1,11 +1,27 @@ +//! Coldkey identity swap: migrate economic ownership from one coldkey SS58 to another. +//! +//! Entry point: [`Pallet::perform_coldkey_swap`]. Runs inside a storage transaction so a +//! late failure (e.g. collateral index) rolls back stake, ownership, locks, and +//! identity writes together. After success, [`Pallet::record_coldkey_swap_lineage`] +//! records global root/successor continuity. +//! +//! Deliberately does **not** move stake into a destination that already has +//! [`StakingHotkeys`] entries or that is itself a registered hotkey. + use frame_support::storage::{TransactionOutcome, with_transaction}; use super::*; impl Pallet { - /// Transfer all assets, stakes, subnet ownerships, and hotkey associations from `old_coldkey` to - /// to `new_coldkey`. - pub fn do_swap_coldkey( + /// Migrate all coldkey-keyed state from `old_coldkey` to `new_coldkey`. + /// + /// Transfers subnet ownership, auto-stake destinations, per-subnet alpha stake, + /// miner collateral bonds, staking-hotkey indexes, owned-hotkey associations, + /// stake locks, and remaining free TAO. Records coldkey lineage and emits + /// [`Event::ColdkeySwapped`] on success. + /// + /// Rejects when `new_coldkey` already has staking associations or is a hotkey. + pub fn perform_coldkey_swap( old_coldkey: &T::AccountId, new_coldkey: &T::AccountId, ) -> DispatchResult { @@ -32,14 +48,14 @@ impl Pallet { Self::set_accept_locked_alpha(new_coldkey, true); for netuid in Self::get_all_subnet_netuids() { - Self::transfer_subnet_ownership(netuid, old_coldkey, new_coldkey); - Self::transfer_auto_stake_destination(netuid, old_coldkey, new_coldkey); - Self::transfer_coldkey_stake(netuid, old_coldkey, new_coldkey); + Self::transfer_coldkey_subnet_ownership(netuid, old_coldkey, new_coldkey); + Self::transfer_coldkey_auto_stake_destination(netuid, old_coldkey, new_coldkey); + Self::transfer_coldkey_subnet_stake(netuid, old_coldkey, new_coldkey); // Stake has moved; migrate the bond so unstake guards stay attached. Self::transfer_coldkey_miner_collateral(netuid, old_coldkey, new_coldkey)?; } - Self::transfer_staking_hotkeys(old_coldkey, new_coldkey); - Self::transfer_hotkeys_ownership(old_coldkey, new_coldkey)?; + Self::transfer_coldkey_staking_hotkeys(old_coldkey, new_coldkey); + Self::transfer_coldkey_owned_hotkeys(old_coldkey, new_coldkey)?; // Transfer stake locks Self::swap_coldkey_locks(old_coldkey, new_coldkey)?; @@ -64,15 +80,17 @@ impl Pallet { }) } - /// Charges the swap cost from the coldkey's account and recycles the tokens. - pub fn charge_swap_cost(coldkey: &T::AccountId, swap_cost: TaoBalance) -> DispatchResult { + /// Recycle `swap_cost` TAO from `coldkey` as the coldkey-swap fee. + /// + /// Maps insufficient free balance to [`Error::NotEnoughBalanceToPaySwapColdKey`]. + pub fn charge_coldkey_swap_cost(coldkey: &T::AccountId, swap_cost: TaoBalance) -> DispatchResult { Self::recycle_tao(coldkey, swap_cost) .map_err(|_| Error::::NotEnoughBalanceToPaySwapColdKey)?; Ok(()) } - /// Transfer the ownership of the subnet to the new coldkey if it is owned by the old coldkey. - fn transfer_subnet_ownership( + /// If `old_coldkey` owns `netuid`, rewrite [`SubnetOwner`] to `new_coldkey`. + fn transfer_coldkey_subnet_ownership( netuid: NetUid, old_coldkey: &T::AccountId, new_coldkey: &T::AccountId, @@ -83,8 +101,8 @@ impl Pallet { } } - /// Transfer the auto stake destination from the old coldkey to the new coldkey if it is set. - fn transfer_auto_stake_destination( + /// Move [`AutoStakeDestination`] / reverse index from `old_coldkey` to `new_coldkey` on `netuid`. + fn transfer_coldkey_auto_stake_destination( netuid: NetUid, old_coldkey: &T::AccountId, new_coldkey: &T::AccountId, @@ -100,8 +118,10 @@ impl Pallet { } } - /// Transfer the stake of all staking hotkeys linked to the old coldkey to the new coldkey. - fn transfer_coldkey_stake( + /// Move every (hotkey, coldkey, netuid) alpha position for `old_coldkey` onto `new_coldkey`. + /// + /// Also migrates root-claimed rows and maintains the root auto-claim coldkey index. + fn transfer_coldkey_subnet_stake( netuid: NetUid, old_coldkey: &T::AccountId, new_coldkey: &T::AccountId, @@ -150,8 +170,8 @@ impl Pallet { } } - /// Transfer staking hotkeys from the old coldkey to the new coldkey. - fn transfer_staking_hotkeys(old_coldkey: &T::AccountId, new_coldkey: &T::AccountId) { + /// Merge [`StakingHotkeys`] from `old_coldkey` into `new_coldkey`, then clear the old list. + fn transfer_coldkey_staking_hotkeys(old_coldkey: &T::AccountId, new_coldkey: &T::AccountId) { let old_staking_hotkeys: Vec = StakingHotkeys::::get(old_coldkey); let mut new_staking_hotkeys: Vec = StakingHotkeys::::get(new_coldkey); for hotkey in old_staking_hotkeys { @@ -165,8 +185,8 @@ impl Pallet { StakingHotkeys::::insert(new_coldkey, new_staking_hotkeys); } - /// Transfer the ownership of the hotkeys owned by the old coldkey to the new coldkey. - fn transfer_hotkeys_ownership( + /// Reassign [`Owner`] / [`OwnedHotkeys`] so every hotkey owned by `old_coldkey` is owned by `new_coldkey`. + fn transfer_coldkey_owned_hotkeys( old_coldkey: &T::AccountId, new_coldkey: &T::AccountId, ) -> DispatchResult { @@ -177,7 +197,7 @@ impl Pallet { Owner::::remove(owned_hotkey); // Add the hotkey to the new coldkey. Self::set_hotkey_owner(new_coldkey, owned_hotkey)?; - // Addd the owned hotkey to the new set of owned hotkeys. + // Add the owned hotkey to the new set of owned hotkeys. if !new_owned_hotkeys.contains(owned_hotkey) { new_owned_hotkeys.push(owned_hotkey.clone()); } diff --git a/pallets/subtensor/src/swap/swap_hotkey.rs b/pallets/subtensor/src/swap/swap_hotkey.rs index b4fb7336f0..6e5d85500a 100644 --- a/pallets/subtensor/src/swap/swap_hotkey.rs +++ b/pallets/subtensor/src/swap/swap_hotkey.rs @@ -1,3 +1,16 @@ +//! Hotkey identity swap: rename a hotkey SS58 on one subnet or across all subnets. +//! +//! Entry points: +//! - [`Pallet::perform_hotkey_swap`] — signed path (fee, cooldown, ownership checks) +//! - [`Pallet::perform_hotkey_swap_on_all_subnets`] / [`Pallet::perform_hotkey_swap_on_one_subnet`] +//! — mutation cores used by the signed path and by tests/migrations +//! +//! `keep_stake=true` leaves alpha on the old hotkey and only moves UID/membership +//! metadata (blocked when miner collateral is standing). Stake-moving paths scan +//! V1/V2 alpha once into [`HotkeySwapStakeSnapshot`] before mutating storage. +//! +//! Not the AMM pallet (`pallets/swap`); see the parent [`crate::swap`] module docs. + use super::*; use frame_support::storage::{TransactionOutcome, with_transaction}; use frame_support::weights::Weight; @@ -6,16 +19,23 @@ use sp_core::Get; use sp_std::collections::btree_map::BTreeMap; use subtensor_runtime_common::{MechId, NetUid, Token}; -struct PreparedHotkeyStake { +/// Preflight snapshot of one hotkey's alpha positions, grouped by netuid. +/// +/// Built once before stake-moving swaps so per-subnet mutation reuses the scan +/// instead of re-iterating V1/V2 prefixes. V2 wins when both rows exist for the +/// same (coldkey, netuid). +struct HotkeySwapStakeSnapshot { positions: Vec<(AccountId, NetUid, SafeFloat)>, coldkeys_by_netuid: BTreeMap>, } impl Pallet { - /// Use the generated all-subnet stake-moving benchmark for every v2 path - /// that scans and moves stake. Preserve the previous lightweight weight for - /// the single-subnet `keep_stake` path, which does not scan stake prefixes. - pub fn swap_hotkey_v2_dispatch_weight(netuid: &Option, keep_stake: bool) -> Weight { + /// Pre-dispatch weight for `swap_hotkey` / `swap_hotkey_v2`. + /// + /// Stake-moving and all-subnet paths use the generated `swap_hotkey` benchmark. + /// The single-subnet `keep_stake` path keeps a lighter fixed weight (no stake + /// prefix scan) with lineage DB ops accounted in. + pub fn hotkey_swap_dispatch_weight(netuid: &Option, keep_stake: bool) -> Weight { if netuid.is_none() || !keep_stake { <::WeightInfo as crate::weights::WeightInfo>::swap_hotkey() } else { @@ -27,9 +47,12 @@ impl Pallet { } } - /// Read and merge the old hotkey's V1/V2 stake rows once. V2 keeps the - /// existing precedence over a duplicate legacy row. - fn prepare_hotkey_stake(old_hotkey: &T::AccountId) -> PreparedHotkeyStake { + /// Scan V1/V2 alpha for `old_hotkey` once into a [`HotkeySwapStakeSnapshot`]. + /// + /// V2 keeps precedence over a duplicate legacy V1 row for the same position. + fn prepare_hotkey_swap_stake_snapshot( + old_hotkey: &T::AccountId, + ) -> HotkeySwapStakeSnapshot { let positions: Vec<(T::AccountId, NetUid, SafeFloat)> = Self::alpha_iter_single_prefix(old_hotkey).collect(); let mut coldkeys_by_netuid: BTreeMap> = BTreeMap::new(); @@ -41,35 +64,24 @@ impl Pallet { .push(coldkey.clone()); } - PreparedHotkeyStake { + HotkeySwapStakeSnapshot { positions, coldkeys_by_netuid, } } - /// Swaps the hotkey of a coldkey account. - /// - /// # Arguments - /// - /// * `origin`: The origin of the transaction, and also the coldkey account. - /// * `old_hotkey`: The old hotkey to be swapped. - /// * `new_hotkey`: The new hotkey to replace the old one. - /// * `netuid`: The hotkey swap in a subnet or all subnets. - /// * `keep_stake`: If `true`, stake remains on the old hotkey and the rest metadata - /// - /// # Returns - /// - /// * `DispatchResultWithPostInfo`: The result of the dispatch. + /// Signed hotkey rename: fee, cooldown, ownership, then storage mutation. /// - /// # Errors + /// - `netuid = Some(n)`: rename on subnet `n` only ([`Self::swap_hotkey_on_single_subnet`]). + /// - `netuid = None`: rename across every subnet the hotkey participates in. + /// - `keep_stake = true`: leave alpha on `old_hotkey`; move UID/membership only + /// (rejected when miner collateral is standing on the scoped netuids). /// - /// * `NonAssociatedColdKey`: If the coldkey does not own the old hotkey. - /// * `NewHotKeyIsSameWithOld`: If the new hotkey is the same as the old hotkey. - /// * `HotKeyAlreadyRegisteredInSubNet`: If the new hotkey is already registered in the subnet. - /// * `NewHotKeyNotCleanForRootSwap`: If the swap touches root and the new hotkey - /// has outstanding `RootClaimable` entries or non-zero root stake. - /// * `NotEnoughBalanceToPaySwapHotKey`: If there is not enough balance to pay for the swap. - pub fn do_swap_hotkey( + /// Errors include [`Error::NonAssociatedColdKey`], [`Error::NewHotKeyIsSameWithOld`], + /// [`Error::HotKeyAlreadyRegisteredInSubNet`], [`Error::NewHotKeyNotCleanForRootSwap`], + /// [`Error::KeepStakeBlockedByCollateral`], [`Error::NotEnoughBalanceToPaySwapHotKey`], + /// and [`Error::HotKeySwapOnSubnetIntervalNotPassed`]. + pub fn perform_hotkey_swap( origin: OriginFor, old_hotkey: &T::AccountId, new_hotkey: &T::AccountId, @@ -80,7 +92,7 @@ impl Pallet { let coldkey = ensure_signed(origin)?; if let Some(netuid) = netuid { - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); } // 2. Ensure the coldkey owns the old hotkey @@ -167,7 +179,7 @@ impl Pallet { let prepared_stake = if keep_stake { None } else { - Some(Self::prepare_hotkey_stake(old_hotkey)) + Some(Self::prepare_hotkey_swap_stake_snapshot(old_hotkey)) }; // Preflight collateral-index capacity before charging or writing so a @@ -200,7 +212,7 @@ impl Pallet { // Per-subnet path after common checks. if let Some(netuid) = netuid { - return Self::swap_hotkey_on_subnet( + return Self::swap_hotkey_on_single_subnet( &coldkey, old_hotkey, new_hotkey, @@ -274,7 +286,7 @@ impl Pallet { Self::recycle_tao(&coldkey, swap_cost.into())?; weight.saturating_accrue(T::DbWeight::get().reads_writes(0, 2)); - Self::perform_hotkey_swap_on_all_subnets_prepared( + Self::perform_prepared_hotkey_swap_on_all_subnets( old_hotkey, new_hotkey, &coldkey, @@ -320,39 +332,12 @@ impl Pallet { }) } - /// Performs the hotkey swap operation, transferring all associated data and state from the old hotkey to the new hotkey. + /// Mutate all-subnet hotkey identity from `old_hotkey` to `new_hotkey`. /// - /// This function executes a series of steps to ensure a complete transfer of all relevant information: - /// 1. Swaps the owner of the hotkey. - /// 2. Updates the list of owned hotkeys for the coldkey. - /// 3. Transfers the total hotkey stake. - /// 4. Moves all stake-related data for the interval. - /// 5. Updates the last transaction block for the new hotkey. - /// 6. Transfers the delegate take information. - /// 7. Updates delegate information. - /// 8. For each subnet: - /// - Updates network membership status. - /// - Transfers UID and key information. - /// - Moves Prometheus data. - /// - Updates axon information. - /// - Transfers weight commits. - /// - Updates loaded emission data. - /// 9. Transfers all stake information, including updating staking hotkeys for each coldkey. - /// - /// Throughout the process, the function accumulates the computational weight of operations performed. - /// - /// # Arguments - /// * `old_hotkey`: The AccountId of the current hotkey to be replaced. - /// * `new_hotkey`: The AccountId of the new hotkey to replace the old one. - /// * `coldkey`: The AccountId of the coldkey that owns both hotkeys. - /// * `weight`: A mutable reference to the Weight, updated as operations are performed. - /// - /// # Returns - /// * `DispatchResult`: Ok(()) if the swap was successful, or an error if any operation failed. - /// - /// # Note - /// This function performs extensive storage reads and writes, which can be computationally expensive. - /// The accumulated weight should be carefully considered in the context of block limits. + /// Builds a stake snapshot when `keep_stake` is false, then delegates to + /// [`Self::perform_prepared_hotkey_swap_on_all_subnets`]. Accrues DB weight into + /// `weight`. Does not charge the swap fee or enforce per-subnet cooldown — callers + /// ([`Self::perform_hotkey_swap`]) handle those. pub fn perform_hotkey_swap_on_all_subnets( old_hotkey: &T::AccountId, new_hotkey: &T::AccountId, @@ -363,10 +348,10 @@ impl Pallet { let prepared_stake = if keep_stake { None } else { - Some(Self::prepare_hotkey_stake(old_hotkey)) + Some(Self::prepare_hotkey_swap_stake_snapshot(old_hotkey)) }; - Self::perform_hotkey_swap_on_all_subnets_prepared( + Self::perform_prepared_hotkey_swap_on_all_subnets( old_hotkey, new_hotkey, coldkey, @@ -376,13 +361,18 @@ impl Pallet { ) } - fn perform_hotkey_swap_on_all_subnets_prepared( + /// Core all-subnet mutation using a preflight [`HotkeySwapStakeSnapshot`]. + /// + /// Moves locks, ownership, per-subnet membership/UID/metadata (via + /// [`Self::perform_prepared_hotkey_swap_on_one_subnet`]), last-tx markers, + /// delegate take, and (when not `keep_stake`) staking-hotkey indexes. + fn perform_prepared_hotkey_swap_on_all_subnets( old_hotkey: &T::AccountId, new_hotkey: &T::AccountId, coldkey: &T::AccountId, weight: &mut Weight, keep_stake: bool, - prepared_stake: Option<&PreparedHotkeyStake>, + prepared_stake: Option<&HotkeySwapStakeSnapshot>, ) -> DispatchResult { // 2. Swap the stake locks let (reads, writes) = Self::swap_hotkey_locks(old_hotkey, new_hotkey); @@ -415,7 +405,7 @@ impl Pallet { .map(Vec::as_slice) .unwrap_or(&[]); - Self::perform_hotkey_swap_on_one_subnet_prepared( + Self::perform_prepared_hotkey_swap_on_one_subnet( old_hotkey, new_hotkey, weight, @@ -474,15 +464,18 @@ impl Pallet { Ok(()) } - #[allow(unused)] - fn swap_hotkey_on_subnet( + /// Single-subnet signed path: cooldown, fee, ownership seed, then one-subnet mutation. + /// + /// Records [`LastHotkeySwapOnNetuid`] + lineage via [`Self::record_hotkey_swap_on_netuid`] + /// and emits [`Event::HotkeySwappedOnSubnet`]. + fn swap_hotkey_on_single_subnet( coldkey: &T::AccountId, old_hotkey: &T::AccountId, new_hotkey: &T::AccountId, netuid: NetUid, init_weight: Weight, keep_stake: bool, - prepared_stake: Option<&PreparedHotkeyStake>, + prepared_stake: Option<&HotkeySwapStakeSnapshot>, ) -> DispatchResultWithPostInfo { // 1. Ensure coldkey not swap hotkey too frequently let mut weight: Weight = init_weight; @@ -498,7 +491,7 @@ impl Pallet { // Check that new hotkey is a non-system hotkey ensure!( - Self::is_subnet_account_id(new_hotkey).is_none(), + Self::netuid_for_subnet_account(new_hotkey).is_none(), Error::::CannotUseSystemAccount ); @@ -550,7 +543,7 @@ impl Pallet { .and_then(|prepared| prepared.coldkeys_by_netuid.get(&netuid)) .map(Vec::as_slice) .unwrap_or(&[]); - Self::perform_hotkey_swap_on_one_subnet_prepared( + Self::perform_prepared_hotkey_swap_on_one_subnet( old_hotkey, new_hotkey, &mut weight, @@ -584,7 +577,10 @@ impl Pallet { } } - // do hotkey swap public part for both swap all subnets and just swap one subnet + /// Mutate hotkey identity on a single `netuid` (tests / migrations / all-subnet loop). + /// + /// Builds a stake snapshot when `keep_stake` is false, then delegates to + /// [`Self::perform_prepared_hotkey_swap_on_one_subnet`]. pub fn perform_hotkey_swap_on_one_subnet( old_hotkey: &T::AccountId, new_hotkey: &T::AccountId, @@ -595,7 +591,7 @@ impl Pallet { let prepared_stake = if keep_stake { None } else { - Some(Self::prepare_hotkey_stake(old_hotkey)) + Some(Self::prepare_hotkey_swap_stake_snapshot(old_hotkey)) }; let stake_coldkeys = prepared_stake .as_ref() @@ -603,7 +599,7 @@ impl Pallet { .map(Vec::as_slice) .unwrap_or(&[]); - Self::perform_hotkey_swap_on_one_subnet_prepared( + Self::perform_prepared_hotkey_swap_on_one_subnet( old_hotkey, new_hotkey, weight, @@ -613,7 +609,12 @@ impl Pallet { ) } - fn perform_hotkey_swap_on_one_subnet_prepared( + /// Core per-subnet mutation: membership, UID, serve data, collateral, children, stake. + /// + /// `stake_coldkeys` are the coldkeys with alpha under `old_hotkey` on this `netuid` + /// from the preflight snapshot (empty when `keep_stake`). Does not stamp swap + /// cooldown or lineage — callers do that after success. + fn perform_prepared_hotkey_swap_on_one_subnet( old_hotkey: &T::AccountId, new_hotkey: &T::AccountId, weight: &mut Weight, diff --git a/pallets/subtensor/src/tests/auto_stake_hotkey.rs b/pallets/subtensor/src/tests/auto_stake_hotkey.rs index ae73d173fa..7b6d2f9888 100644 --- a/pallets/subtensor/src/tests/auto_stake_hotkey.rs +++ b/pallets/subtensor/src/tests/auto_stake_hotkey.rs @@ -1,3 +1,7 @@ +//! Tests for [`crate::Pallet::set_coldkey_auto_stake_hotkey`]. +//! +//! Covers missing-subnet / unregistered-hotkey errors and successful set/change. + use super::mock::*; use crate::*; use frame_support::{assert_noop, assert_ok}; diff --git a/pallets/subtensor/src/tests/batch_tx.rs b/pallets/subtensor/src/tests/batch_tx.rs index 07f63c9933..c33f1d649f 100644 --- a/pallets/subtensor/src/tests/batch_tx.rs +++ b/pallets/subtensor/src/tests/batch_tx.rs @@ -1,3 +1,7 @@ +//! Tests for `pallet_subtensor_utility` batch calls against the subtensor runtime. +//! +//! Ensures nested batches are rejected while flat batches of allowed calls succeed. + use super::mock::*; use frame_support::{ assert_ok, diff --git a/pallets/subtensor/src/tests/children.rs b/pallets/subtensor/src/tests/children.rs deleted file mode 100644 index 703ecb4807..0000000000 --- a/pallets/subtensor/src/tests/children.rs +++ /dev/null @@ -1,4705 +0,0 @@ -#![allow(clippy::indexing_slicing)] -#![allow(clippy::unwrap_used)] -#![allow(clippy::arithmetic_side_effects)] -use super::mock; -use super::mock::*; -use approx::assert_abs_diff_eq; -use frame_support::{assert_err, assert_noop, assert_ok}; -use substrate_fixed::types::{I64F64, I96F32}; -use subtensor_runtime_common::{AlphaBalance, NetUidStorageIndex, TaoBalance}; -use subtensor_swap_interface::SwapHandler; - -use crate::{utils::rate_limiting::TransactionType, *}; -use sp_core::U256; -use sp_runtime::PerU16; - -fn close(value: u64, target: u64, eps: u64, msg: &str) { - assert!( - (value as i64 - target as i64).abs() <= eps as i64, - "{msg}: value = {value}, target = {target}, eps = {eps}" - ) -} - -// 1: Successful setting of a single child -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_set_child_singular_success --exact --show-output --nocapture -#[test] -fn test_do_set_child_singular_success() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child = U256::from(3); - let netuid = NetUid::from(1); - let proportion: u64 = 1000; - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Set child - mock_set_children(&coldkey, &hotkey, netuid, &[(proportion, child)]); - - // Verify child assignment - let children = SubtensorModule::get_children(&hotkey, netuid); - assert_eq!(children, vec![(proportion, child)]); - }); -} - -// 2: Attempt to set child in non-existent network -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_set_child_singular_network_does_not_exist --exact --show-output --nocapture -#[test] -fn test_do_set_child_singular_network_does_not_exist() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child = U256::from(3); - let netuid = NetUid::from(999); // Non-existent network - let proportion: u64 = 1000; - - // Attempt to set child - assert_err!( - SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - vec![(proportion, child)] - ), - Error::::SubnetNotExists - ); - }); -} - -// 3: Attempt to set invalid child (same as hotkey) -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_set_child_singular_invalid_child --exact --show-output --nocapture -#[test] -fn test_do_set_child_singular_invalid_child() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = NetUid::from(1); - let proportion: u64 = 1000; - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Attempt to set child as the same hotkey - assert_err!( - SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - vec![ - (proportion, hotkey) // Invalid child - ] - ), - Error::::InvalidChild - ); - }); -} - -// 4: Attempt to set child with non-associated coldkey -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_set_child_singular_non_associated_coldkey --exact --show-output --nocapture -#[test] -fn test_do_set_child_singular_non_associated_coldkey() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child = U256::from(3); - let netuid = NetUid::from(1); - let proportion: u64 = 1000; - - // Add network and register hotkey with a different coldkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, U256::from(999), 0); - - // Attempt to set child - assert_err!( - SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - vec![(proportion, child)] - ), - Error::::NonAssociatedColdKey - ); - }); -} - -// 5: Attempt to set child in root network -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_set_child_singular_root_network --exact --show-output --nocapture -#[test] -fn test_do_set_child_singular_root_network() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child = U256::from(3); - let netuid = NetUid::ROOT; // Root network - let proportion: u64 = 1000; - - // Add network and register hotkey - add_network(netuid, 13, 0); - - // Attempt to set child - assert_err!( - SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - vec![(proportion, child)] - ), - Error::::RegistrationNotPermittedOnRootSubnet - ); - }); -} - -// 6: Cleanup of old children when setting new ones -// This test verifies that when new children are set, the old ones are properly removed. -// It checks: -// - Setting an initial child -// - Replacing it with a new child -// - Ensuring the old child is no longer associated -// - Confirming the new child is correctly assigned -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_set_child_singular_old_children_cleanup --exact --show-output --nocapture -#[test] -fn test_do_set_child_singular_old_children_cleanup() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let old_child = U256::from(3); - let new_child = U256::from(4); - let netuid = NetUid::from(1); - let proportion: u64 = 1000; - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Set old child - mock_set_children(&coldkey, &hotkey, netuid, &[(proportion, old_child)]); - - step_rate_limit(&TransactionType::SetChildren, netuid); - - // Set new child - mock_set_children(&coldkey, &hotkey, netuid, &[(proportion, new_child)]); - - // Verify old child is removed - let old_child_parents = SubtensorModule::get_parents(&old_child, netuid); - assert!(old_child_parents.is_empty()); - - // Verify new child assignment - let new_child_parents = SubtensorModule::get_parents(&new_child, netuid); - assert_eq!(new_child_parents, vec![(proportion, hotkey)]); - }); -} - -// 7: Verify new children assignment -// This test checks if new children are correctly assigned to a parent. -// It verifies: -// - Setting a child for a parent -// - Confirming the child is correctly listed under the parent -// - Ensuring the parent is correctly listed for the child -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_set_child_singular_new_children_assignment --exact --show-output --nocapture -#[test] -fn test_do_set_child_singular_new_children_assignment() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child = U256::from(3); - let netuid = NetUid::from(1); - let proportion: u64 = 1000; - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Set child - mock_set_children(&coldkey, &hotkey, netuid, &[(proportion, child)]); - - // Verify child assignment - let children = SubtensorModule::get_children(&hotkey, netuid); - assert_eq!(children, vec![(proportion, child)]); - - // Verify parent assignment - let parents = SubtensorModule::get_parents(&child, netuid); - assert_eq!(parents, vec![(proportion, hotkey)]); - }); -} - -// 8: Test edge cases for proportion values -// This test verifies that the system correctly handles minimum and maximum proportion values. -// It checks: -// - Setting a child with the minimum possible proportion (0) -// - Setting a child with the maximum possible proportion (u64::MAX) -// - Confirming both assignments are processed correctly -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_set_child_singular_proportion_edge_cases --exact --show-output --nocapture -#[test] -fn test_do_set_child_singular_proportion_edge_cases() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child = U256::from(3); - let netuid = NetUid::from(1); - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Set child with minimum proportion - let min_proportion: u64 = 0; - mock_set_children(&coldkey, &hotkey, netuid, &[(min_proportion, child)]); - - // Verify child assignment with minimum proportion - let children = SubtensorModule::get_children(&hotkey, netuid); - assert_eq!(children, vec![(min_proportion, child)]); - - step_rate_limit(&TransactionType::SetChildren, netuid); - - // Set child with maximum proportion - let max_proportion: u64 = u64::MAX; - mock_set_children(&coldkey, &hotkey, netuid, &[(max_proportion, child)]); - - // Verify child assignment with maximum proportion - let children = SubtensorModule::get_children(&hotkey, netuid); - assert_eq!(children, vec![(max_proportion, child)]); - }); -} - -// 9: Test setting multiple children -// This test verifies that when multiple children are set, only the last one remains. -// It checks: -// - Setting an initial child -// - Setting a second child -// - Confirming only the second child remains associated -// - Verifying the first child is no longer associated -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_set_child_singular_multiple_children --exact --show-output --nocapture -#[test] -fn test_do_set_child_singular_multiple_children() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child1 = U256::from(3); - let child2 = U256::from(4); - let netuid = NetUid::from(1); - let proportion1: u64 = 500; - let proportion2: u64 = 500; - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Set first child - mock_set_children(&coldkey, &hotkey, netuid, &[(proportion1, child1)]); - - step_rate_limit(&TransactionType::SetChildren, netuid); - - // Set second child - mock_set_children(&coldkey, &hotkey, netuid, &[(proportion1, child2)]); - - // Verify children assignment - let children = SubtensorModule::get_children(&hotkey, netuid); - assert_eq!(children, vec![(proportion2, child2)]); - - // Verify parent assignment for both children - let parents1 = SubtensorModule::get_parents(&child1, netuid); - assert!(parents1.is_empty()); // Old child should be removed - - let parents2 = SubtensorModule::get_parents(&child2, netuid); - assert_eq!(parents2, vec![(proportion2, hotkey)]); - }); -} - -// 10: Test adding a singular child with various error conditions -// This test checks different scenarios when adding a child, including: -// - Attempting to set a child in a non-existent network -// - Trying to set a child with an unassociated coldkey -// - Setting an invalid child -// - Successfully setting a valid child -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_add_singular_child --exact --show-output --nocapture -#[test] -fn test_add_singular_child() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let child = U256::from(1); - let hotkey = U256::from(1); - let coldkey = U256::from(2); - assert_eq!( - SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - vec![(u64::MAX, child)] - ), - Err(Error::::SubnetNotExists.into()) - ); - add_network(netuid, 1, 0); - step_rate_limit(&TransactionType::SetChildren, netuid); - assert_eq!( - SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - vec![(u64::MAX, child)] - ), - Err(Error::::NonAssociatedColdKey.into()) - ); - let _ = SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); - step_rate_limit(&TransactionType::SetChildren, netuid); - assert_eq!( - SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - vec![(u64::MAX, child)] - ), - Err(Error::::InvalidChild.into()) - ); - let child = U256::from(3); - step_rate_limit(&TransactionType::SetChildren, netuid); - - mock_set_children(&coldkey, &hotkey, netuid, &[(u64::MAX, child)]); - }) -} - -// 11: Test getting stake for a hotkey on a subnet -// This test verifies the correct calculation of stake for a parent and child neuron: -// - Sets up a network with a parent and child neuron -// - Stakes tokens to both parent and child from different coldkeys -// - Establishes a parent-child relationship with 100% stake allocation -// - Checks that the parent's stake is correctly transferred to the child -// - Ensures the total stake is preserved in the system -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_get_stake_for_hotkey_on_subnet --exact --show-output --nocapture -#[test] -fn test_get_stake_for_hotkey_on_subnet() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let parent = U256::from(1); - let child = U256::from(2); - let coldkey1 = U256::from(3); - let coldkey2 = U256::from(4); - add_network(netuid, 1, 0); - register_ok_neuron(netuid, parent, coldkey1, 0); - register_ok_neuron(netuid, child, coldkey2, 0); - // Set parent-child relationship with 100% stake allocation - mock_set_children(&coldkey1, &parent, netuid, &[(u64::MAX, child)]); - // Stake 1000 to parent from coldkey1 - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &parent, - &coldkey1, - netuid, - 1000.into(), - ); - // Stake 1000 to parent from coldkey2 - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &parent, - &coldkey2, - netuid, - 1000.into(), - ); - // Stake 1000 to child from coldkey1 - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &child, - &coldkey1, - netuid, - 1000.into(), - ); - // Stake 1000 to child from coldkey2 - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &child, - &coldkey2, - netuid, - 1000.into(), - ); - let parent_stake = SubtensorModule::get_inherited_for_hotkey_on_subnet(&parent, netuid); - let child_stake = SubtensorModule::get_inherited_for_hotkey_on_subnet(&child, netuid); - // The parent should have 0 stake as it's all allocated to the child - assert_eq!(parent_stake, 0.into()); - // The child should have its original stake (2000) plus the parent's stake (2000) - assert_eq!(child_stake, 4000.into()); - - // Ensure total stake is preserved - assert_eq!(parent_stake + child_stake, 4000.into()); - }); -} - -// 12: Test revoking a singular child successfully -// This test checks the process of revoking a child neuron: -// - Sets up a network with a parent and child neuron -// - Establishes a parent-child relationship -// - Revokes the child relationship -// - Verifies that the child is removed from the parent's children list -// - Ensures the parent is removed from the child's parents list -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_revoke_child_singular_success --exact --show-output --nocapture -#[test] -fn test_do_revoke_child_singular_success() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child = U256::from(3); - let netuid = NetUid::from(1); - let proportion: u64 = 1000; - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - // Set child - mock_set_children(&coldkey, &hotkey, netuid, &[(proportion, child)]); - // Verify child assignment - let children = SubtensorModule::get_children(&hotkey, netuid); - assert_eq!(children, vec![(proportion, child)]); - step_rate_limit(&TransactionType::SetChildren, netuid); - // Revoke child - mock_set_children(&coldkey, &hotkey, netuid, &[]); - // Verify child removal - let children = SubtensorModule::get_children(&hotkey, netuid); - assert!(children.is_empty()); - // Verify parent removal - let parents = SubtensorModule::get_parents(&child, netuid); - assert!(parents.is_empty()); - }); -} - -// 13: Test setting empty child vector on a non-existing subnet -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_set_empty_children_network_does_not_exist --exact --show-output --nocapture -#[test] -fn test_do_set_empty_children_network_does_not_exist() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = NetUid::from(999); // Non-existent network - // Attempt to revoke child - assert_err!( - SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - vec![] - ), - Error::::SubnetNotExists - ); - }); -} - -// 14: Test revoking a child with a non-associated coldkey -// This test ensures that attempting to revoke a child using an unassociated coldkey results in an error: -// - Sets up a network with a hotkey registered to a different coldkey -// - Attempts to revoke a child using an unassociated coldkey -// - Verifies that the appropriate error is returned -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_revoke_child_singular_non_associated_coldkey --exact --show-output --nocapture -#[test] -fn test_do_revoke_child_singular_non_associated_coldkey() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = NetUid::from(1); - - // Add network and register hotkey with a different coldkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, U256::from(999), 0); - - // Attempt to revoke child - assert_err!( - SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - vec![] - ), - Error::::NonAssociatedColdKey - ); - }); -} - -// 15: Test revoking a non-associated child -// This test verifies that attempting to revoke a child that is not associated with the parent results in an error: -// - Sets up a network and registers a hotkey -// - Attempts to revoke a child that was never associated with the parent -// - Checks that the appropriate error is returned -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_revoke_child_singular_child_not_associated --exact --show-output --nocapture -#[test] -fn test_do_revoke_child_singular_child_not_associated() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child = U256::from(3); - let netuid = NetUid::from(1); - - // Add network and register hotkey - add_network(netuid, 13, 0); - // Attempt to revoke child that is not associated - assert_err!( - SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - vec![(u64::MAX, child)] - ), - Error::::NonAssociatedColdKey - ); - }); -} - -// 16: Test setting multiple children successfully -// This test verifies that multiple children can be set for a parent successfully: -// - Sets up a network and registers a hotkey -// - Sets multiple children with different proportions -// - Verifies that the children are correctly assigned to the parent -// - Checks that the parent is correctly assigned to each child -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_schedule_children_multiple_success --exact --show-output --nocapture -#[test] -fn test_do_schedule_children_multiple_success() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child1 = U256::from(3); - let child2 = U256::from(4); - let netuid = NetUid::from(1); - let proportion1: u64 = 1000; - let proportion2: u64 = 2000; - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Set multiple children - mock_set_children( - &coldkey, - &hotkey, - netuid, - &[(proportion1, child1), (proportion2, child2)], - ); - - // Verify children assignment - let children = SubtensorModule::get_children(&hotkey, netuid); - assert_eq!(children, vec![(proportion1, child1), (proportion2, child2)]); - - // Verify parent assignment for both children - let parents1 = SubtensorModule::get_parents(&child1, netuid); - assert_eq!(parents1, vec![(proportion1, hotkey)]); - - let parents2 = SubtensorModule::get_parents(&child2, netuid); - assert_eq!(parents2, vec![(proportion2, hotkey)]); - }); -} - -// 17: Test setting multiple children in a non-existent network -// This test ensures that attempting to set multiple children in a non-existent network results in an error: -// - Attempts to set children in a network that doesn't exist -// - Verifies that the appropriate error is returned -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_schedule_children_multiple_network_does_not_exist --exact --show-output --nocapture -#[test] -fn test_do_schedule_children_multiple_network_does_not_exist() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child1 = U256::from(3); - let netuid = NetUid::from(999); // Non-existent network - let proportion: u64 = 1000; - - // Attempt to set children - assert_err!( - SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - vec![(proportion, child1)] - ), - Error::::SubnetNotExists - ); - }); -} - -// 18: Test setting multiple children with an invalid child -// This test verifies that attempting to set multiple children with an invalid child (same as parent) results in an error: -// - Sets up a network and registers a hotkey -// - Attempts to set a child that is the same as the parent hotkey -// - Checks that the appropriate error is returned -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_schedule_children_multiple_invalid_child --exact --show-output --nocapture -#[test] -fn test_do_schedule_children_multiple_invalid_child() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = NetUid::from(1); - let proportion: u64 = 1000; - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Attempt to set child as the same hotkey - assert_err!( - SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - vec![(proportion, hotkey)] - ), - Error::::InvalidChild - ); - }); -} - -// 19: Test setting multiple children with a non-associated coldkey -// This test ensures that attempting to set multiple children using an unassociated coldkey results in an error: -// - Sets up a network with a hotkey registered to a different coldkey -// - Attempts to set children using an unassociated coldkey -// - Verifies that the appropriate error is returned -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_schedule_children_multiple_non_associated_coldkey --exact --show-output --nocapture -#[test] -fn test_do_schedule_children_multiple_non_associated_coldkey() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child = U256::from(3); - let netuid = NetUid::from(1); - let proportion: u64 = 1000; - - // Add network and register hotkey with a different coldkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, U256::from(999), 0); - - // Attempt to set children - assert_err!( - SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - vec![(proportion, child)] - ), - Error::::NonAssociatedColdKey - ); - }); -} - -// 20: Test setting multiple children in root network -// This test verifies that attempting to set children in the root network results in an error: -// - Sets up the root network -// - Attempts to set children in the root network -// - Checks that the appropriate error is returned -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_schedule_children_multiple_root_network --exact --show-output --nocapture -#[test] -fn test_do_schedule_children_multiple_root_network() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child = U256::from(3); - let netuid = NetUid::ROOT; // Root network - let proportion: u64 = 1000; - - // Add network and register hotkey - add_network(netuid, 13, 0); - - // Attempt to set children - assert_err!( - SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - vec![(proportion, child)] - ), - Error::::RegistrationNotPermittedOnRootSubnet - ); - }); -} - -// 21: Test cleanup of old children when setting multiple new ones -// This test ensures that when new children are set, the old ones are properly removed: -// - Sets up a network and registers a hotkey -// - Sets an initial child -// - Replaces it with multiple new children -// - Verifies that the old child is no longer associated -// - Confirms the new children are correctly assigned -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_schedule_children_multiple_old_children_cleanup --exact --show-output --nocapture -#[test] -fn test_do_schedule_children_multiple_old_children_cleanup() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let old_child = U256::from(3); - let new_child1 = U256::from(4); - let new_child2 = U256::from(5); - let netuid = NetUid::from(1); - let proportion: u64 = 1000; - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Set old child - mock_set_children(&coldkey, &hotkey, netuid, &[(proportion, old_child)]); - - step_rate_limit(&TransactionType::SetChildren, netuid); - - // Set new children - mock_set_children( - &coldkey, - &hotkey, - netuid, - &[(proportion, new_child1), (proportion, new_child2)], - ); - - // Verify old child is removed - let old_child_parents = SubtensorModule::get_parents(&old_child, netuid); - assert!(old_child_parents.is_empty()); - - // Verify new children assignment - let new_child1_parents = SubtensorModule::get_parents(&new_child1, netuid); - assert_eq!(new_child1_parents, vec![(proportion, hotkey)]); - - let new_child2_parents = SubtensorModule::get_parents(&new_child2, netuid); - assert_eq!(new_child2_parents, vec![(proportion, hotkey)]); - }); -} - -// 22: Test setting multiple children with edge case proportions -// This test verifies the behavior when setting multiple children with minimum and maximum proportions: -// - Sets up a network and registers a hotkey -// - Sets two children with minimum and maximum proportions respectively -// - Verifies that the children are correctly assigned with their respective proportions -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_schedule_children_multiple_proportion_edge_cases --exact --show-output --nocapture -#[test] -fn test_do_schedule_children_multiple_proportion_edge_cases() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child1 = U256::from(3); - let child2 = U256::from(4); - let netuid = NetUid::from(1); - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Set children with minimum and maximum proportions - let min_proportion: u64 = 0; - let max_proportion: u64 = u64::MAX; - mock_set_children( - &coldkey, - &hotkey, - netuid, - &[(min_proportion, child1), (max_proportion, child2)], - ); - - // Verify children assignment - let children = SubtensorModule::get_children(&hotkey, netuid); - assert_eq!( - children, - vec![(min_proportion, child1), (max_proportion, child2)] - ); - }); -} - -// 23: Test overwriting existing children with new ones -// This test ensures that when new children are set, they correctly overwrite the existing ones: -// - Sets up a network and registers a hotkey -// - Sets initial children -// - Overwrites with new children -// - Verifies that the final children assignment is correct -// - Checks that old children are properly removed and new ones are correctly assigned -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_schedule_children_multiple_overwrite_existing --exact --show-output --nocapture -#[test] -fn test_do_schedule_children_multiple_overwrite_existing() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child1 = U256::from(3); - let child2 = U256::from(4); - let child3 = U256::from(5); - let netuid = NetUid::from(1); - let proportion: u64 = 1000; - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Set initial children - mock_set_children( - &coldkey, - &hotkey, - netuid, - &[(proportion, child1), (proportion, child2)], - ); - - step_rate_limit(&TransactionType::SetChildren, netuid); - - // Overwrite with new children - mock_set_children( - &coldkey, - &hotkey, - netuid, - &[(proportion * 2, child2), (proportion * 3, child3)], - ); - - // Verify final children assignment - let children = SubtensorModule::get_children(&hotkey, netuid); - assert_eq!( - children, - vec![(proportion * 2, child2), (proportion * 3, child3)] - ); - - // Verify parent assignment for all children - let parents1 = SubtensorModule::get_parents(&child1, netuid); - assert!(parents1.is_empty()); - - let parents2 = SubtensorModule::get_parents(&child2, netuid); - assert_eq!(parents2, vec![(proportion * 2, hotkey)]); - - let parents3 = SubtensorModule::get_parents(&child3, netuid); - assert_eq!(parents3, vec![(proportion * 3, hotkey)]); - }); -} - -// 24: Test childkey take functionality -// This test verifies the functionality of setting and getting childkey take: -// - Sets up a network and registers a hotkey -// - Checks default and maximum childkey take values -// - Sets a new childkey take value -// - Verifies the new take value is stored correctly -// - Attempts to set an invalid take value and checks for appropriate error -// - Tries to set take with a non-associated coldkey and verifies the error -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_childkey_take_functionality --exact --show-output --nocapture -#[test] -fn test_childkey_take_functionality() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = NetUid::from(1); - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Test default and max childkey take - let default_take = SubtensorModule::get_default_childkey_take(); - let min_take = SubtensorModule::get_min_childkey_take(); - log::info!("Default take: {default_take}, Max take: {min_take}"); - - // Check if default take and max take are the same - assert_eq!( - default_take, min_take, - "Default take should be equal to max take" - ); - - // Log the actual value of MaxChildkeyTake - log::info!( - "MaxChildkeyTake value: {:?}", - MaxChildkeyTake::::get() - ); - - // Test setting childkey take - let new_take: u16 = SubtensorModule::get_max_childkey_take() / 2; // 50% of max_take - assert_ok!(SubtensorModule::set_childkey_take( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - PerU16::from_parts(new_take) - )); - - // Verify childkey take was set correctly - let stored_take = SubtensorModule::get_childkey_take(&hotkey, netuid); - log::info!("Stored take: {stored_take}"); - assert_eq!(stored_take, new_take); - - // Test setting childkey take outside of allowed range - let invalid_take: u16 = SubtensorModule::get_max_childkey_take() + 1; - assert_noop!( - SubtensorModule::set_childkey_take( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - PerU16::from_parts(invalid_take) - ), - Error::::InvalidChildkeyTake - ); - - // Test setting childkey take with non-associated coldkey - let non_associated_coldkey = U256::from(999); - assert_noop!( - SubtensorModule::set_childkey_take( - RuntimeOrigin::signed(non_associated_coldkey), - hotkey, - netuid, - PerU16::from_parts(new_take) - ), - Error::::NonAssociatedColdKey - ); - }); -} - -#[test] -fn test_childkey_take_respects_effective_subnet_minimum() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = NetUid::from(1); - let subnet_min = SubtensorModule::get_max_childkey_take() / 2; - - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - SubtensorModule::set_min_childkey_take_for_subnet(netuid, PerU16::from_parts(subnet_min)); - - assert_eq!( - SubtensorModule::get_effective_min_childkey_take(netuid), - subnet_min - ); - assert_eq!( - SubtensorModule::get_childkey_take(&hotkey, netuid), - subnet_min - ); - - assert_noop!( - SubtensorModule::set_childkey_take( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - PerU16::from_parts(subnet_min - 1) - ), - Error::::InvalidChildkeyTake - ); - - assert_ok!(SubtensorModule::set_childkey_take( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - PerU16::from_parts(subnet_min) - )); - - ChildkeyTake::::insert(hotkey, netuid, PerU16::from_parts(subnet_min - 1)); - assert_eq!( - SubtensorModule::get_childkey_take(&hotkey, netuid), - subnet_min - ); - }); -} - -// 25: Test childkey take rate limiting -// This test verifies the rate limiting functionality for setting childkey take: -// - Sets up a network and registers a hotkey -// - Sets a rate limit for childkey take changes -// - Performs multiple attempts to set childkey take -// - Verifies that rate limiting prevents frequent changes -// - Advances blocks to bypass rate limit and confirms successful change -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_childkey_take_rate_limiting --exact --show-output --nocapture -#[test] -fn test_childkey_take_rate_limiting() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = NetUid::from(1); - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Set a rate limit for childkey take changes - let rate_limit: u64 = 100; - SubtensorModule::set_tx_childkey_take_rate_limit(rate_limit); - - log::info!( - "Set TxChildkeyTakeRateLimit: {:?}", - TxChildkeyTakeRateLimit::::get() - ); - - // Helper function to log rate limit information - let log_rate_limit_info = || { - let current_block = SubtensorModule::get_current_block_as_u64(); - let last_block = TransactionType::SetChildkeyTake.last_block_on_subnet::( - &hotkey, - netuid, - ); - let passes = TransactionType::SetChildkeyTake.passes_rate_limit_on_subnet::( - &hotkey, - netuid, - ); - let limit = TransactionType::SetChildkeyTake.rate_limit_on_subnet::(netuid); - log::info!( - "Rate limit info: current_block: {}, last_block: {}, limit: {}, passes: {}, diff: {}", - current_block, - last_block, - limit, - passes, - current_block - last_block - ); - }; - - // First transaction (should succeed) - log_rate_limit_info(); - assert_ok!(SubtensorModule::set_childkey_take( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - PerU16::from_parts(500) - )); - log_rate_limit_info(); - - // Second transaction (should fail due to rate limit) - log_rate_limit_info(); - assert_noop!( - SubtensorModule::set_childkey_take( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - PerU16::from_parts(600) - ), - Error::::TxChildkeyTakeRateLimitExceeded - ); - log_rate_limit_info(); - - // Advance the block number to just before the rate limit - run_to_block(rate_limit - 1); - - // Third transaction (should still fail) - log_rate_limit_info(); - assert_noop!( - SubtensorModule::set_childkey_take( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - PerU16::from_parts(650) - ), - Error::::TxChildkeyTakeRateLimitExceeded - ); - log_rate_limit_info(); - - // Advance the block number to just after the rate limit - run_to_block(rate_limit + 1); - - // Fourth transaction (should succeed) - log_rate_limit_info(); - assert_ok!(SubtensorModule::set_childkey_take( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - PerU16::from_parts(700) - )); - log_rate_limit_info(); - - // Verify the final take was set - let stored_take = SubtensorModule::get_childkey_take(&hotkey, netuid); - assert_eq!(stored_take, 700); - }); -} - -// 26: Test childkey take functionality across multiple networks -// This test verifies the childkey take functionality across multiple networks: -// - Creates multiple networks and sets up neurons -// - Sets unique childkey take values for each network -// - Verifies that each network has a different childkey take value -// - Attempts to set childkey take again (should fail due to rate limit) -// - Advances blocks to bypass rate limit and successfully updates take value -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_multiple_networks_childkey_take --exact --show-output --nocapture -#[test] -fn test_multiple_networks_childkey_take() { - new_test_ext(1).execute_with(|| { - const NUM_NETWORKS: u16 = 10; - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - // Create 10 networks and set up neurons (skip network 0) - for netuid in 1..NUM_NETWORKS { - let netuid = NetUid::from(netuid); - // Add network - add_network(netuid, 13, 0); - - // Register neuron - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Set a unique childkey take value for each network - let take_value = u16::from(netuid.next()) * 100; // Values will be 200, 300, ..., 1000 - assert_ok!(SubtensorModule::set_childkey_take( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - PerU16::from_parts(take_value) - )); - - // Verify the childkey take was set correctly - let stored_take = SubtensorModule::get_childkey_take(&hotkey, netuid); - assert_eq!( - stored_take, take_value, - "Childkey take not set correctly for network {netuid}" - ); - - // Log the set value - log::info!("Network {netuid}: Childkey take set to {take_value}"); - } - - // Verify all networks have different childkey take values - for i in 1..NUM_NETWORKS { - for j in (i + 1)..NUM_NETWORKS { - let take_i = SubtensorModule::get_childkey_take(&hotkey, i.into()); - let take_j = SubtensorModule::get_childkey_take(&hotkey, j.into()); - assert_ne!( - take_i, take_j, - "Childkey take values should be different for networks {i} and {j}" - ); - } - } - - // Attempt to set childkey take again (should fail due to rate limit) - let result = SubtensorModule::set_childkey_take( - RuntimeOrigin::signed(coldkey), - hotkey, - 1.into(), - PerU16::from_parts(1100), - ); - assert_noop!(result, Error::::TxChildkeyTakeRateLimitExceeded); - - // Advance blocks to bypass rate limit - run_to_block(SubtensorModule::get_tx_childkey_take_rate_limit() + 1); - - // Now setting childkey take should succeed - assert_ok!(SubtensorModule::set_childkey_take( - RuntimeOrigin::signed(coldkey), - hotkey, - 1.into(), - PerU16::from_parts(1100) - )); - - // Verify the new take value - let new_take = SubtensorModule::get_childkey_take(&hotkey, 1.into()); - assert_eq!(new_take, 1100, "Childkey take not updated after rate limit"); - }); -} - -// 27: Test setting children with an empty list -// This test verifies the behavior of setting an empty children list: -// - Adds a network and registers a hotkey -// - Sets an empty children list for the hotkey -// - Verifies that the children assignment is empty -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_schedule_children_multiple_empty_list --exact --show-output --nocapture -#[test] -fn test_do_schedule_children_multiple_empty_list() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = NetUid::from(1); - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Set empty children list - mock_set_children(&coldkey, &hotkey, netuid, &[]); - - // Verify children assignment is empty - let children = SubtensorModule::get_children(&hotkey, netuid); - assert!(children.is_empty()); - }); -} - -// 28: Test revoking multiple children successfully -// This test verifies the successful revocation of multiple children: -// - Adds a network and registers a hotkey -// - Sets multiple children for the hotkey -// - Revokes all children by setting an empty list -// - Verifies that the children list is empty -// - Verifies that the parent-child relationships are removed for both children -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_revoke_children_multiple_success --exact --show-output --nocapture -#[test] -fn test_do_revoke_children_multiple_success() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child1 = U256::from(3); - let child2 = U256::from(4); - let netuid = NetUid::from(1); - let proportion1: u64 = 1000; - let proportion2: u64 = 2000; - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Set multiple children - mock_set_children( - &coldkey, - &hotkey, - netuid, - &[(proportion1, child1), (proportion2, child2)], - ); - - step_rate_limit(&TransactionType::SetChildren, netuid); - - // Revoke multiple children - mock_set_children(&coldkey, &hotkey, netuid, &[]); - - // Verify children removal - let children = SubtensorModule::get_children(&hotkey, netuid); - assert!(children.is_empty()); - - // Verify parent removal for both children - let parents1 = SubtensorModule::get_parents(&child1, netuid); - assert!(parents1.is_empty()); - - let parents2 = SubtensorModule::get_parents(&child2, netuid); - assert!(parents2.is_empty()); - }); -} - -// 29: Test revoking children when network does not exist -// This test verifies the behavior when attempting to revoke children on a non-existent network: -// - Attempts to revoke children on a network that doesn't exist -// - Verifies that the operation fails with the correct error -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_revoke_children_multiple_network_does_not_exist --exact --show-output --nocapture -#[test] -fn test_do_revoke_children_multiple_network_does_not_exist() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child1 = U256::from(3); - let child2 = U256::from(4); - let netuid = NetUid::from(999); // Non-existent network - // Attempt to revoke children - assert_err!( - SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - vec![(u64::MAX / 2, child1), (u64::MAX / 2, child2)] - ), - Error::::SubnetNotExists - ); - }); -} - -// 30: Test revoking children with non-associated coldkey -// This test verifies the behavior when attempting to revoke children using a non-associated coldkey: -// - Adds a network and registers a hotkey with a different coldkey -// - Attempts to revoke children using an unassociated coldkey -// - Verifies that the operation fails with the correct error -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_revoke_children_multiple_non_associated_coldkey --exact --show-output --nocapture -#[test] -fn test_do_revoke_children_multiple_non_associated_coldkey() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child1 = U256::from(3); - let child2 = U256::from(4); - let netuid = NetUid::from(1); - - // Add network and register hotkey with a different coldkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, U256::from(999), 0); - - // Attempt to revoke children - assert_err!( - SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - vec![(u64::MAX / 2, child1), (u64::MAX / 2, child2)] - ), - Error::::NonAssociatedColdKey - ); - }); -} - -// 31: Test partial revocation of children -// This test verifies the behavior when partially revoking children: -// - Adds a network and registers a hotkey -// - Sets multiple children for the hotkey -// - Revokes one of the children -// - Verifies that the correct children remain and the revoked child is removed -// - Checks the parent-child relationships after partial revocation -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_revoke_children_multiple_partial_revocation --exact --show-output --nocapture -#[test] -fn test_do_revoke_children_multiple_partial_revocation() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child1 = U256::from(3); - let child2 = U256::from(4); - let child3 = U256::from(5); - let netuid = NetUid::from(1); - let proportion: u64 = 1000; - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Set multiple children - mock_set_children( - &coldkey, - &hotkey, - netuid, - &[ - (proportion, child1), - (proportion, child2), - (proportion, child3), - ], - ); - - step_rate_limit(&TransactionType::SetChildren, netuid); - - // Revoke only child3 - mock_set_children( - &coldkey, - &hotkey, - netuid, - &[(proportion, child1), (proportion, child2)], - ); - - // Verify children removal - let children = SubtensorModule::get_children(&hotkey, netuid); - assert_eq!(children, vec![(proportion, child1), (proportion, child2)]); - - // Verify parents. - let parents1 = SubtensorModule::get_parents(&child3, netuid); - assert!(parents1.is_empty()); - let parents1 = SubtensorModule::get_parents(&child1, netuid); - assert_eq!(parents1, vec![(proportion, hotkey)]); - let parents2 = SubtensorModule::get_parents(&child2, netuid); - assert_eq!(parents2, vec![(proportion, hotkey)]); - }); -} - -// 32: Test revoking non-existent children -// This test verifies the behavior when attempting to revoke non-existent children: -// - Adds a network and registers a hotkey -// - Sets one child for the hotkey -// - Attempts to revoke all children (including non-existent ones) -// - Verifies that all children are removed, including the existing one -// - Checks that the parent-child relationship is properly updated -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_revoke_children_multiple_non_existent_children --exact --show-output --nocapture -#[test] -fn test_do_revoke_children_multiple_non_existent_children() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child1 = U256::from(3); - let netuid = NetUid::from(1); - let proportion: u64 = 1000; - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Set one child - mock_set_children(&coldkey, &hotkey, netuid, &[(proportion, child1)]); - - step_rate_limit(&TransactionType::SetChildren, netuid); - - // Attempt to revoke existing and non-existent children - mock_set_children(&coldkey, &hotkey, netuid, &[]); - - // Verify all children are removed - let children = SubtensorModule::get_children(&hotkey, netuid); - assert!(children.is_empty()); - - // Verify parent removal for the existing child - let parents1 = SubtensorModule::get_parents(&child1, netuid); - assert!(parents1.is_empty()); - }); -} - -// 33: Test revoking children with an empty list -// This test verifies the behavior when attempting to revoke children using an empty list: -// - Adds a network and registers a hotkey -// - Attempts to revoke children with an empty list -// - Verifies that no changes occur in the children list -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_revoke_children_multiple_empty_list --exact --show-output --nocapture -#[test] -fn test_do_revoke_children_multiple_empty_list() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = NetUid::from(1); - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Attempt to revoke with an empty list - mock_set_children(&coldkey, &hotkey, netuid, &[]); - - // Verify no changes in children - let children = SubtensorModule::get_children(&hotkey, netuid); - assert!(children.is_empty()); - }); -} - -// 34: Test complex scenario for revoking multiple children -// This test verifies a complex scenario involving setting and revoking multiple children: -// - Adds a network and registers a hotkey -// - Sets multiple children with different proportions -// - Revokes one child and verifies the remaining children -// - Revokes all remaining children -// - Verifies that all parent-child relationships are properly updated -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_revoke_children_multiple_complex_scenario --exact --show-output --nocapture -#[test] -fn test_do_revoke_children_multiple_complex_scenario() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child1 = U256::from(3); - let child2 = U256::from(4); - let child3 = U256::from(5); - let netuid = NetUid::from(1); - let proportion1: u64 = 1000; - let proportion2: u64 = 2000; - let proportion3: u64 = 3000; - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Set multiple children - mock_set_children( - &coldkey, - &hotkey, - netuid, - &[ - (proportion1, child1), - (proportion2, child2), - (proportion3, child3), - ], - ); - - step_rate_limit(&TransactionType::SetChildren, netuid); - - // Revoke child2 - mock_set_children( - &coldkey, - &hotkey, - netuid, - &[(proportion1, child1), (proportion3, child3)], - ); - - // Verify remaining children - let children = SubtensorModule::get_children(&hotkey, netuid); - assert_eq!(children, vec![(proportion1, child1), (proportion3, child3)]); - - // Verify parent removal for child2 - let parents2 = SubtensorModule::get_parents(&child2, netuid); - assert!(parents2.is_empty()); - - step_rate_limit(&TransactionType::SetChildren, netuid); - - // Revoke remaining children - mock_set_children(&coldkey, &hotkey, netuid, &[]); - - // Verify all children are removed - let children = SubtensorModule::get_children(&hotkey, netuid); - assert!(children.is_empty()); - - // Verify parent removal for all children - let parents1 = SubtensorModule::get_parents(&child1, netuid); - assert!(parents1.is_empty()); - let parents3 = SubtensorModule::get_parents(&child3, netuid); - assert!(parents3.is_empty()); - }); -} - -// 39: Test children stake values -// This test verifies the correct distribution of stake among parent and child neurons: -// - Sets up a network with a parent neuron and multiple child neurons -// - Assigns stake to the parent neuron -// - Sets child neurons with specific proportions -// - Verifies that the stake is correctly distributed among parent and child neurons -// - Checks that the total stake remains constant across all neurons -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_children_stake_values --exact --show-output --nocapture -#[test] -fn test_children_stake_values() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child1 = U256::from(3); - let child2 = U256::from(4); - let child3 = U256::from(5); - let proportion1: u64 = u64::MAX / 4; - let proportion2: u64 = u64::MAX / 4; - let proportion3: u64 = u64::MAX / 4; - - // Add network and register hotkey - SubtensorModule::set_max_registrations_per_block(netuid, 4); - SubtensorModule::set_target_registrations_per_interval(netuid, 4); - register_ok_neuron(netuid, hotkey, coldkey, 0); - register_ok_neuron(netuid, child1, coldkey, 0); - register_ok_neuron(netuid, child2, coldkey, 0); - register_ok_neuron(netuid, child3, coldkey, 0); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - netuid, - 100_000_000_000_000_u64.into(), - ); - - // Set multiple children with proportions. - mock_set_children_no_epochs( - netuid, - &hotkey, - &[ - (proportion1, child1), - (proportion2, child2), - (proportion3, child3), - ], - ); - - assert_eq!( - SubtensorModule::get_inherited_for_hotkey_on_subnet(&hotkey, netuid), - 25_000_000_069_849_u64.into() - ); - assert_eq!( - SubtensorModule::get_inherited_for_hotkey_on_subnet(&child1, netuid), - 24_999_999_976_716_u64.into() - ); - assert_eq!( - SubtensorModule::get_inherited_for_hotkey_on_subnet(&child2, netuid), - 24_999_999_976_716_u64.into() - ); - assert_eq!( - SubtensorModule::get_inherited_for_hotkey_on_subnet(&child3, netuid), - 24_999_999_976_716_u64.into() - ); - assert_eq!( - SubtensorModule::get_inherited_for_hotkey_on_subnet(&child3, netuid) - + SubtensorModule::get_inherited_for_hotkey_on_subnet(&child2, netuid) - + SubtensorModule::get_inherited_for_hotkey_on_subnet(&child1, netuid) - + SubtensorModule::get_inherited_for_hotkey_on_subnet(&hotkey, netuid), - 99999999999997_u64.into() - ); - }); -} - -// 40: Test getting parents chain -// This test verifies the correct implementation of parent-child relationships and the get_parents function: -// - Sets up a network with multiple neurons in a chain of parent-child relationships -// - Verifies that each neuron has the correct parent -// - Tests the root neuron has no parents -// - Tests a neuron with multiple parents -// - Verifies correct behavior when adding a new parent to an existing child -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_get_parents_chain --exact --show-output --nocapture -#[test] -fn test_get_parents_chain() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let coldkey = U256::from(1); - let num_keys: usize = 5; - let proportion = u64::MAX / 2; // 50% stake allocation - - log::info!( - "Test setup: netuid={netuid}, coldkey={coldkey}, num_keys={num_keys}, proportion={proportion}" - ); - - // Create a vector of hotkeys - let hotkeys: Vec = (0..num_keys).map(|i| U256::from(i as u64 + 2)).collect(); - log::info!("Created hotkeys: {hotkeys:?}"); - - // Add network - add_network(netuid, 13, 0); - SubtensorModule::set_max_registrations_per_block(netuid, 1000); - SubtensorModule::set_target_registrations_per_interval(netuid, 1000); - log::info!("Network added and parameters set: netuid={netuid}"); - - // Register all neurons - for hotkey in &hotkeys { - register_ok_neuron(netuid, *hotkey, coldkey, 0); - log::info!( - "Registered neuron: hotkey={hotkey}, coldkey={coldkey}, netuid={netuid}" - ); - } - - // Set up parent-child relationships - for i in 0..num_keys - 1 { - mock_schedule_children( - &coldkey, - &hotkeys[i], - netuid, - &[(proportion, hotkeys[i + 1])], - ); - log::info!( - "Set parent-child relationship: parent={}, child={}, proportion={}", - hotkeys[i], - hotkeys[i + 1], - proportion - ); - } - // Wait for children to be set - wait_and_set_pending_children(netuid); - - // Test get_parents for each hotkey - for i in 1..num_keys { - let parents = SubtensorModule::get_parents(&hotkeys[i], netuid); - log::info!( - "Testing get_parents for hotkey {}: {:?}", - hotkeys[i], - parents - ); - assert_eq!( - parents.len(), - 1, - "Hotkey {i} should have exactly one parent" - ); - assert_eq!( - parents[0], - (proportion, hotkeys[i - 1]), - "Incorrect parent for hotkey {i}" - ); - } - - // Test get_parents for the root (should be empty) - let root_parents = SubtensorModule::get_parents(&hotkeys[0], netuid); - log::info!( - "Testing get_parents for root hotkey {}: {:?}", - hotkeys[0], - root_parents - ); - assert!( - root_parents.is_empty(), - "Root hotkey should have no parents" - ); - - // Test multiple parents - let last_hotkey = hotkeys[num_keys - 1]; - let new_parent = U256::from(num_keys as u64 + 2); - // Set reg diff back down (adjusted from last block steps) - SubtensorModule::set_difficulty(netuid, 1); - register_ok_neuron(netuid, new_parent, coldkey, 99 * 2); - log::info!( - "Registered new parent neuron: new_parent={new_parent}, coldkey={coldkey}, netuid={netuid}" - ); - - mock_set_children( - &coldkey, - &new_parent, - netuid, - &[(proportion / 2, last_hotkey)], - ); - - log::info!( - "Set additional parent-child relationship: parent={}, child={}, proportion={}", - new_parent, - last_hotkey, - proportion / 2 - ); - - let last_hotkey_parents = SubtensorModule::get_parents(&last_hotkey, netuid); - log::info!( - "Testing get_parents for last hotkey {last_hotkey} with multiple parents: {last_hotkey_parents:?}" - ); - assert_eq!( - last_hotkey_parents.len(), - 2, - "Last hotkey should have two parents" - ); - assert!( - last_hotkey_parents.contains(&(proportion, hotkeys[num_keys - 2])), - "Last hotkey should still have its original parent" - ); - assert!( - last_hotkey_parents.contains(&(proportion / 2, new_parent)), - "Last hotkey should have the new parent" - ); - }); -} - -// 47: Test basic stake retrieval for a single hotkey on a subnet -/// This test verifies the basic functionality of retrieving stake for a single hotkey on a subnet: -/// - Sets up a network with one neuron -/// - Increases stake for the neuron -/// - Checks if the retrieved stake matches the increased amount -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_get_stake_for_hotkey_on_subnet_basic --exact --show-output --nocapture -#[test] -fn test_get_stake_for_hotkey_on_subnet_basic() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey = U256::from(1); - let coldkey = U256::from(2); - - add_network(netuid, 1, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - netuid, - 1000.into(), - ); - assert_eq!( - SubtensorModule::get_inherited_for_hotkey_on_subnet(&hotkey, netuid), - 1000.into() - ); - }); -} - -// 48: Test stake retrieval for a hotkey with multiple coldkeys on a subnet -/// This test verifies the functionality of retrieving stake for a hotkey with multiple coldkeys on a subnet: -/// - Sets up a network with one neuron and two coldkeys -/// - Increases stake from both coldkeys -/// - Checks if the retrieved stake matches the total increased amount -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_get_stake_for_hotkey_on_subnet_multiple_coldkeys --exact --show-output --nocapture -#[test] -fn test_get_stake_for_hotkey_on_subnet_multiple_coldkeys() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey = U256::from(1); - let coldkey1 = U256::from(2); - let coldkey2 = U256::from(3); - - add_network(netuid, 1, 0); - register_ok_neuron(netuid, hotkey, coldkey1, 0); - - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey1, - netuid, - 1000.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey2, - netuid, - 2000.into(), - ); - - assert_eq!( - SubtensorModule::get_inherited_for_hotkey_on_subnet(&hotkey, netuid), - 3000.into() - ); - }); -} - -// 49: Test stake retrieval for a single parent-child relationship on a subnet -/// This test verifies the functionality of retrieving stake for a single parent-child relationship on a subnet: -/// - Sets up a network with a parent and child neuron -/// - Increases stake for the parent -/// - Sets the child as the parent's only child with 100% stake allocation -/// - Checks if the retrieved stake for both parent and child is correct -/// -/// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_get_stake_for_hotkey_on_subnet_single_parent_child --exact --show-output --nocapture -#[test] -fn test_get_stake_for_hotkey_on_subnet_single_parent_child() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let parent = U256::from(1); - let child = U256::from(2); - let coldkey = U256::from(3); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - register_ok_neuron(netuid, parent, coldkey, 0); - register_ok_neuron(netuid, child, coldkey, 0); - - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &parent, - &coldkey, - netuid, - 1_000_000_000.into(), - ); - - mock_set_children_no_epochs(netuid, &parent, &[(u64::MAX, child)]); - - assert_eq!( - SubtensorModule::get_inherited_for_hotkey_on_subnet(&parent, netuid), - 0.into() - ); - assert_eq!( - SubtensorModule::get_inherited_for_hotkey_on_subnet(&child, netuid), - 1_000_000_000.into() - ); - }); -} - -// 50: Test stake retrieval for multiple parents and a single child on a subnet -/// This test verifies the functionality of retrieving stake for multiple parents and a single child on a subnet: -/// - Sets up a network with two parents and one child neuron -/// - Increases stake for both parents -/// - Sets the child as a 50% stake recipient for both parents -/// - Checks if the retrieved stake for parents and child is correct -/// -/// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_get_stake_for_hotkey_on_subnet_multiple_parents_single_child --exact --show-output --nocapture -#[test] -fn test_get_stake_for_hotkey_on_subnet_multiple_parents_single_child() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - let parent1 = U256::from(1); - let parent2 = U256::from(2); - let child = U256::from(3); - let coldkey = U256::from(4); - - register_ok_neuron(netuid, parent1, coldkey, 0); - register_ok_neuron(netuid, parent2, coldkey, 0); - register_ok_neuron(netuid, child, coldkey, 0); - - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &parent1, - &coldkey, - netuid, - 1000.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &parent2, - &coldkey, - netuid, - 2000.into(), - ); - - mock_set_children_no_epochs(netuid, &parent1, &[(u64::MAX / 2, child)]); - mock_set_children_no_epochs(netuid, &parent2, &[(u64::MAX / 2, child)]); - - close( - SubtensorModule::get_inherited_for_hotkey_on_subnet(&parent1, netuid).into(), - 500, - 10, - "Incorrect inherited stake for parent1", - ); - close( - SubtensorModule::get_inherited_for_hotkey_on_subnet(&parent2, netuid).into(), - 1000, - 10, - "Incorrect inherited stake for parent2", - ); - close( - SubtensorModule::get_inherited_for_hotkey_on_subnet(&child, netuid).into(), - 1499, - 10, - "Incorrect inherited stake for child", - ); - }); -} - -// 51: Test stake retrieval for a single parent with multiple children on a subnet -/// This test verifies the functionality of retrieving stake for a single parent with multiple children on a subnet: -/// - Sets up a network with one parent and two child neurons -/// - Increases stake for the parent -/// - Sets both children as 1/3 stake recipients of the parent -/// - Checks if the retrieved stake for parent and children is correct and preserves total stake -/// -/// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_get_stake_for_hotkey_on_subnet_single_parent_multiple_children --exact --show-output --nocapture -#[test] -fn test_get_stake_for_hotkey_on_subnet_single_parent_multiple_children() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - let parent = U256::from(1); - let child1 = U256::from(2); - let child2 = U256::from(3); - let coldkey = U256::from(4); - - register_ok_neuron(netuid, parent, coldkey, 0); - register_ok_neuron(netuid, child1, coldkey, 0); - register_ok_neuron(netuid, child2, coldkey, 0); - - let total_stake = 3000.into(); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &parent, - &coldkey, - netuid, - total_stake, - ); - - mock_set_children_no_epochs( - netuid, - &parent, - &[(u64::MAX / 3, child1), (u64::MAX / 3, child2)], - ); - - let parent_stake = SubtensorModule::get_inherited_for_hotkey_on_subnet(&parent, netuid); - let child1_stake = SubtensorModule::get_inherited_for_hotkey_on_subnet(&child1, netuid); - let child2_stake = SubtensorModule::get_inherited_for_hotkey_on_subnet(&child2, netuid); - - // Check that the total stake is preserved - close( - (parent_stake + child1_stake + child2_stake).into(), - total_stake.into(), - 10, - "Total stake not preserved", - ); - - // Check that the parent stake is slightly higher due to rounding - close(parent_stake.into(), 1000, 10, "Parent stake incorrect"); - - // Check that each child gets an equal share of the remaining stake - close(child1_stake.into(), 1000, 10, "Child1 stake incorrect"); - close(child2_stake.into(), 1000, 10, "Child2 stake incorrect"); - - // Log the actual stake values - log::info!("Parent stake: {parent_stake}"); - log::info!("Child1 stake: {child1_stake}"); - log::info!("Child2 stake: {child2_stake}"); - }); -} - -// 52: Test stake retrieval for edge cases on a subnet -/// This test verifies the functionality of retrieving stake for edge cases on a subnet: -/// - Sets up a network with one parent and two child neurons -/// - Increases stake to the network maximum -/// - Sets children with 0% and 100% stake allocation -/// - Checks if the retrieved stake for parent and children is correct and preserves total stake -/// -/// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_get_stake_for_hotkey_on_subnet_edge_cases --exact --show-output --nocapture -#[test] -fn test_get_stake_for_hotkey_on_subnet_edge_cases() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - let parent = U256::from(1); - let child1 = U256::from(2); - let child2 = U256::from(3); - let coldkey = U256::from(4); - - register_ok_neuron(netuid, parent, coldkey, 0); - register_ok_neuron(netuid, child1, coldkey, 0); - register_ok_neuron(netuid, child2, coldkey, 0); - - // Set above old value of network max stake - let network_max_stake = 600_000_000_000_000_u64.into(); - - // Increase stake to the network max - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &parent, - &coldkey, - netuid, - network_max_stake, - ); - - // Test with 0% and 100% stake allocation - mock_set_children_no_epochs(netuid, &parent, &[(0, child1), (u64::MAX, child2)]); - - let parent_stake = SubtensorModule::get_inherited_for_hotkey_on_subnet(&parent, netuid); - let child1_stake = SubtensorModule::get_inherited_for_hotkey_on_subnet(&child1, netuid); - let child2_stake = SubtensorModule::get_inherited_for_hotkey_on_subnet(&child2, netuid); - - log::info!("Parent stake: {parent_stake}"); - log::info!("Child1 stake: {child1_stake}"); - log::info!("Child2 stake: {child2_stake}"); - - assert_eq!(parent_stake, 0.into(), "Parent should have 0 stake"); - assert_eq!(child1_stake, 0.into(), "Child1 should have 0 stake"); - assert_eq!( - child2_stake, network_max_stake, - "Child2 should have all the stake" - ); - - // Check that the total stake is preserved and equal to the network max stake - close( - (parent_stake + child1_stake + child2_stake).into(), - network_max_stake.into(), - 10, - "Total stake should equal network max stake", - ); - }); -} - -// 53: Test stake distribution in a complex hierarchy of parent-child relationships -// This test verifies the correct distribution of stake in a multi-level parent-child hierarchy: -// - Sets up a network with four neurons: parent, child1, child2, and grandchild -// - Establishes parent-child relationships between parent and its children, and child1 and grandchild -// - Adds initial stake to the parent -// - Checks stake distribution after setting up the first level of relationships -// - Checks stake distribution after setting up the second level of relationships -// - Verifies correct stake calculations, parent-child relationships, and preservation of total stake -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_get_stake_for_hotkey_on_subnet_complex_hierarchy --exact --show-output --nocapture -#[test] -fn test_get_stake_for_hotkey_on_subnet_complex_hierarchy() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - let parent = U256::from(1); - let child1 = U256::from(2); - let child2 = U256::from(3); - let grandchild = U256::from(4); - let coldkey_parent = U256::from(5); - let coldkey_child1 = U256::from(6); - let coldkey_child2 = U256::from(7); - let coldkey_grandchild = U256::from(8); - - SubtensorModule::set_max_registrations_per_block(netuid, 1000); - SubtensorModule::set_target_registrations_per_interval(netuid, 1000); - register_ok_neuron(netuid, parent, coldkey_parent, 0); - register_ok_neuron(netuid, child1, coldkey_child1, 0); - register_ok_neuron(netuid, child2, coldkey_child2, 0); - register_ok_neuron(netuid, grandchild, coldkey_grandchild, 0); - - let total_stake = 1000.into(); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &parent, - &coldkey_parent, - netuid, - total_stake, - ); - - log::info!("Initial stakes:"); - log::info!( - "Parent stake: {}", - SubtensorModule::get_inherited_for_hotkey_on_subnet(&parent, netuid) - ); - log::info!( - "Child1 stake: {}", - SubtensorModule::get_inherited_for_hotkey_on_subnet(&child1, netuid) - ); - log::info!( - "Child2 stake: {}", - SubtensorModule::get_inherited_for_hotkey_on_subnet(&child2, netuid) - ); - log::info!( - "Grandchild stake: {}", - SubtensorModule::get_inherited_for_hotkey_on_subnet(&grandchild, netuid) - ); - - // Step 1: Set children for parent - mock_set_children_no_epochs( - netuid, - &parent, - &[(u64::MAX / 2, child1), (u64::MAX / 2, child2)], - ); - - log::info!("After setting parent's children:"); - log::info!( - "Parent's children: {:?}", - SubtensorModule::get_children(&parent, netuid) - ); - log::info!( - "Child1's parents: {:?}", - SubtensorModule::get_parents(&child1, netuid) - ); - log::info!( - "Child2's parents: {:?}", - SubtensorModule::get_parents(&child2, netuid) - ); - - let parent_stake_1 = SubtensorModule::get_inherited_for_hotkey_on_subnet(&parent, netuid); - let child1_stake_1 = SubtensorModule::get_inherited_for_hotkey_on_subnet(&child1, netuid); - let child2_stake_1 = SubtensorModule::get_inherited_for_hotkey_on_subnet(&child2, netuid); - - log::info!("Parent stake: {parent_stake_1}"); - log::info!("Child1 stake: {child1_stake_1}"); - log::info!("Child2 stake: {child2_stake_1}"); - - assert_eq!( - parent_stake_1, - 0.into(), - "Parent should have 0 stake after distributing all stake to children" - ); - close( - child1_stake_1.into(), - 499, - 10, - "Child1 should have 499 stake", - ); - close( - child2_stake_1.into(), - 499, - 10, - "Child2 should have 499 stake", - ); - - // Step 2: Set children for child1 - mock_set_children_no_epochs(netuid, &child1, &[(u64::MAX, grandchild)]); - - log::info!("After setting child1's children:"); - log::info!( - "Child1's children: {:?}", - SubtensorModule::get_children(&child1, netuid) - ); - log::info!( - "Grandchild's parents: {:?}", - SubtensorModule::get_parents(&grandchild, netuid) - ); - - let parent_stake_2 = SubtensorModule::get_inherited_for_hotkey_on_subnet(&parent, netuid); - let child1_stake_2 = SubtensorModule::get_inherited_for_hotkey_on_subnet(&child1, netuid); - let child2_stake_2 = SubtensorModule::get_inherited_for_hotkey_on_subnet(&child2, netuid); - let grandchild_stake = - SubtensorModule::get_inherited_for_hotkey_on_subnet(&grandchild, netuid); - - log::info!("Parent stake: {parent_stake_2}"); - log::info!("Child1 stake: {child1_stake_2}"); - log::info!("Child2 stake: {child2_stake_2}"); - log::info!("Grandchild stake: {grandchild_stake}"); - - close(parent_stake_2.into(), 0, 10, "Parent stake should remain 2"); - close( - child1_stake_2.into(), - 499, - 10, - "Child1 should still have 499 stake", - ); - close( - child2_stake_2.into(), - 499, - 10, - "Child2 should still have 499 stake", - ); - close( - grandchild_stake.into(), - 0, - 10, - "Grandchild should have 0 stake, as child1 doesn't have any owned stake", - ); - - // Check that the total stake is preserved - close( - (parent_stake_2 + child1_stake_2 + child2_stake_2 + grandchild_stake).into(), - total_stake.into(), - 10, - "Total stake should equal the initial stake", - ); - - // Additional checks - log::info!("Final parent-child relationships:"); - log::info!( - "Parent's children: {:?}", - SubtensorModule::get_children(&parent, netuid) - ); - log::info!( - "Child1's parents: {:?}", - SubtensorModule::get_parents(&child1, netuid) - ); - log::info!( - "Child2's parents: {:?}", - SubtensorModule::get_parents(&child2, netuid) - ); - log::info!( - "Child1's children: {:?}", - SubtensorModule::get_children(&child1, netuid) - ); - log::info!( - "Grandchild's parents: {:?}", - SubtensorModule::get_parents(&grandchild, netuid) - ); - - // Check if the parent-child relationships are correct - assert_eq!( - SubtensorModule::get_children(&parent, netuid), - vec![(u64::MAX / 2, child1), (u64::MAX / 2, child2)], - "Parent should have both children" - ); - assert_eq!( - SubtensorModule::get_parents(&child1, netuid), - vec![(u64::MAX / 2, parent)], - "Child1 should have parent as its parent" - ); - assert_eq!( - SubtensorModule::get_parents(&child2, netuid), - vec![(u64::MAX / 2, parent)], - "Child2 should have parent as its parent" - ); - assert_eq!( - SubtensorModule::get_children(&child1, netuid), - vec![(u64::MAX, grandchild)], - "Child1 should have grandchild as its child" - ); - assert_eq!( - SubtensorModule::get_parents(&grandchild, netuid), - vec![(u64::MAX, child1)], - "Grandchild should have child1 as its parent" - ); - }); -} - -// 54: Test stake distribution across multiple networks -// This test verifies the correct distribution of stake for a single neuron across multiple networks: -// - Sets up two networks with a single neuron registered on both -// - Adds initial stake to the neuron -// - Checks that the stake is correctly reflected on both networks -// - Verifies that changes in stake are consistently applied across all networks -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_get_stake_for_hotkey_on_subnet_multiple_networks --exact --show-output --nocapture -#[test] -fn test_get_stake_for_hotkey_on_subnet_multiple_networks() { - new_test_ext(1).execute_with(|| { - let netuid1 = NetUid::from(1); - let netuid2 = NetUid::from(2); - let hotkey = U256::from(1); - let coldkey = U256::from(2); - - add_network(netuid1, 1, 0); - add_network(netuid2, 1, 0); - register_ok_neuron(netuid1, hotkey, coldkey, 0); - register_ok_neuron(netuid2, hotkey, coldkey, 0); - - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - netuid1, - 1000.into(), - ); - - close( - SubtensorModule::get_inherited_for_hotkey_on_subnet(&hotkey, netuid1).into(), - 1000, - 10, - "Stake on network 1 incorrect", - ); - close( - SubtensorModule::get_inherited_for_hotkey_on_subnet(&hotkey, netuid2).into(), - 0, - 10, - "Stake on network 2 incorrect", - ); - }); -} - -// Test that min stake is enforced for setting children -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_set_child_below_min_stake --exact --show-output --nocapture -#[test] -fn test_do_set_child_below_min_stake() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child = U256::from(3); - let netuid = NetUid::from(1); - let proportion: u64 = 1000; - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - StakeThreshold::::set(1_000_000_000_000); - - // Attempt to set child - assert_err!( - SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - vec![(proportion, child)] - ), - Error::::NotEnoughStakeToSetChildkeys - ); - }); -} - -/// --- test_do_remove_stake_clears_pending_childkeys --- -/// -/// Test Description: Ensures that removing stake clears any pending childkeys. -/// -/// Expected Behavior: -/// - Pending childkeys should be cleared when stake is removed -/// - Cooldown block should be reset to 0 -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_remove_stake_clears_pending_childkeys --exact --show-output --nocapture -#[test] -fn test_do_remove_stake_clears_pending_childkeys() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child = U256::from(3); - let netuid = NetUid::from(1); - let proportion: u64 = 1000; - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - add_balance_to_coldkey_account(&coldkey, 10_000_000_000_000_u64.into()); - SubtokenEnabled::::insert(netuid, true); - - let reserve = 1_000_000_000_000_000_u64; - mock::setup_reserves(netuid, reserve.into(), reserve.into()); - - // Set non-default value for childkey stake threshold - StakeThreshold::::set(1_000_000_000_000); - - assert_ok!(SubtensorModule::do_add_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - (StakeThreshold::::get() * 2).into() - )); - - let alpha = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); - - println!( - "StakeThreshold::::get() = {:?}", - StakeThreshold::::get() - ); - println!("alpha = {alpha:?}"); - - // Attempt to set child - assert_ok!(SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - vec![(proportion, child)] - )); - - // Check that pending child exists - let pending_before = PendingChildKeys::::get(netuid, hotkey); - assert!(!pending_before.0.is_empty()); - assert!(pending_before.1 > 0); - - // Remove stake - assert_ok!(SubtensorModule::do_remove_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - alpha, - )); - - // Assert that pending child is removed - let pending_after = PendingChildKeys::::get(netuid, hotkey); - close( - pending_after.0.len() as u64, - 0, - 0, - "Pending children vector should be empty", - ); - close(pending_after.1, 0, 0, "Cooldown block should be zero"); - }); -} - -// Test that pending childkeys do not apply immediately and apply after cooldown period -// -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_set_child_cooldown_period --exact --show-output --nocapture -#[cfg(test)] -#[test] -fn test_do_set_child_cooldown_period() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let parent = U256::from(2); - let child = U256::from(3); - let netuid = NetUid::from(1); - let proportion: u64 = 1000; - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, parent, coldkey, 0); - - // Set minimum stake for setting children - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &parent, - &coldkey, - netuid, - StakeThreshold::::get().into(), - ); - - // Schedule parent-child relationship - assert_ok!(SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - parent, - netuid, - vec![(proportion, child)], - )); - - // Ensure the childkeys are not yet applied - let children_before = SubtensorModule::get_children(&parent, netuid); - close( - children_before.len() as u64, - 0, - 0, - "Children vector should be empty before cooldown", - ); - - wait_and_set_pending_children(netuid); - SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( - &parent, - &coldkey, - netuid, - StakeThreshold::::get().into(), - ); - - // Verify child assignment - let children_after = SubtensorModule::get_children(&parent, netuid); - close( - children_after.len() as u64, - 1, - 0, - "Children vector should have one entry after cooldown", - ); - close( - children_after[0].0, - proportion, - 0, - "Child proportion should match", - ); - close( - children_after[0].1.try_into().unwrap(), - child.try_into().unwrap(), - 0, - "Child key should match", - ); - }); -} - -// Test that pending childkeys get set during the epoch after the cooldown period. -// -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_set_pending_children_runs_in_epoch --exact --show-output --nocapture -#[cfg(test)] -#[test] -fn test_do_set_pending_children_runs_in_epoch() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let parent = U256::from(2); - let child = U256::from(3); - let netuid = NetUid::from(1); - let proportion: u64 = 1000; - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, parent, coldkey, 0); - - // Set minimum stake for setting children - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &parent, - &coldkey, - netuid, - StakeThreshold::::get().into(), - ); - - // Schedule parent-child relationship - assert_ok!(SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - parent, - netuid, - vec![(proportion, child)], - )); - - // Ensure the childkeys are not yet applied - let children_before = SubtensorModule::get_children(&parent, netuid); - close( - children_before.len() as u64, - 0, - 0, - "Children vector should be empty before cooldown", - ); - - wait_set_pending_children_cooldown(netuid); - - // Verify child assignment - let children_after = SubtensorModule::get_children(&parent, netuid); - close( - children_after.len() as u64, - 1, - 0, - "Children vector should have one entry after cooldown", - ); - close( - children_after[0].0, - proportion, - 0, - "Child proportion should match", - ); - close( - children_after[0].1.try_into().unwrap(), - child.try_into().unwrap(), - 0, - "Child key should match", - ); - }); -} - -// Test that revoking childkeys does not require minimum stake -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_revoke_child_no_min_stake_check --exact --show-output --nocapture -#[test] -fn test_revoke_child_no_min_stake_check() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let parent = U256::from(2); - let child = U256::from(3); - let netuid = NetUid::from(1); - let proportion: u64 = 1000; - - // Add network and register hotkey - add_network(NetUid::ROOT, 13, 0); - add_network(netuid, 13, 0); - register_ok_neuron(netuid, parent, coldkey, 0); - - let reserve = 1_000_000_000_000_000_u64; - mock::setup_reserves(netuid, reserve.into(), reserve.into()); - mock::setup_reserves(NetUid::ROOT, reserve.into(), reserve.into()); - - // Set minimum stake for setting children - StakeThreshold::::put(1_000_000_000_000); - - let (_, fee) = mock::swap_tao_to_alpha(NetUid::ROOT, StakeThreshold::::get().into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &parent, - &coldkey, - NetUid::ROOT, - (StakeThreshold::::get() + fee).into(), - ); - - // Schedule parent-child relationship - assert_ok!(SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - parent, - netuid, - vec![(proportion, child)], - )); - - // Ensure the childkeys are not yet applied - let children_before = SubtensorModule::get_children(&parent, netuid); - assert_eq!(children_before, vec![]); - - wait_and_set_pending_children(netuid); - SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( - &parent, - &coldkey, - NetUid::ROOT, - (StakeThreshold::::get() + fee).into(), - ); - - // Ensure the childkeys are applied - let children_after = SubtensorModule::get_children(&parent, netuid); - assert_eq!(children_after, vec![(proportion, child)]); - - // Bypass tx rate limit - TransactionType::SetChildren.set_last_block_on_subnet::(&parent, netuid, 0); - - // Schedule parent-child relationship revokation - assert_ok!(SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - parent, - netuid, - vec![], - )); - - wait_and_set_pending_children(netuid); - - // Ensure the childkeys are revoked - let children_after = SubtensorModule::get_children(&parent, netuid); - assert_eq!(children_after, vec![]); - }); -} - -// Test that setting childkeys works even if subnet registration is disabled -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_do_set_child_registration_disabled --exact --show-output --nocapture -#[test] -fn test_do_set_child_registration_disabled() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let parent = U256::from(2); - let child = U256::from(3); - let netuid = NetUid::from(1); - let proportion: u64 = 1000; - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, parent, coldkey, 0); - - let reserve = 1_000_000_000_000_000_u64; - mock::setup_reserves(netuid, reserve.into(), reserve.into()); - - // Set minimum stake for setting children - StakeThreshold::::put(1_000_000_000_000); - let (_, fee) = mock::swap_tao_to_alpha(netuid, StakeThreshold::::get().into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &parent, - &coldkey, - netuid, - (StakeThreshold::::get() + fee).into(), - ); - - // Disable subnet registrations - NetworkRegistrationAllowed::::insert(netuid, false); - - // Schedule parent-child relationship - assert_ok!(SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - parent, - netuid, - vec![(proportion, child)], - )); - - wait_and_set_pending_children(netuid); - SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( - &parent, - &coldkey, - netuid, - (StakeThreshold::::get() + fee).into(), - ); - - // Ensure the childkeys are applied - let children_after = SubtensorModule::get_children(&parent, netuid); - assert_eq!(children_after, vec![(proportion, child)]); - }); -} - -// 60: Test set_children rate limiting - Fail then succeed -// This test ensures that an immediate second `set_children` transaction fails due to rate limiting: -// - Sets up a network and registers a hotkey -// - Performs a `set_children` transaction -// - Attempts a second `set_children` transaction immediately -// - Verifies that the second transaction fails with `TxRateLimitExceeded` -// Then the rate limit period passes and the second transaction succeeds -// - Steps blocks for the rate limit period -// - Attempts the second transaction again and verifies it succeeds -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_set_children_rate_limit_fail_then_succeed --exact --show-output --nocapture -#[test] -fn test_set_children_rate_limit_fail_then_succeed() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child = U256::from(3); - let child2 = U256::from(4); - let netuid = NetUid::from(1); - let tempo = 13; - - // Add network and register hotkey - add_network(netuid, tempo, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // First set_children transaction - mock_set_children(&coldkey, &hotkey, netuid, &[(100, child)]); - - // Immediate second transaction should fail due to rate limit - assert_noop!( - SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - vec![(100, child2)] - ), - Error::::TxRateLimitExceeded - ); - - // Verify first children assignment remains - let children = SubtensorModule::get_children(&hotkey, netuid); - assert_eq!(children, vec![(100, child)]); - - // Try again after rate limit period has passed - // Check rate limit - let limit = TransactionType::SetChildren.rate_limit_on_subnet::(netuid); - - // Step that many blocks - step_block(limit as u16); - - // Verify rate limit passes - assert!(TransactionType::SetChildren.passes_rate_limit_on_subnet::(&hotkey, netuid)); - - // Try again - mock_set_children(&coldkey, &hotkey, netuid, &[(100, child2)]); - - // Verify children assignment has changed - let children = SubtensorModule::get_children(&hotkey, netuid); - assert_eq!(children, vec![(100, child2)]); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_childkey_set_weights_single_parent --exact --show-output --nocapture -#[test] -fn test_childkey_set_weights_single_parent() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let netuid = - add_dynamic_network_disable_commit_reveal(&subnet_owner_hotkey, &subnet_owner_coldkey); - Tempo::::insert(netuid, 1); - - // Define hotkeys - let parent: U256 = U256::from(1); - let child: U256 = U256::from(2); - let weight_setter: U256 = U256::from(3); - - // Define coldkeys with more readable names - let coldkey_parent: U256 = U256::from(100); - let coldkey_child: U256 = U256::from(101); - let coldkey_weight_setter: U256 = U256::from(102); - - let balance_to_give_child = TaoBalance::from(109_999); - let stake_to_give_child = AlphaBalance::from(109_999); - - // Register parent with minimal stake and child with high stake - add_balance_to_coldkey_account(&coldkey_parent, 1.into()); - add_balance_to_coldkey_account(&coldkey_child, balance_to_give_child + 10.into()); - add_balance_to_coldkey_account(&coldkey_weight_setter, 1_000_000.into()); - - // Add neurons for parent, child and weight_setter - register_ok_neuron(netuid, parent, coldkey_parent, 1); - register_ok_neuron(netuid, child, coldkey_child, 1); - register_ok_neuron(netuid, weight_setter, coldkey_weight_setter, 1); - - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &parent, - &coldkey_parent, - netuid, - stake_to_give_child, - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &weight_setter, - &coldkey_weight_setter, - netuid, - 1_000_000.into(), - ); - - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - // Set parent-child relationship - mock_set_children_no_epochs(netuid, &parent, &[(u64::MAX, child)]); - - // Set weights on the child using the weight_setter account - let origin = RuntimeOrigin::signed(weight_setter); - let uids: Vec = vec![1]; // Only set weight for the child (UID 1) - let values: Vec = vec![u16::MAX]; // Use maximum value for u16 - let version_key = SubtensorModule::get_weights_version_key(netuid); - ValidatorPermit::::insert(netuid, vec![true, true, true, true]); - assert_ok!(SubtensorModule::set_weights( - origin, - netuid, - uids.clone(), - values.clone(), - version_key - )); - - // Set the min stake very high - SubtensorModule::set_stake_threshold(u64::from(stake_to_give_child) * 5); - - // Check the child has less stake than required - assert!( - SubtensorModule::get_stake_weights_for_hotkey_on_subnet(&child, netuid).0 - < SubtensorModule::get_stake_threshold() - ); - - // Check the child cannot set weights - assert_noop!( - SubtensorModule::set_weights( - RuntimeOrigin::signed(child), - netuid, - uids.clone(), - values.clone(), - version_key - ), - Error::::NotEnoughStakeToSetWeights - ); - - assert!(!SubtensorModule::check_weights_min_stake(&child, netuid)); - - // Set a minimum stake to set weights - SubtensorModule::set_stake_threshold(u64::from(stake_to_give_child) - 5); - - // Check if the stake for the child is above - assert!( - SubtensorModule::get_stake_weights_for_hotkey_on_subnet(&child, netuid).0 - >= SubtensorModule::get_stake_threshold() - ); - - // Check the child can set weights - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(child), - netuid, - uids, - values, - version_key - )); - - assert!(SubtensorModule::check_weights_min_stake(&child, netuid)); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --test children -- test_set_weights_no_parent --exact --nocapture -#[test] -fn test_set_weights_no_parent() { - // Verify that a regular key without a parent delegation is effected by the minimum stake requirements - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let netuid = - add_dynamic_network_disable_commit_reveal(&subnet_owner_hotkey, &subnet_owner_coldkey); - - let hotkey: U256 = U256::from(2); - let spare_hk: U256 = U256::from(3); - - let coldkey: U256 = U256::from(101); - let spare_ck = U256::from(102); - - let balance_to_give_child = TaoBalance::from(109_999); - let stake_to_give_child = AlphaBalance::from(109_999); - - add_balance_to_coldkey_account(&coldkey, balance_to_give_child + 10.into()); - - // Is registered - register_ok_neuron(netuid, hotkey, coldkey, 1); - // Register a spare key - register_ok_neuron(netuid, spare_hk, spare_ck, 1); - - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - netuid, - stake_to_give_child, - ); - - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - // Has stake and no parent - step_block(7200 + 1); - - let uids: Vec = vec![1]; // Set weights on the other hotkey - let values: Vec = vec![u16::MAX]; // Use maximum value for u16 - let version_key = SubtensorModule::get_weights_version_key(netuid); - - // Check the stake weight - let curr_stake_weight = - SubtensorModule::get_stake_weights_for_hotkey_on_subnet(&hotkey, netuid).0; - - // Set the min stake very high, above the stake weight of the key - SubtensorModule::set_stake_threshold( - curr_stake_weight - .saturating_mul(I64F64::saturating_from_num(5)) - .saturating_to_num::(), - ); - - let curr_stake_threshold = SubtensorModule::get_stake_threshold(); - assert!( - curr_stake_weight < curr_stake_threshold, - "{curr_stake_weight:?} is not less than {curr_stake_threshold:?} " - ); - - // Check the hotkey cannot set weights - assert_noop!( - SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - values.clone(), - version_key - ), - Error::::NotEnoughStakeToSetWeights - ); - - assert!(!SubtensorModule::check_weights_min_stake(&hotkey, netuid)); - - // Set a minimum stake to set weights - SubtensorModule::set_stake_threshold( - (curr_stake_weight - I64F64::from_num(5)).to_num::(), - ); - - // Check if the stake for the hotkey is above - let new_stake_weight = - SubtensorModule::get_stake_weights_for_hotkey_on_subnet(&hotkey, netuid).0; - let new_stake_threshold = SubtensorModule::get_stake_threshold(); - assert!( - new_stake_weight >= new_stake_threshold, - "{new_stake_weight:?} is not greater than or equal to {new_stake_threshold:?} " - ); - - // Check the hotkey can set weights - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids, - values, - version_key - )); - - assert!(SubtensorModule::check_weights_min_stake(&hotkey, netuid)); - }); -} - -/// Test that distribute_emission sends childkey take fully to the nominators if childkey -/// doesn't have its own stake, independently of parent hotkey take. -/// cargo test --package pallet-subtensor --lib -- tests::children::test_childkey_take_drain --exact --show-output -#[allow(clippy::assertions_on_constants)] -#[test] -fn test_childkey_take_drain() { - // Test cases: parent_hotkey_take - [0_u16, u16::MAX / 5].iter().for_each(|parent_hotkey_take| { - new_test_ext(1).execute_with(|| { - let parent_coldkey = U256::from(1); - let parent_hotkey = U256::from(3); - let child_coldkey = U256::from(2); - let child_hotkey = U256::from(4); - let miner_coldkey = U256::from(5); - let miner_hotkey = U256::from(6); - let nominator = U256::from(7); - let netuid = NetUid::from(1); - let subnet_tempo = 10; - let stake = 100_000_000_000_u64; - let proportion: u64 = u64::MAX / 2; - - // Add network, register hotkeys, and setup network parameters - add_network(netuid, subnet_tempo, 0); - SubtensorModule::set_ck_burn(0); - mock::setup_reserves(netuid, (stake * 10_000).into(), (stake * 10_000).into()); - register_ok_neuron(netuid, child_hotkey, child_coldkey, 0); - register_ok_neuron(netuid, parent_hotkey, parent_coldkey, 1); - register_ok_neuron(netuid, miner_hotkey, miner_coldkey, 1); - add_balance_to_coldkey_account( - &parent_coldkey, - TaoBalance::from(stake) + ExistentialDeposit::get(), - ); - add_balance_to_coldkey_account( - &nominator, - TaoBalance::from(stake) + ExistentialDeposit::get(), - ); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - SubtensorModule::set_max_allowed_validators(netuid, 2); - step_block(subnet_tempo); - SubnetOwnerCut::::set(0); - - // Set children - mock_set_children_no_epochs(netuid, &parent_hotkey, &[(proportion, child_hotkey)]); - - // Set 20% childkey take - let max_take: u16 = 0xFFFF / 5; - SubtensorModule::set_max_childkey_take(PerU16::from_parts(max_take)); - assert_ok!(SubtensorModule::set_childkey_take( - RuntimeOrigin::signed(child_coldkey), - child_hotkey, - netuid, - PerU16::from_parts(max_take) - )); - - // Set hotkey take for parent - SubtensorModule::set_max_delegate_take(PerU16::from_parts(*parent_hotkey_take)); - Delegates::::insert(parent_hotkey, PerU16::from_parts(*parent_hotkey_take)); - - // Set 0% for childkey-as-a-delegate take - Delegates::::insert(child_hotkey, PerU16::zero()); - - // Setup stakes: - // Stake from parent - // Stake from nominator to childkey - // Parent gives 50% of stake to childkey - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(parent_coldkey), - parent_hotkey, - netuid, - stake.into() - )); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(nominator), - child_hotkey, - netuid, - stake.into() - )); - - // Setup YUMA so that it creates emissions - Weights::::insert(NetUidStorageIndex::from(netuid), 0, vec![(2, 0xFFFF)]); - Weights::::insert(NetUidStorageIndex::from(netuid), 1, vec![(2, 0xFFFF)]); - BlockAtRegistration::::set(netuid, 0, 1); - BlockAtRegistration::::set(netuid, 1, 1); - BlockAtRegistration::::set(netuid, 2, 1); - LastUpdate::::set(NetUidStorageIndex::from(netuid), vec![2, 2, 2]); - Kappa::::set(netuid, u16::MAX / 5); - ActivityCutoff::::set(netuid, u16::MAX); // makes all stake active - ValidatorPermit::::insert(netuid, vec![true, true, false]); - - // Run run_coinbase to hit subnet epoch - let child_stake_before = SubtensorModule::get_total_stake_for_coldkey(&child_coldkey); - let parent_stake_before = SubtensorModule::get_total_stake_for_coldkey(&parent_coldkey); - let nominator_stake_before = SubtensorModule::get_total_stake_for_coldkey(&nominator); - - step_block(subnet_tempo); - - // Verify how emission is split between keys - // - Child stake remains 0 - // - Childkey take is 20% of its total emission that rewards both inherited from - // parent stake and nominated stake, which all goes to nominators. Because child - // validator emission is 50% of total emission, 20% of it is 10% of total emission - // and it all goes to nominator. If childkey take was 0%, then only 5% would go to - // the nominator, so the final solit is: - // - Parent stake increases by 45% of total emission - // - Nominator stake increases by 55% of total emission - let child_emission = - SubtensorModule::get_total_stake_for_coldkey(&child_coldkey) - child_stake_before; - let parent_emission = - SubtensorModule::get_total_stake_for_coldkey(&parent_coldkey) - parent_stake_before; - let nominator_emission = - SubtensorModule::get_total_stake_for_coldkey(&nominator) - nominator_stake_before; - let total_emission = child_emission + parent_emission + nominator_emission; - - assert_abs_diff_eq!(child_emission, TaoBalance::ZERO, epsilon = 10.into()); - assert_abs_diff_eq!( - parent_emission, - total_emission * 9.into() / 20.into(), - epsilon = 10.into() - ); - assert_abs_diff_eq!( - nominator_emission, - total_emission * 11.into() / 20.into(), - epsilon = 10.into() - ); - }); - }); -} - -// 44: Test with a chain of parent-child relationships (e.g., A -> B -> C) -// This test verifies the correct distribution of emissions in a chain of parent-child relationships: -// - Sets up a network with three neurons A, B, and C in a chain (A -> B -> C) -// - Establishes parent-child relationships with different stake proportions -// - Sets weights for all neurons -// - Runs an epoch with a hardcoded emission value -// - Checks the emission distribution among A, B, and C -// - Verifies that all parties received emissions and the total stake increased correctly -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_parent_child_chain_emission --exact --show-output -#[test] -fn test_parent_child_chain_emission() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - remove_owner_registration_stake(netuid); - SubtensorModule::set_ck_burn(0); - Tempo::::insert(netuid, 1); - - // Setup large LPs to prevent slippage - SubnetTAO::::insert(netuid, TaoBalance::from(1_000_000_000_000_000_u64)); - SubnetAlphaIn::::insert(netuid, AlphaBalance::from(1_000_000_000_000_000_u64)); - - // Set owner cut to 0 - SubtensorModule::set_subnet_owner_cut(0_u16); - - // Define hotkeys and coldkeys - let hotkey_a: U256 = U256::from(1); - let hotkey_b: U256 = U256::from(2); - let hotkey_c: U256 = U256::from(3); - let coldkey_a: U256 = U256::from(100); - let coldkey_b: U256 = U256::from(101); - let coldkey_c: U256 = U256::from(102); - - // Register neurons with decreasing stakes - register_ok_neuron(netuid, hotkey_a, coldkey_a, 0); - register_ok_neuron(netuid, hotkey_b, coldkey_b, 0); - register_ok_neuron(netuid, hotkey_c, coldkey_c, 0); - - // Add initial stakes - add_balance_to_coldkey_account(&coldkey_a, 1_000.into()); - add_balance_to_coldkey_account(&coldkey_b, 1_000.into()); - add_balance_to_coldkey_account(&coldkey_c, 1_000.into()); - - // Swap to alpha - let stake_a = 300_000_000_000_u64; - let stake_b = 100_000_000_000_u64; - let stake_c = 50_000_000_000_u64; - let total_tao: I96F32 = I96F32::from_num(stake_a + stake_b + stake_c); - let total_alpha: I96F32 = I96F32::from_num( - SubtensorModule::swap_tao_for_alpha( - netuid, - total_tao.to_num::().into(), - ::SwapInterface::max_price(), - false, - ) - .unwrap() - .amount_paid_out, - ); - - // Set the stakes directly - // This avoids needing to swap tao to alpha, impacting the initial stake distribution. - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_a, - &coldkey_a, - netuid, - (total_alpha * I96F32::from_num(stake_a) / total_tao) - .saturating_to_num::() - .into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_b, - &coldkey_b, - netuid, - (total_alpha * I96F32::from_num(stake_b) / total_tao) - .saturating_to_num::() - .into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_c, - &coldkey_c, - netuid, - (total_alpha * I96F32::from_num(stake_c) / total_tao) - .saturating_to_num::() - .into(), - ); - - // Get old stakes - let stake_a = SubtensorModule::get_total_stake_for_hotkey(&hotkey_a); - let stake_b = SubtensorModule::get_total_stake_for_hotkey(&hotkey_b); - let stake_c = SubtensorModule::get_total_stake_for_hotkey(&hotkey_c); - - let _total_stake: I96F32 = I96F32::from_num(stake_a + stake_b + stake_c); - - // Assert initial stake is correct - let rel_stake_a = I96F32::from_num(stake_a) / total_tao; - let rel_stake_b = I96F32::from_num(stake_b) / total_tao; - let rel_stake_c = I96F32::from_num(stake_c) / total_tao; - - log::info!("rel_stake_a: {rel_stake_a:?}"); // 0.6666 -> 2/3 - log::info!("rel_stake_b: {rel_stake_b:?}"); // 0.2222 -> 2/9 - log::info!("rel_stake_c: {rel_stake_c:?}"); // 0.1111 -> 1/9 - assert!((rel_stake_a - I96F32::from_num(stake_a) / total_tao).abs() < 0.001); - assert!((rel_stake_b - I96F32::from_num(stake_b) / total_tao).abs() < 0.001); - assert!((rel_stake_c - I96F32::from_num(stake_c) / total_tao).abs() < 0.001); - - // Set parent-child relationships - // A -> B (50% of A's stake) - mock_set_children_no_epochs(netuid, &hotkey_a, &[(u64::MAX / 2, hotkey_b)]); - - // B -> C (50% of B's stake) - mock_set_children_no_epochs(netuid, &hotkey_b, &[(u64::MAX / 2, hotkey_c)]); - - // Get old stakes after children are scheduled - let stake_a_old = SubtensorModule::get_total_stake_for_hotkey(&hotkey_a); - let stake_b_old = SubtensorModule::get_total_stake_for_hotkey(&hotkey_b); - let stake_c_old = SubtensorModule::get_total_stake_for_hotkey(&hotkey_c); - - let total_stake_old: I96F32 = - I96F32::from_num((stake_a_old + stake_b_old + stake_c_old).to_u64()); - log::info!("Old stake for hotkey A: {stake_a_old:?}"); - log::info!("Old stake for hotkey B: {stake_b_old:?}"); - log::info!("Old stake for hotkey C: {stake_c_old:?}"); - log::info!("Total old stake: {total_stake_old:?}"); - - // Set CHK take rate to 1/9 - let chk_take: I96F32 = I96F32::from_num(1_f64 / 9_f64); - let chk_take_u16: u16 = (chk_take * I96F32::from_num(u16::MAX)).saturating_to_num::(); - ChildkeyTake::::insert(hotkey_b, netuid, PerU16::from_parts(chk_take_u16)); - ChildkeyTake::::insert(hotkey_c, netuid, PerU16::from_parts(chk_take_u16)); - - // Set the weight of root TAO to be 0%, so only alpha is effective. - SubtensorModule::set_tao_weight(0); - - let emission = SubtensorModule::get_block_emission(); - - // Set pending emission to 0 - PendingValidatorEmission::::insert(netuid, AlphaBalance::ZERO); - PendingServerEmission::::insert(netuid, AlphaBalance::ZERO); - - // To trigger the epoch, block should be > tempo. So we advance it before - System::set_block_number(2); - - // Run epoch with emission value - let emission_value = u64::from(emission.peek()); - SubtensorModule::run_coinbase(emission); - - // Log new stake - let stake_a_new = SubtensorModule::get_total_stake_for_hotkey(&hotkey_a); - let stake_b_new = SubtensorModule::get_total_stake_for_hotkey(&hotkey_b); - let stake_c_new = SubtensorModule::get_total_stake_for_hotkey(&hotkey_c); - let total_stake_new = I96F32::from_num((stake_a_new + stake_b_new + stake_c_new).to_u64()); - log::info!("Stake for hotkey A: {stake_a_new:?}"); - log::info!("Stake for hotkey B: {stake_b_new:?}"); - log::info!("Stake for hotkey C: {stake_c_new:?}"); - - let stake_inc_a = stake_a_new - stake_a_old; - let stake_inc_b = stake_b_new - stake_b_old; - let stake_inc_c = stake_c_new - stake_c_old; - let total_stake_inc: I96F32 = total_stake_new - total_stake_old; - log::info!("Stake increase for hotkey A: {stake_inc_a:?}"); - log::info!("Stake increase for hotkey B: {stake_inc_b:?}"); - log::info!("Stake increase for hotkey C: {stake_inc_c:?}"); - log::info!("Total stake increase: {total_stake_inc:?}"); - let rel_stake_inc_a = I96F32::from_num(stake_inc_a) / total_stake_inc; - let rel_stake_inc_b = I96F32::from_num(stake_inc_b) / total_stake_inc; - let rel_stake_inc_c = I96F32::from_num(stake_inc_c) / total_stake_inc; - log::info!("rel_stake_inc_a: {rel_stake_inc_a:?}"); - log::info!("rel_stake_inc_b: {rel_stake_inc_b:?}"); - log::info!("rel_stake_inc_c: {rel_stake_inc_c:?}"); - - // Verify the final stake distribution - let stake_inc_eps = I96F32::from_num(1e-4); // 4 decimal places - - // Each child has chk_take take - let expected_a = I96F32::from_num(2_f64 / 3_f64) - * (I96F32::from_num(1_f64) - (I96F32::from_num(1_f64 / 2_f64) * chk_take)); - assert!( - (rel_stake_inc_a - expected_a).abs() // B's take on 50% CHK - <= stake_inc_eps, - "A should have {expected_a:?} of total stake increase; {rel_stake_inc_a:?}" - ); - let expected_b = I96F32::from_num(2_f64 / 9_f64) - * (I96F32::from_num(1_f64) - (I96F32::from_num(1_f64 / 2_f64) * chk_take)) - + I96F32::from_num(2_f64 / 3_f64) * (I96F32::from_num(1_f64 / 2_f64) * chk_take); - assert!( - (rel_stake_inc_b - expected_b).abs() // C's take on 50% CHK + take from A - <= stake_inc_eps, - "B should have {expected_b:?} of total stake increase; {rel_stake_inc_b:?}" - ); - let expected_c = I96F32::from_num(1_f64 / 9_f64) - + (I96F32::from_num(2_f64 / 9_f64) * I96F32::from_num(1_f64 / 2_f64) * chk_take); - assert!( - (rel_stake_inc_c - expected_c).abs() // B's take on 50% CHK - <= stake_inc_eps, - "C should have {expected_c:?} of total stake increase; {rel_stake_inc_c:?}" - ); - - let hotkeys = [hotkey_a, hotkey_b, hotkey_c]; - let mut total_stake_now = AlphaBalance::ZERO; - for (hotkey, netuid, stake) in TotalHotkeyAlpha::::iter() { - if hotkeys.contains(&hotkey) { - total_stake_now += stake; - } else { - log::info!("hotkey: {hotkey:?}, netuid: {netuid:?}, stake: {stake:?}"); - } - } - log::info!("total_stake_now: {total_stake_now:?}, total_stake_new: {total_stake_new:?}"); - - assert_abs_diff_eq!( - total_stake_inc.to_num::(), - emission_value, - epsilon = emission_value / 1000, - ); - }); -} - -// 45: Test *epoch* with a chain of parent-child relationships (e.g., A -> B -> C) -// This test verifies the correct distribution of emissions in a chain of parent-child relationships: -// - Sets up a network with three neurons A, B, and C in a chain (A -> B -> C) -// - Establishes parent-child relationships with different stake proportions -// - Sets weights for all neurons -// - Runs an epoch with a hardcoded emission value -// - Checks the emission distribution among A, B, and C -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_parent_child_chain_epoch --exact --show-output -#[test] -fn test_parent_child_chain_epoch() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - SubtensorModule::set_ck_burn(0); - // Set owner cut to 0 - SubtensorModule::set_subnet_owner_cut(0_u16); - - // Define hotkeys and coldkeys - let hotkey_a: U256 = U256::from(1); - let hotkey_b: U256 = U256::from(2); - let hotkey_c: U256 = U256::from(3); - let coldkey_a: U256 = U256::from(100); - let coldkey_b: U256 = U256::from(101); - let coldkey_c: U256 = U256::from(102); - - // Register neurons with decreasing stakes - register_ok_neuron(netuid, hotkey_a, coldkey_a, 0); - register_ok_neuron(netuid, hotkey_b, coldkey_b, 0); - register_ok_neuron(netuid, hotkey_c, coldkey_c, 0); - - // Add initial stakes - add_balance_to_coldkey_account(&coldkey_a, 1_000.into()); - add_balance_to_coldkey_account(&coldkey_b, 1_000.into()); - add_balance_to_coldkey_account(&coldkey_c, 1_000.into()); - - mock::setup_reserves( - netuid, - 1_000_000_000_000_u64.into(), - 1_000_000_000_000_u64.into(), - ); - - // Swap to alpha - let total_tao = I96F32::from_num(300_000 + 100_000 + 50_000); - let (total_alpha, _) = mock::swap_tao_to_alpha(netuid, total_tao.to_num::().into()); - let total_alpha = I96F32::from_num(total_alpha); - - // Set the stakes directly - // This avoids needing to swap tao to alpha, impacting the initial stake distribution. - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_a, - &coldkey_a, - netuid, - (total_alpha * I96F32::from_num(300_000) / total_tao) - .saturating_to_num::() - .into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_b, - &coldkey_b, - netuid, - (total_alpha * I96F32::from_num(100_000) / total_tao) - .saturating_to_num::() - .into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_c, - &coldkey_c, - netuid, - (total_alpha * I96F32::from_num(50_000) / total_tao) - .saturating_to_num::() - .into(), - ); - - // Get old stakes - let stake_a = SubtensorModule::get_total_stake_for_hotkey(&hotkey_a); - let stake_b = SubtensorModule::get_total_stake_for_hotkey(&hotkey_b); - let stake_c = SubtensorModule::get_total_stake_for_hotkey(&hotkey_c); - - // Assert initial stake is correct - let rel_stake_a = I96F32::from_num(stake_a) / total_alpha; - let rel_stake_b = I96F32::from_num(stake_b) / total_alpha; - let rel_stake_c = I96F32::from_num(stake_c) / total_alpha; - - log::info!("rel_stake_a: {rel_stake_a:?}"); // 0.6666 -> 2/3 - log::info!("rel_stake_b: {rel_stake_b:?}"); // 0.2222 -> 2/9 - log::info!("rel_stake_c: {rel_stake_c:?}"); // 0.1111 -> 1/9 - - assert!(rel_stake_a > I96F32::from_num(0)); - assert!(rel_stake_b > I96F32::from_num(0)); - assert!(rel_stake_c > I96F32::from_num(0)); - - // because of the fee we allow slightly higher range - let epsilon = I96F32::from_num(0.00001); - assert!((rel_stake_a - (I96F32::from_num(300_000) / total_tao)).abs() <= epsilon); - assert!((rel_stake_b - (I96F32::from_num(100_000) / total_tao)).abs() <= epsilon); - assert!((rel_stake_c - (I96F32::from_num(50_000) / total_tao)).abs() <= epsilon); - - // Set parent-child relationships - // A -> B (50% of A's stake) - mock_set_children(&coldkey_a, &hotkey_a, netuid, &[(u64::MAX / 2, hotkey_b)]); - - // B -> C (50% of B's stake) - mock_set_children(&coldkey_b, &hotkey_b, netuid, &[(u64::MAX / 2, hotkey_c)]); - - // Set CHK take rate to 1/9 - let chk_take = I96F32::from_num(1_f64 / 9_f64); - let chk_take_u16: u16 = (chk_take * I96F32::from_num(u16::MAX)).saturating_to_num::(); - ChildkeyTake::::insert(hotkey_b, netuid, PerU16::from_parts(chk_take_u16)); - ChildkeyTake::::insert(hotkey_c, netuid, PerU16::from_parts(chk_take_u16)); - - // Set the weight of root TAO to be 0%, so only alpha is effective. - SubtensorModule::set_tao_weight(0); - - let hardcoded_emission = I96F32::from_num(1_000_000); // 1 million (adjust as needed) - - let hotkey_emission = - SubtensorModule::epoch(netuid, hardcoded_emission.saturating_to_num::().into()); - log::info!("hotkey_emission: {hotkey_emission:?}"); - let total_emission: I96F32 = hotkey_emission - .iter() - .map(|(_, _, emission)| I96F32::from_num(*emission)) - .sum(); - - // Verify emissions match expected from CHK arrangements - let em_eps = I96F32::from_num(1e-4); // 4 decimal places - // A's pending emission: - assert!( - ((I96F32::from_num(hotkey_emission[0].2) / total_emission) - - I96F32::from_num(2_f64 / 3_f64 * 1_f64 / 2_f64)).abs() // 2/3 * 1/2 = 1/3; 50% -> B - <= em_eps, - "A should have pending emission of 1/3 of total emission" - ); - // B's pending emission: - assert!( - ((I96F32::from_num(hotkey_emission[1].2) / total_emission) - - (I96F32::from_num(2_f64 / 9_f64 * 1_f64 / 2_f64 + 2_f64 / 3_f64 * 1_f64 / 2_f64))).abs() // 2/9 * 1/2 + 2/3 * 1/2; 50% -> C + 50% from A - <= em_eps, - "B should have pending emission of 4/9 of total emission" - ); - // C's pending emission: - assert!( - ((I96F32::from_num(hotkey_emission[2].2) / total_emission) - - (I96F32::from_num(1_f64 / 9_f64 + 1_f64 / 2_f64 * 2_f64 / 9_f64))).abs() // 1/9 + 2/9 * 1/2; 50% from B - <= em_eps, - "C should have pending emission of 1/9 of total emission" - ); - }); -} - -// 46: Test dividend distribution with children -// This test verifies the correct distribution of emissions in a chain of parent-child relationships: -// - Sets up a network with three neurons A, B, and C in a chain (A -> B -> C) -// - Establishes parent-child relationships with different stake proportions -// - Adds a childkey take for both B and C -// - Distributes emission across each hotkey using a the helper -// - Checks the emission distribution among A, B, and C -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_dividend_distribution_with_children --exact --show-output -#[test] -fn test_dividend_distribution_with_children() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - SubtensorModule::set_ck_burn(0); - mock::setup_reserves( - netuid, - 1_000_000_000_000_000_u64.into(), - 1_000_000_000_000_000_u64.into(), - ); - // Set owner cut to 0 - SubtensorModule::set_subnet_owner_cut(0_u16); - - // Define hotkeys and coldkeys - let hotkey_a: U256 = U256::from(1); - let hotkey_b: U256 = U256::from(2); - let hotkey_c: U256 = U256::from(3); - let coldkey_a: U256 = U256::from(100); - let coldkey_b: U256 = U256::from(101); - let coldkey_c: U256 = U256::from(102); - - // Register neurons with decreasing stakes - register_ok_neuron(netuid, hotkey_a, coldkey_a, 0); - register_ok_neuron(netuid, hotkey_b, coldkey_b, 0); - register_ok_neuron(netuid, hotkey_c, coldkey_c, 0); - - // Add initial stakes - add_balance_to_coldkey_account(&coldkey_a, 1_000.into()); - add_balance_to_coldkey_account(&coldkey_b, 1_000.into()); - add_balance_to_coldkey_account(&coldkey_c, 1_000.into()); - - // Swap to alpha - let total_tao = I96F32::from_num(300_000 + 100_000 + 50_000); - let (total_alpha, _) = mock::swap_tao_to_alpha(netuid, total_tao.to_num::().into()); - let total_alpha = I96F32::from_num(total_alpha); - - // Set the stakes directly - // This avoids needing to swap tao to alpha, impacting the initial stake distribution. - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_a, - &coldkey_a, - netuid, - (total_alpha * I96F32::from_num(300_000) / total_tao) - .saturating_to_num::() - .into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_b, - &coldkey_b, - netuid, - (total_alpha * I96F32::from_num(100_000) / total_tao) - .saturating_to_num::() - .into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_c, - &coldkey_c, - netuid, - (total_alpha * I96F32::from_num(50_000) / total_tao) - .saturating_to_num::() - .into(), - ); - - // Get old stakes - let stake_a = SubtensorModule::get_total_stake_for_hotkey(&hotkey_a); - let stake_b = SubtensorModule::get_total_stake_for_hotkey(&hotkey_b); - let stake_c = SubtensorModule::get_total_stake_for_hotkey(&hotkey_c); - - // Assert initial stake is correct - let rel_stake_a = I96F32::from_num(stake_a) / total_alpha; - let rel_stake_b = I96F32::from_num(stake_b) / total_alpha; - let rel_stake_c = I96F32::from_num(stake_c) / total_alpha; - - log::info!("rel_stake_a: {rel_stake_a:?}"); // 0.6666 -> 2/3 - log::info!("rel_stake_b: {rel_stake_b:?}"); // 0.2222 -> 2/9 - log::info!("rel_stake_c: {rel_stake_c:?}"); // 0.1111 -> 1/9 - let epsilon = I96F32::from_num(0.00001); - assert!((rel_stake_a - I96F32::from_num(300_000) / total_tao).abs() <= epsilon); - assert!((rel_stake_b - I96F32::from_num(100_000) / total_tao).abs() <= epsilon); - assert!((rel_stake_c - I96F32::from_num(50_000) / total_tao).abs() <= epsilon); - - // Set parent-child relationships - // A -> B (50% of A's stake) - mock_set_children(&coldkey_a, &hotkey_a, netuid, &[(u64::MAX / 2, hotkey_b)]); - - // B -> C (50% of B's stake) - mock_set_children(&coldkey_b, &hotkey_b, netuid, &[(u64::MAX / 2, hotkey_c)]); - - // Set CHK take rate to 1/9 - let chk_take: I96F32 = I96F32::from_num(1_f64 / 9_f64); - let chk_take_u16: u16 = (chk_take * I96F32::from_num(u16::MAX)).saturating_to_num::(); - ChildkeyTake::::insert(hotkey_b, netuid, PerU16::from_parts(chk_take_u16)); - ChildkeyTake::::insert(hotkey_c, netuid, PerU16::from_parts(chk_take_u16)); - - // Set the weight of root TAO to be 0%, so only alpha is effective. - SubtensorModule::set_tao_weight(0); - - let hardcoded_emission: I96F32 = I96F32::from_num(1_000_000); // 1 million (adjust as needed) - - let hotkey_emission = - SubtensorModule::epoch(netuid, hardcoded_emission.saturating_to_num::().into()); - log::info!("hotkey_emission: {hotkey_emission:?}"); - let total_emission: I96F32 = hotkey_emission - .iter() - .map(|(_, _, emission)| I96F32::from_num(*emission)) - .sum(); - - // Verify emissions match expected from CHK arrangements - let em_eps: I96F32 = I96F32::from_num(1e-4); // 4 decimal places - // A's pending emission: - assert!( - ((I96F32::from_num(hotkey_emission[0].2) / total_emission) - - I96F32::from_num(2_f64 / 3_f64 * 1_f64 / 2_f64)).abs() // 2/3 * 1/2 = 1/3; 50% -> B - <= em_eps, - "A should have pending emission of 1/3 of total emission" - ); - // B's pending emission: - assert!( - ((I96F32::from_num(hotkey_emission[1].2) / total_emission) - - (I96F32::from_num(2_f64 / 9_f64 * 1_f64 / 2_f64 + 2_f64 / 3_f64 * 1_f64 / 2_f64))).abs() // 2/9 * 1/2 + 2/3 * 1/2; 50% -> C + 50% from A - <= em_eps, - "B should have pending emission of 4/9 of total emission" - ); - // C's pending emission: - assert!( - ((I96F32::from_num(hotkey_emission[2].2) / total_emission) - - (I96F32::from_num(1_f64 / 9_f64 + 1_f64 / 2_f64 * 2_f64 / 9_f64))).abs() // 1/9 + 2/9 * 1/2; 50% from B - <= em_eps, - "C should have pending emission of 1/9 of total emission" - ); - - let dividends_a = SubtensorModule::get_parent_child_dividends_distribution( - &hotkey_a, - netuid, - hardcoded_emission.saturating_to_num::().into(), - ); - let dividends_b = SubtensorModule::get_parent_child_dividends_distribution( - &hotkey_b, - netuid, - hardcoded_emission.saturating_to_num::().into(), - ); - let dividends_c = SubtensorModule::get_parent_child_dividends_distribution( - &hotkey_c, - netuid, - hardcoded_emission.saturating_to_num::().into(), - ); - log::info!("dividends_a: {dividends_a:?}"); - log::info!("dividends_b: {dividends_b:?}"); - log::info!("dividends_c: {dividends_c:?}"); - - // We expect A to get all of its own emission, as it has no parents. - assert_eq!(dividends_a.len(), 1); - assert_eq!(dividends_a[0].0, hotkey_a); - assert_eq!( - dividends_a[0].1, - hardcoded_emission.saturating_to_num::().into() - ); - assert_abs_diff_eq!( - dividends_a - .iter() - .map(|(_, emission)| u64::from(*emission)) - .sum::(), - hardcoded_emission.saturating_to_num::(), - epsilon = (hardcoded_emission / 1000).saturating_to_num::() - ); - - // We expect B to get a portion of its own emission, and some comission from A, where A gets the rest. - // B re-delegates 0.5 of its stake to C; And A re-delegates 0.5 of its stake to B. - let total_stake_b = rel_stake_b * 1 / 2 + rel_stake_a * 1 / 2; - let expected_b_b: u64 = ((rel_stake_b * 1 / 2) / total_stake_b * hardcoded_emission - + (rel_stake_a * 1 / 2) / total_stake_b * hardcoded_emission * chk_take) - .saturating_to_num::(); - assert_eq!(dividends_b.len(), 2); // A and B - assert_eq!(dividends_b[1].0, hotkey_b); - assert_abs_diff_eq!( - u64::from(dividends_b[1].1), - expected_b_b, - epsilon = (hardcoded_emission / 1000).saturating_to_num::() - ); - let expected_b_a: u64 = hardcoded_emission.saturating_to_num::() - expected_b_b; - assert_eq!(dividends_b[0].0, hotkey_a); - assert_abs_diff_eq!( - u64::from(dividends_b[0].1), - expected_b_a, - epsilon = (hardcoded_emission / 1000).saturating_to_num::() - ); - assert_abs_diff_eq!( - dividends_b - .iter() - .map(|(_, emission)| u64::from(*emission)) - .sum::(), - hardcoded_emission.saturating_to_num::(), - epsilon = (hardcoded_emission / 1000).saturating_to_num::() - ); - - // We expect C to get a portion of its own emission, and some comission from B, where B gets the rest. - let total_stake_c = rel_stake_c + rel_stake_b * 1 / 2; - let expected_c_c: u64 = (rel_stake_c / total_stake_c * hardcoded_emission - + (rel_stake_b * 1 / 2) / total_stake_c * hardcoded_emission * chk_take) - .saturating_to_num::(); - assert_eq!(dividends_c.len(), 2); // B and C - assert_eq!(dividends_c[1].0, hotkey_c); - assert_abs_diff_eq!( - u64::from(dividends_c[1].1), - expected_c_c, - epsilon = (hardcoded_emission / 1000).saturating_to_num::() - ); - let expected_c_b: u64 = hardcoded_emission.saturating_to_num::() - expected_c_c; - assert_eq!(dividends_c[0].0, hotkey_b); - assert_abs_diff_eq!( - u64::from(dividends_c[0].1), - expected_c_b, - epsilon = (hardcoded_emission / 1000).saturating_to_num::() - ); - assert_abs_diff_eq!( - dividends_c - .iter() - .map(|(_, emission)| u64::from(*emission)) - .sum::(), - hardcoded_emission.saturating_to_num::(), - epsilon = (hardcoded_emission / 1000).saturating_to_num::() - ); - }); -} - -// 47: Test emission distribution when adding/removing parent-child relationships mid-epoch -// This test verifies the correct distribution of emissions when parent-child relationships change: -// - Sets up a network with three neurons: parent, child1, and child2 -// - Establishes initial parent-child relationship between parent and child1 -// - Runs first epoch and distributes emissions -// - Changes parent-child relationships to include both child1 and child2 -// - Runs second epoch and distributes emissions -// - Checks final emission distribution and stake updates -// - Verifies correct parent-child relationships and stake proportions -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_dynamic_parent_child_relationships --exact --show-output -#[test] -fn test_dynamic_parent_child_relationships() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - SubtensorModule::set_ck_burn(0); - add_network_disable_commit_reveal(netuid, 1, 0); - - // Define hotkeys and coldkeys - let parent = U256::from(1); - let child1 = U256::from(2); - let child2 = U256::from(3); - let coldkey_parent = U256::from(100); - let coldkey_child1 = U256::from(101); - let coldkey_child2 = U256::from(102); - - // Register neurons with varying stakes - register_ok_neuron(netuid, parent, coldkey_parent, 0); - register_ok_neuron(netuid, child1, coldkey_child1, 0); - register_ok_neuron(netuid, child2, coldkey_child2, 0); - - let chk_take_1 = SubtensorModule::get_childkey_take(&child1, netuid); - let chk_take_2 = SubtensorModule::get_childkey_take(&child2, netuid); - log::info!("child take 1: {chk_take_1:?}"); - log::info!("child take 2: {chk_take_2:?}"); - - // Add initial stakes - add_balance_to_coldkey_account(&coldkey_parent, (500_000 + 1_000).into()); - add_balance_to_coldkey_account(&coldkey_child1, (50_000 + 1_000).into()); - add_balance_to_coldkey_account(&coldkey_child2, (30_000 + 1_000).into()); - - let reserve = 1_000_000_000_000_u64; - mock::setup_reserves(netuid, reserve.into(), reserve.into()); - - // Swap to alpha - let total_tao = I96F32::from_num(500_000 + 50_000 + 30_000); - let (total_alpha, _) = mock::swap_tao_to_alpha(netuid, total_tao.to_num::().into()); - let total_alpha = I96F32::from_num(total_alpha); - log::info!("total_alpha: {total_alpha:?}"); - - // Set the stakes directly - // This avoids needing to swap tao to alpha, impacting the initial stake distribution. - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &parent, - &coldkey_parent, - netuid, - (total_alpha * I96F32::from_num(500_000) / total_tao) - .saturating_to_num::() - .into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &child1, - &coldkey_child1, - netuid, - (total_alpha * I96F32::from_num(50_000) / total_tao) - .saturating_to_num::() - .into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &child2, - &coldkey_child2, - netuid, - (total_alpha * I96F32::from_num(30_000) / total_tao) - .saturating_to_num::() - .into(), - ); - - // Get old stakes - let stake_parent_0 = SubtensorModule::get_stake_for_hotkey_on_subnet(&parent, netuid); - let stake_child1_0 = SubtensorModule::get_stake_for_hotkey_on_subnet(&child1, netuid); - let stake_child2_0 = SubtensorModule::get_stake_for_hotkey_on_subnet(&child2, netuid); - log::info!("stake_parent_0: {stake_parent_0:?}"); - log::info!("stake_child1_0: {stake_child1_0:?}"); - log::info!("stake_child2_0: {stake_child2_0:?}"); - - let total_stake_0 = stake_parent_0 + stake_child1_0 + stake_child2_0; - - // Assert initial stake is correct - let rel_stake_parent_0 = I96F32::from_num(stake_parent_0) / total_alpha; - let rel_stake_child1_0 = I96F32::from_num(stake_child1_0) / total_alpha; - let rel_stake_child2_0 = I96F32::from_num(stake_child2_0) / total_alpha; - - log::info!("rel_stake_parent_0: {rel_stake_parent_0:?}"); - log::info!("rel_stake_child1_0: {rel_stake_child1_0:?}"); - log::info!("rel_stake_child2_0: {rel_stake_child2_0:?}"); - let epsilon = I96F32::from_num(0.00001); - assert!((rel_stake_parent_0 - I96F32::from_num(500_000) / total_tao).abs() <= epsilon); - assert!((rel_stake_child1_0 - I96F32::from_num(50_000) / total_tao).abs() <= epsilon); - assert!((rel_stake_child2_0 - I96F32::from_num(30_000) / total_tao).abs() <= epsilon); - - mock_set_children_no_epochs(netuid, &parent, &[(u64::MAX / 2, child1)]); - - step_block(2); - - // Set weights - let origin = RuntimeOrigin::signed(parent); - let uids: Vec = vec![0, 1, 2]; // UIDs for parent, child1, child2 - let values: Vec = vec![65535, 65535, 65535]; // Set equal weights for all hotkeys - let version_key = SubtensorModule::get_weights_version_key(netuid); - - // Ensure we can set weights without rate limiting - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - assert_ok!(SubtensorModule::set_weights( - origin, - netuid, - uids, - values, - version_key - )); - - // Step blocks to allow for emission distribution - step_block(11); - - // Get total stake after first payout - let total_stake_1 = SubtensorModule::get_stake_for_hotkey_on_subnet(&parent, netuid) - + SubtensorModule::get_stake_for_hotkey_on_subnet(&child1, netuid) - + SubtensorModule::get_stake_for_hotkey_on_subnet(&child2, netuid); - log::info!("total_stake_1: {total_stake_1:?}"); - - // Change parent-child relationships - mock_set_children_no_epochs( - netuid, - &parent, - &[(u64::MAX / 4, child1), (u64::MAX / 3, child2)], - ); - - // Step blocks again to allow for emission distribution - step_block(11); - - // Get total stake after second payout - let total_stake_2 = SubtensorModule::get_stake_for_hotkey_on_subnet(&parent, netuid) - + SubtensorModule::get_stake_for_hotkey_on_subnet(&child1, netuid) - + SubtensorModule::get_stake_for_hotkey_on_subnet(&child2, netuid); - log::info!("total_stake_2: {total_stake_2:?}"); - - // Check final emission distribution - let stake_parent_2 = SubtensorModule::get_inherited_for_hotkey_on_subnet(&parent, netuid); - let stake_child1_2 = SubtensorModule::get_inherited_for_hotkey_on_subnet(&child1, netuid); - let stake_child2_2 = SubtensorModule::get_inherited_for_hotkey_on_subnet(&child2, netuid); - let total_parent_stake = SubtensorModule::get_stake_for_hotkey_on_subnet(&parent, netuid); - let total_child1_stake = SubtensorModule::get_stake_for_hotkey_on_subnet(&child1, netuid); - let total_child2_stake = SubtensorModule::get_stake_for_hotkey_on_subnet(&child2, netuid); - - log::info!("Final stakes:"); - log::info!("Parent stake: {stake_parent_2}"); - log::info!("Child1 stake: {stake_child1_2}"); - log::info!("Child2 stake: {stake_child2_2}"); - - // Payout 1 - let payout_1 = total_stake_1 - total_stake_0; - log::info!("payout_1: {payout_1:?}"); - - // Payout 2 - let payout_2 = total_stake_2 - total_stake_1; - log::info!("payout_2: {payout_2:?}"); - - let total_emission = I96F32::from_num(payout_1 + payout_2); - - #[allow(non_snake_case)] - let TOLERANCE = I96F32::from_num(0.001); // Allow for a small discrepancy due to potential rounding - - // Precise assertions with tolerance - log::info!("total_emission: {total_emission:?}"); - let expected_parent_stake = - I96F32::from_num(total_parent_stake) * I96F32::from_num(5) / I96F32::from_num(12); - assert!( - (I96F32::from_num(stake_parent_2) - expected_parent_stake).abs() - / expected_parent_stake - <= TOLERANCE, - "Parent stake should be close to {expected_parent_stake:?}, but was {stake_parent_2}" - ); - // The final relationship leaves the parent with 1 - 1/4 - 1/3 = 5/12 - // of its current direct stake. - - let expected_child1_stake = I96F32::from_num(total_child1_stake) - + I96F32::from_num(total_parent_stake) / I96F32::from_num(4); - assert!( - (I96F32::from_num(stake_child1_2) - expected_child1_stake).abs() - / expected_child1_stake - <= TOLERANCE, - "Child1 stake should be close to {expected_child1_stake:?}, but was {stake_child1_2}" - ); - // Child1 inherits 1/4 of the parent's current direct stake. - - let expected_child2_stake = I96F32::from_num(total_child2_stake) - + I96F32::from_num(total_parent_stake) / I96F32::from_num(3); - assert!( - (I96F32::from_num(stake_child2_2) - expected_child2_stake).abs() - / expected_child2_stake - <= TOLERANCE, - "Child2 stake should be close to {expected_child2_stake:?}, but was {stake_child2_2}" - ); - // Child2 inherits 1/3 of the parent's current direct stake. - - // Additional checks for parent-child relationships - let parent_children: Vec<(u64, U256)> = SubtensorModule::get_children(&parent, netuid); - assert_eq!( - parent_children, - vec![(u64::MAX / 4, child1), (u64::MAX / 3, child2)], - "Parent should have both children with correct proportions" - ); - // Parent-child relationship: - // child1: 1/4 of parent's stake - // child2: 1/3 of parent's stake - - let child1_parents: Vec<(u64, U256)> = SubtensorModule::get_parents(&child1, netuid); - assert_eq!( - child1_parents, - vec![(u64::MAX / 4, parent)], - "Child1 should have parent as its parent with correct proportion" - ); - // Child1-parent relationship: - // parent: 1/4 of child1's stake - - let child2_parents: Vec<(u64, U256)> = SubtensorModule::get_parents(&child2, netuid); - assert_eq!( - child2_parents, - vec![(u64::MAX / 3, parent)], - "Child2 should have parent as its parent with correct proportion" - ); - // Child2-parent relationship: - // parent: 1/3 of child2's stake - - // Check that child2 has received more stake than child1 - assert!( - stake_child2_2 > stake_child1_2, - "Child2 should have received more emission than Child1 due to higher proportion" - ); - // Child2 stake (874,826) > Child1 stake (778,446) - }); -} - -#[test] -fn test_do_set_child_as_sn_owner_not_enough_stake() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let sn_owner_hotkey = U256::from(4); - - let child_coldkey = U256::from(2); - let child_hotkey = U256::from(5); - - let threshold = 10_000; - SubtensorModule::set_stake_threshold(threshold); - - let proportion: u64 = 1000; - - let netuid = add_dynamic_network(&sn_owner_hotkey, &coldkey); - remove_owner_registration_stake(netuid); - register_ok_neuron(netuid, child_hotkey, child_coldkey, 0); - - // Verify stake of sn_owner_hotkey is NOT enough - assert!( - SubtensorModule::get_total_stake_for_hotkey(&sn_owner_hotkey) - < StakeThreshold::::get().into() - ); - - // Verify that we can set child as sn owner, even though sn_owner_hotkey has insufficient stake - assert_ok!(SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - sn_owner_hotkey, - netuid, - vec![(proportion, child_hotkey)] - )); - - // Make new hotkey from owner coldkey - let other_sn_owner_hotkey = U256::from(6); - register_ok_neuron(netuid, other_sn_owner_hotkey, coldkey, 1234); - - // Verify stake of other_sn_owner_hotkey is NOT enough - assert!( - SubtensorModule::get_total_stake_for_hotkey(&other_sn_owner_hotkey) - < StakeThreshold::::get().into() - ); - - // Can't set child as sn owner, because it is not in SubnetOwnerHotkey map - assert_noop!( - SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(coldkey), - other_sn_owner_hotkey, - netuid, - vec![(proportion, child_hotkey)] - ), - Error::::NotEnoughStakeToSetChildkeys - ); - }); -} - -// Test dividend distribution for children with same coldkey Owner -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_dividend_distribution_with_children_same_coldkey_owner --exact --show-output -#[test] -fn test_dividend_distribution_with_children_same_coldkey_owner() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - // Set SN owner cut to 0 - SubtensorModule::set_subnet_owner_cut(0_u16); - mock::setup_reserves( - netuid, - 1_000_000_000_000_u64.into(), - 1_000_000_000_000_u64.into(), - ); - - // Define hotkeys and coldkeys - let hotkey_a: U256 = U256::from(1); - let hotkey_b: U256 = U256::from(2); - let coldkey_a: U256 = U256::from(100); // Only one coldkey - - // Register neurons with decreasing stakes - register_ok_neuron(netuid, hotkey_a, coldkey_a, 0); - register_ok_neuron(netuid, hotkey_b, coldkey_a, 0); - - // Add initial stakes - add_balance_to_coldkey_account(&coldkey_a, 1_000.into()); - add_balance_to_coldkey_account(&coldkey_a, 1_000.into()); - - // Swap to alpha - let total_tao = 300_000 + 100_000; - let total_alpha = I96F32::from_num(mock::swap_tao_to_alpha(netuid, total_tao.into()).0); - let total_tao = I96F32::from_num(total_tao); - - // Set the stakes directly - // This avoids needing to swap tao to alpha, impacting the initial stake distribution. - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_a, - &coldkey_a, - netuid, - (total_alpha * I96F32::from_num(300_000) / total_tao) - .saturating_to_num::() - .into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_b, - &coldkey_a, - netuid, - (total_alpha * I96F32::from_num(100_000) / total_tao) - .saturating_to_num::() - .into(), - ); - - // Get old stakes - let stake_a = SubtensorModule::get_total_stake_for_hotkey(&hotkey_a); - let stake_b = SubtensorModule::get_total_stake_for_hotkey(&hotkey_b); - - // Assert initial stake is correct - let rel_stake_a = I96F32::from_num(stake_a) / total_alpha; - let rel_stake_b = I96F32::from_num(stake_b) / total_alpha; - - log::info!("rel_stake_a: {rel_stake_a:?}"); // 0.75 -> 3/4 - log::info!("rel_stake_b: {rel_stake_b:?}"); // 0.25 -> 1/4 - let epsilon = I96F32::from_num(0.0001); - assert!((rel_stake_a - I96F32::from_num(300_000) / total_tao).abs() <= epsilon); - assert!((rel_stake_b - I96F32::from_num(100_000) / total_tao).abs() <= epsilon); - - // Set parent-child relationships - // A -> B (50% of A's stake) - mock_set_children(&coldkey_a, &hotkey_a, netuid, &[(u64::MAX / 2, hotkey_b)]); - - // Set CHK take rate to 1/9 - let chk_take: I96F32 = I96F32::from_num(1_f64 / 9_f64); - let chk_take_u16: u16 = (chk_take * I96F32::from_num(u16::MAX)).saturating_to_num::(); - ChildkeyTake::::insert(hotkey_b, netuid, PerU16::from_parts(chk_take_u16)); - - // Set the weight of root TAO to be 0%, so only alpha is effective. - SubtensorModule::set_tao_weight(0); - - let hardcoded_emission: I96F32 = I96F32::from_num(1_000_000); // 1 million (adjust as needed) - - let hotkey_emission = - SubtensorModule::epoch(netuid, hardcoded_emission.saturating_to_num::().into()); - log::info!("hotkey_emission: {hotkey_emission:?}"); - let total_emission: I96F32 = hotkey_emission - .iter() - .map(|(_, _, emission)| I96F32::from_num(*emission)) - .sum(); - - // Verify emissions match expected from CHK arrangements - let em_eps: I96F32 = I96F32::from_num(1e-4); // 4 decimal places - // A's pending emission: - assert!( - ((I96F32::from_num(hotkey_emission[0].2) / total_emission) - - I96F32::from_num(3_f64 / 4_f64 * 1_f64 / 2_f64)).abs() // 3/4 * 1/2 = 3/8; 50% -> B - <= em_eps, - "A should have pending emission of 3/8 of total emission" - ); - // B's pending emission: - assert!( - ((I96F32::from_num(hotkey_emission[1].2) / total_emission) - - (I96F32::from_num(1_f64 / 4_f64 + 3_f64 / 4_f64 * 1_f64 / 2_f64))).abs() // 1/4 + 3/4 * 1/2 = 5/8; 50% from A - <= em_eps, - "B should have pending emission of 5/8 of total emission: {:?}", - I96F32::from_num(hotkey_emission[1].2) / total_emission - ); - - // Get the distribution of dividends including the Parent/Child relationship. - let dividends_a = SubtensorModule::get_parent_child_dividends_distribution( - &hotkey_a, - netuid, - hardcoded_emission.saturating_to_num::().into(), - ); - let dividends_b = SubtensorModule::get_parent_child_dividends_distribution( - &hotkey_b, - netuid, - hardcoded_emission.saturating_to_num::().into(), - ); - log::info!("dividends_a: {dividends_a:?}"); - log::info!("dividends_b: {dividends_b:?}"); - - // We expect A should have no impact from B, as they have the same owner. - assert_eq!(dividends_a.len(), 1); - assert_eq!(dividends_a[0].0, hotkey_a); - assert_eq!( - dividends_a[0].1, - hardcoded_emission.saturating_to_num::().into() - ); - assert_abs_diff_eq!( - dividends_a - .iter() - .map(|(_, emission)| u64::from(*emission)) - .sum::(), - hardcoded_emission.saturating_to_num::(), - epsilon = (hardcoded_emission / 1000).saturating_to_num::() - ); - - // Expect only 2 dividends. Parent key A and child key B. - assert_eq!(dividends_b.len(), 2); // A and B - assert_eq!(dividends_b[0].0, hotkey_a); - assert_eq!(dividends_b[1].0, hotkey_b); - - // We expect B's coldkey to have no increase in dividends from A, as they have the same owner. - // And therefore, B should get no CHK_TAKE. - - // A should also have no decrease because there is no CHK_TAKE. - let total_stake_b = rel_stake_b + rel_stake_a * 1 / 2; - let expected_b_b: u64 = - (rel_stake_b / total_stake_b * hardcoded_emission).saturating_to_num::(); - - assert_abs_diff_eq!( - u64::from(dividends_b[1].1), - expected_b_b, - epsilon = (hardcoded_emission / 1000).saturating_to_num::(), - ); - - let expected_b_a: u64 = - ((rel_stake_a * 1 / 2) / total_stake_b * hardcoded_emission).saturating_to_num::(); - assert_eq!(dividends_b[0].0, hotkey_a); - assert_abs_diff_eq!( - u64::from(dividends_b[0].1), - expected_b_a, - epsilon = (hardcoded_emission / 1000).saturating_to_num::() - ); - assert_abs_diff_eq!( - dividends_b - .iter() - .map(|(_, emission)| u64::from(*emission)) - .sum::(), - hardcoded_emission.saturating_to_num::(), - epsilon = (hardcoded_emission / 1000).saturating_to_num::() - ); - }); -} - -#[test] -fn test_pending_cooldown_as_expected() { - let curr_block = 1; - // TODO: Fix when CHK splitting patched - // let expected_cooldown = prod_or_fast!(7200, 15); - - new_test_ext(curr_block).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child1 = U256::from(3); - let child2 = U256::from(4); - let netuid = NetUid::from(1); - let proportion1: u64 = 1000; - let proportion2: u64 = 2000; - let expected_cooldown = PendingChildKeyCooldown::::get(); - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Set multiple children - mock_schedule_children( - &coldkey, - &hotkey, - netuid, - &[(proportion1, child1), (proportion2, child2)], - ); - - // Verify pending map - let pending_children = PendingChildKeys::::get(netuid, hotkey); - assert_eq!( - pending_children.0, - vec![(proportion1, child1), (proportion2, child2)] - ); - assert_eq!(pending_children.1, curr_block + expected_cooldown); - }); -} - -#[test] -fn test_do_set_childkey_take_success() { - new_test_ext(1).execute_with(|| { - // Setup - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = NetUid::from(1); - let take = 5000; - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Set childkey take - assert_ok!(SubtensorModule::do_set_childkey_take( - coldkey, - hotkey, - netuid, - PerU16::from_parts(take) - )); - - // Verify the take was set correctly - assert_eq!(SubtensorModule::get_childkey_take(&hotkey, netuid), take); - let tx_type: u16 = TransactionType::SetChildkeyTake.into(); - assert_eq!( - TransactionKeyLastBlock::::get((hotkey, netuid, tx_type,)), - System::block_number() - ); - }); -} - -#[test] -fn test_do_set_childkey_take_non_associated_coldkey() { - new_test_ext(1).execute_with(|| { - // Setup - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let hotkey2 = U256::from(3); - let netuid = NetUid::from(1); - let take = 5000; - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Set childkey take - assert_noop!( - SubtensorModule::do_set_childkey_take( - coldkey, - hotkey2, - netuid, - PerU16::from_parts(take) - ), - Error::::NonAssociatedColdKey - ); - }); -} - -#[test] -fn test_do_set_childkey_take_invalid_take_value() { - new_test_ext(1).execute_with(|| { - // Setup - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = NetUid::from(1); - let take = SubtensorModule::get_max_childkey_take() + 1; - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Set childkey take - assert_noop!( - SubtensorModule::do_set_childkey_take( - coldkey, - hotkey, - netuid, - PerU16::from_parts(take) - ), - Error::::InvalidChildkeyTake - ); - }); -} - -#[test] -fn test_do_set_childkey_take_rate_limit_exceeded() { - new_test_ext(1).execute_with(|| { - // Setup - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = NetUid::from(1); - let initial_take = 3000; - let higher_take = 5000; - let lower_take = 1000; - - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Set initial childkey take - assert_ok!(SubtensorModule::do_set_childkey_take( - coldkey, - hotkey, - netuid, - PerU16::from_parts(initial_take) - )); - - // Try to increase the take value, should hit rate limit - assert_noop!( - SubtensorModule::do_set_childkey_take( - coldkey, - hotkey, - netuid, - PerU16::from_parts(higher_take) - ), - Error::::TxChildkeyTakeRateLimitExceeded - ); - - // lower take value should be ok - assert_ok!(SubtensorModule::do_set_childkey_take( - coldkey, - hotkey, - netuid, - PerU16::from_parts(lower_take) - )); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_set_child_keys_empty_vector_clears_storage --exact --show-output -#[test] -fn test_set_child_keys_empty_vector_clears_storage() { - new_test_ext(1).execute_with(|| { - let sn_owner_hotkey = U256::from(1001); - let sn_owner_coldkey = U256::from(1002); - let parent = U256::from(1); - let child = U256::from(2); - let netuid = add_dynamic_network(&sn_owner_hotkey, &sn_owner_coldkey); - - // Initialize ChildKeys for `parent` with a non-empty vector - ChildKeys::::insert(parent, netuid, vec![(u64::MAX, child)]); - ParentKeys::::insert(child, netuid, vec![(u64::MAX, parent)]); - - // Sanity: entry exists right now because we explicitly inserted it - assert!(ChildKeys::::contains_key(parent, netuid)); - assert!(ParentKeys::::contains_key(child, netuid)); - - // Set children to empty - let empty_children: Vec<(u64, U256)> = Vec::new(); - mock_set_children_no_epochs(netuid, &parent, &empty_children); - - // When the child vector is empty, we should NOT keep an empty vec in storage. - // The key must be fully removed (no entry), not just zero-length value. - assert!(!ChildKeys::::contains_key(parent, netuid)); - assert!(!ParentKeys::::contains_key(child, netuid)); - - // `get` returns empty due to ValueQuery default, but presence is false. - assert!(ChildKeys::::get(parent, netuid).is_empty()); - assert!(ParentKeys::::get(child, netuid).is_empty()); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_set_child_keys_no_start_call_sets_immediately --exact --show-output -#[test] -fn test_set_child_keys_no_start_call_sets_immediately() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let child1 = U256::from(3); - let child2 = U256::from(4); - let netuid = NetUid::from(1); - let proportion1: u64 = 1000; - let proportion2: u64 = 2000; - - // Add network and register hotkey - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - - // Clear SubtokenEnabled - SubtokenEnabled::::remove(netuid); - - // Set multiple children - mock_schedule_children( - &coldkey, - &hotkey, - netuid, - &[(proportion1, child1), (proportion2, child2)], - ); - - // Normally happens on epoch - SubtensorModule::do_set_pending_children(netuid); - - // Verify pending map is empty - assert!(!PendingChildKeys::::contains_key(netuid, hotkey)); - - // Verify that childkey is set - assert_eq!( - ChildKeys::::get(hotkey, netuid), - vec![(proportion1, child1), (proportion2, child2)] - ); - }); -} - -// Test that the subnet owner can always set weights (owner bypass in check_weights_min_stake) -// and that do_set_root_validators_for_subnet correctly creates parent-child relationships. -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_root_children_enable_subnet_owner_set_weights --exact --show-output --nocapture -#[test] -fn test_root_children_enable_subnet_owner_set_weights() { - new_test_ext(1).execute_with(|| { - // --- Setup accounts --- - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - - let root_val_coldkey_1 = U256::from(100); - let root_val_hotkey_1 = U256::from(101); - let root_val_coldkey_2 = U256::from(200); - let root_val_hotkey_2 = U256::from(201); - - // --- Create root network and subnet --- - add_network(NetUid::ROOT, 1, 0); - let netuid = - add_dynamic_network_disable_commit_reveal(&subnet_owner_hotkey, &subnet_owner_coldkey); - - // --- Register root validators on a subnet first (required before root_register) --- - register_ok_neuron(netuid, root_val_hotkey_1, root_val_coldkey_1, 0); - register_ok_neuron(netuid, root_val_hotkey_2, root_val_coldkey_2, 0); - - // --- Register root validators on root network --- - assert_ok!(SubtensorModule::root_register( - RuntimeOrigin::signed(root_val_coldkey_1), - root_val_hotkey_1, - )); - assert_ok!(SubtensorModule::root_register( - RuntimeOrigin::signed(root_val_coldkey_2), - root_val_hotkey_2, - )); - - // --- Add stake for root validators on root and the subnet --- - let root_stake = AlphaBalance::from(1_000_000_000); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &root_val_hotkey_1, - &root_val_coldkey_1, - NetUid::ROOT, - root_stake, - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &root_val_hotkey_1, - &root_val_coldkey_1, - netuid, - root_stake, - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &root_val_hotkey_2, - &root_val_coldkey_2, - NetUid::ROOT, - root_stake, - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &root_val_hotkey_2, - &root_val_coldkey_2, - netuid, - root_stake, - ); - - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - let version_key = SubtensorModule::get_weights_version_key(netuid); - let uids: Vec = vec![0]; - let values: Vec = vec![u16::MAX]; - - // Subnet owner can set weights with default (zero) stake threshold. - assert!( - SubtensorModule::check_weights_min_stake(&subnet_owner_hotkey, netuid), - "Subnet owner should pass the min stake check with default threshold" - ); - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(subnet_owner_hotkey), - netuid, - uids.clone(), - values.clone(), - version_key - )); - - // Subnet owner can still set weights after raising the stake threshold (owner bypass). - SubtensorModule::set_stake_threshold(500_000_000u64); - assert!( - SubtensorModule::check_weights_min_stake(&subnet_owner_hotkey, netuid), - "Subnet owner should pass the min stake check even with high threshold" - ); - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(subnet_owner_hotkey), - netuid, - uids.clone(), - values.clone(), - version_key - )); - - // --- Verify do_set_root_validators_for_subnet creates parent-child relationships --- - assert_ok!(SubtensorModule::set_pending_childkey_cooldown( - RuntimeOrigin::root(), - 0, - )); - - assert_ok!(SubtensorModule::do_set_root_validators_for_subnet(netuid)); - - // Activate pending children (cooldown is 0, advance 1 block) - step_block(1); - SubtensorModule::do_set_pending_children(netuid); - - // Each root validator should have the subnet owner hotkey as a child on netuid - let children_1 = SubtensorModule::get_children(&root_val_hotkey_1, netuid); - assert_eq!( - children_1, - vec![(u64::MAX, subnet_owner_hotkey)], - "Root validator 1 should have subnet owner as child" - ); - let children_2 = SubtensorModule::get_children(&root_val_hotkey_2, netuid); - assert_eq!( - children_2, - vec![(u64::MAX, subnet_owner_hotkey)], - "Root validator 2 should have subnet owner as child" - ); - - // Subnet owner should have both root validators as parents - let parents = SubtensorModule::get_parents(&subnet_owner_hotkey, netuid); - assert_eq!(parents.len(), 2, "Subnet owner should have 2 parents"); - }); -} - -// Test that register_network automatically sets root validators as parents of the -// subnet owner, enabling the owner to set weights. Since SubtokenEnabled is false -// for a new subnet (start_call hasn't executed yet), child keys are applied immediately. -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_register_network_schedules_root_validators --exact --show-output --nocapture -#[test] -fn test_register_network_schedules_root_validators() { - new_test_ext(1).execute_with(|| { - // --- Setup root network and root validators --- - let root_val_coldkey_1 = U256::from(100); - let root_val_hotkey_1 = U256::from(101); - let root_val_coldkey_2 = U256::from(200); - let root_val_hotkey_2 = U256::from(201); - - add_network(NetUid::ROOT, 1, 0); - - // Root validators need to be registered on some subnet before root_register. - // Create a bootstrap subnet for that purpose. - let bootstrap_netuid = NetUid::from(1); - add_network(bootstrap_netuid, 1, 0); - register_ok_neuron(bootstrap_netuid, root_val_hotkey_1, root_val_coldkey_1, 0); - register_ok_neuron(bootstrap_netuid, root_val_hotkey_2, root_val_coldkey_2, 0); - - assert_ok!(SubtensorModule::root_register( - RuntimeOrigin::signed(root_val_coldkey_1), - root_val_hotkey_1, - )); - assert_ok!(SubtensorModule::root_register( - RuntimeOrigin::signed(root_val_coldkey_2), - root_val_hotkey_2, - )); - - // Give root validators significant stake on root and bootstrap subnet - let root_stake = AlphaBalance::from(1_000_000_000); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &root_val_hotkey_1, - &root_val_coldkey_1, - NetUid::ROOT, - root_stake, - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &root_val_hotkey_2, - &root_val_coldkey_2, - NetUid::ROOT, - root_stake, - ); - - // --- Minimize cooldown so pending children activate quickly --- - assert_ok!(SubtensorModule::set_pending_childkey_cooldown( - RuntimeOrigin::root(), - 0, - )); - - // --- Set a high stake threshold --- - let high_threshold = 500_000_000u64; - SubtensorModule::set_stake_threshold(high_threshold); - - // --- Register a new subnet (this should automatically call do_set_root_validators_for_subnet) --- - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let lock_cost = SubtensorModule::get_network_lock_cost(); - add_balance_to_coldkey_account(&subnet_owner_coldkey, lock_cost.into()); - TotalIssuance::::mutate(|total| { - *total = total.saturating_add(lock_cost); - }); - assert_ok!(SubtensorModule::register_network( - RuntimeOrigin::signed(subnet_owner_coldkey), - subnet_owner_hotkey, - )); - - // Determine the netuid that was just created - let netuid: NetUid = (TotalNetworks::::get().saturating_sub(1)).into(); - assert_eq!( - SubnetOwnerHotkey::::get(netuid), - subnet_owner_hotkey, - "Subnet owner hotkey should be set" - ); - - // Root validators need stake on the new subnet for child stake inheritance to work - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &root_val_hotkey_1, - &root_val_coldkey_1, - netuid, - root_stake, - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &root_val_hotkey_2, - &root_val_coldkey_2, - netuid, - root_stake, - ); - - // --- Verify child keys were applied immediately (SubtokenEnabled is false for new subnets) --- - let children_1 = SubtensorModule::get_children(&root_val_hotkey_1, netuid); - assert_eq!( - children_1, - vec![(u64::MAX, subnet_owner_hotkey)], - "Root validator 1 should have subnet owner as child" - ); - let children_2 = SubtensorModule::get_children(&root_val_hotkey_2, netuid); - assert_eq!( - children_2, - vec![(u64::MAX, subnet_owner_hotkey)], - "Root validator 2 should have subnet owner as child" - ); - - // --- Verify subnet owner can now set weights --- - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); - let version_key = SubtensorModule::get_weights_version_key(netuid); - - assert!( - SubtensorModule::check_weights_min_stake(&subnet_owner_hotkey, netuid), - "Subnet owner should have enough inherited stake to set weights" - ); - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(subnet_owner_hotkey), - netuid, - vec![0], - vec![u16::MAX], - version_key - )); - }); -} - -// Test that register_network automatically sets root validators as parents of the -// subnet owner, only if AutoParentDelegationEnabled is enabled (default). -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::test_register_network_schedules_root_validators_auto_parent_delegation_flag --exact --show-output --nocapture -#[test] -fn test_register_network_schedules_root_validators_auto_parent_delegation_flag() { - new_test_ext(1).execute_with(|| { - // --- Setup root network and root validators --- - let root_val_coldkey_1 = U256::from(100); - let root_val_hotkey_1 = U256::from(101); - let root_val_coldkey_2 = U256::from(200); - let root_val_hotkey_2 = U256::from(201); - - add_network(NetUid::ROOT, 1, 0); - - // Root validators need to be registered on some subnet before root_register. - // Create a bootstrap subnet for that purpose. - let bootstrap_netuid = NetUid::from(1); - add_network(bootstrap_netuid, 1, 0); - register_ok_neuron(bootstrap_netuid, root_val_hotkey_1, root_val_coldkey_1, 0); - register_ok_neuron(bootstrap_netuid, root_val_hotkey_2, root_val_coldkey_2, 0); - - assert_ok!(SubtensorModule::root_register( - RuntimeOrigin::signed(root_val_coldkey_1), - root_val_hotkey_1, - )); - assert_ok!(SubtensorModule::root_register( - RuntimeOrigin::signed(root_val_coldkey_2), - root_val_hotkey_2, - )); - - // Give root validators significant stake on root and bootstrap subnet - let root_stake = AlphaBalance::from(1_000_000_000); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &root_val_hotkey_1, - &root_val_coldkey_1, - NetUid::ROOT, - root_stake, - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &root_val_hotkey_2, - &root_val_coldkey_2, - NetUid::ROOT, - root_stake, - ); - - // --- Minimize cooldown so pending children activate quickly --- - assert_ok!(SubtensorModule::set_pending_childkey_cooldown( - RuntimeOrigin::root(), - 0, - )); - - // --- Set a high stake threshold --- - let high_threshold = 500_000_000u64; - SubtensorModule::set_stake_threshold(high_threshold); - - // --- Register a new subnet (this should automatically call do_set_root_validators_for_subnet) --- - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let lock_cost = SubtensorModule::get_network_lock_cost(); - add_balance_to_coldkey_account(&subnet_owner_coldkey, lock_cost.into()); - TotalIssuance::::mutate(|total| { - *total = total.saturating_add(lock_cost); - }); - - assert_ok!(SubtensorModule::set_auto_parent_delegation_enabled( - RuntimeOrigin::signed(root_val_coldkey_1), - root_val_hotkey_1, - false, - )); - - assert_ok!(SubtensorModule::register_network( - RuntimeOrigin::signed(subnet_owner_coldkey), - subnet_owner_hotkey, - )); - - // Determine the netuid that was just created - let netuid: NetUid = (TotalNetworks::::get().saturating_sub(1)).into(); - assert_eq!( - SubnetOwnerHotkey::::get(netuid), - subnet_owner_hotkey, - "Subnet owner hotkey should be set" - ); - - // Root validators need stake on the new subnet for child stake inheritance to work - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &root_val_hotkey_1, - &root_val_coldkey_1, - netuid, - root_stake, - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &root_val_hotkey_2, - &root_val_coldkey_2, - netuid, - root_stake, - ); - - // --- Verify child keys were applied immediately (SubtokenEnabled is false for new subnets) --- - let children_1 = SubtensorModule::get_children(&root_val_hotkey_1, netuid); - assert_eq!( - children_1, - vec![], - "Root validator 1 not have subnet owner as a child because AutoParentDelegationEnabled is false" - ); - let children_2 = SubtensorModule::get_children(&root_val_hotkey_2, netuid); - assert_eq!( - children_2, - vec![(u64::MAX, subnet_owner_hotkey)], - "Root validator 2 should have subnet owner as child" - ); - - // --- Verify subnet owner can now set weights --- - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); - let version_key = SubtensorModule::get_weights_version_key(netuid); - - assert!( - SubtensorModule::check_weights_min_stake(&subnet_owner_hotkey, netuid), - "Subnet owner should have enough inherited stake to set weights" - ); - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(subnet_owner_hotkey), - netuid, - vec![0], - vec![u16::MAX], - version_key - )); - }); -} diff --git a/pallets/subtensor/src/tests/children/child_dividends.rs b/pallets/subtensor/src/tests/children/child_dividends.rs new file mode 100644 index 0000000000..74cfb49c23 --- /dev/null +++ b/pallets/subtensor/src/tests/children/child_dividends.rs @@ -0,0 +1,662 @@ +#![allow(clippy::indexing_slicing)] +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] +use super::super::mock; +use super::super::mock::*; +use approx::assert_abs_diff_eq; +use frame_support::assert_ok; +use substrate_fixed::types::I96F32; + +use crate::*; +use sp_core::U256; +use sp_runtime::PerU16; + +// 46: Test dividend distribution with children +// This test verifies the correct distribution of emissions in a chain of parent-child relationships: +// - Sets up a network with three neurons A, B, and C in a chain (A -> B -> C) +// - Establishes parent-child relationships with different stake proportions +// - Adds a childkey take for both B and C +// - Distributes emission across each hotkey using a the helper +// - Checks the emission distribution among A, B, and C +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::child_dividends::test_dividend_distribution_with_children --exact --show-output +#[test] +fn test_dividend_distribution_with_children() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + SubtensorModule::set_ck_burn(0); + mock::setup_reserves( + netuid, + 1_000_000_000_000_000_u64.into(), + 1_000_000_000_000_000_u64.into(), + ); + // Set owner cut to 0 + SubtensorModule::set_subnet_owner_cut(0_u16); + + // Define hotkeys and coldkeys + let hotkey_a: U256 = U256::from(1); + let hotkey_b: U256 = U256::from(2); + let hotkey_c: U256 = U256::from(3); + let coldkey_a: U256 = U256::from(100); + let coldkey_b: U256 = U256::from(101); + let coldkey_c: U256 = U256::from(102); + + // Register neurons with decreasing stakes + register_ok_neuron(netuid, hotkey_a, coldkey_a, 0); + register_ok_neuron(netuid, hotkey_b, coldkey_b, 0); + register_ok_neuron(netuid, hotkey_c, coldkey_c, 0); + + // Add initial stakes + add_balance_to_coldkey_account(&coldkey_a, 1_000.into()); + add_balance_to_coldkey_account(&coldkey_b, 1_000.into()); + add_balance_to_coldkey_account(&coldkey_c, 1_000.into()); + + // Swap to alpha + let total_tao = I96F32::from_num(300_000 + 100_000 + 50_000); + let (total_alpha, _) = mock::swap_tao_to_alpha(netuid, total_tao.to_num::().into()); + let total_alpha = I96F32::from_num(total_alpha); + + // Set the stakes directly + // This avoids needing to swap tao to alpha, impacting the initial stake distribution. + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_a, + &coldkey_a, + netuid, + (total_alpha * I96F32::from_num(300_000) / total_tao) + .saturating_to_num::() + .into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_b, + &coldkey_b, + netuid, + (total_alpha * I96F32::from_num(100_000) / total_tao) + .saturating_to_num::() + .into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_c, + &coldkey_c, + netuid, + (total_alpha * I96F32::from_num(50_000) / total_tao) + .saturating_to_num::() + .into(), + ); + + // Get old stakes + let stake_a = SubtensorModule::get_total_stake_for_hotkey(&hotkey_a); + let stake_b = SubtensorModule::get_total_stake_for_hotkey(&hotkey_b); + let stake_c = SubtensorModule::get_total_stake_for_hotkey(&hotkey_c); + + // Assert initial stake is correct + let rel_stake_a = I96F32::from_num(stake_a) / total_alpha; + let rel_stake_b = I96F32::from_num(stake_b) / total_alpha; + let rel_stake_c = I96F32::from_num(stake_c) / total_alpha; + + log::info!("rel_stake_a: {rel_stake_a:?}"); // 0.6666 -> 2/3 + log::info!("rel_stake_b: {rel_stake_b:?}"); // 0.2222 -> 2/9 + log::info!("rel_stake_c: {rel_stake_c:?}"); // 0.1111 -> 1/9 + let epsilon = I96F32::from_num(0.00001); + assert!((rel_stake_a - I96F32::from_num(300_000) / total_tao).abs() <= epsilon); + assert!((rel_stake_b - I96F32::from_num(100_000) / total_tao).abs() <= epsilon); + assert!((rel_stake_c - I96F32::from_num(50_000) / total_tao).abs() <= epsilon); + + // Set parent-child relationships + // A -> B (50% of A's stake) + mock_set_children(&coldkey_a, &hotkey_a, netuid, &[(u64::MAX / 2, hotkey_b)]); + + // B -> C (50% of B's stake) + mock_set_children(&coldkey_b, &hotkey_b, netuid, &[(u64::MAX / 2, hotkey_c)]); + + // Set CHK take rate to 1/9 + let chk_take: I96F32 = I96F32::from_num(1_f64 / 9_f64); + let chk_take_u16: u16 = (chk_take * I96F32::from_num(u16::MAX)).saturating_to_num::(); + ChildkeyTake::::insert(hotkey_b, netuid, PerU16::from_parts(chk_take_u16)); + ChildkeyTake::::insert(hotkey_c, netuid, PerU16::from_parts(chk_take_u16)); + + // Set the weight of root TAO to be 0%, so only alpha is effective. + SubtensorModule::set_tao_weight(0); + + let hardcoded_emission: I96F32 = I96F32::from_num(1_000_000); // 1 million (adjust as needed) + + let hotkey_emission = + SubtensorModule::epoch(netuid, hardcoded_emission.saturating_to_num::().into()); + log::info!("hotkey_emission: {hotkey_emission:?}"); + let total_emission: I96F32 = hotkey_emission + .iter() + .map(|(_, _, emission)| I96F32::from_num(*emission)) + .sum(); + + // Verify emissions match expected from CHK arrangements + let em_eps: I96F32 = I96F32::from_num(1e-4); // 4 decimal places + // A's pending emission: + assert!( + ((I96F32::from_num(hotkey_emission[0].2) / total_emission) - + I96F32::from_num(2_f64 / 3_f64 * 1_f64 / 2_f64)).abs() // 2/3 * 1/2 = 1/3; 50% -> B + <= em_eps, + "A should have pending emission of 1/3 of total emission" + ); + // B's pending emission: + assert!( + ((I96F32::from_num(hotkey_emission[1].2) / total_emission) - + (I96F32::from_num(2_f64 / 9_f64 * 1_f64 / 2_f64 + 2_f64 / 3_f64 * 1_f64 / 2_f64))).abs() // 2/9 * 1/2 + 2/3 * 1/2; 50% -> C + 50% from A + <= em_eps, + "B should have pending emission of 4/9 of total emission" + ); + // C's pending emission: + assert!( + ((I96F32::from_num(hotkey_emission[2].2) / total_emission) - + (I96F32::from_num(1_f64 / 9_f64 + 1_f64 / 2_f64 * 2_f64 / 9_f64))).abs() // 1/9 + 2/9 * 1/2; 50% from B + <= em_eps, + "C should have pending emission of 1/9 of total emission" + ); + + let dividends_a = SubtensorModule::get_parent_child_dividends_distribution( + &hotkey_a, + netuid, + hardcoded_emission.saturating_to_num::().into(), + ); + let dividends_b = SubtensorModule::get_parent_child_dividends_distribution( + &hotkey_b, + netuid, + hardcoded_emission.saturating_to_num::().into(), + ); + let dividends_c = SubtensorModule::get_parent_child_dividends_distribution( + &hotkey_c, + netuid, + hardcoded_emission.saturating_to_num::().into(), + ); + log::info!("dividends_a: {dividends_a:?}"); + log::info!("dividends_b: {dividends_b:?}"); + log::info!("dividends_c: {dividends_c:?}"); + + // We expect A to get all of its own emission, as it has no parents. + assert_eq!(dividends_a.len(), 1); + assert_eq!(dividends_a[0].0, hotkey_a); + assert_eq!( + dividends_a[0].1, + hardcoded_emission.saturating_to_num::().into() + ); + assert_abs_diff_eq!( + dividends_a + .iter() + .map(|(_, emission)| u64::from(*emission)) + .sum::(), + hardcoded_emission.saturating_to_num::(), + epsilon = (hardcoded_emission / 1000).saturating_to_num::() + ); + + // We expect B to get a portion of its own emission, and some comission from A, where A gets the rest. + // B re-delegates 0.5 of its stake to C; And A re-delegates 0.5 of its stake to B. + let total_stake_b = rel_stake_b * 1 / 2 + rel_stake_a * 1 / 2; + let expected_b_b: u64 = ((rel_stake_b * 1 / 2) / total_stake_b * hardcoded_emission + + (rel_stake_a * 1 / 2) / total_stake_b * hardcoded_emission * chk_take) + .saturating_to_num::(); + assert_eq!(dividends_b.len(), 2); // A and B + assert_eq!(dividends_b[1].0, hotkey_b); + assert_abs_diff_eq!( + u64::from(dividends_b[1].1), + expected_b_b, + epsilon = (hardcoded_emission / 1000).saturating_to_num::() + ); + let expected_b_a: u64 = hardcoded_emission.saturating_to_num::() - expected_b_b; + assert_eq!(dividends_b[0].0, hotkey_a); + assert_abs_diff_eq!( + u64::from(dividends_b[0].1), + expected_b_a, + epsilon = (hardcoded_emission / 1000).saturating_to_num::() + ); + assert_abs_diff_eq!( + dividends_b + .iter() + .map(|(_, emission)| u64::from(*emission)) + .sum::(), + hardcoded_emission.saturating_to_num::(), + epsilon = (hardcoded_emission / 1000).saturating_to_num::() + ); + + // We expect C to get a portion of its own emission, and some comission from B, where B gets the rest. + let total_stake_c = rel_stake_c + rel_stake_b * 1 / 2; + let expected_c_c: u64 = (rel_stake_c / total_stake_c * hardcoded_emission + + (rel_stake_b * 1 / 2) / total_stake_c * hardcoded_emission * chk_take) + .saturating_to_num::(); + assert_eq!(dividends_c.len(), 2); // B and C + assert_eq!(dividends_c[1].0, hotkey_c); + assert_abs_diff_eq!( + u64::from(dividends_c[1].1), + expected_c_c, + epsilon = (hardcoded_emission / 1000).saturating_to_num::() + ); + let expected_c_b: u64 = hardcoded_emission.saturating_to_num::() - expected_c_c; + assert_eq!(dividends_c[0].0, hotkey_b); + assert_abs_diff_eq!( + u64::from(dividends_c[0].1), + expected_c_b, + epsilon = (hardcoded_emission / 1000).saturating_to_num::() + ); + assert_abs_diff_eq!( + dividends_c + .iter() + .map(|(_, emission)| u64::from(*emission)) + .sum::(), + hardcoded_emission.saturating_to_num::(), + epsilon = (hardcoded_emission / 1000).saturating_to_num::() + ); + }); +} + +// 47: Test emission distribution when adding/removing parent-child relationships mid-epoch +// This test verifies the correct distribution of emissions when parent-child relationships change: +// - Sets up a network with three neurons: parent, child1, and child2 +// - Establishes initial parent-child relationship between parent and child1 +// - Runs first epoch and distributes emissions +// - Changes parent-child relationships to include both child1 and child2 +// - Runs second epoch and distributes emissions +// - Checks final emission distribution and stake updates +// - Verifies correct parent-child relationships and stake proportions +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::child_dividends::test_dynamic_parent_child_relationships --exact --show-output +#[test] +fn test_dynamic_parent_child_relationships() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + SubtensorModule::set_ck_burn(0); + add_network_disable_commit_reveal(netuid, 1, 0); + + // Define hotkeys and coldkeys + let parent = U256::from(1); + let child1 = U256::from(2); + let child2 = U256::from(3); + let coldkey_parent = U256::from(100); + let coldkey_child1 = U256::from(101); + let coldkey_child2 = U256::from(102); + + // Register neurons with varying stakes + register_ok_neuron(netuid, parent, coldkey_parent, 0); + register_ok_neuron(netuid, child1, coldkey_child1, 0); + register_ok_neuron(netuid, child2, coldkey_child2, 0); + + let chk_take_1 = SubtensorModule::get_childkey_take(&child1, netuid); + let chk_take_2 = SubtensorModule::get_childkey_take(&child2, netuid); + log::info!("child take 1: {chk_take_1:?}"); + log::info!("child take 2: {chk_take_2:?}"); + + // Add initial stakes + add_balance_to_coldkey_account(&coldkey_parent, (500_000 + 1_000).into()); + add_balance_to_coldkey_account(&coldkey_child1, (50_000 + 1_000).into()); + add_balance_to_coldkey_account(&coldkey_child2, (30_000 + 1_000).into()); + + let reserve = 1_000_000_000_000_u64; + mock::setup_reserves(netuid, reserve.into(), reserve.into()); + + // Swap to alpha + let total_tao = I96F32::from_num(500_000 + 50_000 + 30_000); + let (total_alpha, _) = mock::swap_tao_to_alpha(netuid, total_tao.to_num::().into()); + let total_alpha = I96F32::from_num(total_alpha); + log::info!("total_alpha: {total_alpha:?}"); + + // Set the stakes directly + // This avoids needing to swap tao to alpha, impacting the initial stake distribution. + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &parent, + &coldkey_parent, + netuid, + (total_alpha * I96F32::from_num(500_000) / total_tao) + .saturating_to_num::() + .into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &child1, + &coldkey_child1, + netuid, + (total_alpha * I96F32::from_num(50_000) / total_tao) + .saturating_to_num::() + .into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &child2, + &coldkey_child2, + netuid, + (total_alpha * I96F32::from_num(30_000) / total_tao) + .saturating_to_num::() + .into(), + ); + + // Get old stakes + let stake_parent_0 = SubtensorModule::get_stake_for_hotkey_on_subnet(&parent, netuid); + let stake_child1_0 = SubtensorModule::get_stake_for_hotkey_on_subnet(&child1, netuid); + let stake_child2_0 = SubtensorModule::get_stake_for_hotkey_on_subnet(&child2, netuid); + log::info!("stake_parent_0: {stake_parent_0:?}"); + log::info!("stake_child1_0: {stake_child1_0:?}"); + log::info!("stake_child2_0: {stake_child2_0:?}"); + + let total_stake_0 = stake_parent_0 + stake_child1_0 + stake_child2_0; + + // Assert initial stake is correct + let rel_stake_parent_0 = I96F32::from_num(stake_parent_0) / total_alpha; + let rel_stake_child1_0 = I96F32::from_num(stake_child1_0) / total_alpha; + let rel_stake_child2_0 = I96F32::from_num(stake_child2_0) / total_alpha; + + log::info!("rel_stake_parent_0: {rel_stake_parent_0:?}"); + log::info!("rel_stake_child1_0: {rel_stake_child1_0:?}"); + log::info!("rel_stake_child2_0: {rel_stake_child2_0:?}"); + let epsilon = I96F32::from_num(0.00001); + assert!((rel_stake_parent_0 - I96F32::from_num(500_000) / total_tao).abs() <= epsilon); + assert!((rel_stake_child1_0 - I96F32::from_num(50_000) / total_tao).abs() <= epsilon); + assert!((rel_stake_child2_0 - I96F32::from_num(30_000) / total_tao).abs() <= epsilon); + + mock_set_children_no_epochs(netuid, &parent, &[(u64::MAX / 2, child1)]); + + step_block(2); + + // Set weights + let origin = RuntimeOrigin::signed(parent); + let uids: Vec = vec![0, 1, 2]; // UIDs for parent, child1, child2 + let values: Vec = vec![65535, 65535, 65535]; // Set equal weights for all hotkeys + let version_key = SubtensorModule::get_weights_version_key(netuid); + + // Ensure we can set weights without rate limiting + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + assert_ok!(SubtensorModule::set_weights( + origin, + netuid, + uids, + values, + version_key + )); + + // Step blocks to allow for emission distribution + step_block(11); + + // Get total stake after first payout + let total_stake_1 = SubtensorModule::get_stake_for_hotkey_on_subnet(&parent, netuid) + + SubtensorModule::get_stake_for_hotkey_on_subnet(&child1, netuid) + + SubtensorModule::get_stake_for_hotkey_on_subnet(&child2, netuid); + log::info!("total_stake_1: {total_stake_1:?}"); + + // Change parent-child relationships + mock_set_children_no_epochs( + netuid, + &parent, + &[(u64::MAX / 4, child1), (u64::MAX / 3, child2)], + ); + + // Step blocks again to allow for emission distribution + step_block(11); + + // Get total stake after second payout + let total_stake_2 = SubtensorModule::get_stake_for_hotkey_on_subnet(&parent, netuid) + + SubtensorModule::get_stake_for_hotkey_on_subnet(&child1, netuid) + + SubtensorModule::get_stake_for_hotkey_on_subnet(&child2, netuid); + log::info!("total_stake_2: {total_stake_2:?}"); + + // Check final emission distribution + let stake_parent_2 = SubtensorModule::get_inherited_for_hotkey_on_subnet(&parent, netuid); + let stake_child1_2 = SubtensorModule::get_inherited_for_hotkey_on_subnet(&child1, netuid); + let stake_child2_2 = SubtensorModule::get_inherited_for_hotkey_on_subnet(&child2, netuid); + let total_parent_stake = SubtensorModule::get_stake_for_hotkey_on_subnet(&parent, netuid); + let total_child1_stake = SubtensorModule::get_stake_for_hotkey_on_subnet(&child1, netuid); + let total_child2_stake = SubtensorModule::get_stake_for_hotkey_on_subnet(&child2, netuid); + + log::info!("Final stakes:"); + log::info!("Parent stake: {stake_parent_2}"); + log::info!("Child1 stake: {stake_child1_2}"); + log::info!("Child2 stake: {stake_child2_2}"); + + // Payout 1 + let payout_1 = total_stake_1 - total_stake_0; + log::info!("payout_1: {payout_1:?}"); + + // Payout 2 + let payout_2 = total_stake_2 - total_stake_1; + log::info!("payout_2: {payout_2:?}"); + + let total_emission = I96F32::from_num(payout_1 + payout_2); + + #[allow(non_snake_case)] + let TOLERANCE = I96F32::from_num(0.001); // Allow for a small discrepancy due to potential rounding + + // Precise assertions with tolerance + log::info!("total_emission: {total_emission:?}"); + let expected_parent_stake = + I96F32::from_num(total_parent_stake) * I96F32::from_num(5) / I96F32::from_num(12); + assert!( + (I96F32::from_num(stake_parent_2) - expected_parent_stake).abs() + / expected_parent_stake + <= TOLERANCE, + "Parent stake should be close to {expected_parent_stake:?}, but was {stake_parent_2}" + ); + // The final relationship leaves the parent with 1 - 1/4 - 1/3 = 5/12 + // of its current direct stake. + + let expected_child1_stake = I96F32::from_num(total_child1_stake) + + I96F32::from_num(total_parent_stake) / I96F32::from_num(4); + assert!( + (I96F32::from_num(stake_child1_2) - expected_child1_stake).abs() + / expected_child1_stake + <= TOLERANCE, + "Child1 stake should be close to {expected_child1_stake:?}, but was {stake_child1_2}" + ); + // Child1 inherits 1/4 of the parent's current direct stake. + + let expected_child2_stake = I96F32::from_num(total_child2_stake) + + I96F32::from_num(total_parent_stake) / I96F32::from_num(3); + assert!( + (I96F32::from_num(stake_child2_2) - expected_child2_stake).abs() + / expected_child2_stake + <= TOLERANCE, + "Child2 stake should be close to {expected_child2_stake:?}, but was {stake_child2_2}" + ); + // Child2 inherits 1/3 of the parent's current direct stake. + + // Additional checks for parent-child relationships + let parent_children: Vec<(u64, U256)> = SubtensorModule::get_children(&parent, netuid); + assert_eq!( + parent_children, + vec![(u64::MAX / 4, child1), (u64::MAX / 3, child2)], + "Parent should have both children with correct proportions" + ); + // Parent-child relationship: + // child1: 1/4 of parent's stake + // child2: 1/3 of parent's stake + + let child1_parents: Vec<(u64, U256)> = SubtensorModule::get_parents(&child1, netuid); + assert_eq!( + child1_parents, + vec![(u64::MAX / 4, parent)], + "Child1 should have parent as its parent with correct proportion" + ); + // Child1-parent relationship: + // parent: 1/4 of child1's stake + + let child2_parents: Vec<(u64, U256)> = SubtensorModule::get_parents(&child2, netuid); + assert_eq!( + child2_parents, + vec![(u64::MAX / 3, parent)], + "Child2 should have parent as its parent with correct proportion" + ); + // Child2-parent relationship: + // parent: 1/3 of child2's stake + + // Check that child2 has received more stake than child1 + assert!( + stake_child2_2 > stake_child1_2, + "Child2 should have received more emission than Child1 due to higher proportion" + ); + // Child2 stake (874,826) > Child1 stake (778,446) + }); +} + +// Test dividend distribution for children with same coldkey Owner +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::child_dividends::test_dividend_distribution_with_children_same_coldkey_owner --exact --show-output +#[test] +fn test_dividend_distribution_with_children_same_coldkey_owner() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + // Set SN owner cut to 0 + SubtensorModule::set_subnet_owner_cut(0_u16); + mock::setup_reserves( + netuid, + 1_000_000_000_000_u64.into(), + 1_000_000_000_000_u64.into(), + ); + + // Define hotkeys and coldkeys + let hotkey_a: U256 = U256::from(1); + let hotkey_b: U256 = U256::from(2); + let coldkey_a: U256 = U256::from(100); // Only one coldkey + + // Register neurons with decreasing stakes + register_ok_neuron(netuid, hotkey_a, coldkey_a, 0); + register_ok_neuron(netuid, hotkey_b, coldkey_a, 0); + + // Add initial stakes + add_balance_to_coldkey_account(&coldkey_a, 1_000.into()); + add_balance_to_coldkey_account(&coldkey_a, 1_000.into()); + + // Swap to alpha + let total_tao = 300_000 + 100_000; + let total_alpha = I96F32::from_num(mock::swap_tao_to_alpha(netuid, total_tao.into()).0); + let total_tao = I96F32::from_num(total_tao); + + // Set the stakes directly + // This avoids needing to swap tao to alpha, impacting the initial stake distribution. + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_a, + &coldkey_a, + netuid, + (total_alpha * I96F32::from_num(300_000) / total_tao) + .saturating_to_num::() + .into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_b, + &coldkey_a, + netuid, + (total_alpha * I96F32::from_num(100_000) / total_tao) + .saturating_to_num::() + .into(), + ); + + // Get old stakes + let stake_a = SubtensorModule::get_total_stake_for_hotkey(&hotkey_a); + let stake_b = SubtensorModule::get_total_stake_for_hotkey(&hotkey_b); + + // Assert initial stake is correct + let rel_stake_a = I96F32::from_num(stake_a) / total_alpha; + let rel_stake_b = I96F32::from_num(stake_b) / total_alpha; + + log::info!("rel_stake_a: {rel_stake_a:?}"); // 0.75 -> 3/4 + log::info!("rel_stake_b: {rel_stake_b:?}"); // 0.25 -> 1/4 + let epsilon = I96F32::from_num(0.0001); + assert!((rel_stake_a - I96F32::from_num(300_000) / total_tao).abs() <= epsilon); + assert!((rel_stake_b - I96F32::from_num(100_000) / total_tao).abs() <= epsilon); + + // Set parent-child relationships + // A -> B (50% of A's stake) + mock_set_children(&coldkey_a, &hotkey_a, netuid, &[(u64::MAX / 2, hotkey_b)]); + + // Set CHK take rate to 1/9 + let chk_take: I96F32 = I96F32::from_num(1_f64 / 9_f64); + let chk_take_u16: u16 = (chk_take * I96F32::from_num(u16::MAX)).saturating_to_num::(); + ChildkeyTake::::insert(hotkey_b, netuid, PerU16::from_parts(chk_take_u16)); + + // Set the weight of root TAO to be 0%, so only alpha is effective. + SubtensorModule::set_tao_weight(0); + + let hardcoded_emission: I96F32 = I96F32::from_num(1_000_000); // 1 million (adjust as needed) + + let hotkey_emission = + SubtensorModule::epoch(netuid, hardcoded_emission.saturating_to_num::().into()); + log::info!("hotkey_emission: {hotkey_emission:?}"); + let total_emission: I96F32 = hotkey_emission + .iter() + .map(|(_, _, emission)| I96F32::from_num(*emission)) + .sum(); + + // Verify emissions match expected from CHK arrangements + let em_eps: I96F32 = I96F32::from_num(1e-4); // 4 decimal places + // A's pending emission: + assert!( + ((I96F32::from_num(hotkey_emission[0].2) / total_emission) - + I96F32::from_num(3_f64 / 4_f64 * 1_f64 / 2_f64)).abs() // 3/4 * 1/2 = 3/8; 50% -> B + <= em_eps, + "A should have pending emission of 3/8 of total emission" + ); + // B's pending emission: + assert!( + ((I96F32::from_num(hotkey_emission[1].2) / total_emission) - + (I96F32::from_num(1_f64 / 4_f64 + 3_f64 / 4_f64 * 1_f64 / 2_f64))).abs() // 1/4 + 3/4 * 1/2 = 5/8; 50% from A + <= em_eps, + "B should have pending emission of 5/8 of total emission: {:?}", + I96F32::from_num(hotkey_emission[1].2) / total_emission + ); + + // Get the distribution of dividends including the Parent/Child relationship. + let dividends_a = SubtensorModule::get_parent_child_dividends_distribution( + &hotkey_a, + netuid, + hardcoded_emission.saturating_to_num::().into(), + ); + let dividends_b = SubtensorModule::get_parent_child_dividends_distribution( + &hotkey_b, + netuid, + hardcoded_emission.saturating_to_num::().into(), + ); + log::info!("dividends_a: {dividends_a:?}"); + log::info!("dividends_b: {dividends_b:?}"); + + // We expect A should have no impact from B, as they have the same owner. + assert_eq!(dividends_a.len(), 1); + assert_eq!(dividends_a[0].0, hotkey_a); + assert_eq!( + dividends_a[0].1, + hardcoded_emission.saturating_to_num::().into() + ); + assert_abs_diff_eq!( + dividends_a + .iter() + .map(|(_, emission)| u64::from(*emission)) + .sum::(), + hardcoded_emission.saturating_to_num::(), + epsilon = (hardcoded_emission / 1000).saturating_to_num::() + ); + + // Expect only 2 dividends. Parent key A and child key B. + assert_eq!(dividends_b.len(), 2); // A and B + assert_eq!(dividends_b[0].0, hotkey_a); + assert_eq!(dividends_b[1].0, hotkey_b); + + // We expect B's coldkey to have no increase in dividends from A, as they have the same owner. + // And therefore, B should get no CHK_TAKE. + + // A should also have no decrease because there is no CHK_TAKE. + let total_stake_b = rel_stake_b + rel_stake_a * 1 / 2; + let expected_b_b: u64 = + (rel_stake_b / total_stake_b * hardcoded_emission).saturating_to_num::(); + + assert_abs_diff_eq!( + u64::from(dividends_b[1].1), + expected_b_b, + epsilon = (hardcoded_emission / 1000).saturating_to_num::(), + ); + + let expected_b_a: u64 = + ((rel_stake_a * 1 / 2) / total_stake_b * hardcoded_emission).saturating_to_num::(); + assert_eq!(dividends_b[0].0, hotkey_a); + assert_abs_diff_eq!( + u64::from(dividends_b[0].1), + expected_b_a, + epsilon = (hardcoded_emission / 1000).saturating_to_num::() + ); + assert_abs_diff_eq!( + dividends_b + .iter() + .map(|(_, emission)| u64::from(*emission)) + .sum::(), + hardcoded_emission.saturating_to_num::(), + epsilon = (hardcoded_emission / 1000).saturating_to_num::() + ); + }); +} diff --git a/pallets/subtensor/src/tests/children/child_emission.rs b/pallets/subtensor/src/tests/children/child_emission.rs new file mode 100644 index 0000000000..9bdfee7bc1 --- /dev/null +++ b/pallets/subtensor/src/tests/children/child_emission.rs @@ -0,0 +1,379 @@ +#![allow(clippy::indexing_slicing)] +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] +use super::super::mock; +use super::super::mock::*; +use approx::assert_abs_diff_eq; +use substrate_fixed::types::I96F32; +use subtensor_runtime_common::{AlphaBalance, TaoBalance}; + +use crate::*; +use sp_core::U256; +use sp_runtime::PerU16; +use subtensor_swap_interface::SwapHandler; + +// 44: Test with a chain of parent-child relationships (e.g., A -> B -> C) +// This test verifies the correct distribution of emissions in a chain of parent-child relationships: +// - Sets up a network with three neurons A, B, and C in a chain (A -> B -> C) +// - Establishes parent-child relationships with different stake proportions +// - Sets weights for all neurons +// - Runs an epoch with a hardcoded emission value +// - Checks the emission distribution among A, B, and C +// - Verifies that all parties received emissions and the total stake increased correctly +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::child_emission::test_parent_child_chain_emission --exact --show-output +#[test] +fn test_parent_child_chain_emission() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + remove_owner_registration_stake(netuid); + SubtensorModule::set_ck_burn(0); + Tempo::::insert(netuid, 1); + + // Setup large LPs to prevent slippage + SubnetTAO::::insert(netuid, TaoBalance::from(1_000_000_000_000_000_u64)); + SubnetAlphaIn::::insert(netuid, AlphaBalance::from(1_000_000_000_000_000_u64)); + + // Set owner cut to 0 + SubtensorModule::set_subnet_owner_cut(0_u16); + + // Define hotkeys and coldkeys + let hotkey_a: U256 = U256::from(1); + let hotkey_b: U256 = U256::from(2); + let hotkey_c: U256 = U256::from(3); + let coldkey_a: U256 = U256::from(100); + let coldkey_b: U256 = U256::from(101); + let coldkey_c: U256 = U256::from(102); + + // Register neurons with decreasing stakes + register_ok_neuron(netuid, hotkey_a, coldkey_a, 0); + register_ok_neuron(netuid, hotkey_b, coldkey_b, 0); + register_ok_neuron(netuid, hotkey_c, coldkey_c, 0); + + // Add initial stakes + add_balance_to_coldkey_account(&coldkey_a, 1_000.into()); + add_balance_to_coldkey_account(&coldkey_b, 1_000.into()); + add_balance_to_coldkey_account(&coldkey_c, 1_000.into()); + + // Swap to alpha + let stake_a = 300_000_000_000_u64; + let stake_b = 100_000_000_000_u64; + let stake_c = 50_000_000_000_u64; + let total_tao: I96F32 = I96F32::from_num(stake_a + stake_b + stake_c); + let total_alpha: I96F32 = I96F32::from_num( + SubtensorModule::swap_tao_for_alpha( + netuid, + total_tao.to_num::().into(), + ::SwapInterface::max_price(), + false, + ) + .unwrap() + .amount_paid_out, + ); + + // Set the stakes directly + // This avoids needing to swap tao to alpha, impacting the initial stake distribution. + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_a, + &coldkey_a, + netuid, + (total_alpha * I96F32::from_num(stake_a) / total_tao) + .saturating_to_num::() + .into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_b, + &coldkey_b, + netuid, + (total_alpha * I96F32::from_num(stake_b) / total_tao) + .saturating_to_num::() + .into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_c, + &coldkey_c, + netuid, + (total_alpha * I96F32::from_num(stake_c) / total_tao) + .saturating_to_num::() + .into(), + ); + + // Get old stakes + let stake_a = SubtensorModule::get_total_stake_for_hotkey(&hotkey_a); + let stake_b = SubtensorModule::get_total_stake_for_hotkey(&hotkey_b); + let stake_c = SubtensorModule::get_total_stake_for_hotkey(&hotkey_c); + + let _total_stake: I96F32 = I96F32::from_num(stake_a + stake_b + stake_c); + + // Assert initial stake is correct + let rel_stake_a = I96F32::from_num(stake_a) / total_tao; + let rel_stake_b = I96F32::from_num(stake_b) / total_tao; + let rel_stake_c = I96F32::from_num(stake_c) / total_tao; + + log::info!("rel_stake_a: {rel_stake_a:?}"); // 0.6666 -> 2/3 + log::info!("rel_stake_b: {rel_stake_b:?}"); // 0.2222 -> 2/9 + log::info!("rel_stake_c: {rel_stake_c:?}"); // 0.1111 -> 1/9 + assert!((rel_stake_a - I96F32::from_num(stake_a) / total_tao).abs() < 0.001); + assert!((rel_stake_b - I96F32::from_num(stake_b) / total_tao).abs() < 0.001); + assert!((rel_stake_c - I96F32::from_num(stake_c) / total_tao).abs() < 0.001); + + // Set parent-child relationships + // A -> B (50% of A's stake) + mock_set_children_no_epochs(netuid, &hotkey_a, &[(u64::MAX / 2, hotkey_b)]); + + // B -> C (50% of B's stake) + mock_set_children_no_epochs(netuid, &hotkey_b, &[(u64::MAX / 2, hotkey_c)]); + + // Get old stakes after children are scheduled + let stake_a_old = SubtensorModule::get_total_stake_for_hotkey(&hotkey_a); + let stake_b_old = SubtensorModule::get_total_stake_for_hotkey(&hotkey_b); + let stake_c_old = SubtensorModule::get_total_stake_for_hotkey(&hotkey_c); + + let total_stake_old: I96F32 = + I96F32::from_num((stake_a_old + stake_b_old + stake_c_old).to_u64()); + log::info!("Old stake for hotkey A: {stake_a_old:?}"); + log::info!("Old stake for hotkey B: {stake_b_old:?}"); + log::info!("Old stake for hotkey C: {stake_c_old:?}"); + log::info!("Total old stake: {total_stake_old:?}"); + + // Set CHK take rate to 1/9 + let chk_take: I96F32 = I96F32::from_num(1_f64 / 9_f64); + let chk_take_u16: u16 = (chk_take * I96F32::from_num(u16::MAX)).saturating_to_num::(); + ChildkeyTake::::insert(hotkey_b, netuid, PerU16::from_parts(chk_take_u16)); + ChildkeyTake::::insert(hotkey_c, netuid, PerU16::from_parts(chk_take_u16)); + + // Set the weight of root TAO to be 0%, so only alpha is effective. + SubtensorModule::set_tao_weight(0); + + let emission = SubtensorModule::get_block_emission(); + + // Set pending emission to 0 + PendingValidatorEmission::::insert(netuid, AlphaBalance::ZERO); + PendingServerEmission::::insert(netuid, AlphaBalance::ZERO); + + // To trigger the epoch, block should be > tempo. So we advance it before + System::set_block_number(2); + + // Run epoch with emission value + let emission_value = u64::from(emission.peek()); + SubtensorModule::run_coinbase(emission); + + // Log new stake + let stake_a_new = SubtensorModule::get_total_stake_for_hotkey(&hotkey_a); + let stake_b_new = SubtensorModule::get_total_stake_for_hotkey(&hotkey_b); + let stake_c_new = SubtensorModule::get_total_stake_for_hotkey(&hotkey_c); + let total_stake_new = I96F32::from_num((stake_a_new + stake_b_new + stake_c_new).to_u64()); + log::info!("Stake for hotkey A: {stake_a_new:?}"); + log::info!("Stake for hotkey B: {stake_b_new:?}"); + log::info!("Stake for hotkey C: {stake_c_new:?}"); + + let stake_inc_a = stake_a_new - stake_a_old; + let stake_inc_b = stake_b_new - stake_b_old; + let stake_inc_c = stake_c_new - stake_c_old; + let total_stake_inc: I96F32 = total_stake_new - total_stake_old; + log::info!("Stake increase for hotkey A: {stake_inc_a:?}"); + log::info!("Stake increase for hotkey B: {stake_inc_b:?}"); + log::info!("Stake increase for hotkey C: {stake_inc_c:?}"); + log::info!("Total stake increase: {total_stake_inc:?}"); + let rel_stake_inc_a = I96F32::from_num(stake_inc_a) / total_stake_inc; + let rel_stake_inc_b = I96F32::from_num(stake_inc_b) / total_stake_inc; + let rel_stake_inc_c = I96F32::from_num(stake_inc_c) / total_stake_inc; + log::info!("rel_stake_inc_a: {rel_stake_inc_a:?}"); + log::info!("rel_stake_inc_b: {rel_stake_inc_b:?}"); + log::info!("rel_stake_inc_c: {rel_stake_inc_c:?}"); + + // Verify the final stake distribution + let stake_inc_eps = I96F32::from_num(1e-4); // 4 decimal places + + // Each child has chk_take take + let expected_a = I96F32::from_num(2_f64 / 3_f64) + * (I96F32::from_num(1_f64) - (I96F32::from_num(1_f64 / 2_f64) * chk_take)); + assert!( + (rel_stake_inc_a - expected_a).abs() // B's take on 50% CHK + <= stake_inc_eps, + "A should have {expected_a:?} of total stake increase; {rel_stake_inc_a:?}" + ); + let expected_b = I96F32::from_num(2_f64 / 9_f64) + * (I96F32::from_num(1_f64) - (I96F32::from_num(1_f64 / 2_f64) * chk_take)) + + I96F32::from_num(2_f64 / 3_f64) * (I96F32::from_num(1_f64 / 2_f64) * chk_take); + assert!( + (rel_stake_inc_b - expected_b).abs() // C's take on 50% CHK + take from A + <= stake_inc_eps, + "B should have {expected_b:?} of total stake increase; {rel_stake_inc_b:?}" + ); + let expected_c = I96F32::from_num(1_f64 / 9_f64) + + (I96F32::from_num(2_f64 / 9_f64) * I96F32::from_num(1_f64 / 2_f64) * chk_take); + assert!( + (rel_stake_inc_c - expected_c).abs() // B's take on 50% CHK + <= stake_inc_eps, + "C should have {expected_c:?} of total stake increase; {rel_stake_inc_c:?}" + ); + + let hotkeys = [hotkey_a, hotkey_b, hotkey_c]; + let mut total_stake_now = AlphaBalance::ZERO; + for (hotkey, netuid, stake) in TotalHotkeyAlpha::::iter() { + if hotkeys.contains(&hotkey) { + total_stake_now += stake; + } else { + log::info!("hotkey: {hotkey:?}, netuid: {netuid:?}, stake: {stake:?}"); + } + } + log::info!("total_stake_now: {total_stake_now:?}, total_stake_new: {total_stake_new:?}"); + + assert_abs_diff_eq!( + total_stake_inc.to_num::(), + emission_value, + epsilon = emission_value / 1000, + ); + }); +} + +// 45: Test *epoch* with a chain of parent-child relationships (e.g., A -> B -> C) +// This test verifies the correct distribution of emissions in a chain of parent-child relationships: +// - Sets up a network with three neurons A, B, and C in a chain (A -> B -> C) +// - Establishes parent-child relationships with different stake proportions +// - Sets weights for all neurons +// - Runs an epoch with a hardcoded emission value +// - Checks the emission distribution among A, B, and C +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::child_emission::test_parent_child_chain_epoch --exact --show-output +#[test] +fn test_parent_child_chain_epoch() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + SubtensorModule::set_ck_burn(0); + // Set owner cut to 0 + SubtensorModule::set_subnet_owner_cut(0_u16); + + // Define hotkeys and coldkeys + let hotkey_a: U256 = U256::from(1); + let hotkey_b: U256 = U256::from(2); + let hotkey_c: U256 = U256::from(3); + let coldkey_a: U256 = U256::from(100); + let coldkey_b: U256 = U256::from(101); + let coldkey_c: U256 = U256::from(102); + + // Register neurons with decreasing stakes + register_ok_neuron(netuid, hotkey_a, coldkey_a, 0); + register_ok_neuron(netuid, hotkey_b, coldkey_b, 0); + register_ok_neuron(netuid, hotkey_c, coldkey_c, 0); + + // Add initial stakes + add_balance_to_coldkey_account(&coldkey_a, 1_000.into()); + add_balance_to_coldkey_account(&coldkey_b, 1_000.into()); + add_balance_to_coldkey_account(&coldkey_c, 1_000.into()); + + mock::setup_reserves( + netuid, + 1_000_000_000_000_u64.into(), + 1_000_000_000_000_u64.into(), + ); + + // Swap to alpha + let total_tao = I96F32::from_num(300_000 + 100_000 + 50_000); + let (total_alpha, _) = mock::swap_tao_to_alpha(netuid, total_tao.to_num::().into()); + let total_alpha = I96F32::from_num(total_alpha); + + // Set the stakes directly + // This avoids needing to swap tao to alpha, impacting the initial stake distribution. + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_a, + &coldkey_a, + netuid, + (total_alpha * I96F32::from_num(300_000) / total_tao) + .saturating_to_num::() + .into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_b, + &coldkey_b, + netuid, + (total_alpha * I96F32::from_num(100_000) / total_tao) + .saturating_to_num::() + .into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_c, + &coldkey_c, + netuid, + (total_alpha * I96F32::from_num(50_000) / total_tao) + .saturating_to_num::() + .into(), + ); + + // Get old stakes + let stake_a = SubtensorModule::get_total_stake_for_hotkey(&hotkey_a); + let stake_b = SubtensorModule::get_total_stake_for_hotkey(&hotkey_b); + let stake_c = SubtensorModule::get_total_stake_for_hotkey(&hotkey_c); + + // Assert initial stake is correct + let rel_stake_a = I96F32::from_num(stake_a) / total_alpha; + let rel_stake_b = I96F32::from_num(stake_b) / total_alpha; + let rel_stake_c = I96F32::from_num(stake_c) / total_alpha; + + log::info!("rel_stake_a: {rel_stake_a:?}"); // 0.6666 -> 2/3 + log::info!("rel_stake_b: {rel_stake_b:?}"); // 0.2222 -> 2/9 + log::info!("rel_stake_c: {rel_stake_c:?}"); // 0.1111 -> 1/9 + + assert!(rel_stake_a > I96F32::from_num(0)); + assert!(rel_stake_b > I96F32::from_num(0)); + assert!(rel_stake_c > I96F32::from_num(0)); + + // because of the fee we allow slightly higher range + let epsilon = I96F32::from_num(0.00001); + assert!((rel_stake_a - (I96F32::from_num(300_000) / total_tao)).abs() <= epsilon); + assert!((rel_stake_b - (I96F32::from_num(100_000) / total_tao)).abs() <= epsilon); + assert!((rel_stake_c - (I96F32::from_num(50_000) / total_tao)).abs() <= epsilon); + + // Set parent-child relationships + // A -> B (50% of A's stake) + mock_set_children(&coldkey_a, &hotkey_a, netuid, &[(u64::MAX / 2, hotkey_b)]); + + // B -> C (50% of B's stake) + mock_set_children(&coldkey_b, &hotkey_b, netuid, &[(u64::MAX / 2, hotkey_c)]); + + // Set CHK take rate to 1/9 + let chk_take = I96F32::from_num(1_f64 / 9_f64); + let chk_take_u16: u16 = (chk_take * I96F32::from_num(u16::MAX)).saturating_to_num::(); + ChildkeyTake::::insert(hotkey_b, netuid, PerU16::from_parts(chk_take_u16)); + ChildkeyTake::::insert(hotkey_c, netuid, PerU16::from_parts(chk_take_u16)); + + // Set the weight of root TAO to be 0%, so only alpha is effective. + SubtensorModule::set_tao_weight(0); + + let hardcoded_emission = I96F32::from_num(1_000_000); // 1 million (adjust as needed) + + let hotkey_emission = + SubtensorModule::epoch(netuid, hardcoded_emission.saturating_to_num::().into()); + log::info!("hotkey_emission: {hotkey_emission:?}"); + let total_emission: I96F32 = hotkey_emission + .iter() + .map(|(_, _, emission)| I96F32::from_num(*emission)) + .sum(); + + // Verify emissions match expected from CHK arrangements + let em_eps = I96F32::from_num(1e-4); // 4 decimal places + // A's pending emission: + assert!( + ((I96F32::from_num(hotkey_emission[0].2) / total_emission) - + I96F32::from_num(2_f64 / 3_f64 * 1_f64 / 2_f64)).abs() // 2/3 * 1/2 = 1/3; 50% -> B + <= em_eps, + "A should have pending emission of 1/3 of total emission" + ); + // B's pending emission: + assert!( + ((I96F32::from_num(hotkey_emission[1].2) / total_emission) - + (I96F32::from_num(2_f64 / 9_f64 * 1_f64 / 2_f64 + 2_f64 / 3_f64 * 1_f64 / 2_f64))).abs() // 2/9 * 1/2 + 2/3 * 1/2; 50% -> C + 50% from A + <= em_eps, + "B should have pending emission of 4/9 of total emission" + ); + // C's pending emission: + assert!( + ((I96F32::from_num(hotkey_emission[2].2) / total_emission) - + (I96F32::from_num(1_f64 / 9_f64 + 1_f64 / 2_f64 * 2_f64 / 9_f64))).abs() // 1/9 + 2/9 * 1/2; 50% from B + <= em_eps, + "C should have pending emission of 1/9 of total emission" + ); + }); +} diff --git a/pallets/subtensor/src/tests/children/child_weights.rs b/pallets/subtensor/src/tests/children/child_weights.rs new file mode 100644 index 0000000000..9a30b9f018 --- /dev/null +++ b/pallets/subtensor/src/tests/children/child_weights.rs @@ -0,0 +1,346 @@ +#![allow(clippy::indexing_slicing)] +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] +use super::super::mock::*; +use frame_support::{assert_noop, assert_ok}; +use substrate_fixed::types::I64F64; +use subtensor_runtime_common::{AlphaBalance, TaoBalance}; + +use crate::*; +use sp_core::U256; + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::child_weights::test_childkey_set_weights_single_parent --exact --show-output --nocapture +#[test] +fn test_childkey_set_weights_single_parent() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = + add_dynamic_network_disable_commit_reveal(&subnet_owner_hotkey, &subnet_owner_coldkey); + Tempo::::insert(netuid, 1); + + // Define hotkeys + let parent: U256 = U256::from(1); + let child: U256 = U256::from(2); + let weight_setter: U256 = U256::from(3); + + // Define coldkeys with more readable names + let coldkey_parent: U256 = U256::from(100); + let coldkey_child: U256 = U256::from(101); + let coldkey_weight_setter: U256 = U256::from(102); + + let balance_to_give_child = TaoBalance::from(109_999); + let stake_to_give_child = AlphaBalance::from(109_999); + + // Register parent with minimal stake and child with high stake + add_balance_to_coldkey_account(&coldkey_parent, 1.into()); + add_balance_to_coldkey_account(&coldkey_child, balance_to_give_child + 10.into()); + add_balance_to_coldkey_account(&coldkey_weight_setter, 1_000_000.into()); + + // Add neurons for parent, child and weight_setter + register_ok_neuron(netuid, parent, coldkey_parent, 1); + register_ok_neuron(netuid, child, coldkey_child, 1); + register_ok_neuron(netuid, weight_setter, coldkey_weight_setter, 1); + + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &parent, + &coldkey_parent, + netuid, + stake_to_give_child, + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &weight_setter, + &coldkey_weight_setter, + netuid, + 1_000_000.into(), + ); + + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + // Set parent-child relationship + mock_set_children_no_epochs(netuid, &parent, &[(u64::MAX, child)]); + + // Set weights on the child using the weight_setter account + let origin = RuntimeOrigin::signed(weight_setter); + let uids: Vec = vec![1]; // Only set weight for the child (UID 1) + let values: Vec = vec![u16::MAX]; // Use maximum value for u16 + let version_key = SubtensorModule::get_weights_version_key(netuid); + ValidatorPermit::::insert(netuid, vec![true, true, true, true]); + assert_ok!(SubtensorModule::set_weights( + origin, + netuid, + uids.clone(), + values.clone(), + version_key + )); + + // Set the min stake very high + SubtensorModule::set_stake_threshold(u64::from(stake_to_give_child) * 5); + + // Check the child has less stake than required + assert!( + SubtensorModule::get_stake_weights_for_hotkey_on_subnet(&child, netuid).0 + < SubtensorModule::get_stake_threshold() + ); + + // Check the child cannot set weights + assert_noop!( + SubtensorModule::set_weights( + RuntimeOrigin::signed(child), + netuid, + uids.clone(), + values.clone(), + version_key + ), + Error::::NotEnoughStakeToSetWeights + ); + + assert!(!SubtensorModule::check_weights_min_stake(&child, netuid)); + + // Set a minimum stake to set weights + SubtensorModule::set_stake_threshold(u64::from(stake_to_give_child) - 5); + + // Check if the stake for the child is above + assert!( + SubtensorModule::get_stake_weights_for_hotkey_on_subnet(&child, netuid).0 + >= SubtensorModule::get_stake_threshold() + ); + + // Check the child can set weights + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(child), + netuid, + uids, + values, + version_key + )); + + assert!(SubtensorModule::check_weights_min_stake(&child, netuid)); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --test children -- test_set_weights_no_parent --exact --nocapture +#[test] +fn test_set_weights_no_parent() { + // Verify that a regular key without a parent delegation is effected by the minimum stake requirements + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = + add_dynamic_network_disable_commit_reveal(&subnet_owner_hotkey, &subnet_owner_coldkey); + + let hotkey: U256 = U256::from(2); + let spare_hk: U256 = U256::from(3); + + let coldkey: U256 = U256::from(101); + let spare_ck = U256::from(102); + + let balance_to_give_child = TaoBalance::from(109_999); + let stake_to_give_child = AlphaBalance::from(109_999); + + add_balance_to_coldkey_account(&coldkey, balance_to_give_child + 10.into()); + + // Is registered + register_ok_neuron(netuid, hotkey, coldkey, 1); + // Register a spare key + register_ok_neuron(netuid, spare_hk, spare_ck, 1); + + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + stake_to_give_child, + ); + + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + // Has stake and no parent + step_block(7200 + 1); + + let uids: Vec = vec![1]; // Set weights on the other hotkey + let values: Vec = vec![u16::MAX]; // Use maximum value for u16 + let version_key = SubtensorModule::get_weights_version_key(netuid); + + // Check the stake weight + let curr_stake_weight = + SubtensorModule::get_stake_weights_for_hotkey_on_subnet(&hotkey, netuid).0; + + // Set the min stake very high, above the stake weight of the key + SubtensorModule::set_stake_threshold( + curr_stake_weight + .saturating_mul(I64F64::saturating_from_num(5)) + .saturating_to_num::(), + ); + + let curr_stake_threshold = SubtensorModule::get_stake_threshold(); + assert!( + curr_stake_weight < curr_stake_threshold, + "{curr_stake_weight:?} is not less than {curr_stake_threshold:?} " + ); + + // Check the hotkey cannot set weights + assert_noop!( + SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + values.clone(), + version_key + ), + Error::::NotEnoughStakeToSetWeights + ); + + assert!(!SubtensorModule::check_weights_min_stake(&hotkey, netuid)); + + // Set a minimum stake to set weights + SubtensorModule::set_stake_threshold( + (curr_stake_weight - I64F64::from_num(5)).to_num::(), + ); + + // Check if the stake for the hotkey is above + let new_stake_weight = + SubtensorModule::get_stake_weights_for_hotkey_on_subnet(&hotkey, netuid).0; + let new_stake_threshold = SubtensorModule::get_stake_threshold(); + assert!( + new_stake_weight >= new_stake_threshold, + "{new_stake_weight:?} is not greater than or equal to {new_stake_threshold:?} " + ); + + // Check the hotkey can set weights + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids, + values, + version_key + )); + + assert!(SubtensorModule::check_weights_min_stake(&hotkey, netuid)); + }); +} + +// Test that the subnet owner can always set weights (owner bypass in check_weights_min_stake) +// and that do_set_root_validators_for_subnet correctly creates parent-child relationships. +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::child_weights::test_root_children_enable_subnet_owner_set_weights --exact --show-output --nocapture +#[test] +fn test_root_children_enable_subnet_owner_set_weights() { + new_test_ext(1).execute_with(|| { + // --- Setup accounts --- + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + + let root_val_coldkey_1 = U256::from(100); + let root_val_hotkey_1 = U256::from(101); + let root_val_coldkey_2 = U256::from(200); + let root_val_hotkey_2 = U256::from(201); + + // --- Create root network and subnet --- + add_network(NetUid::ROOT, 1, 0); + let netuid = + add_dynamic_network_disable_commit_reveal(&subnet_owner_hotkey, &subnet_owner_coldkey); + + // --- Register root validators on a subnet first (required before root_register) --- + register_ok_neuron(netuid, root_val_hotkey_1, root_val_coldkey_1, 0); + register_ok_neuron(netuid, root_val_hotkey_2, root_val_coldkey_2, 0); + + // --- Register root validators on root network --- + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(root_val_coldkey_1), + root_val_hotkey_1, + )); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(root_val_coldkey_2), + root_val_hotkey_2, + )); + + // --- Add stake for root validators on root and the subnet --- + let root_stake = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &root_val_hotkey_1, + &root_val_coldkey_1, + NetUid::ROOT, + root_stake, + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &root_val_hotkey_1, + &root_val_coldkey_1, + netuid, + root_stake, + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &root_val_hotkey_2, + &root_val_coldkey_2, + NetUid::ROOT, + root_stake, + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &root_val_hotkey_2, + &root_val_coldkey_2, + netuid, + root_stake, + ); + + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + let version_key = SubtensorModule::get_weights_version_key(netuid); + let uids: Vec = vec![0]; + let values: Vec = vec![u16::MAX]; + + // Subnet owner can set weights with default (zero) stake threshold. + assert!( + SubtensorModule::check_weights_min_stake(&subnet_owner_hotkey, netuid), + "Subnet owner should pass the min stake check with default threshold" + ); + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(subnet_owner_hotkey), + netuid, + uids.clone(), + values.clone(), + version_key + )); + + // Subnet owner can still set weights after raising the stake threshold (owner bypass). + SubtensorModule::set_stake_threshold(500_000_000u64); + assert!( + SubtensorModule::check_weights_min_stake(&subnet_owner_hotkey, netuid), + "Subnet owner should pass the min stake check even with high threshold" + ); + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(subnet_owner_hotkey), + netuid, + uids.clone(), + values.clone(), + version_key + )); + + // --- Verify do_set_root_validators_for_subnet creates parent-child relationships --- + assert_ok!(SubtensorModule::set_pending_childkey_cooldown( + RuntimeOrigin::root(), + 0, + )); + + assert_ok!(SubtensorModule::do_set_root_validators_for_subnet(netuid)); + + // Activate pending children (cooldown is 0, advance 1 block) + step_block(1); + SubtensorModule::do_set_pending_children(netuid); + + // Each root validator should have the subnet owner hotkey as a child on netuid + let children_1 = SubtensorModule::get_children(&root_val_hotkey_1, netuid); + assert_eq!( + children_1, + vec![(u64::MAX, subnet_owner_hotkey)], + "Root validator 1 should have subnet owner as child" + ); + let children_2 = SubtensorModule::get_children(&root_val_hotkey_2, netuid); + assert_eq!( + children_2, + vec![(u64::MAX, subnet_owner_hotkey)], + "Root validator 2 should have subnet owner as child" + ); + + // Subnet owner should have both root validators as parents + let parents = SubtensorModule::get_parents(&subnet_owner_hotkey, netuid); + assert_eq!(parents.len(), 2, "Subnet owner should have 2 parents"); + }); +} diff --git a/pallets/subtensor/src/tests/children/childkey_take.rs b/pallets/subtensor/src/tests/children/childkey_take.rs new file mode 100644 index 0000000000..0bd90afa58 --- /dev/null +++ b/pallets/subtensor/src/tests/children/childkey_take.rs @@ -0,0 +1,580 @@ +#![allow(clippy::indexing_slicing)] +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] +use super::super::mock; +use super::super::mock::*; +use approx::assert_abs_diff_eq; +use frame_support::{assert_noop, assert_ok}; +use subtensor_runtime_common::{NetUidStorageIndex, TaoBalance}; + +use crate::{utils::rate_limiting::TransactionType, *}; +use sp_core::U256; +use sp_runtime::PerU16; + +// 24: Test childkey take functionality +// This test verifies the functionality of setting and getting childkey take: +// - Sets up a network and registers a hotkey +// - Checks default and maximum childkey take values +// - Sets a new childkey take value +// - Verifies the new take value is stored correctly +// - Attempts to set an invalid take value and checks for appropriate error +// - Tries to set take with a non-associated coldkey and verifies the error +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::childkey_take::test_childkey_take_functionality --exact --show-output --nocapture +#[test] +fn test_childkey_take_functionality() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = NetUid::from(1); + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Test default and max childkey take + let default_take = SubtensorModule::get_default_childkey_take(); + let min_take = SubtensorModule::get_min_childkey_take(); + log::info!("Default take: {default_take}, Max take: {min_take}"); + + // Check if default take and max take are the same + assert_eq!( + default_take, min_take, + "Default take should be equal to max take" + ); + + // Log the actual value of MaxChildkeyTake + log::info!( + "MaxChildkeyTake value: {:?}", + MaxChildkeyTake::::get() + ); + + // Test setting childkey take + let new_take: u16 = SubtensorModule::get_max_childkey_take() / 2; // 50% of max_take + assert_ok!(SubtensorModule::set_childkey_take( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + PerU16::from_parts(new_take) + )); + + // Verify childkey take was set correctly + let stored_take = SubtensorModule::get_childkey_take(&hotkey, netuid); + log::info!("Stored take: {stored_take}"); + assert_eq!(stored_take, new_take); + + // Test setting childkey take outside of allowed range + let invalid_take: u16 = SubtensorModule::get_max_childkey_take() + 1; + assert_noop!( + SubtensorModule::set_childkey_take( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + PerU16::from_parts(invalid_take) + ), + Error::::InvalidChildkeyTake + ); + + // Test setting childkey take with non-associated coldkey + let non_associated_coldkey = U256::from(999); + assert_noop!( + SubtensorModule::set_childkey_take( + RuntimeOrigin::signed(non_associated_coldkey), + hotkey, + netuid, + PerU16::from_parts(new_take) + ), + Error::::NonAssociatedColdKey + ); + }); +} + +#[test] +fn test_childkey_take_respects_effective_subnet_minimum() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = NetUid::from(1); + let subnet_min = SubtensorModule::get_max_childkey_take() / 2; + + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + SubtensorModule::set_min_childkey_take_for_subnet(netuid, PerU16::from_parts(subnet_min)); + + assert_eq!( + SubtensorModule::get_effective_min_childkey_take(netuid), + subnet_min + ); + assert_eq!( + SubtensorModule::get_childkey_take(&hotkey, netuid), + subnet_min + ); + + assert_noop!( + SubtensorModule::set_childkey_take( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + PerU16::from_parts(subnet_min - 1) + ), + Error::::InvalidChildkeyTake + ); + + assert_ok!(SubtensorModule::set_childkey_take( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + PerU16::from_parts(subnet_min) + )); + + ChildkeyTake::::insert(hotkey, netuid, PerU16::from_parts(subnet_min - 1)); + assert_eq!( + SubtensorModule::get_childkey_take(&hotkey, netuid), + subnet_min + ); + }); +} + +// 25: Test childkey take rate limiting +// This test verifies the rate limiting functionality for setting childkey take: +// - Sets up a network and registers a hotkey +// - Sets a rate limit for childkey take changes +// - Performs multiple attempts to set childkey take +// - Verifies that rate limiting prevents frequent changes +// - Advances blocks to bypass rate limit and confirms successful change +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::childkey_take::test_childkey_take_rate_limiting --exact --show-output --nocapture +#[test] +fn test_childkey_take_rate_limiting() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = NetUid::from(1); + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Set a rate limit for childkey take changes + let rate_limit: u64 = 100; + SubtensorModule::set_tx_childkey_take_rate_limit(rate_limit); + + log::info!( + "Set TxChildkeyTakeRateLimit: {:?}", + TxChildkeyTakeRateLimit::::get() + ); + + // Helper function to log rate limit information + let log_rate_limit_info = || { + let current_block = SubtensorModule::get_current_block_as_u64(); + let last_block = TransactionType::SetChildkeyTake.last_block_on_subnet::( + &hotkey, + netuid, + ); + let passes = TransactionType::SetChildkeyTake.passes_rate_limit_on_subnet::( + &hotkey, + netuid, + ); + let limit = TransactionType::SetChildkeyTake.rate_limit_on_subnet::(netuid); + log::info!( + "Rate limit info: current_block: {}, last_block: {}, limit: {}, passes: {}, diff: {}", + current_block, + last_block, + limit, + passes, + current_block - last_block + ); + }; + + // First transaction (should succeed) + log_rate_limit_info(); + assert_ok!(SubtensorModule::set_childkey_take( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + PerU16::from_parts(500) + )); + log_rate_limit_info(); + + // Second transaction (should fail due to rate limit) + log_rate_limit_info(); + assert_noop!( + SubtensorModule::set_childkey_take( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + PerU16::from_parts(600) + ), + Error::::TxChildkeyTakeRateLimitExceeded + ); + log_rate_limit_info(); + + // Advance the block number to just before the rate limit + run_to_block(rate_limit - 1); + + // Third transaction (should still fail) + log_rate_limit_info(); + assert_noop!( + SubtensorModule::set_childkey_take( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + PerU16::from_parts(650) + ), + Error::::TxChildkeyTakeRateLimitExceeded + ); + log_rate_limit_info(); + + // Advance the block number to just after the rate limit + run_to_block(rate_limit + 1); + + // Fourth transaction (should succeed) + log_rate_limit_info(); + assert_ok!(SubtensorModule::set_childkey_take( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + PerU16::from_parts(700) + )); + log_rate_limit_info(); + + // Verify the final take was set + let stored_take = SubtensorModule::get_childkey_take(&hotkey, netuid); + assert_eq!(stored_take, 700); + }); +} + +// 26: Test childkey take functionality across multiple networks +// This test verifies the childkey take functionality across multiple networks: +// - Creates multiple networks and sets up neurons +// - Sets unique childkey take values for each network +// - Verifies that each network has a different childkey take value +// - Attempts to set childkey take again (should fail due to rate limit) +// - Advances blocks to bypass rate limit and successfully updates take value +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::childkey_take::test_multiple_networks_childkey_take --exact --show-output --nocapture +#[test] +fn test_multiple_networks_childkey_take() { + new_test_ext(1).execute_with(|| { + const NUM_NETWORKS: u16 = 10; + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + // Create 10 networks and set up neurons (skip network 0) + for netuid in 1..NUM_NETWORKS { + let netuid = NetUid::from(netuid); + // Add network + add_network(netuid, 13, 0); + + // Register neuron + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Set a unique childkey take value for each network + let take_value = u16::from(netuid.next()) * 100; // Values will be 200, 300, ..., 1000 + assert_ok!(SubtensorModule::set_childkey_take( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + PerU16::from_parts(take_value) + )); + + // Verify the childkey take was set correctly + let stored_take = SubtensorModule::get_childkey_take(&hotkey, netuid); + assert_eq!( + stored_take, take_value, + "Childkey take not set correctly for network {netuid}" + ); + + // Log the set value + log::info!("Network {netuid}: Childkey take set to {take_value}"); + } + + // Verify all networks have different childkey take values + for i in 1..NUM_NETWORKS { + for j in (i + 1)..NUM_NETWORKS { + let take_i = SubtensorModule::get_childkey_take(&hotkey, i.into()); + let take_j = SubtensorModule::get_childkey_take(&hotkey, j.into()); + assert_ne!( + take_i, take_j, + "Childkey take values should be different for networks {i} and {j}" + ); + } + } + + // Attempt to set childkey take again (should fail due to rate limit) + let result = SubtensorModule::set_childkey_take( + RuntimeOrigin::signed(coldkey), + hotkey, + 1.into(), + PerU16::from_parts(1100), + ); + assert_noop!(result, Error::::TxChildkeyTakeRateLimitExceeded); + + // Advance blocks to bypass rate limit + run_to_block(SubtensorModule::get_tx_childkey_take_rate_limit() + 1); + + // Now setting childkey take should succeed + assert_ok!(SubtensorModule::set_childkey_take( + RuntimeOrigin::signed(coldkey), + hotkey, + 1.into(), + PerU16::from_parts(1100) + )); + + // Verify the new take value + let new_take = SubtensorModule::get_childkey_take(&hotkey, 1.into()); + assert_eq!(new_take, 1100, "Childkey take not updated after rate limit"); + }); +} + +/// Test that distribute_emission sends childkey take fully to the nominators if childkey +/// doesn't have its own stake, independently of parent hotkey take. +/// cargo test --package pallet-subtensor --lib -- tests::children::childkey_take::test_childkey_take_drain --exact --show-output +#[allow(clippy::assertions_on_constants)] +#[test] +fn test_childkey_take_drain() { + // Test cases: parent_hotkey_take + [0_u16, u16::MAX / 5].iter().for_each(|parent_hotkey_take| { + new_test_ext(1).execute_with(|| { + let parent_coldkey = U256::from(1); + let parent_hotkey = U256::from(3); + let child_coldkey = U256::from(2); + let child_hotkey = U256::from(4); + let miner_coldkey = U256::from(5); + let miner_hotkey = U256::from(6); + let nominator = U256::from(7); + let netuid = NetUid::from(1); + let subnet_tempo = 10; + let stake = 100_000_000_000_u64; + let proportion: u64 = u64::MAX / 2; + + // Add network, register hotkeys, and setup network parameters + add_network(netuid, subnet_tempo, 0); + SubtensorModule::set_ck_burn(0); + mock::setup_reserves(netuid, (stake * 10_000).into(), (stake * 10_000).into()); + register_ok_neuron(netuid, child_hotkey, child_coldkey, 0); + register_ok_neuron(netuid, parent_hotkey, parent_coldkey, 1); + register_ok_neuron(netuid, miner_hotkey, miner_coldkey, 1); + add_balance_to_coldkey_account( + &parent_coldkey, + TaoBalance::from(stake) + ExistentialDeposit::get(), + ); + add_balance_to_coldkey_account( + &nominator, + TaoBalance::from(stake) + ExistentialDeposit::get(), + ); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_max_allowed_validators(netuid, 2); + step_block(subnet_tempo); + SubnetOwnerCut::::set(0); + + // Set children + mock_set_children_no_epochs(netuid, &parent_hotkey, &[(proportion, child_hotkey)]); + + // Set 20% childkey take + let max_take: u16 = 0xFFFF / 5; + SubtensorModule::set_max_childkey_take(PerU16::from_parts(max_take)); + assert_ok!(SubtensorModule::set_childkey_take( + RuntimeOrigin::signed(child_coldkey), + child_hotkey, + netuid, + PerU16::from_parts(max_take) + )); + + // Set hotkey take for parent + SubtensorModule::set_max_delegate_take(PerU16::from_parts(*parent_hotkey_take)); + Delegates::::insert(parent_hotkey, PerU16::from_parts(*parent_hotkey_take)); + + // Set 0% for childkey-as-a-delegate take + Delegates::::insert(child_hotkey, PerU16::zero()); + + // Setup stakes: + // Stake from parent + // Stake from nominator to childkey + // Parent gives 50% of stake to childkey + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(parent_coldkey), + parent_hotkey, + netuid, + stake.into() + )); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(nominator), + child_hotkey, + netuid, + stake.into() + )); + + // Setup YUMA so that it creates emissions + Weights::::insert(NetUidStorageIndex::from(netuid), 0, vec![(2, 0xFFFF)]); + Weights::::insert(NetUidStorageIndex::from(netuid), 1, vec![(2, 0xFFFF)]); + BlockAtRegistration::::set(netuid, 0, 1); + BlockAtRegistration::::set(netuid, 1, 1); + BlockAtRegistration::::set(netuid, 2, 1); + LastUpdate::::set(NetUidStorageIndex::from(netuid), vec![2, 2, 2]); + Kappa::::set(netuid, u16::MAX / 5); + ActivityCutoff::::set(netuid, u16::MAX); // makes all stake active + ValidatorPermit::::insert(netuid, vec![true, true, false]); + + // Run run_coinbase to hit subnet epoch + let child_stake_before = SubtensorModule::get_total_stake_for_coldkey(&child_coldkey); + let parent_stake_before = SubtensorModule::get_total_stake_for_coldkey(&parent_coldkey); + let nominator_stake_before = SubtensorModule::get_total_stake_for_coldkey(&nominator); + + step_block(subnet_tempo); + + // Verify how emission is split between keys + // - Child stake remains 0 + // - Childkey take is 20% of its total emission that rewards both inherited from + // parent stake and nominated stake, which all goes to nominators. Because child + // validator emission is 50% of total emission, 20% of it is 10% of total emission + // and it all goes to nominator. If childkey take was 0%, then only 5% would go to + // the nominator, so the final solit is: + // - Parent stake increases by 45% of total emission + // - Nominator stake increases by 55% of total emission + let child_emission = + SubtensorModule::get_total_stake_for_coldkey(&child_coldkey) - child_stake_before; + let parent_emission = + SubtensorModule::get_total_stake_for_coldkey(&parent_coldkey) - parent_stake_before; + let nominator_emission = + SubtensorModule::get_total_stake_for_coldkey(&nominator) - nominator_stake_before; + let total_emission = child_emission + parent_emission + nominator_emission; + + assert_abs_diff_eq!(child_emission, TaoBalance::ZERO, epsilon = 10.into()); + assert_abs_diff_eq!( + parent_emission, + total_emission * 9.into() / 20.into(), + epsilon = 10.into() + ); + assert_abs_diff_eq!( + nominator_emission, + total_emission * 11.into() / 20.into(), + epsilon = 10.into() + ); + }); + }); +} + +#[test] +fn test_do_set_childkey_take_success() { + new_test_ext(1).execute_with(|| { + // Setup + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = NetUid::from(1); + let take = 5000; + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Set childkey take + assert_ok!(SubtensorModule::do_set_childkey_take( + coldkey, + hotkey, + netuid, + PerU16::from_parts(take) + )); + + // Verify the take was set correctly + assert_eq!(SubtensorModule::get_childkey_take(&hotkey, netuid), take); + let tx_type: u16 = TransactionType::SetChildkeyTake.into(); + assert_eq!( + TransactionKeyLastBlock::::get((hotkey, netuid, tx_type,)), + System::block_number() + ); + }); +} + +#[test] +fn test_do_set_childkey_take_non_associated_coldkey() { + new_test_ext(1).execute_with(|| { + // Setup + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let hotkey2 = U256::from(3); + let netuid = NetUid::from(1); + let take = 5000; + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Set childkey take + assert_noop!( + SubtensorModule::do_set_childkey_take( + coldkey, + hotkey2, + netuid, + PerU16::from_parts(take) + ), + Error::::NonAssociatedColdKey + ); + }); +} + +#[test] +fn test_do_set_childkey_take_invalid_take_value() { + new_test_ext(1).execute_with(|| { + // Setup + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = NetUid::from(1); + let take = SubtensorModule::get_max_childkey_take() + 1; + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Set childkey take + assert_noop!( + SubtensorModule::do_set_childkey_take( + coldkey, + hotkey, + netuid, + PerU16::from_parts(take) + ), + Error::::InvalidChildkeyTake + ); + }); +} + +#[test] +fn test_do_set_childkey_take_rate_limit_exceeded() { + new_test_ext(1).execute_with(|| { + // Setup + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = NetUid::from(1); + let initial_take = 3000; + let higher_take = 5000; + let lower_take = 1000; + + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Set initial childkey take + assert_ok!(SubtensorModule::do_set_childkey_take( + coldkey, + hotkey, + netuid, + PerU16::from_parts(initial_take) + )); + + // Try to increase the take value, should hit rate limit + assert_noop!( + SubtensorModule::do_set_childkey_take( + coldkey, + hotkey, + netuid, + PerU16::from_parts(higher_take) + ), + Error::::TxChildkeyTakeRateLimitExceeded + ); + + // lower take value should be ok + assert_ok!(SubtensorModule::do_set_childkey_take( + coldkey, + hotkey, + netuid, + PerU16::from_parts(lower_take) + )); + }); +} diff --git a/pallets/subtensor/src/tests/children/helpers.rs b/pallets/subtensor/src/tests/children/helpers.rs new file mode 100644 index 0000000000..10e0f066d7 --- /dev/null +++ b/pallets/subtensor/src/tests/children/helpers.rs @@ -0,0 +1,13 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::unwrap_used, + clippy::indexing_slicing +)] +//! Shared numeric assert helper for children tests. + +pub(super) fn close(value: u64, target: u64, eps: u64, msg: &str) { + assert!( + (value as i64 - target as i64).abs() <= eps as i64, + "{msg}: value = {value}, target = {target}, eps = {eps}" + ) +} diff --git a/pallets/subtensor/src/tests/children/inherited_stake.rs b/pallets/subtensor/src/tests/children/inherited_stake.rs new file mode 100644 index 0000000000..3278e3f84e --- /dev/null +++ b/pallets/subtensor/src/tests/children/inherited_stake.rs @@ -0,0 +1,821 @@ +#![allow(clippy::indexing_slicing)] +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] +use super::super::mock::*; + +use crate::*; +use sp_core::U256; + +use super::helpers::close; + +// 11: Test getting stake for a hotkey on a subnet +// This test verifies the correct calculation of stake for a parent and child neuron: +// - Sets up a network with a parent and child neuron +// - Stakes tokens to both parent and child from different coldkeys +// - Establishes a parent-child relationship with 100% stake allocation +// - Checks that the parent's stake is correctly transferred to the child +// - Ensures the total stake is preserved in the system +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::inherited_stake::test_get_stake_for_hotkey_on_subnet --exact --show-output --nocapture +#[test] +fn test_get_stake_for_hotkey_on_subnet() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let parent = U256::from(1); + let child = U256::from(2); + let coldkey1 = U256::from(3); + let coldkey2 = U256::from(4); + add_network(netuid, 1, 0); + register_ok_neuron(netuid, parent, coldkey1, 0); + register_ok_neuron(netuid, child, coldkey2, 0); + // Set parent-child relationship with 100% stake allocation + mock_set_children(&coldkey1, &parent, netuid, &[(u64::MAX, child)]); + // Stake 1000 to parent from coldkey1 + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &parent, + &coldkey1, + netuid, + 1000.into(), + ); + // Stake 1000 to parent from coldkey2 + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &parent, + &coldkey2, + netuid, + 1000.into(), + ); + // Stake 1000 to child from coldkey1 + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &child, + &coldkey1, + netuid, + 1000.into(), + ); + // Stake 1000 to child from coldkey2 + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &child, + &coldkey2, + netuid, + 1000.into(), + ); + let parent_stake = SubtensorModule::get_inherited_for_hotkey_on_subnet(&parent, netuid); + let child_stake = SubtensorModule::get_inherited_for_hotkey_on_subnet(&child, netuid); + // The parent should have 0 stake as it's all allocated to the child + assert_eq!(parent_stake, 0.into()); + // The child should have its original stake (2000) plus the parent's stake (2000) + assert_eq!(child_stake, 4000.into()); + + // Ensure total stake is preserved + assert_eq!(parent_stake + child_stake, 4000.into()); + }); +} + +// 39: Test children stake values +// This test verifies the correct distribution of stake among parent and child neurons: +// - Sets up a network with a parent neuron and multiple child neurons +// - Assigns stake to the parent neuron +// - Sets child neurons with specific proportions +// - Verifies that the stake is correctly distributed among parent and child neurons +// - Checks that the total stake remains constant across all neurons +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::inherited_stake::test_children_stake_values --exact --show-output --nocapture +#[test] +fn test_children_stake_values() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child1 = U256::from(3); + let child2 = U256::from(4); + let child3 = U256::from(5); + let proportion1: u64 = u64::MAX / 4; + let proportion2: u64 = u64::MAX / 4; + let proportion3: u64 = u64::MAX / 4; + + // Add network and register hotkey + SubtensorModule::set_max_registrations_per_block(netuid, 4); + SubtensorModule::set_target_registrations_per_interval(netuid, 4); + register_ok_neuron(netuid, hotkey, coldkey, 0); + register_ok_neuron(netuid, child1, coldkey, 0); + register_ok_neuron(netuid, child2, coldkey, 0); + register_ok_neuron(netuid, child3, coldkey, 0); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + 100_000_000_000_000_u64.into(), + ); + + // Set multiple children with proportions. + mock_set_children_no_epochs( + netuid, + &hotkey, + &[ + (proportion1, child1), + (proportion2, child2), + (proportion3, child3), + ], + ); + + assert_eq!( + SubtensorModule::get_inherited_for_hotkey_on_subnet(&hotkey, netuid), + 25_000_000_069_849_u64.into() + ); + assert_eq!( + SubtensorModule::get_inherited_for_hotkey_on_subnet(&child1, netuid), + 24_999_999_976_716_u64.into() + ); + assert_eq!( + SubtensorModule::get_inherited_for_hotkey_on_subnet(&child2, netuid), + 24_999_999_976_716_u64.into() + ); + assert_eq!( + SubtensorModule::get_inherited_for_hotkey_on_subnet(&child3, netuid), + 24_999_999_976_716_u64.into() + ); + assert_eq!( + SubtensorModule::get_inherited_for_hotkey_on_subnet(&child3, netuid) + + SubtensorModule::get_inherited_for_hotkey_on_subnet(&child2, netuid) + + SubtensorModule::get_inherited_for_hotkey_on_subnet(&child1, netuid) + + SubtensorModule::get_inherited_for_hotkey_on_subnet(&hotkey, netuid), + 99999999999997_u64.into() + ); + }); +} + +// 40: Test getting parents chain +// This test verifies the correct implementation of parent-child relationships and the get_parents function: +// - Sets up a network with multiple neurons in a chain of parent-child relationships +// - Verifies that each neuron has the correct parent +// - Tests the root neuron has no parents +// - Tests a neuron with multiple parents +// - Verifies correct behavior when adding a new parent to an existing child +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::inherited_stake::test_get_parents_chain --exact --show-output --nocapture +#[test] +fn test_get_parents_chain() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let coldkey = U256::from(1); + let num_keys: usize = 5; + let proportion = u64::MAX / 2; // 50% stake allocation + + log::info!( + "Test setup: netuid={netuid}, coldkey={coldkey}, num_keys={num_keys}, proportion={proportion}" + ); + + // Create a vector of hotkeys + let hotkeys: Vec = (0..num_keys).map(|i| U256::from(i as u64 + 2)).collect(); + log::info!("Created hotkeys: {hotkeys:?}"); + + // Add network + add_network(netuid, 13, 0); + SubtensorModule::set_max_registrations_per_block(netuid, 1000); + SubtensorModule::set_target_registrations_per_interval(netuid, 1000); + log::info!("Network added and parameters set: netuid={netuid}"); + + // Register all neurons + for hotkey in &hotkeys { + register_ok_neuron(netuid, *hotkey, coldkey, 0); + log::info!( + "Registered neuron: hotkey={hotkey}, coldkey={coldkey}, netuid={netuid}" + ); + } + + // Set up parent-child relationships + for i in 0..num_keys - 1 { + mock_schedule_children( + &coldkey, + &hotkeys[i], + netuid, + &[(proportion, hotkeys[i + 1])], + ); + log::info!( + "Set parent-child relationship: parent={}, child={}, proportion={}", + hotkeys[i], + hotkeys[i + 1], + proportion + ); + } + // Wait for children to be set + wait_and_set_pending_children(netuid); + + // Test get_parents for each hotkey + for i in 1..num_keys { + let parents = SubtensorModule::get_parents(&hotkeys[i], netuid); + log::info!( + "Testing get_parents for hotkey {}: {:?}", + hotkeys[i], + parents + ); + assert_eq!( + parents.len(), + 1, + "Hotkey {i} should have exactly one parent" + ); + assert_eq!( + parents[0], + (proportion, hotkeys[i - 1]), + "Incorrect parent for hotkey {i}" + ); + } + + // Test get_parents for the root (should be empty) + let root_parents = SubtensorModule::get_parents(&hotkeys[0], netuid); + log::info!( + "Testing get_parents for root hotkey {}: {:?}", + hotkeys[0], + root_parents + ); + assert!( + root_parents.is_empty(), + "Root hotkey should have no parents" + ); + + // Test multiple parents + let last_hotkey = hotkeys[num_keys - 1]; + let new_parent = U256::from(num_keys as u64 + 2); + // Set reg diff back down (adjusted from last block steps) + SubtensorModule::set_difficulty(netuid, 1); + register_ok_neuron(netuid, new_parent, coldkey, 99 * 2); + log::info!( + "Registered new parent neuron: new_parent={new_parent}, coldkey={coldkey}, netuid={netuid}" + ); + + mock_set_children( + &coldkey, + &new_parent, + netuid, + &[(proportion / 2, last_hotkey)], + ); + + log::info!( + "Set additional parent-child relationship: parent={}, child={}, proportion={}", + new_parent, + last_hotkey, + proportion / 2 + ); + + let last_hotkey_parents = SubtensorModule::get_parents(&last_hotkey, netuid); + log::info!( + "Testing get_parents for last hotkey {last_hotkey} with multiple parents: {last_hotkey_parents:?}" + ); + assert_eq!( + last_hotkey_parents.len(), + 2, + "Last hotkey should have two parents" + ); + assert!( + last_hotkey_parents.contains(&(proportion, hotkeys[num_keys - 2])), + "Last hotkey should still have its original parent" + ); + assert!( + last_hotkey_parents.contains(&(proportion / 2, new_parent)), + "Last hotkey should have the new parent" + ); + }); +} + +// 47: Test basic stake retrieval for a single hotkey on a subnet +/// This test verifies the basic functionality of retrieving stake for a single hotkey on a subnet: +/// - Sets up a network with one neuron +/// - Increases stake for the neuron +/// - Checks if the retrieved stake matches the increased amount +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::inherited_stake::test_get_stake_for_hotkey_on_subnet_basic --exact --show-output --nocapture +#[test] +fn test_get_stake_for_hotkey_on_subnet_basic() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey = U256::from(1); + let coldkey = U256::from(2); + + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + 1000.into(), + ); + assert_eq!( + SubtensorModule::get_inherited_for_hotkey_on_subnet(&hotkey, netuid), + 1000.into() + ); + }); +} + +// 48: Test stake retrieval for a hotkey with multiple coldkeys on a subnet +/// This test verifies the functionality of retrieving stake for a hotkey with multiple coldkeys on a subnet: +/// - Sets up a network with one neuron and two coldkeys +/// - Increases stake from both coldkeys +/// - Checks if the retrieved stake matches the total increased amount +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::inherited_stake::test_get_stake_for_hotkey_on_subnet_multiple_coldkeys --exact --show-output --nocapture +#[test] +fn test_get_stake_for_hotkey_on_subnet_multiple_coldkeys() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey = U256::from(1); + let coldkey1 = U256::from(2); + let coldkey2 = U256::from(3); + + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey, coldkey1, 0); + + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey1, + netuid, + 1000.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey2, + netuid, + 2000.into(), + ); + + assert_eq!( + SubtensorModule::get_inherited_for_hotkey_on_subnet(&hotkey, netuid), + 3000.into() + ); + }); +} + +// 49: Test stake retrieval for a single parent-child relationship on a subnet +/// This test verifies the functionality of retrieving stake for a single parent-child relationship on a subnet: +/// - Sets up a network with a parent and child neuron +/// - Increases stake for the parent +/// - Sets the child as the parent's only child with 100% stake allocation +/// - Checks if the retrieved stake for both parent and child is correct +/// +/// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::inherited_stake::test_get_stake_for_hotkey_on_subnet_single_parent_child --exact --show-output --nocapture +#[test] +fn test_get_stake_for_hotkey_on_subnet_single_parent_child() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let parent = U256::from(1); + let child = U256::from(2); + let coldkey = U256::from(3); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, parent, coldkey, 0); + register_ok_neuron(netuid, child, coldkey, 0); + + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &parent, + &coldkey, + netuid, + 1_000_000_000.into(), + ); + + mock_set_children_no_epochs(netuid, &parent, &[(u64::MAX, child)]); + + assert_eq!( + SubtensorModule::get_inherited_for_hotkey_on_subnet(&parent, netuid), + 0.into() + ); + assert_eq!( + SubtensorModule::get_inherited_for_hotkey_on_subnet(&child, netuid), + 1_000_000_000.into() + ); + }); +} + +// 50: Test stake retrieval for multiple parents and a single child on a subnet +/// This test verifies the functionality of retrieving stake for multiple parents and a single child on a subnet: +/// - Sets up a network with two parents and one child neuron +/// - Increases stake for both parents +/// - Sets the child as a 50% stake recipient for both parents +/// - Checks if the retrieved stake for parents and child is correct +/// +/// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::inherited_stake::test_get_stake_for_hotkey_on_subnet_multiple_parents_single_child --exact --show-output --nocapture +#[test] +fn test_get_stake_for_hotkey_on_subnet_multiple_parents_single_child() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + let parent1 = U256::from(1); + let parent2 = U256::from(2); + let child = U256::from(3); + let coldkey = U256::from(4); + + register_ok_neuron(netuid, parent1, coldkey, 0); + register_ok_neuron(netuid, parent2, coldkey, 0); + register_ok_neuron(netuid, child, coldkey, 0); + + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &parent1, + &coldkey, + netuid, + 1000.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &parent2, + &coldkey, + netuid, + 2000.into(), + ); + + mock_set_children_no_epochs(netuid, &parent1, &[(u64::MAX / 2, child)]); + mock_set_children_no_epochs(netuid, &parent2, &[(u64::MAX / 2, child)]); + + close( + SubtensorModule::get_inherited_for_hotkey_on_subnet(&parent1, netuid).into(), + 500, + 10, + "Incorrect inherited stake for parent1", + ); + close( + SubtensorModule::get_inherited_for_hotkey_on_subnet(&parent2, netuid).into(), + 1000, + 10, + "Incorrect inherited stake for parent2", + ); + close( + SubtensorModule::get_inherited_for_hotkey_on_subnet(&child, netuid).into(), + 1499, + 10, + "Incorrect inherited stake for child", + ); + }); +} + +// 51: Test stake retrieval for a single parent with multiple children on a subnet +/// This test verifies the functionality of retrieving stake for a single parent with multiple children on a subnet: +/// - Sets up a network with one parent and two child neurons +/// - Increases stake for the parent +/// - Sets both children as 1/3 stake recipients of the parent +/// - Checks if the retrieved stake for parent and children is correct and preserves total stake +/// +/// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::inherited_stake::test_get_stake_for_hotkey_on_subnet_single_parent_multiple_children --exact --show-output --nocapture +#[test] +fn test_get_stake_for_hotkey_on_subnet_single_parent_multiple_children() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + let parent = U256::from(1); + let child1 = U256::from(2); + let child2 = U256::from(3); + let coldkey = U256::from(4); + + register_ok_neuron(netuid, parent, coldkey, 0); + register_ok_neuron(netuid, child1, coldkey, 0); + register_ok_neuron(netuid, child2, coldkey, 0); + + let total_stake = 3000.into(); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &parent, + &coldkey, + netuid, + total_stake, + ); + + mock_set_children_no_epochs( + netuid, + &parent, + &[(u64::MAX / 3, child1), (u64::MAX / 3, child2)], + ); + + let parent_stake = SubtensorModule::get_inherited_for_hotkey_on_subnet(&parent, netuid); + let child1_stake = SubtensorModule::get_inherited_for_hotkey_on_subnet(&child1, netuid); + let child2_stake = SubtensorModule::get_inherited_for_hotkey_on_subnet(&child2, netuid); + + // Check that the total stake is preserved + close( + (parent_stake + child1_stake + child2_stake).into(), + total_stake.into(), + 10, + "Total stake not preserved", + ); + + // Check that the parent stake is slightly higher due to rounding + close(parent_stake.into(), 1000, 10, "Parent stake incorrect"); + + // Check that each child gets an equal share of the remaining stake + close(child1_stake.into(), 1000, 10, "Child1 stake incorrect"); + close(child2_stake.into(), 1000, 10, "Child2 stake incorrect"); + + // Log the actual stake values + log::info!("Parent stake: {parent_stake}"); + log::info!("Child1 stake: {child1_stake}"); + log::info!("Child2 stake: {child2_stake}"); + }); +} + +// 52: Test stake retrieval for edge cases on a subnet +/// This test verifies the functionality of retrieving stake for edge cases on a subnet: +/// - Sets up a network with one parent and two child neurons +/// - Increases stake to the network maximum +/// - Sets children with 0% and 100% stake allocation +/// - Checks if the retrieved stake for parent and children is correct and preserves total stake +/// +/// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::inherited_stake::test_get_stake_for_hotkey_on_subnet_edge_cases --exact --show-output --nocapture +#[test] +fn test_get_stake_for_hotkey_on_subnet_edge_cases() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + let parent = U256::from(1); + let child1 = U256::from(2); + let child2 = U256::from(3); + let coldkey = U256::from(4); + + register_ok_neuron(netuid, parent, coldkey, 0); + register_ok_neuron(netuid, child1, coldkey, 0); + register_ok_neuron(netuid, child2, coldkey, 0); + + // Set above old value of network max stake + let network_max_stake = 600_000_000_000_000_u64.into(); + + // Increase stake to the network max + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &parent, + &coldkey, + netuid, + network_max_stake, + ); + + // Test with 0% and 100% stake allocation + mock_set_children_no_epochs(netuid, &parent, &[(0, child1), (u64::MAX, child2)]); + + let parent_stake = SubtensorModule::get_inherited_for_hotkey_on_subnet(&parent, netuid); + let child1_stake = SubtensorModule::get_inherited_for_hotkey_on_subnet(&child1, netuid); + let child2_stake = SubtensorModule::get_inherited_for_hotkey_on_subnet(&child2, netuid); + + log::info!("Parent stake: {parent_stake}"); + log::info!("Child1 stake: {child1_stake}"); + log::info!("Child2 stake: {child2_stake}"); + + assert_eq!(parent_stake, 0.into(), "Parent should have 0 stake"); + assert_eq!(child1_stake, 0.into(), "Child1 should have 0 stake"); + assert_eq!( + child2_stake, network_max_stake, + "Child2 should have all the stake" + ); + + // Check that the total stake is preserved and equal to the network max stake + close( + (parent_stake + child1_stake + child2_stake).into(), + network_max_stake.into(), + 10, + "Total stake should equal network max stake", + ); + }); +} + +// 53: Test stake distribution in a complex hierarchy of parent-child relationships +// This test verifies the correct distribution of stake in a multi-level parent-child hierarchy: +// - Sets up a network with four neurons: parent, child1, child2, and grandchild +// - Establishes parent-child relationships between parent and its children, and child1 and grandchild +// - Adds initial stake to the parent +// - Checks stake distribution after setting up the first level of relationships +// - Checks stake distribution after setting up the second level of relationships +// - Verifies correct stake calculations, parent-child relationships, and preservation of total stake +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::inherited_stake::test_get_stake_for_hotkey_on_subnet_complex_hierarchy --exact --show-output --nocapture +#[test] +fn test_get_stake_for_hotkey_on_subnet_complex_hierarchy() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + let parent = U256::from(1); + let child1 = U256::from(2); + let child2 = U256::from(3); + let grandchild = U256::from(4); + let coldkey_parent = U256::from(5); + let coldkey_child1 = U256::from(6); + let coldkey_child2 = U256::from(7); + let coldkey_grandchild = U256::from(8); + + SubtensorModule::set_max_registrations_per_block(netuid, 1000); + SubtensorModule::set_target_registrations_per_interval(netuid, 1000); + register_ok_neuron(netuid, parent, coldkey_parent, 0); + register_ok_neuron(netuid, child1, coldkey_child1, 0); + register_ok_neuron(netuid, child2, coldkey_child2, 0); + register_ok_neuron(netuid, grandchild, coldkey_grandchild, 0); + + let total_stake = 1000.into(); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &parent, + &coldkey_parent, + netuid, + total_stake, + ); + + log::info!("Initial stakes:"); + log::info!( + "Parent stake: {}", + SubtensorModule::get_inherited_for_hotkey_on_subnet(&parent, netuid) + ); + log::info!( + "Child1 stake: {}", + SubtensorModule::get_inherited_for_hotkey_on_subnet(&child1, netuid) + ); + log::info!( + "Child2 stake: {}", + SubtensorModule::get_inherited_for_hotkey_on_subnet(&child2, netuid) + ); + log::info!( + "Grandchild stake: {}", + SubtensorModule::get_inherited_for_hotkey_on_subnet(&grandchild, netuid) + ); + + // Step 1: Set children for parent + mock_set_children_no_epochs( + netuid, + &parent, + &[(u64::MAX / 2, child1), (u64::MAX / 2, child2)], + ); + + log::info!("After setting parent's children:"); + log::info!( + "Parent's children: {:?}", + SubtensorModule::get_children(&parent, netuid) + ); + log::info!( + "Child1's parents: {:?}", + SubtensorModule::get_parents(&child1, netuid) + ); + log::info!( + "Child2's parents: {:?}", + SubtensorModule::get_parents(&child2, netuid) + ); + + let parent_stake_1 = SubtensorModule::get_inherited_for_hotkey_on_subnet(&parent, netuid); + let child1_stake_1 = SubtensorModule::get_inherited_for_hotkey_on_subnet(&child1, netuid); + let child2_stake_1 = SubtensorModule::get_inherited_for_hotkey_on_subnet(&child2, netuid); + + log::info!("Parent stake: {parent_stake_1}"); + log::info!("Child1 stake: {child1_stake_1}"); + log::info!("Child2 stake: {child2_stake_1}"); + + assert_eq!( + parent_stake_1, + 0.into(), + "Parent should have 0 stake after distributing all stake to children" + ); + close( + child1_stake_1.into(), + 499, + 10, + "Child1 should have 499 stake", + ); + close( + child2_stake_1.into(), + 499, + 10, + "Child2 should have 499 stake", + ); + + // Step 2: Set children for child1 + mock_set_children_no_epochs(netuid, &child1, &[(u64::MAX, grandchild)]); + + log::info!("After setting child1's children:"); + log::info!( + "Child1's children: {:?}", + SubtensorModule::get_children(&child1, netuid) + ); + log::info!( + "Grandchild's parents: {:?}", + SubtensorModule::get_parents(&grandchild, netuid) + ); + + let parent_stake_2 = SubtensorModule::get_inherited_for_hotkey_on_subnet(&parent, netuid); + let child1_stake_2 = SubtensorModule::get_inherited_for_hotkey_on_subnet(&child1, netuid); + let child2_stake_2 = SubtensorModule::get_inherited_for_hotkey_on_subnet(&child2, netuid); + let grandchild_stake = + SubtensorModule::get_inherited_for_hotkey_on_subnet(&grandchild, netuid); + + log::info!("Parent stake: {parent_stake_2}"); + log::info!("Child1 stake: {child1_stake_2}"); + log::info!("Child2 stake: {child2_stake_2}"); + log::info!("Grandchild stake: {grandchild_stake}"); + + close(parent_stake_2.into(), 0, 10, "Parent stake should remain 2"); + close( + child1_stake_2.into(), + 499, + 10, + "Child1 should still have 499 stake", + ); + close( + child2_stake_2.into(), + 499, + 10, + "Child2 should still have 499 stake", + ); + close( + grandchild_stake.into(), + 0, + 10, + "Grandchild should have 0 stake, as child1 doesn't have any owned stake", + ); + + // Check that the total stake is preserved + close( + (parent_stake_2 + child1_stake_2 + child2_stake_2 + grandchild_stake).into(), + total_stake.into(), + 10, + "Total stake should equal the initial stake", + ); + + // Additional checks + log::info!("Final parent-child relationships:"); + log::info!( + "Parent's children: {:?}", + SubtensorModule::get_children(&parent, netuid) + ); + log::info!( + "Child1's parents: {:?}", + SubtensorModule::get_parents(&child1, netuid) + ); + log::info!( + "Child2's parents: {:?}", + SubtensorModule::get_parents(&child2, netuid) + ); + log::info!( + "Child1's children: {:?}", + SubtensorModule::get_children(&child1, netuid) + ); + log::info!( + "Grandchild's parents: {:?}", + SubtensorModule::get_parents(&grandchild, netuid) + ); + + // Check if the parent-child relationships are correct + assert_eq!( + SubtensorModule::get_children(&parent, netuid), + vec![(u64::MAX / 2, child1), (u64::MAX / 2, child2)], + "Parent should have both children" + ); + assert_eq!( + SubtensorModule::get_parents(&child1, netuid), + vec![(u64::MAX / 2, parent)], + "Child1 should have parent as its parent" + ); + assert_eq!( + SubtensorModule::get_parents(&child2, netuid), + vec![(u64::MAX / 2, parent)], + "Child2 should have parent as its parent" + ); + assert_eq!( + SubtensorModule::get_children(&child1, netuid), + vec![(u64::MAX, grandchild)], + "Child1 should have grandchild as its child" + ); + assert_eq!( + SubtensorModule::get_parents(&grandchild, netuid), + vec![(u64::MAX, child1)], + "Grandchild should have child1 as its parent" + ); + }); +} + +// 54: Test stake distribution across multiple networks +// This test verifies the correct distribution of stake for a single neuron across multiple networks: +// - Sets up two networks with a single neuron registered on both +// - Adds initial stake to the neuron +// - Checks that the stake is correctly reflected on both networks +// - Verifies that changes in stake are consistently applied across all networks +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::inherited_stake::test_get_stake_for_hotkey_on_subnet_multiple_networks --exact --show-output --nocapture +#[test] +fn test_get_stake_for_hotkey_on_subnet_multiple_networks() { + new_test_ext(1).execute_with(|| { + let netuid1 = NetUid::from(1); + let netuid2 = NetUid::from(2); + let hotkey = U256::from(1); + let coldkey = U256::from(2); + + add_network(netuid1, 1, 0); + add_network(netuid2, 1, 0); + register_ok_neuron(netuid1, hotkey, coldkey, 0); + register_ok_neuron(netuid2, hotkey, coldkey, 0); + + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid1, + 1000.into(), + ); + + close( + SubtensorModule::get_inherited_for_hotkey_on_subnet(&hotkey, netuid1).into(), + 1000, + 10, + "Stake on network 1 incorrect", + ); + close( + SubtensorModule::get_inherited_for_hotkey_on_subnet(&hotkey, netuid2).into(), + 0, + 10, + "Stake on network 2 incorrect", + ); + }); +} diff --git a/pallets/subtensor/src/tests/children/mod.rs b/pallets/subtensor/src/tests/children/mod.rs new file mode 100644 index 0000000000..76967966cc --- /dev/null +++ b/pallets/subtensor/src/tests/children/mod.rs @@ -0,0 +1,35 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::unwrap_used, + clippy::indexing_slicing +)] +//! Integration tests for parent/child hotkeys ([`crate::staking::set_children`]). +//! +//! Layout mirrors `staking/set_children/` plus inherited-stake / emission concepts +//! exercised through childkey edges. +//! +//! ## Search anchors +//! +//! | Module | Owns | +//! |--------|------| +//! | [`helpers`] | `close` numeric assert helper | +//! | [`schedule_singular`] | singular `do_schedule_children` / revoke | +//! | [`schedule_multiple`] | multi-child schedule, revoke, storage clear | +//! | [`pending_children`] | cooldown, pending apply, min-stake / rate-limit gates | +//! | [`childkey_take`] | `do_set_childkey_take` / take drain | +//! | [`inherited_stake`] | inherited stake via parent/child proportions | +//! | [`child_weights`] | set_weights with parent/child edges | +//! | [`child_emission`] | emission / epoch through parent-child chains | +//! | [`child_dividends`] | dividend distribution with children | +//! | [`root_validators`] | root-validator auto child scheduling | + +mod child_dividends; +mod child_emission; +mod child_weights; +mod childkey_take; +mod helpers; +mod inherited_stake; +mod pending_children; +mod root_validators; +mod schedule_multiple; +mod schedule_singular; diff --git a/pallets/subtensor/src/tests/children/pending_children.rs b/pallets/subtensor/src/tests/children/pending_children.rs new file mode 100644 index 0000000000..105fa55b11 --- /dev/null +++ b/pallets/subtensor/src/tests/children/pending_children.rs @@ -0,0 +1,539 @@ +#![allow(clippy::indexing_slicing)] +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] +use super::super::mock; +use super::super::mock::*; +use frame_support::{assert_err, assert_noop, assert_ok}; + +use crate::{utils::rate_limiting::TransactionType, *}; +use sp_core::U256; + +use super::helpers::close; + +// Test that min stake is enforced for setting children +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::pending_children::test_do_set_child_below_min_stake --exact --show-output --nocapture +#[test] +fn test_do_set_child_below_min_stake() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child = U256::from(3); + let netuid = NetUid::from(1); + let proportion: u64 = 1000; + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + StakeThreshold::::set(1_000_000_000_000); + + // Attempt to set child + assert_err!( + SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + vec![(proportion, child)] + ), + Error::::NotEnoughStakeToSetChildkeys + ); + }); +} + +/// --- test_do_remove_stake_clears_pending_childkeys --- +/// +/// Test Description: Ensures that removing stake clears any pending childkeys. +/// +/// Expected Behavior: +/// - Pending childkeys should be cleared when stake is removed +/// - Cooldown block should be reset to 0 +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::pending_children::test_do_remove_stake_clears_pending_childkeys --exact --show-output --nocapture +#[test] +fn test_do_remove_stake_clears_pending_childkeys() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child = U256::from(3); + let netuid = NetUid::from(1); + let proportion: u64 = 1000; + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + add_balance_to_coldkey_account(&coldkey, 10_000_000_000_000_u64.into()); + SubtokenEnabled::::insert(netuid, true); + + let reserve = 1_000_000_000_000_000_u64; + mock::setup_reserves(netuid, reserve.into(), reserve.into()); + + // Set non-default value for childkey stake threshold + StakeThreshold::::set(1_000_000_000_000); + + assert_ok!(SubtensorModule::do_add_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + (StakeThreshold::::get() * 2).into() + )); + + let alpha = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + + println!( + "StakeThreshold::::get() = {:?}", + StakeThreshold::::get() + ); + println!("alpha = {alpha:?}"); + + // Attempt to set child + assert_ok!(SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + vec![(proportion, child)] + )); + + // Check that pending child exists + let pending_before = PendingChildKeys::::get(netuid, hotkey); + assert!(!pending_before.0.is_empty()); + assert!(pending_before.1 > 0); + + // Remove stake + assert_ok!(SubtensorModule::do_remove_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + alpha, + )); + + // Assert that pending child is removed + let pending_after = PendingChildKeys::::get(netuid, hotkey); + close( + pending_after.0.len() as u64, + 0, + 0, + "Pending children vector should be empty", + ); + close(pending_after.1, 0, 0, "Cooldown block should be zero"); + }); +} + +// Test that pending childkeys do not apply immediately and apply after cooldown period +// +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::pending_children::test_do_set_child_cooldown_period --exact --show-output --nocapture +#[cfg(test)] +#[test] +fn test_do_set_child_cooldown_period() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let parent = U256::from(2); + let child = U256::from(3); + let netuid = NetUid::from(1); + let proportion: u64 = 1000; + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, parent, coldkey, 0); + + // Set minimum stake for setting children + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &parent, + &coldkey, + netuid, + StakeThreshold::::get().into(), + ); + + // Schedule parent-child relationship + assert_ok!(SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + parent, + netuid, + vec![(proportion, child)], + )); + + // Ensure the childkeys are not yet applied + let children_before = SubtensorModule::get_children(&parent, netuid); + close( + children_before.len() as u64, + 0, + 0, + "Children vector should be empty before cooldown", + ); + + wait_and_set_pending_children(netuid); + SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( + &parent, + &coldkey, + netuid, + StakeThreshold::::get().into(), + ); + + // Verify child assignment + let children_after = SubtensorModule::get_children(&parent, netuid); + close( + children_after.len() as u64, + 1, + 0, + "Children vector should have one entry after cooldown", + ); + close( + children_after[0].0, + proportion, + 0, + "Child proportion should match", + ); + close( + children_after[0].1.try_into().unwrap(), + child.try_into().unwrap(), + 0, + "Child key should match", + ); + }); +} + +// Test that pending childkeys get set during the epoch after the cooldown period. +// +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::pending_children::test_do_set_pending_children_runs_in_epoch --exact --show-output --nocapture +#[cfg(test)] +#[test] +fn test_do_set_pending_children_runs_in_epoch() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let parent = U256::from(2); + let child = U256::from(3); + let netuid = NetUid::from(1); + let proportion: u64 = 1000; + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, parent, coldkey, 0); + + // Set minimum stake for setting children + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &parent, + &coldkey, + netuid, + StakeThreshold::::get().into(), + ); + + // Schedule parent-child relationship + assert_ok!(SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + parent, + netuid, + vec![(proportion, child)], + )); + + // Ensure the childkeys are not yet applied + let children_before = SubtensorModule::get_children(&parent, netuid); + close( + children_before.len() as u64, + 0, + 0, + "Children vector should be empty before cooldown", + ); + + wait_set_pending_children_cooldown(netuid); + + // Verify child assignment + let children_after = SubtensorModule::get_children(&parent, netuid); + close( + children_after.len() as u64, + 1, + 0, + "Children vector should have one entry after cooldown", + ); + close( + children_after[0].0, + proportion, + 0, + "Child proportion should match", + ); + close( + children_after[0].1.try_into().unwrap(), + child.try_into().unwrap(), + 0, + "Child key should match", + ); + }); +} + +// Test that revoking childkeys does not require minimum stake +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::pending_children::test_revoke_child_no_min_stake_check --exact --show-output --nocapture +#[test] +fn test_revoke_child_no_min_stake_check() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let parent = U256::from(2); + let child = U256::from(3); + let netuid = NetUid::from(1); + let proportion: u64 = 1000; + + // Add network and register hotkey + add_network(NetUid::ROOT, 13, 0); + add_network(netuid, 13, 0); + register_ok_neuron(netuid, parent, coldkey, 0); + + let reserve = 1_000_000_000_000_000_u64; + mock::setup_reserves(netuid, reserve.into(), reserve.into()); + mock::setup_reserves(NetUid::ROOT, reserve.into(), reserve.into()); + + // Set minimum stake for setting children + StakeThreshold::::put(1_000_000_000_000); + + let (_, fee) = mock::swap_tao_to_alpha(NetUid::ROOT, StakeThreshold::::get().into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &parent, + &coldkey, + NetUid::ROOT, + (StakeThreshold::::get() + fee).into(), + ); + + // Schedule parent-child relationship + assert_ok!(SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + parent, + netuid, + vec![(proportion, child)], + )); + + // Ensure the childkeys are not yet applied + let children_before = SubtensorModule::get_children(&parent, netuid); + assert_eq!(children_before, vec![]); + + wait_and_set_pending_children(netuid); + SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( + &parent, + &coldkey, + NetUid::ROOT, + (StakeThreshold::::get() + fee).into(), + ); + + // Ensure the childkeys are applied + let children_after = SubtensorModule::get_children(&parent, netuid); + assert_eq!(children_after, vec![(proportion, child)]); + + // Bypass tx rate limit + TransactionType::SetChildren.set_last_block_on_subnet::(&parent, netuid, 0); + + // Schedule parent-child relationship revokation + assert_ok!(SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + parent, + netuid, + vec![], + )); + + wait_and_set_pending_children(netuid); + + // Ensure the childkeys are revoked + let children_after = SubtensorModule::get_children(&parent, netuid); + assert_eq!(children_after, vec![]); + }); +} + +// Test that setting childkeys works even if subnet registration is disabled +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::pending_children::test_do_set_child_registration_disabled --exact --show-output --nocapture +#[test] +fn test_do_set_child_registration_disabled() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let parent = U256::from(2); + let child = U256::from(3); + let netuid = NetUid::from(1); + let proportion: u64 = 1000; + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, parent, coldkey, 0); + + let reserve = 1_000_000_000_000_000_u64; + mock::setup_reserves(netuid, reserve.into(), reserve.into()); + + // Set minimum stake for setting children + StakeThreshold::::put(1_000_000_000_000); + let (_, fee) = mock::swap_tao_to_alpha(netuid, StakeThreshold::::get().into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &parent, + &coldkey, + netuid, + (StakeThreshold::::get() + fee).into(), + ); + + // Disable subnet registrations + NetworkRegistrationAllowed::::insert(netuid, false); + + // Schedule parent-child relationship + assert_ok!(SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + parent, + netuid, + vec![(proportion, child)], + )); + + wait_and_set_pending_children(netuid); + SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( + &parent, + &coldkey, + netuid, + (StakeThreshold::::get() + fee).into(), + ); + + // Ensure the childkeys are applied + let children_after = SubtensorModule::get_children(&parent, netuid); + assert_eq!(children_after, vec![(proportion, child)]); + }); +} + +// 60: Test set_children rate limiting - Fail then succeed +// This test ensures that an immediate second `set_children` transaction fails due to rate limiting: +// - Sets up a network and registers a hotkey +// - Performs a `set_children` transaction +// - Attempts a second `set_children` transaction immediately +// - Verifies that the second transaction fails with `TxRateLimitExceeded` +// Then the rate limit period passes and the second transaction succeeds +// - Steps blocks for the rate limit period +// - Attempts the second transaction again and verifies it succeeds +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::pending_children::test_set_children_rate_limit_fail_then_succeed --exact --show-output --nocapture +#[test] +fn test_set_children_rate_limit_fail_then_succeed() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child = U256::from(3); + let child2 = U256::from(4); + let netuid = NetUid::from(1); + let tempo = 13; + + // Add network and register hotkey + add_network(netuid, tempo, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // First set_children transaction + mock_set_children(&coldkey, &hotkey, netuid, &[(100, child)]); + + // Immediate second transaction should fail due to rate limit + assert_noop!( + SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + vec![(100, child2)] + ), + Error::::TxRateLimitExceeded + ); + + // Verify first children assignment remains + let children = SubtensorModule::get_children(&hotkey, netuid); + assert_eq!(children, vec![(100, child)]); + + // Try again after rate limit period has passed + // Check rate limit + let limit = TransactionType::SetChildren.rate_limit_on_subnet::(netuid); + + // Step that many blocks + step_block(limit as u16); + + // Verify rate limit passes + assert!(TransactionType::SetChildren.passes_rate_limit_on_subnet::(&hotkey, netuid)); + + // Try again + mock_set_children(&coldkey, &hotkey, netuid, &[(100, child2)]); + + // Verify children assignment has changed + let children = SubtensorModule::get_children(&hotkey, netuid); + assert_eq!(children, vec![(100, child2)]); + }); +} + +#[test] +fn test_do_set_child_as_sn_owner_not_enough_stake() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let sn_owner_hotkey = U256::from(4); + + let child_coldkey = U256::from(2); + let child_hotkey = U256::from(5); + + let threshold = 10_000; + SubtensorModule::set_stake_threshold(threshold); + + let proportion: u64 = 1000; + + let netuid = add_dynamic_network(&sn_owner_hotkey, &coldkey); + remove_owner_registration_stake(netuid); + register_ok_neuron(netuid, child_hotkey, child_coldkey, 0); + + // Verify stake of sn_owner_hotkey is NOT enough + assert!( + SubtensorModule::get_total_stake_for_hotkey(&sn_owner_hotkey) + < StakeThreshold::::get().into() + ); + + // Verify that we can set child as sn owner, even though sn_owner_hotkey has insufficient stake + assert_ok!(SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + sn_owner_hotkey, + netuid, + vec![(proportion, child_hotkey)] + )); + + // Make new hotkey from owner coldkey + let other_sn_owner_hotkey = U256::from(6); + register_ok_neuron(netuid, other_sn_owner_hotkey, coldkey, 1234); + + // Verify stake of other_sn_owner_hotkey is NOT enough + assert!( + SubtensorModule::get_total_stake_for_hotkey(&other_sn_owner_hotkey) + < StakeThreshold::::get().into() + ); + + // Can't set child as sn owner, because it is not in SubnetOwnerHotkey map + assert_noop!( + SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + other_sn_owner_hotkey, + netuid, + vec![(proportion, child_hotkey)] + ), + Error::::NotEnoughStakeToSetChildkeys + ); + }); +} + +#[test] +fn test_pending_cooldown_as_expected() { + let curr_block = 1; + // TODO: Fix when CHK splitting patched + // let expected_cooldown = prod_or_fast!(7200, 15); + + new_test_ext(curr_block).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child1 = U256::from(3); + let child2 = U256::from(4); + let netuid = NetUid::from(1); + let proportion1: u64 = 1000; + let proportion2: u64 = 2000; + let expected_cooldown = PendingChildKeyCooldown::::get(); + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Set multiple children + mock_schedule_children( + &coldkey, + &hotkey, + netuid, + &[(proportion1, child1), (proportion2, child2)], + ); + + // Verify pending map + let pending_children = PendingChildKeys::::get(netuid, hotkey); + assert_eq!( + pending_children.0, + vec![(proportion1, child1), (proportion2, child2)] + ); + assert_eq!(pending_children.1, curr_block + expected_cooldown); + }); +} diff --git a/pallets/subtensor/src/tests/children/root_validators.rs b/pallets/subtensor/src/tests/children/root_validators.rs new file mode 100644 index 0000000000..d70d660811 --- /dev/null +++ b/pallets/subtensor/src/tests/children/root_validators.rs @@ -0,0 +1,263 @@ +#![allow(clippy::indexing_slicing)] +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] +use super::super::mock::*; +use frame_support::assert_ok; +use subtensor_runtime_common::AlphaBalance; + +use crate::*; +use sp_core::U256; + +// Test that register_network automatically sets root validators as parents of the +// subnet owner, enabling the owner to set weights. Since SubtokenEnabled is false +// for a new subnet (start_call hasn't executed yet), child keys are applied immediately. +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::root_validators::test_register_network_schedules_root_validators --exact --show-output --nocapture +#[test] +fn test_register_network_schedules_root_validators() { + new_test_ext(1).execute_with(|| { + // --- Setup root network and root validators --- + let root_val_coldkey_1 = U256::from(100); + let root_val_hotkey_1 = U256::from(101); + let root_val_coldkey_2 = U256::from(200); + let root_val_hotkey_2 = U256::from(201); + + add_network(NetUid::ROOT, 1, 0); + + // Root validators need to be registered on some subnet before root_register. + // Create a bootstrap subnet for that purpose. + let bootstrap_netuid = NetUid::from(1); + add_network(bootstrap_netuid, 1, 0); + register_ok_neuron(bootstrap_netuid, root_val_hotkey_1, root_val_coldkey_1, 0); + register_ok_neuron(bootstrap_netuid, root_val_hotkey_2, root_val_coldkey_2, 0); + + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(root_val_coldkey_1), + root_val_hotkey_1, + )); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(root_val_coldkey_2), + root_val_hotkey_2, + )); + + // Give root validators significant stake on root and bootstrap subnet + let root_stake = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &root_val_hotkey_1, + &root_val_coldkey_1, + NetUid::ROOT, + root_stake, + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &root_val_hotkey_2, + &root_val_coldkey_2, + NetUid::ROOT, + root_stake, + ); + + // --- Minimize cooldown so pending children activate quickly --- + assert_ok!(SubtensorModule::set_pending_childkey_cooldown( + RuntimeOrigin::root(), + 0, + )); + + // --- Set a high stake threshold --- + let high_threshold = 500_000_000u64; + SubtensorModule::set_stake_threshold(high_threshold); + + // --- Register a new subnet (this should automatically call do_set_root_validators_for_subnet) --- + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let lock_cost = SubtensorModule::get_network_lock_cost(); + add_balance_to_coldkey_account(&subnet_owner_coldkey, lock_cost.into()); + TotalIssuance::::mutate(|total| { + *total = total.saturating_add(lock_cost); + }); + assert_ok!(SubtensorModule::register_network( + RuntimeOrigin::signed(subnet_owner_coldkey), + subnet_owner_hotkey, + )); + + // Determine the netuid that was just created + let netuid: NetUid = (TotalNetworks::::get().saturating_sub(1)).into(); + assert_eq!( + SubnetOwnerHotkey::::get(netuid), + subnet_owner_hotkey, + "Subnet owner hotkey should be set" + ); + + // Root validators need stake on the new subnet for child stake inheritance to work + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &root_val_hotkey_1, + &root_val_coldkey_1, + netuid, + root_stake, + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &root_val_hotkey_2, + &root_val_coldkey_2, + netuid, + root_stake, + ); + + // --- Verify child keys were applied immediately (SubtokenEnabled is false for new subnets) --- + let children_1 = SubtensorModule::get_children(&root_val_hotkey_1, netuid); + assert_eq!( + children_1, + vec![(u64::MAX, subnet_owner_hotkey)], + "Root validator 1 should have subnet owner as child" + ); + let children_2 = SubtensorModule::get_children(&root_val_hotkey_2, netuid); + assert_eq!( + children_2, + vec![(u64::MAX, subnet_owner_hotkey)], + "Root validator 2 should have subnet owner as child" + ); + + // --- Verify subnet owner can now set weights --- + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); + let version_key = SubtensorModule::get_weights_version_key(netuid); + + assert!( + SubtensorModule::check_weights_min_stake(&subnet_owner_hotkey, netuid), + "Subnet owner should have enough inherited stake to set weights" + ); + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(subnet_owner_hotkey), + netuid, + vec![0], + vec![u16::MAX], + version_key + )); + }); +} + +// Test that register_network automatically sets root validators as parents of the +// subnet owner, only if AutoParentDelegationEnabled is enabled (default). +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::root_validators::test_register_network_schedules_root_validators_auto_parent_delegation_flag --exact --show-output --nocapture +#[test] +fn test_register_network_schedules_root_validators_auto_parent_delegation_flag() { + new_test_ext(1).execute_with(|| { + // --- Setup root network and root validators --- + let root_val_coldkey_1 = U256::from(100); + let root_val_hotkey_1 = U256::from(101); + let root_val_coldkey_2 = U256::from(200); + let root_val_hotkey_2 = U256::from(201); + + add_network(NetUid::ROOT, 1, 0); + + // Root validators need to be registered on some subnet before root_register. + // Create a bootstrap subnet for that purpose. + let bootstrap_netuid = NetUid::from(1); + add_network(bootstrap_netuid, 1, 0); + register_ok_neuron(bootstrap_netuid, root_val_hotkey_1, root_val_coldkey_1, 0); + register_ok_neuron(bootstrap_netuid, root_val_hotkey_2, root_val_coldkey_2, 0); + + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(root_val_coldkey_1), + root_val_hotkey_1, + )); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(root_val_coldkey_2), + root_val_hotkey_2, + )); + + // Give root validators significant stake on root and bootstrap subnet + let root_stake = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &root_val_hotkey_1, + &root_val_coldkey_1, + NetUid::ROOT, + root_stake, + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &root_val_hotkey_2, + &root_val_coldkey_2, + NetUid::ROOT, + root_stake, + ); + + // --- Minimize cooldown so pending children activate quickly --- + assert_ok!(SubtensorModule::set_pending_childkey_cooldown( + RuntimeOrigin::root(), + 0, + )); + + // --- Set a high stake threshold --- + let high_threshold = 500_000_000u64; + SubtensorModule::set_stake_threshold(high_threshold); + + // --- Register a new subnet (this should automatically call do_set_root_validators_for_subnet) --- + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let lock_cost = SubtensorModule::get_network_lock_cost(); + add_balance_to_coldkey_account(&subnet_owner_coldkey, lock_cost.into()); + TotalIssuance::::mutate(|total| { + *total = total.saturating_add(lock_cost); + }); + + assert_ok!(SubtensorModule::set_auto_parent_delegation_enabled( + RuntimeOrigin::signed(root_val_coldkey_1), + root_val_hotkey_1, + false, + )); + + assert_ok!(SubtensorModule::register_network( + RuntimeOrigin::signed(subnet_owner_coldkey), + subnet_owner_hotkey, + )); + + // Determine the netuid that was just created + let netuid: NetUid = (TotalNetworks::::get().saturating_sub(1)).into(); + assert_eq!( + SubnetOwnerHotkey::::get(netuid), + subnet_owner_hotkey, + "Subnet owner hotkey should be set" + ); + + // Root validators need stake on the new subnet for child stake inheritance to work + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &root_val_hotkey_1, + &root_val_coldkey_1, + netuid, + root_stake, + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &root_val_hotkey_2, + &root_val_coldkey_2, + netuid, + root_stake, + ); + + // --- Verify child keys were applied immediately (SubtokenEnabled is false for new subnets) --- + let children_1 = SubtensorModule::get_children(&root_val_hotkey_1, netuid); + assert_eq!( + children_1, + vec![], + "Root validator 1 not have subnet owner as a child because AutoParentDelegationEnabled is false" + ); + let children_2 = SubtensorModule::get_children(&root_val_hotkey_2, netuid); + assert_eq!( + children_2, + vec![(u64::MAX, subnet_owner_hotkey)], + "Root validator 2 should have subnet owner as child" + ); + + // --- Verify subnet owner can now set weights --- + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); + let version_key = SubtensorModule::get_weights_version_key(netuid); + + assert!( + SubtensorModule::check_weights_min_stake(&subnet_owner_hotkey, netuid), + "Subnet owner should have enough inherited stake to set weights" + ); + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(subnet_owner_hotkey), + netuid, + vec![0], + vec![u16::MAX], + version_key + )); + }); +} diff --git a/pallets/subtensor/src/tests/children/schedule_multiple.rs b/pallets/subtensor/src/tests/children/schedule_multiple.rs new file mode 100644 index 0000000000..5ac0a33417 --- /dev/null +++ b/pallets/subtensor/src/tests/children/schedule_multiple.rs @@ -0,0 +1,722 @@ +#![allow(clippy::indexing_slicing)] +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] +use super::super::mock::*; +use frame_support::assert_err; + +use crate::{utils::rate_limiting::TransactionType, *}; +use sp_core::U256; + +// 16: Test setting multiple children successfully +// This test verifies that multiple children can be set for a parent successfully: +// - Sets up a network and registers a hotkey +// - Sets multiple children with different proportions +// - Verifies that the children are correctly assigned to the parent +// - Checks that the parent is correctly assigned to each child +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_multiple::test_do_schedule_children_multiple_success --exact --show-output --nocapture +#[test] +fn test_do_schedule_children_multiple_success() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child1 = U256::from(3); + let child2 = U256::from(4); + let netuid = NetUid::from(1); + let proportion1: u64 = 1000; + let proportion2: u64 = 2000; + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Set multiple children + mock_set_children( + &coldkey, + &hotkey, + netuid, + &[(proportion1, child1), (proportion2, child2)], + ); + + // Verify children assignment + let children = SubtensorModule::get_children(&hotkey, netuid); + assert_eq!(children, vec![(proportion1, child1), (proportion2, child2)]); + + // Verify parent assignment for both children + let parents1 = SubtensorModule::get_parents(&child1, netuid); + assert_eq!(parents1, vec![(proportion1, hotkey)]); + + let parents2 = SubtensorModule::get_parents(&child2, netuid); + assert_eq!(parents2, vec![(proportion2, hotkey)]); + }); +} + +// 17: Test setting multiple children in a non-existent network +// This test ensures that attempting to set multiple children in a non-existent network results in an error: +// - Attempts to set children in a network that doesn't exist +// - Verifies that the appropriate error is returned +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_multiple::test_do_schedule_children_multiple_network_does_not_exist --exact --show-output --nocapture +#[test] +fn test_do_schedule_children_multiple_network_does_not_exist() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child1 = U256::from(3); + let netuid = NetUid::from(999); // Non-existent network + let proportion: u64 = 1000; + + // Attempt to set children + assert_err!( + SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + vec![(proportion, child1)] + ), + Error::::SubnetNotExists + ); + }); +} + +// 18: Test setting multiple children with an invalid child +// This test verifies that attempting to set multiple children with an invalid child (same as parent) results in an error: +// - Sets up a network and registers a hotkey +// - Attempts to set a child that is the same as the parent hotkey +// - Checks that the appropriate error is returned +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_multiple::test_do_schedule_children_multiple_invalid_child --exact --show-output --nocapture +#[test] +fn test_do_schedule_children_multiple_invalid_child() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = NetUid::from(1); + let proportion: u64 = 1000; + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Attempt to set child as the same hotkey + assert_err!( + SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + vec![(proportion, hotkey)] + ), + Error::::InvalidChild + ); + }); +} + +// 19: Test setting multiple children with a non-associated coldkey +// This test ensures that attempting to set multiple children using an unassociated coldkey results in an error: +// - Sets up a network with a hotkey registered to a different coldkey +// - Attempts to set children using an unassociated coldkey +// - Verifies that the appropriate error is returned +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_multiple::test_do_schedule_children_multiple_non_associated_coldkey --exact --show-output --nocapture +#[test] +fn test_do_schedule_children_multiple_non_associated_coldkey() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child = U256::from(3); + let netuid = NetUid::from(1); + let proportion: u64 = 1000; + + // Add network and register hotkey with a different coldkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, U256::from(999), 0); + + // Attempt to set children + assert_err!( + SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + vec![(proportion, child)] + ), + Error::::NonAssociatedColdKey + ); + }); +} + +// 20: Test setting multiple children in root network +// This test verifies that attempting to set children in the root network results in an error: +// - Sets up the root network +// - Attempts to set children in the root network +// - Checks that the appropriate error is returned +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_multiple::test_do_schedule_children_multiple_root_network --exact --show-output --nocapture +#[test] +fn test_do_schedule_children_multiple_root_network() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child = U256::from(3); + let netuid = NetUid::ROOT; // Root network + let proportion: u64 = 1000; + + // Add network and register hotkey + add_network(netuid, 13, 0); + + // Attempt to set children + assert_err!( + SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + vec![(proportion, child)] + ), + Error::::RegistrationNotPermittedOnRootSubnet + ); + }); +} + +// 21: Test cleanup of old children when setting multiple new ones +// This test ensures that when new children are set, the old ones are properly removed: +// - Sets up a network and registers a hotkey +// - Sets an initial child +// - Replaces it with multiple new children +// - Verifies that the old child is no longer associated +// - Confirms the new children are correctly assigned +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_multiple::test_do_schedule_children_multiple_old_children_cleanup --exact --show-output --nocapture +#[test] +fn test_do_schedule_children_multiple_old_children_cleanup() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let old_child = U256::from(3); + let new_child1 = U256::from(4); + let new_child2 = U256::from(5); + let netuid = NetUid::from(1); + let proportion: u64 = 1000; + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Set old child + mock_set_children(&coldkey, &hotkey, netuid, &[(proportion, old_child)]); + + step_rate_limit(&TransactionType::SetChildren, netuid); + + // Set new children + mock_set_children( + &coldkey, + &hotkey, + netuid, + &[(proportion, new_child1), (proportion, new_child2)], + ); + + // Verify old child is removed + let old_child_parents = SubtensorModule::get_parents(&old_child, netuid); + assert!(old_child_parents.is_empty()); + + // Verify new children assignment + let new_child1_parents = SubtensorModule::get_parents(&new_child1, netuid); + assert_eq!(new_child1_parents, vec![(proportion, hotkey)]); + + let new_child2_parents = SubtensorModule::get_parents(&new_child2, netuid); + assert_eq!(new_child2_parents, vec![(proportion, hotkey)]); + }); +} + +// 22: Test setting multiple children with edge case proportions +// This test verifies the behavior when setting multiple children with minimum and maximum proportions: +// - Sets up a network and registers a hotkey +// - Sets two children with minimum and maximum proportions respectively +// - Verifies that the children are correctly assigned with their respective proportions +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_multiple::test_do_schedule_children_multiple_proportion_edge_cases --exact --show-output --nocapture +#[test] +fn test_do_schedule_children_multiple_proportion_edge_cases() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child1 = U256::from(3); + let child2 = U256::from(4); + let netuid = NetUid::from(1); + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Set children with minimum and maximum proportions + let min_proportion: u64 = 0; + let max_proportion: u64 = u64::MAX; + mock_set_children( + &coldkey, + &hotkey, + netuid, + &[(min_proportion, child1), (max_proportion, child2)], + ); + + // Verify children assignment + let children = SubtensorModule::get_children(&hotkey, netuid); + assert_eq!( + children, + vec![(min_proportion, child1), (max_proportion, child2)] + ); + }); +} + +// 23: Test overwriting existing children with new ones +// This test ensures that when new children are set, they correctly overwrite the existing ones: +// - Sets up a network and registers a hotkey +// - Sets initial children +// - Overwrites with new children +// - Verifies that the final children assignment is correct +// - Checks that old children are properly removed and new ones are correctly assigned +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_multiple::test_do_schedule_children_multiple_overwrite_existing --exact --show-output --nocapture +#[test] +fn test_do_schedule_children_multiple_overwrite_existing() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child1 = U256::from(3); + let child2 = U256::from(4); + let child3 = U256::from(5); + let netuid = NetUid::from(1); + let proportion: u64 = 1000; + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Set initial children + mock_set_children( + &coldkey, + &hotkey, + netuid, + &[(proportion, child1), (proportion, child2)], + ); + + step_rate_limit(&TransactionType::SetChildren, netuid); + + // Overwrite with new children + mock_set_children( + &coldkey, + &hotkey, + netuid, + &[(proportion * 2, child2), (proportion * 3, child3)], + ); + + // Verify final children assignment + let children = SubtensorModule::get_children(&hotkey, netuid); + assert_eq!( + children, + vec![(proportion * 2, child2), (proportion * 3, child3)] + ); + + // Verify parent assignment for all children + let parents1 = SubtensorModule::get_parents(&child1, netuid); + assert!(parents1.is_empty()); + + let parents2 = SubtensorModule::get_parents(&child2, netuid); + assert_eq!(parents2, vec![(proportion * 2, hotkey)]); + + let parents3 = SubtensorModule::get_parents(&child3, netuid); + assert_eq!(parents3, vec![(proportion * 3, hotkey)]); + }); +} + +// 27: Test setting children with an empty list +// This test verifies the behavior of setting an empty children list: +// - Adds a network and registers a hotkey +// - Sets an empty children list for the hotkey +// - Verifies that the children assignment is empty +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_multiple::test_do_schedule_children_multiple_empty_list --exact --show-output --nocapture +#[test] +fn test_do_schedule_children_multiple_empty_list() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = NetUid::from(1); + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Set empty children list + mock_set_children(&coldkey, &hotkey, netuid, &[]); + + // Verify children assignment is empty + let children = SubtensorModule::get_children(&hotkey, netuid); + assert!(children.is_empty()); + }); +} + +// 28: Test revoking multiple children successfully +// This test verifies the successful revocation of multiple children: +// - Adds a network and registers a hotkey +// - Sets multiple children for the hotkey +// - Revokes all children by setting an empty list +// - Verifies that the children list is empty +// - Verifies that the parent-child relationships are removed for both children +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_multiple::test_do_revoke_children_multiple_success --exact --show-output --nocapture +#[test] +fn test_do_revoke_children_multiple_success() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child1 = U256::from(3); + let child2 = U256::from(4); + let netuid = NetUid::from(1); + let proportion1: u64 = 1000; + let proportion2: u64 = 2000; + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Set multiple children + mock_set_children( + &coldkey, + &hotkey, + netuid, + &[(proportion1, child1), (proportion2, child2)], + ); + + step_rate_limit(&TransactionType::SetChildren, netuid); + + // Revoke multiple children + mock_set_children(&coldkey, &hotkey, netuid, &[]); + + // Verify children removal + let children = SubtensorModule::get_children(&hotkey, netuid); + assert!(children.is_empty()); + + // Verify parent removal for both children + let parents1 = SubtensorModule::get_parents(&child1, netuid); + assert!(parents1.is_empty()); + + let parents2 = SubtensorModule::get_parents(&child2, netuid); + assert!(parents2.is_empty()); + }); +} + +// 29: Test revoking children when network does not exist +// This test verifies the behavior when attempting to revoke children on a non-existent network: +// - Attempts to revoke children on a network that doesn't exist +// - Verifies that the operation fails with the correct error +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_multiple::test_do_revoke_children_multiple_network_does_not_exist --exact --show-output --nocapture +#[test] +fn test_do_revoke_children_multiple_network_does_not_exist() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child1 = U256::from(3); + let child2 = U256::from(4); + let netuid = NetUid::from(999); // Non-existent network + // Attempt to revoke children + assert_err!( + SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + vec![(u64::MAX / 2, child1), (u64::MAX / 2, child2)] + ), + Error::::SubnetNotExists + ); + }); +} + +// 30: Test revoking children with non-associated coldkey +// This test verifies the behavior when attempting to revoke children using a non-associated coldkey: +// - Adds a network and registers a hotkey with a different coldkey +// - Attempts to revoke children using an unassociated coldkey +// - Verifies that the operation fails with the correct error +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_multiple::test_do_revoke_children_multiple_non_associated_coldkey --exact --show-output --nocapture +#[test] +fn test_do_revoke_children_multiple_non_associated_coldkey() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child1 = U256::from(3); + let child2 = U256::from(4); + let netuid = NetUid::from(1); + + // Add network and register hotkey with a different coldkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, U256::from(999), 0); + + // Attempt to revoke children + assert_err!( + SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + vec![(u64::MAX / 2, child1), (u64::MAX / 2, child2)] + ), + Error::::NonAssociatedColdKey + ); + }); +} + +// 31: Test partial revocation of children +// This test verifies the behavior when partially revoking children: +// - Adds a network and registers a hotkey +// - Sets multiple children for the hotkey +// - Revokes one of the children +// - Verifies that the correct children remain and the revoked child is removed +// - Checks the parent-child relationships after partial revocation +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_multiple::test_do_revoke_children_multiple_partial_revocation --exact --show-output --nocapture +#[test] +fn test_do_revoke_children_multiple_partial_revocation() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child1 = U256::from(3); + let child2 = U256::from(4); + let child3 = U256::from(5); + let netuid = NetUid::from(1); + let proportion: u64 = 1000; + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Set multiple children + mock_set_children( + &coldkey, + &hotkey, + netuid, + &[ + (proportion, child1), + (proportion, child2), + (proportion, child3), + ], + ); + + step_rate_limit(&TransactionType::SetChildren, netuid); + + // Revoke only child3 + mock_set_children( + &coldkey, + &hotkey, + netuid, + &[(proportion, child1), (proportion, child2)], + ); + + // Verify children removal + let children = SubtensorModule::get_children(&hotkey, netuid); + assert_eq!(children, vec![(proportion, child1), (proportion, child2)]); + + // Verify parents. + let parents1 = SubtensorModule::get_parents(&child3, netuid); + assert!(parents1.is_empty()); + let parents1 = SubtensorModule::get_parents(&child1, netuid); + assert_eq!(parents1, vec![(proportion, hotkey)]); + let parents2 = SubtensorModule::get_parents(&child2, netuid); + assert_eq!(parents2, vec![(proportion, hotkey)]); + }); +} + +// 32: Test revoking non-existent children +// This test verifies the behavior when attempting to revoke non-existent children: +// - Adds a network and registers a hotkey +// - Sets one child for the hotkey +// - Attempts to revoke all children (including non-existent ones) +// - Verifies that all children are removed, including the existing one +// - Checks that the parent-child relationship is properly updated +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_multiple::test_do_revoke_children_multiple_non_existent_children --exact --show-output --nocapture +#[test] +fn test_do_revoke_children_multiple_non_existent_children() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child1 = U256::from(3); + let netuid = NetUid::from(1); + let proportion: u64 = 1000; + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Set one child + mock_set_children(&coldkey, &hotkey, netuid, &[(proportion, child1)]); + + step_rate_limit(&TransactionType::SetChildren, netuid); + + // Attempt to revoke existing and non-existent children + mock_set_children(&coldkey, &hotkey, netuid, &[]); + + // Verify all children are removed + let children = SubtensorModule::get_children(&hotkey, netuid); + assert!(children.is_empty()); + + // Verify parent removal for the existing child + let parents1 = SubtensorModule::get_parents(&child1, netuid); + assert!(parents1.is_empty()); + }); +} + +// 33: Test revoking children with an empty list +// This test verifies the behavior when attempting to revoke children using an empty list: +// - Adds a network and registers a hotkey +// - Attempts to revoke children with an empty list +// - Verifies that no changes occur in the children list +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_multiple::test_do_revoke_children_multiple_empty_list --exact --show-output --nocapture +#[test] +fn test_do_revoke_children_multiple_empty_list() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = NetUid::from(1); + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Attempt to revoke with an empty list + mock_set_children(&coldkey, &hotkey, netuid, &[]); + + // Verify no changes in children + let children = SubtensorModule::get_children(&hotkey, netuid); + assert!(children.is_empty()); + }); +} + +// 34: Test complex scenario for revoking multiple children +// This test verifies a complex scenario involving setting and revoking multiple children: +// - Adds a network and registers a hotkey +// - Sets multiple children with different proportions +// - Revokes one child and verifies the remaining children +// - Revokes all remaining children +// - Verifies that all parent-child relationships are properly updated +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_multiple::test_do_revoke_children_multiple_complex_scenario --exact --show-output --nocapture +#[test] +fn test_do_revoke_children_multiple_complex_scenario() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child1 = U256::from(3); + let child2 = U256::from(4); + let child3 = U256::from(5); + let netuid = NetUid::from(1); + let proportion1: u64 = 1000; + let proportion2: u64 = 2000; + let proportion3: u64 = 3000; + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Set multiple children + mock_set_children( + &coldkey, + &hotkey, + netuid, + &[ + (proportion1, child1), + (proportion2, child2), + (proportion3, child3), + ], + ); + + step_rate_limit(&TransactionType::SetChildren, netuid); + + // Revoke child2 + mock_set_children( + &coldkey, + &hotkey, + netuid, + &[(proportion1, child1), (proportion3, child3)], + ); + + // Verify remaining children + let children = SubtensorModule::get_children(&hotkey, netuid); + assert_eq!(children, vec![(proportion1, child1), (proportion3, child3)]); + + // Verify parent removal for child2 + let parents2 = SubtensorModule::get_parents(&child2, netuid); + assert!(parents2.is_empty()); + + step_rate_limit(&TransactionType::SetChildren, netuid); + + // Revoke remaining children + mock_set_children(&coldkey, &hotkey, netuid, &[]); + + // Verify all children are removed + let children = SubtensorModule::get_children(&hotkey, netuid); + assert!(children.is_empty()); + + // Verify parent removal for all children + let parents1 = SubtensorModule::get_parents(&child1, netuid); + assert!(parents1.is_empty()); + let parents3 = SubtensorModule::get_parents(&child3, netuid); + assert!(parents3.is_empty()); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_multiple::test_set_child_keys_empty_vector_clears_storage --exact --show-output +#[test] +fn test_set_child_keys_empty_vector_clears_storage() { + new_test_ext(1).execute_with(|| { + let sn_owner_hotkey = U256::from(1001); + let sn_owner_coldkey = U256::from(1002); + let parent = U256::from(1); + let child = U256::from(2); + let netuid = add_dynamic_network(&sn_owner_hotkey, &sn_owner_coldkey); + + // Initialize ChildKeys for `parent` with a non-empty vector + ChildKeys::::insert(parent, netuid, vec![(u64::MAX, child)]); + ParentKeys::::insert(child, netuid, vec![(u64::MAX, parent)]); + + // Sanity: entry exists right now because we explicitly inserted it + assert!(ChildKeys::::contains_key(parent, netuid)); + assert!(ParentKeys::::contains_key(child, netuid)); + + // Set children to empty + let empty_children: Vec<(u64, U256)> = Vec::new(); + mock_set_children_no_epochs(netuid, &parent, &empty_children); + + // When the child vector is empty, we should NOT keep an empty vec in storage. + // The key must be fully removed (no entry), not just zero-length value. + assert!(!ChildKeys::::contains_key(parent, netuid)); + assert!(!ParentKeys::::contains_key(child, netuid)); + + // `get` returns empty due to ValueQuery default, but presence is false. + assert!(ChildKeys::::get(parent, netuid).is_empty()); + assert!(ParentKeys::::get(child, netuid).is_empty()); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_multiple::test_set_child_keys_no_start_call_sets_immediately --exact --show-output +#[test] +fn test_set_child_keys_no_start_call_sets_immediately() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child1 = U256::from(3); + let child2 = U256::from(4); + let netuid = NetUid::from(1); + let proportion1: u64 = 1000; + let proportion2: u64 = 2000; + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Clear SubtokenEnabled + SubtokenEnabled::::remove(netuid); + + // Set multiple children + mock_schedule_children( + &coldkey, + &hotkey, + netuid, + &[(proportion1, child1), (proportion2, child2)], + ); + + // Normally happens on epoch + SubtensorModule::do_set_pending_children(netuid); + + // Verify pending map is empty + assert!(!PendingChildKeys::::contains_key(netuid, hotkey)); + + // Verify that childkey is set + assert_eq!( + ChildKeys::::get(hotkey, netuid), + vec![(proportion1, child1), (proportion2, child2)] + ); + }); +} diff --git a/pallets/subtensor/src/tests/children/schedule_singular.rs b/pallets/subtensor/src/tests/children/schedule_singular.rs new file mode 100644 index 0000000000..22840078d4 --- /dev/null +++ b/pallets/subtensor/src/tests/children/schedule_singular.rs @@ -0,0 +1,464 @@ +#![allow(clippy::indexing_slicing)] +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] +use super::super::mock::*; +use frame_support::assert_err; + +use crate::{utils::rate_limiting::TransactionType, *}; +use sp_core::U256; + +// 1: Successful setting of a single child +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_singular::test_do_set_child_singular_success --exact --show-output --nocapture +#[test] +fn test_do_set_child_singular_success() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child = U256::from(3); + let netuid = NetUid::from(1); + let proportion: u64 = 1000; + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Set child + mock_set_children(&coldkey, &hotkey, netuid, &[(proportion, child)]); + + // Verify child assignment + let children = SubtensorModule::get_children(&hotkey, netuid); + assert_eq!(children, vec![(proportion, child)]); + }); +} + +// 2: Attempt to set child in non-existent network +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_singular::test_do_set_child_singular_network_does_not_exist --exact --show-output --nocapture +#[test] +fn test_do_set_child_singular_network_does_not_exist() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child = U256::from(3); + let netuid = NetUid::from(999); // Non-existent network + let proportion: u64 = 1000; + + // Attempt to set child + assert_err!( + SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + vec![(proportion, child)] + ), + Error::::SubnetNotExists + ); + }); +} + +// 3: Attempt to set invalid child (same as hotkey) +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_singular::test_do_set_child_singular_invalid_child --exact --show-output --nocapture +#[test] +fn test_do_set_child_singular_invalid_child() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = NetUid::from(1); + let proportion: u64 = 1000; + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Attempt to set child as the same hotkey + assert_err!( + SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + vec![ + (proportion, hotkey) // Invalid child + ] + ), + Error::::InvalidChild + ); + }); +} + +// 4: Attempt to set child with non-associated coldkey +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_singular::test_do_set_child_singular_non_associated_coldkey --exact --show-output --nocapture +#[test] +fn test_do_set_child_singular_non_associated_coldkey() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child = U256::from(3); + let netuid = NetUid::from(1); + let proportion: u64 = 1000; + + // Add network and register hotkey with a different coldkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, U256::from(999), 0); + + // Attempt to set child + assert_err!( + SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + vec![(proportion, child)] + ), + Error::::NonAssociatedColdKey + ); + }); +} + +// 5: Attempt to set child in root network +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_singular::test_do_set_child_singular_root_network --exact --show-output --nocapture +#[test] +fn test_do_set_child_singular_root_network() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child = U256::from(3); + let netuid = NetUid::ROOT; // Root network + let proportion: u64 = 1000; + + // Add network and register hotkey + add_network(netuid, 13, 0); + + // Attempt to set child + assert_err!( + SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + vec![(proportion, child)] + ), + Error::::RegistrationNotPermittedOnRootSubnet + ); + }); +} + +// 6: Cleanup of old children when setting new ones +// This test verifies that when new children are set, the old ones are properly removed. +// It checks: +// - Setting an initial child +// - Replacing it with a new child +// - Ensuring the old child is no longer associated +// - Confirming the new child is correctly assigned +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_singular::test_do_set_child_singular_old_children_cleanup --exact --show-output --nocapture +#[test] +fn test_do_set_child_singular_old_children_cleanup() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let old_child = U256::from(3); + let new_child = U256::from(4); + let netuid = NetUid::from(1); + let proportion: u64 = 1000; + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Set old child + mock_set_children(&coldkey, &hotkey, netuid, &[(proportion, old_child)]); + + step_rate_limit(&TransactionType::SetChildren, netuid); + + // Set new child + mock_set_children(&coldkey, &hotkey, netuid, &[(proportion, new_child)]); + + // Verify old child is removed + let old_child_parents = SubtensorModule::get_parents(&old_child, netuid); + assert!(old_child_parents.is_empty()); + + // Verify new child assignment + let new_child_parents = SubtensorModule::get_parents(&new_child, netuid); + assert_eq!(new_child_parents, vec![(proportion, hotkey)]); + }); +} + +// 7: Verify new children assignment +// This test checks if new children are correctly assigned to a parent. +// It verifies: +// - Setting a child for a parent +// - Confirming the child is correctly listed under the parent +// - Ensuring the parent is correctly listed for the child +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_singular::test_do_set_child_singular_new_children_assignment --exact --show-output --nocapture +#[test] +fn test_do_set_child_singular_new_children_assignment() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child = U256::from(3); + let netuid = NetUid::from(1); + let proportion: u64 = 1000; + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Set child + mock_set_children(&coldkey, &hotkey, netuid, &[(proportion, child)]); + + // Verify child assignment + let children = SubtensorModule::get_children(&hotkey, netuid); + assert_eq!(children, vec![(proportion, child)]); + + // Verify parent assignment + let parents = SubtensorModule::get_parents(&child, netuid); + assert_eq!(parents, vec![(proportion, hotkey)]); + }); +} + +// 8: Test edge cases for proportion values +// This test verifies that the system correctly handles minimum and maximum proportion values. +// It checks: +// - Setting a child with the minimum possible proportion (0) +// - Setting a child with the maximum possible proportion (u64::MAX) +// - Confirming both assignments are processed correctly +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_singular::test_do_set_child_singular_proportion_edge_cases --exact --show-output --nocapture +#[test] +fn test_do_set_child_singular_proportion_edge_cases() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child = U256::from(3); + let netuid = NetUid::from(1); + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Set child with minimum proportion + let min_proportion: u64 = 0; + mock_set_children(&coldkey, &hotkey, netuid, &[(min_proportion, child)]); + + // Verify child assignment with minimum proportion + let children = SubtensorModule::get_children(&hotkey, netuid); + assert_eq!(children, vec![(min_proportion, child)]); + + step_rate_limit(&TransactionType::SetChildren, netuid); + + // Set child with maximum proportion + let max_proportion: u64 = u64::MAX; + mock_set_children(&coldkey, &hotkey, netuid, &[(max_proportion, child)]); + + // Verify child assignment with maximum proportion + let children = SubtensorModule::get_children(&hotkey, netuid); + assert_eq!(children, vec![(max_proportion, child)]); + }); +} + +// 9: Test setting multiple children +// This test verifies that when multiple children are set, only the last one remains. +// It checks: +// - Setting an initial child +// - Setting a second child +// - Confirming only the second child remains associated +// - Verifying the first child is no longer associated +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_singular::test_do_set_child_singular_multiple_children --exact --show-output --nocapture +#[test] +fn test_do_set_child_singular_multiple_children() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child1 = U256::from(3); + let child2 = U256::from(4); + let netuid = NetUid::from(1); + let proportion1: u64 = 500; + let proportion2: u64 = 500; + + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + // Set first child + mock_set_children(&coldkey, &hotkey, netuid, &[(proportion1, child1)]); + + step_rate_limit(&TransactionType::SetChildren, netuid); + + // Set second child + mock_set_children(&coldkey, &hotkey, netuid, &[(proportion1, child2)]); + + // Verify children assignment + let children = SubtensorModule::get_children(&hotkey, netuid); + assert_eq!(children, vec![(proportion2, child2)]); + + // Verify parent assignment for both children + let parents1 = SubtensorModule::get_parents(&child1, netuid); + assert!(parents1.is_empty()); // Old child should be removed + + let parents2 = SubtensorModule::get_parents(&child2, netuid); + assert_eq!(parents2, vec![(proportion2, hotkey)]); + }); +} + +// 10: Test adding a singular child with various error conditions +// This test checks different scenarios when adding a child, including: +// - Attempting to set a child in a non-existent network +// - Trying to set a child with an unassociated coldkey +// - Setting an invalid child +// - Successfully setting a valid child +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_singular::test_add_singular_child --exact --show-output --nocapture +#[test] +fn test_add_singular_child() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let child = U256::from(1); + let hotkey = U256::from(1); + let coldkey = U256::from(2); + assert_eq!( + SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + vec![(u64::MAX, child)] + ), + Err(Error::::SubnetNotExists.into()) + ); + add_network(netuid, 1, 0); + step_rate_limit(&TransactionType::SetChildren, netuid); + assert_eq!( + SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + vec![(u64::MAX, child)] + ), + Err(Error::::NonAssociatedColdKey.into()) + ); + let _ = SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + step_rate_limit(&TransactionType::SetChildren, netuid); + assert_eq!( + SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + vec![(u64::MAX, child)] + ), + Err(Error::::InvalidChild.into()) + ); + let child = U256::from(3); + step_rate_limit(&TransactionType::SetChildren, netuid); + + mock_set_children(&coldkey, &hotkey, netuid, &[(u64::MAX, child)]); + }) +} + +// 12: Test revoking a singular child successfully +// This test checks the process of revoking a child neuron: +// - Sets up a network with a parent and child neuron +// - Establishes a parent-child relationship +// - Revokes the child relationship +// - Verifies that the child is removed from the parent's children list +// - Ensures the parent is removed from the child's parents list +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_singular::test_do_revoke_child_singular_success --exact --show-output --nocapture +#[test] +fn test_do_revoke_child_singular_success() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child = U256::from(3); + let netuid = NetUid::from(1); + let proportion: u64 = 1000; + // Add network and register hotkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + // Set child + mock_set_children(&coldkey, &hotkey, netuid, &[(proportion, child)]); + // Verify child assignment + let children = SubtensorModule::get_children(&hotkey, netuid); + assert_eq!(children, vec![(proportion, child)]); + step_rate_limit(&TransactionType::SetChildren, netuid); + // Revoke child + mock_set_children(&coldkey, &hotkey, netuid, &[]); + // Verify child removal + let children = SubtensorModule::get_children(&hotkey, netuid); + assert!(children.is_empty()); + // Verify parent removal + let parents = SubtensorModule::get_parents(&child, netuid); + assert!(parents.is_empty()); + }); +} + +// 13: Test setting empty child vector on a non-existing subnet +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_singular::test_do_set_empty_children_network_does_not_exist --exact --show-output --nocapture +#[test] +fn test_do_set_empty_children_network_does_not_exist() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = NetUid::from(999); // Non-existent network + // Attempt to revoke child + assert_err!( + SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + vec![] + ), + Error::::SubnetNotExists + ); + }); +} + +// 14: Test revoking a child with a non-associated coldkey +// This test ensures that attempting to revoke a child using an unassociated coldkey results in an error: +// - Sets up a network with a hotkey registered to a different coldkey +// - Attempts to revoke a child using an unassociated coldkey +// - Verifies that the appropriate error is returned +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_singular::test_do_revoke_child_singular_non_associated_coldkey --exact --show-output --nocapture +#[test] +fn test_do_revoke_child_singular_non_associated_coldkey() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = NetUid::from(1); + + // Add network and register hotkey with a different coldkey + add_network(netuid, 13, 0); + register_ok_neuron(netuid, hotkey, U256::from(999), 0); + + // Attempt to revoke child + assert_err!( + SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + vec![] + ), + Error::::NonAssociatedColdKey + ); + }); +} + +// 15: Test revoking a non-associated child +// This test verifies that attempting to revoke a child that is not associated with the parent results in an error: +// - Sets up a network and registers a hotkey +// - Attempts to revoke a child that was never associated with the parent +// - Checks that the appropriate error is returned +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::children::schedule_singular::test_do_revoke_child_singular_child_not_associated --exact --show-output --nocapture +#[test] +fn test_do_revoke_child_singular_child_not_associated() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child = U256::from(3); + let netuid = NetUid::from(1); + + // Add network and register hotkey + add_network(netuid, 13, 0); + // Attempt to revoke child that is not associated + assert_err!( + SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + vec![(u64::MAX, child)] + ), + Error::::NonAssociatedColdKey + ); + }); +} diff --git a/pallets/subtensor/src/tests/claim_root.rs b/pallets/subtensor/src/tests/claim_root.rs index bc756f50a0..39ad7a373d 100644 --- a/pallets/subtensor/src/tests/claim_root.rs +++ b/pallets/subtensor/src/tests/claim_root.rs @@ -1,3 +1,7 @@ +//! Tests for root alpha claims ([`crate::staking::claim_root`]). +//! +//! Covers claim thresholds, pending dividends, dissolve interaction, and claim types. + #![allow(clippy::expect_used, clippy::unwrap_used)] use super::mock::run_block_idle; @@ -1311,7 +1315,7 @@ fn test_claim_root_with_swap_coldkey() { ); // Swap coldkey - assert_ok!(SubtensorModule::do_swap_coldkey(&coldkey, &new_coldkey,)); + assert_ok!(SubtensorModule::perform_coldkey_swap(&coldkey, &new_coldkey,)); // Check swapped keys claimed values @@ -2443,7 +2447,7 @@ fn ghsa_2026_012_staking_coldkey_index_never_decremented() { ); // Swap the coldkey via the real extrinsic helper. - assert_ok!(SubtensorModule::do_swap_coldkey(&coldkey, &new_coldkey)); + assert_ok!(SubtensorModule::perform_coldkey_swap(&coldkey, &new_coldkey)); // FIXED (GHSA-2026-012): the swap prunes the now-zero-stake old coldkey from the // index and decrements the counter. The index is empty again. diff --git a/pallets/subtensor/src/tests/cleanup_tests.rs b/pallets/subtensor/src/tests/cleanup_tests.rs index ed7a61aade..ca50c839b8 100644 --- a/pallets/subtensor/src/tests/cleanup_tests.rs +++ b/pallets/subtensor/src/tests/cleanup_tests.rs @@ -1,3 +1,7 @@ +//! Unit tests for [`crate::utils::cleanup`] `remove_storage_entries_for_netuid`. +//! +//! Exercises read/write weight budgeting and deferred removals during dissolve cleanup. + #![allow(clippy::unwrap_used)] use super::mock::*; @@ -7,15 +11,18 @@ use subtensor_runtime_common::NetUid; type TestEntry = (NetUid, u64); -fn db_read() -> Weight { +/// Single DB-read weight unit for budget tests. +fn db_read_weight() -> Weight { ::DbWeight::get().reads(1) } -fn db_writes(n: u64) -> Weight { +/// `n` DB-write weight units for budget tests. +fn db_write_weight(n: u64) -> Weight { ::DbWeight::get().writes(n) } -fn run_cleanup( +/// Drive [`SubtensorModule::remove_storage_entries_for_netuid`] and collect removed ids. +fn run_remove_storage_entries_cleanup( weight_meter: &mut WeightMeter, entries: Vec, target: NetUid, @@ -40,7 +47,7 @@ fn remove_storage_entries_for_netuid_empty_iterator() { let mut weight_meter = WeightMeter::with_limit(limit); let (read_all, last_item, removed) = - run_cleanup(&mut weight_meter, vec![], NetUid::from(1), 1); + run_remove_storage_entries_cleanup(&mut weight_meter, vec![], NetUid::from(1), 1); assert!(read_all); assert!(last_item.is_none()); @@ -60,15 +67,16 @@ fn remove_storage_entries_for_netuid_removes_matching_entries() { ]; let mut weight_meter = WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)); - let (read_all, last_item, removed) = run_cleanup(&mut weight_meter, entries, target, 1); + let (read_all, last_item, removed) = + run_remove_storage_entries_cleanup(&mut weight_meter, entries, target, 1); assert!(read_all); assert_eq!(last_item, Some((NetUid::from(1), 30))); assert_eq!(removed, vec![10, 30]); - let expected = db_read() + let expected = db_read_weight() .saturating_mul(3) - .saturating_add(db_writes(1).saturating_mul(2)); + .saturating_add(db_write_weight(1).saturating_mul(2)); assert_eq!(weight_meter.consumed(), expected); }); } @@ -84,12 +92,13 @@ fn remove_storage_entries_for_netuid_skips_non_matching_entries() { ]; let mut weight_meter = WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)); - let (read_all, last_item, removed) = run_cleanup(&mut weight_meter, entries, target, 1); + let (read_all, last_item, removed) = + run_remove_storage_entries_cleanup(&mut weight_meter, entries, target, 1); assert!(read_all); assert_eq!(last_item, Some((NetUid::from(3), 30))); assert!(removed.is_empty()); - assert_eq!(weight_meter.consumed(), db_read().saturating_mul(3)); + assert_eq!(weight_meter.consumed(), db_read_weight().saturating_mul(3)); }); } @@ -103,10 +112,11 @@ fn remove_storage_entries_for_netuid_stops_when_read_budget_exhausted() { (NetUid::from(1), 30), ]; // Budget for two reads only; the third entry is never scanned. - let limit = db_read().saturating_mul(2); + let limit = db_read_weight().saturating_mul(2); let mut weight_meter = WeightMeter::with_limit(limit); - let (read_all, last_item, removed) = run_cleanup(&mut weight_meter, entries, target, 1); + let (read_all, last_item, removed) = + run_remove_storage_entries_cleanup(&mut weight_meter, entries, target, 1); assert!(!read_all); assert_eq!(last_item, Some((NetUid::from(2), 20))); @@ -121,10 +131,13 @@ fn remove_storage_entries_for_netuid_stops_when_write_budget_exhausted() { let target = NetUid::from(1); let entries = vec![(NetUid::from(1), 10), (NetUid::from(1), 20)]; // Two reads and one write: first match is removed, second match reads but cannot write. - let limit = db_read().saturating_mul(2).saturating_add(db_writes(1)); + let limit = db_read_weight() + .saturating_mul(2) + .saturating_add(db_write_weight(1)); let mut weight_meter = WeightMeter::with_limit(limit); - let (read_all, last_item, removed) = run_cleanup(&mut weight_meter, entries, target, 1); + let (read_all, last_item, removed) = + run_remove_storage_entries_cleanup(&mut weight_meter, entries, target, 1); assert!(!read_all); assert_eq!(last_item, Some((NetUid::from(1), 10))); @@ -140,13 +153,17 @@ fn remove_storage_entries_for_netuid_respects_writes_per_match() { let entries = vec![(NetUid::from(1), 10), (NetUid::from(1), 20)]; let writes_per_match = 2_u64; // Two reads and two writes: first match is removed, second match reads but cannot write. - let limit = db_read() + let limit = db_read_weight() .saturating_mul(2) - .saturating_add(db_writes(writes_per_match)); + .saturating_add(db_write_weight(writes_per_match)); let mut weight_meter = WeightMeter::with_limit(limit); - let (read_all, last_item, removed) = - run_cleanup(&mut weight_meter, entries, target, writes_per_match); + let (read_all, last_item, removed) = run_remove_storage_entries_cleanup( + &mut weight_meter, + entries, + target, + writes_per_match, + ); assert!(!read_all); assert_eq!(last_item, Some((NetUid::from(1), 10))); diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs deleted file mode 100644 index fafa689661..0000000000 --- a/pallets/subtensor/src/tests/coinbase.rs +++ /dev/null @@ -1,4705 +0,0 @@ -#![allow( - unused, - clippy::arithmetic_side_effects, - clippy::indexing_slicing, - clippy::panic, - clippy::unwrap_used, - clippy::expect_used -)] -use super::mock::*; - -use crate::tests::mock; -use crate::*; -use alloc::collections::BTreeMap; -use approx::assert_abs_diff_eq; -use frame_support::assert_ok; -use sp_core::U256; -use sp_runtime::PerU16; -use substrate_fixed::types::{I64F64, I96F32, U64F64, U96F32}; -use subtensor_runtime_common::{AlphaBalance, NetUidStorageIndex}; -use subtensor_swap_interface::SwapHandler; - -#[allow(clippy::arithmetic_side_effects)] -fn close(value: u64, target: u64, eps: u64) { - assert!( - (value as i64 - target as i64).abs() < eps as i64, - "Assertion failed: value = {value}, target = {target}, eps = {eps}" - ) -} - -/// Seed a large root stake with full TAO weight so that -/// `root_proportion = tao_weight / (tao_weight + alpha_issuance)` is ~1. -/// This keeps the alpha-injection cap (`root_proportion * alpha_emission`) from -/// spuriously binding for small per-subnet emissions, preserving the liquidity -/// injection behavior these tests were written for. -fn set_full_injection_root_stake() { - SubnetTAO::::insert( - NetUid::ROOT, - TaoBalance::from(1_000_000_000_000_000_000_u64), - ); - SubtensorModule::set_tao_weight(u64::MAX); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_hotkey_take --exact --show-output --nocapture -#[test] -fn test_hotkey_take() { - new_test_ext(1).execute_with(|| { - let hotkey = U256::from(1); - Delegates::::insert(hotkey, PerU16::from_parts(u16::MAX / 2)); - log::info!( - "expected: {:?}", - SubtensorModule::get_hotkey_take_float(&hotkey) - ); - log::info!( - "expected: {:?}", - SubtensorModule::get_hotkey_take_float(&hotkey) - ); - }); -} - -// Test the base case of running coinbase with zero emission. -// This test verifies that the coinbase mechanism can handle the edge case -// of zero emission without errors or unexpected behavior. -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_basecase --exact --show-output --nocapture -#[test] -fn test_coinbase_basecase() { - new_test_ext(1).execute_with(|| { - let zero_emission = SubtensorModule::mint_tao(0.into()); - SubtensorModule::run_coinbase(zero_emission); - }); -} - -// Test the emission distribution for a single subnet. -// This test verifies that: -// - Single subnet gets cutoff by lower flow limit, so nothing is distributed -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_tao_issuance_base --exact --show-output --nocapture -#[test] -fn test_coinbase_tao_issuance_base() { - new_test_ext(1).execute_with(|| { - let emission = TaoBalance::from(1_234_567); - let subnet_owner_ck = U256::from(1001); - let subnet_owner_hk = U256::from(1002); - let netuid = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); - // Dynamic subnets register with emission disabled by default. - SubnetEmissionEnabled::::insert(netuid, true); - // Price-based emission shares require a non-zero moving price. - SubnetMovingPrice::::insert(netuid, I96F32::from_num(1)); - // Keep root_proportion ~1 so the injection cap does not bind. - set_full_injection_root_stake(); - let total_issuance_before = TotalIssuance::::get(); - let tao_in_before = SubnetTAO::::get(netuid); - let total_stake_before = TotalStake::::get(); - let emission_credit = SubtensorModule::mint_tao(emission); - SubtensorModule::run_coinbase(emission_credit); - assert_eq!(SubnetTAO::::get(netuid), tao_in_before + emission); - assert_eq!( - TotalIssuance::::get(), - total_issuance_before + emission - ); - assert_eq!(TotalStake::::get(), total_stake_before + emission); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_tao_issuance_base_low --exact --show-output --nocapture -#[test] -fn test_coinbase_tao_issuance_base_low() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let emission = TaoBalance::from(1); - let emission_credit = SubtensorModule::mint_tao(emission); - add_network(netuid, 1, 0); - assert_eq!(SubnetTAO::::get(netuid), TaoBalance::ZERO); - // Set subnet flow to non-zero - SubnetTaoFlow::::insert(netuid, 33433_i64); - SubtensorModule::run_coinbase(emission_credit); - assert_eq!(SubnetTAO::::get(netuid), emission); - assert_eq!(TotalIssuance::::get(), emission); - assert_eq!(TotalStake::::get(), emission); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_tao_issuance_base_low_flow --exact --show-output --nocapture -// #[test] -// fn test_coinbase_tao_issuance_base_low_flow() { -// new_test_ext(1).execute_with(|| { -// let emission = TaoBalance::from(1_234_567); -// let subnet_owner_ck = U256::from(1001); -// let subnet_owner_hk = U256::from(1002); -// let netuid = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); -// let emission = TaoBalance::from(1); - -// // 100% tao flow method -// let block_num = FlowHalfLife::::get(); -// SubnetEmaTaoFlow::::insert(netuid, (block_num, I64F64::from_num(1_000_000_000))); -// System::set_block_number(block_num); - -// let tao_in_before = SubnetTAO::::get(netuid); -// let total_stake_before = TotalStake::::get(); -// SubtensorModule::run_coinbase(U96F32::from_num(emission)); -// assert_eq!(SubnetTAO::::get(netuid), tao_in_before + emission); -// assert_eq!(TotalIssuance::::get(), emission); -// assert_eq!(TotalStake::::get(), total_stake_before + emission); -// }); -// } - -// Test emission distribution across multiple subnets. -// This test verifies that: -// - Multiple subnets receive equal portions of the total emission -// - Each subnet's TAO balance is updated correctly -// - Total issuance and total stake reflect the full emission amount -// - The emission is split evenly between all subnets -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_tao_issuance_multiple --exact --show-output --nocapture -#[test] -fn test_coinbase_tao_issuance_multiple() { - new_test_ext(1).execute_with(|| { - let netuid1 = NetUid::from(1); - let netuid2 = NetUid::from(2); - let netuid3 = NetUid::from(3); - let emission = TaoBalance::from(3_333_333); - let emission_credit = SubtensorModule::mint_tao(emission); - add_network(netuid1, 1, 0); - add_network(netuid2, 1, 0); - add_network(netuid3, 1, 0); - assert_eq!(SubnetTAO::::get(netuid1), TaoBalance::ZERO); - assert_eq!(SubnetTAO::::get(netuid2), TaoBalance::ZERO); - assert_eq!(SubnetTAO::::get(netuid3), TaoBalance::ZERO); - // Set Tao flows to equal and non-zero - SubnetTaoFlow::::insert(netuid1, 100_000_000_i64); - SubnetTaoFlow::::insert(netuid2, 100_000_000_i64); - SubnetTaoFlow::::insert(netuid3, 100_000_000_i64); - SubtensorModule::run_coinbase(emission_credit); - assert_abs_diff_eq!( - SubnetTAO::::get(netuid1), - emission / 3.into(), - epsilon = 1.into(), - ); - assert_abs_diff_eq!( - SubnetTAO::::get(netuid2), - emission / 3.into(), - epsilon = 1.into(), - ); - assert_abs_diff_eq!( - SubnetTAO::::get(netuid3), - emission / 3.into(), - epsilon = 1.into(), - ); - assert_abs_diff_eq!(TotalIssuance::::get(), emission, epsilon = 3.into(),); - assert_abs_diff_eq!(TotalStake::::get(), emission, epsilon = 3.into(),); - }); -} - -#[test] -fn test_coinbase_disabled_subnet_emission_redistributes_tao_to_enabled_subnets() { - new_test_ext(1).execute_with(|| { - let netuid1 = NetUid::from(1); - let netuid2 = NetUid::from(2); - let netuid3 = NetUid::from(3); - let emission = TaoBalance::from(3_333_333); - - add_network(netuid1, 1, 0); - add_network(netuid2, 1, 0); - add_network(netuid3, 1, 0); - - SubnetEmissionEnabled::::insert(netuid2, false); - - SubnetTaoFlow::::insert(netuid1, 100_000_000_i64); - SubnetTaoFlow::::insert(netuid2, 100_000_000_i64); - SubnetTaoFlow::::insert(netuid3, 100_000_000_i64); - - let subnet_emissions = SubtensorModule::get_subnet_block_emissions( - &[netuid1, netuid2, netuid3], - U96F32::saturating_from_num(emission.to_u64()), - ); - - assert_abs_diff_eq!( - subnet_emissions[&netuid1].to_num::(), - (emission.to_u64() / 2) as f64, - epsilon = 2.0, - ); - assert_abs_diff_eq!( - subnet_emissions[&netuid2].to_num::(), - 0.0, - epsilon = 1.0 - ); - assert_abs_diff_eq!( - subnet_emissions[&netuid3].to_num::(), - (emission.to_u64() / 2) as f64, - epsilon = 2.0, - ); - - let (_tao_in, alpha_in, alpha_out, excess_tao) = - SubtensorModule::get_subnet_terms(&subnet_emissions); - assert_eq!(alpha_in[&netuid2], U96F32::from_num(0.0)); - assert_eq!(excess_tao[&netuid2], U96F32::from_num(0.0)); - assert!(alpha_out[&netuid2] > U96F32::from_num(0.0)); - - let total_issuance_before = TotalIssuance::::get(); - let total_stake_before = TotalStake::::get(); - let emission_credit = SubtensorModule::mint_tao(emission); - SubtensorModule::run_coinbase(emission_credit); - - assert_abs_diff_eq!( - SubnetTAO::::get(netuid1), - emission / 2.into(), - epsilon = 2.into(), - ); - assert_eq!(SubnetTAO::::get(netuid2), TaoBalance::ZERO); - assert_abs_diff_eq!( - SubnetTAO::::get(netuid3), - emission / 2.into(), - epsilon = 2.into(), - ); - assert_abs_diff_eq!( - TotalIssuance::::get(), - total_issuance_before + emission, - epsilon = 2.into(), - ); - assert_abs_diff_eq!( - TotalStake::::get(), - total_stake_before + emission, - epsilon = 2.into(), - ); - }); -} - -#[test] -fn test_sudo_set_subnet_emission_enabled_multiple_subnets_multiple_toggles() { - new_test_ext(1).execute_with(|| { - let netuid1 = NetUid::from(1); - let netuid2 = NetUid::from(2); - let netuid3 = NetUid::from(3); - let emission = TaoBalance::from(3_000_000); - - add_network(netuid1, 1, 0); - add_network(netuid2, 1, 0); - add_network(netuid3, 1, 0); - - // Keep root_proportion ~1 so TAO-side emission is injected (populating - // SubnetTaoInEmission) rather than routed entirely to chain buys. - set_full_injection_root_stake(); - - let assert_emission_storage = |expected1: u64, expected2: u64, expected3: u64| { - assert_abs_diff_eq!( - SubnetTaoInEmission::::get(netuid1), - TaoBalance::from(expected1), - epsilon = 2.into(), - ); - assert_abs_diff_eq!( - SubnetTaoInEmission::::get(netuid2), - TaoBalance::from(expected2), - epsilon = 2.into(), - ); - assert_abs_diff_eq!( - SubnetTaoInEmission::::get(netuid3), - TaoBalance::from(expected3), - epsilon = 2.into(), - ); - - assert_eq!( - SubnetAlphaInEmission::::get(netuid1) == AlphaBalance::from(0), - expected1 == 0 - ); - assert_eq!( - SubnetAlphaInEmission::::get(netuid2) == AlphaBalance::from(0), - expected2 == 0 - ); - assert_eq!( - SubnetAlphaInEmission::::get(netuid3) == AlphaBalance::from(0), - expected3 == 0 - ); - - assert!(SubnetAlphaOutEmission::::get(netuid1) > AlphaBalance::from(0)); - assert!(SubnetAlphaOutEmission::::get(netuid2) > AlphaBalance::from(0)); - assert!(SubnetAlphaOutEmission::::get(netuid3) > AlphaBalance::from(0)); - }; - - let run_coinbase = || { - let emission_credit = SubtensorModule::mint_tao(emission); - SubtensorModule::run_coinbase(emission_credit); - }; - - // All enabled: split TAO-side emission equally across all three subnets. - run_coinbase(); - assert_emission_storage(1_000_000, 1_000_000, 1_000_000); - - // Seed stale values and then disable netuid2. The next coinbase run must clear - // netuid2's per-block TAO-side emission storage while preserving alpha_out. - SubnetTaoInEmission::::insert(netuid2, TaoBalance::from(123)); - SubnetAlphaInEmission::::insert(netuid2, AlphaBalance::from(123)); - SubnetExcessTao::::insert(netuid2, TaoBalance::from(123)); - SubnetEmissionEnabled::::insert(netuid2, false); - run_coinbase(); - assert_emission_storage(1_500_000, 0, 1_500_000); - assert_eq!(SubnetExcessTao::::get(netuid2), TaoBalance::from(0)); - - // Toggle a different subnet off and netuid2 back on. - SubnetTaoInEmission::::insert(netuid1, TaoBalance::from(456)); - SubnetAlphaInEmission::::insert(netuid1, AlphaBalance::from(456)); - SubnetExcessTao::::insert(netuid1, TaoBalance::from(456)); - SubnetEmissionEnabled::::insert(netuid1, false); - SubnetEmissionEnabled::::insert(netuid2, true); - run_coinbase(); - assert_emission_storage(0, 1_500_000, 1_500_000); - assert_eq!(SubnetExcessTao::::get(netuid1), TaoBalance::from(0)); - - // Toggle everything back on: TAO-side emission should return to an even split. - SubnetEmissionEnabled::::insert(netuid1, true); - SubnetEmissionEnabled::::insert(netuid2, true); - SubnetEmissionEnabled::::insert(netuid3, true); - run_coinbase(); - assert_emission_storage(1_000_000, 1_000_000, 1_000_000); - }); -} - -// Test emission distribution with different subnet prices. -// This test verifies that: -// - Subnets with different prices receive proportional emission shares -// - A subnet with double the price receives double the emission -// - Total issuance and total stake reflect the full emission amount -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_tao_issuance_different_prices --exact --show-output --nocapture -#[test] -fn test_coinbase_tao_issuance_different_prices() { - new_test_ext(1).execute_with(|| { - let netuid1 = NetUid::from(1); - let netuid2 = NetUid::from(2); - let emission = 100_000_000; - let emission_credit = SubtensorModule::mint_tao(emission.into()); - add_network(netuid1, 1, 0); - add_network(netuid2, 1, 0); - - // Setup prices 0.1 and 0.2 - let initial_tao: u64 = 100_000_u64; - let initial_alpha1: u64 = initial_tao * 10; - let initial_alpha2: u64 = initial_tao * 5; - mock::setup_reserves(netuid1, initial_tao.into(), initial_alpha1.into()); - mock::setup_reserves(netuid2, initial_tao.into(), initial_alpha2.into()); - - // Force the swap to initialize - ::SwapInterface::init_swap(netuid1, None); - ::SwapInterface::init_swap(netuid2, None); - - // Make subnets dynamic. - SubnetMechanism::::insert(netuid1, 1); - SubnetMechanism::::insert(netuid2, 1); - - // Price-based shares: subnet 2 has twice the moving price of subnet 1, - // so it should receive twice the TAO emission. - SubnetMovingPrice::::insert(netuid1, I96F32::from_num(0.1)); - SubnetMovingPrice::::insert(netuid2, I96F32::from_num(0.2)); - // Keep root_proportion ~1 so the injection cap does not bind. - set_full_injection_root_stake(); - - // Assert initial TAO reserves. - assert_eq!(SubnetTAO::::get(netuid1), initial_tao.into()); - assert_eq!(SubnetTAO::::get(netuid2), initial_tao.into()); - - // Run the coinbase with the emission amount. - SubtensorModule::run_coinbase(emission_credit); - - // Assert tao emission is split evenly. - assert_abs_diff_eq!( - SubnetTAO::::get(netuid1), - TaoBalance::from(initial_tao + emission / 3), - epsilon = 10.into(), - ); - assert_abs_diff_eq!( - SubnetTAO::::get(netuid2), - TaoBalance::from(initial_tao + 2 * emission / 3), - epsilon = 10.into(), - ); - - // Prices are low => we limit tao issued (buy alpha with it) - let tao_issued = TaoBalance::from(((1.0) * emission as f64) as u64); - assert_abs_diff_eq!( - TotalIssuance::::get(), - tao_issued, - epsilon = 10.into() - ); - assert_abs_diff_eq!( - TotalStake::::get(), - emission.into(), - epsilon = 10.into() - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_tao_issuance_different_flows --exact --show-output --nocapture -// #[test] -// fn test_coinbase_tao_issuance_different_flows() { -// new_test_ext(1).execute_with(|| { -// let subnet_owner_ck = U256::from(1001); -// let subnet_owner_hk = U256::from(1002); -// let netuid1 = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); -// let netuid2 = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); -// let emission = 100_000_000; - -// // Setup prices 0.1 and 0.2 -// let initial_tao: u64 = 100_000_u64; -// let initial_alpha1: u64 = initial_tao * 10; -// let initial_alpha2: u64 = initial_tao * 5; -// mock::setup_reserves(netuid1, initial_tao.into(), initial_alpha1.into()); -// mock::setup_reserves(netuid2, initial_tao.into(), initial_alpha2.into()); - -// // Force the swap to initialize -// ::SwapInterface::init_swap(netuid1); -// ::SwapInterface::init_swap(netuid2); - -// // Set subnet prices to reversed proportion to ensure they don't affect emissions. -// SubnetMovingPrice::::insert(netuid1, I96F32::from_num(2)); -// SubnetMovingPrice::::insert(netuid2, I96F32::from_num(1)); - -// // Set subnet tao flow ema. -// let block_num = FlowHalfLife::::get(); -// SubnetEmaTaoFlow::::insert(netuid1, (block_num, I64F64::from_num(1))); -// SubnetEmaTaoFlow::::insert(netuid2, (block_num, I64F64::from_num(2))); -// System::set_block_number(block_num); - -// // Set normalization exponent to 1 for simplicity -// FlowNormExponent::::set(U64F64::from(1_u64)); - -// // Assert initial TAO reserves. -// assert_eq!(SubnetTAO::::get(netuid1), initial_tao.into()); -// assert_eq!(SubnetTAO::::get(netuid2), initial_tao.into()); -// let total_stake_before = TotalStake::::get(); - -// // Run the coinbase with the emission amount. -// SubtensorModule::run_coinbase(U96F32::from_num(emission)); - -// // Assert tao emission is split evenly. -// assert_abs_diff_eq!( -// SubnetTAO::::get(netuid1), -// TaoBalance::from(initial_tao + emission / 3), -// epsilon = 10.into(), -// ); -// assert_abs_diff_eq!( -// SubnetTAO::::get(netuid2), -// TaoBalance::from(initial_tao + 2 * emission / 3), -// epsilon = 10.into(), -// ); - -// // Prices are low => we limit tao issued (buy alpha with it) -// let tao_issued = TaoBalance::from(((0.1 + 0.2) * emission as f64) as u64); -// assert_abs_diff_eq!( -// TotalIssuance::::get(), -// tao_issued, -// epsilon = 10.into() -// ); -// assert_abs_diff_eq!( -// TotalStake::::get(), -// total_stake_before + emission.into(), -// epsilon = 10.into() -// ); -// }); -// } - -// Test moving price updates with different alpha values. -// This test verifies that: -// - Moving price stays constant when alpha is 1.0 -// - Moving price converges to real price at expected rate with alpha 0.1 -// - Moving price updates correctly over multiple iterations -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_moving_prices --exact --show-output --nocapture -#[test] -fn test_coinbase_moving_prices() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - // Set price to 1.0 - SubnetTAO::::insert(netuid, TaoBalance::from(1_000_000)); - SubnetAlphaIn::::insert(netuid, AlphaBalance::from(1_000_000)); - SubnetMechanism::::insert(netuid, 1); - SubnetMovingPrice::::insert(netuid, I96F32::from_num(1)); - FirstEmissionBlockNumber::::insert(netuid, 1); - - // Updating the moving price keeps it the same. - assert_eq!( - SubtensorModule::get_moving_alpha_price(netuid), - I96F32::from_num(1) - ); - // Skip some blocks so that EMA price is not slowed down - System::set_block_number(7_200_000); - - SubtensorModule::update_moving_price(netuid); - assert_eq!( - SubtensorModule::get_moving_alpha_price(netuid), - I96F32::from_num(1) - ); - // Check alpha of 1. - // Set price to zero. - SubnetMovingPrice::::insert(netuid, I96F32::from_num(0)); - SubnetMovingAlpha::::set(I96F32::from_num(1.0)); - // Run moving 1 times. - SubtensorModule::update_moving_price(netuid); - // Assert price is ~ 100% of the real price. - assert!(U64F64::from_num(1.0) - SubtensorModule::get_moving_alpha_price(netuid) < 0.05); - // Set price to zero. - SubnetMovingPrice::::insert(netuid, I96F32::from_num(0)); - SubnetMovingAlpha::::set(I96F32::from_num(0.1)); - - // EMA price 28 days after registration - System::set_block_number(7_200 * 28); - - // Run moving 14 times. - for _ in 0..14 { - SubtensorModule::update_moving_price(netuid); - } - - // Assert price is > 50% of the real price. - assert_abs_diff_eq!( - 0.512325, - SubtensorModule::get_moving_alpha_price(netuid).to_num::(), - epsilon = 0.001 - ); - }); -} - -// Test moving price updates slow down at the beginning. -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_update_moving_price_initial --exact --show-output --nocapture -#[test] -fn test_update_moving_price_initial() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - // Set current price to 1.0 - SubnetTAO::::insert(netuid, TaoBalance::from(1_000_000)); - SubnetAlphaIn::::insert(netuid, AlphaBalance::from(1_000_000)); - SubnetMechanism::::insert(netuid, 1); - SubnetMovingAlpha::::set(I96F32::from_num(0.5)); - SubnetMovingPrice::::insert(netuid, I96F32::from_num(0)); - - // Registered recently - System::set_block_number(510); - FirstEmissionBlockNumber::::insert(netuid, 500); - - SubtensorModule::update_moving_price(netuid); - - let new_price = SubnetMovingPrice::::get(netuid); - assert!(new_price.to_num::() < 0.001); - }); -} - -// Test moving price updates slow down at the beginning. -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_update_moving_price_after_time --exact --show-output --nocapture -#[test] -fn test_update_moving_price_after_time() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - // Set current price to 1.0 - SubnetTAO::::insert(netuid, TaoBalance::from(1_000_000)); - SubnetAlphaIn::::insert(netuid, AlphaBalance::from(1_000_000)); - SubnetMechanism::::insert(netuid, 1); - SubnetMovingAlpha::::set(I96F32::from_num(0.5)); - SubnetMovingPrice::::insert(netuid, I96F32::from_num(0)); - - // Registered long time ago - System::set_block_number(144_000_500); - FirstEmissionBlockNumber::::insert(netuid, 500); - - SubtensorModule::update_moving_price(netuid); - - let new_price = SubnetMovingPrice::::get(netuid); - assert!((new_price.to_num::() - 0.5).abs() < 0.001); - }); -} - -// Test basic alpha issuance in coinbase mechanism. -// This test verifies that: -// - Alpha issuance is initialized to 0 for new subnets -// - Alpha issuance is split evenly between subnets during coinbase -// - Each subnet receives the expected fraction of total emission -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_alpha_issuance_base --exact --show-output --nocapture -#[test] -fn test_coinbase_alpha_issuance_base() { - new_test_ext(1).execute_with(|| { - let netuid1 = NetUid::from(1); - let netuid2 = NetUid::from(2); - let emission: u64 = 1_000_000; - let emission_credit = SubtensorModule::mint_tao(emission.into()); - add_network(netuid1, 1, 0); - add_network(netuid2, 1, 0); - // Set up prices 1 and 1 - let initial: u64 = 1_000_000; - SubnetTAO::::insert(netuid1, TaoBalance::from(initial)); - SubnetAlphaIn::::insert(netuid1, AlphaBalance::from(initial)); - SubnetTAO::::insert(netuid2, TaoBalance::from(initial)); - SubnetAlphaIn::::insert(netuid2, AlphaBalance::from(initial)); - // Keep root_proportion ~1 so the injection cap does not bind. - set_full_injection_root_stake(); - // Check initial - SubtensorModule::run_coinbase(emission_credit); - // tao_in = 500_000 - // alpha_in = 500_000/price = 500_000 - assert_eq!( - SubnetAlphaIn::::get(netuid1), - (initial + emission / 2).into() - ); - assert_eq!( - SubnetAlphaIn::::get(netuid2), - (initial + emission / 2).into() - ); - }); -} - -// Test alpha issuance with different subnet flows. -// This test verifies that: -// - Alpha issuance is proportional to subnet flows -// - Higher priced subnets receive more TAO emission -// - Alpha issuance is correctly calculated based on price ratios -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_alpha_issuance_different --exact --show-output --nocapture -#[test] -fn test_coinbase_alpha_issuance_different() { - new_test_ext(1).execute_with(|| { - let netuid1 = NetUid::from(1); - let netuid2 = NetUid::from(2); - let emission: u64 = 1_000_000; - let emission_credit = SubtensorModule::mint_tao(emission.into()); - add_network(netuid1, 1, 0); - add_network(netuid2, 1, 0); - // Make subnets dynamic. - SubnetMechanism::::insert(netuid1, 1); - SubnetMechanism::::insert(netuid2, 1); - // Setup prices 1 and 2 - let initial: u64 = 1_000_000; - SubnetTAO::::insert(netuid1, TaoBalance::from(initial)); - SubnetAlphaIn::::insert(netuid1, AlphaBalance::from(initial)); - SubnetTAO::::insert(netuid2, TaoBalance::from(2 * initial)); - SubnetAlphaIn::::insert(netuid2, AlphaBalance::from(initial)); - // Price-based shares with prices 1 and 2 (1:2 ratio). - SubnetMovingPrice::::insert(netuid1, I96F32::from_num(1)); - SubnetMovingPrice::::insert(netuid2, I96F32::from_num(2)); - // Keep root_proportion ~1 so the injection cap does not bind. - set_full_injection_root_stake(); - // Run coinbase - SubtensorModule::run_coinbase(emission_credit); - // tao_in = 333_333 - // alpha_in = 333_333/price = 333_333 + initial - assert_eq!( - SubnetAlphaIn::::get(netuid1), - (initial + emission / 3).into() - ); - // tao_in = 666_666 - // alpha_in = 666_666/price = 333_333 + initial - assert_eq!( - SubnetAlphaIn::::get(netuid2), - (initial + (emission * 2 / 3) / 2).into() - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_alpha_issuance_with_cap_trigger --exact --show-output --nocapture -#[test] -fn test_coinbase_alpha_issuance_with_cap_trigger() { - new_test_ext(1).execute_with(|| { - let netuid1 = NetUid::from(1); - let netuid2 = NetUid::from(2); - let emission: u64 = 1_000_000; - let emission_credit = SubtensorModule::mint_tao(emission.into()); - add_network(netuid1, 1, 0); - add_network(netuid2, 1, 0); - // Make subnets dynamic. - SubnetMechanism::::insert(netuid1, 1); - SubnetMechanism::::insert(netuid2, 1); - // Setup prices 1000000 - let initial: u64 = 1_000; - let initial_alpha: u64 = initial * 1000000; - SubnetTAO::::insert(netuid1, TaoBalance::from(initial)); - SubnetAlphaIn::::insert(netuid1, AlphaBalance::from(initial_alpha)); // Make price extremely low. - SubnetTAO::::insert(netuid2, TaoBalance::from(initial)); - SubnetAlphaIn::::insert(netuid2, AlphaBalance::from(initial_alpha)); // Make price extremely low. - // Set subnet prices. - SubnetMovingPrice::::insert(netuid1, I96F32::from_num(1)); - SubnetMovingPrice::::insert(netuid2, I96F32::from_num(2)); - // Keep root_proportion ~1 so the injection cap binds at alpha_emission. - set_full_injection_root_stake(); - // Run coinbase - SubtensorModule::run_coinbase(emission_credit); - // alpha_in is capped at the injection cap, so injected alpha stays below - // a full block emission on top of the initial reserve. - assert!(SubnetAlphaIn::::get(netuid1) < (initial_alpha + 1_000_000_000).into()); - // Per-block alpha emission is the full block emission regardless of the cap. - assert_eq!( - SubnetAlphaOutEmission::::get(netuid1), - 1_000_000_000.into() - ); - assert!(SubnetAlphaIn::::get(netuid2) < (initial_alpha + 1_000_000_000).into()); - assert_eq!( - SubnetAlphaOutEmission::::get(netuid2), - 1_000_000_000.into() - ); // Gets full block emission. - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_alpha_issuance_with_cap_trigger_and_block_emission --exact --show-output --nocapture -#[test] -fn test_coinbase_alpha_issuance_with_cap_trigger_and_block_emission() { - new_test_ext(1).execute_with(|| { - let netuid1 = NetUid::from(1); - let netuid2 = NetUid::from(2); - let emission: u64 = 1_000_000; - let emission_credit = SubtensorModule::mint_tao(emission.into()); - add_network(netuid1, 1, 0); - add_network(netuid2, 1, 0); - - // Make subnets dynamic. - SubnetMechanism::::insert(netuid1, 1); - SubnetMechanism::::insert(netuid2, 1); - - // Setup prices 0.000001 - let initial_tao: u64 = 10_000_u64; - let initial_alpha: u64 = initial_tao * 100_000_u64; - mock::setup_reserves(netuid1, initial_tao.into(), initial_alpha.into()); - mock::setup_reserves(netuid2, initial_tao.into(), initial_alpha.into()); - - // Enable emission - FirstEmissionBlockNumber::::insert(netuid1, 0); - FirstEmissionBlockNumber::::insert(netuid2, 0); - // Price-based shares (1:2 ratio). Low pool prices mean alpha_in exceeds the - // injection cap, so the surplus TAO is spent on chain buys. - SubnetMovingPrice::::insert(netuid1, I96F32::from_num(1)); - SubnetMovingPrice::::insert(netuid2, I96F32::from_num(2)); - - // Force the swap to initialize - ::SwapInterface::init_swap(netuid1, None); - ::SwapInterface::init_swap(netuid2, None); - - // Get the prices before the run_coinbase - let price_1_before = ::SwapInterface::current_alpha_price(netuid1); - let price_2_before = ::SwapInterface::current_alpha_price(netuid2); - - // Set issuance at 21M - SubnetAlphaOut::::insert(netuid1, AlphaBalance::from(21_000_000_000_000_000_u64)); // Set issuance above 21M - SubnetAlphaOut::::insert(netuid2, AlphaBalance::from(21_000_000_000_000_000_u64)); // Set issuance above 21M - - // Run coinbase - SubtensorModule::run_coinbase(emission_credit); - - // New behavior: chain-bought alpha is cached instead of recycled. - // The cached amount remains part of outstanding alpha supply. - assert!( - !SubnetProtocolAlpha::::get(netuid1).is_zero() - || !SubnetProtocolAlpha::::get(netuid2).is_zero() - ); - - // Get the prices after the run_coinbase - let price_1_after = ::SwapInterface::current_alpha_price(netuid1); - let price_2_after = ::SwapInterface::current_alpha_price(netuid2); - - // AlphaIn gets decreased beacuse of a buy - assert!(u64::from(SubnetAlphaIn::::get(netuid1)) < initial_alpha); - assert_eq!( - u64::from(SubnetAlphaOut::::get(netuid2)), - 21_000_000_000_000_000_u64 - .saturating_add(u64::from(SubnetProtocolAlpha::::get(netuid2))) - ); - assert!(u64::from(SubnetAlphaIn::::get(netuid2)) < initial_alpha); - assert_eq!( - u64::from(SubnetAlphaOut::::get(netuid2)), - 21_000_000_000_000_000_u64 - .saturating_add(u64::from(SubnetProtocolAlpha::::get(netuid2))) - ); - - assert!(price_1_after > price_1_before); - assert!(price_2_after > price_2_before); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_owner_cut_base --exact --show-output --nocapture -#[test] -fn test_owner_cut_base() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - mock::setup_reserves( - netuid, - 1_000_000_000_000_u64.into(), - 1_000_000_000_000_u64.into(), - ); - SubtensorModule::set_tempo_unchecked(netuid, 10000); // Large number (dont drain) - SubtensorModule::set_subnet_owner_cut(0); - SubtensorModule::run_coinbase(SubtensorModule::mint_tao(0.into())); - assert_eq!(PendingOwnerCut::::get(netuid), 0.into()); // No cut - SubtensorModule::set_subnet_owner_cut(u16::MAX); - SubtensorModule::run_coinbase(SubtensorModule::mint_tao(0.into())); - assert_eq!(PendingOwnerCut::::get(netuid), 1_000_000_000.into()); // Full cut. - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_pending_emission --exact --show-output --nocapture -#[test] -fn test_pending_emission() { - new_test_ext(1).execute_with(|| { - let hotkey = U256::from(1); - let coldkey = U256::from(2); - let netuid = add_dynamic_network(&hotkey, &coldkey); - remove_owner_registration_stake(netuid); - Tempo::::insert(netuid, 1); - FirstEmissionBlockNumber::::insert(netuid, 0); - - mock::setup_reserves(netuid, 1_000_000.into(), 1.into()); - LastEpochBlock::::insert(netuid, 0); - System::set_block_number(10); - SubtensorModule::run_coinbase(SubtensorModule::mint_tao(0.into())); - SubnetTAO::::insert(NetUid::ROOT, TaoBalance::from(1_000_000_000)); // Add root weight. - System::set_block_number(12); - SubtensorModule::run_coinbase(SubtensorModule::mint_tao(0.into())); - SubtensorModule::set_tempo_unchecked(netuid, 10000); // Large number (dont drain) - SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 - - // Set moving price > 1.0 - SubnetMovingPrice::::insert(netuid, I96F32::from_num(2)); - - // Make sure we are root selling, so we have root alpha divs. - let root_sell_flag = SubtensorModule::get_network_root_sell_flag(&[netuid]); - assert!(root_sell_flag, "Root sell flag should be true"); - - SubtensorModule::run_coinbase(SubtensorModule::mint_tao(0.into())); - // 1 TAO / ( 1 + 3 ) = 0.25 * 1 / 2 = 125000000 - - assert_abs_diff_eq!( - u64::from(PendingServerEmission::::get(netuid)), - 500_000_000, - epsilon = 1 - ); // 1 / 2. - - assert_abs_diff_eq!( - u64::from(PendingValidatorEmission::::get(netuid)), - 500_000_000 - 125000000, - epsilon = 1 - ); // 1 / 2 - swapped. - - assert_abs_diff_eq!( - u64::from(PendingRootAlphaDivs::::get(netuid)), - 125000000, - epsilon = 1 - ); // 1 / 2 * 0.25 --> (from root_prop) - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_drain_base --exact --show-output --nocapture -#[test] -fn test_drain_base() { - new_test_ext(1).execute_with(|| { - SubtensorModule::distribute_emission( - 0.into(), - AlphaBalance::ZERO, - AlphaBalance::ZERO, - AlphaBalance::ZERO, - AlphaBalance::ZERO, - ) - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_drain_base_with_subnet --exact --show-output --nocapture -#[test] -fn test_drain_base_with_subnet() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - SubtensorModule::distribute_emission( - netuid, - AlphaBalance::ZERO, - AlphaBalance::ZERO, - AlphaBalance::ZERO, - AlphaBalance::ZERO, - ) - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_drain_base_with_subnet_with_single_staker_not_registered --exact --show-output --nocapture -#[test] -fn test_drain_base_with_subnet_with_single_staker_not_registered() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - let hotkey = U256::from(1); - let coldkey = U256::from(2); - let stake_before = AlphaBalance::from(1_000_000_000); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - netuid, - stake_before, - ); - let pending_alpha = AlphaBalance::from(1_000_000_000); - SubtensorModule::distribute_emission( - netuid, - pending_alpha.saturating_div(2.into()).into(), - pending_alpha.saturating_div(2.into()).into(), - AlphaBalance::ZERO, - AlphaBalance::ZERO, - ); - let stake_after = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); - assert_eq!(stake_before, stake_after); // Not registered. - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_drain_base_with_subnet_with_single_staker_registered --exact --show-output --nocapture -#[test] -fn test_drain_base_with_subnet_with_single_staker_registered() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - let hotkey = U256::from(1); - let coldkey = U256::from(2); - let stake_before = AlphaBalance::from(1_000_000_000); - register_ok_neuron(netuid, hotkey, coldkey, 0); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - netuid, - stake_before, - ); - let pending_alpha = AlphaBalance::from(1_000_000_000); - SubtensorModule::distribute_emission( - netuid, - pending_alpha.saturating_div(2.into()).into(), - pending_alpha.saturating_div(2.into()).into(), - AlphaBalance::ZERO, - AlphaBalance::ZERO, - ); - let stake_after = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); - close( - (stake_before + pending_alpha).into(), - stake_after.into(), - 10, - ); // Registered gets all emission. - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_drain_base_with_subnet_with_single_staker_registered_root_weight --exact --show-output --nocapture -#[test] -fn test_drain_base_with_subnet_with_single_staker_registered_root_weight() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - let hotkey = U256::from(1); - let coldkey = U256::from(2); - let stake_before = AlphaBalance::from(1_000_000_000); - // register_ok_neuron(root, hotkey, coldkey, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - Delegates::::insert(hotkey, PerU16::zero()); - SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - NetUid::ROOT, - stake_before, - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - netuid, - stake_before, - ); - let pending_alpha = AlphaBalance::from(1_000_000_000); - let pending_root_alpha = AlphaBalance::from(1_000_000_000); - assert_eq!(SubnetTAO::::get(NetUid::ROOT), TaoBalance::ZERO); - SubtensorModule::distribute_emission( - netuid, - pending_alpha.saturating_div(2.into()).into(), - pending_alpha.saturating_div(2.into()).into(), - pending_root_alpha, - AlphaBalance::ZERO, - ); - let stake_after = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); - let root_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - NetUid::ROOT, - ); - close( - (stake_before + pending_alpha).into(), - stake_after.into(), - 10, - ); // Registered gets all alpha emission. - close(stake_before.to_u64(), root_after.into(), 10); // Registered doesn't get tao immediately - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_drain_base_with_subnet_with_two_stakers_registered --exact --show-output --nocapture -#[test] -fn test_drain_base_with_subnet_with_two_stakers_registered() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - let hotkey1 = U256::from(1); - let hotkey2 = U256::from(2); - let coldkey = U256::from(3); - let stake_before = AlphaBalance::from(1_000_000_000); - register_ok_neuron(netuid, hotkey1, coldkey, 0); - register_ok_neuron(netuid, hotkey2, coldkey, 0); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey1, - &coldkey, - netuid, - stake_before, - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey2, - &coldkey, - netuid, - stake_before, - ); - let pending_alpha = AlphaBalance::from(1_000_000_000); - SubtensorModule::distribute_emission( - netuid, - pending_alpha.saturating_div(2.into()).into(), - pending_alpha.saturating_div(2.into()).into(), - AlphaBalance::ZERO, - AlphaBalance::ZERO, - ); - let stake_after1 = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey1, &coldkey, netuid); - let stake_after2 = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey2, &coldkey, netuid); - close( - (stake_before + pending_alpha / 2.into()).into(), - stake_after1.into(), - 10, - ); // Registered gets 1/2 emission - close( - (stake_before + pending_alpha / 2.into()).into(), - stake_after2.into(), - 10, - ); // Registered gets 1/2 emission. - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_drain_base_with_subnet_with_two_stakers_registered_and_root --exact --show-output --nocapture -#[test] -fn test_drain_base_with_subnet_with_two_stakers_registered_and_root() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - let hotkey1 = U256::from(1); - let hotkey2 = U256::from(2); - let coldkey = U256::from(3); - let stake_before = AlphaBalance::from(1_000_000_000); - register_ok_neuron(netuid, hotkey1, coldkey, 0); - register_ok_neuron(netuid, hotkey2, coldkey, 0); - Delegates::::insert(hotkey1, PerU16::zero()); - Delegates::::insert(hotkey2, PerU16::zero()); - SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey1, - &coldkey, - netuid, - stake_before, - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey1, - &coldkey, - NetUid::ROOT, - stake_before, - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey2, - &coldkey, - netuid, - stake_before, - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey2, - &coldkey, - NetUid::ROOT, - stake_before, - ); - let pending_tao = TaoBalance::from(1_000_000_000); - let pending_alpha = AlphaBalance::from(1_000_000_000); - assert_eq!(SubnetTAO::::get(NetUid::ROOT), TaoBalance::ZERO); - SubtensorModule::distribute_emission( - netuid, - pending_alpha.saturating_div(2.into()).into(), - pending_alpha.saturating_div(2.into()).into(), - AlphaBalance::ZERO, - AlphaBalance::ZERO, - ); - let stake_after1 = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey1, &coldkey, netuid); - let root_after1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey1, - &coldkey, - NetUid::ROOT, - ); - let stake_after2 = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey2, &coldkey, netuid); - let root_after2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey2, - &coldkey, - NetUid::ROOT, - ); - close( - (stake_before + pending_alpha / 2.into()).into(), - stake_after1.into(), - 10, - ); // Registered gets 1/2 emission - close( - (stake_before + pending_alpha / 2.into()).into(), - stake_after2.into(), - 10, - ); // Registered gets 1/2 emission. - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_drain_base_with_subnet_with_two_stakers_registered_and_root_different_amounts --exact --show-output --nocapture -#[test] -fn test_drain_base_with_subnet_with_two_stakers_registered_and_root_different_amounts() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - let hotkey1 = U256::from(1); - let hotkey2 = U256::from(2); - let coldkey = U256::from(3); - let stake_before = AlphaBalance::from(1_000_000_000); - Delegates::::insert(hotkey1, PerU16::zero()); - Delegates::::insert(hotkey2, PerU16::zero()); - register_ok_neuron(netuid, hotkey1, coldkey, 0); - register_ok_neuron(netuid, hotkey2, coldkey, 0); - SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey1, - &coldkey, - netuid, - stake_before, - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey1, - &coldkey, - NetUid::ROOT, - stake_before * 2.into(), // Hotkey 1 has twice as much root weight. - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey2, - &coldkey, - netuid, - stake_before, - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey2, - &coldkey, - NetUid::ROOT, - stake_before, - ); - let pending_tao = TaoBalance::from(1_000_000_000); - let pending_alpha = AlphaBalance::from(1_000_000_000); - assert_eq!(SubnetTAO::::get(NetUid::ROOT), TaoBalance::ZERO); - SubtensorModule::distribute_emission( - netuid, - pending_alpha.saturating_div(2.into()).into(), - pending_alpha.saturating_div(2.into()).into(), - AlphaBalance::ZERO, - AlphaBalance::ZERO, - ); - let stake_after1 = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey1, &coldkey, netuid); - let root_after1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey1, - &coldkey, - NetUid::ROOT, - ); - let stake_after2 = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey2, &coldkey, netuid); - let root_after2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey2, - &coldkey, - NetUid::ROOT, - ); - let expected_stake = I96F32::from_num(stake_before) - + (I96F32::from_num(pending_alpha) * I96F32::from_num(1.0 / 2.0)); - assert_abs_diff_eq!( - expected_stake.to_num::(), - stake_after1.into(), - epsilon = 10 - ); // Registered gets 50% of alpha emission - let expected_stake2 = I96F32::from_num(stake_before) - + I96F32::from_num(pending_alpha) * I96F32::from_num(1.0 / 2.0); - assert_abs_diff_eq!( - expected_stake2.to_num::(), - stake_after2.into(), - epsilon = 10 - ); // Registered gets 50% emission - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_drain_base_with_subnet_with_two_stakers_registered_and_root_different_amounts_half_tao_weight --exact --show-output --nocapture -#[test] -fn test_drain_base_with_subnet_with_two_stakers_registered_and_root_different_amounts_half_tao_weight() - { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - let hotkey1 = U256::from(1); - let hotkey2 = U256::from(2); - let coldkey = U256::from(3); - let stake_before = AlphaBalance::from(1_000_000_000); - Delegates::::insert(hotkey1, PerU16::zero()); - Delegates::::insert(hotkey2, PerU16::zero()); - register_ok_neuron(netuid, hotkey1, coldkey, 0); - register_ok_neuron(netuid, hotkey2, coldkey, 0); - SubtensorModule::set_tao_weight(u64::MAX / 2); // Set TAO weight to 0.5 - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey1, - &coldkey, - netuid, - stake_before, - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey1, - &coldkey, - NetUid::ROOT, - stake_before * 2.into(), // Hotkey 1 has twice as much root weight. - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey2, - &coldkey, - netuid, - stake_before, - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey2, - &coldkey, - NetUid::ROOT, - stake_before, - ); - let pending_tao = TaoBalance::from(1_000_000_000); - let pending_alpha = AlphaBalance::from(1_000_000_000); - assert_eq!(SubnetTAO::::get(NetUid::ROOT), TaoBalance::ZERO); - SubtensorModule::distribute_emission( - netuid, - pending_alpha.saturating_div(2.into()).into(), - pending_alpha.saturating_div(2.into()).into(), - AlphaBalance::ZERO, - AlphaBalance::ZERO, - ); - let stake_after1 = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey1, &coldkey, netuid); - let root_after1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey1, - &coldkey, - NetUid::ROOT, - ); - let stake_after2 = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey2, &coldkey, netuid); - let root_after2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey2, - &coldkey, - NetUid::ROOT, - ); - let expected_stake = I96F32::from_num(stake_before) - + I96F32::from_num(pending_alpha) * I96F32::from_num(1.0 / 2.0); - assert_abs_diff_eq!( - expected_stake.to_num::(), - u64::from(stake_after1), - epsilon = 10 - ); - let expected_stake2 = I96F32::from_num(stake_before) - + I96F32::from_num(pending_alpha) * I96F32::from_num(1.0 / 2.0); - assert_abs_diff_eq!( - expected_stake2.to_num::(), - u64::from(stake_after2), - epsilon = 10 - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_drain_alpha_childkey_parentkey --exact --show-output --nocapture -#[test] -fn test_drain_alpha_childkey_parentkey() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - SubtensorModule::set_ck_burn(0); - let parent = U256::from(1); - let child = U256::from(2); - let coldkey = U256::from(3); - let stake_before = AlphaBalance::from(1_000_000_000); - register_ok_neuron(netuid, child, coldkey, 0); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &parent, - &coldkey, - netuid, - stake_before, - ); - mock_set_children_no_epochs(netuid, &parent, &[(u64::MAX, child)]); - - // Childkey take is 10% - ChildkeyTake::::insert(child, netuid, PerU16::from_parts(u16::MAX / 10)); - - let pending_alpha = AlphaBalance::from(1_000_000_000); - SubtensorModule::distribute_emission( - netuid, - pending_alpha.saturating_div(2.into()).into(), - pending_alpha.saturating_div(2.into()).into(), - AlphaBalance::ZERO, - AlphaBalance::ZERO, - ); - let parent_stake_after = SubtensorModule::get_stake_for_hotkey_on_subnet(&parent, netuid); - let child_stake_after = SubtensorModule::get_stake_for_hotkey_on_subnet(&child, netuid); - - // Child gets 10%, parent gets 90% - let expected = I96F32::from_num(stake_before) - + I96F32::from_num(pending_alpha) * I96F32::from_num(9.0 / 10.0); - log::info!( - "expected: {:?}, parent_stake_after: {:?}", - expected.to_num::(), - parent_stake_after - ); - close(expected.to_num::(), parent_stake_after.into(), 10_000); - let expected = I96F32::from_num(u64::from(pending_alpha)) / I96F32::from_num(10); - close(expected.to_num::(), child_stake_after.into(), 10_000); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_get_root_children --exact --show-output --nocapture -#[test] -fn test_get_root_children() { - new_test_ext(1).execute_with(|| { - // Init netuid 1 - let alpha = NetUid::from(1); - add_network(NetUid::ROOT, 1, 0); - add_network(alpha, 1, 0); - - // Set TAO weight to 1. - SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1. - - // Create keys. - let cold = U256::from(0); - let alice = U256::from(1); - let bob = U256::from(2); - - // Register Alice and Bob to the root network and alpha subnet. - register_ok_neuron(alpha, alice, cold, 0); - register_ok_neuron(alpha, bob, cold, 0); - assert_ok!(SubtensorModule::root_register( - RuntimeOrigin::signed(cold).clone(), - alice, - )); - assert_ok!(SubtensorModule::root_register( - RuntimeOrigin::signed(cold).clone(), - bob, - )); - - // Add stake for Alice and Bob on root. - let alice_root_stake = AlphaBalance::from(1_000_000_000); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &alice, - &cold, - NetUid::ROOT, - alice_root_stake, - ); - let bob_root_stake = AlphaBalance::from(1_000_000_000); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &bob, - &cold, - NetUid::ROOT, - alice_root_stake, - ); - - // Add stake for Alice and Bob on netuid. - let alice_alpha_stake = AlphaBalance::from(1_000_000_000); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &alice, - &cold, - alpha, - alice_alpha_stake, - ); - let bob_alpha_stake = AlphaBalance::from(1_000_000_000); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &bob, - &cold, - alpha, - bob_alpha_stake, - ); - - // Set Bob as 100% child of Alice on root. - // mock_set_children_no_epochs( NetUid::ROOT, &alice, &[(u64::MAX, bob)]); - mock_set_children_no_epochs(alpha, &alice, &[(u64::MAX, bob)]); - - // Assert Alice and Bob stake on root and netuid - assert_eq!( - SubtensorModule::get_stake_for_hotkey_on_subnet(&alice, NetUid::ROOT), - alice_root_stake - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_on_subnet(&bob, NetUid::ROOT), - bob_root_stake - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_on_subnet(&alice, alpha), - alice_alpha_stake - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_on_subnet(&bob, alpha), - bob_alpha_stake - ); - - // Assert Alice and Bob inherited stakes - assert_eq!( - SubtensorModule::get_inherited_for_hotkey_on_subnet(&alice, NetUid::ROOT), - alice_root_stake - ); - assert_eq!( - SubtensorModule::get_inherited_for_hotkey_on_subnet(&alice, alpha), - 0.into() - ); - assert_eq!( - SubtensorModule::get_inherited_for_hotkey_on_subnet(&bob, NetUid::ROOT), - bob_root_stake - ); - assert_eq!( - SubtensorModule::get_inherited_for_hotkey_on_subnet(&bob, alpha), - bob_alpha_stake + alice_alpha_stake - ); - - // Assert Alice and Bob TAO inherited stakes - assert_eq!( - SubtensorModule::get_tao_inherited_for_hotkey_on_subnet(&alice, alpha), - TaoBalance::ZERO - ); - assert_eq!( - SubtensorModule::get_tao_inherited_for_hotkey_on_subnet(&bob, alpha), - u64::from(bob_root_stake + alice_root_stake).into() - ); - - // Get Alice stake amounts on subnet alpha. - let (alice_total, alice_alpha, alice_tao): (I64F64, I64F64, I64F64) = - SubtensorModule::get_stake_weights_for_hotkey_on_subnet(&alice, alpha); - assert_eq!(alice_total, I64F64::from_num(0)); - - // Get Bob stake amounts on subnet alpha. - let (bob_total, bob_alpha, bob_tao): (I64F64, I64F64, I64F64) = - SubtensorModule::get_stake_weights_for_hotkey_on_subnet(&bob, alpha); - assert_eq!( - bob_total, - I64F64::from_num(u64::from(bob_root_stake * 4.into())) - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_get_root_children_drain --exact --show-output --nocapture -#[test] -fn test_get_root_children_drain() { - new_test_ext(1).execute_with(|| { - // Init netuid 1 - let alpha = NetUid::from(1); - add_network(NetUid::ROOT, 1, 0); - add_network(alpha, 1, 0); - SubtensorModule::set_ck_burn(0); - // Set TAO weight to 1. - SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1. - // Create keys. - let cold_alice = U256::from(0); - let cold_bob = U256::from(1); - let alice = U256::from(2); - let bob = U256::from(3); - // Register Alice and Bob to the root network and alpha subnet. - register_ok_neuron(alpha, alice, cold_alice, 0); - register_ok_neuron(alpha, bob, cold_bob, 0); - assert_ok!(SubtensorModule::root_register( - RuntimeOrigin::signed(cold_alice).clone(), - alice, - )); - assert_ok!(SubtensorModule::root_register( - RuntimeOrigin::signed(cold_bob).clone(), - bob, - )); - // Add stake for Alice and Bob on root. - let alice_root_stake = 1_000_000_000; - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &alice, - &cold_alice, - NetUid::ROOT, - alice_root_stake.into(), - ); - let bob_root_stake = 1_000_000_000; - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &bob, - &cold_bob, - NetUid::ROOT, - bob_root_stake.into(), - ); - // Add stake for Alice and Bob on netuid. - let alice_alpha_stake = AlphaBalance::from(1_000_000_000); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &alice, - &cold_alice, - alpha, - alice_alpha_stake, - ); - let bob_alpha_stake = AlphaBalance::from(1_000_000_000); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &bob, - &cold_bob, - alpha, - bob_alpha_stake, - ); - // Set Bob as 100% child of Alice on root. - mock_set_children_no_epochs(alpha, &alice, &[(u64::MAX, bob)]); - // Set Bob childkey take to zero. - ChildkeyTake::::insert(bob, alpha, PerU16::zero()); - Delegates::::insert(alice, PerU16::zero()); - Delegates::::insert(bob, PerU16::zero()); - - // Get Alice stake amounts on subnet alpha. - let (alice_total, alice_alpha, alice_tao): (I64F64, I64F64, I64F64) = - SubtensorModule::get_stake_weights_for_hotkey_on_subnet(&alice, alpha); - assert_eq!(alice_total, I64F64::from_num(0)); - - // Get Bob stake amounts on subnet alpha. - let (bob_total, bob_alpha, bob_tao): (I64F64, I64F64, I64F64) = - SubtensorModule::get_stake_weights_for_hotkey_on_subnet(&bob, alpha); - assert_eq!(bob_total, I64F64::from_num(4_u64 * bob_root_stake)); - - // Lets drain - let pending_alpha = AlphaBalance::from(1_000_000_000); - SubtensorModule::distribute_emission( - alpha, - pending_alpha.saturating_div(2.into()).into(), - pending_alpha.saturating_div(2.into()).into(), - AlphaBalance::ZERO, - AlphaBalance::ZERO, - ); - - // Alice and Bob both made half of the dividends. - assert_eq!( - SubtensorModule::get_stake_for_hotkey_on_subnet(&alice, alpha), - alice_alpha_stake + pending_alpha / 2.into() - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_on_subnet(&bob, alpha), - bob_alpha_stake + pending_alpha / 2.into() - ); - - // There should be no TAO on the root subnet. - assert_eq!(SubnetTAO::::get(NetUid::ROOT), TaoBalance::ZERO); - - // Lets drain - let pending_alpha = AlphaBalance::from(1_000_000_000); - let pending_root1 = TaoBalance::from(1_000_000_000); - SubtensorModule::distribute_emission( - alpha, - pending_alpha.saturating_div(2.into()).into(), - pending_alpha.saturating_div(2.into()).into(), - AlphaBalance::ZERO, - AlphaBalance::ZERO, - ); - - // Alice and Bob both made half of the dividends. - assert_eq!( - SubtensorModule::get_stake_for_hotkey_on_subnet(&alice, NetUid::ROOT), - AlphaBalance::from(alice_root_stake) - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_on_subnet(&bob, NetUid::ROOT), - AlphaBalance::from(bob_root_stake) - ); - - // Lets change the take value. (Bob is greedy.) - ChildkeyTake::::insert(bob, alpha, PerU16::from_parts(u16::MAX)); - - // Lets drain - let pending_alpha = AlphaBalance::from(1_000_000_000); - let pending_root2 = TaoBalance::from(1_000_000_000); - SubtensorModule::distribute_emission( - alpha, - pending_alpha.saturating_div(2.into()).into(), - pending_alpha.saturating_div(2.into()).into(), - AlphaBalance::ZERO, - AlphaBalance::ZERO, - ); - - // Alice makes nothing - assert_eq!( - AlphaDividendsPerSubnet::::get(alpha, alice), - AlphaBalance::ZERO - ); - // Bob makes it all. - assert_abs_diff_eq!( - AlphaDividendsPerSubnet::::get(alpha, bob), - pending_alpha, - epsilon = 1.into() - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_get_root_children_drain_half_proportion --exact --show-output --nocapture -#[test] -fn test_get_root_children_drain_half_proportion() { - new_test_ext(1).execute_with(|| { - // Init netuid 1 - let alpha = NetUid::from(1); - add_network(NetUid::ROOT, 1, 0); - add_network(alpha, 1, 0); - SubtensorModule::set_ck_burn(0); - // Set TAO weight to 1. - SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1. - // Create keys. - let cold_alice = U256::from(0); - let cold_bob = U256::from(1); - let alice = U256::from(2); - let bob = U256::from(3); - // Register Alice and Bob to the root network and alpha subnet. - register_ok_neuron(alpha, alice, cold_alice, 0); - register_ok_neuron(alpha, bob, cold_bob, 0); - assert_ok!(SubtensorModule::root_register( - RuntimeOrigin::signed(cold_alice).clone(), - alice, - )); - assert_ok!(SubtensorModule::root_register( - RuntimeOrigin::signed(cold_bob).clone(), - bob, - )); - // Add stake for Alice and Bob on root. - let alice_root_stake = AlphaBalance::from(1_000_000_000); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &alice, - &cold_alice, - NetUid::ROOT, - alice_root_stake, - ); - let bob_root_stake = AlphaBalance::from(1_000_000_000); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &bob, - &cold_bob, - NetUid::ROOT, - alice_root_stake, - ); - // Add stake for Alice and Bob on netuid. - let alice_alpha_stake = AlphaBalance::from(1_000_000_000); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &alice, - &cold_alice, - alpha, - alice_alpha_stake, - ); - let bob_alpha_stake = AlphaBalance::from(1_000_000_000); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &bob, - &cold_bob, - alpha, - bob_alpha_stake, - ); - // Set Bob as 100% child of Alice on root. - mock_set_children_no_epochs(alpha, &alice, &[(u64::MAX / 2, bob)]); - - // Set Bob childkey take to zero. - ChildkeyTake::::insert(bob, alpha, PerU16::zero()); - Delegates::::insert(alice, PerU16::zero()); - Delegates::::insert(bob, PerU16::zero()); - - // Lets drain! - let pending_alpha = AlphaBalance::from(1_000_000_000); - SubtensorModule::distribute_emission( - alpha, - pending_alpha.saturating_div(2.into()).into(), - pending_alpha.saturating_div(2.into()).into(), - AlphaBalance::ZERO, - AlphaBalance::ZERO, - ); - - // Alice and Bob make the same amount. - close( - AlphaDividendsPerSubnet::::get(alpha, alice).into(), - (pending_alpha / 2.into()).into(), - 10, - ); - close( - AlphaDividendsPerSubnet::::get(alpha, bob).into(), - (pending_alpha / 2.into()).into(), - 10, - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_get_root_children_drain_with_take --exact --show-output --nocapture -#[test] -fn test_get_root_children_drain_with_take() { - new_test_ext(1).execute_with(|| { - // Init netuid 1 - let alpha = NetUid::from(1); - add_network(NetUid::ROOT, 1, 0); - add_network(alpha, 1, 0); - // Set TAO weight to 1. - SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1. - // Create keys. - let cold_alice = U256::from(0); - let cold_bob = U256::from(1); - let alice = U256::from(2); - let bob = U256::from(3); - // Register Alice and Bob to the root network and alpha subnet. - register_ok_neuron(alpha, alice, cold_alice, 0); - register_ok_neuron(alpha, bob, cold_bob, 0); - assert_ok!(SubtensorModule::root_register( - RuntimeOrigin::signed(cold_alice).clone(), - alice, - )); - assert_ok!(SubtensorModule::root_register( - RuntimeOrigin::signed(cold_bob).clone(), - bob, - )); - // Add stake for Alice and Bob on root. - let alice_root_stake = AlphaBalance::from(1_000_000_000); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &alice, - &cold_alice, - NetUid::ROOT, - alice_root_stake, - ); - let bob_root_stake = AlphaBalance::from(1_000_000_000); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &bob, - &cold_bob, - NetUid::ROOT, - alice_root_stake, - ); - // Add stake for Alice and Bob on netuid. - let alice_alpha_stake = AlphaBalance::from(1_000_000_000); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &alice, - &cold_alice, - alpha, - alice_alpha_stake, - ); - let bob_alpha_stake = AlphaBalance::from(1_000_000_000); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &bob, - &cold_bob, - alpha, - bob_alpha_stake, - ); - // Set Bob as 100% child of Alice on root. - ChildkeyTake::::insert(bob, alpha, PerU16::from_parts(u16::MAX)); - mock_set_children_no_epochs(alpha, &alice, &[(u64::MAX, bob)]); - // Set Bob validator take to zero. - Delegates::::insert(alice, PerU16::zero()); - Delegates::::insert(bob, PerU16::zero()); - - // Lets drain! - let pending_alpha = AlphaBalance::from(1_000_000_000); - SubtensorModule::distribute_emission( - alpha, - pending_alpha.saturating_div(2.into()).into(), - pending_alpha.saturating_div(2.into()).into(), - AlphaBalance::ZERO, - AlphaBalance::ZERO, - ); - - // Bob makes it all. - close( - AlphaDividendsPerSubnet::::get(alpha, alice).into(), - 0, - 10, - ); - close( - AlphaDividendsPerSubnet::::get(alpha, bob).into(), - pending_alpha.into(), - 10, - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_get_root_children_drain_with_half_take --exact --show-output --nocapture -#[test] -fn test_get_root_children_drain_with_half_take() { - new_test_ext(1).execute_with(|| { - // Init netuid 1 - let alpha = NetUid::from(1); - add_network(NetUid::ROOT, 1, 0); - add_network(alpha, 1, 0); - // Set TAO weight to 1. - SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1. - SubtensorModule::set_ck_burn(0); - // Create keys. - let cold_alice = U256::from(0); - let cold_bob = U256::from(1); - let alice = U256::from(2); - let bob = U256::from(3); - // Register Alice and Bob to the root network and alpha subnet. - register_ok_neuron(alpha, alice, cold_alice, 0); - register_ok_neuron(alpha, bob, cold_bob, 0); - assert_ok!(SubtensorModule::root_register( - RuntimeOrigin::signed(cold_alice).clone(), - alice, - )); - assert_ok!(SubtensorModule::root_register( - RuntimeOrigin::signed(cold_bob).clone(), - bob, - )); - // Add stake for Alice and Bob on root. - let alice_root_stake = AlphaBalance::from(1_000_000_000); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &alice, - &cold_alice, - NetUid::ROOT, - alice_root_stake, - ); - let bob_root_stake = AlphaBalance::from(1_000_000_000); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &bob, - &cold_bob, - NetUid::ROOT, - alice_root_stake, - ); - // Add stake for Alice and Bob on netuid. - let alice_alpha_stake = AlphaBalance::from(1_000_000_000); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &alice, - &cold_alice, - alpha, - alice_alpha_stake, - ); - let bob_alpha_stake = AlphaBalance::from(1_000_000_000); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &bob, - &cold_bob, - alpha, - bob_alpha_stake, - ); - // Set Bob as 100% child of Alice on root. - ChildkeyTake::::insert(bob, alpha, PerU16::from_parts(u16::MAX / 2)); - mock_set_children_no_epochs(alpha, &alice, &[(u64::MAX, bob)]); - // Set Bob childkey take to zero. - Delegates::::insert(alice, PerU16::zero()); - Delegates::::insert(bob, PerU16::zero()); - - // Lets drain! - let pending_alpha = AlphaBalance::from(1_000_000_000); - SubtensorModule::distribute_emission( - alpha, - pending_alpha.saturating_div(2.into()).into(), - pending_alpha.saturating_div(2.into()).into(), - AlphaBalance::ZERO, - AlphaBalance::ZERO, - ); - - // Alice and Bob make the same amount. - close( - AlphaDividendsPerSubnet::::get(alpha, alice).into(), - (pending_alpha / 4.into()).into(), - 10000, - ); - close( - AlphaDividendsPerSubnet::::get(alpha, bob).into(), - 3 * u64::from(pending_alpha / 4.into()), - 10000, - ); - }); -} - -// // SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_get_root_children_with_weights --exact --show-output --nocapture -// #[test] -// fn test_get_root_children_with_weights() { -// new_test_ext(1).execute_with(|| { -// // Init netuid 1 -// let alpha = NetUid::from(1); -// add_network(NetUid::ROOT, 1, 0); -// add_network(alpha, 1, 0); -// // Set TAO weight to 1. -// SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1. -// // Create keys. -// let cold = U256::from(0); -// let alice = U256::from(1); -// let bob = U256::from(2); -// // Register Alice and Bob to the root network and alpha subnet. -// register_ok_neuron(alpha, alice, cold, 0); -// register_ok_neuron(alpha, bob, cold, 0); -// assert_ok!(SubtensorModule::root_register( -// RuntimeOrigin::signed(cold).clone(), -// alice, -// )); -// assert_ok!(SubtensorModule::root_register( -// RuntimeOrigin::signed(cold).clone(), -// bob, -// )); -// // Add stake for Alice and Bob on root. -// let alice_root_stake = AlphaBalance::from(1_000_000_000); -// SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( -// &alice, -// &cold, -// NetUid::ROOT, -// alice_root_stake, -// ); -// let bob_root_stake = AlphaBalance::from(1_000_000_000); -// SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( -// &bob, -// &cold, -// NetUid::ROOT, -// alice_root_stake, -// ); -// // Add stake for Alice and Bob on netuid. -// let alice_alpha_stake = AlphaBalance::from(1_000_000_000); -// SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( -// &alice, -// &cold, -// alpha, -// alice_alpha_stake, -// ); -// let bob_alpha_stake = AlphaBalance::from(1_000_000_000); -// SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( -// &bob, -// &cold, -// alpha, -// bob_alpha_stake, -// ); -// // Set Bob as 100% child of Alice on root. -// mock_set_children_no_epochs(alpha, &alice, &[(u64::MAX, bob)]); - -// // Set Bob childkey take to zero. -// ChildkeyTake::::insert(bob, alpha, 0); -// Delegates::::insert(alice, 0); -// Delegates::::insert(bob, 0); - -// // Set weights on the subnet. -// assert_ok!(SubtensorModule::set_weights( -// RuntimeOrigin::signed(alice), -// alpha, -// vec![0, 1], -// vec![1, 1], -// 0, -// )); -// assert_ok!(SubtensorModule::set_weights( -// RuntimeOrigin::signed(bob), -// alpha, -// vec![0, 1], -// vec![1, 1], -// 0, -// )); - -// // Lets drain! -// let pending_alpha = AlphaBalance::from(1_000_000_000); -// SubtensorModule::distribute_emission(alpha, pending_alpha, 0, 0.into(), 0.into()); - -// // Alice and Bob make the same amount. -// close( -// AlphaDividendsPerSubnet::::get(alpha, alice), -// pending_alpha / 2, -// 10, -// ); -// close( -// AlphaDividendsPerSubnet::::get(alpha, bob), -// pending_alpha / 2, -// 10, -// ); -// }); -// } - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_incentive_to_subnet_owner_is_burned --exact --show-output --nocapture -#[test] -fn test_incentive_to_subnet_owner_is_burned() { - new_test_ext(1).execute_with(|| { - let subnet_owner_ck = U256::from(0); - let subnet_owner_hk = U256::from(1); - - let other_ck = U256::from(2); - let other_hk = U256::from(3); - Owner::::insert(other_hk, other_ck); - - let netuid = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); - remove_owner_registration_stake(netuid); - - let pending_tao: u64 = 1_000_000_000; - let pending_alpha = AlphaBalance::ZERO; // None to valis - let owner_cut = AlphaBalance::ZERO; - let mut incentives: BTreeMap = BTreeMap::new(); - - // Give incentive to other_hk - incentives.insert(other_hk, 10_000_000.into()); - - // Give incentives to subnet_owner_hk - incentives.insert(subnet_owner_hk, 10_000_000.into()); - - // Verify stake before - let subnet_owner_stake_before = - SubtensorModule::get_stake_for_hotkey_on_subnet(&subnet_owner_hk, netuid); - assert_eq!(subnet_owner_stake_before, 0.into()); - let other_stake_before = SubtensorModule::get_stake_for_hotkey_on_subnet(&other_hk, netuid); - assert_eq!(other_stake_before, 0.into()); - - // Distribute dividends and incentives - SubtensorModule::distribute_dividends_and_incentives( - netuid, - owner_cut, - incentives, - BTreeMap::new(), - BTreeMap::new(), - ); - - // Verify stake after - let subnet_owner_stake_after = - SubtensorModule::get_stake_for_hotkey_on_subnet(&subnet_owner_hk, netuid); - assert_eq!(subnet_owner_stake_after, 0.into()); - let other_stake_after = SubtensorModule::get_stake_for_hotkey_on_subnet(&other_hk, netuid); - assert!(other_stake_after > 0.into()); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_incentive_to_subnet_owners_hotkey_is_burned --exact --show-output --nocapture -#[test] -fn test_incentive_to_subnet_owners_hotkey_is_burned() { - new_test_ext(1).execute_with(|| { - let subnet_owner_ck = U256::from(0); - let subnet_owner_hk = U256::from(1); - - // Other hk owned by owner - let other_hk = U256::from(3); - Owner::::insert(other_hk, subnet_owner_ck); - OwnedHotkeys::::insert(subnet_owner_ck, vec![subnet_owner_hk, other_hk]); - - let netuid = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); - remove_owner_registration_stake(netuid); - Uids::::insert(netuid, other_hk, 1); - - // Set the burn key limit to 2 - ImmuneOwnerUidsLimit::::insert(netuid, 2); - - let pending_tao: u64 = 1_000_000_000; - let pending_alpha = AlphaBalance::ZERO; // None to valis - let owner_cut = AlphaBalance::ZERO; - let mut incentives: BTreeMap = BTreeMap::new(); - - // Give incentive to other_hk - incentives.insert(other_hk, 10_000_000.into()); - - // Give incentives to subnet_owner_hk - incentives.insert(subnet_owner_hk, 10_000_000.into()); - - // Verify stake before - let subnet_owner_stake_before = - SubtensorModule::get_stake_for_hotkey_on_subnet(&subnet_owner_hk, netuid); - assert_eq!(subnet_owner_stake_before, 0.into()); - let other_stake_before = SubtensorModule::get_stake_for_hotkey_on_subnet(&other_hk, netuid); - assert_eq!(other_stake_before, 0.into()); - - // Distribute dividends and incentives - SubtensorModule::distribute_dividends_and_incentives( - netuid, - owner_cut, - incentives, - BTreeMap::new(), - BTreeMap::new(), - ); - - // Verify stake after - let subnet_owner_stake_after = - SubtensorModule::get_stake_for_hotkey_on_subnet(&subnet_owner_hk, netuid); - assert_eq!(subnet_owner_stake_after, 0.into()); - let other_stake_after = SubtensorModule::get_stake_for_hotkey_on_subnet(&other_hk, netuid); - assert_eq!(other_stake_after, 0.into()); - }); -} - -// Test that if number of sn owner hotkeys is greater than ImmuneOwnerUidsLimit, then the ones with -// higher BlockAtRegistration are used to burn -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_burn_key_sorting --exact --show-output --nocapture -#[test] -fn test_burn_key_sorting() { - new_test_ext(1).execute_with(|| { - let subnet_owner_ck = U256::from(0); - let subnet_owner_hk = U256::from(1); - - // Other hk owned by owner - let other_hk_1 = U256::from(3); - let other_hk_2 = U256::from(4); - let other_hk_3 = U256::from(5); - Owner::::insert(other_hk_1, subnet_owner_ck); - Owner::::insert(other_hk_2, subnet_owner_ck); - Owner::::insert(other_hk_3, subnet_owner_ck); - OwnedHotkeys::::insert( - subnet_owner_ck, - vec![subnet_owner_hk, other_hk_1, other_hk_2, other_hk_3], - ); - - let netuid = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); - remove_owner_registration_stake(netuid); - - // Set block of registration and UIDs for other hotkeys - // HK1 has block of registration 2 - // HK2 and HK3 have the same block of registration 1, so they are sorted by UID - // Set HK2 UID = 3 and HK3 UID = 2 so that HK3 is burned and HK2 is not - // Summary: HK1 and HK3 should be burned, HK2 should be not. - // Let's test it now. - BlockAtRegistration::::insert(netuid, 1, 2); - BlockAtRegistration::::insert(netuid, 3, 1); - BlockAtRegistration::::insert(netuid, 2, 1); - Uids::::insert(netuid, other_hk_1, 1); - Uids::::insert(netuid, other_hk_2, 3); - Uids::::insert(netuid, other_hk_3, 2); - - let pending_tao: u64 = 1_000_000_000; - let pending_alpha = AlphaBalance::ZERO; // None to valis - let owner_cut = AlphaBalance::ZERO; - let mut incentives: BTreeMap = BTreeMap::new(); - - // Give incentive to hotkeys - incentives.insert(other_hk_1, 10_000_000.into()); - incentives.insert(other_hk_2, 10_000_000.into()); - incentives.insert(other_hk_3, 10_000_000.into()); - - // Give incentives to subnet_owner_hk - incentives.insert(subnet_owner_hk, 10_000_000.into()); - - // Distribute dividends and incentives - SubtensorModule::distribute_dividends_and_incentives( - netuid, - owner_cut, - incentives, - BTreeMap::new(), - BTreeMap::new(), - ); - - // SN owner is burned - let subnet_owner_stake_after = - SubtensorModule::get_stake_for_hotkey_on_subnet(&subnet_owner_hk, netuid); - assert_eq!(subnet_owner_stake_after, 0.into()); - - // No burn limits, all HKs should be burned - let other_stake_after_1 = - SubtensorModule::get_stake_for_hotkey_on_subnet(&other_hk_1, netuid); - let other_stake_after_2 = - SubtensorModule::get_stake_for_hotkey_on_subnet(&other_hk_2, netuid); - let other_stake_after_3 = - SubtensorModule::get_stake_for_hotkey_on_subnet(&other_hk_3, netuid); - assert_eq!(other_stake_after_1, 0.into()); - assert_eq!(other_stake_after_2, 0.into()); - assert_eq!(other_stake_after_3, 0.into()); - }); -} - -#[test] -fn test_calculate_dividend_distribution_totals() { - new_test_ext(1).execute_with(|| { - let mut stake_map: BTreeMap = BTreeMap::new(); - let mut dividends: BTreeMap = BTreeMap::new(); - - let pending_validator_alpha = AlphaBalance::from(183_123_567_452_u64); - let pending_root_alpha = AlphaBalance::from(837_120_949_872_u64); - let tao_weight: U96F32 = U96F32::from_num(0.18); // 18% - - let hotkeys = [U256::from(0), U256::from(1)]; - - // Stake map and dividends shouldn't matter for this test. - stake_map.insert(hotkeys[0], (4_859_302.into(), 2_342_352.into())); - stake_map.insert(hotkeys[1], (23_423.into(), 859_273.into())); - dividends.insert(hotkeys[0], 77_783_738_u64.into()); - dividends.insert(hotkeys[1], 19_283_940_u64.into()); - - let (alpha_dividends, root_alpha_dividends) = - SubtensorModule::calculate_dividend_distribution( - pending_validator_alpha, - pending_root_alpha, - tao_weight, - stake_map, - dividends, - ); - - // Verify the total of each dividends type is close to the inputs. - let total_alpha_dividends = alpha_dividends.values().sum::(); - let total_root_alpha_dividends = root_alpha_dividends.values().sum::(); - - assert_abs_diff_eq!( - total_alpha_dividends.to_num::(), - u64::from(pending_validator_alpha), - epsilon = 1_000 - ); - assert_abs_diff_eq!( - total_root_alpha_dividends.to_num::(), - pending_root_alpha.to_u64(), - epsilon = 1_000 - ); - }); -} - -#[test] -fn test_calculate_dividend_distribution_total_only_tao() { - new_test_ext(1).execute_with(|| { - let mut stake_map: BTreeMap = BTreeMap::new(); - let mut dividends: BTreeMap = BTreeMap::new(); - - let pending_validator_alpha = AlphaBalance::ZERO; - let pending_root_alpha = AlphaBalance::from(837_120_949_872_u64); - let tao_weight: U96F32 = U96F32::from_num(0.18); // 18% - - let hotkeys = [U256::from(0), U256::from(1)]; - - // Stake map and dividends shouldn't matter for this test. - stake_map.insert(hotkeys[0], (4_859_302.into(), 2_342_352.into())); - stake_map.insert(hotkeys[1], (23_423.into(), 859_273.into())); - dividends.insert(hotkeys[0], 77_783_738_u64.into()); - dividends.insert(hotkeys[1], 19_283_940_u64.into()); - - let (alpha_dividends, root_alpha_dividends) = - SubtensorModule::calculate_dividend_distribution( - pending_validator_alpha, - pending_root_alpha, - tao_weight, - stake_map, - dividends, - ); - - // Verify the total of each dividends type is close to the inputs. - let total_alpha_dividends = alpha_dividends.values().sum::(); - let total_root_alpha_dividends = root_alpha_dividends.values().sum::(); - - assert_abs_diff_eq!( - total_alpha_dividends.to_num::(), - u64::from(pending_validator_alpha), - epsilon = 1_000 - ); - assert_abs_diff_eq!( - total_root_alpha_dividends.to_num::(), - pending_root_alpha.to_u64(), - epsilon = 1_000 - ); - }); -} - -#[test] -fn test_calculate_dividend_distribution_total_no_tao_weight() { - new_test_ext(1).execute_with(|| { - let mut stake_map: BTreeMap = BTreeMap::new(); - let mut dividends: BTreeMap = BTreeMap::new(); - - let pending_validator_alpha = AlphaBalance::from(183_123_567_452_u64); - let pending_tao = TaoBalance::ZERO; // If tao weight is 0, then only alpha dividends should be input. - let tao_weight: U96F32 = U96F32::from_num(0.0); // 0% - - let hotkeys = [U256::from(0), U256::from(1)]; - - // Stake map and dividends shouldn't matter for this test. - stake_map.insert(hotkeys[0], (4_859_302.into(), 2_342_352.into())); - stake_map.insert(hotkeys[1], (23_423.into(), 859_273.into())); - dividends.insert(hotkeys[0], 77_783_738_u64.into()); - dividends.insert(hotkeys[1], 19_283_940_u64.into()); - - let (alpha_dividends, tao_dividends) = SubtensorModule::calculate_dividend_distribution( - pending_validator_alpha, - // pending_tao, - AlphaBalance::ZERO, - tao_weight, - stake_map, - dividends, - ); - - // Verify the total of each dividends type is close to the inputs. - let total_alpha_dividends = alpha_dividends.values().sum::(); - let total_tao_dividends = tao_dividends.values().sum::(); - - assert_abs_diff_eq!( - total_alpha_dividends.to_num::(), - u64::from(pending_validator_alpha), - epsilon = 1_000 - ); - assert_abs_diff_eq!( - total_tao_dividends.to_num::(), - pending_tao.to_u64(), - epsilon = 1_000 - ); - }); -} - -#[test] -fn test_calculate_dividend_distribution_total_only_alpha() { - new_test_ext(1).execute_with(|| { - let mut stake_map: BTreeMap = BTreeMap::new(); - let mut dividends: BTreeMap = BTreeMap::new(); - - let pending_validator_alpha = AlphaBalance::from(183_123_567_452_u64); - let pending_tao = TaoBalance::ZERO; - let tao_weight: U96F32 = U96F32::from_num(0.18); // 18% - - let hotkeys = [U256::from(0), U256::from(1)]; - - // Stake map and dividends shouldn't matter for this test. - stake_map.insert(hotkeys[0], (4_859_302.into(), 2_342_352.into())); - stake_map.insert(hotkeys[1], (23_423.into(), 859_273.into())); - dividends.insert(hotkeys[0], 77_783_738_u64.into()); - dividends.insert(hotkeys[1], 19_283_940_u64.into()); - - let (alpha_dividends, tao_dividends) = SubtensorModule::calculate_dividend_distribution( - pending_validator_alpha, - // pending_tao, - AlphaBalance::ZERO, - tao_weight, - stake_map, - dividends, - ); - - // Verify the total of each dividends type is close to the inputs. - let total_alpha_dividends = alpha_dividends.values().sum::(); - let total_tao_dividends = tao_dividends.values().sum::(); - - assert_abs_diff_eq!( - total_alpha_dividends.to_num::(), - u64::from(pending_validator_alpha), - epsilon = 1_000 - ); - assert_abs_diff_eq!( - total_tao_dividends.to_num::(), - pending_tao.to_u64(), - epsilon = 1_000 - ); - }); -} - -#[test] -fn test_calculate_dividend_and_incentive_distribution() { - new_test_ext(1).execute_with(|| { - let sn_owner_hk = U256::from(0); - let sn_owner_ck = U256::from(1); - let netuid = add_dynamic_network(&sn_owner_hk, &sn_owner_ck); - - // Register a single neuron. - let hotkey = U256::from(1); - let coldkey = U256::from(2); - register_ok_neuron(netuid, hotkey, coldkey, 0); - // Give non-zero alpha - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - netuid, - 1.into(), - ); - - let pending_alpha = AlphaBalance::from(123_456_789); - let pending_validator_alpha = pending_alpha / 2.into(); // Pay half to validators. - let pending_tao = TaoBalance::ZERO; - let pending_swapped = 0; // Only alpha output. - let tao_weight: U96F32 = U96F32::from_num(0.0); // 0% - - // Hotkey, Incentive, Dividend - let hotkey_emission = vec![(hotkey, pending_alpha / 2.into(), pending_alpha / 2.into())]; - - let (incentives, (alpha_dividends, tao_dividends)) = - SubtensorModule::calculate_dividend_and_incentive_distribution( - netuid, - // pending_tao, - AlphaBalance::ZERO, - pending_validator_alpha, - hotkey_emission, - tao_weight, - ); - - let incentives_total = incentives.values().copied().map(u64::from).sum::(); - let dividends_total = alpha_dividends.values().sum::().to_num::(); - - assert_abs_diff_eq!( - dividends_total + incentives_total, - u64::from(pending_alpha), - epsilon = 2 - ); - }); -} - -#[test] -fn test_calculate_dividend_and_incentive_distribution_all_to_validators() { - new_test_ext(1).execute_with(|| { - let sn_owner_hk = U256::from(0); - let sn_owner_ck = U256::from(1); - let netuid = add_dynamic_network(&sn_owner_hk, &sn_owner_ck); - - // Register a single neuron. - let hotkey = U256::from(1); - let coldkey = U256::from(2); - register_ok_neuron(netuid, hotkey, coldkey, 0); - // Give non-zero alpha - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - netuid, - 1.into(), - ); - - let pending_alpha = AlphaBalance::from(123_456_789); - let pending_validator_alpha = pending_alpha; // Pay all to validators. - let pending_tao = TaoBalance::ZERO; - let tao_weight: U96F32 = U96F32::from_num(0.0); // 0% - - // Hotkey, Incentive, Dividend - let hotkey_emission = vec![(hotkey, 0.into(), pending_alpha)]; - - let (incentives, (alpha_dividends, tao_dividends)) = - SubtensorModule::calculate_dividend_and_incentive_distribution( - netuid, - // pending_tao, - AlphaBalance::ZERO, - pending_validator_alpha, - hotkey_emission, - tao_weight, - ); - - let incentives_total = incentives.values().copied().map(u64::from).sum::(); - let dividends_total = alpha_dividends.values().sum::().to_num::(); - - assert_eq!( - AlphaBalance::from(dividends_total + incentives_total), - pending_alpha - ); - }); -} - -#[test] -fn test_calculate_dividends_and_incentives() { - new_test_ext(1).execute_with(|| { - let sn_owner_hk = U256::from(0); - let sn_owner_ck = U256::from(1); - let netuid = add_dynamic_network(&sn_owner_hk, &sn_owner_ck); - - // Register a single neuron. - let hotkey = U256::from(1); - let coldkey = U256::from(2); - register_ok_neuron(netuid, hotkey, coldkey, 0); - // Give non-zero alpha - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - netuid, - 1.into(), - ); - - let divdends = AlphaBalance::from(123_456_789); - let incentive = AlphaBalance::from(683_051_923); - let total_emission = divdends + incentive; - - // Hotkey, Incentive, Dividend - let hotkey_emission = vec![(hotkey, incentive, divdends)]; - - let (incentives, dividends) = - SubtensorModule::calculate_dividends_and_incentives(netuid, hotkey_emission); - - let incentives_total = incentives - .values() - .copied() - .fold(AlphaBalance::ZERO, |acc, x| acc + x); - let dividends_total = - AlphaBalance::from(dividends.values().sum::().to_num::()); - - assert_eq!(dividends_total + incentives_total, total_emission); - }); -} - -#[test] -fn test_calculate_dividends_and_incentives_only_validators() { - new_test_ext(1).execute_with(|| { - let sn_owner_hk = U256::from(0); - let sn_owner_ck = U256::from(1); - let netuid = add_dynamic_network(&sn_owner_hk, &sn_owner_ck); - - // Register a single neuron. - let hotkey = U256::from(1); - let coldkey = U256::from(2); - register_ok_neuron(netuid, hotkey, coldkey, 0); - // Give non-zero alpha - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - netuid, - 1.into(), - ); - - let divdends = AlphaBalance::from(123_456_789); - let incentive = AlphaBalance::ZERO; - - // Hotkey, Incentive, Dividend - let hotkey_emission = vec![(hotkey, incentive, divdends)]; - - let (incentives, dividends) = - SubtensorModule::calculate_dividends_and_incentives(netuid, hotkey_emission); - - let incentives_total = incentives - .values() - .copied() - .fold(AlphaBalance::ZERO, |acc, x| acc + x); - let dividends_total = - AlphaBalance::from(dividends.values().sum::().to_num::()); - - assert_eq!(dividends_total, divdends); - assert_eq!(incentives_total, AlphaBalance::ZERO); - }); -} - -#[test] -fn test_calculate_dividends_and_incentives_only_miners() { - new_test_ext(1).execute_with(|| { - let sn_owner_hk = U256::from(0); - let sn_owner_ck = U256::from(1); - let netuid = add_dynamic_network(&sn_owner_hk, &sn_owner_ck); - - // Register a single neuron. - let hotkey = U256::from(1); - let coldkey = U256::from(2); - register_ok_neuron(netuid, hotkey, coldkey, 0); - // Give non-zero alpha - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - netuid, - 1.into(), - ); - - let divdends = AlphaBalance::ZERO; - let incentive = AlphaBalance::from(123_456_789); - - // Hotkey, Incentive, Dividend - let hotkey_emission = vec![(hotkey, incentive, divdends)]; - - let (incentives, dividends) = - SubtensorModule::calculate_dividends_and_incentives(netuid, hotkey_emission); - - let incentives_total = incentives - .values() - .copied() - .fold(AlphaBalance::ZERO, |acc, x| acc + x); - let dividends_total = - AlphaBalance::from(dividends.values().sum::().to_num::()); - - assert_eq!(incentives_total, incentive); - assert_eq!(dividends_total, divdends); - }); -} - -#[test] -fn test_distribute_emission_no_miners_all_drained() { - new_test_ext(1).execute_with(|| { - let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); - remove_owner_registration_stake(netuid); - let hotkey = U256::from(3); - let coldkey = U256::from(4); - let init_stake = 1; - SubtensorModule::set_burn(netuid, TaoBalance::from(0)); - register_ok_neuron(netuid, hotkey, coldkey, 0); - // Give non-zero stake - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - netuid, - init_stake.into(), - ); - assert_eq!( - SubtensorModule::get_total_stake_for_hotkey(&hotkey), - init_stake.into() - ); - - // Set the weight of root TAO to be 0%, so only alpha is effective. - SubtensorModule::set_tao_weight(0); - - // Set the emission to be 1 million. - let emission = AlphaBalance::from(1_000_000); - // Run drain pending without any miners. - SubtensorModule::distribute_emission( - netuid, - emission.saturating_div(2.into()).into(), - emission.saturating_div(2.into()).into(), - AlphaBalance::ZERO, - AlphaBalance::ZERO, - ); - - // Get the new stake of the hotkey. - let new_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); - // We expect this neuron to get *all* the emission. - // Slight epsilon due to rounding (hotkey_take). - assert_abs_diff_eq!( - new_stake, - u64::from(emission + init_stake.into()).into(), - epsilon = 1.into() - ); - }); -} - -// cargo test --package pallet-subtensor --lib -- tests::coinbase::test_distribute_emission_zero_emission --exact --show-output -#[test] -fn test_distribute_emission_zero_emission() { - new_test_ext(1).execute_with(|| { - let netuid = add_dynamic_network_disable_commit_reveal(&U256::from(1), &U256::from(2)); - let hotkey = U256::from(3); - let coldkey = U256::from(4); - let miner_hk = U256::from(5); - let miner_ck = U256::from(6); - let init_stake: u64 = 100_000_000_000_000; - let tempo = 2; - SubtensorModule::set_tempo_unchecked(netuid, tempo); - // Set weight-set limit to 0. - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - register_ok_neuron(netuid, hotkey, coldkey, 0); - register_ok_neuron(netuid, miner_hk, miner_ck, 0); - // Give non-zero stake - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - netuid, - init_stake.into(), - ); - assert_eq!( - SubtensorModule::get_total_stake_for_hotkey(&hotkey), - init_stake.into() - ); - - // Set the weight of root TAO to be 0%, so only alpha is effective. - SubtensorModule::set_tao_weight(0); - - run_to_block_no_epoch(netuid, 50); - - // Run epoch for initial setup. - SubtensorModule::epoch(netuid, AlphaBalance::ZERO); - - // Set weights on miner - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid, - vec![0, 1, 2], - vec![0, 0, 1], - 0, - )); - - run_to_block_no_epoch(netuid, 50); - - // Clear incentive and dividends. - Incentive::::remove(NetUidStorageIndex::from(netuid)); - Dividends::::remove(netuid); - - // Capture stake right before the zero-emission distribution so the assertion - // isolates that call (the subnet legitimately accrues emission during the - // preceding block runs under price-based shares). - let stake_before_distribute = SubtensorModule::get_total_stake_for_hotkey(&hotkey); - - // Set the emission to be ZERO. - SubtensorModule::distribute_emission( - netuid, - AlphaBalance::ZERO, - AlphaBalance::ZERO, - AlphaBalance::ZERO, - AlphaBalance::ZERO, - ); - - // Get the new stake of the hotkey. - let new_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); - // We expect the stake to remain unchanged by the zero-emission distribution. - assert_eq!(new_stake, stake_before_distribute); - - // Check that the incentive and dividends are set by epoch. - assert!( - Incentive::::get(NetUidStorageIndex::from(netuid)) - .iter() - .map(|p| p.deconstruct()) - .sum::() - > 0 - ); - assert!( - Dividends::::get(netuid) - .iter() - .map(|p| p.deconstruct()) - .sum::() - > 0 - ); - }); -} - -#[test] -fn test_run_coinbase_not_started() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let tempo = 2; - - let sn_owner_hk = U256::from(7); - let sn_owner_ck = U256::from(8); - - add_network_without_emission_block(netuid, tempo, 0); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); - assert_eq!(FirstEmissionBlockNumber::::get(netuid), None); - - SubnetOwner::::insert(netuid, sn_owner_ck); - SubnetOwnerHotkey::::insert(netuid, sn_owner_hk); - - let hotkey = U256::from(3); - let coldkey = U256::from(4); - let miner_hk = U256::from(5); - let miner_ck = U256::from(6); - let init_stake: u64 = 100_000_000_000_000; - let tempo = 2; - SubtensorModule::set_tempo_unchecked(netuid, tempo); - // Set weight-set limit to 0. - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - let reserve = init_stake * 1000; - mock::setup_reserves(netuid, reserve.into(), reserve.into()); - - register_ok_neuron(netuid, hotkey, coldkey, 0); - register_ok_neuron(netuid, miner_hk, miner_ck, 0); - register_ok_neuron(netuid, sn_owner_hk, sn_owner_ck, 0); - // Give non-zero stake - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - netuid, - init_stake.into(), - ); - assert_eq!( - SubtensorModule::get_total_stake_for_hotkey(&hotkey), - init_stake.into() - ); - - // Set the weight of root TAO to be 0%, so only alpha is effective. - SubtensorModule::set_tao_weight(0); - - run_to_block_no_epoch(netuid, 30); - - // Run epoch for initial setup. - SubtensorModule::epoch(netuid, AlphaBalance::ZERO); - - // Set weights on miner - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid, - vec![0, 1, 2], - vec![0, 0, 1], - 0, - )); - - // Clear incentive and dividends. - Incentive::::remove(NetUidStorageIndex::from(netuid)); - Dividends::::remove(netuid); - - // Step so tempo should run. - next_block_no_epoch(netuid); - next_block_no_epoch(netuid); - next_block_no_epoch(netuid); - let current_block = System::block_number(); - assert!(SubtensorModule::should_run_epoch(netuid, current_block)); - - // Run coinbase with emission. - let emission_credit = SubtensorModule::mint_tao(100_000_000.into()); - SubtensorModule::run_coinbase(emission_credit); - - // We expect that the epoch ran. - assert_eq!(BlocksSinceLastStep::::get(netuid), 0); - - // Get the new stake of the hotkey. We expect no emissions. - let new_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); - // We expect the stake to remain unchanged. - assert_eq!(new_stake, init_stake.into()); - - // Check that the incentive and dividends are set. - assert!( - Incentive::::get(NetUidStorageIndex::from(netuid)) - .iter() - .map(|p| p.deconstruct()) - .sum::() - > 0 - ); - assert!( - Dividends::::get(netuid) - .iter() - .map(|p| p.deconstruct()) - .sum::() - > 0 - ); - }); -} - -#[test] -fn test_run_coinbase_not_started_start_after() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let tempo = 2; - - let sn_owner_hk = U256::from(7); - let sn_owner_ck = U256::from(8); - - add_network_without_emission_block(netuid, tempo, 0); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); - assert_eq!(FirstEmissionBlockNumber::::get(netuid), None); - - SubnetOwner::::insert(netuid, sn_owner_ck); - SubnetOwnerHotkey::::insert(netuid, sn_owner_hk); - - let hotkey = U256::from(3); - let coldkey = U256::from(4); - let miner_hk = U256::from(5); - let miner_ck = U256::from(6); - let init_stake: u64 = 100_000_000_000_000; - let tempo = 2; - SubtensorModule::set_tempo_unchecked(netuid, tempo); - // Set weight-set limit to 0. - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - register_ok_neuron(netuid, hotkey, coldkey, 0); - register_ok_neuron(netuid, miner_hk, miner_ck, 0); - register_ok_neuron(netuid, sn_owner_hk, sn_owner_ck, 0); - // Give non-zero stake - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - netuid, - init_stake.into(), - ); - assert_eq!( - SubtensorModule::get_total_stake_for_hotkey(&hotkey), - init_stake.into() - ); - - // Set the weight of root TAO to be 0%, so only alpha is effective. - SubtensorModule::set_tao_weight(0); - - run_to_block_no_epoch(netuid, 30); - - // Run epoch for initial setup. - SubtensorModule::epoch(netuid, AlphaBalance::ZERO); - - // Set weights on miner - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid, - vec![0, 1, 2], - vec![0, 0, 1], - 0, - )); - - // Clear incentive and dividends. - Incentive::::remove(NetUidStorageIndex::from(netuid)); - Dividends::::remove(netuid); - - // Step so tempo should run. - next_block_no_epoch(netuid); - next_block_no_epoch(netuid); - next_block_no_epoch(netuid); - let current_block = System::block_number(); - assert!(SubtensorModule::should_run_epoch(netuid, current_block)); - - // Run coinbase with emission. - let emission_credit = SubtensorModule::mint_tao(100_000_000.into()); - SubtensorModule::run_coinbase(emission_credit); - // We expect that the epoch ran. - assert_eq!(BlocksSinceLastStep::::get(netuid), 0); - - let block_number = StartCallDelay::::get(); - run_to_block_no_epoch(netuid, block_number); - - let current_block = System::block_number(); - - // Run start call. - assert_ok!(SubtensorModule::start_call( - RuntimeOrigin::signed(sn_owner_ck), - netuid - )); - assert_eq!( - FirstEmissionBlockNumber::::get(netuid), - Some(current_block + 1) - ); - - // Advance the block past `LastEpochBlock + tempo` so the state-based - // scheduler is due again (the previous `run_coinbase` advanced it). - next_block_no_epoch(netuid); - next_block_no_epoch(netuid); - next_block_no_epoch(netuid); - - // Run coinbase with emission. - let emission_credit = SubtensorModule::mint_tao(100_000_000.into()); - SubtensorModule::run_coinbase(emission_credit); - // We expect that the epoch ran. - assert_eq!(BlocksSinceLastStep::::get(netuid), 0); - - // Get the new stake of the hotkey. We expect no emissions. - let new_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); - // We expect the stake to remain unchanged. - assert!(new_stake > init_stake.into()); - log::info!("new_stake: {new_stake}"); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_drain_alpha_childkey_parentkey_with_burn --exact --show-output --nocapture -#[test] -fn test_drain_alpha_childkey_parentkey_with_burn() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - let parent = U256::from(1); - let child = U256::from(2); - let coldkey = U256::from(3); - let stake_before = AlphaBalance::from(1_000_000_000); - register_ok_neuron(netuid, child, coldkey, 0); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &parent, - &coldkey, - netuid, - stake_before, - ); - mock_set_children_no_epochs(netuid, &parent, &[(u64::MAX, child)]); - - // Childkey take is 10% - ChildkeyTake::::insert(child, netuid, PerU16::from_parts(u16::MAX / 10)); - - let burn_rate = SubtensorModule::get_ck_burn(); - let parent_stake_before = SubtensorModule::get_stake_for_hotkey_on_subnet(&parent, netuid); - let child_stake_before = SubtensorModule::get_stake_for_hotkey_on_subnet(&child, netuid); - - let pending_alpha = AlphaBalance::from(1_000_000_000); - SubtensorModule::distribute_emission( - netuid, - pending_alpha.saturating_div(2.into()).into(), - pending_alpha.saturating_div(2.into()).into(), - AlphaBalance::ZERO, - AlphaBalance::ZERO, - ); - let parent_stake_after = SubtensorModule::get_stake_for_hotkey_on_subnet(&parent, netuid); - let child_stake_after = SubtensorModule::get_stake_for_hotkey_on_subnet(&child, netuid); - - let expected_ck_burn = I96F32::from_num(pending_alpha) - * I96F32::from_num(9.0 / 10.0) - * I96F32::from_num(burn_rate); - - let expected_total = I96F32::from_num(pending_alpha) - expected_ck_burn; - let parent_ratio = (I96F32::from_num(pending_alpha) * I96F32::from_num(9.0 / 10.0) - - expected_ck_burn) - / expected_total; - let child_ratio = (I96F32::from_num(pending_alpha) / I96F32::from_num(10)) / expected_total; - - let expected = - I96F32::from_num(stake_before) + I96F32::from_num(pending_alpha) * parent_ratio; - log::info!( - "expected: {:?}, parent_stake_after: {:?}", - expected.to_num::(), - parent_stake_after - ); - - close( - expected.to_num::(), - parent_stake_after.into(), - 3_000_000, - ); - let expected = I96F32::from_num(u64::from(pending_alpha)) * child_ratio; - close( - expected.to_num::(), - child_stake_after.into(), - 3_000_000, - ); - }); -} - -#[test] -fn test_incentive_is_autostaked_to_owner_destination() { - new_test_ext(1).execute_with(|| { - let subnet_owner_ck = U256::from(0); - let subnet_owner_hk = U256::from(1); - - let miner_ck = U256::from(10); - let miner_hk = U256::from(11); - let dest_hk = U256::from(12); - - Owner::::insert(miner_hk, miner_ck); - Owner::::insert(dest_hk, miner_ck); - OwnedHotkeys::::insert(miner_ck, vec![miner_hk, dest_hk]); - - let netuid = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); - - Uids::::insert(netuid, miner_hk, 1); - Uids::::insert(netuid, dest_hk, 2); - - // Set autostake destination for the miner's coldkey - assert_ok!(SubtensorModule::set_coldkey_auto_stake_hotkey( - RuntimeOrigin::signed(miner_ck), - netuid, - dest_hk, - )); - - assert_eq!( - SubtensorModule::get_stake_for_hotkey_on_subnet(&miner_hk, netuid), - 0.into() - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_on_subnet(&dest_hk, netuid), - 0.into() - ); - - // Distribute an incentive to the miner hotkey - let mut incentives: BTreeMap = BTreeMap::new(); - let incentive: AlphaBalance = 10_000_000u64.into(); - incentives.insert(miner_hk, incentive); - - SubtensorModule::distribute_dividends_and_incentives( - netuid, - AlphaBalance::ZERO, // owner_cut - incentives, - BTreeMap::new(), // alpha_dividends - BTreeMap::new(), // tao_dividends - ); - - // Expect the stake to land on the destination hotkey (not the original miner hotkey) - assert_eq!( - SubtensorModule::get_stake_for_hotkey_on_subnet(&miner_hk, netuid), - 0.into() - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_on_subnet(&dest_hk, netuid), - incentive - ); - }); -} - -#[test] -fn test_incentive_goes_to_hotkey_when_no_autostake_destination() { - new_test_ext(1).execute_with(|| { - let subnet_owner_ck = U256::from(0); - let subnet_owner_hk = U256::from(1); - - let miner_ck = U256::from(20); - let miner_hk = U256::from(21); - - Owner::::insert(miner_hk, miner_ck); - OwnedHotkeys::::insert(miner_ck, vec![miner_hk]); - - let netuid = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); - - Uids::::insert(netuid, miner_hk, 1); - - assert_eq!( - SubtensorModule::get_stake_for_hotkey_on_subnet(&miner_hk, netuid), - 0.into() - ); - - // Distribute an incentive to the miner hotkey - let mut incentives: BTreeMap = BTreeMap::new(); - let incentive: AlphaBalance = 5_000_000u64.into(); - incentives.insert(miner_hk, incentive); - - SubtensorModule::distribute_dividends_and_incentives( - netuid, - AlphaBalance::ZERO, // owner_cut - incentives, - BTreeMap::new(), // alpha_dividends - BTreeMap::new(), // tao_dividends - ); - - // With no autostake destination, the incentive should be staked to the original hotkey - assert_eq!( - SubtensorModule::get_stake_for_hotkey_on_subnet(&miner_hk, netuid), - incentive - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_zero_shares_zero_emission --exact --show-output --nocapture -#[test] -fn test_zero_shares_zero_emission() { - new_test_ext(1).execute_with(|| { - let subnet_owner_ck = U256::from(0); - let subnet_owner_hk = U256::from(1); - let netuid1 = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); - let netuid2 = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); - let emission: u64 = 1_000_000; - let emission_credit = SubtensorModule::mint_tao(emission.into()); - // Setup prices 1 and 1 - let initial: u64 = 1_000_000; - SubnetTAO::::insert(netuid1, TaoBalance::from(initial)); - SubnetAlphaIn::::insert(netuid1, AlphaBalance::from(initial)); - SubnetTAO::::insert(netuid2, TaoBalance::from(initial)); - SubnetAlphaIn::::insert(netuid2, AlphaBalance::from(initial)); - // Set subnet prices so that both are - // - cut off by lower limit for tao flow method - // - zeroed out for price ema method - SubnetMovingPrice::::insert(netuid1, I96F32::from_num(0)); - SubnetMovingPrice::::insert(netuid2, I96F32::from_num(0)); - // Run coinbase - SubtensorModule::run_coinbase(emission_credit); - // Netuid 1 is cut off by lower limit, all emission goes to netuid2 - assert_eq!(SubnetAlphaIn::::get(netuid1), initial.into()); - assert_eq!(SubnetAlphaIn::::get(netuid2), initial.into()); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_mining_emission_distribution_with_no_root_sell --exact --show-output --nocapture -#[test] -fn test_mining_emission_distribution_with_no_root_sell() { - new_test_ext(1).execute_with(|| { - let validator_coldkey = U256::from(1); - let validator_hotkey = U256::from(2); - let validator_miner_coldkey = U256::from(3); - let validator_miner_hotkey = U256::from(4); - let miner_coldkey = U256::from(5); - let miner_hotkey = U256::from(6); - let netuid = NetUid::from(1); - let subnet_tempo = 10; - let stake: u64 = 100_000_000_000; - let root_stake: u64 = 200_000_000_000; // 200 TAO - - // Create root network - SubtensorModule::set_tao_weight(0); // Start tao weight at 0 - SubtokenEnabled::::insert(NetUid::ROOT, true); - NetworksAdded::::insert(NetUid::ROOT, true); - - // Add network, register hotkeys, and setup network parameters - add_network(netuid, subnet_tempo, 0); - SubnetMechanism::::insert(netuid, 1); // Set mechanism to 1 - - // Setup large LPs to prevent slippage - SubnetTAO::::insert(netuid, TaoBalance::from(1_000_000_000_000_000_u64)); - SubnetAlphaIn::::insert(netuid, AlphaBalance::from(1_000_000_000_000_000_u64)); - - register_ok_neuron(netuid, validator_hotkey, validator_coldkey, 0); - register_ok_neuron(netuid, validator_miner_hotkey, validator_miner_coldkey, 1); - register_ok_neuron(netuid, miner_hotkey, miner_coldkey, 2); - add_balance_to_coldkey_account( - &validator_coldkey, - TaoBalance::from(stake) + ExistentialDeposit::get(), - ); - add_balance_to_coldkey_account( - &validator_miner_coldkey, - TaoBalance::from(stake) + ExistentialDeposit::get(), - ); - add_balance_to_coldkey_account( - &miner_coldkey, - TaoBalance::from(stake) + ExistentialDeposit::get(), - ); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - step_block(subnet_tempo); - SubnetOwnerCut::::set(u16::MAX / 10); - // There are two validators and three neurons - MaxAllowedUids::::set(netuid, 3); - SubtensorModule::set_max_allowed_validators(netuid, 2); - - // Setup stakes: - // Stake from validator - // Stake from valiminer - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(validator_coldkey), - validator_hotkey, - netuid, - stake.into() - )); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(validator_miner_coldkey), - validator_miner_hotkey, - netuid, - stake.into() - )); - - // Setup YUMA so that it creates emissions - Weights::::insert(NetUidStorageIndex::from(netuid), 0, vec![(1, 0xFFFF)]); - Weights::::insert(NetUidStorageIndex::from(netuid), 1, vec![(2, 0xFFFF)]); - BlockAtRegistration::::set(netuid, 0, 1); - BlockAtRegistration::::set(netuid, 1, 1); - BlockAtRegistration::::set(netuid, 2, 1); - LastUpdate::::set(NetUidStorageIndex::from(netuid), vec![2, 2, 2]); - Kappa::::set(netuid, u16::MAX / 5); - ActivityCutoff::::set(netuid, u16::MAX); // makes all stake active - ValidatorPermit::::insert(netuid, vec![true, true, false]); - - // Run run_coinbase until emissions are drained - step_block(subnet_tempo); - - // Add stake to validator so it has root stake - add_balance_to_coldkey_account(&validator_coldkey, root_stake.into()); - // init root - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(validator_coldkey), - validator_hotkey, - NetUid::ROOT, - root_stake.into() - )); - // Set tao weight non zero - SubtensorModule::set_tao_weight(u64::MAX / 10); - - // Make root sell NOT happen - // set price very low, e.g. a lot of alpha in - let alpha = AlphaBalance::from(1_000_000_000_000_000_000_u64); - SubnetAlphaIn::::insert(netuid, alpha); - - // Make sure we ARE NOT root selling, so we do not have root alpha divs. - let root_sell_flag = SubtensorModule::get_network_root_sell_flag(&[netuid]); - assert!(!root_sell_flag, "Root sell flag should be false"); - - // Run run_coinbase until emissions are drained - step_block(subnet_tempo); - - let old_root_alpha_divs = PendingRootAlphaDivs::::get(netuid); - let per_block_emission = SubtensorModule::get_block_emission_for_issuance( - SubtensorModule::get_alpha_issuance(netuid).into(), - ) - .unwrap_or(0); - - // step by one block - step_block(1); - // Verify that root alpha divs - let new_root_alpha_divs = PendingRootAlphaDivs::::get(netuid); - // Check that we are indeed NOT root selling, i.e. that root alpha divs are NOT increasing - assert_eq!( - new_root_alpha_divs, old_root_alpha_divs, - "Root alpha divs should not increase" - ); - // Check root divs are zero - assert_eq!( - new_root_alpha_divs, - AlphaBalance::ZERO, - "Root alpha divs should be zero" - ); - step_block(1); - // Drain to a clean epoch boundary so accumulation starts fresh. - step_epochs(1, netuid); - let miner_stake_before_epoch = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &miner_hotkey, - &miner_coldkey, - netuid, - ); - // Run again but with some root stake - step_block(subnet_tempo - 1); - assert_abs_diff_eq!( - PendingServerEmission::::get(netuid).to_u64(), - U96F32::saturating_from_num(per_block_emission) - .saturating_mul(U96F32::saturating_from_num((subnet_tempo - 1) as u64)) - .saturating_mul(U96F32::saturating_from_num(0.5)) // miner cut - .saturating_mul(U96F32::saturating_from_num(0.90)) - .saturating_to_num::(), - epsilon = 100_000_u64.into() - ); - step_block(1); - assert!( - BlocksSinceLastStep::::get(netuid) == 0, - "Blocks since last step should be 0" - ); - - let miner_uid = Uids::::get(netuid, miner_hotkey).unwrap_or(0); - log::info!("Miner uid: {miner_uid:?}"); - let miner_incentive: AlphaBalance = { - let miner_incentive = Incentive::::get(NetUidStorageIndex::from(netuid)) - .get(miner_uid as usize) - .copied(); - - assert!(miner_incentive.is_some()); - - (miner_incentive.unwrap_or_default().deconstruct() as u64).into() - }; - log::info!("Miner incentive: {miner_incentive:?}"); - - // Miner emissions - let miner_emission_1: u64 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &miner_hotkey, - &miner_coldkey, - netuid, - ) - .to_u64() - - miner_stake_before_epoch.to_u64(); - - assert_abs_diff_eq!( - Incentive::::get(NetUidStorageIndex::from(netuid)) - .iter() - .map(|p| p.deconstruct()) - .sum::(), - u16::MAX, - epsilon = 10 - ); - - assert_abs_diff_eq!( - miner_emission_1, - U96F32::saturating_from_num(miner_incentive) - .saturating_div(u16::MAX.into()) - .saturating_mul(U96F32::saturating_from_num(per_block_emission)) - .saturating_mul(U96F32::saturating_from_num(subnet_tempo)) - .saturating_mul(U96F32::saturating_from_num(0.45)) // miner cut - .saturating_to_num::(), - epsilon = 1_000_000_u64 - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_mining_emission_distribution_with_root_sell --exact --show-output --nocapture -#[test] -fn test_mining_emission_distribution_with_root_sell() { - new_test_ext(1).execute_with(|| { - let validator_coldkey = U256::from(1); - let validator_hotkey = U256::from(2); - let validator_miner_coldkey = U256::from(3); - let validator_miner_hotkey = U256::from(4); - let miner_coldkey = U256::from(5); - let miner_hotkey = U256::from(6); - let subnet_tempo = 10; - let stake: u64 = 100_000_000_000; - let root_stake: u64 = 200_000_000_000; // 200 TAO - - // Create root network - SubtensorModule::set_tao_weight(0); // Start tao weight at 0 - SubtokenEnabled::::insert(NetUid::ROOT, true); - NetworksAdded::::insert(NetUid::ROOT, true); - - // Add network, register hotkeys, and setup network parameters - let owner_hotkey = U256::from(10); - let owner_coldkey = U256::from(11); - let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); - // Period is `tempo`; `tempo = 2` keeps a one-block gap between epochs so - // pending root-alpha-divs can be observed accumulating before a drain. - Tempo::::insert(netuid, 2); - FirstEmissionBlockNumber::::insert(netuid, 0); - - // Setup large LPs to prevent slippage - SubnetTAO::::insert(netuid, TaoBalance::from(1_000_000_000_000_000_u64)); - SubnetAlphaIn::::insert(netuid, AlphaBalance::from(1_000_000_000_000_000_u64)); - - register_ok_neuron(netuid, validator_hotkey, validator_coldkey, 0); - register_ok_neuron(netuid, validator_miner_hotkey, validator_miner_coldkey, 1); - register_ok_neuron(netuid, miner_hotkey, miner_coldkey, 2); - add_balance_to_coldkey_account( - &validator_coldkey, - TaoBalance::from(stake) + ExistentialDeposit::get(), - ); - add_balance_to_coldkey_account( - &validator_miner_coldkey, - TaoBalance::from(stake) + ExistentialDeposit::get(), - ); - add_balance_to_coldkey_account( - &miner_coldkey, - TaoBalance::from(stake) + ExistentialDeposit::get(), - ); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - step_block(subnet_tempo); - SubnetOwnerCut::::set(u16::MAX / 10); - // There are two validators and three neurons - MaxAllowedUids::::set(netuid, 3); - SubtensorModule::set_max_allowed_validators(netuid, 2); - - // Setup stakes: - // Stake from validator - // Stake from valiminer - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(validator_coldkey), - validator_hotkey, - netuid, - stake.into() - )); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(validator_miner_coldkey), - validator_miner_hotkey, - netuid, - stake.into() - )); - - // Setup YUMA so that it creates emissions - Weights::::insert(NetUidStorageIndex::from(netuid), 0, vec![(1, 0xFFFF)]); - Weights::::insert(NetUidStorageIndex::from(netuid), 1, vec![(2, 0xFFFF)]); - BlockAtRegistration::::set(netuid, 0, 1); - BlockAtRegistration::::set(netuid, 1, 1); - BlockAtRegistration::::set(netuid, 2, 1); - LastUpdate::::set(NetUidStorageIndex::from(netuid), vec![2, 2, 2]); - Kappa::::set(netuid, u16::MAX / 5); - ActivityCutoff::::set(netuid, u16::MAX); // makes all stake active - ValidatorPermit::::insert(netuid, vec![true, true, false]); - - // Run run_coinbase until emissions are drained - step_block(subnet_tempo); - - // Add stake to validator so it has root stake - add_balance_to_coldkey_account(&validator_coldkey, root_stake.into()); - // init root - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(validator_coldkey), - validator_hotkey, - NetUid::ROOT, - root_stake.into() - )); - // Set tao weight non zero - SubtensorModule::set_tao_weight(u64::MAX / 10); - - // Make root sell happen - // Set moving price > 1.0 - // Set price > 1.0 - let alpha = AlphaBalance::from(100_000_000_000_000_u64); - SubnetAlphaIn::::insert(netuid, alpha); - - SubnetMovingPrice::::insert(netuid, I96F32::from_num(2)); - - // Make sure we are root selling, so we have root alpha divs. - let root_sell_flag = SubtensorModule::get_network_root_sell_flag(&[netuid]); - assert!(root_sell_flag, "Root sell flag should be true"); - - // Run run_coinbase until emissions are drained - step_block(subnet_tempo); - - LastEpochBlock::::insert(netuid, SubtensorModule::get_current_block_as_u64()); - let old_root_alpha_divs = PendingRootAlphaDivs::::get(netuid); - let miner_stake_before_epoch = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &miner_hotkey, - &miner_coldkey, - netuid, - ); - - // step by one block - step_block(1); - // Verify root alpha divs - let new_root_alpha_divs = PendingRootAlphaDivs::::get(netuid); - // Check that we ARE root selling, i.e. that root alpha divs are changing - assert_ne!( - new_root_alpha_divs, old_root_alpha_divs, - "Root alpha divs should be changing" - ); - assert!( - new_root_alpha_divs > AlphaBalance::ZERO, - "Root alpha divs should be greater than 0" - ); - - // Run again but with some root stake - step_block(subnet_tempo - 1); - - let miner_uid = Uids::::get(netuid, miner_hotkey).unwrap_or(0); - let miner_incentive: AlphaBalance = { - let miner_incentive = Incentive::::get(NetUidStorageIndex::from(netuid)) - .get(miner_uid as usize) - .copied(); - - assert!(miner_incentive.is_some()); - - (miner_incentive.unwrap_or_default().deconstruct() as u64).into() - }; - log::info!("Miner incentive: {miner_incentive:?}"); - - let per_block_emission = SubtensorModule::get_block_emission_for_issuance( - SubtensorModule::get_alpha_issuance(netuid).into(), - ) - .unwrap_or(0); - - // Miner emissions - let miner_emission_1: u64 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &miner_hotkey, - &miner_coldkey, - netuid, - ) - .to_u64() - - miner_stake_before_epoch.to_u64(); - - assert_abs_diff_eq!( - miner_emission_1, - U96F32::saturating_from_num(miner_incentive) - .saturating_div(u16::MAX.into()) - .saturating_mul(U96F32::saturating_from_num(per_block_emission)) - .saturating_mul(U96F32::saturating_from_num(subnet_tempo)) - .saturating_mul(U96F32::saturating_from_num(0.45)) // miner cut - .saturating_to_num::(), - epsilon = 1_000_000_u64 - ); - }); -} - -#[test] -fn test_coinbase_subnets_with_no_reg_get_no_emission() { - new_test_ext(1).execute_with(|| { - let zero = U96F32::saturating_from_num(0); - let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); - let netuid1 = add_dynamic_network(&U256::from(3), &U256::from(4)); - - // Setup initial state - SubtokenEnabled::::insert(netuid0, true); - SubtokenEnabled::::insert(netuid1, true); - FirstEmissionBlockNumber::::insert(netuid0, 0); - FirstEmissionBlockNumber::::insert(netuid1, 0); - // Explicitly allow registration for both subnets - NetworkRegistrationAllowed::::insert(netuid0, true); - NetworkRegistrationAllowed::::insert(netuid1, true); - NetworkPowRegistrationAllowed::::insert(netuid0, false); - NetworkPowRegistrationAllowed::::insert(netuid1, true); - - // Note that netuid0 has only one method allowed - // And, netuid1 has *both* methods allowed - // Both should be in the list. - let subnets_to_emit_to_0 = SubtensorModule::get_subnets_to_emit_to(&[netuid0, netuid1]); - // Check that both subnets are in the list - assert_eq!(subnets_to_emit_to_0.len(), 2); - assert!(subnets_to_emit_to_0.contains(&netuid0)); - assert!(subnets_to_emit_to_0.contains(&netuid1)); - - // Disabled registration of both methods on ONLY netuid0 - NetworkRegistrationAllowed::::insert(netuid0, false); - NetworkPowRegistrationAllowed::::insert(netuid0, false); - - // Check that netuid0 is not in the list - let subnets_to_emit_to_1 = SubtensorModule::get_subnets_to_emit_to(&[netuid0, netuid1]); - assert_eq!(subnets_to_emit_to_1.len(), 1); - assert!(!subnets_to_emit_to_1.contains(&netuid0)); - // Netuid1 still in the list - assert!(subnets_to_emit_to_1.contains(&netuid1)); - }); -} - -// Tests for the excess TAO condition -#[test] -fn test_coinbase_subnet_terms_with_alpha_in_gt_alpha_emission() { - new_test_ext(1).execute_with(|| { - let zero = U96F32::saturating_from_num(0); - let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); - mock::setup_reserves( - netuid0, - TaoBalance::from(1_000_000_000_000_000_u64), - AlphaBalance::from(1_000_000_000_000_000_u64), - ); - // Initialize swap - Swap::maybe_initialize_palswap(netuid0, None); - - // Set netuid0 to have price tao_emission / price > alpha_emission - let alpha_emission = U96F32::saturating_from_num( - SubtensorModule::get_block_emission_for_issuance( - SubtensorModule::get_alpha_issuance(netuid0).into(), - ) - .unwrap_or(0), - ); - let price_to_set: U64F64 = U64F64::saturating_from_num(0.01); - let price_to_set_fixed: U96F32 = U96F32::saturating_from_num(price_to_set); - - let tao_emission: U96F32 = U96F32::saturating_from_num(alpha_emission) - .saturating_mul(price_to_set_fixed) - .saturating_add(U96F32::saturating_from_num(0.01)); - - // Set the price - let tao = TaoBalance::from(1_000_000_000_u64); - let alpha = AlphaBalance::from( - (U64F64::saturating_from_num(u64::from(tao)) / price_to_set).to_num::(), - ); - SubnetTAO::::insert(netuid0, tao); - SubnetAlphaIn::::insert(netuid0, alpha); - - // Check the price is set - assert_abs_diff_eq!( - pallet_subtensor_swap::Pallet::::current_alpha_price(netuid0).to_num::(), - price_to_set.to_num::(), - epsilon = 0.001 - ); - - let subnet_emissions = BTreeMap::from([(netuid0, tao_emission)]); - - // The injection cap is root_proportion * alpha_emission. Seed root stake so - // root_proportion is well-defined and the cap is positive. - set_full_injection_root_stake(); - let root_prop: U96F32 = SubtensorModule::root_proportion(netuid0); - let injection_cap: U96F32 = root_prop.saturating_mul(alpha_emission); - - let (tao_in, alpha_in, alpha_out, excess_tao) = - SubtensorModule::get_subnet_terms(&subnet_emissions); - - // Check our condition is met: the raw alpha_in exceeds the cap, so it binds. - assert!(tao_emission / price_to_set_fixed > injection_cap); - - // alpha_out should be the alpha_emission, always - assert_abs_diff_eq!( - alpha_out[&netuid0].to_num::(), - alpha_emission.to_num::(), - epsilon = 0.01 - ); - - // alpha_in should be capped at root_proportion * alpha_emission - assert_abs_diff_eq!( - alpha_in[&netuid0].to_num::(), - injection_cap.to_num::(), - epsilon = injection_cap.to_num::() / 1_000.0 - ); - // tao_in should be the alpha_in at the ratio of the price - assert_abs_diff_eq!( - tao_in[&netuid0].to_num::(), - alpha_in[&netuid0] - .saturating_mul(price_to_set_fixed) - .to_num::(), - epsilon = 0.01 - ); - - // excess_tao should be the difference between the tao_emission and the tao_in - assert_abs_diff_eq!( - excess_tao[&netuid0].to_num::(), - tao_emission.to_num::() - tao_in[&netuid0].to_num::(), - epsilon = 0.01 - ); - }); -} - -#[test] -fn test_coinbase_subnet_terms_with_alpha_in_lte_alpha_emission() { - new_test_ext(1).execute_with(|| { - let zero = U96F32::saturating_from_num(0); - let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); - mock::setup_reserves( - netuid0, - TaoBalance::from(1_000_000_000_000_000_u64), - AlphaBalance::from(1_000_000_000_000_000_u64), - ); - // Initialize swap - Swap::maybe_initialize_palswap(netuid0, None); - - let alpha_emission = U96F32::saturating_from_num( - SubtensorModule::get_block_emission_for_issuance( - SubtensorModule::get_alpha_issuance(netuid0).into(), - ) - .unwrap_or(0), - ); - let tao_emission = U96F32::saturating_from_num(34566756_u64); - - let price: U96F32 = U96F32::saturating_from_num(Swap::current_alpha_price(netuid0)); - - let subnet_emissions = BTreeMap::from([(netuid0, tao_emission)]); - - // The injection cap is root_proportion * alpha_emission. Seed root stake so - // the cap is large enough that raw alpha_in stays under it (no excess). - set_full_injection_root_stake(); - let root_prop: U96F32 = SubtensorModule::root_proportion(netuid0); - let injection_cap: U96F32 = root_prop.saturating_mul(alpha_emission); - - let (tao_in, alpha_in, alpha_out, excess_tao) = - SubtensorModule::get_subnet_terms(&subnet_emissions); - - // Check our condition is met: raw alpha_in stays under the cap. - assert!(tao_emission / price <= injection_cap); - - // alpha_out should be the alpha_emission, always - assert_abs_diff_eq!( - alpha_out[&netuid0].to_num::(), - alpha_emission.to_num::(), - epsilon = 0.1 - ); - - // assuming alpha_in < alpha_emission - // Then alpha_in should be tao_emission / price - assert_abs_diff_eq!( - alpha_in[&netuid0].to_num::(), - tao_emission.to_num::() / price.to_num::(), - epsilon = 0.01 - ); - - // tao_in should be the tao_emission - assert_abs_diff_eq!( - tao_in[&netuid0].to_num::(), - tao_emission.to_num::(), - epsilon = 0.01 - ); - - // excess_tao should be 0 - assert_abs_diff_eq!( - excess_tao[&netuid0].to_num::(), - tao_emission.to_num::() - tao_in[&netuid0].to_num::(), - epsilon = 0.01 - ); - }); -} - -// Tests for the inject and swap are in the right order. -#[test] -fn test_coinbase_inject_and_maybe_swap_does_not_skew_reserves() { - new_test_ext(1).execute_with(|| { - let zero = U96F32::saturating_from_num(0); - let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); - mock::setup_reserves( - netuid0, - TaoBalance::from(1_000_000_000_000_000_u64), - AlphaBalance::from(1_000_000_000_000_000_u64), - ); - // Initialize swap - Swap::maybe_initialize_palswap(netuid0, None); - - let tao_in = BTreeMap::from([(netuid0, U96F32::saturating_from_num(123))]); - let alpha_in = BTreeMap::from([(netuid0, U96F32::saturating_from_num(456))]); - // We have excess TAO, so we will be swapping with it. - let excess_tao = BTreeMap::from([(netuid0, U96F32::saturating_from_num(789100))]); - - // Run the inject and maybe swap - let credit = SubtensorModule::mint_tao((123 + 789100).into()); - SubtensorModule::inject_and_maybe_swap(&[netuid0], &tao_in, &alpha_in, &excess_tao, credit); - - let tao_in_after = SubnetTAO::::get(netuid0); - let alpha_in_after = SubnetAlphaIn::::get(netuid0); - - // Make sure that when we inject and swap, we do it in the right order. - // Thereby not skewing the ratio away from the price. - let ratio_after: U96F32 = U96F32::saturating_from_num(alpha_in_after.to_u64()) - .saturating_div(U96F32::saturating_from_num(tao_in_after.to_u64())); - let price_after: U96F32 = U96F32::saturating_from_num( - pallet_subtensor_swap::Pallet::::current_alpha_price(netuid0).to_num::(), - ); - assert_abs_diff_eq!( - ratio_after.to_num::(), - price_after.to_num::(), - epsilon = 1.0 - ); - }); -} - -#[test] -fn test_coinbase_failed_tao_materialization_does_not_activate_current_tao() { - new_test_ext(1).execute_with(|| { - let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); - let initial_reserve = TaoBalance::from(1_000_000_u64); - let reservoir_tao = TaoBalance::from(100_u64); - let current_tao = TaoBalance::from(200_u64); - let current_alpha = AlphaBalance::from(100_u64); - - mock::setup_reserves(netuid, initial_reserve, AlphaBalance::from(1_000_000_u64)); - Swap::maybe_initialize_palswap(netuid, None); - pallet_subtensor_swap::BalancerTaoReservoir::::insert(netuid, reservoir_tao); - - let tao_in = BTreeMap::from([(netuid, U96F32::saturating_from_num(current_tao))]); - let alpha_in = BTreeMap::from([(netuid, U96F32::saturating_from_num(current_alpha))]); - let excess_tao = BTreeMap::new(); - let credit = SubtensorModule::mint_tao(TaoBalance::ZERO); - - SubtensorModule::inject_and_maybe_swap(&[netuid], &tao_in, &alpha_in, &excess_tao, credit); - - assert_eq!( - SubnetTAO::::get(netuid), - initial_reserve.saturating_add(reservoir_tao) - ); - assert_eq!(SubnetTaoInEmission::::get(netuid), reservoir_tao); - assert_eq!( - SubnetProtocolFlow::::get(netuid), - reservoir_tao.to_u64() as i64 - ); - assert_eq!( - pallet_subtensor_swap::BalancerTaoReservoir::::get(netuid), - TaoBalance::ZERO - ); - }); -} - -#[test] -fn test_alpha_reservoir_counts_toward_subnet_issuance_across_blocks() { - new_test_ext(1).execute_with(|| { - let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); - let alpha_in = AlphaBalance::from(10_000_u64); - let alpha_out = AlphaBalance::from(20_000_u64); - let reservoir_alpha = AlphaBalance::from(30_000_u64); - - SubnetAlphaIn::::insert(netuid, alpha_in); - SubnetAlphaOut::::insert(netuid, alpha_out); - pallet_subtensor_swap::BalancerAlphaReservoir::::insert(netuid, reservoir_alpha); - - let expected = alpha_in - .saturating_add(alpha_out) - .saturating_add(reservoir_alpha); - assert_eq!(SubtensorModule::get_alpha_issuance(netuid), expected); - - System::set_block_number(System::block_number().saturating_add(1)); - - assert_eq!(SubnetAlphaIn::::get(netuid), alpha_in); - assert_eq!( - pallet_subtensor_swap::BalancerAlphaReservoir::::get(netuid), - reservoir_alpha - ); - assert_eq!(SubtensorModule::get_alpha_issuance(netuid), expected); - }); -} - -#[test] -fn test_coinbase_inject_and_maybe_swap_reverts_excess_tao_deposit_on_swap_failure() { - new_test_ext(1).execute_with(|| { - let zero = U96F32::saturating_from_num(0); - let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); - let tao_to_swap = TaoBalance::from(789_100_u64); - - mock::setup_reserves( - netuid, - TaoBalance::from(1_000_000_000_000_u64), - AlphaBalance::from(1_000_000_000_000_u64), - ); - Swap::maybe_initialize_palswap(netuid, None); - - // Force the buy swap to fail after the excess TAO credit is deposited. - SubnetAlphaIn::::set( - netuid, - AlphaBalance::from(u64::from(mock::SwapMinimumReserve::get()) - 1), - ); - assert!( - SubtensorModule::swap_tao_for_alpha( - netuid, - tao_to_swap, - ::SwapInterface::max_price(), - true, - ) - .is_err() - ); - - let subnet_account = SubtensorModule::get_subnet_account_id(netuid).unwrap(); - let chain_before = Balances::free_balance(subnet_account); - let subnet_tao_before = SubnetTAO::::get(netuid); - let total_issuance_before = TotalIssuance::::get(); - let balances_issuance_before = Balances::total_issuance(); - - let tao_in = BTreeMap::from([(netuid, zero)]); - let alpha_in = BTreeMap::from([(netuid, zero)]); - let excess_tao = BTreeMap::from([(netuid, U96F32::saturating_from_num(tao_to_swap))]); - let credit = SubtensorModule::mint_tao(tao_to_swap); - - SubtensorModule::inject_and_maybe_swap(&[netuid], &tao_in, &alpha_in, &excess_tao, credit); - - assert_eq!(Balances::free_balance(subnet_account), chain_before); - assert_eq!(SubnetTAO::::get(netuid), subnet_tao_before); - assert_eq!(SubnetExcessTao::::get(netuid), TaoBalance::ZERO); - assert_eq!(TotalIssuance::::get(), total_issuance_before); - assert_eq!(Balances::total_issuance(), balances_issuance_before); - }); -} - -#[test] -fn test_coinbase_drain_pending_increments_blockssincelaststep() { - new_test_ext(1).execute_with(|| { - let zero = U96F32::saturating_from_num(0); - let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); - - let blocks_since_last_step_before = BlocksSinceLastStep::::get(netuid0); - - // Check that blockssincelaststep is incremented - SubtensorModule::drain_pending(&[netuid0], 1); - - let blocks_since_last_step_after = BlocksSinceLastStep::::get(netuid0); - assert!(blocks_since_last_step_after > blocks_since_last_step_before); - assert_eq!( - blocks_since_last_step_after, - blocks_since_last_step_before + 1 - ); - }); -} - -#[test] -fn test_coinbase_drain_pending_caps_blockssincelaststep_when_epoch_is_deferred() { - new_test_ext(1).execute_with(|| { - let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); - let tempo = 1; - Tempo::::insert(netuid, tempo); - PendingEpochAt::::insert(netuid, 1); - SubtensorModule::set_max_epochs_per_block(0); - - for block in 1..=10 { - SubtensorModule::drain_pending(&[netuid], block); - } - - assert_eq!( - BlocksSinceLastStep::::get(netuid), - u64::from(tempo) + 1 - ); - assert!(SubtensorModule::should_run_epoch(netuid, 11)); - }); -} - -#[test] -fn test_coinbase_drain_pending_caps_blockssincelaststep_for_inconsistent_epoch() { - new_test_ext(1).execute_with(|| { - let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); - let tempo = 1; - Tempo::::insert(netuid, tempo); - PendingEpochAt::::insert(netuid, 1); - - let duplicate_hotkey = U256::from(99); - Keys::::insert(netuid, 0, duplicate_hotkey); - Keys::::insert(netuid, 1, duplicate_hotkey); - assert!(!SubtensorModule::is_epoch_input_state_consistent(netuid)); - - for block in 1..=10 { - SubtensorModule::drain_pending(&[netuid], block); - } - - assert_eq!( - BlocksSinceLastStep::::get(netuid), - u64::from(tempo) + 1 - ); - assert!(SubtensorModule::should_run_epoch(netuid, 11)); - }); -} - -#[test] -fn test_should_run_epoch_uses_subnet_tempo_for_step_age_safety_net() { - new_test_ext(1).execute_with(|| { - let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); - let tempo = 1; - Tempo::::insert(netuid, tempo); - LastEpochBlock::::insert(netuid, 100); - PendingEpochAt::::insert(netuid, 0); - BlocksSinceLastStep::::insert(netuid, u64::from(tempo) + 1); - - assert!(SubtensorModule::should_run_epoch(netuid, 2)); - }); -} - -#[test] -fn test_coinbase_drain_pending_resets_blockssincelaststep() { - new_test_ext(1).execute_with(|| { - let zero = U96F32::saturating_from_num(0); - let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); - Tempo::::insert(netuid0, 100); - LastEpochBlock::::insert(netuid0, 0); - let block_number = 102; - assert!(SubtensorModule::should_run_epoch(netuid0, block_number)); - - let blocks_since_last_step_before = 12345678; - BlocksSinceLastStep::::insert(netuid0, blocks_since_last_step_before); - LastMechansimStepBlock::::insert(netuid0, 12345); // garbage value - - // Check that blockssincelaststep is reset to 0 on tempo - SubtensorModule::drain_pending(&[netuid0], block_number); - - let blocks_since_last_step_after = BlocksSinceLastStep::::get(netuid0); - assert_eq!(blocks_since_last_step_after, 0); - assert_eq!(LastMechansimStepBlock::::get(netuid0), 12345); - }); -} - -#[test] -fn test_coinbase_drain_pending_gets_counters_and_resets_them() { - new_test_ext(1).execute_with(|| { - let zero = U96F32::saturating_from_num(0); - let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); - Tempo::::insert(netuid0, 100); - LastEpochBlock::::insert(netuid0, 0); - let block_number = 102; - assert!(SubtensorModule::should_run_epoch(netuid0, block_number)); - - let pending_server_em = AlphaBalance::from(123434534); - let pending_validator_em = AlphaBalance::from(111111); - let pending_root = AlphaBalance::from(12222222); - let pending_owner_cut = AlphaBalance::from(12345678); - - PendingServerEmission::::insert(netuid0, pending_server_em); - PendingValidatorEmission::::insert(netuid0, pending_validator_em); - PendingRootAlphaDivs::::insert(netuid0, pending_root); - PendingOwnerCut::::insert(netuid0, pending_owner_cut); - - let emissions_to_distribute = SubtensorModule::drain_pending(&[netuid0], block_number); - assert_eq!(emissions_to_distribute.len(), 1); - assert_eq!( - emissions_to_distribute[&netuid0], - ( - pending_server_em, - pending_validator_em, - pending_root, - pending_owner_cut - ) - ); - - // Check that the pending emissions are reset - assert_eq!( - PendingServerEmission::::get(netuid0), - AlphaBalance::ZERO - ); - assert_eq!( - PendingValidatorEmission::::get(netuid0), - AlphaBalance::ZERO - ); - assert_eq!( - PendingRootAlphaDivs::::get(netuid0), - AlphaBalance::ZERO - ); - assert_eq!(PendingOwnerCut::::get(netuid0), AlphaBalance::ZERO); - }); -} - -#[test] -fn test_coinbase_emit_to_subnets_with_no_root_sell() { - new_test_ext(1).execute_with(|| { - let zero = U96F32::saturating_from_num(0); - let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); - // Set owner cut to ~10% - SubnetOwnerCut::::set(u16::MAX / 10); - mock::setup_reserves( - netuid0, - TaoBalance::from(1_000_000_000_000_000_u64), - AlphaBalance::from(1_000_000_000_000_000_u64), - ); - // Initialize swap - Swap::maybe_initialize_palswap(netuid0, None); - - let tao_emission = U96F32::saturating_from_num(12345678); - let subnet_emissions = BTreeMap::from([(netuid0, tao_emission)]); - - // NO root sell - let root_sell_flag = false; - - let alpha_emission = U96F32::saturating_from_num( - SubtensorModule::get_block_emission_for_issuance( - SubtensorModule::get_alpha_issuance(netuid0).into(), - ) - .unwrap_or(0), - ); - let price: U96F32 = U96F32::saturating_from_num(Swap::current_alpha_price(netuid0)); - let (tao_in, alpha_in, alpha_out, excess_tao) = - SubtensorModule::get_subnet_terms(&subnet_emissions); - // Based on the price, we should have NO excess TAO - assert!(tao_emission / price <= alpha_emission); - - // ==== Run the emit to subnets ===== - let credit = SubtensorModule::mint_tao(12345678.into()); - SubtensorModule::emit_to_subnets(&[netuid0], &subnet_emissions, credit, root_sell_flag); - - // Find the owner cut expected - let owner_cut: U96F32 = SubtensorModule::get_float_subnet_owner_cut(); - let owner_cut_expected: U96F32 = owner_cut.saturating_mul(alpha_emission); - log::info!("owner_cut_expected: {owner_cut_expected:?}"); - log::info!("alpha_emission: {alpha_emission:?}"); - log::info!("owner_cut: {owner_cut:?}"); - - let alpha_issuance: U96F32 = - U96F32::saturating_from_num(SubtensorModule::get_alpha_issuance(netuid0)); - let root_tao: U96F32 = U96F32::saturating_from_num(SubnetTAO::::get(NetUid::ROOT)); - let tao_weight: U96F32 = root_tao.saturating_mul(SubtensorModule::get_tao_weight()); - let root_prop: U96F32 = tao_weight - .checked_div(tao_weight.saturating_add(alpha_issuance)) - .unwrap_or(U96F32::min_value()); - // Expect root alpha divs to be root prop * alpha emission - let expected_root_alpha_divs: AlphaBalance = AlphaBalance::from( - root_prop - .saturating_mul(alpha_emission) - .saturating_to_num::(), - ); - - // ===== Check that the pending emissions are set correctly ===== - // Owner cut is as expected - assert_abs_diff_eq!( - PendingOwnerCut::::get(netuid0).to_u64(), - owner_cut_expected.saturating_to_num::(), - epsilon = 200_u64 - ); - // NO root sell, so no root alpha divs - assert_eq!( - PendingRootAlphaDivs::::get(netuid0), - AlphaBalance::ZERO - ); - // Should be alpha_emission minus the owner cut, - assert_abs_diff_eq!( - PendingServerEmission::::get(netuid0).to_u64(), - alpha_emission - .saturating_sub(owner_cut_expected) - .saturating_div(U96F32::saturating_from_num(2)) - .saturating_to_num::(), - epsilon = 200_u64 - ); - // We ALWAYS deduct the root alpha divs - assert_abs_diff_eq!( - PendingValidatorEmission::::get(netuid0).to_u64(), - alpha_emission - .saturating_sub(owner_cut_expected) - .saturating_div(U96F32::saturating_from_num(2)) - .saturating_sub(expected_root_alpha_divs.to_u64().into()) - .saturating_to_num::(), - epsilon = 200_u64 - ); - }); -} - -#[test] -fn test_coinbase_emit_to_subnets_with_root_sell() { - new_test_ext(1).execute_with(|| { - let zero = U96F32::saturating_from_num(0); - let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); - // Set owner cut to ~10% - SubnetOwnerCut::::set(u16::MAX / 10); - mock::setup_reserves( - netuid0, - TaoBalance::from(1_000_000_000_000_000_u64), - AlphaBalance::from(1_000_000_000_000_000_u64), - ); - // Initialize swap - Swap::maybe_initialize_palswap(netuid0, None); - - let tao_emission = U96F32::saturating_from_num(12345678); - let subnet_emissions = BTreeMap::from([(netuid0, tao_emission)]); - - // NO root sell - let root_sell_flag = true; - - let alpha_emission: U96F32 = U96F32::saturating_from_num( - SubtensorModule::get_block_emission_for_issuance( - SubtensorModule::get_alpha_issuance(netuid0).into(), - ) - .unwrap_or(0), - ); - let price: U96F32 = U96F32::saturating_from_num(Swap::current_alpha_price(netuid0)); - let (tao_in, alpha_in, alpha_out, excess_tao) = - SubtensorModule::get_subnet_terms(&subnet_emissions); - // Based on the price, we should have NO excess TAO - assert!(tao_emission / price <= alpha_emission); - - // ==== Run the emit to subnets ===== - let credit = SubtensorModule::mint_tao(12345678.into()); - SubtensorModule::emit_to_subnets(&[netuid0], &subnet_emissions, credit, root_sell_flag); - - // Find the owner cut expected - let owner_cut: U96F32 = SubtensorModule::get_float_subnet_owner_cut(); - let owner_cut_expected: U96F32 = owner_cut.saturating_mul(alpha_emission); - log::info!("owner_cut_expected: {owner_cut_expected:?}"); - log::info!("alpha_emission: {alpha_emission:?}"); - log::info!("owner_cut: {owner_cut:?}"); - - let alpha_issuance: U96F32 = - U96F32::saturating_from_num(SubtensorModule::get_alpha_issuance(netuid0)); - let root_tao: U96F32 = U96F32::saturating_from_num(SubnetTAO::::get(NetUid::ROOT)); - let tao_weight: U96F32 = root_tao.saturating_mul(SubtensorModule::get_tao_weight()); - let root_prop: U96F32 = tao_weight - .checked_div(tao_weight.saturating_add(alpha_issuance)) - .unwrap_or(U96F32::min_value()); - // Expect root alpha divs to be root prop * alpha emission - let expected_root_alpha_divs: AlphaBalance = AlphaBalance::from( - root_prop - .saturating_mul(alpha_emission) - .saturating_to_num::(), - ); - - // ===== Check that the pending emissions are set correctly ===== - // Owner cut is as expected - assert_abs_diff_eq!( - PendingOwnerCut::::get(netuid0).to_u64(), - owner_cut_expected.saturating_to_num::(), - epsilon = 200_u64 - ); - // YES root sell, so we have root alpha divs - assert_abs_diff_eq!( - PendingRootAlphaDivs::::get(netuid0).to_u64(), - expected_root_alpha_divs.to_u64(), - epsilon = 200_u64 - ); - // Should be alpha_emission minus the owner cut - assert_abs_diff_eq!( - PendingServerEmission::::get(netuid0).to_u64(), - alpha_emission - .saturating_sub(owner_cut_expected) - .saturating_div(U96F32::saturating_from_num(2)) - .saturating_to_num::(), - epsilon = 200_u64 - ); - // Validator emission is also minus root alpha divs - assert_abs_diff_eq!( - PendingValidatorEmission::::get(netuid0).to_u64(), - alpha_emission - .saturating_sub(owner_cut_expected) - .saturating_div(U96F32::saturating_from_num(2)) - .saturating_sub(expected_root_alpha_divs.to_u64().into()) - .saturating_to_num::(), - epsilon = 200_u64 - ); - }); -} - -// cargo test --package pallet-subtensor --lib -- tests::coinbase::test_disabling_owner_cut_sends_subnet_emission_to_miners_and_validators --exact --nocapture -#[test] -fn test_disabling_owner_cut_sends_subnet_emission_to_miners_and_validators() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let validator_coldkey = U256::from(1); - let validator_hotkey = U256::from(2); - let miner_coldkey = U256::from(5); - let miner_hotkey = U256::from(6); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - LastEpochBlock::::insert(netuid, SubtensorModule::get_current_block_as_u64()); - let subnet_tempo = 10; - let stake = 100_000_000_000u64; - - SubtensorModule::set_tempo_unchecked(netuid, subnet_tempo); - setup_reserves(netuid, (stake * 10_000).into(), (stake * 10_000).into()); - - register_ok_neuron(netuid, validator_hotkey, validator_coldkey, 0); - register_ok_neuron(netuid, miner_hotkey, miner_coldkey, 1); - - add_balance_to_coldkey_account( - &validator_coldkey, - TaoBalance::from(stake) + ExistentialDeposit::get(), - ); - - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(validator_coldkey), - validator_hotkey, - netuid, - stake.into() - )); - - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - SubtensorModule::set_max_allowed_validators(netuid, 1); - step_block(subnet_tempo); - - SubnetOwnerCut::::set(u16::MAX / 10); - SubtensorModule::set_owner_cut_enabled_flag(netuid, false); - - let owner_uid = - SubtensorModule::get_uid_for_net_and_hotkey(netuid, &subnet_owner_hotkey).unwrap(); - let validator_uid = - SubtensorModule::get_uid_for_net_and_hotkey(netuid, &validator_hotkey).unwrap(); - let miner_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &miner_hotkey).unwrap(); - let uid_count = [ - owner_uid as usize, - validator_uid as usize, - miner_uid as usize, - ] - .into_iter() - .max() - .unwrap() - + 1; - - Weights::::insert( - NetUidStorageIndex::from(netuid), - validator_uid, - vec![(miner_uid, 0xFFFF)], - ); - BlockAtRegistration::::set(netuid, owner_uid, 1); - BlockAtRegistration::::set(netuid, validator_uid, 1); - BlockAtRegistration::::set(netuid, miner_uid, 1); - LastUpdate::::set(NetUidStorageIndex::from(netuid), vec![2; uid_count]); - Kappa::::set(netuid, u16::MAX / 5); - ActivityCutoff::::set(netuid, u16::MAX); - let mut validator_permit = vec![false; uid_count]; - validator_permit[validator_uid as usize] = true; - ValidatorPermit::::insert(netuid, validator_permit); - - let owner_stake_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &subnet_owner_hotkey, - &subnet_owner_coldkey, - netuid, - ); - let validator_stake_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &validator_hotkey, - &validator_coldkey, - netuid, - ); - let miner_stake_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &miner_hotkey, - &miner_coldkey, - netuid, - ); - - // Disabling owner cut removes the subnet owner from emission distribution, so the - // subnet emission is fully distributed across the validator and miner paths instead. - step_block(subnet_tempo); - - let owner_stake_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &subnet_owner_hotkey, - &subnet_owner_coldkey, - netuid, - ); - let validator_stake_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &validator_hotkey, - &validator_coldkey, - netuid, - ); - let miner_stake_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &miner_hotkey, - &miner_coldkey, - netuid, - ); - - assert_eq!(owner_stake_after, owner_stake_before); - assert!(validator_stake_after > validator_stake_before); - assert!(miner_stake_after > miner_stake_before); - assert_eq!(PendingOwnerCut::::get(netuid), AlphaBalance::ZERO); - assert!( - Lock::::iter_prefix((subnet_owner_coldkey, netuid)) - .next() - .is_none() - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_pending_emission_start_call_not_done --exact --show-output --nocapture -#[test] -fn test_pending_emission_start_call_not_done() { - new_test_ext(1).execute_with(|| { - let validator_coldkey = U256::from(1); - let validator_hotkey = U256::from(2); - let subnet_tempo = 10; - let stake: u64 = 100_000_000_000; - let root_stake: u64 = 200_000_000_000; // 200 TAO - - // Create root network - NetworksAdded::::insert(NetUid::ROOT, true); - // enabled root - SubtokenEnabled::::insert(NetUid::ROOT, true); - - // Add network, register hotkeys, and setup network parameters - let owner_hotkey = U256::from(10); - let owner_coldkey = U256::from(11); - let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); - // Remove FirstEmissionBlockNumber - FirstEmissionBlockNumber::::remove(netuid); - Tempo::::insert(netuid, subnet_tempo); - - register_ok_neuron(netuid, validator_hotkey, validator_coldkey, 0); - add_balance_to_coldkey_account( - &validator_coldkey, - TaoBalance::from(stake) + ExistentialDeposit::get(), - ); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - step_block(subnet_tempo); - SubnetOwnerCut::::set(u16::MAX / 10); - // There are two validators and three neurons - MaxAllowedUids::::set(netuid, 3); - SubtensorModule::set_max_allowed_validators(netuid, 2); - - // Add stake to validator so it has root stake - add_balance_to_coldkey_account(&validator_coldkey, root_stake.into()); - // init root - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(validator_coldkey), - validator_hotkey, - NetUid::ROOT, - root_stake.into() - )); - // Set tao weight non zero - SubtensorModule::set_tao_weight(u64::MAX / 10); - - // Make root sell happen - // Set moving price > 1.0 - // Set price > 1.0 - let tao = TaoBalance::from(10_000_000_000_u64); - let alpha = AlphaBalance::from(1_000_000_000_u64); - SubnetTAO::::insert(netuid, tao); - SubnetAlphaIn::::insert(netuid, alpha); - - SubnetMovingPrice::::insert(netuid, I96F32::from_num(2)); - - // Make sure we are root selling, so we have root alpha divs. - let root_sell_flag = SubtensorModule::get_network_root_sell_flag(&[netuid]); - assert!(root_sell_flag, "Root sell flag should be true"); - - // !!! Check that the subnet FirstEmissionBlockNumber is None -- no entry - assert!(FirstEmissionBlockNumber::::get(netuid).is_none()); - - // Run run_coinbase until emissions are accumulated - step_block(subnet_tempo - 2); - - // Verify that all pending emissions are zero - assert_eq!( - PendingServerEmission::::get(netuid), - AlphaBalance::ZERO - ); - assert_eq!( - PendingValidatorEmission::::get(netuid), - AlphaBalance::ZERO - ); - assert_eq!( - PendingRootAlphaDivs::::get(netuid), - AlphaBalance::ZERO - ); - }); -} - -#[test] -fn test_root_prop_filled_on_block_step() { - new_test_ext(1).execute_with(|| { - let hotkey = U256::from(10); - let coldkey = U256::from(11); - let netuid1 = add_dynamic_network(&hotkey, &coldkey); - let netuid2 = add_dynamic_network(&hotkey, &coldkey); - - SubnetTAO::::insert(NetUid::ROOT, TaoBalance::from(1_000_000_000_000u64)); - SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 - - let tao_reserve = TaoBalance::from(50_000_000_000_u64); - let alpha_in = AlphaBalance::from(100_000_000_000_u64); - SubnetTAO::::insert(netuid1, tao_reserve); - SubnetAlphaIn::::insert(netuid1, alpha_in); - SubnetTAO::::insert(netuid2, tao_reserve); - SubnetAlphaIn::::insert(netuid2, alpha_in); - - assert!(!RootProp::::contains_key(netuid1)); - assert!(!RootProp::::contains_key(netuid2)); - - run_to_block(2); - - assert!(RootProp::::get(netuid1) > U96F32::from_num(0)); - assert!(RootProp::::get(netuid2) > U96F32::from_num(0)); - }); -} - -#[test] -fn test_root_proportion() { - new_test_ext(1).execute_with(|| { - let hotkey = U256::from(10); - let coldkey = U256::from(11); - let netuid = add_dynamic_network(&hotkey, &coldkey); - - let root_tao_reserve = 1_000_000_000_000u64; - SubnetTAO::::insert(NetUid::ROOT, TaoBalance::from(root_tao_reserve)); - - let tao_weight = 3_320_413_933_267_719_290u64; - SubtensorModule::set_tao_weight(tao_weight); - - let alpha_in = 100_000_000_000u64; - SubnetAlphaIn::::insert(netuid, AlphaBalance::from(alpha_in)); - - let actual_root_proportion = SubtensorModule::root_proportion(netuid); - let expected_root_prop = { - let tao_weight = SubtensorModule::get_tao_weight(); - let root_tao = U96F32::from_num(root_tao_reserve); - let alpha_in = { - let alpha: u64 = SubtensorModule::get_alpha_issuance(netuid).into(); - - U96F32::from_num(alpha) - }; - - tao_weight * root_tao / (tao_weight * root_tao + alpha_in) - }; - - assert_eq!(actual_root_proportion, expected_root_prop); - }); -} - -#[test] -fn test_get_subnet_terms_alpha_emissions_cap() { - new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(10); - let owner_coldkey = U256::from(11); - let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); - - // The injection cap is now root_proportion * alpha_emission. Seed root stake - // so root_proportion is well-defined, and derive the cap from the live values. - set_full_injection_root_stake(); - let alpha_emission_i: U96F32 = U96F32::saturating_from_num( - SubtensorModule::get_block_emission_for_issuance( - SubtensorModule::get_alpha_issuance(netuid).into(), - ) - .unwrap_or(0), - ); - let injection_cap: U96F32 = - SubtensorModule::root_proportion(netuid).saturating_mul(alpha_emission_i); - - // price = 1.0, alpha_in_i (== emissions1) <= alpha_injection_cap (not capped) - let emissions1 = U96F32::from_num(100_000_000); - assert!(emissions1 < injection_cap); - - let subnet_emissions1 = BTreeMap::from([(netuid, emissions1)]); - let (_, alpha_in, _, _) = SubtensorModule::get_subnet_terms(&subnet_emissions1); - - assert_eq!(alpha_in.get(&netuid).copied().unwrap(), emissions1); - - // price = 1.0, alpha_in_i (== emissions2) > alpha_injection_cap (capped) - let emissions2 = U96F32::from_num(10_000_000_000u64); - assert!(emissions2 > injection_cap); - - let subnet_emissions2 = BTreeMap::from([(netuid, emissions2)]); - let (_, alpha_in, _, _) = SubtensorModule::get_subnet_terms(&subnet_emissions2); - - assert_eq!(alpha_in.get(&netuid).copied().unwrap(), injection_cap); - }); -} - -#[test] -fn test_epochs_deferred_this_block_respects_cap() { - new_test_ext(1).execute_with(|| { - let cap = SubtensorModule::get_max_epochs_per_block() as usize; - let n = cap + 2; - - for i in 0..n { - let netuid = NetUid::from((i + 1) as u16); - add_network(netuid, 100, 0); - // Force "due this block". - PendingEpochAt::::insert(netuid, 1); - } - - let block = SubtensorModule::get_current_block_as_u64(); - let subnets: Vec = SubtensorModule::get_all_subnet_netuids() - .into_iter() - .filter(|x| *x != NetUid::ROOT) - .collect(); - - // All `n` subnets are due, but only `cap` may fire — the rest are deferred. - let deferred = SubtensorModule::epochs_deferred_this_block(&subnets, block); - assert_eq!( - deferred.len(), - n - cap, - "exactly the due subnets beyond MaxEpochsPerBlock are deferred" - ); - for netuid in &deferred { - assert!(SubtensorModule::should_run_epoch(*netuid, block)); - } - }); -} - -// Regression test for the dynamic-tempo / CR-v3 interaction: when a subnet's epoch -// is deferred by the per-block cap, its timelock reveal must be held back to the -// deferred fire-block (not run on the originally-scheduled block, which would -// surface weights before the epoch consumes them). -// -// Crypto-free probe: the reveal path removes *expired* commits only when it runs -// for a subnet, so a retained expired (epoch-0) commit means the reveal was skipped. -#[test] -fn test_reveal_crv3_defers_with_capped_epoch() { - new_test_ext(1).execute_with(|| { - let cap = SubtensorModule::get_max_epochs_per_block() as usize; - let n = cap + 2; - let mec0 = subtensor_runtime_common::MechId::from(0); - - for i in 0..n { - let netuid = NetUid::from((i + 1) as u16); - add_network(netuid, 100, 0); - PendingEpochAt::::insert(netuid, 1); // due this block - SubnetEpochIndex::::insert(netuid, 10); // cur_epoch >> reveal_period - // Plant an expired commit at epoch 0 (field types inferred from the queue). - let idx = SubtensorModule::get_mechanism_storage_index(netuid, mec0); - TimelockedWeightCommits::::mutate(idx, 0u64, |q| { - q.push_back((U256::from(1u64), 0u64, Default::default(), 0u64)); - }); - } - - let subnets: Vec = SubtensorModule::get_all_subnet_netuids() - .into_iter() - .filter(|x| *x != NetUid::ROOT) - .collect(); - - let still_holds = |netuid: NetUid| -> bool { - let idx = SubtensorModule::get_mechanism_storage_index(netuid, mec0); - TimelockedWeightCommits::::contains_key(idx, 0u64) - }; - let retained = |subnets: &[NetUid]| subnets.iter().filter(|n| still_holds(**n)).count(); - - // --- Phase 1: cap-deferred subnets must NOT reveal this block. - SubtensorModule::reveal_crv3_commits(); - assert_eq!( - retained(&subnets), - n - cap, - "only cap-deferred subnets keep their commit (their reveal was skipped)" - ); - - let deferred: Vec = subnets - .iter() - .copied() - .filter(|n| still_holds(*n)) - .collect(); - - // --- Phase 2: drop the cap pressure so only the deferred subnets are due; - // they should now reveal (and clean their expired commit). - for netuid in &subnets { - if !deferred.contains(netuid) { - PendingEpochAt::::insert(*netuid, 0); - LastEpochBlock::::insert(*netuid, 1); // blocks_since < tempo => not due - } - } - SubtensorModule::reveal_crv3_commits(); - assert_eq!( - retained(&subnets), - 0, - "deferred subnets reveal once they actually fire" - ); - }); -} - -// SKIP_WASM_BUILD=1 cargo test -p pallet-subtensor --lib alpha_dividends -- --nocapture -#[test] -fn test_alpha_dividends_release_collateral_on_full_emission() { - new_test_ext(1).execute_with(|| { - let owner_ck = U256::from(10); - let owner_hk = U256::from(11); - let sn_owner_ck = U256::from(0); - let sn_owner_hk = U256::from(1); - - Owner::::insert(owner_hk, owner_ck); - OwnedHotkeys::::insert(owner_ck, vec![owner_hk]); - - let netuid = add_dynamic_network(&sn_owner_hk, &sn_owner_ck); - Uids::::insert(netuid, owner_hk, 1); - Delegates::::insert(owner_hk, PerU16::from_parts(u16::MAX / 2)); - - let locked: AlphaBalance = 5_000_000u64.into(); - MinerCollateral::::insert( - (netuid, owner_hk, owner_ck), - MinerCollateralState { - locked, - drain_ratio: U64F64::from_num(1), - min_locked: AlphaBalance::ZERO, - earned: AlphaBalance::ZERO, - }, - ); - ColdkeyMinerCollateral::::insert(netuid, owner_ck, locked); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &owner_hk, - &owner_ck, - netuid, - 1_000_000u64.into(), - ); - - let dividend: U96F32 = U96F32::from_num(1_000_000u64); - let mut alpha_dividends: BTreeMap = BTreeMap::new(); - alpha_dividends.insert(owner_hk, dividend); - - SubtensorModule::distribute_dividends_and_incentives( - netuid, - AlphaBalance::ZERO, - BTreeMap::new(), - alpha_dividends, - BTreeMap::new(), - ); - - let alpha_take = SubtensorModule::get_hotkey_take_float(&owner_hk).saturating_mul(dividend); - let nominator: AlphaBalance = dividend - .saturating_sub(alpha_take) - .saturating_to_num::() - .into(); - let state = - MinerCollateral::::get((netuid, owner_hk, owner_ck)).expect("still locked"); - assert_eq!(state.locked, locked.saturating_sub(1_000_000u64.into())); - assert_eq!(state.earned, 1_000_000u64.into()); - assert_eq!( - AlphaDividendsPerSubnet::::get(netuid, owner_hk), - nominator - ); - }); -} - -#[test] -fn test_alpha_dividends_floor_capture_only_from_take() { - new_test_ext(1).execute_with(|| { - let owner_ck = U256::from(20); - let owner_hk = U256::from(21); - let sn_owner_ck = U256::from(0); - let sn_owner_hk = U256::from(1); - - Owner::::insert(owner_hk, owner_ck); - OwnedHotkeys::::insert(owner_ck, vec![owner_hk]); - - let netuid = add_dynamic_network(&sn_owner_hk, &sn_owner_ck); - Uids::::insert(netuid, owner_hk, 1); - Delegates::::insert(owner_hk, PerU16::from_parts(u16::MAX / 2)); - - let locked: AlphaBalance = 100u64.into(); - let min_locked: AlphaBalance = 10_000_000u64.into(); - MinerCollateral::::insert( - (netuid, owner_hk, owner_ck), - MinerCollateralState { - locked, - drain_ratio: U64F64::from_num(1), - min_locked, - earned: AlphaBalance::ZERO, - }, - ); - ColdkeyMinerCollateral::::insert(netuid, owner_ck, locked); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &owner_hk, - &owner_ck, - netuid, - 1_000_000u64.into(), - ); - let owner_stake_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &owner_hk, &owner_ck, netuid, - ); - - let dividend: U96F32 = U96F32::from_num(1_000_000u64); - let alpha_take = SubtensorModule::get_hotkey_take_float(&owner_hk).saturating_mul(dividend); - let take: AlphaBalance = alpha_take.saturating_to_num::().into(); - let nominator: AlphaBalance = dividend - .saturating_sub(alpha_take) - .saturating_to_num::() - .into(); - let mut alpha_dividends: BTreeMap = BTreeMap::new(); - alpha_dividends.insert(owner_hk, dividend); - - SubtensorModule::distribute_dividends_and_incentives( - netuid, - AlphaBalance::ZERO, - BTreeMap::new(), - alpha_dividends, - BTreeMap::new(), - ); - - let state = - MinerCollateral::::get((netuid, owner_hk, owner_ck)).expect("still locked"); - assert_eq!(state.locked, locked.saturating_add(take)); - assert_eq!(state.earned, 1_000_000u64.into()); - assert_eq!( - AlphaDividendsPerSubnet::::get(netuid, owner_hk), - nominator - ); - - let owner_stake_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &owner_hk, &owner_ck, netuid, - ); - let gained = owner_stake_after.saturating_sub(owner_stake_before); - assert!( - gained >= 999_999u64.into() && gained <= 1_000_000u64.into(), - "unexpected owner stake gain: {gained:?}" - ); - }); -} diff --git a/pallets/subtensor/src/tests/coinbase/alpha_dividends.rs b/pallets/subtensor/src/tests/coinbase/alpha_dividends.rs new file mode 100644 index 0000000000..5e3c0f91ff --- /dev/null +++ b/pallets/subtensor/src/tests/coinbase/alpha_dividends.rs @@ -0,0 +1,149 @@ +#![allow( + unused, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used +)] +//! Alpha dividend collateral release and take-floor capture. + +use super::helpers::*; +use super::prelude::*; + +// SKIP_WASM_BUILD=1 cargo test -p pallet-subtensor --lib alpha_dividends -- --nocapture +#[test] +fn test_alpha_dividends_release_collateral_on_full_emission() { + new_test_ext(1).execute_with(|| { + let owner_ck = U256::from(10); + let owner_hk = U256::from(11); + let sn_owner_ck = U256::from(0); + let sn_owner_hk = U256::from(1); + + Owner::::insert(owner_hk, owner_ck); + OwnedHotkeys::::insert(owner_ck, vec![owner_hk]); + + let netuid = add_dynamic_network(&sn_owner_hk, &sn_owner_ck); + Uids::::insert(netuid, owner_hk, 1); + Delegates::::insert(owner_hk, PerU16::from_parts(u16::MAX / 2)); + + let locked: AlphaBalance = 5_000_000u64.into(); + MinerCollateral::::insert( + (netuid, owner_hk, owner_ck), + MinerCollateralState { + locked, + drain_ratio: U64F64::from_num(1), + min_locked: AlphaBalance::ZERO, + earned: AlphaBalance::ZERO, + }, + ); + ColdkeyMinerCollateral::::insert(netuid, owner_ck, locked); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &owner_hk, + &owner_ck, + netuid, + 1_000_000u64.into(), + ); + + let dividend: U96F32 = U96F32::from_num(1_000_000u64); + let mut alpha_dividends: BTreeMap = BTreeMap::new(); + alpha_dividends.insert(owner_hk, dividend); + + SubtensorModule::distribute_dividends_and_incentives( + netuid, + AlphaBalance::ZERO, + BTreeMap::new(), + alpha_dividends, + BTreeMap::new(), + ); + + let alpha_take = SubtensorModule::get_hotkey_take_float(&owner_hk).saturating_mul(dividend); + let nominator: AlphaBalance = dividend + .saturating_sub(alpha_take) + .saturating_to_num::() + .into(); + let state = + MinerCollateral::::get((netuid, owner_hk, owner_ck)).expect("still locked"); + assert_eq!(state.locked, locked.saturating_sub(1_000_000u64.into())); + assert_eq!(state.earned, 1_000_000u64.into()); + assert_eq!( + AlphaDividendsPerSubnet::::get(netuid, owner_hk), + nominator + ); + }); +} + +#[test] +fn test_alpha_dividends_floor_capture_only_from_take() { + new_test_ext(1).execute_with(|| { + let owner_ck = U256::from(20); + let owner_hk = U256::from(21); + let sn_owner_ck = U256::from(0); + let sn_owner_hk = U256::from(1); + + Owner::::insert(owner_hk, owner_ck); + OwnedHotkeys::::insert(owner_ck, vec![owner_hk]); + + let netuid = add_dynamic_network(&sn_owner_hk, &sn_owner_ck); + Uids::::insert(netuid, owner_hk, 1); + Delegates::::insert(owner_hk, PerU16::from_parts(u16::MAX / 2)); + + let locked: AlphaBalance = 100u64.into(); + let min_locked: AlphaBalance = 10_000_000u64.into(); + MinerCollateral::::insert( + (netuid, owner_hk, owner_ck), + MinerCollateralState { + locked, + drain_ratio: U64F64::from_num(1), + min_locked, + earned: AlphaBalance::ZERO, + }, + ); + ColdkeyMinerCollateral::::insert(netuid, owner_ck, locked); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &owner_hk, + &owner_ck, + netuid, + 1_000_000u64.into(), + ); + let owner_stake_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &owner_hk, &owner_ck, netuid, + ); + + let dividend: U96F32 = U96F32::from_num(1_000_000u64); + let alpha_take = SubtensorModule::get_hotkey_take_float(&owner_hk).saturating_mul(dividend); + let take: AlphaBalance = alpha_take.saturating_to_num::().into(); + let nominator: AlphaBalance = dividend + .saturating_sub(alpha_take) + .saturating_to_num::() + .into(); + let mut alpha_dividends: BTreeMap = BTreeMap::new(); + alpha_dividends.insert(owner_hk, dividend); + + SubtensorModule::distribute_dividends_and_incentives( + netuid, + AlphaBalance::ZERO, + BTreeMap::new(), + alpha_dividends, + BTreeMap::new(), + ); + + let state = + MinerCollateral::::get((netuid, owner_hk, owner_ck)).expect("still locked"); + assert_eq!(state.locked, locked.saturating_add(take)); + assert_eq!(state.earned, 1_000_000u64.into()); + assert_eq!( + AlphaDividendsPerSubnet::::get(netuid, owner_hk), + nominator + ); + + let owner_stake_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &owner_hk, &owner_ck, netuid, + ); + let gained = owner_stake_after.saturating_sub(owner_stake_before); + assert!( + gained >= 999_999u64.into() && gained <= 1_000_000u64.into(), + "unexpected owner stake gain: {gained:?}" + ); + }); +} diff --git a/pallets/subtensor/src/tests/coinbase/alpha_issuance.rs b/pallets/subtensor/src/tests/coinbase/alpha_issuance.rs new file mode 100644 index 0000000000..5df720eeca --- /dev/null +++ b/pallets/subtensor/src/tests/coinbase/alpha_issuance.rs @@ -0,0 +1,213 @@ +#![allow( + unused, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used +)] +//! Alpha issuance and emission-cap triggers. + +use super::helpers::*; +use super::prelude::*; + +// Test basic alpha issuance in coinbase mechanism. +// This test verifies that: +// - Alpha issuance is initialized to 0 for new subnets +// - Alpha issuance is split evenly between subnets during coinbase +// - Each subnet receives the expected fraction of total emission +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::alpha_issuance::test_coinbase_alpha_issuance_base --exact --show-output --nocapture +#[test] +fn test_coinbase_alpha_issuance_base() { + new_test_ext(1).execute_with(|| { + let netuid1 = NetUid::from(1); + let netuid2 = NetUid::from(2); + let emission: u64 = 1_000_000; + let emission_credit = SubtensorModule::mint_tao(emission.into()); + add_network(netuid1, 1, 0); + add_network(netuid2, 1, 0); + // Set up prices 1 and 1 + let initial: u64 = 1_000_000; + SubnetTAO::::insert(netuid1, TaoBalance::from(initial)); + SubnetAlphaIn::::insert(netuid1, AlphaBalance::from(initial)); + SubnetTAO::::insert(netuid2, TaoBalance::from(initial)); + SubnetAlphaIn::::insert(netuid2, AlphaBalance::from(initial)); + // Keep root_proportion ~1 so the injection cap does not bind. + set_full_injection_root_stake(); + // Check initial + SubtensorModule::run_coinbase(emission_credit); + // tao_in = 500_000 + // alpha_in = 500_000/price = 500_000 + assert_eq!( + SubnetAlphaIn::::get(netuid1), + (initial + emission / 2).into() + ); + assert_eq!( + SubnetAlphaIn::::get(netuid2), + (initial + emission / 2).into() + ); + }); +} + +// Test alpha issuance with different subnet flows. +// This test verifies that: +// - Alpha issuance is proportional to subnet flows +// - Higher priced subnets receive more TAO emission +// - Alpha issuance is correctly calculated based on price ratios +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::alpha_issuance::test_coinbase_alpha_issuance_different --exact --show-output --nocapture +#[test] +fn test_coinbase_alpha_issuance_different() { + new_test_ext(1).execute_with(|| { + let netuid1 = NetUid::from(1); + let netuid2 = NetUid::from(2); + let emission: u64 = 1_000_000; + let emission_credit = SubtensorModule::mint_tao(emission.into()); + add_network(netuid1, 1, 0); + add_network(netuid2, 1, 0); + // Make subnets dynamic. + SubnetMechanism::::insert(netuid1, 1); + SubnetMechanism::::insert(netuid2, 1); + // Setup prices 1 and 2 + let initial: u64 = 1_000_000; + SubnetTAO::::insert(netuid1, TaoBalance::from(initial)); + SubnetAlphaIn::::insert(netuid1, AlphaBalance::from(initial)); + SubnetTAO::::insert(netuid2, TaoBalance::from(2 * initial)); + SubnetAlphaIn::::insert(netuid2, AlphaBalance::from(initial)); + // Price-based shares with prices 1 and 2 (1:2 ratio). + SubnetMovingPrice::::insert(netuid1, I96F32::from_num(1)); + SubnetMovingPrice::::insert(netuid2, I96F32::from_num(2)); + // Keep root_proportion ~1 so the injection cap does not bind. + set_full_injection_root_stake(); + // Run coinbase + SubtensorModule::run_coinbase(emission_credit); + // tao_in = 333_333 + // alpha_in = 333_333/price = 333_333 + initial + assert_eq!( + SubnetAlphaIn::::get(netuid1), + (initial + emission / 3).into() + ); + // tao_in = 666_666 + // alpha_in = 666_666/price = 333_333 + initial + assert_eq!( + SubnetAlphaIn::::get(netuid2), + (initial + (emission * 2 / 3) / 2).into() + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::alpha_issuance::test_coinbase_alpha_issuance_with_cap_trigger --exact --show-output --nocapture +#[test] +fn test_coinbase_alpha_issuance_with_cap_trigger() { + new_test_ext(1).execute_with(|| { + let netuid1 = NetUid::from(1); + let netuid2 = NetUid::from(2); + let emission: u64 = 1_000_000; + let emission_credit = SubtensorModule::mint_tao(emission.into()); + add_network(netuid1, 1, 0); + add_network(netuid2, 1, 0); + // Make subnets dynamic. + SubnetMechanism::::insert(netuid1, 1); + SubnetMechanism::::insert(netuid2, 1); + // Setup prices 1000000 + let initial: u64 = 1_000; + let initial_alpha: u64 = initial * 1000000; + SubnetTAO::::insert(netuid1, TaoBalance::from(initial)); + SubnetAlphaIn::::insert(netuid1, AlphaBalance::from(initial_alpha)); // Make price extremely low. + SubnetTAO::::insert(netuid2, TaoBalance::from(initial)); + SubnetAlphaIn::::insert(netuid2, AlphaBalance::from(initial_alpha)); // Make price extremely low. + // Set subnet prices. + SubnetMovingPrice::::insert(netuid1, I96F32::from_num(1)); + SubnetMovingPrice::::insert(netuid2, I96F32::from_num(2)); + // Keep root_proportion ~1 so the injection cap binds at alpha_emission. + set_full_injection_root_stake(); + // Run coinbase + SubtensorModule::run_coinbase(emission_credit); + // alpha_in is capped at the injection cap, so injected alpha stays below + // a full block emission on top of the initial reserve. + assert!(SubnetAlphaIn::::get(netuid1) < (initial_alpha + 1_000_000_000).into()); + // Per-block alpha emission is the full block emission regardless of the cap. + assert_eq!( + SubnetAlphaOutEmission::::get(netuid1), + 1_000_000_000.into() + ); + assert!(SubnetAlphaIn::::get(netuid2) < (initial_alpha + 1_000_000_000).into()); + assert_eq!( + SubnetAlphaOutEmission::::get(netuid2), + 1_000_000_000.into() + ); // Gets full block emission. + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::alpha_issuance::test_coinbase_alpha_issuance_with_cap_trigger_and_block_emission --exact --show-output --nocapture +#[test] +fn test_coinbase_alpha_issuance_with_cap_trigger_and_block_emission() { + new_test_ext(1).execute_with(|| { + let netuid1 = NetUid::from(1); + let netuid2 = NetUid::from(2); + let emission: u64 = 1_000_000; + let emission_credit = SubtensorModule::mint_tao(emission.into()); + add_network(netuid1, 1, 0); + add_network(netuid2, 1, 0); + + // Make subnets dynamic. + SubnetMechanism::::insert(netuid1, 1); + SubnetMechanism::::insert(netuid2, 1); + + // Setup prices 0.000001 + let initial_tao: u64 = 10_000_u64; + let initial_alpha: u64 = initial_tao * 100_000_u64; + mock::setup_reserves(netuid1, initial_tao.into(), initial_alpha.into()); + mock::setup_reserves(netuid2, initial_tao.into(), initial_alpha.into()); + + // Enable emission + FirstEmissionBlockNumber::::insert(netuid1, 0); + FirstEmissionBlockNumber::::insert(netuid2, 0); + // Price-based shares (1:2 ratio). Low pool prices mean alpha_in exceeds the + // injection cap, so the surplus TAO is spent on chain buys. + SubnetMovingPrice::::insert(netuid1, I96F32::from_num(1)); + SubnetMovingPrice::::insert(netuid2, I96F32::from_num(2)); + + // Force the swap to initialize + ::SwapInterface::init_swap(netuid1, None); + ::SwapInterface::init_swap(netuid2, None); + + // Get the prices before the run_coinbase + let price_1_before = ::SwapInterface::current_alpha_price(netuid1); + let price_2_before = ::SwapInterface::current_alpha_price(netuid2); + + // Set issuance at 21M + SubnetAlphaOut::::insert(netuid1, AlphaBalance::from(21_000_000_000_000_000_u64)); // Set issuance above 21M + SubnetAlphaOut::::insert(netuid2, AlphaBalance::from(21_000_000_000_000_000_u64)); // Set issuance above 21M + + // Run coinbase + SubtensorModule::run_coinbase(emission_credit); + + // New behavior: chain-bought alpha is cached instead of recycled. + // The cached amount remains part of outstanding alpha supply. + assert!( + !SubnetProtocolAlpha::::get(netuid1).is_zero() + || !SubnetProtocolAlpha::::get(netuid2).is_zero() + ); + + // Get the prices after the run_coinbase + let price_1_after = ::SwapInterface::current_alpha_price(netuid1); + let price_2_after = ::SwapInterface::current_alpha_price(netuid2); + + // AlphaIn gets decreased beacuse of a buy + assert!(u64::from(SubnetAlphaIn::::get(netuid1)) < initial_alpha); + assert_eq!( + u64::from(SubnetAlphaOut::::get(netuid2)), + 21_000_000_000_000_000_u64 + .saturating_add(u64::from(SubnetProtocolAlpha::::get(netuid2))) + ); + assert!(u64::from(SubnetAlphaIn::::get(netuid2)) < initial_alpha); + assert_eq!( + u64::from(SubnetAlphaOut::::get(netuid2)), + 21_000_000_000_000_000_u64 + .saturating_add(u64::from(SubnetProtocolAlpha::::get(netuid2))) + ); + + assert!(price_1_after > price_1_before); + assert!(price_2_after > price_2_before); + }); +} diff --git a/pallets/subtensor/src/tests/coinbase/distribute_emission.rs b/pallets/subtensor/src/tests/coinbase/distribute_emission.rs new file mode 100644 index 0000000000..bbd97d3211 --- /dev/null +++ b/pallets/subtensor/src/tests/coinbase/distribute_emission.rs @@ -0,0 +1,178 @@ +#![allow( + unused, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used +)] +//! Distribute-emission edge cases (no miners, zero emission). + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_distribute_emission_no_miners_all_drained() { + new_test_ext(1).execute_with(|| { + let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); + remove_owner_registration_stake(netuid); + let hotkey = U256::from(3); + let coldkey = U256::from(4); + let init_stake = 1; + SubtensorModule::set_burn(netuid, TaoBalance::from(0)); + register_ok_neuron(netuid, hotkey, coldkey, 0); + // Give non-zero stake + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + init_stake.into(), + ); + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey), + init_stake.into() + ); + + // Set the weight of root TAO to be 0%, so only alpha is effective. + SubtensorModule::set_tao_weight(0); + + // Set the emission to be 1 million. + let emission = AlphaBalance::from(1_000_000); + // Run drain pending without any miners. + SubtensorModule::distribute_emission( + netuid, + emission.saturating_div(2.into()).into(), + emission.saturating_div(2.into()).into(), + AlphaBalance::ZERO, + AlphaBalance::ZERO, + ); + + // Get the new stake of the hotkey. + let new_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + // We expect this neuron to get *all* the emission. + // Slight epsilon due to rounding (hotkey_take). + assert_abs_diff_eq!( + new_stake, + u64::from(emission + init_stake.into()).into(), + epsilon = 1.into() + ); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::coinbase::distribute_emission::test_distribute_emission_zero_emission --exact --show-output +#[test] +fn test_distribute_emission_zero_emission() { + new_test_ext(1).execute_with(|| { + let netuid = add_dynamic_network_disable_commit_reveal(&U256::from(1), &U256::from(2)); + let hotkey = U256::from(3); + let coldkey = U256::from(4); + let miner_hk = U256::from(5); + let miner_ck = U256::from(6); + let init_stake: u64 = 100_000_000_000_000; + let tempo = 2; + SubtensorModule::set_tempo_unchecked(netuid, tempo); + // Set weight-set limit to 0. + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + register_ok_neuron(netuid, hotkey, coldkey, 0); + register_ok_neuron(netuid, miner_hk, miner_ck, 0); + // Give non-zero stake + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + init_stake.into(), + ); + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey), + init_stake.into() + ); + + // Set the weight of root TAO to be 0%, so only alpha is effective. + SubtensorModule::set_tao_weight(0); + + run_to_block_no_epoch(netuid, 50); + + // Run epoch for initial setup. + SubtensorModule::epoch(netuid, AlphaBalance::ZERO); + + // Set weights on miner + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid, + vec![0, 1, 2], + vec![0, 0, 1], + 0, + )); + + run_to_block_no_epoch(netuid, 50); + + // Clear incentive and dividends. + Incentive::::remove(NetUidStorageIndex::from(netuid)); + Dividends::::remove(netuid); + + // Capture stake right before the zero-emission distribution so the assertion + // isolates that call (the subnet legitimately accrues emission during the + // preceding block runs under price-based shares). + let stake_before_distribute = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + + // Set the emission to be ZERO. + SubtensorModule::distribute_emission( + netuid, + AlphaBalance::ZERO, + AlphaBalance::ZERO, + AlphaBalance::ZERO, + AlphaBalance::ZERO, + ); + + // Get the new stake of the hotkey. + let new_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + // We expect the stake to remain unchanged by the zero-emission distribution. + assert_eq!(new_stake, stake_before_distribute); + + // Check that the incentive and dividends are set by epoch. + assert!( + Incentive::::get(NetUidStorageIndex::from(netuid)) + .iter() + .map(|p| p.deconstruct()) + .sum::() + > 0 + ); + assert!( + Dividends::::get(netuid) + .iter() + .map(|p| p.deconstruct()) + .sum::() + > 0 + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::distribute_emission::test_zero_shares_zero_emission --exact --show-output --nocapture +#[test] +fn test_zero_shares_zero_emission() { + new_test_ext(1).execute_with(|| { + let subnet_owner_ck = U256::from(0); + let subnet_owner_hk = U256::from(1); + let netuid1 = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); + let netuid2 = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); + let emission: u64 = 1_000_000; + let emission_credit = SubtensorModule::mint_tao(emission.into()); + // Setup prices 1 and 1 + let initial: u64 = 1_000_000; + SubnetTAO::::insert(netuid1, TaoBalance::from(initial)); + SubnetAlphaIn::::insert(netuid1, AlphaBalance::from(initial)); + SubnetTAO::::insert(netuid2, TaoBalance::from(initial)); + SubnetAlphaIn::::insert(netuid2, AlphaBalance::from(initial)); + // Set subnet prices so that both are + // - cut off by lower limit for tao flow method + // - zeroed out for price ema method + SubnetMovingPrice::::insert(netuid1, I96F32::from_num(0)); + SubnetMovingPrice::::insert(netuid2, I96F32::from_num(0)); + // Run coinbase + SubtensorModule::run_coinbase(emission_credit); + // Netuid 1 is cut off by lower limit, all emission goes to netuid2 + assert_eq!(SubnetAlphaIn::::get(netuid1), initial.into()); + assert_eq!(SubnetAlphaIn::::get(netuid2), initial.into()); + }); +} diff --git a/pallets/subtensor/src/tests/coinbase/dividend_distribution.rs b/pallets/subtensor/src/tests/coinbase/dividend_distribution.rs new file mode 100644 index 0000000000..f98cbbceaa --- /dev/null +++ b/pallets/subtensor/src/tests/coinbase/dividend_distribution.rs @@ -0,0 +1,404 @@ +#![allow( + unused, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used +)] +//! Dividend and incentive distribution math helpers. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_calculate_dividend_distribution_totals() { + new_test_ext(1).execute_with(|| { + let mut stake_map: BTreeMap = BTreeMap::new(); + let mut dividends: BTreeMap = BTreeMap::new(); + + let pending_validator_alpha = AlphaBalance::from(183_123_567_452_u64); + let pending_root_alpha = AlphaBalance::from(837_120_949_872_u64); + let tao_weight: U96F32 = U96F32::from_num(0.18); // 18% + + let hotkeys = [U256::from(0), U256::from(1)]; + + // Stake map and dividends shouldn't matter for this test. + stake_map.insert(hotkeys[0], (4_859_302.into(), 2_342_352.into())); + stake_map.insert(hotkeys[1], (23_423.into(), 859_273.into())); + dividends.insert(hotkeys[0], 77_783_738_u64.into()); + dividends.insert(hotkeys[1], 19_283_940_u64.into()); + + let (alpha_dividends, root_alpha_dividends) = + SubtensorModule::calculate_dividend_distribution( + pending_validator_alpha, + pending_root_alpha, + tao_weight, + stake_map, + dividends, + ); + + // Verify the total of each dividends type is close to the inputs. + let total_alpha_dividends = alpha_dividends.values().sum::(); + let total_root_alpha_dividends = root_alpha_dividends.values().sum::(); + + assert_abs_diff_eq!( + total_alpha_dividends.to_num::(), + u64::from(pending_validator_alpha), + epsilon = 1_000 + ); + assert_abs_diff_eq!( + total_root_alpha_dividends.to_num::(), + pending_root_alpha.to_u64(), + epsilon = 1_000 + ); + }); +} + +#[test] +fn test_calculate_dividend_distribution_total_only_tao() { + new_test_ext(1).execute_with(|| { + let mut stake_map: BTreeMap = BTreeMap::new(); + let mut dividends: BTreeMap = BTreeMap::new(); + + let pending_validator_alpha = AlphaBalance::ZERO; + let pending_root_alpha = AlphaBalance::from(837_120_949_872_u64); + let tao_weight: U96F32 = U96F32::from_num(0.18); // 18% + + let hotkeys = [U256::from(0), U256::from(1)]; + + // Stake map and dividends shouldn't matter for this test. + stake_map.insert(hotkeys[0], (4_859_302.into(), 2_342_352.into())); + stake_map.insert(hotkeys[1], (23_423.into(), 859_273.into())); + dividends.insert(hotkeys[0], 77_783_738_u64.into()); + dividends.insert(hotkeys[1], 19_283_940_u64.into()); + + let (alpha_dividends, root_alpha_dividends) = + SubtensorModule::calculate_dividend_distribution( + pending_validator_alpha, + pending_root_alpha, + tao_weight, + stake_map, + dividends, + ); + + // Verify the total of each dividends type is close to the inputs. + let total_alpha_dividends = alpha_dividends.values().sum::(); + let total_root_alpha_dividends = root_alpha_dividends.values().sum::(); + + assert_abs_diff_eq!( + total_alpha_dividends.to_num::(), + u64::from(pending_validator_alpha), + epsilon = 1_000 + ); + assert_abs_diff_eq!( + total_root_alpha_dividends.to_num::(), + pending_root_alpha.to_u64(), + epsilon = 1_000 + ); + }); +} + +#[test] +fn test_calculate_dividend_distribution_total_no_tao_weight() { + new_test_ext(1).execute_with(|| { + let mut stake_map: BTreeMap = BTreeMap::new(); + let mut dividends: BTreeMap = BTreeMap::new(); + + let pending_validator_alpha = AlphaBalance::from(183_123_567_452_u64); + let pending_tao = TaoBalance::ZERO; // If tao weight is 0, then only alpha dividends should be input. + let tao_weight: U96F32 = U96F32::from_num(0.0); // 0% + + let hotkeys = [U256::from(0), U256::from(1)]; + + // Stake map and dividends shouldn't matter for this test. + stake_map.insert(hotkeys[0], (4_859_302.into(), 2_342_352.into())); + stake_map.insert(hotkeys[1], (23_423.into(), 859_273.into())); + dividends.insert(hotkeys[0], 77_783_738_u64.into()); + dividends.insert(hotkeys[1], 19_283_940_u64.into()); + + let (alpha_dividends, tao_dividends) = SubtensorModule::calculate_dividend_distribution( + pending_validator_alpha, + // pending_tao, + AlphaBalance::ZERO, + tao_weight, + stake_map, + dividends, + ); + + // Verify the total of each dividends type is close to the inputs. + let total_alpha_dividends = alpha_dividends.values().sum::(); + let total_tao_dividends = tao_dividends.values().sum::(); + + assert_abs_diff_eq!( + total_alpha_dividends.to_num::(), + u64::from(pending_validator_alpha), + epsilon = 1_000 + ); + assert_abs_diff_eq!( + total_tao_dividends.to_num::(), + pending_tao.to_u64(), + epsilon = 1_000 + ); + }); +} + +#[test] +fn test_calculate_dividend_distribution_total_only_alpha() { + new_test_ext(1).execute_with(|| { + let mut stake_map: BTreeMap = BTreeMap::new(); + let mut dividends: BTreeMap = BTreeMap::new(); + + let pending_validator_alpha = AlphaBalance::from(183_123_567_452_u64); + let pending_tao = TaoBalance::ZERO; + let tao_weight: U96F32 = U96F32::from_num(0.18); // 18% + + let hotkeys = [U256::from(0), U256::from(1)]; + + // Stake map and dividends shouldn't matter for this test. + stake_map.insert(hotkeys[0], (4_859_302.into(), 2_342_352.into())); + stake_map.insert(hotkeys[1], (23_423.into(), 859_273.into())); + dividends.insert(hotkeys[0], 77_783_738_u64.into()); + dividends.insert(hotkeys[1], 19_283_940_u64.into()); + + let (alpha_dividends, tao_dividends) = SubtensorModule::calculate_dividend_distribution( + pending_validator_alpha, + // pending_tao, + AlphaBalance::ZERO, + tao_weight, + stake_map, + dividends, + ); + + // Verify the total of each dividends type is close to the inputs. + let total_alpha_dividends = alpha_dividends.values().sum::(); + let total_tao_dividends = tao_dividends.values().sum::(); + + assert_abs_diff_eq!( + total_alpha_dividends.to_num::(), + u64::from(pending_validator_alpha), + epsilon = 1_000 + ); + assert_abs_diff_eq!( + total_tao_dividends.to_num::(), + pending_tao.to_u64(), + epsilon = 1_000 + ); + }); +} + +#[test] +fn test_calculate_dividend_and_incentive_distribution() { + new_test_ext(1).execute_with(|| { + let sn_owner_hk = U256::from(0); + let sn_owner_ck = U256::from(1); + let netuid = add_dynamic_network(&sn_owner_hk, &sn_owner_ck); + + // Register a single neuron. + let hotkey = U256::from(1); + let coldkey = U256::from(2); + register_ok_neuron(netuid, hotkey, coldkey, 0); + // Give non-zero alpha + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + 1.into(), + ); + + let pending_alpha = AlphaBalance::from(123_456_789); + let pending_validator_alpha = pending_alpha / 2.into(); // Pay half to validators. + let pending_tao = TaoBalance::ZERO; + let pending_swapped = 0; // Only alpha output. + let tao_weight: U96F32 = U96F32::from_num(0.0); // 0% + + // Hotkey, Incentive, Dividend + let hotkey_emission = vec![(hotkey, pending_alpha / 2.into(), pending_alpha / 2.into())]; + + let (incentives, (alpha_dividends, tao_dividends)) = + SubtensorModule::calculate_dividend_and_incentive_distribution( + netuid, + // pending_tao, + AlphaBalance::ZERO, + pending_validator_alpha, + hotkey_emission, + tao_weight, + ); + + let incentives_total = incentives.values().copied().map(u64::from).sum::(); + let dividends_total = alpha_dividends.values().sum::().to_num::(); + + assert_abs_diff_eq!( + dividends_total + incentives_total, + u64::from(pending_alpha), + epsilon = 2 + ); + }); +} + +#[test] +fn test_calculate_dividend_and_incentive_distribution_all_to_validators() { + new_test_ext(1).execute_with(|| { + let sn_owner_hk = U256::from(0); + let sn_owner_ck = U256::from(1); + let netuid = add_dynamic_network(&sn_owner_hk, &sn_owner_ck); + + // Register a single neuron. + let hotkey = U256::from(1); + let coldkey = U256::from(2); + register_ok_neuron(netuid, hotkey, coldkey, 0); + // Give non-zero alpha + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + 1.into(), + ); + + let pending_alpha = AlphaBalance::from(123_456_789); + let pending_validator_alpha = pending_alpha; // Pay all to validators. + let pending_tao = TaoBalance::ZERO; + let tao_weight: U96F32 = U96F32::from_num(0.0); // 0% + + // Hotkey, Incentive, Dividend + let hotkey_emission = vec![(hotkey, 0.into(), pending_alpha)]; + + let (incentives, (alpha_dividends, tao_dividends)) = + SubtensorModule::calculate_dividend_and_incentive_distribution( + netuid, + // pending_tao, + AlphaBalance::ZERO, + pending_validator_alpha, + hotkey_emission, + tao_weight, + ); + + let incentives_total = incentives.values().copied().map(u64::from).sum::(); + let dividends_total = alpha_dividends.values().sum::().to_num::(); + + assert_eq!( + AlphaBalance::from(dividends_total + incentives_total), + pending_alpha + ); + }); +} + +#[test] +fn test_calculate_dividends_and_incentives() { + new_test_ext(1).execute_with(|| { + let sn_owner_hk = U256::from(0); + let sn_owner_ck = U256::from(1); + let netuid = add_dynamic_network(&sn_owner_hk, &sn_owner_ck); + + // Register a single neuron. + let hotkey = U256::from(1); + let coldkey = U256::from(2); + register_ok_neuron(netuid, hotkey, coldkey, 0); + // Give non-zero alpha + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + 1.into(), + ); + + let divdends = AlphaBalance::from(123_456_789); + let incentive = AlphaBalance::from(683_051_923); + let total_emission = divdends + incentive; + + // Hotkey, Incentive, Dividend + let hotkey_emission = vec![(hotkey, incentive, divdends)]; + + let (incentives, dividends) = + SubtensorModule::calculate_dividends_and_incentives(netuid, hotkey_emission); + + let incentives_total = incentives + .values() + .copied() + .fold(AlphaBalance::ZERO, |acc, x| acc + x); + let dividends_total = + AlphaBalance::from(dividends.values().sum::().to_num::()); + + assert_eq!(dividends_total + incentives_total, total_emission); + }); +} + +#[test] +fn test_calculate_dividends_and_incentives_only_validators() { + new_test_ext(1).execute_with(|| { + let sn_owner_hk = U256::from(0); + let sn_owner_ck = U256::from(1); + let netuid = add_dynamic_network(&sn_owner_hk, &sn_owner_ck); + + // Register a single neuron. + let hotkey = U256::from(1); + let coldkey = U256::from(2); + register_ok_neuron(netuid, hotkey, coldkey, 0); + // Give non-zero alpha + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + 1.into(), + ); + + let divdends = AlphaBalance::from(123_456_789); + let incentive = AlphaBalance::ZERO; + + // Hotkey, Incentive, Dividend + let hotkey_emission = vec![(hotkey, incentive, divdends)]; + + let (incentives, dividends) = + SubtensorModule::calculate_dividends_and_incentives(netuid, hotkey_emission); + + let incentives_total = incentives + .values() + .copied() + .fold(AlphaBalance::ZERO, |acc, x| acc + x); + let dividends_total = + AlphaBalance::from(dividends.values().sum::().to_num::()); + + assert_eq!(dividends_total, divdends); + assert_eq!(incentives_total, AlphaBalance::ZERO); + }); +} + +#[test] +fn test_calculate_dividends_and_incentives_only_miners() { + new_test_ext(1).execute_with(|| { + let sn_owner_hk = U256::from(0); + let sn_owner_ck = U256::from(1); + let netuid = add_dynamic_network(&sn_owner_hk, &sn_owner_ck); + + // Register a single neuron. + let hotkey = U256::from(1); + let coldkey = U256::from(2); + register_ok_neuron(netuid, hotkey, coldkey, 0); + // Give non-zero alpha + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + 1.into(), + ); + + let divdends = AlphaBalance::ZERO; + let incentive = AlphaBalance::from(123_456_789); + + // Hotkey, Incentive, Dividend + let hotkey_emission = vec![(hotkey, incentive, divdends)]; + + let (incentives, dividends) = + SubtensorModule::calculate_dividends_and_incentives(netuid, hotkey_emission); + + let incentives_total = incentives + .values() + .copied() + .fold(AlphaBalance::ZERO, |acc, x| acc + x); + let dividends_total = + AlphaBalance::from(dividends.values().sum::().to_num::()); + + assert_eq!(incentives_total, incentive); + assert_eq!(dividends_total, divdends); + }); +} diff --git a/pallets/subtensor/src/tests/coinbase/drain_emission.rs b/pallets/subtensor/src/tests/coinbase/drain_emission.rs new file mode 100644 index 0000000000..5fa14ddbda --- /dev/null +++ b/pallets/subtensor/src/tests/coinbase/drain_emission.rs @@ -0,0 +1,559 @@ +#![allow( + unused, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used +)] +//! Drain pending emission to stakers, including childkey edges. + +use super::helpers::*; +use super::prelude::*; + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::drain_emission::test_drain_base --exact --show-output --nocapture +#[test] +fn test_drain_base() { + new_test_ext(1).execute_with(|| { + SubtensorModule::distribute_emission( + 0.into(), + AlphaBalance::ZERO, + AlphaBalance::ZERO, + AlphaBalance::ZERO, + AlphaBalance::ZERO, + ) + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::drain_emission::test_drain_base_with_subnet --exact --show-output --nocapture +#[test] +fn test_drain_base_with_subnet() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + SubtensorModule::distribute_emission( + netuid, + AlphaBalance::ZERO, + AlphaBalance::ZERO, + AlphaBalance::ZERO, + AlphaBalance::ZERO, + ) + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::drain_emission::test_drain_base_with_subnet_with_single_staker_not_registered --exact --show-output --nocapture +#[test] +fn test_drain_base_with_subnet_with_single_staker_not_registered() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + let hotkey = U256::from(1); + let coldkey = U256::from(2); + let stake_before = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + stake_before, + ); + let pending_alpha = AlphaBalance::from(1_000_000_000); + SubtensorModule::distribute_emission( + netuid, + pending_alpha.saturating_div(2.into()).into(), + pending_alpha.saturating_div(2.into()).into(), + AlphaBalance::ZERO, + AlphaBalance::ZERO, + ); + let stake_after = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + assert_eq!(stake_before, stake_after); // Not registered. + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::drain_emission::test_drain_base_with_subnet_with_single_staker_registered --exact --show-output --nocapture +#[test] +fn test_drain_base_with_subnet_with_single_staker_registered() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + let hotkey = U256::from(1); + let coldkey = U256::from(2); + let stake_before = AlphaBalance::from(1_000_000_000); + register_ok_neuron(netuid, hotkey, coldkey, 0); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + stake_before, + ); + let pending_alpha = AlphaBalance::from(1_000_000_000); + SubtensorModule::distribute_emission( + netuid, + pending_alpha.saturating_div(2.into()).into(), + pending_alpha.saturating_div(2.into()).into(), + AlphaBalance::ZERO, + AlphaBalance::ZERO, + ); + let stake_after = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + close( + (stake_before + pending_alpha).into(), + stake_after.into(), + 10, + ); // Registered gets all emission. + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::drain_emission::test_drain_base_with_subnet_with_single_staker_registered_root_weight --exact --show-output --nocapture +#[test] +fn test_drain_base_with_subnet_with_single_staker_registered_root_weight() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + let hotkey = U256::from(1); + let coldkey = U256::from(2); + let stake_before = AlphaBalance::from(1_000_000_000); + // register_ok_neuron(root, hotkey, coldkey, 0); + register_ok_neuron(netuid, hotkey, coldkey, 0); + Delegates::::insert(hotkey, PerU16::zero()); + SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + NetUid::ROOT, + stake_before, + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + stake_before, + ); + let pending_alpha = AlphaBalance::from(1_000_000_000); + let pending_root_alpha = AlphaBalance::from(1_000_000_000); + assert_eq!(SubnetTAO::::get(NetUid::ROOT), TaoBalance::ZERO); + SubtensorModule::distribute_emission( + netuid, + pending_alpha.saturating_div(2.into()).into(), + pending_alpha.saturating_div(2.into()).into(), + pending_root_alpha, + AlphaBalance::ZERO, + ); + let stake_after = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + let root_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + NetUid::ROOT, + ); + close( + (stake_before + pending_alpha).into(), + stake_after.into(), + 10, + ); // Registered gets all alpha emission. + close(stake_before.to_u64(), root_after.into(), 10); // Registered doesn't get tao immediately + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::drain_emission::test_drain_base_with_subnet_with_two_stakers_registered --exact --show-output --nocapture +#[test] +fn test_drain_base_with_subnet_with_two_stakers_registered() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + let hotkey1 = U256::from(1); + let hotkey2 = U256::from(2); + let coldkey = U256::from(3); + let stake_before = AlphaBalance::from(1_000_000_000); + register_ok_neuron(netuid, hotkey1, coldkey, 0); + register_ok_neuron(netuid, hotkey2, coldkey, 0); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &coldkey, + netuid, + stake_before, + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey2, + &coldkey, + netuid, + stake_before, + ); + let pending_alpha = AlphaBalance::from(1_000_000_000); + SubtensorModule::distribute_emission( + netuid, + pending_alpha.saturating_div(2.into()).into(), + pending_alpha.saturating_div(2.into()).into(), + AlphaBalance::ZERO, + AlphaBalance::ZERO, + ); + let stake_after1 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey1, &coldkey, netuid); + let stake_after2 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey2, &coldkey, netuid); + close( + (stake_before + pending_alpha / 2.into()).into(), + stake_after1.into(), + 10, + ); // Registered gets 1/2 emission + close( + (stake_before + pending_alpha / 2.into()).into(), + stake_after2.into(), + 10, + ); // Registered gets 1/2 emission. + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::drain_emission::test_drain_base_with_subnet_with_two_stakers_registered_and_root --exact --show-output --nocapture +#[test] +fn test_drain_base_with_subnet_with_two_stakers_registered_and_root() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + let hotkey1 = U256::from(1); + let hotkey2 = U256::from(2); + let coldkey = U256::from(3); + let stake_before = AlphaBalance::from(1_000_000_000); + register_ok_neuron(netuid, hotkey1, coldkey, 0); + register_ok_neuron(netuid, hotkey2, coldkey, 0); + Delegates::::insert(hotkey1, PerU16::zero()); + Delegates::::insert(hotkey2, PerU16::zero()); + SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &coldkey, + netuid, + stake_before, + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &coldkey, + NetUid::ROOT, + stake_before, + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey2, + &coldkey, + netuid, + stake_before, + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey2, + &coldkey, + NetUid::ROOT, + stake_before, + ); + let pending_tao = TaoBalance::from(1_000_000_000); + let pending_alpha = AlphaBalance::from(1_000_000_000); + assert_eq!(SubnetTAO::::get(NetUid::ROOT), TaoBalance::ZERO); + SubtensorModule::distribute_emission( + netuid, + pending_alpha.saturating_div(2.into()).into(), + pending_alpha.saturating_div(2.into()).into(), + AlphaBalance::ZERO, + AlphaBalance::ZERO, + ); + let stake_after1 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey1, &coldkey, netuid); + let root_after1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &coldkey, + NetUid::ROOT, + ); + let stake_after2 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey2, &coldkey, netuid); + let root_after2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey2, + &coldkey, + NetUid::ROOT, + ); + close( + (stake_before + pending_alpha / 2.into()).into(), + stake_after1.into(), + 10, + ); // Registered gets 1/2 emission + close( + (stake_before + pending_alpha / 2.into()).into(), + stake_after2.into(), + 10, + ); // Registered gets 1/2 emission. + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::drain_emission::test_drain_base_with_subnet_with_two_stakers_registered_and_root_different_amounts --exact --show-output --nocapture +#[test] +fn test_drain_base_with_subnet_with_two_stakers_registered_and_root_different_amounts() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + let hotkey1 = U256::from(1); + let hotkey2 = U256::from(2); + let coldkey = U256::from(3); + let stake_before = AlphaBalance::from(1_000_000_000); + Delegates::::insert(hotkey1, PerU16::zero()); + Delegates::::insert(hotkey2, PerU16::zero()); + register_ok_neuron(netuid, hotkey1, coldkey, 0); + register_ok_neuron(netuid, hotkey2, coldkey, 0); + SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &coldkey, + netuid, + stake_before, + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &coldkey, + NetUid::ROOT, + stake_before * 2.into(), // Hotkey 1 has twice as much root weight. + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey2, + &coldkey, + netuid, + stake_before, + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey2, + &coldkey, + NetUid::ROOT, + stake_before, + ); + let pending_tao = TaoBalance::from(1_000_000_000); + let pending_alpha = AlphaBalance::from(1_000_000_000); + assert_eq!(SubnetTAO::::get(NetUid::ROOT), TaoBalance::ZERO); + SubtensorModule::distribute_emission( + netuid, + pending_alpha.saturating_div(2.into()).into(), + pending_alpha.saturating_div(2.into()).into(), + AlphaBalance::ZERO, + AlphaBalance::ZERO, + ); + let stake_after1 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey1, &coldkey, netuid); + let root_after1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &coldkey, + NetUid::ROOT, + ); + let stake_after2 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey2, &coldkey, netuid); + let root_after2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey2, + &coldkey, + NetUid::ROOT, + ); + let expected_stake = I96F32::from_num(stake_before) + + (I96F32::from_num(pending_alpha) * I96F32::from_num(1.0 / 2.0)); + assert_abs_diff_eq!( + expected_stake.to_num::(), + stake_after1.into(), + epsilon = 10 + ); // Registered gets 50% of alpha emission + let expected_stake2 = I96F32::from_num(stake_before) + + I96F32::from_num(pending_alpha) * I96F32::from_num(1.0 / 2.0); + assert_abs_diff_eq!( + expected_stake2.to_num::(), + stake_after2.into(), + epsilon = 10 + ); // Registered gets 50% emission + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::drain_emission::test_drain_base_with_subnet_with_two_stakers_registered_and_root_different_amounts_half_tao_weight --exact --show-output --nocapture +#[test] +fn test_drain_base_with_subnet_with_two_stakers_registered_and_root_different_amounts_half_tao_weight() + { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + let hotkey1 = U256::from(1); + let hotkey2 = U256::from(2); + let coldkey = U256::from(3); + let stake_before = AlphaBalance::from(1_000_000_000); + Delegates::::insert(hotkey1, PerU16::zero()); + Delegates::::insert(hotkey2, PerU16::zero()); + register_ok_neuron(netuid, hotkey1, coldkey, 0); + register_ok_neuron(netuid, hotkey2, coldkey, 0); + SubtensorModule::set_tao_weight(u64::MAX / 2); // Set TAO weight to 0.5 + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &coldkey, + netuid, + stake_before, + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &coldkey, + NetUid::ROOT, + stake_before * 2.into(), // Hotkey 1 has twice as much root weight. + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey2, + &coldkey, + netuid, + stake_before, + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey2, + &coldkey, + NetUid::ROOT, + stake_before, + ); + let pending_tao = TaoBalance::from(1_000_000_000); + let pending_alpha = AlphaBalance::from(1_000_000_000); + assert_eq!(SubnetTAO::::get(NetUid::ROOT), TaoBalance::ZERO); + SubtensorModule::distribute_emission( + netuid, + pending_alpha.saturating_div(2.into()).into(), + pending_alpha.saturating_div(2.into()).into(), + AlphaBalance::ZERO, + AlphaBalance::ZERO, + ); + let stake_after1 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey1, &coldkey, netuid); + let root_after1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &coldkey, + NetUid::ROOT, + ); + let stake_after2 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey2, &coldkey, netuid); + let root_after2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey2, + &coldkey, + NetUid::ROOT, + ); + let expected_stake = I96F32::from_num(stake_before) + + I96F32::from_num(pending_alpha) * I96F32::from_num(1.0 / 2.0); + assert_abs_diff_eq!( + expected_stake.to_num::(), + u64::from(stake_after1), + epsilon = 10 + ); + let expected_stake2 = I96F32::from_num(stake_before) + + I96F32::from_num(pending_alpha) * I96F32::from_num(1.0 / 2.0); + assert_abs_diff_eq!( + expected_stake2.to_num::(), + u64::from(stake_after2), + epsilon = 10 + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::drain_emission::test_drain_alpha_childkey_parentkey --exact --show-output --nocapture +#[test] +fn test_drain_alpha_childkey_parentkey() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + SubtensorModule::set_ck_burn(0); + let parent = U256::from(1); + let child = U256::from(2); + let coldkey = U256::from(3); + let stake_before = AlphaBalance::from(1_000_000_000); + register_ok_neuron(netuid, child, coldkey, 0); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &parent, + &coldkey, + netuid, + stake_before, + ); + mock_set_children_no_epochs(netuid, &parent, &[(u64::MAX, child)]); + + // Childkey take is 10% + ChildkeyTake::::insert(child, netuid, PerU16::from_parts(u16::MAX / 10)); + + let pending_alpha = AlphaBalance::from(1_000_000_000); + SubtensorModule::distribute_emission( + netuid, + pending_alpha.saturating_div(2.into()).into(), + pending_alpha.saturating_div(2.into()).into(), + AlphaBalance::ZERO, + AlphaBalance::ZERO, + ); + let parent_stake_after = SubtensorModule::get_stake_for_hotkey_on_subnet(&parent, netuid); + let child_stake_after = SubtensorModule::get_stake_for_hotkey_on_subnet(&child, netuid); + + // Child gets 10%, parent gets 90% + let expected = I96F32::from_num(stake_before) + + I96F32::from_num(pending_alpha) * I96F32::from_num(9.0 / 10.0); + log::info!( + "expected: {:?}, parent_stake_after: {:?}", + expected.to_num::(), + parent_stake_after + ); + close(expected.to_num::(), parent_stake_after.into(), 10_000); + let expected = I96F32::from_num(u64::from(pending_alpha)) / I96F32::from_num(10); + close(expected.to_num::(), child_stake_after.into(), 10_000); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::drain_emission::test_drain_alpha_childkey_parentkey_with_burn --exact --show-output --nocapture +#[test] +fn test_drain_alpha_childkey_parentkey_with_burn() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + let parent = U256::from(1); + let child = U256::from(2); + let coldkey = U256::from(3); + let stake_before = AlphaBalance::from(1_000_000_000); + register_ok_neuron(netuid, child, coldkey, 0); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &parent, + &coldkey, + netuid, + stake_before, + ); + mock_set_children_no_epochs(netuid, &parent, &[(u64::MAX, child)]); + + // Childkey take is 10% + ChildkeyTake::::insert(child, netuid, PerU16::from_parts(u16::MAX / 10)); + + let burn_rate = SubtensorModule::get_ck_burn(); + let parent_stake_before = SubtensorModule::get_stake_for_hotkey_on_subnet(&parent, netuid); + let child_stake_before = SubtensorModule::get_stake_for_hotkey_on_subnet(&child, netuid); + + let pending_alpha = AlphaBalance::from(1_000_000_000); + SubtensorModule::distribute_emission( + netuid, + pending_alpha.saturating_div(2.into()).into(), + pending_alpha.saturating_div(2.into()).into(), + AlphaBalance::ZERO, + AlphaBalance::ZERO, + ); + let parent_stake_after = SubtensorModule::get_stake_for_hotkey_on_subnet(&parent, netuid); + let child_stake_after = SubtensorModule::get_stake_for_hotkey_on_subnet(&child, netuid); + + let expected_ck_burn = I96F32::from_num(pending_alpha) + * I96F32::from_num(9.0 / 10.0) + * I96F32::from_num(burn_rate); + + let expected_total = I96F32::from_num(pending_alpha) - expected_ck_burn; + let parent_ratio = (I96F32::from_num(pending_alpha) * I96F32::from_num(9.0 / 10.0) + - expected_ck_burn) + / expected_total; + let child_ratio = (I96F32::from_num(pending_alpha) / I96F32::from_num(10)) / expected_total; + + let expected = + I96F32::from_num(stake_before) + I96F32::from_num(pending_alpha) * parent_ratio; + log::info!( + "expected: {:?}, parent_stake_after: {:?}", + expected.to_num::(), + parent_stake_after + ); + + close( + expected.to_num::(), + parent_stake_after.into(), + 3_000_000, + ); + let expected = I96F32::from_num(u64::from(pending_alpha)) * child_ratio; + close( + expected.to_num::(), + child_stake_after.into(), + 3_000_000, + ); + }); +} diff --git a/pallets/subtensor/src/tests/coinbase/drain_pending_epoch.rs b/pallets/subtensor/src/tests/coinbase/drain_pending_epoch.rs new file mode 100644 index 0000000000..bb9da6d252 --- /dev/null +++ b/pallets/subtensor/src/tests/coinbase/drain_pending_epoch.rs @@ -0,0 +1,164 @@ +#![allow( + unused, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used +)] +//! Drain-pending BlocksSinceLastStep and epoch deferral interaction. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_coinbase_drain_pending_increments_blockssincelaststep() { + new_test_ext(1).execute_with(|| { + let zero = U96F32::saturating_from_num(0); + let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); + + let blocks_since_last_step_before = BlocksSinceLastStep::::get(netuid0); + + // Check that blockssincelaststep is incremented + SubtensorModule::drain_pending_subnet_emissions(&[netuid0], 1); + + let blocks_since_last_step_after = BlocksSinceLastStep::::get(netuid0); + assert!(blocks_since_last_step_after > blocks_since_last_step_before); + assert_eq!( + blocks_since_last_step_after, + blocks_since_last_step_before + 1 + ); + }); +} + +#[test] +fn test_coinbase_drain_pending_caps_blockssincelaststep_when_epoch_is_deferred() { + new_test_ext(1).execute_with(|| { + let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); + let tempo = 1; + Tempo::::insert(netuid, tempo); + PendingEpochAt::::insert(netuid, 1); + SubtensorModule::set_max_epochs_per_block(0); + + for block in 1..=10 { + SubtensorModule::drain_pending_subnet_emissions(&[netuid], block); + } + + assert_eq!( + BlocksSinceLastStep::::get(netuid), + u64::from(tempo) + 1 + ); + assert!(SubtensorModule::should_run_epoch(netuid, 11)); + }); +} + +#[test] +fn test_coinbase_drain_pending_caps_blockssincelaststep_for_inconsistent_epoch() { + new_test_ext(1).execute_with(|| { + let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); + let tempo = 1; + Tempo::::insert(netuid, tempo); + PendingEpochAt::::insert(netuid, 1); + + let duplicate_hotkey = U256::from(99); + Keys::::insert(netuid, 0, duplicate_hotkey); + Keys::::insert(netuid, 1, duplicate_hotkey); + assert!(!SubtensorModule::epoch_keys_have_unique_hotkeys(netuid)); + + for block in 1..=10 { + SubtensorModule::drain_pending_subnet_emissions(&[netuid], block); + } + + assert_eq!( + BlocksSinceLastStep::::get(netuid), + u64::from(tempo) + 1 + ); + assert!(SubtensorModule::should_run_epoch(netuid, 11)); + }); +} + +#[test] +fn test_should_run_epoch_uses_subnet_tempo_for_step_age_safety_net() { + new_test_ext(1).execute_with(|| { + let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); + let tempo = 1; + Tempo::::insert(netuid, tempo); + LastEpochBlock::::insert(netuid, 100); + PendingEpochAt::::insert(netuid, 0); + BlocksSinceLastStep::::insert(netuid, u64::from(tempo) + 1); + + assert!(SubtensorModule::should_run_epoch(netuid, 2)); + }); +} + +#[test] +fn test_coinbase_drain_pending_resets_blockssincelaststep() { + new_test_ext(1).execute_with(|| { + let zero = U96F32::saturating_from_num(0); + let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); + Tempo::::insert(netuid0, 100); + LastEpochBlock::::insert(netuid0, 0); + let block_number = 102; + assert!(SubtensorModule::should_run_epoch(netuid0, block_number)); + + let blocks_since_last_step_before = 12345678; + BlocksSinceLastStep::::insert(netuid0, blocks_since_last_step_before); + LastMechansimStepBlock::::insert(netuid0, 12345); // garbage value + + // Check that blockssincelaststep is reset to 0 on tempo + SubtensorModule::drain_pending_subnet_emissions(&[netuid0], block_number); + + let blocks_since_last_step_after = BlocksSinceLastStep::::get(netuid0); + assert_eq!(blocks_since_last_step_after, 0); + assert_eq!(LastMechansimStepBlock::::get(netuid0), 12345); + }); +} + +#[test] +fn test_coinbase_drain_pending_gets_counters_and_resets_them() { + new_test_ext(1).execute_with(|| { + let zero = U96F32::saturating_from_num(0); + let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); + Tempo::::insert(netuid0, 100); + LastEpochBlock::::insert(netuid0, 0); + let block_number = 102; + assert!(SubtensorModule::should_run_epoch(netuid0, block_number)); + + let pending_server_em = AlphaBalance::from(123434534); + let pending_validator_em = AlphaBalance::from(111111); + let pending_root = AlphaBalance::from(12222222); + let pending_owner_cut = AlphaBalance::from(12345678); + + PendingServerEmission::::insert(netuid0, pending_server_em); + PendingValidatorEmission::::insert(netuid0, pending_validator_em); + PendingRootAlphaDivs::::insert(netuid0, pending_root); + PendingOwnerCut::::insert(netuid0, pending_owner_cut); + + let emissions_to_distribute = SubtensorModule::drain_pending_subnet_emissions(&[netuid0], block_number); + assert_eq!(emissions_to_distribute.len(), 1); + assert_eq!( + emissions_to_distribute[&netuid0], + ( + pending_server_em, + pending_validator_em, + pending_root, + pending_owner_cut + ) + ); + + // Check that the pending emissions are reset + assert_eq!( + PendingServerEmission::::get(netuid0), + AlphaBalance::ZERO + ); + assert_eq!( + PendingValidatorEmission::::get(netuid0), + AlphaBalance::ZERO + ); + assert_eq!( + PendingRootAlphaDivs::::get(netuid0), + AlphaBalance::ZERO + ); + assert_eq!(PendingOwnerCut::::get(netuid0), AlphaBalance::ZERO); + }); +} diff --git a/pallets/subtensor/src/tests/coinbase/emit_to_subnets.rs b/pallets/subtensor/src/tests/coinbase/emit_to_subnets.rs new file mode 100644 index 0000000000..29a0c1a07b --- /dev/null +++ b/pallets/subtensor/src/tests/coinbase/emit_to_subnets.rs @@ -0,0 +1,197 @@ +#![allow( + unused, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used +)] +//! emit_to_subnets with/without root sell. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_coinbase_emit_to_subnets_with_no_root_sell() { + new_test_ext(1).execute_with(|| { + let zero = U96F32::saturating_from_num(0); + let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); + // Set owner cut to ~10% + SubnetOwnerCut::::set(u16::MAX / 10); + mock::setup_reserves( + netuid0, + TaoBalance::from(1_000_000_000_000_000_u64), + AlphaBalance::from(1_000_000_000_000_000_u64), + ); + // Initialize swap + Swap::maybe_initialize_palswap(netuid0, None); + + let tao_emission = U96F32::saturating_from_num(12345678); + let subnet_emissions = BTreeMap::from([(netuid0, tao_emission)]); + + // NO root sell + let root_sell_flag = false; + + let alpha_emission = U96F32::saturating_from_num( + SubtensorModule::get_block_emission_for_issuance( + SubtensorModule::get_alpha_issuance(netuid0).into(), + ) + .unwrap_or(0), + ); + let price: U96F32 = U96F32::saturating_from_num(Swap::current_alpha_price(netuid0)); + let (tao_in, alpha_in, alpha_out, excess_tao) = + SubtensorModule::compute_subnet_emission_terms(&subnet_emissions); + // Based on the price, we should have NO excess TAO + assert!(tao_emission / price <= alpha_emission); + + // ==== Run the emit to subnets ===== + let credit = SubtensorModule::mint_tao(12345678.into()); + SubtensorModule::emit_to_subnets(&[netuid0], &subnet_emissions, credit, root_sell_flag); + + // Find the owner cut expected + let owner_cut: U96F32 = SubtensorModule::get_float_subnet_owner_cut(); + let owner_cut_expected: U96F32 = owner_cut.saturating_mul(alpha_emission); + log::info!("owner_cut_expected: {owner_cut_expected:?}"); + log::info!("alpha_emission: {alpha_emission:?}"); + log::info!("owner_cut: {owner_cut:?}"); + + let alpha_issuance: U96F32 = + U96F32::saturating_from_num(SubtensorModule::get_alpha_issuance(netuid0)); + let root_tao: U96F32 = U96F32::saturating_from_num(SubnetTAO::::get(NetUid::ROOT)); + let tao_weight: U96F32 = root_tao.saturating_mul(SubtensorModule::get_tao_weight()); + let root_prop: U96F32 = tao_weight + .checked_div(tao_weight.saturating_add(alpha_issuance)) + .unwrap_or(U96F32::min_value()); + // Expect root alpha divs to be root prop * alpha emission + let expected_root_alpha_divs: AlphaBalance = AlphaBalance::from( + root_prop + .saturating_mul(alpha_emission) + .saturating_to_num::(), + ); + + // ===== Check that the pending emissions are set correctly ===== + // Owner cut is as expected + assert_abs_diff_eq!( + PendingOwnerCut::::get(netuid0).to_u64(), + owner_cut_expected.saturating_to_num::(), + epsilon = 200_u64 + ); + // NO root sell, so no root alpha divs + assert_eq!( + PendingRootAlphaDivs::::get(netuid0), + AlphaBalance::ZERO + ); + // Should be alpha_emission minus the owner cut, + assert_abs_diff_eq!( + PendingServerEmission::::get(netuid0).to_u64(), + alpha_emission + .saturating_sub(owner_cut_expected) + .saturating_div(U96F32::saturating_from_num(2)) + .saturating_to_num::(), + epsilon = 200_u64 + ); + // We ALWAYS deduct the root alpha divs + assert_abs_diff_eq!( + PendingValidatorEmission::::get(netuid0).to_u64(), + alpha_emission + .saturating_sub(owner_cut_expected) + .saturating_div(U96F32::saturating_from_num(2)) + .saturating_sub(expected_root_alpha_divs.to_u64().into()) + .saturating_to_num::(), + epsilon = 200_u64 + ); + }); +} + +#[test] +fn test_coinbase_emit_to_subnets_with_root_sell() { + new_test_ext(1).execute_with(|| { + let zero = U96F32::saturating_from_num(0); + let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); + // Set owner cut to ~10% + SubnetOwnerCut::::set(u16::MAX / 10); + mock::setup_reserves( + netuid0, + TaoBalance::from(1_000_000_000_000_000_u64), + AlphaBalance::from(1_000_000_000_000_000_u64), + ); + // Initialize swap + Swap::maybe_initialize_palswap(netuid0, None); + + let tao_emission = U96F32::saturating_from_num(12345678); + let subnet_emissions = BTreeMap::from([(netuid0, tao_emission)]); + + // NO root sell + let root_sell_flag = true; + + let alpha_emission: U96F32 = U96F32::saturating_from_num( + SubtensorModule::get_block_emission_for_issuance( + SubtensorModule::get_alpha_issuance(netuid0).into(), + ) + .unwrap_or(0), + ); + let price: U96F32 = U96F32::saturating_from_num(Swap::current_alpha_price(netuid0)); + let (tao_in, alpha_in, alpha_out, excess_tao) = + SubtensorModule::compute_subnet_emission_terms(&subnet_emissions); + // Based on the price, we should have NO excess TAO + assert!(tao_emission / price <= alpha_emission); + + // ==== Run the emit to subnets ===== + let credit = SubtensorModule::mint_tao(12345678.into()); + SubtensorModule::emit_to_subnets(&[netuid0], &subnet_emissions, credit, root_sell_flag); + + // Find the owner cut expected + let owner_cut: U96F32 = SubtensorModule::get_float_subnet_owner_cut(); + let owner_cut_expected: U96F32 = owner_cut.saturating_mul(alpha_emission); + log::info!("owner_cut_expected: {owner_cut_expected:?}"); + log::info!("alpha_emission: {alpha_emission:?}"); + log::info!("owner_cut: {owner_cut:?}"); + + let alpha_issuance: U96F32 = + U96F32::saturating_from_num(SubtensorModule::get_alpha_issuance(netuid0)); + let root_tao: U96F32 = U96F32::saturating_from_num(SubnetTAO::::get(NetUid::ROOT)); + let tao_weight: U96F32 = root_tao.saturating_mul(SubtensorModule::get_tao_weight()); + let root_prop: U96F32 = tao_weight + .checked_div(tao_weight.saturating_add(alpha_issuance)) + .unwrap_or(U96F32::min_value()); + // Expect root alpha divs to be root prop * alpha emission + let expected_root_alpha_divs: AlphaBalance = AlphaBalance::from( + root_prop + .saturating_mul(alpha_emission) + .saturating_to_num::(), + ); + + // ===== Check that the pending emissions are set correctly ===== + // Owner cut is as expected + assert_abs_diff_eq!( + PendingOwnerCut::::get(netuid0).to_u64(), + owner_cut_expected.saturating_to_num::(), + epsilon = 200_u64 + ); + // YES root sell, so we have root alpha divs + assert_abs_diff_eq!( + PendingRootAlphaDivs::::get(netuid0).to_u64(), + expected_root_alpha_divs.to_u64(), + epsilon = 200_u64 + ); + // Should be alpha_emission minus the owner cut + assert_abs_diff_eq!( + PendingServerEmission::::get(netuid0).to_u64(), + alpha_emission + .saturating_sub(owner_cut_expected) + .saturating_div(U96F32::saturating_from_num(2)) + .saturating_to_num::(), + epsilon = 200_u64 + ); + // Validator emission is also minus root alpha divs + assert_abs_diff_eq!( + PendingValidatorEmission::::get(netuid0).to_u64(), + alpha_emission + .saturating_sub(owner_cut_expected) + .saturating_div(U96F32::saturating_from_num(2)) + .saturating_sub(expected_root_alpha_divs.to_u64().into()) + .saturating_to_num::(), + epsilon = 200_u64 + ); + }); +} diff --git a/pallets/subtensor/src/tests/coinbase/epoch_cap_deferral.rs b/pallets/subtensor/src/tests/coinbase/epoch_cap_deferral.rs new file mode 100644 index 0000000000..252073071d --- /dev/null +++ b/pallets/subtensor/src/tests/coinbase/epoch_cap_deferral.rs @@ -0,0 +1,112 @@ +#![allow( + unused, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used +)] +//! Epoch cap deferral and CRV3 reveal interaction. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_epochs_deferred_this_block_respects_cap() { + new_test_ext(1).execute_with(|| { + let cap = SubtensorModule::get_max_epochs_per_block() as usize; + let n = cap + 2; + + for i in 0..n { + let netuid = NetUid::from((i + 1) as u16); + add_network(netuid, 100, 0); + // Force "due this block". + PendingEpochAt::::insert(netuid, 1); + } + + let block = SubtensorModule::get_current_block_as_u64(); + let subnets: Vec = SubtensorModule::get_all_subnet_netuids() + .into_iter() + .filter(|x| *x != NetUid::ROOT) + .collect(); + + // All `n` subnets are due, but only `cap` may fire — the rest are deferred. + let deferred = SubtensorModule::epochs_deferred_this_block(&subnets, block); + assert_eq!( + deferred.len(), + n - cap, + "exactly the due subnets beyond MaxEpochsPerBlock are deferred" + ); + for netuid in &deferred { + assert!(SubtensorModule::should_run_epoch(*netuid, block)); + } + }); +} + +// Regression test for the dynamic-tempo / CR-v3 interaction: when a subnet's epoch +// is deferred by the per-block cap, its timelock reveal must be held back to the +// deferred fire-block (not run on the originally-scheduled block, which would +// surface weights before the epoch consumes them). +// +// Crypto-free probe: the reveal path removes *expired* commits only when it runs +// for a subnet, so a retained expired (epoch-0) commit means the reveal was skipped. +#[test] +fn test_reveal_crv3_defers_with_capped_epoch() { + new_test_ext(1).execute_with(|| { + let cap = SubtensorModule::get_max_epochs_per_block() as usize; + let n = cap + 2; + let mec0 = subtensor_runtime_common::MechId::from(0); + + for i in 0..n { + let netuid = NetUid::from((i + 1) as u16); + add_network(netuid, 100, 0); + PendingEpochAt::::insert(netuid, 1); // due this block + SubnetEpochIndex::::insert(netuid, 10); // cur_epoch >> reveal_period + // Plant an expired commit at epoch 0 (field types inferred from the queue). + let idx = SubtensorModule::get_mechanism_storage_index(netuid, mec0); + TimelockedWeightCommits::::mutate(idx, 0u64, |q| { + q.push_back((U256::from(1u64), 0u64, Default::default(), 0u64)); + }); + } + + let subnets: Vec = SubtensorModule::get_all_subnet_netuids() + .into_iter() + .filter(|x| *x != NetUid::ROOT) + .collect(); + + let still_holds = |netuid: NetUid| -> bool { + let idx = SubtensorModule::get_mechanism_storage_index(netuid, mec0); + TimelockedWeightCommits::::contains_key(idx, 0u64) + }; + let retained = |subnets: &[NetUid]| subnets.iter().filter(|n| still_holds(**n)).count(); + + // --- Phase 1: cap-deferred subnets must NOT reveal this block. + SubtensorModule::reveal_crv3_commits(); + assert_eq!( + retained(&subnets), + n - cap, + "only cap-deferred subnets keep their commit (their reveal was skipped)" + ); + + let deferred: Vec = subnets + .iter() + .copied() + .filter(|n| still_holds(*n)) + .collect(); + + // --- Phase 2: drop the cap pressure so only the deferred subnets are due; + // they should now reveal (and clean their expired commit). + for netuid in &subnets { + if !deferred.contains(netuid) { + PendingEpochAt::::insert(*netuid, 0); + LastEpochBlock::::insert(*netuid, 1); // blocks_since < tempo => not due + } + } + SubtensorModule::reveal_crv3_commits(); + assert_eq!( + retained(&subnets), + 0, + "deferred subnets reveal once they actually fire" + ); + }); +} diff --git a/pallets/subtensor/src/tests/coinbase/helpers.rs b/pallets/subtensor/src/tests/coinbase/helpers.rs new file mode 100644 index 0000000000..5bff147a26 --- /dev/null +++ b/pallets/subtensor/src/tests/coinbase/helpers.rs @@ -0,0 +1,26 @@ +#![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] +//! Shared fixtures for coinbase emission tests. + +use super::super::mock::*; +use crate::*; +use subtensor_runtime_common::TaoBalance; + +pub(super) fn close(value: u64, target: u64, eps: u64) { + assert!( + (value as i64 - target as i64).abs() < eps as i64, + "Assertion failed: value = {value}, target = {target}, eps = {eps}" + ) +} + +/// Seed a large root stake with full TAO weight so that +/// `root_proportion = tao_weight / (tao_weight + alpha_issuance)` is ~1. +/// This keeps the alpha-injection cap (`root_proportion * alpha_emission`) from +/// spuriously binding for small per-subnet emissions, preserving the liquidity +/// injection behavior these tests were written for. +pub(super) fn set_full_injection_root_stake() { + SubnetTAO::::insert( + NetUid::ROOT, + TaoBalance::from(1_000_000_000_000_000_000_u64), + ); + SubtensorModule::set_tao_weight(u64::MAX); +} diff --git a/pallets/subtensor/src/tests/coinbase/incentive_autostake.rs b/pallets/subtensor/src/tests/coinbase/incentive_autostake.rs new file mode 100644 index 0000000000..80151ba20d --- /dev/null +++ b/pallets/subtensor/src/tests/coinbase/incentive_autostake.rs @@ -0,0 +1,114 @@ +#![allow( + unused, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used +)] +//! Incentive autostake destination vs hotkey fallback. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_incentive_is_autostaked_to_owner_destination() { + new_test_ext(1).execute_with(|| { + let subnet_owner_ck = U256::from(0); + let subnet_owner_hk = U256::from(1); + + let miner_ck = U256::from(10); + let miner_hk = U256::from(11); + let dest_hk = U256::from(12); + + Owner::::insert(miner_hk, miner_ck); + Owner::::insert(dest_hk, miner_ck); + OwnedHotkeys::::insert(miner_ck, vec![miner_hk, dest_hk]); + + let netuid = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); + + Uids::::insert(netuid, miner_hk, 1); + Uids::::insert(netuid, dest_hk, 2); + + // Set autostake destination for the miner's coldkey + assert_ok!(SubtensorModule::set_coldkey_auto_stake_hotkey( + RuntimeOrigin::signed(miner_ck), + netuid, + dest_hk, + )); + + assert_eq!( + SubtensorModule::get_stake_for_hotkey_on_subnet(&miner_hk, netuid), + 0.into() + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_on_subnet(&dest_hk, netuid), + 0.into() + ); + + // Distribute an incentive to the miner hotkey + let mut incentives: BTreeMap = BTreeMap::new(); + let incentive: AlphaBalance = 10_000_000u64.into(); + incentives.insert(miner_hk, incentive); + + SubtensorModule::distribute_dividends_and_incentives( + netuid, + AlphaBalance::ZERO, // owner_cut + incentives, + BTreeMap::new(), // alpha_dividends + BTreeMap::new(), // tao_dividends + ); + + // Expect the stake to land on the destination hotkey (not the original miner hotkey) + assert_eq!( + SubtensorModule::get_stake_for_hotkey_on_subnet(&miner_hk, netuid), + 0.into() + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_on_subnet(&dest_hk, netuid), + incentive + ); + }); +} + +#[test] +fn test_incentive_goes_to_hotkey_when_no_autostake_destination() { + new_test_ext(1).execute_with(|| { + let subnet_owner_ck = U256::from(0); + let subnet_owner_hk = U256::from(1); + + let miner_ck = U256::from(20); + let miner_hk = U256::from(21); + + Owner::::insert(miner_hk, miner_ck); + OwnedHotkeys::::insert(miner_ck, vec![miner_hk]); + + let netuid = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); + + Uids::::insert(netuid, miner_hk, 1); + + assert_eq!( + SubtensorModule::get_stake_for_hotkey_on_subnet(&miner_hk, netuid), + 0.into() + ); + + // Distribute an incentive to the miner hotkey + let mut incentives: BTreeMap = BTreeMap::new(); + let incentive: AlphaBalance = 5_000_000u64.into(); + incentives.insert(miner_hk, incentive); + + SubtensorModule::distribute_dividends_and_incentives( + netuid, + AlphaBalance::ZERO, // owner_cut + incentives, + BTreeMap::new(), // alpha_dividends + BTreeMap::new(), // tao_dividends + ); + + // With no autostake destination, the incentive should be staked to the original hotkey + assert_eq!( + SubtensorModule::get_stake_for_hotkey_on_subnet(&miner_hk, netuid), + incentive + ); + }); +} diff --git a/pallets/subtensor/src/tests/coinbase/incentive_burn.rs b/pallets/subtensor/src/tests/coinbase/incentive_burn.rs new file mode 100644 index 0000000000..a9de3799c4 --- /dev/null +++ b/pallets/subtensor/src/tests/coinbase/incentive_burn.rs @@ -0,0 +1,291 @@ +#![allow( + unused, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used +)] +//! Incentive burn to subnet owner / burn-key sorting. + +use super::helpers::*; +use super::prelude::*; + +// // SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_get_root_children_with_weights --exact --show-output --nocapture +// #[test] +// fn test_get_root_children_with_weights() { +// new_test_ext(1).execute_with(|| { +// // Init netuid 1 +// let alpha = NetUid::from(1); +// add_network(NetUid::ROOT, 1, 0); +// add_network(alpha, 1, 0); +// // Set TAO weight to 1. +// SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1. +// // Create keys. +// let cold = U256::from(0); +// let alice = U256::from(1); +// let bob = U256::from(2); +// // Register Alice and Bob to the root network and alpha subnet. +// register_ok_neuron(alpha, alice, cold, 0); +// register_ok_neuron(alpha, bob, cold, 0); +// assert_ok!(SubtensorModule::root_register( +// RuntimeOrigin::signed(cold).clone(), +// alice, +// )); +// assert_ok!(SubtensorModule::root_register( +// RuntimeOrigin::signed(cold).clone(), +// bob, +// )); +// // Add stake for Alice and Bob on root. +// let alice_root_stake = AlphaBalance::from(1_000_000_000); +// SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( +// &alice, +// &cold, +// NetUid::ROOT, +// alice_root_stake, +// ); +// let bob_root_stake = AlphaBalance::from(1_000_000_000); +// SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( +// &bob, +// &cold, +// NetUid::ROOT, +// alice_root_stake, +// ); +// // Add stake for Alice and Bob on netuid. +// let alice_alpha_stake = AlphaBalance::from(1_000_000_000); +// SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( +// &alice, +// &cold, +// alpha, +// alice_alpha_stake, +// ); +// let bob_alpha_stake = AlphaBalance::from(1_000_000_000); +// SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( +// &bob, +// &cold, +// alpha, +// bob_alpha_stake, +// ); +// // Set Bob as 100% child of Alice on root. +// mock_set_children_no_epochs(alpha, &alice, &[(u64::MAX, bob)]); + +// // Set Bob childkey take to zero. +// ChildkeyTake::::insert(bob, alpha, 0); +// Delegates::::insert(alice, 0); +// Delegates::::insert(bob, 0); + +// // Set weights on the subnet. +// assert_ok!(SubtensorModule::set_weights( +// RuntimeOrigin::signed(alice), +// alpha, +// vec![0, 1], +// vec![1, 1], +// 0, +// )); +// assert_ok!(SubtensorModule::set_weights( +// RuntimeOrigin::signed(bob), +// alpha, +// vec![0, 1], +// vec![1, 1], +// 0, +// )); + +// // Lets drain! +// let pending_alpha = AlphaBalance::from(1_000_000_000); +// SubtensorModule::distribute_emission(alpha, pending_alpha, 0, 0.into(), 0.into()); + +// // Alice and Bob make the same amount. +// close( +// AlphaDividendsPerSubnet::::get(alpha, alice), +// pending_alpha / 2, +// 10, +// ); +// close( +// AlphaDividendsPerSubnet::::get(alpha, bob), +// pending_alpha / 2, +// 10, +// ); +// }); +// } + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::incentive_burn::test_incentive_to_subnet_owner_is_burned --exact --show-output --nocapture +#[test] +fn test_incentive_to_subnet_owner_is_burned() { + new_test_ext(1).execute_with(|| { + let subnet_owner_ck = U256::from(0); + let subnet_owner_hk = U256::from(1); + + let other_ck = U256::from(2); + let other_hk = U256::from(3); + Owner::::insert(other_hk, other_ck); + + let netuid = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); + remove_owner_registration_stake(netuid); + + let pending_tao: u64 = 1_000_000_000; + let pending_alpha = AlphaBalance::ZERO; // None to valis + let owner_cut = AlphaBalance::ZERO; + let mut incentives: BTreeMap = BTreeMap::new(); + + // Give incentive to other_hk + incentives.insert(other_hk, 10_000_000.into()); + + // Give incentives to subnet_owner_hk + incentives.insert(subnet_owner_hk, 10_000_000.into()); + + // Verify stake before + let subnet_owner_stake_before = + SubtensorModule::get_stake_for_hotkey_on_subnet(&subnet_owner_hk, netuid); + assert_eq!(subnet_owner_stake_before, 0.into()); + let other_stake_before = SubtensorModule::get_stake_for_hotkey_on_subnet(&other_hk, netuid); + assert_eq!(other_stake_before, 0.into()); + + // Distribute dividends and incentives + SubtensorModule::distribute_dividends_and_incentives( + netuid, + owner_cut, + incentives, + BTreeMap::new(), + BTreeMap::new(), + ); + + // Verify stake after + let subnet_owner_stake_after = + SubtensorModule::get_stake_for_hotkey_on_subnet(&subnet_owner_hk, netuid); + assert_eq!(subnet_owner_stake_after, 0.into()); + let other_stake_after = SubtensorModule::get_stake_for_hotkey_on_subnet(&other_hk, netuid); + assert!(other_stake_after > 0.into()); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::incentive_burn::test_incentive_to_subnet_owners_hotkey_is_burned --exact --show-output --nocapture +#[test] +fn test_incentive_to_subnet_owners_hotkey_is_burned() { + new_test_ext(1).execute_with(|| { + let subnet_owner_ck = U256::from(0); + let subnet_owner_hk = U256::from(1); + + // Other hk owned by owner + let other_hk = U256::from(3); + Owner::::insert(other_hk, subnet_owner_ck); + OwnedHotkeys::::insert(subnet_owner_ck, vec![subnet_owner_hk, other_hk]); + + let netuid = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); + remove_owner_registration_stake(netuid); + Uids::::insert(netuid, other_hk, 1); + + // Set the burn key limit to 2 + ImmuneOwnerUidsLimit::::insert(netuid, 2); + + let pending_tao: u64 = 1_000_000_000; + let pending_alpha = AlphaBalance::ZERO; // None to valis + let owner_cut = AlphaBalance::ZERO; + let mut incentives: BTreeMap = BTreeMap::new(); + + // Give incentive to other_hk + incentives.insert(other_hk, 10_000_000.into()); + + // Give incentives to subnet_owner_hk + incentives.insert(subnet_owner_hk, 10_000_000.into()); + + // Verify stake before + let subnet_owner_stake_before = + SubtensorModule::get_stake_for_hotkey_on_subnet(&subnet_owner_hk, netuid); + assert_eq!(subnet_owner_stake_before, 0.into()); + let other_stake_before = SubtensorModule::get_stake_for_hotkey_on_subnet(&other_hk, netuid); + assert_eq!(other_stake_before, 0.into()); + + // Distribute dividends and incentives + SubtensorModule::distribute_dividends_and_incentives( + netuid, + owner_cut, + incentives, + BTreeMap::new(), + BTreeMap::new(), + ); + + // Verify stake after + let subnet_owner_stake_after = + SubtensorModule::get_stake_for_hotkey_on_subnet(&subnet_owner_hk, netuid); + assert_eq!(subnet_owner_stake_after, 0.into()); + let other_stake_after = SubtensorModule::get_stake_for_hotkey_on_subnet(&other_hk, netuid); + assert_eq!(other_stake_after, 0.into()); + }); +} + +// Test that if number of sn owner hotkeys is greater than ImmuneOwnerUidsLimit, then the ones with +// higher BlockAtRegistration are used to burn +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::incentive_burn::test_burn_key_sorting --exact --show-output --nocapture +#[test] +fn test_burn_key_sorting() { + new_test_ext(1).execute_with(|| { + let subnet_owner_ck = U256::from(0); + let subnet_owner_hk = U256::from(1); + + // Other hk owned by owner + let other_hk_1 = U256::from(3); + let other_hk_2 = U256::from(4); + let other_hk_3 = U256::from(5); + Owner::::insert(other_hk_1, subnet_owner_ck); + Owner::::insert(other_hk_2, subnet_owner_ck); + Owner::::insert(other_hk_3, subnet_owner_ck); + OwnedHotkeys::::insert( + subnet_owner_ck, + vec![subnet_owner_hk, other_hk_1, other_hk_2, other_hk_3], + ); + + let netuid = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); + remove_owner_registration_stake(netuid); + + // Set block of registration and UIDs for other hotkeys + // HK1 has block of registration 2 + // HK2 and HK3 have the same block of registration 1, so they are sorted by UID + // Set HK2 UID = 3 and HK3 UID = 2 so that HK3 is burned and HK2 is not + // Summary: HK1 and HK3 should be burned, HK2 should be not. + // Let's test it now. + BlockAtRegistration::::insert(netuid, 1, 2); + BlockAtRegistration::::insert(netuid, 3, 1); + BlockAtRegistration::::insert(netuid, 2, 1); + Uids::::insert(netuid, other_hk_1, 1); + Uids::::insert(netuid, other_hk_2, 3); + Uids::::insert(netuid, other_hk_3, 2); + + let pending_tao: u64 = 1_000_000_000; + let pending_alpha = AlphaBalance::ZERO; // None to valis + let owner_cut = AlphaBalance::ZERO; + let mut incentives: BTreeMap = BTreeMap::new(); + + // Give incentive to hotkeys + incentives.insert(other_hk_1, 10_000_000.into()); + incentives.insert(other_hk_2, 10_000_000.into()); + incentives.insert(other_hk_3, 10_000_000.into()); + + // Give incentives to subnet_owner_hk + incentives.insert(subnet_owner_hk, 10_000_000.into()); + + // Distribute dividends and incentives + SubtensorModule::distribute_dividends_and_incentives( + netuid, + owner_cut, + incentives, + BTreeMap::new(), + BTreeMap::new(), + ); + + // SN owner is burned + let subnet_owner_stake_after = + SubtensorModule::get_stake_for_hotkey_on_subnet(&subnet_owner_hk, netuid); + assert_eq!(subnet_owner_stake_after, 0.into()); + + // No burn limits, all HKs should be burned + let other_stake_after_1 = + SubtensorModule::get_stake_for_hotkey_on_subnet(&other_hk_1, netuid); + let other_stake_after_2 = + SubtensorModule::get_stake_for_hotkey_on_subnet(&other_hk_2, netuid); + let other_stake_after_3 = + SubtensorModule::get_stake_for_hotkey_on_subnet(&other_hk_3, netuid); + assert_eq!(other_stake_after_1, 0.into()); + assert_eq!(other_stake_after_2, 0.into()); + assert_eq!(other_stake_after_3, 0.into()); + }); +} diff --git a/pallets/subtensor/src/tests/coinbase/inject_and_swap.rs b/pallets/subtensor/src/tests/coinbase/inject_and_swap.rs new file mode 100644 index 0000000000..916b87abb8 --- /dev/null +++ b/pallets/subtensor/src/tests/coinbase/inject_and_swap.rs @@ -0,0 +1,167 @@ +#![allow( + unused, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used +)] +//! Liquidity inject-and-maybe-swap and TAO materialization. + +use super::helpers::*; +use super::prelude::*; + +// Tests for the inject and swap are in the right order. +#[test] +fn test_coinbase_inject_and_maybe_swap_does_not_skew_reserves() { + new_test_ext(1).execute_with(|| { + let zero = U96F32::saturating_from_num(0); + let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); + mock::setup_reserves( + netuid0, + TaoBalance::from(1_000_000_000_000_000_u64), + AlphaBalance::from(1_000_000_000_000_000_u64), + ); + // Initialize swap + Swap::maybe_initialize_palswap(netuid0, None); + + let tao_in = BTreeMap::from([(netuid0, U96F32::saturating_from_num(123))]); + let alpha_in = BTreeMap::from([(netuid0, U96F32::saturating_from_num(456))]); + // We have excess TAO, so we will be swapping with it. + let excess_tao = BTreeMap::from([(netuid0, U96F32::saturating_from_num(789100))]); + + // Run the inject and maybe swap + let credit = SubtensorModule::mint_tao((123 + 789100).into()); + SubtensorModule::inject_pool_liquidity_and_swap_excess(&[netuid0], &tao_in, &alpha_in, &excess_tao, credit); + + let tao_in_after = SubnetTAO::::get(netuid0); + let alpha_in_after = SubnetAlphaIn::::get(netuid0); + + // Make sure that when we inject and swap, we do it in the right order. + // Thereby not skewing the ratio away from the price. + let ratio_after: U96F32 = U96F32::saturating_from_num(alpha_in_after.to_u64()) + .saturating_div(U96F32::saturating_from_num(tao_in_after.to_u64())); + let price_after: U96F32 = U96F32::saturating_from_num( + pallet_subtensor_swap::Pallet::::current_alpha_price(netuid0).to_num::(), + ); + assert_abs_diff_eq!( + ratio_after.to_num::(), + price_after.to_num::(), + epsilon = 1.0 + ); + }); +} + +#[test] +fn test_coinbase_failed_tao_materialization_does_not_activate_current_tao() { + new_test_ext(1).execute_with(|| { + let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); + let initial_reserve = TaoBalance::from(1_000_000_u64); + let reservoir_tao = TaoBalance::from(100_u64); + let current_tao = TaoBalance::from(200_u64); + let current_alpha = AlphaBalance::from(100_u64); + + mock::setup_reserves(netuid, initial_reserve, AlphaBalance::from(1_000_000_u64)); + Swap::maybe_initialize_palswap(netuid, None); + pallet_subtensor_swap::BalancerTaoReservoir::::insert(netuid, reservoir_tao); + + let tao_in = BTreeMap::from([(netuid, U96F32::saturating_from_num(current_tao))]); + let alpha_in = BTreeMap::from([(netuid, U96F32::saturating_from_num(current_alpha))]); + let excess_tao = BTreeMap::new(); + let credit = SubtensorModule::mint_tao(TaoBalance::ZERO); + + SubtensorModule::inject_pool_liquidity_and_swap_excess(&[netuid], &tao_in, &alpha_in, &excess_tao, credit); + + assert_eq!( + SubnetTAO::::get(netuid), + initial_reserve.saturating_add(reservoir_tao) + ); + assert_eq!(SubnetTaoInEmission::::get(netuid), reservoir_tao); + assert_eq!( + SubnetProtocolFlow::::get(netuid), + reservoir_tao.to_u64() as i64 + ); + assert_eq!( + pallet_subtensor_swap::BalancerTaoReservoir::::get(netuid), + TaoBalance::ZERO + ); + }); +} + +#[test] +fn test_alpha_reservoir_counts_toward_subnet_issuance_across_blocks() { + new_test_ext(1).execute_with(|| { + let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); + let alpha_in = AlphaBalance::from(10_000_u64); + let alpha_out = AlphaBalance::from(20_000_u64); + let reservoir_alpha = AlphaBalance::from(30_000_u64); + + SubnetAlphaIn::::insert(netuid, alpha_in); + SubnetAlphaOut::::insert(netuid, alpha_out); + pallet_subtensor_swap::BalancerAlphaReservoir::::insert(netuid, reservoir_alpha); + + let expected = alpha_in + .saturating_add(alpha_out) + .saturating_add(reservoir_alpha); + assert_eq!(SubtensorModule::get_alpha_issuance(netuid), expected); + + System::set_block_number(System::block_number().saturating_add(1)); + + assert_eq!(SubnetAlphaIn::::get(netuid), alpha_in); + assert_eq!( + pallet_subtensor_swap::BalancerAlphaReservoir::::get(netuid), + reservoir_alpha + ); + assert_eq!(SubtensorModule::get_alpha_issuance(netuid), expected); + }); +} + +#[test] +fn test_coinbase_inject_and_maybe_swap_reverts_excess_tao_deposit_on_swap_failure() { + new_test_ext(1).execute_with(|| { + let zero = U96F32::saturating_from_num(0); + let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); + let tao_to_swap = TaoBalance::from(789_100_u64); + + mock::setup_reserves( + netuid, + TaoBalance::from(1_000_000_000_000_u64), + AlphaBalance::from(1_000_000_000_000_u64), + ); + Swap::maybe_initialize_palswap(netuid, None); + + // Force the buy swap to fail after the excess TAO credit is deposited. + SubnetAlphaIn::::set( + netuid, + AlphaBalance::from(u64::from(mock::SwapMinimumReserve::get()) - 1), + ); + assert!( + SubtensorModule::swap_tao_for_alpha( + netuid, + tao_to_swap, + ::SwapInterface::max_price(), + true, + ) + .is_err() + ); + + let subnet_account = SubtensorModule::get_subnet_account_id(netuid).unwrap(); + let chain_before = Balances::free_balance(subnet_account); + let subnet_tao_before = SubnetTAO::::get(netuid); + let total_issuance_before = TotalIssuance::::get(); + let balances_issuance_before = Balances::total_issuance(); + + let tao_in = BTreeMap::from([(netuid, zero)]); + let alpha_in = BTreeMap::from([(netuid, zero)]); + let excess_tao = BTreeMap::from([(netuid, U96F32::saturating_from_num(tao_to_swap))]); + let credit = SubtensorModule::mint_tao(tao_to_swap); + + SubtensorModule::inject_pool_liquidity_and_swap_excess(&[netuid], &tao_in, &alpha_in, &excess_tao, credit); + + assert_eq!(Balances::free_balance(subnet_account), chain_before); + assert_eq!(SubnetTAO::::get(netuid), subnet_tao_before); + assert_eq!(SubnetExcessTao::::get(netuid), TaoBalance::ZERO); + assert_eq!(TotalIssuance::::get(), total_issuance_before); + assert_eq!(Balances::total_issuance(), balances_issuance_before); + }); +} diff --git a/pallets/subtensor/src/tests/coinbase/mining_emission.rs b/pallets/subtensor/src/tests/coinbase/mining_emission.rs new file mode 100644 index 0000000000..4640ce90ed --- /dev/null +++ b/pallets/subtensor/src/tests/coinbase/mining_emission.rs @@ -0,0 +1,381 @@ +#![allow( + unused, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used +)] +//! Mining emission distribution with/without root sell. + +use super::helpers::*; +use super::prelude::*; + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::mining_emission::test_mining_emission_distribution_with_no_root_sell --exact --show-output --nocapture +#[test] +fn test_mining_emission_distribution_with_no_root_sell() { + new_test_ext(1).execute_with(|| { + let validator_coldkey = U256::from(1); + let validator_hotkey = U256::from(2); + let validator_miner_coldkey = U256::from(3); + let validator_miner_hotkey = U256::from(4); + let miner_coldkey = U256::from(5); + let miner_hotkey = U256::from(6); + let netuid = NetUid::from(1); + let subnet_tempo = 10; + let stake: u64 = 100_000_000_000; + let root_stake: u64 = 200_000_000_000; // 200 TAO + + // Create root network + SubtensorModule::set_tao_weight(0); // Start tao weight at 0 + SubtokenEnabled::::insert(NetUid::ROOT, true); + NetworksAdded::::insert(NetUid::ROOT, true); + + // Add network, register hotkeys, and setup network parameters + add_network(netuid, subnet_tempo, 0); + SubnetMechanism::::insert(netuid, 1); // Set mechanism to 1 + + // Setup large LPs to prevent slippage + SubnetTAO::::insert(netuid, TaoBalance::from(1_000_000_000_000_000_u64)); + SubnetAlphaIn::::insert(netuid, AlphaBalance::from(1_000_000_000_000_000_u64)); + + register_ok_neuron(netuid, validator_hotkey, validator_coldkey, 0); + register_ok_neuron(netuid, validator_miner_hotkey, validator_miner_coldkey, 1); + register_ok_neuron(netuid, miner_hotkey, miner_coldkey, 2); + add_balance_to_coldkey_account( + &validator_coldkey, + TaoBalance::from(stake) + ExistentialDeposit::get(), + ); + add_balance_to_coldkey_account( + &validator_miner_coldkey, + TaoBalance::from(stake) + ExistentialDeposit::get(), + ); + add_balance_to_coldkey_account( + &miner_coldkey, + TaoBalance::from(stake) + ExistentialDeposit::get(), + ); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + step_block(subnet_tempo); + SubnetOwnerCut::::set(u16::MAX / 10); + // There are two validators and three neurons + MaxAllowedUids::::set(netuid, 3); + SubtensorModule::set_max_allowed_validators(netuid, 2); + + // Setup stakes: + // Stake from validator + // Stake from valiminer + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(validator_coldkey), + validator_hotkey, + netuid, + stake.into() + )); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(validator_miner_coldkey), + validator_miner_hotkey, + netuid, + stake.into() + )); + + // Setup YUMA so that it creates emissions + Weights::::insert(NetUidStorageIndex::from(netuid), 0, vec![(1, 0xFFFF)]); + Weights::::insert(NetUidStorageIndex::from(netuid), 1, vec![(2, 0xFFFF)]); + BlockAtRegistration::::set(netuid, 0, 1); + BlockAtRegistration::::set(netuid, 1, 1); + BlockAtRegistration::::set(netuid, 2, 1); + LastUpdate::::set(NetUidStorageIndex::from(netuid), vec![2, 2, 2]); + Kappa::::set(netuid, u16::MAX / 5); + ActivityCutoff::::set(netuid, u16::MAX); // makes all stake active + ValidatorPermit::::insert(netuid, vec![true, true, false]); + + // Run run_coinbase until emissions are drained + step_block(subnet_tempo); + + // Add stake to validator so it has root stake + add_balance_to_coldkey_account(&validator_coldkey, root_stake.into()); + // init root + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(validator_coldkey), + validator_hotkey, + NetUid::ROOT, + root_stake.into() + )); + // Set tao weight non zero + SubtensorModule::set_tao_weight(u64::MAX / 10); + + // Make root sell NOT happen + // set price very low, e.g. a lot of alpha in + let alpha = AlphaBalance::from(1_000_000_000_000_000_000_u64); + SubnetAlphaIn::::insert(netuid, alpha); + + // Make sure we ARE NOT root selling, so we do not have root alpha divs. + let root_sell_flag = SubtensorModule::get_network_root_sell_flag(&[netuid]); + assert!(!root_sell_flag, "Root sell flag should be false"); + + // Run run_coinbase until emissions are drained + step_block(subnet_tempo); + + let old_root_alpha_divs = PendingRootAlphaDivs::::get(netuid); + let per_block_emission = SubtensorModule::get_block_emission_for_issuance( + SubtensorModule::get_alpha_issuance(netuid).into(), + ) + .unwrap_or(0); + + // step by one block + step_block(1); + // Verify that root alpha divs + let new_root_alpha_divs = PendingRootAlphaDivs::::get(netuid); + // Check that we are indeed NOT root selling, i.e. that root alpha divs are NOT increasing + assert_eq!( + new_root_alpha_divs, old_root_alpha_divs, + "Root alpha divs should not increase" + ); + // Check root divs are zero + assert_eq!( + new_root_alpha_divs, + AlphaBalance::ZERO, + "Root alpha divs should be zero" + ); + step_block(1); + // Drain to a clean epoch boundary so accumulation starts fresh. + step_epochs(1, netuid); + let miner_stake_before_epoch = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &miner_hotkey, + &miner_coldkey, + netuid, + ); + // Run again but with some root stake + step_block(subnet_tempo - 1); + assert_abs_diff_eq!( + PendingServerEmission::::get(netuid).to_u64(), + U96F32::saturating_from_num(per_block_emission) + .saturating_mul(U96F32::saturating_from_num((subnet_tempo - 1) as u64)) + .saturating_mul(U96F32::saturating_from_num(0.5)) // miner cut + .saturating_mul(U96F32::saturating_from_num(0.90)) + .saturating_to_num::(), + epsilon = 100_000_u64.into() + ); + step_block(1); + assert!( + BlocksSinceLastStep::::get(netuid) == 0, + "Blocks since last step should be 0" + ); + + let miner_uid = Uids::::get(netuid, miner_hotkey).unwrap_or(0); + log::info!("Miner uid: {miner_uid:?}"); + let miner_incentive: AlphaBalance = { + let miner_incentive = Incentive::::get(NetUidStorageIndex::from(netuid)) + .get(miner_uid as usize) + .copied(); + + assert!(miner_incentive.is_some()); + + (miner_incentive.unwrap_or_default().deconstruct() as u64).into() + }; + log::info!("Miner incentive: {miner_incentive:?}"); + + // Miner emissions + let miner_emission_1: u64 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &miner_hotkey, + &miner_coldkey, + netuid, + ) + .to_u64() + - miner_stake_before_epoch.to_u64(); + + assert_abs_diff_eq!( + Incentive::::get(NetUidStorageIndex::from(netuid)) + .iter() + .map(|p| p.deconstruct()) + .sum::(), + u16::MAX, + epsilon = 10 + ); + + assert_abs_diff_eq!( + miner_emission_1, + U96F32::saturating_from_num(miner_incentive) + .saturating_div(u16::MAX.into()) + .saturating_mul(U96F32::saturating_from_num(per_block_emission)) + .saturating_mul(U96F32::saturating_from_num(subnet_tempo)) + .saturating_mul(U96F32::saturating_from_num(0.45)) // miner cut + .saturating_to_num::(), + epsilon = 1_000_000_u64 + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::mining_emission::test_mining_emission_distribution_with_root_sell --exact --show-output --nocapture +#[test] +fn test_mining_emission_distribution_with_root_sell() { + new_test_ext(1).execute_with(|| { + let validator_coldkey = U256::from(1); + let validator_hotkey = U256::from(2); + let validator_miner_coldkey = U256::from(3); + let validator_miner_hotkey = U256::from(4); + let miner_coldkey = U256::from(5); + let miner_hotkey = U256::from(6); + let subnet_tempo = 10; + let stake: u64 = 100_000_000_000; + let root_stake: u64 = 200_000_000_000; // 200 TAO + + // Create root network + SubtensorModule::set_tao_weight(0); // Start tao weight at 0 + SubtokenEnabled::::insert(NetUid::ROOT, true); + NetworksAdded::::insert(NetUid::ROOT, true); + + // Add network, register hotkeys, and setup network parameters + let owner_hotkey = U256::from(10); + let owner_coldkey = U256::from(11); + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + // Period is `tempo`; `tempo = 2` keeps a one-block gap between epochs so + // pending root-alpha-divs can be observed accumulating before a drain. + Tempo::::insert(netuid, 2); + FirstEmissionBlockNumber::::insert(netuid, 0); + + // Setup large LPs to prevent slippage + SubnetTAO::::insert(netuid, TaoBalance::from(1_000_000_000_000_000_u64)); + SubnetAlphaIn::::insert(netuid, AlphaBalance::from(1_000_000_000_000_000_u64)); + + register_ok_neuron(netuid, validator_hotkey, validator_coldkey, 0); + register_ok_neuron(netuid, validator_miner_hotkey, validator_miner_coldkey, 1); + register_ok_neuron(netuid, miner_hotkey, miner_coldkey, 2); + add_balance_to_coldkey_account( + &validator_coldkey, + TaoBalance::from(stake) + ExistentialDeposit::get(), + ); + add_balance_to_coldkey_account( + &validator_miner_coldkey, + TaoBalance::from(stake) + ExistentialDeposit::get(), + ); + add_balance_to_coldkey_account( + &miner_coldkey, + TaoBalance::from(stake) + ExistentialDeposit::get(), + ); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + step_block(subnet_tempo); + SubnetOwnerCut::::set(u16::MAX / 10); + // There are two validators and three neurons + MaxAllowedUids::::set(netuid, 3); + SubtensorModule::set_max_allowed_validators(netuid, 2); + + // Setup stakes: + // Stake from validator + // Stake from valiminer + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(validator_coldkey), + validator_hotkey, + netuid, + stake.into() + )); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(validator_miner_coldkey), + validator_miner_hotkey, + netuid, + stake.into() + )); + + // Setup YUMA so that it creates emissions + Weights::::insert(NetUidStorageIndex::from(netuid), 0, vec![(1, 0xFFFF)]); + Weights::::insert(NetUidStorageIndex::from(netuid), 1, vec![(2, 0xFFFF)]); + BlockAtRegistration::::set(netuid, 0, 1); + BlockAtRegistration::::set(netuid, 1, 1); + BlockAtRegistration::::set(netuid, 2, 1); + LastUpdate::::set(NetUidStorageIndex::from(netuid), vec![2, 2, 2]); + Kappa::::set(netuid, u16::MAX / 5); + ActivityCutoff::::set(netuid, u16::MAX); // makes all stake active + ValidatorPermit::::insert(netuid, vec![true, true, false]); + + // Run run_coinbase until emissions are drained + step_block(subnet_tempo); + + // Add stake to validator so it has root stake + add_balance_to_coldkey_account(&validator_coldkey, root_stake.into()); + // init root + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(validator_coldkey), + validator_hotkey, + NetUid::ROOT, + root_stake.into() + )); + // Set tao weight non zero + SubtensorModule::set_tao_weight(u64::MAX / 10); + + // Make root sell happen + // Set moving price > 1.0 + // Set price > 1.0 + let alpha = AlphaBalance::from(100_000_000_000_000_u64); + SubnetAlphaIn::::insert(netuid, alpha); + + SubnetMovingPrice::::insert(netuid, I96F32::from_num(2)); + + // Make sure we are root selling, so we have root alpha divs. + let root_sell_flag = SubtensorModule::get_network_root_sell_flag(&[netuid]); + assert!(root_sell_flag, "Root sell flag should be true"); + + // Run run_coinbase until emissions are drained + step_block(subnet_tempo); + + LastEpochBlock::::insert(netuid, SubtensorModule::get_current_block_as_u64()); + let old_root_alpha_divs = PendingRootAlphaDivs::::get(netuid); + let miner_stake_before_epoch = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &miner_hotkey, + &miner_coldkey, + netuid, + ); + + // step by one block + step_block(1); + // Verify root alpha divs + let new_root_alpha_divs = PendingRootAlphaDivs::::get(netuid); + // Check that we ARE root selling, i.e. that root alpha divs are changing + assert_ne!( + new_root_alpha_divs, old_root_alpha_divs, + "Root alpha divs should be changing" + ); + assert!( + new_root_alpha_divs > AlphaBalance::ZERO, + "Root alpha divs should be greater than 0" + ); + + // Run again but with some root stake + step_block(subnet_tempo - 1); + + let miner_uid = Uids::::get(netuid, miner_hotkey).unwrap_or(0); + let miner_incentive: AlphaBalance = { + let miner_incentive = Incentive::::get(NetUidStorageIndex::from(netuid)) + .get(miner_uid as usize) + .copied(); + + assert!(miner_incentive.is_some()); + + (miner_incentive.unwrap_or_default().deconstruct() as u64).into() + }; + log::info!("Miner incentive: {miner_incentive:?}"); + + let per_block_emission = SubtensorModule::get_block_emission_for_issuance( + SubtensorModule::get_alpha_issuance(netuid).into(), + ) + .unwrap_or(0); + + // Miner emissions + let miner_emission_1: u64 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &miner_hotkey, + &miner_coldkey, + netuid, + ) + .to_u64() + - miner_stake_before_epoch.to_u64(); + + assert_abs_diff_eq!( + miner_emission_1, + U96F32::saturating_from_num(miner_incentive) + .saturating_div(u16::MAX.into()) + .saturating_mul(U96F32::saturating_from_num(per_block_emission)) + .saturating_mul(U96F32::saturating_from_num(subnet_tempo)) + .saturating_mul(U96F32::saturating_from_num(0.45)) // miner cut + .saturating_to_num::(), + epsilon = 1_000_000_u64 + ); + }); +} diff --git a/pallets/subtensor/src/tests/coinbase/mod.rs b/pallets/subtensor/src/tests/coinbase/mod.rs new file mode 100644 index 0000000000..7296e634ec --- /dev/null +++ b/pallets/subtensor/src/tests/coinbase/mod.rs @@ -0,0 +1,60 @@ +#![allow( + unused, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used +)] +//! Unit tests for coinbase emission, drain, and dividend distribution. +//! +//! Split from the former monolithic `tests/coinbase.rs` into concept modules. +//! +//! ## Search anchors +//! +//! | Module | Owns | +//! |--------|------| +//! | [`helpers`] | `close` / `set_full_injection_root_stake` fixtures | +//! | [`tao_issuance`] | TAO issuance and emission-enable redistribution | +//! | [`moving_price`] | Moving-price updates | +//! | [`alpha_issuance`] | Alpha issuance and cap triggers | +//! | [`owner_cut`] | Subnet owner cut | +//! | [`pending_emission`] | Pending emission accumulation | +//! | [`drain_emission`] | Drain pending emission to stakers / childkeys | +//! | [`root_children_drain`] | Root children dividend drain | +//! | [`incentive_burn`] | Incentive burn / burn-key sorting | +//! | [`dividend_distribution`] | Dividend and incentive distribution math | +//! | [`distribute_emission`] | Distribute-emission edge cases | +//! | [`run_coinbase_lifecycle`] | run_coinbase start-block gating | +//! | [`incentive_autostake`] | Incentive autostake destination | +//! | [`mining_emission`] | Mining emission with/without root sell | +//! | [`subnet_terms`] | Subnet terms / registration gates | +//! | [`inject_and_swap`] | Inject-and-maybe-swap / TAO materialization | +//! | [`drain_pending_epoch`] | BlocksSinceLastStep / epoch deferral | +//! | [`emit_to_subnets`] | emit_to_subnets root-sell variants | +//! | [`root_proportion`] | Root proportion bookkeeping | +//! | [`epoch_cap_deferral`] | Epoch cap deferral / CRV3 reveal | +//! | [`alpha_dividends`] | Alpha dividend collateral / take floor | + +mod alpha_dividends; +mod alpha_issuance; +mod distribute_emission; +mod dividend_distribution; +mod drain_emission; +mod drain_pending_epoch; +mod emit_to_subnets; +mod epoch_cap_deferral; +mod helpers; +mod incentive_autostake; +mod incentive_burn; +mod inject_and_swap; +mod mining_emission; +mod moving_price; +mod owner_cut; +mod pending_emission; +mod prelude; +mod root_children_drain; +mod root_proportion; +mod run_coinbase_lifecycle; +mod subnet_terms; +mod tao_issuance; diff --git a/pallets/subtensor/src/tests/coinbase/moving_price.rs b/pallets/subtensor/src/tests/coinbase/moving_price.rs new file mode 100644 index 0000000000..2a6ddca2f0 --- /dev/null +++ b/pallets/subtensor/src/tests/coinbase/moving_price.rs @@ -0,0 +1,191 @@ +#![allow( + unused, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used +)] +//! Subnet moving-price updates during coinbase. + +use super::helpers::*; +use super::prelude::*; + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_tao_issuance_different_flows --exact --show-output --nocapture +// #[test] +// fn test_coinbase_tao_issuance_different_flows() { +// new_test_ext(1).execute_with(|| { +// let subnet_owner_ck = U256::from(1001); +// let subnet_owner_hk = U256::from(1002); +// let netuid1 = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); +// let netuid2 = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); +// let emission = 100_000_000; + +// // Setup prices 0.1 and 0.2 +// let initial_tao: u64 = 100_000_u64; +// let initial_alpha1: u64 = initial_tao * 10; +// let initial_alpha2: u64 = initial_tao * 5; +// mock::setup_reserves(netuid1, initial_tao.into(), initial_alpha1.into()); +// mock::setup_reserves(netuid2, initial_tao.into(), initial_alpha2.into()); + +// // Force the swap to initialize +// ::SwapInterface::init_swap(netuid1); +// ::SwapInterface::init_swap(netuid2); + +// // Set subnet prices to reversed proportion to ensure they don't affect emissions. +// SubnetMovingPrice::::insert(netuid1, I96F32::from_num(2)); +// SubnetMovingPrice::::insert(netuid2, I96F32::from_num(1)); + +// // Set subnet tao flow ema. +// let block_num = FlowHalfLife::::get(); +// SubnetEmaTaoFlow::::insert(netuid1, (block_num, I64F64::from_num(1))); +// SubnetEmaTaoFlow::::insert(netuid2, (block_num, I64F64::from_num(2))); +// System::set_block_number(block_num); + +// // Set normalization exponent to 1 for simplicity +// FlowNormExponent::::set(U64F64::from(1_u64)); + +// // Assert initial TAO reserves. +// assert_eq!(SubnetTAO::::get(netuid1), initial_tao.into()); +// assert_eq!(SubnetTAO::::get(netuid2), initial_tao.into()); +// let total_stake_before = TotalStake::::get(); + +// // Run the coinbase with the emission amount. +// SubtensorModule::run_coinbase(U96F32::from_num(emission)); + +// // Assert tao emission is split evenly. +// assert_abs_diff_eq!( +// SubnetTAO::::get(netuid1), +// TaoBalance::from(initial_tao + emission / 3), +// epsilon = 10.into(), +// ); +// assert_abs_diff_eq!( +// SubnetTAO::::get(netuid2), +// TaoBalance::from(initial_tao + 2 * emission / 3), +// epsilon = 10.into(), +// ); + +// // Prices are low => we limit tao issued (buy alpha with it) +// let tao_issued = TaoBalance::from(((0.1 + 0.2) * emission as f64) as u64); +// assert_abs_diff_eq!( +// TotalIssuance::::get(), +// tao_issued, +// epsilon = 10.into() +// ); +// assert_abs_diff_eq!( +// TotalStake::::get(), +// total_stake_before + emission.into(), +// epsilon = 10.into() +// ); +// }); +// } + +// Test moving price updates with different alpha values. +// This test verifies that: +// - Moving price stays constant when alpha is 1.0 +// - Moving price converges to real price at expected rate with alpha 0.1 +// - Moving price updates correctly over multiple iterations +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::moving_price::test_coinbase_moving_prices --exact --show-output --nocapture +#[test] +fn test_coinbase_moving_prices() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + // Set price to 1.0 + SubnetTAO::::insert(netuid, TaoBalance::from(1_000_000)); + SubnetAlphaIn::::insert(netuid, AlphaBalance::from(1_000_000)); + SubnetMechanism::::insert(netuid, 1); + SubnetMovingPrice::::insert(netuid, I96F32::from_num(1)); + FirstEmissionBlockNumber::::insert(netuid, 1); + + // Updating the moving price keeps it the same. + assert_eq!( + SubtensorModule::get_moving_alpha_price(netuid), + I96F32::from_num(1) + ); + // Skip some blocks so that EMA price is not slowed down + System::set_block_number(7_200_000); + + SubtensorModule::update_moving_price(netuid); + assert_eq!( + SubtensorModule::get_moving_alpha_price(netuid), + I96F32::from_num(1) + ); + // Check alpha of 1. + // Set price to zero. + SubnetMovingPrice::::insert(netuid, I96F32::from_num(0)); + SubnetMovingAlpha::::set(I96F32::from_num(1.0)); + // Run moving 1 times. + SubtensorModule::update_moving_price(netuid); + // Assert price is ~ 100% of the real price. + assert!(U64F64::from_num(1.0) - SubtensorModule::get_moving_alpha_price(netuid) < 0.05); + // Set price to zero. + SubnetMovingPrice::::insert(netuid, I96F32::from_num(0)); + SubnetMovingAlpha::::set(I96F32::from_num(0.1)); + + // EMA price 28 days after registration + System::set_block_number(7_200 * 28); + + // Run moving 14 times. + for _ in 0..14 { + SubtensorModule::update_moving_price(netuid); + } + + // Assert price is > 50% of the real price. + assert_abs_diff_eq!( + 0.512325, + SubtensorModule::get_moving_alpha_price(netuid).to_num::(), + epsilon = 0.001 + ); + }); +} + +// Test moving price updates slow down at the beginning. +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::moving_price::test_update_moving_price_initial --exact --show-output --nocapture +#[test] +fn test_update_moving_price_initial() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + // Set current price to 1.0 + SubnetTAO::::insert(netuid, TaoBalance::from(1_000_000)); + SubnetAlphaIn::::insert(netuid, AlphaBalance::from(1_000_000)); + SubnetMechanism::::insert(netuid, 1); + SubnetMovingAlpha::::set(I96F32::from_num(0.5)); + SubnetMovingPrice::::insert(netuid, I96F32::from_num(0)); + + // Registered recently + System::set_block_number(510); + FirstEmissionBlockNumber::::insert(netuid, 500); + + SubtensorModule::update_moving_price(netuid); + + let new_price = SubnetMovingPrice::::get(netuid); + assert!(new_price.to_num::() < 0.001); + }); +} + +// Test moving price updates slow down at the beginning. +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::moving_price::test_update_moving_price_after_time --exact --show-output --nocapture +#[test] +fn test_update_moving_price_after_time() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + // Set current price to 1.0 + SubnetTAO::::insert(netuid, TaoBalance::from(1_000_000)); + SubnetAlphaIn::::insert(netuid, AlphaBalance::from(1_000_000)); + SubnetMechanism::::insert(netuid, 1); + SubnetMovingAlpha::::set(I96F32::from_num(0.5)); + SubnetMovingPrice::::insert(netuid, I96F32::from_num(0)); + + // Registered long time ago + System::set_block_number(144_000_500); + FirstEmissionBlockNumber::::insert(netuid, 500); + + SubtensorModule::update_moving_price(netuid); + + let new_price = SubnetMovingPrice::::get(netuid); + assert!((new_price.to_num::() - 0.5).abs() < 0.001); + }); +} diff --git a/pallets/subtensor/src/tests/coinbase/owner_cut.rs b/pallets/subtensor/src/tests/coinbase/owner_cut.rs new file mode 100644 index 0000000000..2a3a7ef9f9 --- /dev/null +++ b/pallets/subtensor/src/tests/coinbase/owner_cut.rs @@ -0,0 +1,151 @@ +#![allow( + unused, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used +)] +//! Subnet owner cut base case and disabled-owner-cut redistribution. + +use super::helpers::*; +use super::prelude::*; + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::owner_cut::test_owner_cut_base --exact --show-output --nocapture +#[test] +fn test_owner_cut_base() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + mock::setup_reserves( + netuid, + 1_000_000_000_000_u64.into(), + 1_000_000_000_000_u64.into(), + ); + SubtensorModule::set_tempo_unchecked(netuid, 10000); // Large number (dont drain) + SubtensorModule::set_subnet_owner_cut(0); + SubtensorModule::run_coinbase(SubtensorModule::mint_tao(0.into())); + assert_eq!(PendingOwnerCut::::get(netuid), 0.into()); // No cut + SubtensorModule::set_subnet_owner_cut(u16::MAX); + SubtensorModule::run_coinbase(SubtensorModule::mint_tao(0.into())); + assert_eq!(PendingOwnerCut::::get(netuid), 1_000_000_000.into()); // Full cut. + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::coinbase::owner_cut::test_disabling_owner_cut_sends_subnet_emission_to_miners_and_validators --exact --nocapture +#[test] +fn test_disabling_owner_cut_sends_subnet_emission_to_miners_and_validators() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let validator_coldkey = U256::from(1); + let validator_hotkey = U256::from(2); + let miner_coldkey = U256::from(5); + let miner_hotkey = U256::from(6); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + LastEpochBlock::::insert(netuid, SubtensorModule::get_current_block_as_u64()); + let subnet_tempo = 10; + let stake = 100_000_000_000u64; + + SubtensorModule::set_tempo_unchecked(netuid, subnet_tempo); + setup_reserves(netuid, (stake * 10_000).into(), (stake * 10_000).into()); + + register_ok_neuron(netuid, validator_hotkey, validator_coldkey, 0); + register_ok_neuron(netuid, miner_hotkey, miner_coldkey, 1); + + add_balance_to_coldkey_account( + &validator_coldkey, + TaoBalance::from(stake) + ExistentialDeposit::get(), + ); + + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(validator_coldkey), + validator_hotkey, + netuid, + stake.into() + )); + + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_max_allowed_validators(netuid, 1); + step_block(subnet_tempo); + + SubnetOwnerCut::::set(u16::MAX / 10); + SubtensorModule::set_owner_cut_enabled_flag(netuid, false); + + let owner_uid = + SubtensorModule::get_uid_for_net_and_hotkey(netuid, &subnet_owner_hotkey).unwrap(); + let validator_uid = + SubtensorModule::get_uid_for_net_and_hotkey(netuid, &validator_hotkey).unwrap(); + let miner_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &miner_hotkey).unwrap(); + let uid_count = [ + owner_uid as usize, + validator_uid as usize, + miner_uid as usize, + ] + .into_iter() + .max() + .unwrap() + + 1; + + Weights::::insert( + NetUidStorageIndex::from(netuid), + validator_uid, + vec![(miner_uid, 0xFFFF)], + ); + BlockAtRegistration::::set(netuid, owner_uid, 1); + BlockAtRegistration::::set(netuid, validator_uid, 1); + BlockAtRegistration::::set(netuid, miner_uid, 1); + LastUpdate::::set(NetUidStorageIndex::from(netuid), vec![2; uid_count]); + Kappa::::set(netuid, u16::MAX / 5); + ActivityCutoff::::set(netuid, u16::MAX); + let mut validator_permit = vec![false; uid_count]; + validator_permit[validator_uid as usize] = true; + ValidatorPermit::::insert(netuid, validator_permit); + + let owner_stake_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &subnet_owner_hotkey, + &subnet_owner_coldkey, + netuid, + ); + let validator_stake_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &validator_hotkey, + &validator_coldkey, + netuid, + ); + let miner_stake_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &miner_hotkey, + &miner_coldkey, + netuid, + ); + + // Disabling owner cut removes the subnet owner from emission distribution, so the + // subnet emission is fully distributed across the validator and miner paths instead. + step_block(subnet_tempo); + + let owner_stake_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &subnet_owner_hotkey, + &subnet_owner_coldkey, + netuid, + ); + let validator_stake_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &validator_hotkey, + &validator_coldkey, + netuid, + ); + let miner_stake_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &miner_hotkey, + &miner_coldkey, + netuid, + ); + + assert_eq!(owner_stake_after, owner_stake_before); + assert!(validator_stake_after > validator_stake_before); + assert!(miner_stake_after > miner_stake_before); + assert_eq!(PendingOwnerCut::::get(netuid), AlphaBalance::ZERO); + assert!( + Lock::::iter_prefix((subnet_owner_coldkey, netuid)) + .next() + .is_none() + ); + }); +} diff --git a/pallets/subtensor/src/tests/coinbase/pending_emission.rs b/pallets/subtensor/src/tests/coinbase/pending_emission.rs new file mode 100644 index 0000000000..a6048ee2f6 --- /dev/null +++ b/pallets/subtensor/src/tests/coinbase/pending_emission.rs @@ -0,0 +1,146 @@ +#![allow( + unused, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used +)] +//! Pending emission accumulation before and after start. + +use super::helpers::*; +use super::prelude::*; + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::pending_emission::test_pending_emission --exact --show-output --nocapture +#[test] +fn test_pending_emission() { + new_test_ext(1).execute_with(|| { + let hotkey = U256::from(1); + let coldkey = U256::from(2); + let netuid = add_dynamic_network(&hotkey, &coldkey); + remove_owner_registration_stake(netuid); + Tempo::::insert(netuid, 1); + FirstEmissionBlockNumber::::insert(netuid, 0); + + mock::setup_reserves(netuid, 1_000_000.into(), 1.into()); + LastEpochBlock::::insert(netuid, 0); + System::set_block_number(10); + SubtensorModule::run_coinbase(SubtensorModule::mint_tao(0.into())); + SubnetTAO::::insert(NetUid::ROOT, TaoBalance::from(1_000_000_000)); // Add root weight. + System::set_block_number(12); + SubtensorModule::run_coinbase(SubtensorModule::mint_tao(0.into())); + SubtensorModule::set_tempo_unchecked(netuid, 10000); // Large number (dont drain) + SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 + + // Set moving price > 1.0 + SubnetMovingPrice::::insert(netuid, I96F32::from_num(2)); + + // Make sure we are root selling, so we have root alpha divs. + let root_sell_flag = SubtensorModule::get_network_root_sell_flag(&[netuid]); + assert!(root_sell_flag, "Root sell flag should be true"); + + SubtensorModule::run_coinbase(SubtensorModule::mint_tao(0.into())); + // 1 TAO / ( 1 + 3 ) = 0.25 * 1 / 2 = 125000000 + + assert_abs_diff_eq!( + u64::from(PendingServerEmission::::get(netuid)), + 500_000_000, + epsilon = 1 + ); // 1 / 2. + + assert_abs_diff_eq!( + u64::from(PendingValidatorEmission::::get(netuid)), + 500_000_000 - 125000000, + epsilon = 1 + ); // 1 / 2 - swapped. + + assert_abs_diff_eq!( + u64::from(PendingRootAlphaDivs::::get(netuid)), + 125000000, + epsilon = 1 + ); // 1 / 2 * 0.25 --> (from root_prop) + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::pending_emission::test_pending_emission_start_call_not_done --exact --show-output --nocapture +#[test] +fn test_pending_emission_start_call_not_done() { + new_test_ext(1).execute_with(|| { + let validator_coldkey = U256::from(1); + let validator_hotkey = U256::from(2); + let subnet_tempo = 10; + let stake: u64 = 100_000_000_000; + let root_stake: u64 = 200_000_000_000; // 200 TAO + + // Create root network + NetworksAdded::::insert(NetUid::ROOT, true); + // enabled root + SubtokenEnabled::::insert(NetUid::ROOT, true); + + // Add network, register hotkeys, and setup network parameters + let owner_hotkey = U256::from(10); + let owner_coldkey = U256::from(11); + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + // Remove FirstEmissionBlockNumber + FirstEmissionBlockNumber::::remove(netuid); + Tempo::::insert(netuid, subnet_tempo); + + register_ok_neuron(netuid, validator_hotkey, validator_coldkey, 0); + add_balance_to_coldkey_account( + &validator_coldkey, + TaoBalance::from(stake) + ExistentialDeposit::get(), + ); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + step_block(subnet_tempo); + SubnetOwnerCut::::set(u16::MAX / 10); + // There are two validators and three neurons + MaxAllowedUids::::set(netuid, 3); + SubtensorModule::set_max_allowed_validators(netuid, 2); + + // Add stake to validator so it has root stake + add_balance_to_coldkey_account(&validator_coldkey, root_stake.into()); + // init root + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(validator_coldkey), + validator_hotkey, + NetUid::ROOT, + root_stake.into() + )); + // Set tao weight non zero + SubtensorModule::set_tao_weight(u64::MAX / 10); + + // Make root sell happen + // Set moving price > 1.0 + // Set price > 1.0 + let tao = TaoBalance::from(10_000_000_000_u64); + let alpha = AlphaBalance::from(1_000_000_000_u64); + SubnetTAO::::insert(netuid, tao); + SubnetAlphaIn::::insert(netuid, alpha); + + SubnetMovingPrice::::insert(netuid, I96F32::from_num(2)); + + // Make sure we are root selling, so we have root alpha divs. + let root_sell_flag = SubtensorModule::get_network_root_sell_flag(&[netuid]); + assert!(root_sell_flag, "Root sell flag should be true"); + + // !!! Check that the subnet FirstEmissionBlockNumber is None -- no entry + assert!(FirstEmissionBlockNumber::::get(netuid).is_none()); + + // Run run_coinbase until emissions are accumulated + step_block(subnet_tempo - 2); + + // Verify that all pending emissions are zero + assert_eq!( + PendingServerEmission::::get(netuid), + AlphaBalance::ZERO + ); + assert_eq!( + PendingValidatorEmission::::get(netuid), + AlphaBalance::ZERO + ); + assert_eq!( + PendingRootAlphaDivs::::get(netuid), + AlphaBalance::ZERO + ); + }); +} diff --git a/pallets/subtensor/src/tests/coinbase/prelude.rs b/pallets/subtensor/src/tests/coinbase/prelude.rs new file mode 100644 index 0000000000..406bfcd5bc --- /dev/null +++ b/pallets/subtensor/src/tests/coinbase/prelude.rs @@ -0,0 +1,22 @@ +#![allow( + unused, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used +)] +//! Shared imports for coinbase unit tests. + +pub(super) use super::super::mock; +pub use super::super::mock::*; + +pub use crate::*; +pub use alloc::collections::BTreeMap; +pub use approx::assert_abs_diff_eq; +pub use frame_support::assert_ok; +pub use sp_core::U256; +pub use sp_runtime::PerU16; +pub use substrate_fixed::types::{I64F64, I96F32, U64F64, U96F32}; +pub use subtensor_runtime_common::{AlphaBalance, NetUidStorageIndex}; +pub use subtensor_swap_interface::SwapHandler; diff --git a/pallets/subtensor/src/tests/coinbase/root_children_drain.rs b/pallets/subtensor/src/tests/coinbase/root_children_drain.rs new file mode 100644 index 0000000000..76662a049a --- /dev/null +++ b/pallets/subtensor/src/tests/coinbase/root_children_drain.rs @@ -0,0 +1,548 @@ +#![allow( + unused, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used +)] +//! Root children dividend drain through coinbase. + +use super::helpers::*; +use super::prelude::*; + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::root_children_drain::test_get_root_children --exact --show-output --nocapture +#[test] +fn test_get_root_children() { + new_test_ext(1).execute_with(|| { + // Init netuid 1 + let alpha = NetUid::from(1); + add_network(NetUid::ROOT, 1, 0); + add_network(alpha, 1, 0); + + // Set TAO weight to 1. + SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1. + + // Create keys. + let cold = U256::from(0); + let alice = U256::from(1); + let bob = U256::from(2); + + // Register Alice and Bob to the root network and alpha subnet. + register_ok_neuron(alpha, alice, cold, 0); + register_ok_neuron(alpha, bob, cold, 0); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(cold).clone(), + alice, + )); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(cold).clone(), + bob, + )); + + // Add stake for Alice and Bob on root. + let alice_root_stake = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &alice, + &cold, + NetUid::ROOT, + alice_root_stake, + ); + let bob_root_stake = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &bob, + &cold, + NetUid::ROOT, + alice_root_stake, + ); + + // Add stake for Alice and Bob on netuid. + let alice_alpha_stake = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &alice, + &cold, + alpha, + alice_alpha_stake, + ); + let bob_alpha_stake = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &bob, + &cold, + alpha, + bob_alpha_stake, + ); + + // Set Bob as 100% child of Alice on root. + // mock_set_children_no_epochs( NetUid::ROOT, &alice, &[(u64::MAX, bob)]); + mock_set_children_no_epochs(alpha, &alice, &[(u64::MAX, bob)]); + + // Assert Alice and Bob stake on root and netuid + assert_eq!( + SubtensorModule::get_stake_for_hotkey_on_subnet(&alice, NetUid::ROOT), + alice_root_stake + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_on_subnet(&bob, NetUid::ROOT), + bob_root_stake + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_on_subnet(&alice, alpha), + alice_alpha_stake + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_on_subnet(&bob, alpha), + bob_alpha_stake + ); + + // Assert Alice and Bob inherited stakes + assert_eq!( + SubtensorModule::get_inherited_for_hotkey_on_subnet(&alice, NetUid::ROOT), + alice_root_stake + ); + assert_eq!( + SubtensorModule::get_inherited_for_hotkey_on_subnet(&alice, alpha), + 0.into() + ); + assert_eq!( + SubtensorModule::get_inherited_for_hotkey_on_subnet(&bob, NetUid::ROOT), + bob_root_stake + ); + assert_eq!( + SubtensorModule::get_inherited_for_hotkey_on_subnet(&bob, alpha), + bob_alpha_stake + alice_alpha_stake + ); + + // Assert Alice and Bob TAO inherited stakes + assert_eq!( + SubtensorModule::get_tao_inherited_for_hotkey_on_subnet(&alice, alpha), + TaoBalance::ZERO + ); + assert_eq!( + SubtensorModule::get_tao_inherited_for_hotkey_on_subnet(&bob, alpha), + u64::from(bob_root_stake + alice_root_stake).into() + ); + + // Get Alice stake amounts on subnet alpha. + let (alice_total, alice_alpha, alice_tao): (I64F64, I64F64, I64F64) = + SubtensorModule::get_stake_weights_for_hotkey_on_subnet(&alice, alpha); + assert_eq!(alice_total, I64F64::from_num(0)); + + // Get Bob stake amounts on subnet alpha. + let (bob_total, bob_alpha, bob_tao): (I64F64, I64F64, I64F64) = + SubtensorModule::get_stake_weights_for_hotkey_on_subnet(&bob, alpha); + assert_eq!( + bob_total, + I64F64::from_num(u64::from(bob_root_stake * 4.into())) + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::root_children_drain::test_get_root_children_drain --exact --show-output --nocapture +#[test] +fn test_get_root_children_drain() { + new_test_ext(1).execute_with(|| { + // Init netuid 1 + let alpha = NetUid::from(1); + add_network(NetUid::ROOT, 1, 0); + add_network(alpha, 1, 0); + SubtensorModule::set_ck_burn(0); + // Set TAO weight to 1. + SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1. + // Create keys. + let cold_alice = U256::from(0); + let cold_bob = U256::from(1); + let alice = U256::from(2); + let bob = U256::from(3); + // Register Alice and Bob to the root network and alpha subnet. + register_ok_neuron(alpha, alice, cold_alice, 0); + register_ok_neuron(alpha, bob, cold_bob, 0); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(cold_alice).clone(), + alice, + )); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(cold_bob).clone(), + bob, + )); + // Add stake for Alice and Bob on root. + let alice_root_stake = 1_000_000_000; + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &alice, + &cold_alice, + NetUid::ROOT, + alice_root_stake.into(), + ); + let bob_root_stake = 1_000_000_000; + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &bob, + &cold_bob, + NetUid::ROOT, + bob_root_stake.into(), + ); + // Add stake for Alice and Bob on netuid. + let alice_alpha_stake = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &alice, + &cold_alice, + alpha, + alice_alpha_stake, + ); + let bob_alpha_stake = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &bob, + &cold_bob, + alpha, + bob_alpha_stake, + ); + // Set Bob as 100% child of Alice on root. + mock_set_children_no_epochs(alpha, &alice, &[(u64::MAX, bob)]); + // Set Bob childkey take to zero. + ChildkeyTake::::insert(bob, alpha, PerU16::zero()); + Delegates::::insert(alice, PerU16::zero()); + Delegates::::insert(bob, PerU16::zero()); + + // Get Alice stake amounts on subnet alpha. + let (alice_total, alice_alpha, alice_tao): (I64F64, I64F64, I64F64) = + SubtensorModule::get_stake_weights_for_hotkey_on_subnet(&alice, alpha); + assert_eq!(alice_total, I64F64::from_num(0)); + + // Get Bob stake amounts on subnet alpha. + let (bob_total, bob_alpha, bob_tao): (I64F64, I64F64, I64F64) = + SubtensorModule::get_stake_weights_for_hotkey_on_subnet(&bob, alpha); + assert_eq!(bob_total, I64F64::from_num(4_u64 * bob_root_stake)); + + // Lets drain + let pending_alpha = AlphaBalance::from(1_000_000_000); + SubtensorModule::distribute_emission( + alpha, + pending_alpha.saturating_div(2.into()).into(), + pending_alpha.saturating_div(2.into()).into(), + AlphaBalance::ZERO, + AlphaBalance::ZERO, + ); + + // Alice and Bob both made half of the dividends. + assert_eq!( + SubtensorModule::get_stake_for_hotkey_on_subnet(&alice, alpha), + alice_alpha_stake + pending_alpha / 2.into() + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_on_subnet(&bob, alpha), + bob_alpha_stake + pending_alpha / 2.into() + ); + + // There should be no TAO on the root subnet. + assert_eq!(SubnetTAO::::get(NetUid::ROOT), TaoBalance::ZERO); + + // Lets drain + let pending_alpha = AlphaBalance::from(1_000_000_000); + let pending_root1 = TaoBalance::from(1_000_000_000); + SubtensorModule::distribute_emission( + alpha, + pending_alpha.saturating_div(2.into()).into(), + pending_alpha.saturating_div(2.into()).into(), + AlphaBalance::ZERO, + AlphaBalance::ZERO, + ); + + // Alice and Bob both made half of the dividends. + assert_eq!( + SubtensorModule::get_stake_for_hotkey_on_subnet(&alice, NetUid::ROOT), + AlphaBalance::from(alice_root_stake) + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_on_subnet(&bob, NetUid::ROOT), + AlphaBalance::from(bob_root_stake) + ); + + // Lets change the take value. (Bob is greedy.) + ChildkeyTake::::insert(bob, alpha, PerU16::from_parts(u16::MAX)); + + // Lets drain + let pending_alpha = AlphaBalance::from(1_000_000_000); + let pending_root2 = TaoBalance::from(1_000_000_000); + SubtensorModule::distribute_emission( + alpha, + pending_alpha.saturating_div(2.into()).into(), + pending_alpha.saturating_div(2.into()).into(), + AlphaBalance::ZERO, + AlphaBalance::ZERO, + ); + + // Alice makes nothing + assert_eq!( + AlphaDividendsPerSubnet::::get(alpha, alice), + AlphaBalance::ZERO + ); + // Bob makes it all. + assert_abs_diff_eq!( + AlphaDividendsPerSubnet::::get(alpha, bob), + pending_alpha, + epsilon = 1.into() + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::root_children_drain::test_get_root_children_drain_half_proportion --exact --show-output --nocapture +#[test] +fn test_get_root_children_drain_half_proportion() { + new_test_ext(1).execute_with(|| { + // Init netuid 1 + let alpha = NetUid::from(1); + add_network(NetUid::ROOT, 1, 0); + add_network(alpha, 1, 0); + SubtensorModule::set_ck_burn(0); + // Set TAO weight to 1. + SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1. + // Create keys. + let cold_alice = U256::from(0); + let cold_bob = U256::from(1); + let alice = U256::from(2); + let bob = U256::from(3); + // Register Alice and Bob to the root network and alpha subnet. + register_ok_neuron(alpha, alice, cold_alice, 0); + register_ok_neuron(alpha, bob, cold_bob, 0); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(cold_alice).clone(), + alice, + )); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(cold_bob).clone(), + bob, + )); + // Add stake for Alice and Bob on root. + let alice_root_stake = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &alice, + &cold_alice, + NetUid::ROOT, + alice_root_stake, + ); + let bob_root_stake = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &bob, + &cold_bob, + NetUid::ROOT, + alice_root_stake, + ); + // Add stake for Alice and Bob on netuid. + let alice_alpha_stake = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &alice, + &cold_alice, + alpha, + alice_alpha_stake, + ); + let bob_alpha_stake = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &bob, + &cold_bob, + alpha, + bob_alpha_stake, + ); + // Set Bob as 100% child of Alice on root. + mock_set_children_no_epochs(alpha, &alice, &[(u64::MAX / 2, bob)]); + + // Set Bob childkey take to zero. + ChildkeyTake::::insert(bob, alpha, PerU16::zero()); + Delegates::::insert(alice, PerU16::zero()); + Delegates::::insert(bob, PerU16::zero()); + + // Lets drain! + let pending_alpha = AlphaBalance::from(1_000_000_000); + SubtensorModule::distribute_emission( + alpha, + pending_alpha.saturating_div(2.into()).into(), + pending_alpha.saturating_div(2.into()).into(), + AlphaBalance::ZERO, + AlphaBalance::ZERO, + ); + + // Alice and Bob make the same amount. + close( + AlphaDividendsPerSubnet::::get(alpha, alice).into(), + (pending_alpha / 2.into()).into(), + 10, + ); + close( + AlphaDividendsPerSubnet::::get(alpha, bob).into(), + (pending_alpha / 2.into()).into(), + 10, + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::root_children_drain::test_get_root_children_drain_with_take --exact --show-output --nocapture +#[test] +fn test_get_root_children_drain_with_take() { + new_test_ext(1).execute_with(|| { + // Init netuid 1 + let alpha = NetUid::from(1); + add_network(NetUid::ROOT, 1, 0); + add_network(alpha, 1, 0); + // Set TAO weight to 1. + SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1. + // Create keys. + let cold_alice = U256::from(0); + let cold_bob = U256::from(1); + let alice = U256::from(2); + let bob = U256::from(3); + // Register Alice and Bob to the root network and alpha subnet. + register_ok_neuron(alpha, alice, cold_alice, 0); + register_ok_neuron(alpha, bob, cold_bob, 0); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(cold_alice).clone(), + alice, + )); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(cold_bob).clone(), + bob, + )); + // Add stake for Alice and Bob on root. + let alice_root_stake = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &alice, + &cold_alice, + NetUid::ROOT, + alice_root_stake, + ); + let bob_root_stake = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &bob, + &cold_bob, + NetUid::ROOT, + alice_root_stake, + ); + // Add stake for Alice and Bob on netuid. + let alice_alpha_stake = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &alice, + &cold_alice, + alpha, + alice_alpha_stake, + ); + let bob_alpha_stake = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &bob, + &cold_bob, + alpha, + bob_alpha_stake, + ); + // Set Bob as 100% child of Alice on root. + ChildkeyTake::::insert(bob, alpha, PerU16::from_parts(u16::MAX)); + mock_set_children_no_epochs(alpha, &alice, &[(u64::MAX, bob)]); + // Set Bob validator take to zero. + Delegates::::insert(alice, PerU16::zero()); + Delegates::::insert(bob, PerU16::zero()); + + // Lets drain! + let pending_alpha = AlphaBalance::from(1_000_000_000); + SubtensorModule::distribute_emission( + alpha, + pending_alpha.saturating_div(2.into()).into(), + pending_alpha.saturating_div(2.into()).into(), + AlphaBalance::ZERO, + AlphaBalance::ZERO, + ); + + // Bob makes it all. + close( + AlphaDividendsPerSubnet::::get(alpha, alice).into(), + 0, + 10, + ); + close( + AlphaDividendsPerSubnet::::get(alpha, bob).into(), + pending_alpha.into(), + 10, + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::root_children_drain::test_get_root_children_drain_with_half_take --exact --show-output --nocapture +#[test] +fn test_get_root_children_drain_with_half_take() { + new_test_ext(1).execute_with(|| { + // Init netuid 1 + let alpha = NetUid::from(1); + add_network(NetUid::ROOT, 1, 0); + add_network(alpha, 1, 0); + // Set TAO weight to 1. + SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1. + SubtensorModule::set_ck_burn(0); + // Create keys. + let cold_alice = U256::from(0); + let cold_bob = U256::from(1); + let alice = U256::from(2); + let bob = U256::from(3); + // Register Alice and Bob to the root network and alpha subnet. + register_ok_neuron(alpha, alice, cold_alice, 0); + register_ok_neuron(alpha, bob, cold_bob, 0); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(cold_alice).clone(), + alice, + )); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(cold_bob).clone(), + bob, + )); + // Add stake for Alice and Bob on root. + let alice_root_stake = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &alice, + &cold_alice, + NetUid::ROOT, + alice_root_stake, + ); + let bob_root_stake = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &bob, + &cold_bob, + NetUid::ROOT, + alice_root_stake, + ); + // Add stake for Alice and Bob on netuid. + let alice_alpha_stake = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &alice, + &cold_alice, + alpha, + alice_alpha_stake, + ); + let bob_alpha_stake = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &bob, + &cold_bob, + alpha, + bob_alpha_stake, + ); + // Set Bob as 100% child of Alice on root. + ChildkeyTake::::insert(bob, alpha, PerU16::from_parts(u16::MAX / 2)); + mock_set_children_no_epochs(alpha, &alice, &[(u64::MAX, bob)]); + // Set Bob childkey take to zero. + Delegates::::insert(alice, PerU16::zero()); + Delegates::::insert(bob, PerU16::zero()); + + // Lets drain! + let pending_alpha = AlphaBalance::from(1_000_000_000); + SubtensorModule::distribute_emission( + alpha, + pending_alpha.saturating_div(2.into()).into(), + pending_alpha.saturating_div(2.into()).into(), + AlphaBalance::ZERO, + AlphaBalance::ZERO, + ); + + // Alice and Bob make the same amount. + close( + AlphaDividendsPerSubnet::::get(alpha, alice).into(), + (pending_alpha / 4.into()).into(), + 10000, + ); + close( + AlphaDividendsPerSubnet::::get(alpha, bob).into(), + 3 * u64::from(pending_alpha / 4.into()), + 10000, + ); + }); +} diff --git a/pallets/subtensor/src/tests/coinbase/root_proportion.rs b/pallets/subtensor/src/tests/coinbase/root_proportion.rs new file mode 100644 index 0000000000..8120182690 --- /dev/null +++ b/pallets/subtensor/src/tests/coinbase/root_proportion.rs @@ -0,0 +1,73 @@ +#![allow( + unused, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used +)] +//! Root proportion bookkeeping on block step. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_root_prop_filled_on_block_step() { + new_test_ext(1).execute_with(|| { + let hotkey = U256::from(10); + let coldkey = U256::from(11); + let netuid1 = add_dynamic_network(&hotkey, &coldkey); + let netuid2 = add_dynamic_network(&hotkey, &coldkey); + + SubnetTAO::::insert(NetUid::ROOT, TaoBalance::from(1_000_000_000_000u64)); + SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 + + let tao_reserve = TaoBalance::from(50_000_000_000_u64); + let alpha_in = AlphaBalance::from(100_000_000_000_u64); + SubnetTAO::::insert(netuid1, tao_reserve); + SubnetAlphaIn::::insert(netuid1, alpha_in); + SubnetTAO::::insert(netuid2, tao_reserve); + SubnetAlphaIn::::insert(netuid2, alpha_in); + + assert!(!RootProp::::contains_key(netuid1)); + assert!(!RootProp::::contains_key(netuid2)); + + run_to_block(2); + + assert!(RootProp::::get(netuid1) > U96F32::from_num(0)); + assert!(RootProp::::get(netuid2) > U96F32::from_num(0)); + }); +} + +#[test] +fn test_root_proportion() { + new_test_ext(1).execute_with(|| { + let hotkey = U256::from(10); + let coldkey = U256::from(11); + let netuid = add_dynamic_network(&hotkey, &coldkey); + + let root_tao_reserve = 1_000_000_000_000u64; + SubnetTAO::::insert(NetUid::ROOT, TaoBalance::from(root_tao_reserve)); + + let tao_weight = 3_320_413_933_267_719_290u64; + SubtensorModule::set_tao_weight(tao_weight); + + let alpha_in = 100_000_000_000u64; + SubnetAlphaIn::::insert(netuid, AlphaBalance::from(alpha_in)); + + let actual_root_proportion = SubtensorModule::root_proportion(netuid); + let expected_root_prop = { + let tao_weight = SubtensorModule::get_tao_weight(); + let root_tao = U96F32::from_num(root_tao_reserve); + let alpha_in = { + let alpha: u64 = SubtensorModule::get_alpha_issuance(netuid).into(); + + U96F32::from_num(alpha) + }; + + tao_weight * root_tao / (tao_weight * root_tao + alpha_in) + }; + + assert_eq!(actual_root_proportion, expected_root_prop); + }); +} diff --git a/pallets/subtensor/src/tests/coinbase/run_coinbase_lifecycle.rs b/pallets/subtensor/src/tests/coinbase/run_coinbase_lifecycle.rs new file mode 100644 index 0000000000..d33060eb3d --- /dev/null +++ b/pallets/subtensor/src/tests/coinbase/run_coinbase_lifecycle.rs @@ -0,0 +1,224 @@ +#![allow( + unused, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used +)] +//! run_coinbase gating before subnet start block. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_run_coinbase_not_started() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let tempo = 2; + + let sn_owner_hk = U256::from(7); + let sn_owner_ck = U256::from(8); + + add_network_without_emission_block(netuid, tempo, 0); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); + assert_eq!(FirstEmissionBlockNumber::::get(netuid), None); + + SubnetOwner::::insert(netuid, sn_owner_ck); + SubnetOwnerHotkey::::insert(netuid, sn_owner_hk); + + let hotkey = U256::from(3); + let coldkey = U256::from(4); + let miner_hk = U256::from(5); + let miner_ck = U256::from(6); + let init_stake: u64 = 100_000_000_000_000; + let tempo = 2; + SubtensorModule::set_tempo_unchecked(netuid, tempo); + // Set weight-set limit to 0. + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + let reserve = init_stake * 1000; + mock::setup_reserves(netuid, reserve.into(), reserve.into()); + + register_ok_neuron(netuid, hotkey, coldkey, 0); + register_ok_neuron(netuid, miner_hk, miner_ck, 0); + register_ok_neuron(netuid, sn_owner_hk, sn_owner_ck, 0); + // Give non-zero stake + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + init_stake.into(), + ); + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey), + init_stake.into() + ); + + // Set the weight of root TAO to be 0%, so only alpha is effective. + SubtensorModule::set_tao_weight(0); + + run_to_block_no_epoch(netuid, 30); + + // Run epoch for initial setup. + SubtensorModule::epoch(netuid, AlphaBalance::ZERO); + + // Set weights on miner + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid, + vec![0, 1, 2], + vec![0, 0, 1], + 0, + )); + + // Clear incentive and dividends. + Incentive::::remove(NetUidStorageIndex::from(netuid)); + Dividends::::remove(netuid); + + // Step so tempo should run. + next_block_no_epoch(netuid); + next_block_no_epoch(netuid); + next_block_no_epoch(netuid); + let current_block = System::block_number(); + assert!(SubtensorModule::should_run_epoch(netuid, current_block)); + + // Run coinbase with emission. + let emission_credit = SubtensorModule::mint_tao(100_000_000.into()); + SubtensorModule::run_coinbase(emission_credit); + + // We expect that the epoch ran. + assert_eq!(BlocksSinceLastStep::::get(netuid), 0); + + // Get the new stake of the hotkey. We expect no emissions. + let new_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + // We expect the stake to remain unchanged. + assert_eq!(new_stake, init_stake.into()); + + // Check that the incentive and dividends are set. + assert!( + Incentive::::get(NetUidStorageIndex::from(netuid)) + .iter() + .map(|p| p.deconstruct()) + .sum::() + > 0 + ); + assert!( + Dividends::::get(netuid) + .iter() + .map(|p| p.deconstruct()) + .sum::() + > 0 + ); + }); +} + +#[test] +fn test_run_coinbase_not_started_start_after() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let tempo = 2; + + let sn_owner_hk = U256::from(7); + let sn_owner_ck = U256::from(8); + + add_network_without_emission_block(netuid, tempo, 0); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); + assert_eq!(FirstEmissionBlockNumber::::get(netuid), None); + + SubnetOwner::::insert(netuid, sn_owner_ck); + SubnetOwnerHotkey::::insert(netuid, sn_owner_hk); + + let hotkey = U256::from(3); + let coldkey = U256::from(4); + let miner_hk = U256::from(5); + let miner_ck = U256::from(6); + let init_stake: u64 = 100_000_000_000_000; + let tempo = 2; + SubtensorModule::set_tempo_unchecked(netuid, tempo); + // Set weight-set limit to 0. + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + register_ok_neuron(netuid, hotkey, coldkey, 0); + register_ok_neuron(netuid, miner_hk, miner_ck, 0); + register_ok_neuron(netuid, sn_owner_hk, sn_owner_ck, 0); + // Give non-zero stake + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + init_stake.into(), + ); + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey), + init_stake.into() + ); + + // Set the weight of root TAO to be 0%, so only alpha is effective. + SubtensorModule::set_tao_weight(0); + + run_to_block_no_epoch(netuid, 30); + + // Run epoch for initial setup. + SubtensorModule::epoch(netuid, AlphaBalance::ZERO); + + // Set weights on miner + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid, + vec![0, 1, 2], + vec![0, 0, 1], + 0, + )); + + // Clear incentive and dividends. + Incentive::::remove(NetUidStorageIndex::from(netuid)); + Dividends::::remove(netuid); + + // Step so tempo should run. + next_block_no_epoch(netuid); + next_block_no_epoch(netuid); + next_block_no_epoch(netuid); + let current_block = System::block_number(); + assert!(SubtensorModule::should_run_epoch(netuid, current_block)); + + // Run coinbase with emission. + let emission_credit = SubtensorModule::mint_tao(100_000_000.into()); + SubtensorModule::run_coinbase(emission_credit); + // We expect that the epoch ran. + assert_eq!(BlocksSinceLastStep::::get(netuid), 0); + + let block_number = StartCallDelay::::get(); + run_to_block_no_epoch(netuid, block_number); + + let current_block = System::block_number(); + + // Run start call. + assert_ok!(SubtensorModule::start_call( + RuntimeOrigin::signed(sn_owner_ck), + netuid + )); + assert_eq!( + FirstEmissionBlockNumber::::get(netuid), + Some(current_block + 1) + ); + + // Advance the block past `LastEpochBlock + tempo` so the state-based + // scheduler is due again (the previous `run_coinbase` advanced it). + next_block_no_epoch(netuid); + next_block_no_epoch(netuid); + next_block_no_epoch(netuid); + + // Run coinbase with emission. + let emission_credit = SubtensorModule::mint_tao(100_000_000.into()); + SubtensorModule::run_coinbase(emission_credit); + // We expect that the epoch ran. + assert_eq!(BlocksSinceLastStep::::get(netuid), 0); + + // Get the new stake of the hotkey. We expect no emissions. + let new_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + // We expect the stake to remain unchanged. + assert!(new_stake > init_stake.into()); + log::info!("new_stake: {new_stake}"); + }); +} diff --git a/pallets/subtensor/src/tests/coinbase/subnet_terms.rs b/pallets/subtensor/src/tests/coinbase/subnet_terms.rs new file mode 100644 index 0000000000..345f3fd08e --- /dev/null +++ b/pallets/subtensor/src/tests/coinbase/subnet_terms.rs @@ -0,0 +1,247 @@ +#![allow( + unused, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used +)] +//! Subnet terms / registration gates for coinbase emission. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_coinbase_subnets_with_no_reg_get_no_emission() { + new_test_ext(1).execute_with(|| { + let zero = U96F32::saturating_from_num(0); + let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); + let netuid1 = add_dynamic_network(&U256::from(3), &U256::from(4)); + + // Setup initial state + SubtokenEnabled::::insert(netuid0, true); + SubtokenEnabled::::insert(netuid1, true); + FirstEmissionBlockNumber::::insert(netuid0, 0); + FirstEmissionBlockNumber::::insert(netuid1, 0); + // Explicitly allow registration for both subnets + NetworkRegistrationAllowed::::insert(netuid0, true); + NetworkRegistrationAllowed::::insert(netuid1, true); + NetworkPowRegistrationAllowed::::insert(netuid0, false); + NetworkPowRegistrationAllowed::::insert(netuid1, true); + + // Note that netuid0 has only one method allowed + // And, netuid1 has *both* methods allowed + // Both should be in the list. + let subnets_to_emit_to_0 = SubtensorModule::get_subnets_to_emit_to(&[netuid0, netuid1]); + // Check that both subnets are in the list + assert_eq!(subnets_to_emit_to_0.len(), 2); + assert!(subnets_to_emit_to_0.contains(&netuid0)); + assert!(subnets_to_emit_to_0.contains(&netuid1)); + + // Disabled registration of both methods on ONLY netuid0 + NetworkRegistrationAllowed::::insert(netuid0, false); + NetworkPowRegistrationAllowed::::insert(netuid0, false); + + // Check that netuid0 is not in the list + let subnets_to_emit_to_1 = SubtensorModule::get_subnets_to_emit_to(&[netuid0, netuid1]); + assert_eq!(subnets_to_emit_to_1.len(), 1); + assert!(!subnets_to_emit_to_1.contains(&netuid0)); + // Netuid1 still in the list + assert!(subnets_to_emit_to_1.contains(&netuid1)); + }); +} + +// Tests for the excess TAO condition +#[test] +fn test_coinbase_subnet_terms_with_alpha_in_gt_alpha_emission() { + new_test_ext(1).execute_with(|| { + let zero = U96F32::saturating_from_num(0); + let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); + mock::setup_reserves( + netuid0, + TaoBalance::from(1_000_000_000_000_000_u64), + AlphaBalance::from(1_000_000_000_000_000_u64), + ); + // Initialize swap + Swap::maybe_initialize_palswap(netuid0, None); + + // Set netuid0 to have price tao_emission / price > alpha_emission + let alpha_emission = U96F32::saturating_from_num( + SubtensorModule::get_block_emission_for_issuance( + SubtensorModule::get_alpha_issuance(netuid0).into(), + ) + .unwrap_or(0), + ); + let price_to_set: U64F64 = U64F64::saturating_from_num(0.01); + let price_to_set_fixed: U96F32 = U96F32::saturating_from_num(price_to_set); + + let tao_emission: U96F32 = U96F32::saturating_from_num(alpha_emission) + .saturating_mul(price_to_set_fixed) + .saturating_add(U96F32::saturating_from_num(0.01)); + + // Set the price + let tao = TaoBalance::from(1_000_000_000_u64); + let alpha = AlphaBalance::from( + (U64F64::saturating_from_num(u64::from(tao)) / price_to_set).to_num::(), + ); + SubnetTAO::::insert(netuid0, tao); + SubnetAlphaIn::::insert(netuid0, alpha); + + // Check the price is set + assert_abs_diff_eq!( + pallet_subtensor_swap::Pallet::::current_alpha_price(netuid0).to_num::(), + price_to_set.to_num::(), + epsilon = 0.001 + ); + + let subnet_emissions = BTreeMap::from([(netuid0, tao_emission)]); + + // The injection cap is root_proportion * alpha_emission. Seed root stake so + // root_proportion is well-defined and the cap is positive. + set_full_injection_root_stake(); + let root_prop: U96F32 = SubtensorModule::root_proportion(netuid0); + let injection_cap: U96F32 = root_prop.saturating_mul(alpha_emission); + + let (tao_in, alpha_in, alpha_out, excess_tao) = + SubtensorModule::compute_subnet_emission_terms(&subnet_emissions); + + // Check our condition is met: the raw alpha_in exceeds the cap, so it binds. + assert!(tao_emission / price_to_set_fixed > injection_cap); + + // alpha_out should be the alpha_emission, always + assert_abs_diff_eq!( + alpha_out[&netuid0].to_num::(), + alpha_emission.to_num::(), + epsilon = 0.01 + ); + + // alpha_in should be capped at root_proportion * alpha_emission + assert_abs_diff_eq!( + alpha_in[&netuid0].to_num::(), + injection_cap.to_num::(), + epsilon = injection_cap.to_num::() / 1_000.0 + ); + // tao_in should be the alpha_in at the ratio of the price + assert_abs_diff_eq!( + tao_in[&netuid0].to_num::(), + alpha_in[&netuid0] + .saturating_mul(price_to_set_fixed) + .to_num::(), + epsilon = 0.01 + ); + + // excess_tao should be the difference between the tao_emission and the tao_in + assert_abs_diff_eq!( + excess_tao[&netuid0].to_num::(), + tao_emission.to_num::() - tao_in[&netuid0].to_num::(), + epsilon = 0.01 + ); + }); +} + +#[test] +fn test_coinbase_subnet_terms_with_alpha_in_lte_alpha_emission() { + new_test_ext(1).execute_with(|| { + let zero = U96F32::saturating_from_num(0); + let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); + mock::setup_reserves( + netuid0, + TaoBalance::from(1_000_000_000_000_000_u64), + AlphaBalance::from(1_000_000_000_000_000_u64), + ); + // Initialize swap + Swap::maybe_initialize_palswap(netuid0, None); + + let alpha_emission = U96F32::saturating_from_num( + SubtensorModule::get_block_emission_for_issuance( + SubtensorModule::get_alpha_issuance(netuid0).into(), + ) + .unwrap_or(0), + ); + let tao_emission = U96F32::saturating_from_num(34566756_u64); + + let price: U96F32 = U96F32::saturating_from_num(Swap::current_alpha_price(netuid0)); + + let subnet_emissions = BTreeMap::from([(netuid0, tao_emission)]); + + // The injection cap is root_proportion * alpha_emission. Seed root stake so + // the cap is large enough that raw alpha_in stays under it (no excess). + set_full_injection_root_stake(); + let root_prop: U96F32 = SubtensorModule::root_proportion(netuid0); + let injection_cap: U96F32 = root_prop.saturating_mul(alpha_emission); + + let (tao_in, alpha_in, alpha_out, excess_tao) = + SubtensorModule::compute_subnet_emission_terms(&subnet_emissions); + + // Check our condition is met: raw alpha_in stays under the cap. + assert!(tao_emission / price <= injection_cap); + + // alpha_out should be the alpha_emission, always + assert_abs_diff_eq!( + alpha_out[&netuid0].to_num::(), + alpha_emission.to_num::(), + epsilon = 0.1 + ); + + // assuming alpha_in < alpha_emission + // Then alpha_in should be tao_emission / price + assert_abs_diff_eq!( + alpha_in[&netuid0].to_num::(), + tao_emission.to_num::() / price.to_num::(), + epsilon = 0.01 + ); + + // tao_in should be the tao_emission + assert_abs_diff_eq!( + tao_in[&netuid0].to_num::(), + tao_emission.to_num::(), + epsilon = 0.01 + ); + + // excess_tao should be 0 + assert_abs_diff_eq!( + excess_tao[&netuid0].to_num::(), + tao_emission.to_num::() - tao_in[&netuid0].to_num::(), + epsilon = 0.01 + ); + }); +} + +#[test] +fn test_get_subnet_terms_alpha_emissions_cap() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(10); + let owner_coldkey = U256::from(11); + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + // The injection cap is now root_proportion * alpha_emission. Seed root stake + // so root_proportion is well-defined, and derive the cap from the live values. + set_full_injection_root_stake(); + let alpha_emission_i: U96F32 = U96F32::saturating_from_num( + SubtensorModule::get_block_emission_for_issuance( + SubtensorModule::get_alpha_issuance(netuid).into(), + ) + .unwrap_or(0), + ); + let injection_cap: U96F32 = + SubtensorModule::root_proportion(netuid).saturating_mul(alpha_emission_i); + + // price = 1.0, alpha_in_i (== emissions1) <= alpha_injection_cap (not capped) + let emissions1 = U96F32::from_num(100_000_000); + assert!(emissions1 < injection_cap); + + let subnet_emissions1 = BTreeMap::from([(netuid, emissions1)]); + let (_, alpha_in, _, _) = SubtensorModule::compute_subnet_emission_terms(&subnet_emissions1); + + assert_eq!(alpha_in.get(&netuid).copied().unwrap(), emissions1); + + // price = 1.0, alpha_in_i (== emissions2) > alpha_injection_cap (capped) + let emissions2 = U96F32::from_num(10_000_000_000u64); + assert!(emissions2 > injection_cap); + + let subnet_emissions2 = BTreeMap::from([(netuid, emissions2)]); + let (_, alpha_in, _, _) = SubtensorModule::compute_subnet_emission_terms(&subnet_emissions2); + + assert_eq!(alpha_in.get(&netuid).copied().unwrap(), injection_cap); + }); +} diff --git a/pallets/subtensor/src/tests/coinbase/tao_issuance.rs b/pallets/subtensor/src/tests/coinbase/tao_issuance.rs new file mode 100644 index 0000000000..f9ad194945 --- /dev/null +++ b/pallets/subtensor/src/tests/coinbase/tao_issuance.rs @@ -0,0 +1,395 @@ +#![allow( + unused, + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used +)] +//! TAO issuance and subnet emission-enable redistribution. + +use super::helpers::*; +use super::prelude::*; + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::tao_issuance::test_hotkey_take --exact --show-output --nocapture +#[test] +fn test_hotkey_take() { + new_test_ext(1).execute_with(|| { + let hotkey = U256::from(1); + Delegates::::insert(hotkey, PerU16::from_parts(u16::MAX / 2)); + log::info!( + "expected: {:?}", + SubtensorModule::get_hotkey_take_float(&hotkey) + ); + log::info!( + "expected: {:?}", + SubtensorModule::get_hotkey_take_float(&hotkey) + ); + }); +} + +// Test the base case of running coinbase with zero emission. +// This test verifies that the coinbase mechanism can handle the edge case +// of zero emission without errors or unexpected behavior. +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::tao_issuance::test_coinbase_basecase --exact --show-output --nocapture +#[test] +fn test_coinbase_basecase() { + new_test_ext(1).execute_with(|| { + let zero_emission = SubtensorModule::mint_tao(0.into()); + SubtensorModule::run_coinbase(zero_emission); + }); +} + +// Test the emission distribution for a single subnet. +// This test verifies that: +// - Single subnet gets cutoff by lower flow limit, so nothing is distributed +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::tao_issuance::test_coinbase_tao_issuance_base --exact --show-output --nocapture +#[test] +fn test_coinbase_tao_issuance_base() { + new_test_ext(1).execute_with(|| { + let emission = TaoBalance::from(1_234_567); + let subnet_owner_ck = U256::from(1001); + let subnet_owner_hk = U256::from(1002); + let netuid = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); + // Dynamic subnets register with emission disabled by default. + SubnetEmissionEnabled::::insert(netuid, true); + // Price-based emission shares require a non-zero moving price. + SubnetMovingPrice::::insert(netuid, I96F32::from_num(1)); + // Keep root_proportion ~1 so the injection cap does not bind. + set_full_injection_root_stake(); + let total_issuance_before = TotalIssuance::::get(); + let tao_in_before = SubnetTAO::::get(netuid); + let total_stake_before = TotalStake::::get(); + let emission_credit = SubtensorModule::mint_tao(emission); + SubtensorModule::run_coinbase(emission_credit); + assert_eq!(SubnetTAO::::get(netuid), tao_in_before + emission); + assert_eq!( + TotalIssuance::::get(), + total_issuance_before + emission + ); + assert_eq!(TotalStake::::get(), total_stake_before + emission); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::tao_issuance::test_coinbase_tao_issuance_base_low --exact --show-output --nocapture +#[test] +fn test_coinbase_tao_issuance_base_low() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let emission = TaoBalance::from(1); + let emission_credit = SubtensorModule::mint_tao(emission); + add_network(netuid, 1, 0); + assert_eq!(SubnetTAO::::get(netuid), TaoBalance::ZERO); + // Set subnet flow to non-zero + SubnetTaoFlow::::insert(netuid, 33433_i64); + SubtensorModule::run_coinbase(emission_credit); + assert_eq!(SubnetTAO::::get(netuid), emission); + assert_eq!(TotalIssuance::::get(), emission); + assert_eq!(TotalStake::::get(), emission); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::tao_issuance::test_coinbase_tao_issuance_base_low_flow --exact --show-output --nocapture +// #[test] +// fn test_coinbase_tao_issuance_base_low_flow() { +// new_test_ext(1).execute_with(|| { +// let emission = TaoBalance::from(1_234_567); +// let subnet_owner_ck = U256::from(1001); +// let subnet_owner_hk = U256::from(1002); +// let netuid = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); +// let emission = TaoBalance::from(1); + +// // 100% tao flow method +// let block_num = FlowHalfLife::::get(); +// SubnetEmaTaoFlow::::insert(netuid, (block_num, I64F64::from_num(1_000_000_000))); +// System::set_block_number(block_num); + +// let tao_in_before = SubnetTAO::::get(netuid); +// let total_stake_before = TotalStake::::get(); +// SubtensorModule::run_coinbase(U96F32::from_num(emission)); +// assert_eq!(SubnetTAO::::get(netuid), tao_in_before + emission); +// assert_eq!(TotalIssuance::::get(), emission); +// assert_eq!(TotalStake::::get(), total_stake_before + emission); +// }); +// } + +// Test emission distribution across multiple subnets. +// This test verifies that: +// - Multiple subnets receive equal portions of the total emission +// - Each subnet's TAO balance is updated correctly +// - Total issuance and total stake reflect the full emission amount +// - The emission is split evenly between all subnets +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::tao_issuance::test_coinbase_tao_issuance_multiple --exact --show-output --nocapture +#[test] +fn test_coinbase_tao_issuance_multiple() { + new_test_ext(1).execute_with(|| { + let netuid1 = NetUid::from(1); + let netuid2 = NetUid::from(2); + let netuid3 = NetUid::from(3); + let emission = TaoBalance::from(3_333_333); + let emission_credit = SubtensorModule::mint_tao(emission); + add_network(netuid1, 1, 0); + add_network(netuid2, 1, 0); + add_network(netuid3, 1, 0); + assert_eq!(SubnetTAO::::get(netuid1), TaoBalance::ZERO); + assert_eq!(SubnetTAO::::get(netuid2), TaoBalance::ZERO); + assert_eq!(SubnetTAO::::get(netuid3), TaoBalance::ZERO); + // Set Tao flows to equal and non-zero + SubnetTaoFlow::::insert(netuid1, 100_000_000_i64); + SubnetTaoFlow::::insert(netuid2, 100_000_000_i64); + SubnetTaoFlow::::insert(netuid3, 100_000_000_i64); + SubtensorModule::run_coinbase(emission_credit); + assert_abs_diff_eq!( + SubnetTAO::::get(netuid1), + emission / 3.into(), + epsilon = 1.into(), + ); + assert_abs_diff_eq!( + SubnetTAO::::get(netuid2), + emission / 3.into(), + epsilon = 1.into(), + ); + assert_abs_diff_eq!( + SubnetTAO::::get(netuid3), + emission / 3.into(), + epsilon = 1.into(), + ); + assert_abs_diff_eq!(TotalIssuance::::get(), emission, epsilon = 3.into(),); + assert_abs_diff_eq!(TotalStake::::get(), emission, epsilon = 3.into(),); + }); +} + +#[test] +fn test_coinbase_disabled_subnet_emission_redistributes_tao_to_enabled_subnets() { + new_test_ext(1).execute_with(|| { + let netuid1 = NetUid::from(1); + let netuid2 = NetUid::from(2); + let netuid3 = NetUid::from(3); + let emission = TaoBalance::from(3_333_333); + + add_network(netuid1, 1, 0); + add_network(netuid2, 1, 0); + add_network(netuid3, 1, 0); + + SubnetEmissionEnabled::::insert(netuid2, false); + + SubnetTaoFlow::::insert(netuid1, 100_000_000_i64); + SubnetTaoFlow::::insert(netuid2, 100_000_000_i64); + SubnetTaoFlow::::insert(netuid3, 100_000_000_i64); + + let subnet_emissions = SubtensorModule::get_subnet_block_emissions( + &[netuid1, netuid2, netuid3], + U96F32::saturating_from_num(emission.to_u64()), + ); + + assert_abs_diff_eq!( + subnet_emissions[&netuid1].to_num::(), + (emission.to_u64() / 2) as f64, + epsilon = 2.0, + ); + assert_abs_diff_eq!( + subnet_emissions[&netuid2].to_num::(), + 0.0, + epsilon = 1.0 + ); + assert_abs_diff_eq!( + subnet_emissions[&netuid3].to_num::(), + (emission.to_u64() / 2) as f64, + epsilon = 2.0, + ); + + let (_tao_in, alpha_in, alpha_out, excess_tao) = + SubtensorModule::compute_subnet_emission_terms(&subnet_emissions); + assert_eq!(alpha_in[&netuid2], U96F32::from_num(0.0)); + assert_eq!(excess_tao[&netuid2], U96F32::from_num(0.0)); + assert!(alpha_out[&netuid2] > U96F32::from_num(0.0)); + + let total_issuance_before = TotalIssuance::::get(); + let total_stake_before = TotalStake::::get(); + let emission_credit = SubtensorModule::mint_tao(emission); + SubtensorModule::run_coinbase(emission_credit); + + assert_abs_diff_eq!( + SubnetTAO::::get(netuid1), + emission / 2.into(), + epsilon = 2.into(), + ); + assert_eq!(SubnetTAO::::get(netuid2), TaoBalance::ZERO); + assert_abs_diff_eq!( + SubnetTAO::::get(netuid3), + emission / 2.into(), + epsilon = 2.into(), + ); + assert_abs_diff_eq!( + TotalIssuance::::get(), + total_issuance_before + emission, + epsilon = 2.into(), + ); + assert_abs_diff_eq!( + TotalStake::::get(), + total_stake_before + emission, + epsilon = 2.into(), + ); + }); +} + +#[test] +fn test_sudo_set_subnet_emission_enabled_multiple_subnets_multiple_toggles() { + new_test_ext(1).execute_with(|| { + let netuid1 = NetUid::from(1); + let netuid2 = NetUid::from(2); + let netuid3 = NetUid::from(3); + let emission = TaoBalance::from(3_000_000); + + add_network(netuid1, 1, 0); + add_network(netuid2, 1, 0); + add_network(netuid3, 1, 0); + + // Keep root_proportion ~1 so TAO-side emission is injected (populating + // SubnetTaoInEmission) rather than routed entirely to chain buys. + set_full_injection_root_stake(); + + let assert_emission_storage = |expected1: u64, expected2: u64, expected3: u64| { + assert_abs_diff_eq!( + SubnetTaoInEmission::::get(netuid1), + TaoBalance::from(expected1), + epsilon = 2.into(), + ); + assert_abs_diff_eq!( + SubnetTaoInEmission::::get(netuid2), + TaoBalance::from(expected2), + epsilon = 2.into(), + ); + assert_abs_diff_eq!( + SubnetTaoInEmission::::get(netuid3), + TaoBalance::from(expected3), + epsilon = 2.into(), + ); + + assert_eq!( + SubnetAlphaInEmission::::get(netuid1) == AlphaBalance::from(0), + expected1 == 0 + ); + assert_eq!( + SubnetAlphaInEmission::::get(netuid2) == AlphaBalance::from(0), + expected2 == 0 + ); + assert_eq!( + SubnetAlphaInEmission::::get(netuid3) == AlphaBalance::from(0), + expected3 == 0 + ); + + assert!(SubnetAlphaOutEmission::::get(netuid1) > AlphaBalance::from(0)); + assert!(SubnetAlphaOutEmission::::get(netuid2) > AlphaBalance::from(0)); + assert!(SubnetAlphaOutEmission::::get(netuid3) > AlphaBalance::from(0)); + }; + + let run_coinbase = || { + let emission_credit = SubtensorModule::mint_tao(emission); + SubtensorModule::run_coinbase(emission_credit); + }; + + // All enabled: split TAO-side emission equally across all three subnets. + run_coinbase(); + assert_emission_storage(1_000_000, 1_000_000, 1_000_000); + + // Seed stale values and then disable netuid2. The next coinbase run must clear + // netuid2's per-block TAO-side emission storage while preserving alpha_out. + SubnetTaoInEmission::::insert(netuid2, TaoBalance::from(123)); + SubnetAlphaInEmission::::insert(netuid2, AlphaBalance::from(123)); + SubnetExcessTao::::insert(netuid2, TaoBalance::from(123)); + SubnetEmissionEnabled::::insert(netuid2, false); + run_coinbase(); + assert_emission_storage(1_500_000, 0, 1_500_000); + assert_eq!(SubnetExcessTao::::get(netuid2), TaoBalance::from(0)); + + // Toggle a different subnet off and netuid2 back on. + SubnetTaoInEmission::::insert(netuid1, TaoBalance::from(456)); + SubnetAlphaInEmission::::insert(netuid1, AlphaBalance::from(456)); + SubnetExcessTao::::insert(netuid1, TaoBalance::from(456)); + SubnetEmissionEnabled::::insert(netuid1, false); + SubnetEmissionEnabled::::insert(netuid2, true); + run_coinbase(); + assert_emission_storage(0, 1_500_000, 1_500_000); + assert_eq!(SubnetExcessTao::::get(netuid1), TaoBalance::from(0)); + + // Toggle everything back on: TAO-side emission should return to an even split. + SubnetEmissionEnabled::::insert(netuid1, true); + SubnetEmissionEnabled::::insert(netuid2, true); + SubnetEmissionEnabled::::insert(netuid3, true); + run_coinbase(); + assert_emission_storage(1_000_000, 1_000_000, 1_000_000); + }); +} + +// Test emission distribution with different subnet prices. +// This test verifies that: +// - Subnets with different prices receive proportional emission shares +// - A subnet with double the price receives double the emission +// - Total issuance and total stake reflect the full emission amount +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::tao_issuance::test_coinbase_tao_issuance_different_prices --exact --show-output --nocapture +#[test] +fn test_coinbase_tao_issuance_different_prices() { + new_test_ext(1).execute_with(|| { + let netuid1 = NetUid::from(1); + let netuid2 = NetUid::from(2); + let emission = 100_000_000; + let emission_credit = SubtensorModule::mint_tao(emission.into()); + add_network(netuid1, 1, 0); + add_network(netuid2, 1, 0); + + // Setup prices 0.1 and 0.2 + let initial_tao: u64 = 100_000_u64; + let initial_alpha1: u64 = initial_tao * 10; + let initial_alpha2: u64 = initial_tao * 5; + mock::setup_reserves(netuid1, initial_tao.into(), initial_alpha1.into()); + mock::setup_reserves(netuid2, initial_tao.into(), initial_alpha2.into()); + + // Force the swap to initialize + ::SwapInterface::init_swap(netuid1, None); + ::SwapInterface::init_swap(netuid2, None); + + // Make subnets dynamic. + SubnetMechanism::::insert(netuid1, 1); + SubnetMechanism::::insert(netuid2, 1); + + // Price-based shares: subnet 2 has twice the moving price of subnet 1, + // so it should receive twice the TAO emission. + SubnetMovingPrice::::insert(netuid1, I96F32::from_num(0.1)); + SubnetMovingPrice::::insert(netuid2, I96F32::from_num(0.2)); + // Keep root_proportion ~1 so the injection cap does not bind. + set_full_injection_root_stake(); + + // Assert initial TAO reserves. + assert_eq!(SubnetTAO::::get(netuid1), initial_tao.into()); + assert_eq!(SubnetTAO::::get(netuid2), initial_tao.into()); + + // Run the coinbase with the emission amount. + SubtensorModule::run_coinbase(emission_credit); + + // Assert tao emission is split evenly. + assert_abs_diff_eq!( + SubnetTAO::::get(netuid1), + TaoBalance::from(initial_tao + emission / 3), + epsilon = 10.into(), + ); + assert_abs_diff_eq!( + SubnetTAO::::get(netuid2), + TaoBalance::from(initial_tao + 2 * emission / 3), + epsilon = 10.into(), + ); + + // Prices are low => we limit tao issued (buy alpha with it) + let tao_issued = TaoBalance::from(((1.0) * emission as f64) as u64); + assert_abs_diff_eq!( + TotalIssuance::::get(), + tao_issued, + epsilon = 10.into() + ); + assert_abs_diff_eq!( + TotalStake::::get(), + emission.into(), + epsilon = 10.into() + ); + }); +} diff --git a/pallets/subtensor/src/tests/coldkey_lineage.rs b/pallets/subtensor/src/tests/coldkey_lineage.rs index 1c5742a510..a5a0ab19ed 100644 --- a/pallets/subtensor/src/tests/coldkey_lineage.rs +++ b/pallets/subtensor/src/tests/coldkey_lineage.rs @@ -1,3 +1,7 @@ +//! Tests for coldkey-swap lineage recording ([`crate::swap::coldkey_lineage`]). +//! +//! Verifies tip/chain updates, reverse-swap non-cycles, and rollback on failed swap. + #![allow(clippy::unwrap_used)] use frame_support::{assert_noop, assert_ok}; @@ -33,7 +37,7 @@ fn test_coldkey_swap_records_lineage() { ) .unwrap(); - assert_ok!(SubtensorModule::do_swap_coldkey(&c0, &c1)); + assert_ok!(SubtensorModule::perform_coldkey_swap(&c0, &c1)); assert_eq!(ColdkeySuccessor::::get(c0), Some(c1)); assert_eq!(SubtensorModule::coldkey_root(&c1), c0); @@ -67,8 +71,8 @@ fn test_coldkey_swap_lineage_chain_and_tip() { ) .unwrap(); - assert_ok!(SubtensorModule::do_swap_coldkey(&c0, &c1)); - assert_ok!(SubtensorModule::do_swap_coldkey(&c1, &c2)); + assert_ok!(SubtensorModule::perform_coldkey_swap(&c0, &c1)); + assert_ok!(SubtensorModule::perform_coldkey_swap(&c1, &c2)); assert_eq!(ColdkeySuccessor::::get(c0), Some(c1)); assert_eq!(ColdkeySuccessor::::get(c1), Some(c2)); @@ -102,12 +106,12 @@ fn test_coldkey_lineage_reverse_swap_does_not_cycle() { ) .unwrap(); - assert_ok!(SubtensorModule::do_swap_coldkey(&c0, &c1)); + assert_ok!(SubtensorModule::perform_coldkey_swap(&c0, &c1)); assert_eq!(ColdkeySuccessor::::get(c0), Some(c1)); // c0 was killed; fund it again as a fresh destination for the reverse swap. add_balance_to_coldkey_account(&c0, ExistentialDeposit::get()); - assert_ok!(SubtensorModule::do_swap_coldkey(&c1, &c0)); + assert_ok!(SubtensorModule::perform_coldkey_swap(&c1, &c0)); assert!(ColdkeySuccessor::::get(c0).is_none()); assert_eq!(ColdkeySuccessor::::get(c1), Some(c0)); @@ -154,7 +158,7 @@ fn test_coldkey_lineage_rolls_back_with_failed_swap() { ); assert_noop!( - SubtensorModule::do_swap_coldkey(&c0, &c1), + SubtensorModule::perform_coldkey_swap(&c0, &c1), Error::::ActiveLockExists ); assert!(ColdkeySuccessor::::get(c0).is_none()); diff --git a/pallets/subtensor/src/tests/consensus.rs b/pallets/subtensor/src/tests/consensus.rs index 495633d131..305ce10f13 100644 --- a/pallets/subtensor/src/tests/consensus.rs +++ b/pallets/subtensor/src/tests/consensus.rs @@ -1,3 +1,7 @@ +//! Synthetic consensus / map-consensus stress tests for Yuma-style epochs. +//! +//! Builds large random weight graphs and checks stake-weighted consensus invariants. + #![allow( clippy::arithmetic_side_effects, clippy::expect_used, diff --git a/pallets/subtensor/src/tests/delegate_info.rs b/pallets/subtensor/src/tests/delegate_info.rs index 0ca61c62ff..337700aee3 100644 --- a/pallets/subtensor/src/tests/delegate_info.rs +++ b/pallets/subtensor/src/tests/delegate_info.rs @@ -1,4 +1,9 @@ +//! Tests for RPC delegate-info helpers ([`crate::rpc_info::delegate_info`]). +//! +//! Covers `return_per_1000_tao` and `get_delegated` stake/take aggregation. + #![allow(clippy::expect_used)] + use super::mock::*; use codec::Compact; @@ -22,7 +27,7 @@ fn test_return_per_1000_tao() { let emissions_per_day = U64F64::from_num(1000.0 * 1e9); let return_per_1000 = - SubtensorModule::return_per_1000_tao_test(take, total_stake, emissions_per_day); + SubtensorModule::delegator_return_per_1000_tao_test(take, total_stake, emissions_per_day); // We expect 82 TAO per day with 10% of total_stake let expected_return_per_1000 = U64F64::from_num(82.0); diff --git a/pallets/subtensor/src/tests/destroy_alpha_tests.rs b/pallets/subtensor/src/tests/destroy_alpha_tests.rs index bc4211c79e..e07eda7085 100644 --- a/pallets/subtensor/src/tests/destroy_alpha_tests.rs +++ b/pallets/subtensor/src/tests/destroy_alpha_tests.rs @@ -1,3 +1,8 @@ +//! Tests for dissolve-path destroy of alpha in/out stakes. +//! +//! Production path: [`crate::staking::remove_stake::destroy_alpha`]. +//! Covers settle, clean-alpha resume under weight limits, and multi-block issuance. + #![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] use super::mock::*; @@ -7,7 +12,7 @@ use sp_core::U256; use subtensor_runtime_common::TaoBalance; use subtensor_swap_interface::SwapHandler; -fn setup_staked_subnet() -> (U256, U256, NetUid) { +fn setup_destroy_alpha_staked_subnet() -> (U256, U256, NetUid) { let owner_cold = U256::from(1001); let owner_hot = U256::from(1002); let netuid = add_dynamic_network(&owner_hot, &owner_cold); @@ -39,7 +44,7 @@ fn setup_staked_subnet() -> (U256, U256, NetUid) { #[test] fn test_destroy_alpha_in_out_stakes_get_total_alpha_value() { new_test_ext(0).execute_with(|| { - let (_, _, netuid) = setup_staked_subnet(); + let (_, _, netuid) = setup_destroy_alpha_staked_subnet(); let w = Weight::from_parts(u64::MAX, u64::MAX); let mut weight_meter = WeightMeter::with_limit(w); assert!( @@ -79,7 +84,7 @@ fn test_destroy_alpha_in_out_stakes_get_total_alpha_value() { #[test] fn test_destroy_alpha_in_out_stakes_settle_stakes() { new_test_ext(0).execute_with(|| { - let (_, _, netuid) = setup_staked_subnet(); + let (_, _, netuid) = setup_destroy_alpha_staked_subnet(); run_destroy_alpha_get_total_and_settle(netuid); }); } @@ -87,7 +92,7 @@ fn test_destroy_alpha_in_out_stakes_settle_stakes() { #[test] fn test_destroy_alpha_in_out_stakes_clean_alpha() { new_test_ext(0).execute_with(|| { - let (_, owner_hot, netuid) = setup_staked_subnet(); + let (_, owner_hot, netuid) = setup_destroy_alpha_staked_subnet(); let w = Weight::from_parts(u64::MAX, u64::MAX); let mut weight_meter = WeightMeter::with_limit(w); let mut status = dissolve_cleanup_status(netuid); @@ -141,7 +146,7 @@ fn test_destroy_alpha_in_out_stakes_clean_alpha() { #[test] fn test_destroy_alpha_in_out_stakes_clear_hotkey_totals() { new_test_ext(0).execute_with(|| { - let (_, owner_hot, netuid) = setup_staked_subnet(); + let (_, owner_hot, netuid) = setup_destroy_alpha_staked_subnet(); let w = Weight::from_parts(u64::MAX, u64::MAX); let mut weight_meter = WeightMeter::with_limit(w); let mut status = dissolve_cleanup_status(netuid); @@ -195,7 +200,7 @@ fn test_destroy_alpha_in_out_stakes_clear_hotkey_totals() { #[test] fn test_destroy_alpha_in_out_stakes_clear_locks() { new_test_ext(0).execute_with(|| { - let (owner_cold, owner_hot, netuid) = setup_staked_subnet(); + let (owner_cold, owner_hot, netuid) = setup_destroy_alpha_staked_subnet(); let w = Weight::from_parts(u64::MAX, u64::MAX); let mut weight_meter = WeightMeter::with_limit(w); let mut status = dissolve_cleanup_status(netuid); @@ -265,7 +270,7 @@ fn test_destroy_alpha_in_out_stakes_clear_locks() { #[test] fn test_destroy_alpha_in_out_stakes() { new_test_ext(0).execute_with(|| { - let (_, _, netuid) = setup_staked_subnet(); + let (_, _, netuid) = setup_destroy_alpha_staked_subnet(); let mut status = run_destroy_alpha_get_total_and_settle(netuid); let w = Weight::from_parts(u64::MAX, u64::MAX); let mut weight_meter = WeightMeter::with_limit(w); @@ -279,7 +284,7 @@ fn test_destroy_alpha_in_out_stakes() { #[test] fn test_destroy_alpha_clean_alpha_resumes_with_limited_weight() { new_test_ext(0).execute_with(|| { - let (_, _, netuid) = setup_staked_subnet(); + let (_, _, netuid) = setup_destroy_alpha_staked_subnet(); let w = Weight::from_parts(u64::MAX, u64::MAX); let mut weight_meter = WeightMeter::with_limit(w); let mut status = dissolve_cleanup_status(netuid); diff --git a/pallets/subtensor/src/tests/dissolution.rs b/pallets/subtensor/src/tests/dissolution.rs index ec336db93d..d709f7d199 100644 --- a/pallets/subtensor/src/tests/dissolution.rs +++ b/pallets/subtensor/src/tests/dissolution.rs @@ -1,3 +1,8 @@ +//! End-to-end subnet dissolution / cleanup netuid reuse tests. +//! +//! Production path: [`crate::subnets::dissolution`]. +//! Guards against reusing a netuid while cleanup is still in progress. + #![allow( clippy::unwrap_used, clippy::indexing_slicing, @@ -121,7 +126,7 @@ fn in_progress_cleanup_netuid_must_not_be_reused() { let _n1 = add_dynamic_network(&U256::from(101), &U256::from(1)); let n2 = add_dynamic_network(&U256::from(102), &U256::from(2)); let _n3 = add_dynamic_network(&U256::from(103), &U256::from(3)); - assert!(SubtensorModule::if_subnet_exist(n2)); + assert!(SubtensorModule::subnet_exists(n2)); // Governance dissolves the middle subnet -> queued for cleanup. assert_ok!(SubtensorModule::do_dissolve_network(n2)); @@ -169,7 +174,7 @@ fn e2e_registration_reuses_in_progress_cleanup_netuid() { CurrentDissolveCleanupStatus::::set(Some( crate::subnets::dissolution::DissolveCleanupStatus::new(n2), )); - assert!(!SubtensorModule::if_subnet_exist(n2)); + assert!(!SubtensorModule::subnet_exists(n2)); // Fresh coldkey/hotkey -> passes the per-coldkey registration rate limit. let new_cold = U256::from(909); @@ -187,7 +192,7 @@ fn e2e_registration_reuses_in_progress_cleanup_netuid() { // The collision happened: n2 is live again... assert!( - !SubtensorModule::if_subnet_exist(n2), + !SubtensorModule::subnet_exists(n2), "registration did not reuse n2 (good - bug may be fixed)" ); // ...while its cleanup is still pending and will keep deleting the new subnet's storage. diff --git a/pallets/subtensor/src/tests/emission.rs b/pallets/subtensor/src/tests/emission.rs index 151fd3cddb..e6b041b6cc 100644 --- a/pallets/subtensor/src/tests/emission.rs +++ b/pallets/subtensor/src/tests/emission.rs @@ -1,3 +1,7 @@ +//! Tests for [`crate::Pallet::blocks_until_next_auto_epoch`]. +//! +//! Covers tempo=0, wrap-around, boundaries, and multi-netuid alignment. + use subtensor_runtime_common::NetUid; use super::mock::*; diff --git a/pallets/subtensor/src/tests/ensure.rs b/pallets/subtensor/src/tests/ensure.rs index 008be48b15..276e1c612a 100644 --- a/pallets/subtensor/src/tests/ensure.rs +++ b/pallets/subtensor/src/tests/ensure.rs @@ -1,4 +1,9 @@ +//! Tests for subnet-owner / root / admin-window origin guards. +//! +//! Production path: [`crate::utils::misc::origin_and_admin`]. + #![allow(clippy::expect_used)] + use frame_support::{assert_noop, assert_ok}; use frame_system::Config; use sp_core::U256; @@ -104,7 +109,7 @@ fn ensure_owner_or_root_with_limits_checks_rl_and_freeze() { assert_eq!(OwnerHyperparamRateLimit::::get(), 2); // Outside freeze window initially; should pass and return Some(owner) - let res = crate::Pallet::::ensure_sn_owner_or_root_with_limits( + let res = crate::Pallet::::ensure_subnet_owner_or_root_with_limits( <::RuntimeOrigin>::signed(owner), netuid, &[Hyperparameter::Kappa.into()], @@ -118,7 +123,7 @@ fn ensure_owner_or_root_with_limits_checks_rl_and_freeze() { TransactionType::from(Hyperparameter::Kappa) .set_last_block_on_subnet::(&owner, netuid, now); assert_noop!( - crate::Pallet::::ensure_sn_owner_or_root_with_limits( + crate::Pallet::::ensure_subnet_owner_or_root_with_limits( <::RuntimeOrigin>::signed(owner), netuid, &[Hyperparameter::Kappa.into()], @@ -130,7 +135,7 @@ fn ensure_owner_or_root_with_limits_checks_rl_and_freeze() { run_to_block(now + 3); TransactionType::from(Hyperparameter::Kappa) .set_last_block_on_subnet::(&owner, netuid, 0); - assert_ok!(crate::Pallet::::ensure_sn_owner_or_root_with_limits( + assert_ok!(crate::Pallet::::ensure_subnet_owner_or_root_with_limits( <::RuntimeOrigin>::signed(owner), netuid, &[Hyperparameter::Kappa.into()] @@ -152,7 +157,7 @@ fn ensure_owner_or_root_with_limits_checks_rl_and_freeze() { } run_to_block(cur + 1); } - assert_ok!(crate::Pallet::::ensure_sn_owner_or_root_with_limits( + assert_ok!(crate::Pallet::::ensure_subnet_owner_or_root_with_limits( <::RuntimeOrigin>::signed(owner), netuid, &[Hyperparameter::Kappa.into()] diff --git a/pallets/subtensor/src/tests/epoch.rs b/pallets/subtensor/src/tests/epoch.rs deleted file mode 100644 index b0383521a8..0000000000 --- a/pallets/subtensor/src/tests/epoch.rs +++ /dev/null @@ -1,3973 +0,0 @@ -#![allow( - clippy::arithmetic_side_effects, - clippy::expect_used, - clippy::indexing_slicing, - clippy::unwrap_used -)] - -use std::time::Instant; - -use approx::assert_abs_diff_eq; -use frame_support::{assert_err, assert_ok}; -use rand::{RngExt, SeedableRng, distr::Uniform, rngs::StdRng, seq::SliceRandom}; -use sp_core::{Get, U256}; -use substrate_fixed::types::I32F32; -use subtensor_runtime_common::{AlphaBalance, NetUidStorageIndex, TaoBalance}; -use subtensor_swap_interface::SwapHandler; - -use super::mock::*; -use crate::epoch::math::{fixed, u16_proportion_to_fixed}; -use crate::tests::math::{assert_mat_compare, vec_to_fixed, vec_to_mat_fixed}; -use crate::*; - -// Normalizes (sum to 1 except 0) the input vector directly in-place. -#[allow(dead_code)] -pub fn inplace_normalize(x: &mut [I32F32]) { - let x_sum: I32F32 = x.iter().sum(); - if x_sum == I32F32::from_num(0.0_f32) { - return; - } - for i in x.iter_mut() { - *i /= x_sum; - } -} - -// Inplace normalize the passed positive integer weights so that they sum to u16 max value. -#[allow(dead_code)] -fn normalize_weights(mut weights: Vec) -> Vec { - let sum: u64 = weights.iter().map(|x| *x as u64).sum(); - if sum == 0 { - return weights; - } - weights.iter_mut().for_each(|x| { - *x = (*x as u64 * u16::MAX as u64 / sum) as u16; - }); - weights -} - -// // Return as usize an I32F32 ratio of a usize input, avoiding the 0% and 100% extremes. -// fn non_extreme_fixed_ratio(ratio: I32F32, total: usize) -> usize { -// if total == 0 { -// return total; -// } -// let mut subset: usize = (ratio * I32F32::from_num(total)).to_num::(); -// if subset == 0 { -// subset = 1; -// } else if subset == total { -// subset = total - 1; -// } -// return subset; -// } - -// // Box-Muller Transform converting two uniform random samples to a normal random sample. -// fn normal(size: usize, rng: &mut StdRng, dist: &Uniform) -> Vec { -// let max: I32F32 = I32F32::from_num(u16::MAX); -// let two: I32F32 = I32F32::from_num(2); -// let eps: I32F32 = I32F32::from_num(0.000001); -// let pi: I32F32 = I32F32::from_num(PI); - -// let uniform_u16: Vec = (0..(2 * size)).map(|_| rng.sample(&dist)).collect(); -// let uniform: Vec = uniform_u16 -// .iter() -// .map(|&x| I32F32::from_num(x) / max) -// .collect(); -// let mut normal: Vec = vec![I32F32::from_num(0); size as usize]; - -// for i in 0..size { -// let u1: I32F32 = uniform[i] + eps; -// let u2: I32F32 = uniform[i + size] + eps; -// normal[i] = sqrt::(-two * ln::(u1).expect("")).expect("") -// * cos(two * pi * u2); -// } -// normal -// } - -// Returns validators and servers uids with either blockwise, regular, or random interleaving. -fn distribute_nodes( - validators_n: usize, - network_n: usize, - interleave: usize, -) -> (Vec, Vec) { - let mut validators: Vec = vec![]; - let mut servers: Vec = vec![]; - - if interleave == 0 { - // blockwise [validator_block, server_block] - validators = (0..validators_n as u16).collect(); - servers = (validators_n as u16..network_n as u16).collect(); - } else if interleave == 1 { - // regular interleaving [val, srv, srv, ..., srv, val, srv, srv, ..., srv, val, srv, ..., srv] - (validators, servers) = (0..network_n as u16) - .collect::>() - .iter() - .partition(|&i| *i as usize % (network_n / validators_n) == 0); - } else if interleave == 2 { - // random interleaving - let mut permuted_uids: Vec = (0..network_n as u16).collect(); - permuted_uids.shuffle(&mut rand::rng()); - validators = permuted_uids[0..validators_n].into(); - servers = permuted_uids[validators_n..network_n].into(); - } - - (validators, servers) -} - -#[allow(dead_code)] -fn uid_stats(netuid: NetUid, uid: u16) { - log::info!( - "stake: {:?}", - SubtensorModule::get_total_stake_for_hotkey(&(U256::from(uid))) - ); - log::info!("rank: {:?}", SubtensorModule::get_rank_for_uid(netuid, uid)); - log::info!( - "trust: {:?}", - SubtensorModule::get_trust_for_uid(netuid, uid) - ); - log::info!( - "consensus: {:?}", - SubtensorModule::get_consensus_for_uid(netuid, uid) - ); - log::info!( - "incentive: {:?}", - SubtensorModule::get_incentive_for_uid(NetUidStorageIndex::from(netuid), uid) - ); - log::info!( - "dividend: {:?}", - SubtensorModule::get_dividends_for_uid(netuid, uid) - ); - log::info!( - "emission: {:?}", - SubtensorModule::get_emission_for_uid(netuid, uid) - ); -} - -#[allow(clippy::too_many_arguments)] -fn init_run_epochs( - netuid: NetUid, - n: u16, - validators: &[u16], - servers: &[u16], - epochs: u16, - stake_per_validator: u64, - server_self: bool, - input_stake: &[u64], - use_input_stake: bool, - input_weights: &[Vec<(u16, u16)>], - use_input_weights: bool, - random_weights: bool, - random_seed: u64, - sparse: bool, - bonds_penalty: u16, -) { - // === Create the network - add_network_disable_commit_reveal(netuid, u16::MAX - 1, 0); // set higher tempo to avoid built-in epoch, then manual epoch instead - - // === Set bonds penalty - SubtensorModule::set_bonds_penalty(netuid, bonds_penalty); - - // === Register uids - SubtensorModule::set_max_allowed_uids(netuid, n); - for key in 0..n { - let stake = if use_input_stake { - input_stake[key as usize] - } else if validators.contains(&key) { - stake_per_validator - } else { - // only validators receive stake - 0 - }; - - // let stake: u64 = 1; // alternative test: all nodes receive stake, should be same outcome, except stake - add_balance_to_coldkey_account(&(U256::from(key)), stake.into()); - SubtensorModule::append_neuron(netuid, &(U256::from(key)), 0); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &U256::from(key), - &U256::from(key), - netuid, - stake.into(), - ); - } - assert_eq!(SubtensorModule::get_subnetwork_n(netuid), n); - - // === Issue validator permits - SubtensorModule::set_max_allowed_validators(netuid, validators.len() as u16); - assert_eq!( - SubtensorModule::get_max_allowed_validators(netuid), - validators.len() as u16 - ); - SubtensorModule::epoch(netuid, 1_000_000_000.into()); // run first epoch to set allowed validators - run_to_block(1); // run to next block to ensure weights are set on nodes after their registration block - - // === Set weights - let mut rng = StdRng::seed_from_u64(random_seed); // constant seed so weights over multiple runs are equal - let range = Uniform::new(0, u16::MAX).unwrap(); - let mut weights: Vec = vec![u16::MAX / n; servers.len()]; - for uid in validators { - if random_weights { - weights = (0..servers.len()).map(|_| rng.sample(range)).collect(); - weights = normalize_weights(weights); - // assert_eq!(weights.iter().map(|x| *x as u64).sum::(), u16::MAX as u64); // normalized weight sum not always u16::MAX - } - if use_input_weights { - let sparse_weights = input_weights[*uid as usize].clone(); - weights = sparse_weights.iter().map(|(_, w)| *w).collect(); - let srvs: Vec = sparse_weights.iter().map(|(s, _)| *s).collect(); - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(U256::from(*uid as u64)), - netuid, - srvs, - weights.clone(), - 0 - )); - } else { - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(U256::from(*uid as u64)), - netuid, - servers.to_vec(), - weights.clone(), - 0 - )); - } - } - if server_self { - for uid in servers { - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(U256::from(*uid as u64)), - netuid, - vec![*uid], - vec![u16::MAX], - 0 - )); // server self-weight - } - } - - // === Run the epochs. - log::info!("Start {epochs} epoch(s)"); - let start = Instant::now(); - for _ in 0..epochs { - if sparse { - SubtensorModule::epoch(netuid, 1_000_000_000.into()); - } else { - SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); - } - } - let duration = start.elapsed(); - log::info!("Time elapsed in (sparse={sparse}) epoch() is: {duration:?}"); - - // let bonds = SubtensorModule::get_bonds( netuid ); - // for (uid, node) in vec![ (validators[0], "validator"), (servers[0], "server") ] { - // log::info!("\n{node}" ); - // uid_stats(netuid, uid); - // log::info!("bonds: {:?} (on validator), {:?} (on server)", bonds[uid as usize][0], bonds[uid as usize][servers[0] as usize]); - // } -} - -// // Generate a random graph that is split into a major and minor set, each setting specific weight on itself and the complement on the other. -// fn split_graph( -// major_stake: I32F32, -// major_weight: I32F32, -// minor_weight: I32F32, -// weight_stddev: I32F32, -// validators_n: usize, -// network_n: usize, -// interleave: usize, -// ) -> ( -// Vec, -// Vec, -// Vec, -// Vec, -// Vec, -// Vec, -// Vec, -// Vec>, -// I32F32, -// ) { -// let servers_n: usize = network_n - validators_n; -// let major_servers_n: usize = non_extreme_fixed_ratio(major_stake, servers_n); -// let major_validators_n: usize = non_extreme_fixed_ratio(major_stake, validators_n); - -// let (validators, servers) = distribute_nodes(validators_n, network_n, interleave as usize); -// let major_validators: Vec = (0..major_validators_n).map(|i| validators[i]).collect(); -// let minor_validators: Vec = (major_validators_n..validators_n) -// .map(|i| validators[i]) -// .collect(); -// let major_servers: Vec = (0..major_servers_n).map(|i| servers[i]).collect(); -// let minor_servers: Vec = (major_servers_n..servers_n).map(|i| servers[i]).collect(); - -// let zero: I32F32 = I32F32::from_num(0); -// let one: I32F32 = I32F32::from_num(1); -// let stddev: I32F32 = I32F32::from_num(0.3); -// let total_stake: I64F64 = I64F64::from_num(21_000_000_000_000_000 as u64); -// let mut rng = StdRng::seed_from_u64(0); // constant seed so weights over multiple runs are equal -// let dist = Uniform::new(0, u16::MAX); - -// let mut stake: Vec = vec![0; network_n]; -// let mut stake_fixed: Vec = vec![zero; network_n]; -// for (ratio, vals) in vec![ -// (major_stake, &major_validators), -// (one - major_stake, &minor_validators), -// ] { -// let mut sample = normal(vals.len(), &mut rng, &dist) -// .iter() -// .map(|x: &I32F32| { -// let v: I32F32 = (stddev * x) + one; -// if v < zero { -// zero -// } else { -// v -// } -// }) -// .collect(); -// inplace_normalize(&mut sample); -// for (i, &val) in vals.iter().enumerate() { -// stake[val as usize] = -// (I64F64::from_num(ratio) * I64F64::from_num(sample[i]) * total_stake) -// .to_num::(); -// stake_fixed[val as usize] = -// I32F32::from_num(I64F64::from_num(ratio) * I64F64::from_num(sample[i])); -// } -// } - -// let mut weights: Vec> = vec![vec![]; network_n as usize]; -// let mut weights_fixed: Vec> = vec![vec![zero; network_n]; network_n]; -// for (first, second, vals) in vec![ -// (major_weight, one - major_weight, &major_validators), -// (one - minor_weight, minor_weight, &minor_validators), -// ] { -// for &val in vals { -// for (weight, srvs) in vec![(first, &major_servers), (second, &minor_servers)] { -// let mut sample: Vec = normal(srvs.len(), &mut rng, &dist) -// .iter() -// .map(|x: &I32F32| { -// let v: I32F32 = (weight_stddev * x) + one; -// if v < zero { -// zero -// } else { -// v -// } -// }) -// .collect(); -// inplace_normalize(&mut sample); - -// for (i, &srv) in srvs.iter().enumerate() { -// weights[val as usize].push((srv, fixed_proportion_to_u16(weight * sample[i]))); -// weights_fixed[val as usize][srv as usize] = weight * sample[i]; -// } -// } -// inplace_normalize(&mut weights_fixed[val as usize]); -// } -// } - -// inplace_normalize(&mut stake_fixed); - -// // Calculate stake-weighted mean per server -// let mut weight_mean: Vec = vec![zero; network_n]; -// for val in 0..network_n { -// if stake_fixed[val] > zero { -// for srv in 0..network_n { -// weight_mean[srv] += stake_fixed[val] * weights_fixed[val][srv]; -// } -// } -// } - -// // Calculate stake-weighted absolute standard deviation -// let mut weight_dev: Vec = vec![zero; network_n]; -// for val in 0..network_n { -// if stake_fixed[val] > zero { -// for srv in 0..network_n { -// weight_dev[srv] += -// stake_fixed[val] * (weight_mean[srv] - weights_fixed[val][srv]).abs(); -// } -// } -// } - -// // Calculate rank-weighted mean of weight_dev -// let avg_weight_dev: I32F32 = -// weight_dev.iter().sum::() / weight_mean.iter().sum::(); - -// ( -// validators, -// servers, -// major_validators, -// minor_validators, -// major_servers, -// minor_servers, -// stake, -// weights, -// avg_weight_dev, -// ) -// } - -// Test consensus guarantees with an epoch on a graph with 4096 nodes, of which the first 128 are validators, the graph is split into a major and minor set, each setting specific weight on itself and the complement on the other. Asserts that the major emission ratio >= major stake ratio. -// #[test] -// fn test_consensus_guarantees() { -// let netuid = NetUid::from(0); -// let network_n: u16 = 512; -// let validators_n: u16 = 64; -// let epochs: u16 = 1; -// let interleave = 2; -// log::info!("test_consensus_guarantees ({network_n:?}, {validators_n:?} validators)"); -// for (major_stake, major_weight, minor_weight, weight_stddev, bonds_penalty) in vec![ -// (0.51, 1., 1., 0.001, u16::MAX), -// (0.51, 0.03, 0., 0.001, u16::MAX), -// (0.51, 0.51, 0.49, 0.001, u16::MAX), -// (0.51, 0.51, 1., 0.001, u16::MAX), -// (0.51, 0.61, 0.8, 0.1, u16::MAX), -// (0.6, 0.67, 0.65, 0.2, u16::MAX), -// (0.6, 0.74, 0.77, 0.4, u16::MAX), -// (0.6, 0.76, 0.8, 0.4, u16::MAX), -// (0.6, 0.73, 1., 0.4, u16::MAX), // bonds_penalty = 100% -// (0.6, 0.74, 1., 0.4, 55800), // bonds_penalty = 85% -// (0.6, 0.76, 1., 0.4, 43690), // bonds_penalty = 66% -// (0.6, 0.78, 1., 0.4, 21845), // bonds_penalty = 33% -// (0.6, 0.79, 1., 0.4, 0), // bonds_penalty = 0% -// (0.6, 0.92, 1., 0.4, u16::MAX), -// (0.6, 0.94, 1., 0.4, u16::MAX), -// (0.65, 0.78, 0.85, 0.6, u16::MAX), -// (0.7, 0.81, 0.85, 0.8, u16::MAX), -// (0.7, 0.83, 0.85, 1., u16::MAX), -// ] { -// let ( -// validators, -// servers, -// major_validators, -// minor_validators, -// major_servers, -// minor_servers, -// stake, -// weights, -// _avg_weight_dev, -// ) = split_graph( -// fixed(major_stake), -// fixed(major_weight), -// fixed(minor_weight), -// fixed(weight_stddev), -// validators_n as usize, -// network_n as usize, -// interleave as usize, -// ); - -// new_test_ext(1).execute_with(|| { -// init_run_epochs( -// netuid, -// network_n, -// &validators, -// &servers, -// epochs, -// 1, -// true, -// &stake, -// true, -// &weights, -// true, -// false, -// 0, -// false, -// bonds_penalty -// ); - -// let mut major_emission: I64F64 = I64F64::from_num(0); -// let mut minor_emission: I64F64 = I64F64::from_num(0); -// for set in vec![major_validators, major_servers] { -// for uid in set { -// major_emission += -// I64F64::from_num(SubtensorModule::get_emission_for_uid(netuid, uid)); -// } -// } -// for set in vec![minor_validators, minor_servers] { -// for uid in set { -// minor_emission += -// I64F64::from_num(SubtensorModule::get_emission_for_uid(netuid, uid)); -// } -// } -// let major_ratio: I32F32 = -// I32F32::from_num(major_emission / (major_emission + minor_emission)); -// assert!(major_stake <= major_ratio); -// }); -// } -// } - -// Test an epoch on an empty graph. -// #[test] -// fn test_overflow() { -// new_test_ext(1).execute_with(|| { -// log::info!("test_overflow:"); -// let netuid = NetUid::from(1); -// add_network(netuid, 1, 0); -// SubtensorModule::set_max_allowed_uids(netuid, 3); -// SubtensorModule::increase_stake_on_coldkey_hotkey_account( -// &U256::from(0), -// &U256::from(0), -// 10, -// ); -// SubtensorModule::increase_stake_on_coldkey_hotkey_account( -// &U256::from(1), -// &U256::from(1), -// 10, -// ); -// SubtensorModule::increase_stake_on_coldkey_hotkey_account( -// &U256::from(2), -// &U256::from(2), -// 10, -// ); -// SubtensorModule::append_neuron(netuid, &U256::from(0), 0); -// SubtensorModule::append_neuron(netuid, &U256::from(1), 0); -// SubtensorModule::append_neuron(netuid, &U256::from(2), 0); -// SubtensorModule::set_validator_permit_for_uid(0, 0, true); -// SubtensorModule::set_validator_permit_for_uid(0, 1, true); -// SubtensorModule::set_validator_permit_for_uid(0, 2, true); -// assert_ok!(SubtensorModule::set_weights( -// RuntimeOrigin::signed(U256::from(0)), -// netuid, -// vec![0, 1, 2], -// vec![u16::MAX / 3, u16::MAX / 3, u16::MAX], -// 0 -// )); -// assert_ok!(SubtensorModule::set_weights( -// RuntimeOrigin::signed(U256::from(1)), -// netuid, -// vec![1, 2], -// vec![u16::MAX / 2, u16::MAX / 2], -// 0 -// )); -// assert_ok!(SubtensorModule::set_weights( -// RuntimeOrigin::signed(U256::from(2)), -// netuid, -// vec![2], -// vec![u16::MAX], -// 0 -// )); -// SubtensorModule::epoch(0, u64::MAX); -// }); -// } - -// Test an epoch on an empty graph. -// #[test] -// fn test_nill_epoch_subtensor() { -// new_test_ext(1).execute_with(|| { -// log::info!("test_nill_epoch:"); -// SubtensorModule::epoch(0, 0); -// }); -// } - -// Test an epoch on a graph with a single item. -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::epoch::test_1_graph --exact --show-output --nocapture -#[test] -fn test_1_graph() { - new_test_ext(1).execute_with(|| { - log::info!("test_1_graph:"); - let netuid = NetUid::from(1); - let coldkey = U256::from(0); - let hotkey = U256::from(0); - let uid: u16 = 0; - let stake_amount: TaoBalance = 1_000_000_000.into(); - add_network_disable_commit_reveal(netuid, u16::MAX - 1, 0); // set higher tempo to avoid built-in epoch, then manual epoch instead - SubtensorModule::set_max_allowed_uids(netuid, 1); - add_balance_to_coldkey_account( - &coldkey, - stake_amount + ExistentialDeposit::get() + SubtensorModule::get_network_min_lock(), - ); - register_ok_neuron(netuid, hotkey, coldkey, 1); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - stake_amount.into() - )); - - assert_eq!(SubtensorModule::get_subnetwork_n(netuid), 1); - run_to_block(1); // run to next block to ensure weights are set on nodes after their registration block - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(U256::from(uid)), - netuid, - vec![uid], - vec![u16::MAX], - 0 - )); - // SubtensorModule::set_weights_for_testing( netuid, i as u16, vec![ ( 0, u16::MAX )]); // doesn't set update status - // SubtensorModule::set_bonds_for_testing( netuid, uid, vec![ ( 0, u16::MAX )]); // rather, bonds are calculated in epoch - SubtensorModule::epoch(netuid, 1_000_000_000.into()); - assert_eq!( - SubtensorModule::get_total_stake_for_hotkey(&hotkey), - stake_amount.into() - ); - assert_eq!(SubtensorModule::get_rank_for_uid(netuid, uid), 0); - assert_eq!(SubtensorModule::get_trust_for_uid(netuid, uid), 0); - assert_eq!(SubtensorModule::get_consensus_for_uid(netuid, uid), 0); - assert_eq!( - SubtensorModule::get_incentive_for_uid(netuid.into(), uid), - 0 - ); - assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, uid), 0); - }); -} -// Test an epoch on a graph with two items. -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::epoch::test_10_graph --exact --show-output --nocapture -#[test] -fn test_10_graph() { - new_test_ext(1).execute_with(|| { - log::info!("test_10_graph"); - // Function for adding a nodes to the graph. - pub fn add_node(netuid: NetUid, coldkey: U256, hotkey: U256, uid: u16, stake_amount: u64) { - log::info!( - "+Add net:{:?} coldkey:{:?} hotkey:{:?} uid:{:?} stake_amount: {:?} subn: {:?}", - netuid, - coldkey, - hotkey, - uid, - stake_amount, - SubtensorModule::get_subnetwork_n(netuid), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - netuid, - stake_amount.into(), - ); - SubtensorModule::append_neuron(netuid, &hotkey, 0); - assert_eq!(SubtensorModule::get_subnetwork_n(netuid) - 1, uid); - } - // Build the graph with 10 items - // each with 1 stake and self weights. - let n: usize = 10; - let netuid = NetUid::from(1); - add_network_disable_commit_reveal(netuid, u16::MAX - 1, 0); // set higher tempo to avoid built-in epoch, then manual epoch instead - SubtensorModule::set_max_allowed_uids(netuid, n as u16); - for i in 0..10 { - add_node(netuid, U256::from(i), U256::from(i), i as u16, 1) - } - assert_eq!(SubtensorModule::get_subnetwork_n(netuid), 10); - run_to_block(1); // run to next block to ensure weights are set on nodes after their registration block - for i in 0..10 { - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(U256::from(i)), - netuid, - vec![i as u16], - vec![u16::MAX], - 0 - )); - } - // Run the epoch. - SubtensorModule::epoch(netuid, 1_000_000_000.into()); - // Check return values. - for i in 0..n { - assert_eq!( - SubtensorModule::get_total_stake_for_hotkey(&(U256::from(i))), - TaoBalance::from(1) - ); - assert_eq!(SubtensorModule::get_rank_for_uid(netuid, i as u16), 0); - assert_eq!(SubtensorModule::get_trust_for_uid(netuid, i as u16), 0); - assert_eq!(SubtensorModule::get_consensus_for_uid(netuid, i as u16), 0); - assert_eq!( - SubtensorModule::get_incentive_for_uid(netuid.into(), i as u16), - 0 - ); - assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, i as u16), 0); - assert_eq!( - SubtensorModule::get_emission_for_uid(netuid, i as u16), - 99999999.into() - ); - } - }); -} - -// Test an epoch on a graph with 512 nodes, of which the first 64 are validators setting non-self weights, and the rest servers setting only self-weights. -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::epoch::test_512_graph --exact --show-output --nocapture -#[test] -fn test_512_graph() { - let netuid = NetUid::from(1); - let network_n: u16 = 512; - let validators_n: u16 = 64; - let max_stake_per_validator: u64 = 328_125_000_000_000; // 21_000_000_000_000_000 / 64 - let epochs: u16 = 3; - log::info!("test_{network_n:?}_graph ({validators_n:?} validators)"); - for interleave in 0..3 { - for server_self in [false, true] { - // server-self weight off/on - let (validators, servers) = distribute_nodes( - validators_n as usize, - network_n as usize, - interleave as usize, - ); - let server: usize = servers[0] as usize; - let validator: usize = validators[0] as usize; - new_test_ext(1).execute_with(|| { - init_run_epochs( - netuid, - network_n, - &validators, - &servers, - epochs, - max_stake_per_validator, - server_self, - &[], - false, - &[], - false, - false, - 0, - false, - u16::MAX, - ); - let bonds = SubtensorModule::get_bonds(netuid.into()); - for uid in validators { - assert_eq!( - SubtensorModule::get_total_stake_for_hotkey(&(U256::from(uid))), - max_stake_per_validator.into() - ); - assert_eq!(SubtensorModule::get_consensus_for_uid(netuid, uid), 0); - assert_eq!( - SubtensorModule::get_incentive_for_uid(netuid.into(), uid), - 0 - ); - assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, uid), 1023); // floor(1 / 64 * 65_535) - assert_eq!( - SubtensorModule::get_emission_for_uid(netuid, uid), - 7812500.into() - ); // 0.5 / 200 * 1_000_000_000 - assert_eq!(bonds[uid as usize][validator], 0.0); - assert_eq!(bonds[uid as usize][server], I32F32::from_num(65_535)); - } - for uid in servers { - assert_eq!( - SubtensorModule::get_total_stake_for_hotkey(&(U256::from(uid))), - TaoBalance::ZERO - ); - assert_eq!(SubtensorModule::get_consensus_for_uid(netuid, uid), 146); - assert_eq!( - SubtensorModule::get_incentive_for_uid(netuid.into(), uid), - 146 - ); // floor(1 / (512 - 64) * 65_535) - assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, uid), 0); - assert_eq!( - SubtensorModule::get_emission_for_uid(netuid, uid), - 1116071.into() - ); // floor(0.5 / (512 - 64) * 1_000_000_000) - assert_eq!(bonds[uid as usize][validator], 0.0); - assert_eq!(bonds[uid as usize][server], 0.0); - } - }); - } - } -} - -// Test an epoch on a graph with 4096 nodes, of which the first 256 are validators setting random non-self weights, and the rest servers setting only self-weights. -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::epoch::test_512_graph_random_weights --exact --show-output --nocapture -#[test] -fn test_512_graph_random_weights() { - let netuid = NetUid::from(1); - let network_n: u16 = 512; - let validators_n: u16 = 64; - let epochs: u16 = 1; - log::info!("test_{network_n:?}_graph_random_weights ({validators_n:?} validators)"); - for interleave in 0..3 { - // server-self weight off/on - for server_self in [false, true] { - for bonds_penalty in [0, u16::MAX / 2, u16::MAX] { - let (validators, servers) = distribute_nodes( - validators_n as usize, - network_n as usize, - interleave as usize, - ); - let server: usize = servers[0] as usize; - let validator: usize = validators[0] as usize; - let (mut rank, mut incentive, mut dividend, mut emission, mut bondv, mut bonds): ( - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - ) = (vec![], vec![], vec![], vec![], vec![], vec![]); - - // Dense epoch - new_test_ext(1).execute_with(|| { - init_run_epochs( - netuid, - network_n, - &validators, - &servers, - epochs, - 1, - server_self, - &[], - false, - &[], - false, - true, - interleave as u64, - false, - bonds_penalty, - ); - - let bond = SubtensorModule::get_bonds(netuid.into()); - for uid in 0..network_n { - rank.push(SubtensorModule::get_rank_for_uid(netuid, uid)); - incentive.push(SubtensorModule::get_incentive_for_uid(netuid.into(), uid)); - dividend.push(SubtensorModule::get_dividends_for_uid(netuid, uid)); - emission.push(SubtensorModule::get_emission_for_uid(netuid, uid)); - bondv.push(bond[uid as usize][validator]); - bonds.push(bond[uid as usize][server]); - } - }); - - // Sparse epoch (same random seed as dense) - new_test_ext(1).execute_with(|| { - init_run_epochs( - netuid, - network_n, - &validators, - &servers, - epochs, - 1, - server_self, - &[], - false, - &[], - false, - true, - interleave as u64, - true, - bonds_penalty, - ); - // Assert that dense and sparse epoch results are equal - let bond = SubtensorModule::get_bonds(netuid.into()); - for uid in 0..network_n { - assert_eq!( - SubtensorModule::get_rank_for_uid(netuid, uid), - rank[uid as usize] - ); - assert_eq!( - SubtensorModule::get_incentive_for_uid(netuid.into(), uid), - incentive[uid as usize] - ); - assert_eq!( - SubtensorModule::get_dividends_for_uid(netuid, uid), - dividend[uid as usize] - ); - assert_eq!( - SubtensorModule::get_emission_for_uid(netuid, uid), - emission[uid as usize] - ); - assert_eq!(bond[uid as usize][validator], bondv[uid as usize]); - assert_eq!(bond[uid as usize][server], bonds[uid as usize]); - } - }); - } - } - } -} - -// Test an epoch on a graph with 4096 nodes, of which the first 256 are validators setting non-self weights, and the rest servers setting only self-weights. -// #[test] -// fn test_4096_graph() { -// let netuid = NetUid::from(1); -// let network_n: u16 = 4096; -// let validators_n: u16 = 256; -// let epochs: u16 = 1; -// let max_stake_per_validator: u64 = 82_031_250_000_000; // 21_000_000_000_000_000 / 256 -// log::info!("test_{network_n:?}_graph ({validators_n:?} validators)"); -// for interleave in 0..3 { -// let (validators, servers) = distribute_nodes( -// validators_n as usize, -// network_n as usize, -// interleave as usize, -// ); -// let server: usize = servers[0] as usize; -// let validator: usize = validators[0] as usize; -// for server_self in [false, true] { -// // server-self weight off/on -// new_test_ext(1).execute_with(|| { -// init_run_epochs( -// netuid, -// network_n, -// &validators, -// &servers, -// epochs, -// max_stake_per_validator, -// server_self, -// &[], -// false, -// &[], -// false, -// false, -// 0, -// true, -// u16::MAX, -// ); -// let (total_stake, _, _) = SubtensorModule::get_stake_weights_for_network(netuid); -// assert_eq!(total_stake.iter().map(|s| s.to_num::()).sum::(), 21_000_000_000_000_000); -// let bonds = SubtensorModule::get_bonds(netuid); -// for uid in &validators { -// assert_eq!( -// SubtensorModule::get_total_stake_for_hotkey(&(U256::from(*uid as u64))), -// max_stake_per_validator -// ); -// assert_eq!(SubtensorModule::get_rank_for_uid(netuid, *uid), 0); -// assert_eq!(SubtensorModule::get_trust_for_uid(netuid, *uid), 0); -// assert_eq!(SubtensorModule::get_consensus_for_uid(netuid, *uid), 0); -// assert_eq!(SubtensorModule::get_incentive_for_uid(netuid, *uid), 0); -// assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, *uid), 255); // Note D = floor(1 / 256 * 65_535) -// assert_eq!(SubtensorModule::get_emission_for_uid(netuid, *uid), 1953125); // Note E = 0.5 / 256 * 1_000_000_000 = 1953125 -// assert_eq!(bonds[*uid as usize][validator], 0.0); -// assert_eq!( -// bonds[*uid as usize][server], -// I32F32::from_num(255) / I32F32::from_num(65_535) -// ); // Note B_ij = floor(1 / 256 * 65_535) / 65_535 -// } -// for uid in &servers { -// assert_eq!( -// SubtensorModule::get_total_stake_for_hotkey(&(U256::from(*uid as u64))), -// 0 -// ); -// assert_eq!(SubtensorModule::get_rank_for_uid(netuid, *uid), 17); // Note R = floor(1 / (4096 - 256) * 65_535) = 17 -// assert_eq!(SubtensorModule::get_trust_for_uid(netuid, *uid), 65535); -// assert_eq!(SubtensorModule::get_consensus_for_uid(netuid, *uid), 17); // Note C = floor(1 / (4096 - 256) * 65_535) = 17 -// assert_eq!(SubtensorModule::get_incentive_for_uid(netuid, *uid), 17); // Note I = floor(1 / (4096 - 256) * 65_535) = 17 -// assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, *uid), 0); -// assert_eq!(SubtensorModule::get_emission_for_uid(netuid, *uid), 130208); // Note E = floor(0.5 / (4096 - 256) * 1_000_000_000) = 130208 -// assert_eq!(bonds[*uid as usize][validator], 0.0); -// assert_eq!(bonds[*uid as usize][server], 0.0); -// } -// }); -// } -// } -// } - -// Test an epoch_sparse on a graph with 16384 nodes, of which the first 512 are validators setting non-self weights, and the rest servers setting only self-weights. -// #[test] -// fn test_16384_graph_sparse() { -// new_test_ext(1).execute_with(|| { -// let netuid = NetUid::from(1); -// let n: u16 = 16384; -// let validators_n: u16 = 512; -// let validators: Vec = (0..validators_n).collect(); -// let servers: Vec = (validators_n..n).collect(); -// let server: u16 = servers[0]; -// let epochs: u16 = 1; -// log::info!("test_{n:?}_graph ({validators_n:?} validators)"); -// init_run_epochs( -// netuid, -// n, -// &validators, -// &servers, -// epochs, -// 1, -// false, -// &[], -// false, -// &[], -// false, -// false, -// 0, -// true, -// u16::MAX, -// ); -// let bonds = SubtensorModule::get_bonds(netuid); -// for uid in validators { -// assert_eq!( -// SubtensorModule::get_total_stake_for_hotkey(&(U256::from(uid))), -// 1 -// ); -// assert_eq!(SubtensorModule::get_rank_for_uid(netuid, uid), 0); -// assert_eq!(SubtensorModule::get_trust_for_uid(netuid, uid), 0); -// assert_eq!(SubtensorModule::get_consensus_for_uid(netuid, uid), 438); // Note C = 0.0066928507 = (0.0066928507*65_535) = floor( 438.6159706245 ) -// assert_eq!(SubtensorModule::get_incentive_for_uid(netuid, uid), 0); -// assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, uid), 127); // Note D = floor(1 / 512 * 65_535) = 127 -// assert_eq!(SubtensorModule::get_emission_for_uid(netuid, uid), 976085); // Note E = 0.5 / 512 * 1_000_000_000 = 976_562 (discrepancy) -// assert_eq!(bonds[uid as usize][0], 0.0); -// assert_eq!( -// bonds[uid as usize][server as usize], -// I32F32::from_num(127) / I32F32::from_num(65_535) -// ); // Note B_ij = floor(1 / 512 * 65_535) / 65_535 = 127 / 65_535 -// } -// for uid in servers { -// assert_eq!( -// SubtensorModule::get_total_stake_for_hotkey(&(U256::from(uid))), -// 0 -// ); -// assert_eq!(SubtensorModule::get_rank_for_uid(netuid, uid), 4); // Note R = floor(1 / (16384 - 512) * 65_535) = 4 -// assert_eq!(SubtensorModule::get_trust_for_uid(netuid, uid), 65535); -// assert_eq!(SubtensorModule::get_consensus_for_uid(netuid, uid), 4); // Note C = floor(1 / (16384 - 512) * 65_535) = 4 -// assert_eq!(SubtensorModule::get_incentive_for_uid(netuid, uid), 4); // Note I = floor(1 / (16384 - 512) * 65_535) = 4 -// assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, uid), 0); -// assert_eq!(SubtensorModule::get_emission_for_uid(netuid, uid), 31517); // Note E = floor(0.5 / (16384 - 512) * 1_000_000_000) = 31502 (discrepancy) -// assert_eq!(bonds[uid as usize][0], 0.0); -// assert_eq!(bonds[uid as usize][server as usize], 0.0); -// } -// }); -// } - -// Test bonds exponential moving average over a sequence of epochs - no liquid alpha -#[test] -fn test_bonds() { - new_test_ext(1).execute_with(|| { - let sparse: bool = true; - let n: u16 = 8; - let netuid = NetUid::from(1); - let tempo: u16 = 1; - let max_stake: TaoBalance = 4.into(); - let stakes: Vec = vec![1, 2, 3, 4, 0, 0, 0, 0]; - let block_number = System::block_number(); - add_network_disable_commit_reveal(netuid, tempo, 0); - SubtensorModule::set_max_allowed_uids( netuid, n ); - assert_eq!(SubtensorModule::get_max_allowed_uids(netuid), n); - SubtensorModule::set_max_registrations_per_block( netuid, n ); - SubtensorModule::set_target_registrations_per_interval(netuid, n); - SubtensorModule::set_weights_set_rate_limit( netuid, 0 ); - SubtensorModule::set_min_allowed_weights( netuid, 1 ); - SubtensorModule::set_bonds_penalty(netuid, u16::MAX); - - - // === Register [validator1, validator2, validator3, validator4, server1, server2, server3, server4] - for key in 0..n as u64 { - add_balance_to_coldkey_account( - &U256::from(key), - max_stake + ExistentialDeposit::get() + SubtensorModule::get_network_min_lock() - ); - let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( netuid, block_number, key * 1_000_000, &U256::from(key)); - assert_ok!(SubtensorModule::register(<::RuntimeOrigin>::signed(U256::from(key)), netuid, block_number, nonce, work, U256::from(key), U256::from(key))); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( &U256::from(key), &U256::from(key), netuid, stakes[key as usize].into() ); - } - assert_eq!(SubtensorModule::get_max_allowed_uids(netuid), n); - assert_eq!(SubtensorModule::get_subnetwork_n(netuid), n); - - // === Issue validator permits - SubtensorModule::set_max_allowed_validators(netuid, n); - assert_eq!( SubtensorModule::get_max_allowed_validators(netuid), n); - SubtensorModule::epoch( netuid, 1_000_000_000 .into()); // run first epoch to set allowed validators - next_block_no_epoch(netuid); // run to next block to ensure weights are set on nodes after their registration block - - // === Set weights [val->srv1: 0.1, val->srv2: 0.2, val->srv3: 0.3, val->srv4: 0.4] - for uid in 0..(n/2) as u64 { - assert_ok!(SubtensorModule::set_weights(RuntimeOrigin::signed(U256::from(uid)), netuid, ((n/2)..n).collect(), vec![ u16::MAX/4, u16::MAX/2, (u16::MAX/4)*3, u16::MAX], 0)); - } - if sparse { SubtensorModule::epoch( netuid, 1_000_000_000 .into()); } - else { SubtensorModule::epoch_dense( netuid, 1_000_000_000 .into()); } - /* n: 8 - current_block: 1; activity_cutoff: 5000; Last update: [1, 1, 1, 1, 0, 0, 0, 0] - Inactive: [false, false, false, false, false, false, false, false] - Block at registration: [0, 0, 0, 0, 0, 0, 0, 0] - hotkeys: [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7)] - S: [0.0999999999, 0.2, 0.2999999998, 0.4, 0, 0, 0, 0] - validator_permits: [true, true, true, true, true, true, true, true] - max_allowed_validators: 8 - new_validator_permits: [true, true, true, true, true, true, true, true] - S: [0.0999999999, 0.2, 0.2999999998, 0.4, 0, 0, 0, 0] - W: [[(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] - W (permit): [[(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] - W (permit+diag): [[(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] - W (permit+diag+outdate): [[(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] - W (mask+norm): [[(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [], [], [], []] - R (before): [0, 0, 0, 0, 0.099997558, 0.2000012202, 0.2999926745, 0.4000085443] - C: [0, 0, 0, 0, 0.0999975584, 0.2000012207, 0.2999926754, 0.400008545] - W: [[(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [], [], [], []] - Tv: [0.9999999995, 0.9999999995, 0.9999999995, 0.9999999995, 0, 0, 0, 0] - R (after): [0, 0, 0, 0, 0.099997558, 0.2000012202, 0.2999926745, 0.4000085443] - T: [0, 0, 0, 0, 1, 1, 1, 1] - I (=R): [0, 0, 0, 0, 0.0999975582, 0.2000012207, 0.2999926752, 0.4000085455] - B: [[], [], [], [], [], [], [], []] - B (outdatedmask): [[], [], [], [], [], [], [], []] - B (mask+norm): [[], [], [], [], [], [], [], []] - ΔB: [[(4, 0.0099997558), (5, 0.020000122), (6, 0.0299992673), (7, 0.0400008543)], [(4, 0.0199995115), (5, 0.040000244), (6, 0.0599985349), (7, 0.0800017088)], [(4, 0.0299992673), (5, 0.060000366), (6, 0.0899978024), (7, 0.1200025633)], [(4, 0.0399990233), (5, 0.080000488), (6, 0.11999707), (7, 0.1600034179)], [], [], [], []] - ΔB (norm): [[(4, 0.0999999996), (5, 0.0999999999), (6, 0.0999999994), (7, 0.0999999996)], [(4, 0.1999999995), (5, 0.2), (6, 0.1999999997), (7, 0.1999999997)], [(4, 0.299999999), (5, 0.2999999998), (6, 0.3), (7, 0.3)], [(4, 0.4000000013), (5, 0.4), (6, 0.4000000004), (7, 0.4000000001)], [], [], [], []] - emaB: [[(4, 0.0999999982), (5, 0.0999999985), (6, 0.099999998), (7, 0.099999998)], [(4, 0.199999999), (5, 0.1999999995), (6, 0.1999999986), (7, 0.1999999986)], [(4, 0.2999999996), (5, 0.3000000003), (6, 0.3000000012), (7, 0.3000000012)], [(4, 0.4000000027), (5, 0.4000000013), (6, 0.4000000018), (7, 0.4000000018)], [], [], [], []] - D: [0.0999999978, 0.1999999983, 0.3000000012, 0.4000000022, 0, 0, 0, 0] - nE: [0.0499999989, 0.0999999992, 0.1500000006, 0.2000000011, 0.049998779, 0.1000006103, 0.1499963375, 0.2000042726] - E: [49999998, 99999999, 150000000, 200000001, 49998779, 100000610, 149996337, 200004272] - P: [0.0499999989, 0.0999999992, 0.1500000006, 0.2000000011, 0.049998779, 0.1000006103, 0.1499963375, 0.2000042726] - emaB: [[(4, 0.2499999937), (5, 0.2499999953), (6, 0.2499999937), (7, 0.2499999937)], [(4, 0.4999999942), (5, 0.499999997), (6, 0.4999999942), (7, 0.4999999942)], [(4, 0.7499999937), (5, 0.7499999981), (6, 0.7499999995), (7, 0.7499999995)], [(4, 1), (5, 1), (6, 1), (7, 1)], [], [], [], []] */ - let bonds = SubtensorModule::get_bonds( netuid.into() ); - assert_eq!(bonds[0][4], 16383); - assert_eq!(bonds[1][4], 32767); - assert_eq!(bonds[2][4], 49151); - assert_eq!(bonds[3][4], 65535); - - // === Set self-weight only on val1 - let uid = 0; - assert_ok!(SubtensorModule::set_weights(RuntimeOrigin::signed(U256::from(uid)), netuid, vec![uid], vec![u16::MAX], 0)); - next_block_no_epoch(netuid); - - if sparse { SubtensorModule::epoch( netuid, 1_000_000_000 .into()); } - else { SubtensorModule::epoch_dense( netuid, 1_000_000_000.into() ); } - /* n: 8 - current_block: 2 - activity_cutoff: 5000 - Last update: [1, 1, 1, 1, 0, 0, 0, 0] - Inactive: [false, false, false, false, false, false, false, false] - Block at registration: [0, 0, 0, 0, 0, 0, 0, 0] - hotkeys: [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7)] - S: [0.0999999999, 0.2, 0.2999999998, 0.4, 0, 0, 0, 0] - validator_permits: [true, true, true, true, true, true, true, true] - max_allowed_validators: 8 - new_validator_permits: [true, true, true, true, true, true, true, true] - S: [0.0999999999, 0.2, 0.2999999998, 0.4, 0, 0, 0, 0] - W: [[(0, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] - W (permit): [[(0, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] - W (permit+diag): [[], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] - W (permit+diag+outdate): [[], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] - W (mask+norm): [[], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [], [], [], []] - R (before): [0, 0, 0, 0, 0.0899978022, 0.1800010982, 0.2699934072, 0.36000769] - C: [0, 0, 0, 0, 0.0999975584, 0.2000012207, 0.2999926754, 0.400008545] - W: [[], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [], [], [], []] - Tv: [0, 0.9999999995, 0.9999999995, 0.9999999995, 0, 0, 0, 0] - R (after): [0, 0, 0, 0, 0.0899978022, 0.1800010982, 0.2699934072, 0.36000769] - T: [0, 0, 0, 0, 1, 1, 1, 1] - I (=R): [0, 0, 0, 0, 0.0999975582, 0.2000012207, 0.2999926754, 0.4000085455] - B: [[(4, 16383), (5, 16383), (6, 16383), (7, 16383)], [(4, 32767), (5, 32767), (6, 32767), (7, 32767)], [(4, 49151), (5, 49151), (6, 49151), (7, 49151)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] - B (outdatedmask): [[(4, 16383), (5, 16383), (6, 16383), (7, 16383)], [(4, 32767), (5, 32767), (6, 32767), (7, 32767)], [(4, 49151), (5, 49151), (6, 49151), (7, 49151)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] - B (mask+norm): [[(4, 0.0999963377), (5, 0.0999963377), (6, 0.0999963377), (7, 0.0999963377)], [(4, 0.1999987792), (5, 0.1999987792), (6, 0.1999987792), (7, 0.1999987792)], [(4, 0.3000012205), (5, 0.3000012205), (6, 0.3000012205), (7, 0.3000012205)], [(4, 0.400003662), (5, 0.400003662), (6, 0.400003662), (7, 0.400003662)], [], [], [], []] - ΔB: [[], [(4, 0.0199995115), (5, 0.040000244), (6, 0.0599985349), (7, 0.0800017088)], [(4, 0.0299992673), (5, 0.060000366), (6, 0.0899978024), (7, 0.1200025633)], [(4, 0.0399990233), (5, 0.080000488), (6, 0.11999707), (7, 0.1600034179)], [], [], [], []] - ΔB (norm): [[], [(4, 0.2222222215), (5, 0.222222222), (6, 0.2222222218), (7, 0.2222222218)], [(4, 0.3333333323), (5, 0.3333333333), (6, 0.3333333333), (7, 0.3333333333)], [(4, 0.4444444457), (5, 0.4444444443), (6, 0.4444444447), (7, 0.4444444445)], [], [], [], []] - emaB: [[(4, 0.0899967037), (5, 0.0899967037), (6, 0.0899967037), (7, 0.0899967037)], [(4, 0.2022211235), (5, 0.2022211235), (6, 0.2022211235), (7, 0.2022211235)], [(4, 0.3033344317), (5, 0.3033344317), (6, 0.3033344317), (7, 0.3033344317)], [(4, 0.4044477409), (5, 0.4044477406), (6, 0.4044477406), (7, 0.4044477406)], [], [], [], []] - D: [0.0899967032, 0.2022211233, 0.303334432, 0.404447741, 0, 0, 0, 0] - nE: [0.0449983515, 0.1011105615, 0.1516672159, 0.2022238704, 0.049998779, 0.1000006103, 0.1499963377, 0.2000042726] - E: [44998351, 101110561, 151667215, 202223870, 49998779, 100000610, 149996337, 200004272] - P: [0.0449983515, 0.1011105615, 0.1516672159, 0.2022238704, 0.049998779, 0.1000006103, 0.1499963377, 0.2000042726] - emaB: [[(4, 0.2225175085), (5, 0.2225175085), (6, 0.2225175085), (7, 0.2225175085)], [(4, 0.499993208), (5, 0.4999932083), (6, 0.4999932083), (7, 0.4999932083)], [(4, 0.7499966028), (5, 0.7499966032), (6, 0.7499966032), (7, 0.7499966032)], [(4, 1), (5, 1), (6, 1), (7, 1)], [], [], [], []] */ - let bonds = SubtensorModule::get_bonds( netuid.into() ); - assert_eq!(bonds[0][4], 14582); - assert_eq!(bonds[1][4], 32767); - assert_eq!(bonds[2][4], 49151); - assert_eq!(bonds[3][4], 65535); - - // === Set self-weight only on val2 - let uid = 1; - assert_ok!(SubtensorModule::set_weights(RuntimeOrigin::signed(U256::from(uid)), netuid, vec![uid], vec![u16::MAX], 0)); - next_block_no_epoch(netuid); - - if sparse { SubtensorModule::epoch( netuid, 1_000_000_000 .into()); } - else { SubtensorModule::epoch_dense( netuid, 1_000_000_000.into() ); } - /* current_block: 3 - W: [[(0, 65535)], [(1, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] - W (permit): [[(0, 65535)], [(1, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] - W (permit+diag): [[], [], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] - W (permit+diag+outdate): [[], [], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] - W (mask+norm): [[], [], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [], [], [], []] - R (before): [0, 0, 0, 0, 0.0699982906, 0.1400008542, 0.2099948723, 0.2800059812] - C: [0, 0, 0, 0, 0.0999975584, 0.2000012207, 0.2999926754, 0.400008545] - W: [[], [], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [], [], [], []] - Tv: [0, 0, 0.9999999995, 0.9999999995, 0, 0, 0, 0] - R (after): [0, 0, 0, 0, 0.0699982906, 0.1400008542, 0.2099948723, 0.2800059812] - T: [0, 0, 0, 0, 1, 1, 1, 1] - I (=R): [0, 0, 0, 0, 0.0999975582, 0.2000012207, 0.2999926754, 0.4000085455] - B: [[(4, 14582), (5, 14582), (6, 14582), (7, 14582)], [(4, 32767), (5, 32767), (6, 32767), (7, 32767)], [(4, 49151), (5, 49151), (6, 49151), (7, 49151)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] - B (outdatedmask): [[(4, 14582), (5, 14582), (6, 14582), (7, 14582)], [(4, 32767), (5, 32767), (6, 32767), (7, 32767)], [(4, 49151), (5, 49151), (6, 49151), (7, 49151)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] - B (mask+norm): [[(4, 0.0899929027), (5, 0.0899929027), (6, 0.0899929027), (7, 0.0899929027)], [(4, 0.2022217421), (5, 0.2022217421), (6, 0.2022217421), (7, 0.2022217421)], [(4, 0.303335699), (5, 0.303335699), (6, 0.303335699), (7, 0.303335699)], [(4, 0.404449656), (5, 0.404449656), (6, 0.404449656), (7, 0.404449656)], [], [], [], []] - ΔB: [[], [], [(4, 0.0299992673), (5, 0.060000366), (6, 0.0899978024), (7, 0.1200025633)], [(4, 0.0399990233), (5, 0.080000488), (6, 0.11999707), (7, 0.1600034179)], [], [], [], []] - ΔB (norm): [[], [], [(4, 0.428571427), (5, 0.4285714284), (6, 0.4285714284), (7, 0.4285714284)], [(4, 0.5714285728), (5, 0.5714285714), (6, 0.5714285714), (7, 0.5714285714)], [], [], [], []] - emaB: [[(4, 0.0809936123), (5, 0.0809936123), (6, 0.0809936123), (7, 0.0809936123)], [(4, 0.181999568), (5, 0.181999568), (6, 0.181999568), (7, 0.181999568)], [(4, 0.3158592717), (5, 0.315859272), (6, 0.315859272), (7, 0.315859272)], [(4, 0.4211475477), (5, 0.4211475474), (6, 0.4211475474), (7, 0.4211475474)], [], [], [], []] - D: [0.0809936118, 0.1819995677, 0.3158592721, 0.421147548, 0, 0, 0, 0] - nE: [0.040496806, 0.0909997837, 0.157929636, 0.2105737738, 0.049998779, 0.1000006103, 0.1499963377, 0.2000042726] - E: [40496805, 90999783, 157929636, 210573773, 49998779, 100000610, 149996337, 200004272] - P: [0.040496806, 0.0909997837, 0.157929636, 0.2105737738, 0.049998779, 0.1000006103, 0.1499963377, 0.2000042726] - emaB: [[(4, 0.192316476), (5, 0.192316476), (6, 0.192316476), (7, 0.192316476)], [(4, 0.4321515555), (5, 0.4321515558), (6, 0.4321515558), (7, 0.4321515558)], [(4, 0.7499967015), (5, 0.7499967027), (6, 0.7499967027), (7, 0.7499967027)], [(4, 1), (5, 1), (6, 1), (7, 1)], [], [], [], []] */ - let bonds = SubtensorModule::get_bonds( netuid.into() ); - assert_eq!(bonds[0][4], 12603); - assert_eq!(bonds[1][4], 28321); - assert_eq!(bonds[2][4], 49151); - assert_eq!(bonds[3][4], 65535); - - // === Set self-weight only on val3 - let uid = 2; - assert_ok!(SubtensorModule::set_weights(RuntimeOrigin::signed(U256::from(uid)), netuid, vec![uid], vec![u16::MAX], 0)); - next_block_no_epoch(netuid); - - if sparse { SubtensorModule::epoch( netuid, 1_000_000_000 .into()); } - else { SubtensorModule::epoch_dense( netuid, 1_000_000_000.into() ); } - /* current_block: 4 - W: [[(0, 65535)], [(1, 65535)], [(2, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] - W (permit): [[(0, 65535)], [(1, 65535)], [(2, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] - W (permit+diag): [[], [], [], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] - W (permit+diag+outdate): [[], [], [], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] - W (mask+norm): [[], [], [], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [], [], [], []] - R (before): [0, 0, 0, 0, 0.0399990233, 0.080000488, 0.11999707, 0.1600034179] - C: [0, 0, 0, 0, 0, 0, 0, 0] - W: [[], [], [], [], [], [], [], []] - Tv: [0, 0, 0, 0, 0, 0, 0, 0] - R (after): [0, 0, 0, 0, 0, 0, 0, 0] - T: [0, 0, 0, 0, 0, 0, 0, 0] - I (=R): [0, 0, 0, 0, 0, 0, 0, 0] - B: [[(4, 12603), (5, 12603), (6, 12603), (7, 12603)], [(4, 28321), (5, 28321), (6, 28321), (7, 28321)], [(4, 49151), (5, 49151), (6, 49151), (7, 49151)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] - B (outdatedmask): [[(4, 12603), (5, 12603), (6, 12603), (7, 12603)], [(4, 28321), (5, 28321), (6, 28321), (7, 28321)], [(4, 49151), (5, 49151), (6, 49151), (7, 49151)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] - B (mask+norm): [[(4, 0.0809909387), (5, 0.0809909387), (6, 0.0809909387), (7, 0.0809909387)], [(4, 0.1819998713), (5, 0.1819998713), (6, 0.1819998713), (7, 0.1819998713)], [(4, 0.3158601632), (5, 0.3158601632), (6, 0.3158601632), (7, 0.3158601632)], [(4, 0.4211490264), (5, 0.4211490264), (6, 0.4211490264), (7, 0.4211490264)], [], [], [], []] - ΔB: [[], [], [], [], [], [], [], []] - ΔB (norm): [[], [], [], [], [], [], [], []] - emaB: [[(4, 0.0809909385), (5, 0.0809909385), (6, 0.0809909385), (7, 0.0809909385)], [(4, 0.1819998713), (5, 0.1819998713), (6, 0.1819998713), (7, 0.1819998713)], [(4, 0.3158601632), (5, 0.3158601632), (6, 0.3158601632), (7, 0.3158601632)], [(4, 0.4211490266), (5, 0.4211490266), (6, 0.4211490266), (7, 0.4211490266)], [], [], [], []] - D: [0, 0, 0, 0, 0, 0, 0, 0] - nE: [0.0999999999, 0.2, 0.2999999998, 0.4, 0, 0, 0, 0] - E: [99999999, 199999999, 299999999, 399999999, 0, 0, 0, 0] - P: [0.0999999999, 0.2, 0.2999999998, 0.4, 0, 0, 0, 0] - emaB: [[(4, 0.1923094518), (5, 0.1923094518), (6, 0.1923094518), (7, 0.1923094518)], [(4, 0.4321507583), (5, 0.4321507583), (6, 0.4321507583), (7, 0.4321507583)], [(4, 0.7499961846), (5, 0.7499961846), (6, 0.7499961846), (7, 0.7499961846)], [(4, 1), (5, 1), (6, 1), (7, 1)], [], [], [], []] */ - let bonds = SubtensorModule::get_bonds( netuid.into() ); - assert_eq!(bonds[0][7], 12602); - assert_eq!(bonds[1][7], 28320); - assert_eq!(bonds[2][7], 49150); - assert_eq!(bonds[3][7], 65535); - - // === Set val3->srv4: 1 - assert_ok!(SubtensorModule::set_weights(RuntimeOrigin::signed(U256::from(2)), netuid, vec![7], vec![u16::MAX], 0)); - next_block_no_epoch(netuid); - - if sparse { SubtensorModule::epoch( netuid, 1_000_000_000 .into()); } - else { SubtensorModule::epoch_dense( netuid, 1_000_000_000.into() ); } - /* current_block: 5 - W: [[(0, 65535)], [(1, 65535)], [(7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] - W (permit): [[(0, 65535)], [(1, 65535)], [(7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] - W (permit+diag): [[], [], [(7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] - W (permit+diag+outdate): [[], [], [(7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] - W (mask+norm): [[], [], [(7, 1)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [], [], [], []] - R (before): [0, 0, 0, 0, 0.0399990233, 0.080000488, 0.11999707, 0.4600034177] - C: [0, 0, 0, 0, 0, 0, 0, 0.400008545] - W: [[], [], [(7, 0.400008545)], [(7, 0.400008545)], [], [], [], []] - Tv: [0, 0, 0.400008545, 0.400008545, 0, 0, 0, 0] - R (after): [0, 0, 0, 0, 0, 0, 0, 0.2800059812] - T: [0, 0, 0, 0, 0, 0, 0, 0.6087041323] - I (=R): [0, 0, 0, 0, 0, 0, 0, 1] - B: [[(4, 12602), (5, 12602), (6, 12602), (7, 12602)], [(4, 28320), (5, 28320), (6, 28320), (7, 28320)], [(4, 49150), (5, 49150), (6, 49150), (7, 49150)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] - B (outdatedmask): [[(4, 12602), (5, 12602), (6, 12602), (7, 12602)], [(4, 28320), (5, 28320), (6, 28320), (7, 28320)], [(4, 49150), (5, 49150), (6, 49150), (7, 49150)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] - B (mask+norm): [[(4, 0.0809860737), (5, 0.0809860737), (6, 0.0809860737), (7, 0.0809860737)], [(4, 0.1819969537), (5, 0.1819969537), (6, 0.1819969537), (7, 0.1819969537)], [(4, 0.3158598263), (5, 0.3158598263), (6, 0.3158598263), (7, 0.3158598263)], [(4, 0.4211571459), (5, 0.4211571459), (6, 0.4211571459), (7, 0.4211571459)], [], [], [], []] - ΔB: [[], [], [(7, 0.1200025633)], [(7, 0.1600034179)], [], [], [], []] - ΔB (norm): [[], [], [(7, 0.4285714284)], [(7, 0.5714285714)], [], [], [], []] - emaB: [[(4, 0.0809860737), (5, 0.0809860737), (6, 0.0809860737), (7, 0.0728874663)], [(4, 0.1819969537), (5, 0.1819969537), (6, 0.1819969537), (7, 0.1637972582)], [(4, 0.3158598263), (5, 0.3158598263), (6, 0.3158598263), (7, 0.3271309866)], [(4, 0.421157146), (5, 0.421157146), (6, 0.421157146), (7, 0.4361842885)], [], [], [], []] - D: [0.0728874663, 0.1637972582, 0.3271309866, 0.4361842885, 0, 0, 0, 0] - nE: [0.0364437331, 0.081898629, 0.1635654932, 0.2180921442, 0, 0, 0, 0.5] - E: [36443733, 81898628, 163565493, 218092144, 0, 0, 0, 500000000] - P: [0.0364437331, 0.081898629, 0.1635654932, 0.2180921442, 0, 0, 0, 0.5] - emaB: [[(4, 0.1922941932), (5, 0.1922941932), (6, 0.1922941932), (7, 0.1671024568)], [(4, 0.4321354993), (5, 0.4321354993), (6, 0.4321354993), (7, 0.3755230587)], [(4, 0.7499809256), (5, 0.7499809256), (6, 0.7499809256), (7, 0.749983425)], [(4, 1), (5, 1), (6, 1), (7, 1)], [], [], [], []] */ - let bonds = SubtensorModule::get_bonds( netuid.into() ); - assert_eq!(bonds[0][7], 10951); - assert_eq!(bonds[1][7], 24609); - assert_eq!(bonds[2][7], 49150); - assert_eq!(bonds[3][7], 65535); - - next_block_no_epoch(netuid); - - if sparse { SubtensorModule::epoch( netuid, 1_000_000_000 .into()); } - else { SubtensorModule::epoch_dense( netuid, 1_000_000_000.into() ); } - /* current_block: 6 - B: [[(4, 12601), (5, 12601), (6, 12601), (7, 10951)], [(4, 28319), (5, 28319), (6, 28319), (7, 24609)], [(4, 49149), (5, 49149), (6, 49149), (7, 49150)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] - B (outdatedmask): [[(4, 12601), (5, 12601), (6, 12601), (7, 10951)], [(4, 28319), (5, 28319), (6, 28319), (7, 24609)], [(4, 49149), (5, 49149), (6, 49149), (7, 49150)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] - B (mask+norm): [[(4, 0.0809812085), (5, 0.0809812085), (6, 0.0809812085), (7, 0.0728876167)], [(4, 0.181994036), (5, 0.181994036), (6, 0.181994036), (7, 0.163792472)], [(4, 0.3158594894), (5, 0.3158594894), (6, 0.3158594894), (7, 0.3271323503)], [(4, 0.4211652656), (5, 0.4211652656), (6, 0.4211652656), (7, 0.4361875602)], [], [], [], []] - ΔB: [[], [], [(7, 0.1200025633)], [(7, 0.1600034179)], [], [], [], []] - ΔB (norm): [[], [], [(7, 0.4285714284)], [(7, 0.5714285714)], [], [], [], []] - emaB: [[(4, 0.0809812082), (5, 0.0809812082), (6, 0.0809812082), (7, 0.0655988548)], [(4, 0.181994036), (5, 0.181994036), (6, 0.181994036), (7, 0.1474132247)], [(4, 0.3158594896), (5, 0.3158594896), (6, 0.3158594896), (7, 0.3372762585)], [(4, 0.4211652658), (5, 0.4211652658), (6, 0.4211652658), (7, 0.4497116616)], [], [], [], []] - D: [0.0655988548, 0.1474132247, 0.3372762585, 0.4497116616, 0, 0, 0, 0] - nE: [0.0327994274, 0.0737066122, 0.1686381293, 0.2248558307, 0, 0, 0, 0.5] - E: [32799427, 73706612, 168638129, 224855830, 0, 0, 0, 500000000] - P: [0.0327994274, 0.0737066122, 0.1686381293, 0.2248558307, 0, 0, 0, 0.5] - emaB: [[(4, 0.1922789337), (5, 0.1922789337), (6, 0.1922789337), (7, 0.1458686984)], [(4, 0.4321202405), (5, 0.4321202405), (6, 0.4321202405), (7, 0.3277949789)], [(4, 0.749965667), (5, 0.749965667), (6, 0.749965667), (7, 0.74998335)], [(4, 1), (5, 1), (6, 1), (7, 1)], [], [], [], []] */ - let bonds = SubtensorModule::get_bonds( netuid.into() ); - assert_eq!(bonds[0][7], 9559); - assert_eq!(bonds[1][7], 21482); - assert_eq!(bonds[2][7], 49150); - assert_eq!(bonds[3][7], 65535); - - next_block_no_epoch(netuid); - - if sparse { SubtensorModule::epoch( netuid, 1_000_000_000 .into()); } - else { SubtensorModule::epoch_dense( netuid, 1_000_000_000.into() ); } - /* current_block: 7 - B: [[(4, 12600), (5, 12600), (6, 12600), (7, 9559)], [(4, 28318), (5, 28318), (6, 28318), (7, 21482)], [(4, 49148), (5, 49148), (6, 49148), (7, 49150)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] - B (outdatedmask): [[(4, 12600), (5, 12600), (6, 12600), (7, 9559)], [(4, 28318), (5, 28318), (6, 28318), (7, 21482)], [(4, 49148), (5, 49148), (6, 49148), (7, 49150)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] - B (mask+norm): [[(4, 0.0809763432), (5, 0.0809763432), (6, 0.0809763432), (7, 0.065595707)], [(4, 0.1819911182), (5, 0.1819911182), (6, 0.1819911182), (7, 0.1474136391)], [(4, 0.3158591525), (5, 0.3158591525), (6, 0.3158591525), (7, 0.337276807)], [(4, 0.4211733856), (5, 0.4211733856), (6, 0.4211733856), (7, 0.4497138464)], [], [], [], []] - ΔB: [[], [], [(7, 0.1200025633)], [(7, 0.1600034179)], [], [], [], []] - ΔB (norm): [[], [], [(7, 0.4285714284)], [(7, 0.5714285714)], [], [], [], []] - emaB: [[(4, 0.080976343), (5, 0.080976343), (6, 0.080976343), (7, 0.0590361361)], [(4, 0.181991118), (5, 0.181991118), (6, 0.181991118), (7, 0.1326722752)], [(4, 0.3158591525), (5, 0.3158591525), (6, 0.3158591525), (7, 0.3464062694)], [(4, 0.4211733858), (5, 0.4211733858), (6, 0.4211733858), (7, 0.4618853189)], [], [], [], []] - D: [0.0590361361, 0.1326722752, 0.3464062694, 0.4618853189, 0, 0, 0, 0] - nE: [0.029518068, 0.0663361375, 0.1732031347, 0.2309426593, 0, 0, 0, 0.5] - E: [29518068, 66336137, 173203134, 230942659, 0, 0, 0, 500000000] - P: [0.029518068, 0.0663361375, 0.1732031347, 0.2309426593, 0, 0, 0, 0.5] - emaB: [[(4, 0.192263675), (5, 0.192263675), (6, 0.192263675), (7, 0.1278155716)], [(4, 0.4321049813), (5, 0.4321049813), (6, 0.4321049813), (7, 0.2872407278)], [(4, 0.7499504078), (5, 0.7499504078), (6, 0.7499504078), (7, 0.7499832863)], [(4, 1), (5, 1), (6, 1), (7, 1)], [], [], [], []] */ - let bonds = SubtensorModule::get_bonds( netuid.into() ); - assert_eq!(bonds[0][7], 8376); - assert_eq!(bonds[1][7], 18824); - assert_eq!(bonds[2][7], 49150); - assert_eq!(bonds[3][7], 65535); - - next_block_no_epoch(netuid); - - if sparse { SubtensorModule::epoch( netuid, 1_000_000_000 .into()); } - else { SubtensorModule::epoch_dense( netuid, 1_000_000_000.into() ); } - /* current_block: 8 - B: [[(4, 12599), (5, 12599), (6, 12599), (7, 8376)], [(4, 28317), (5, 28317), (6, 28317), (7, 18824)], [(4, 49147), (5, 49147), (6, 49147), (7, 49150)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] - B (outdatedmask): [[(4, 12599), (5, 12599), (6, 12599), (7, 8376)], [(4, 28317), (5, 28317), (6, 28317), (7, 18824)], [(4, 49147), (5, 49147), (6, 49147), (7, 49150)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] - B (mask+norm): [[(4, 0.0809714776), (5, 0.0809714776), (6, 0.0809714776), (7, 0.0590337245)], [(4, 0.1819882002), (5, 0.1819882002), (6, 0.1819882002), (7, 0.1326708249)], [(4, 0.3158588156), (5, 0.3158588156), (6, 0.3158588156), (7, 0.3464073015)], [(4, 0.421181506), (5, 0.421181506), (6, 0.421181506), (7, 0.4618881487)], [], [], [], []] - ΔB: [[], [], [(7, 0.1200025633)], [(7, 0.1600034179)], [], [], [], []] - ΔB (norm): [[], [], [(7, 0.4285714284)], [(7, 0.5714285714)], [], [], [], []] - emaB: [[(4, 0.0809714776), (5, 0.0809714776), (6, 0.0809714776), (7, 0.053130352)], [(4, 0.1819882002), (5, 0.1819882002), (6, 0.1819882002), (7, 0.1194037423)], [(4, 0.3158588156), (5, 0.3158588156), (6, 0.3158588156), (7, 0.3546237142)], [(4, 0.4211815062), (5, 0.4211815062), (6, 0.4211815062), (7, 0.472842191)], [], [], [], []] - D: [0.053130352, 0.1194037423, 0.3546237142, 0.472842191, 0, 0, 0, 0] - nE: [0.026565176, 0.0597018711, 0.177311857, 0.2364210954, 0, 0, 0, 0.5] - E: [26565175, 59701871, 177311856, 236421095, 0, 0, 0, 500000000] - P: [0.026565176, 0.0597018711, 0.177311857, 0.2364210954, 0, 0, 0, 0.5] - emaB: [[(4, 0.1922484161), (5, 0.1922484161), (6, 0.1922484161), (7, 0.1123638137)], [(4, 0.4320897225), (5, 0.4320897225), (6, 0.4320897225), (7, 0.2525234516)], [(4, 0.7499351487), (5, 0.7499351487), (6, 0.7499351487), (7, 0.7499832308)], [(4, 1), (5, 1), (6, 1), (7, 1)], [], [], [], []] */ - }); -} - -#[test] -fn test_set_alpha_disabled() { - new_test_ext(1).execute_with(|| { - let hotkey = U256::from(1); - let coldkey = U256::from(1 + 456); - let netuid = add_dynamic_network(&hotkey, &coldkey); - let signer = RuntimeOrigin::signed(coldkey); - - // Enable Liquid Alpha and setup - SubtensorModule::set_liquid_alpha_enabled(netuid, true); - migrations::migrate_create_root_network::migrate_create_root_network::(); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_000_u64.into()); - assert_ok!(SubtensorModule::root_register(signer.clone(), hotkey,)); - let fee = ::SwapInterface::approx_fee_amount( - netuid.into(), - DefaultMinStake::::get(), - ); - assert_ok!(SubtensorModule::add_stake( - signer.clone(), - hotkey, - netuid, - TaoBalance::from(5) * DefaultMinStake::::get() + fee - )); - // Only owner can set alpha values - assert_ok!(SubtensorModule::register_network(signer.clone(), hotkey)); - - // Explicitly set to false - SubtensorModule::set_liquid_alpha_enabled(netuid, false); - assert_err!( - SubtensorModule::do_set_alpha_values(signer.clone(), netuid, 1638_u16, u16::MAX), - Error::::LiquidAlphaDisabled - ); - - SubtensorModule::set_liquid_alpha_enabled(netuid, true); - assert_ok!(SubtensorModule::do_set_alpha_values( - signer.clone(), - netuid, - 1638_u16, - u16::MAX - )); - }); -} - -// Test that epoch masks out inactive stake of validators with outdated weights beyond activity cutoff. -#[test] -fn test_active_stake() { - new_test_ext(1).execute_with(|| { - System::set_block_number(0); - let sparse: bool = true; - let n: u16 = 4; - let netuid = NetUid::from(1); - let tempo: u16 = 1; - let block_number: u64 = System::block_number(); - let stake: TaoBalance = 1.into(); - add_network_disable_commit_reveal(netuid, tempo, 0); - SubtensorModule::set_max_allowed_uids(netuid, n); - assert_eq!(SubtensorModule::get_max_allowed_uids(netuid), n); - SubtensorModule::set_max_registrations_per_block(netuid, n); - SubtensorModule::set_target_registrations_per_interval(netuid, n); - SubtensorModule::set_min_allowed_weights(netuid, 0); - - // === Register [validator1, validator2, server1, server2] - for key in 0..n as u64 { - add_balance_to_coldkey_account( - &U256::from(key), - stake + ExistentialDeposit::get() + SubtensorModule::get_network_min_lock(), - ); - let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( - netuid, - block_number, - key * 1_000_000, - &U256::from(key), - ); - assert_ok!(SubtensorModule::register( - RuntimeOrigin::signed(U256::from(key)), - netuid, - block_number, - nonce, - work, - U256::from(key), - U256::from(key) - )); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &U256::from(key), - &U256::from(key), - netuid, - AlphaBalance::from(stake.to_u64()), - ); - } - assert_eq!(SubtensorModule::get_max_allowed_uids(netuid), n); - assert_eq!(SubtensorModule::get_subnetwork_n(netuid), n); - - // === Issue validator permits - SubtensorModule::set_max_allowed_validators(netuid, n); - assert_eq!(SubtensorModule::get_max_allowed_validators(netuid), n); - SubtensorModule::epoch(netuid, 1_000_000_000.into()); // run first epoch to set allowed validators - next_block_no_epoch(netuid); // run to next block to ensure weights are set on nodes after their registration block - - // === Set weights [val1->srv1: 0.5, val1->srv2: 0.5, val2->srv1: 0.5, val2->srv2: 0.5] - for uid in 0..(n / 2) as u64 { - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(U256::from(uid)), - netuid, - ((n / 2)..n).collect(), - vec![u16::MAX / (n / 2); (n / 2) as usize], - 0 - )); - } - if sparse { - SubtensorModule::epoch(netuid, 1_000_000_000.into()); - } else { - SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); - } - let bonds = SubtensorModule::get_bonds(netuid.into()); - for uid in 0..n { - // log::info!("\n{uid}" ); - // uid_stats(netuid, uid); - // log::info!("bonds: {:?}", bonds[uid as usize]); - if uid < n / 2 { - assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, uid), 32767); - // Note D = floor(0.5 * 65_535) - } - assert_eq!( - SubtensorModule::get_emission_for_uid(netuid, uid), - 250000000.into() - ); // Note E = 0.5 / (n/2) * 1_000_000_000 = 250_000_000 - } - for bond in bonds.iter().take((n / 2) as usize) { - // for on_validator in 0..(n / 2) as usize { - for i in bond.iter().take((n / 2) as usize) { - assert_eq!(*i, 0); - } - for i in bond.iter().take(n as usize).skip((n / 2) as usize) { - assert_eq!(*i, I32F32::from_num(65_535)); // floor(0.5*(2^16-1))/(2^16-1), then max-upscale to 65_535 - } - } - let activity_cutoff: u64 = SubtensorModule::get_activity_cutoff(netuid) as u64; - run_to_block_no_epoch(netuid, activity_cutoff + 2); // run to block where validator (uid 0, 1) weights become outdated - - // === Update uid 0 weights - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(U256::from(0)), - netuid, - ((n / 2)..n).collect(), - vec![u16::MAX / (n / 2); (n / 2) as usize], - 0 - )); - if sparse { - SubtensorModule::epoch(netuid, 1_000_000_000.into()); - } else { - SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); - } - /* current_block: 5002; activity_cutoff: 5000 - Last update: [5002, 1, 0, 0]; Inactive: [false, true, true, true]; Block at registration: [0, 0, 0, 0] - S: [0.25, 0.25, 0.25, 0.25]; S (mask): [0.25, 0, 0, 0]; S (mask+norm): [1, 0, 0, 0] - validator_permits: [true, true, true, true]; max_allowed_validators: 4; new_validator_permits: [true, true, true, true] - W: [[(2, 0.4999923704), (3, 0.4999923704)], [(2, 0.4999923704), (3, 0.4999923704)], [], []] - W (permit): [[(2, 0.4999923704), (3, 0.4999923704)], [(2, 0.4999923704), (3, 0.4999923704)], [], []] - W (permit+diag): [[(2, 0.4999923704), (3, 0.4999923704)], [(2, 0.4999923704), (3, 0.4999923704)], [], []] - W (permit+diag+outdate): [[(2, 0.4999923704), (3, 0.4999923704)], [(2, 0.4999923704), (3, 0.4999923704)], [], []] - W (mask+norm): [[(2, 0.5), (3, 0.5)], [(2, 0.5), (3, 0.5)], [], []] - R: [0, 0, 0.5, 0.5] - W (threshold): [[(2, 1), (3, 1)], [(2, 1), (3, 1)], [], []] - T: [0, 0, 1, 1] - C: [0.006693358, 0.006693358, 0.9933076561, 0.9933076561] - I: [0, 0, 0.5, 0.5] - B: [[(2, 0.4999923704), (3, 0.4999923704)], [(2, 0.4999923704), (3, 0.4999923704)], [], []] - B (outdatedmask): [[(2, 0.4999923704), (3, 0.4999923704)], [(2, 0.4999923704), (3, 0.4999923704)], [], []] - B (mask+norm): [[(2, 0.5), (3, 0.5)], [(2, 0.5), (3, 0.5)], [], []] - ΔB: [[(2, 0.5), (3, 0.5)], [(2, 0), (3, 0)], [], []] - ΔB (norm): [[(2, 1), (3, 1)], [(2, 0), (3, 0)], [], []] - emaB: [[(2, 0.55), (3, 0.55)], [(2, 0.45), (3, 0.45)], [], []] - emaB (max-upscale): [[(2, 1), (3, 1)], [(2, 1), (3, 1)], [], []] - D: [0.55, 0.4499999997, 0, 0] - nE: [0.275, 0.2249999999, 0.25, 0.25] - E: [274999999, 224999999, 250000000, 250000000] - P: [0.275, 0.2249999999, 0.25, 0.25] - P (u16): [65535, 53619, 59577, 59577] */ - let bonds = SubtensorModule::get_bonds(netuid.into()); - assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, 0), 36044); // Note D = floor((0.5 * 0.9 + 0.1) * 65_535) - assert_eq!( - SubtensorModule::get_emission_for_uid(netuid, 0), - 274999999.into() - ); // Note E = 0.5 * 0.55 * 1_000_000_000 = 275_000_000 (discrepancy) - for server in ((n / 2) as usize)..n as usize { - assert_eq!(bonds[0][server], I32F32::from_num(65_535)); // floor(0.55*(2^16-1))/(2^16-1), then max-upscale - } - for validator in 1..(n / 2) { - assert_eq!( - SubtensorModule::get_dividends_for_uid(netuid, validator), - 29490 - ); // Note D = floor((0.5 * 0.9) * 65_535) - assert_eq!( - SubtensorModule::get_emission_for_uid(netuid, validator), - 224999999.into() - ); // Note E = 0.5 * 0.45 * 1_000_000_000 = 225_000_000 (discrepancy) - for server in ((n / 2) as usize)..n as usize { - assert_eq!(bonds[validator as usize][server], I32F32::from_num(53619)); - // floor(0.45*(2^16-1))/(2^16-1), then max-upscale - } - } - - // === Update uid 1 weights as well - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(U256::from(1)), - netuid, - ((n / 2)..n).collect(), - vec![u16::MAX / (n / 2); (n / 2) as usize], - 0 - )); - run_to_block_no_epoch(netuid, activity_cutoff + 3); // run to block where validator (uid 0, 1) weights become outdated - if sparse { - SubtensorModule::epoch(netuid, 1_000_000_000.into()); - } else { - SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); - } - /* current_block: 5003; activity_cutoff: 5000 - Last update: [5002, 5002, 0, 0]; Inactive: [false, false, true, true]; Block at registration: [0, 0, 0, 0] - S: [0.25, 0.25, 0.25, 0.25]; S (mask): [0.25, 0.25, 0, 0]; S (mask+norm): [0.5, 0.5, 0, 0] - validator_permits: [true, true, true, true]; max_allowed_validators: 4; new_validator_permits: [true, true, true, true] - W: [[(2, 0.4999923704), (3, 0.4999923704)], [(2, 0.4999923704), (3, 0.4999923704)], [], []] - W (permit): [[(2, 0.4999923704), (3, 0.4999923704)], [(2, 0.4999923704), (3, 0.4999923704)], [], []] - W (permit+diag): [[(2, 0.4999923704), (3, 0.4999923704)], [(2, 0.4999923704), (3, 0.4999923704)], [], []] - W (permit+diag+outdate): [[(2, 0.4999923704), (3, 0.4999923704)], [(2, 0.4999923704), (3, 0.4999923704)], [], []] - W (mask+norm): [[(2, 0.5), (3, 0.5)], [(2, 0.5), (3, 0.5)], [], []] - R: [0, 0, 0.5, 0.5] - W (threshold): [[(2, 1), (3, 1)], [(2, 1), (3, 1)], [], []] - T: [0, 0, 1, 1] - C: [0.006693358, 0.006693358, 0.9933076561, 0.9933076561] - I: [0, 0, 0.5, 0.5] - B: [[(2, 65535), (3, 65535)], [(2, 53619), (3, 53619)], [], []] - B (outdatedmask): [[(2, 65535), (3, 65535)], [(2, 53619), (3, 53619)], [], []] - B (mask+norm): [[(2, 0.5500025176), (3, 0.5500025176)], [(2, 0.4499974821), (3, 0.4499974821)], [], []] - ΔB: [[(2, 0.25), (3, 0.25)], [(2, 0.25), (3, 0.25)], [], []] - ΔB (norm): [[(2, 0.5), (3, 0.5)], [(2, 0.5), (3, 0.5)], [], []] - emaB: [[(2, 0.545002266), (3, 0.545002266)], [(2, 0.4549977337), (3, 0.4549977337)], [], []] - emaB (max-upscale): [[(2, 1), (3, 1)], [(2, 0.8348547556), (3, 0.8348547556)], [], []] - D: [0.545002266, 0.4549977337, 0, 0] - nE: [0.272501133, 0.2274988669, 0.25, 0.25] - E: [272501132, 227498866, 250000000, 250000000] - P: [0.272501133, 0.2274988669, 0.25, 0.25] - P (u16): [65535, 54711, 60123, 60123] */ - let bonds = SubtensorModule::get_bonds(netuid.into()); - assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, 0), 35716); // Note D = floor((0.55 * 0.9 + 0.5 * 0.1) * 65_535) - assert_eq!( - SubtensorModule::get_emission_for_uid(netuid, 0), - 272501132.into() - ); // Note E = 0.5 * (0.55 * 0.9 + 0.5 * 0.1) * 1_000_000_000 = 272_500_000 (discrepancy) - for server in ((n / 2) as usize)..n as usize { - assert_eq!(bonds[0][server], I32F32::from_num(65_535)); // floor((0.55 * 0.9 + 0.5 * 0.1)*(2^16-1))/(2^16-1), then max-upscale - } - assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, 1), 29818); // Note D = floor((0.45 * 0.9 + 0.5 * 0.1) * 65_535) - assert_eq!( - SubtensorModule::get_emission_for_uid(netuid, 1), - 227498866.into() - ); // Note E = 0.5 * (0.45 * 0.9 + 0.5 * 0.1) * 1_000_000_000 = 227_500_000 (discrepancy) - for server in ((n / 2) as usize)..n as usize { - assert_eq!(bonds[1][server], I32F32::from_num(54712)); // floor((0.45 * 0.9 + 0.5 * 0.1)/(0.55 * 0.9 + 0.5 * 0.1)*(2^16-1)) - } - }); -} - -// Test that epoch masks out outdated weights and bonds of validators on deregistered servers. -// -#[test] -fn test_outdated_weights() { - new_test_ext(1).execute_with(|| { - let sparse: bool = true; - let n: u16 = 4; - let netuid = NetUid::from(1); - let tempo: u16 = 0; - let mut block_number: u64 = System::block_number(); - let stake: TaoBalance = 1.into(); - add_network_disable_commit_reveal(netuid, tempo, 0); - SubtensorModule::set_max_allowed_uids(netuid, n); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - SubtensorModule::set_max_registrations_per_block(netuid, n); - SubtensorModule::set_target_registrations_per_interval(netuid, n); - SubtensorModule::set_min_allowed_weights(netuid, 0); - SubtensorModule::set_bonds_penalty(netuid, u16::MAX); - assert_eq!(SubtensorModule::get_registrations_this_block(netuid), 0); - - // === Register [validator1, validator2, server1, server2] - for key in 0..n as u64 { - add_balance_to_coldkey_account( - &U256::from(key), - stake - + ExistentialDeposit::get() - + (SubtensorModule::get_network_min_lock() * 2.into()), - ); - let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( - netuid, - block_number, - key * 1_000_000, - &U256::from(key), - ); - assert_ok!(SubtensorModule::register( - RuntimeOrigin::signed(U256::from(key)), - netuid, - block_number, - nonce, - work, - U256::from(key), - U256::from(key) - )); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &U256::from(key), - &U256::from(key), - netuid, - AlphaBalance::from(stake.to_u64()), - ); - } - assert_eq!(SubtensorModule::get_subnetwork_n(netuid), n); - assert_eq!(SubtensorModule::get_registrations_this_block(netuid), 4); - - // === Issue validator permits - SubtensorModule::set_max_allowed_validators(netuid, n); - assert_eq!(SubtensorModule::get_max_allowed_validators(netuid), n); - SubtensorModule::epoch(netuid, 1_000_000_000.into()); // run first epoch to set allowed validators - assert_eq!(SubtensorModule::get_registrations_this_block(netuid), 4); - block_number = next_block_no_epoch(netuid); // run to next block to ensure weights are set on nodes after their registration block - assert_eq!(SubtensorModule::get_registrations_this_block(netuid), 0); - - // === Set weights [val1->srv1: 2/3, val1->srv2: 1/3, val2->srv1: 2/3, val2->srv2: 1/3, srv1->srv1: 1, srv2->srv2: 1] - for uid in 0..(n / 2) as u64 { - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(U256::from(uid)), - netuid, - ((n / 2)..n).collect(), - vec![2 * (u16::MAX / 3), u16::MAX / 3], - 0 - )); - } - for uid in ((n / 2) as u64)..n as u64 { - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(U256::from(uid)), - netuid, - vec![uid as u16], - vec![u16::MAX], - 0 - )); // server self-weight - } - if sparse { - SubtensorModule::epoch(netuid, 1_000_000_000.into()); - } else { - SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); - } - /* current_block: 1; activity_cutoff: 5000 - Last update: [1, 1, 1, 1]; Inactive: [false, false, false, false]; Block at registration: [0, 0, 0, 0] - S: [0.25, 0.25, 0.25, 0.25]; S (mask): [0.25, 0.25, 0.25, 0.25]; S (mask+norm): [0.25, 0.25, 0.25, 0.25] - validator_permits: [true, true, true, true]; max_allowed_validators: 4; new_validator_permits: [true, true, true, true] - W: [[(2, 65535), (3, 32768)], [(2, 65535), (3, 32768)], [(2, 65535)], [(3, 65535)]] - W (permit): [[(2, 65535), (3, 32768)], [(2, 65535), (3, 32768)], [(2, 65535)], [(3, 65535)]] - W (permit+diag): [[(2, 65535), (3, 32768)], [(2, 65535), (3, 32768)], [], []] - W (permit+diag+outdate): [[(2, 65535), (3, 32768)], [(2, 65535), (3, 32768)], [], []] - W (mask+norm): [[(2, 0.6666632756), (3, 0.3333367242)], [(2, 0.6666632756), (3, 0.3333367242)], [], []] - R (before): [0, 0, 0.3333316376, 0.166668362] - C: [0, 0, 0.6666632756, 0.3333367242] - W: [[(2, 0.6666632756), (3, 0.3333367242)], [(2, 0.6666632756), (3, 0.3333367242)], [], []] - Tv: [0.9999999998, 0.9999999998, 0, 0] - R (after): [0, 0, 0.3333316376, 0.166668362] - T: [0, 0, 1, 1] - I (=R): [0, 0, 0.6666632756, 0.3333367242] - B: [[], [], [], []] - B (outdatedmask): [[], [], [], []] - B (mask+norm): [[], [], [], []] - ΔB: [[(2, 0.1666658188), (3, 0.083334181)], [(2, 0.1666658188), (3, 0.083334181)], [], []] - ΔB (norm): [[(2, 0.5), (3, 0.5)], [(2, 0.5), (3, 0.5)], [], []] - emaB: [[(2, 0.5), (3, 0.5)], [(2, 0.5), (3, 0.5)], [], []] - D: [0.5, 0.5, 0, 0] - nE: [0.25, 0.25, 0.3333316378, 0.166668362] - E: [250000000, 250000000, 333331637, 166668361] - P: [0.25, 0.25, 0.3333316378, 0.166668362] - P (u16): [49151, 49151, 65535, 32767] */ - - // === Dereg server2 at uid3 (least emission) + register new key over uid3 - let new_key: u64 = n as u64; // register a new key while at max capacity, which means the least incentive uid will be deregistered - add_balance_to_coldkey_account( - &U256::from(new_key), - stake - + ExistentialDeposit::get() - + (SubtensorModule::get_network_min_lock() * 2.into()), - ); - let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( - netuid, - block_number, - 0, - &U256::from(new_key), - ); - assert_eq!(System::block_number(), block_number); - assert_eq!(SubtensorModule::get_max_registrations_per_block(netuid), n); - assert_eq!(SubtensorModule::get_registrations_this_block(netuid), 0); - assert_ok!(SubtensorModule::register( - RuntimeOrigin::signed(U256::from(new_key)), - netuid, - block_number, - nonce, - work, - U256::from(new_key), - U256::from(new_key) - )); - let deregistered_uid: u16 = n - 1; // since uid=n-1 only recieved 1/3 of weight, it will get pruned first - assert_eq!( - U256::from(new_key), - SubtensorModule::get_hotkey_for_net_and_uid(netuid, deregistered_uid) - .expect("Not registered") - ); - next_block_no_epoch(netuid); // run to next block to outdate weights and bonds set on deregistered uid - - // === Update weights from only uid=0 - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(U256::from(0)), - netuid, - ((n / 2)..n).collect(), - vec![2 * (u16::MAX / 3), u16::MAX / 3], - 0 - )); - if sparse { - SubtensorModule::epoch(netuid, 1_000_000_000.into()); - } else { - SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); - } - /* current_block: 2; activity_cutoff: 5000 - Last update: [2, 1, 1, 1]; Inactive: [false, false, false, false]; Block at registration: [0, 0, 0, 1] - S: [0.3333333333, 0.3333333333, 0.3333333333, 0] - S (mask): [0.3333333333, 0.3333333333, 0.3333333333, 0] - S (mask+norm): [0.3333333333, 0.3333333333, 0.3333333333, 0] - validator_permits: [true, true, true, false]; max_allowed_validators: 4; new_validator_permits: [true, true, true, true] - W: [[(2, 65535), (3, 32768)], [(2, 65535), (3, 32768)], [(2, 65535)], [(3, 65535)]] - W (permit): [[(2, 65535), (3, 32768)], [(2, 65535), (3, 32768)], [(2, 65535)], [(3, 65535)]] - W (permit+diag): [[(2, 65535), (3, 32768)], [(2, 65535), (3, 32768)], [], []] - W (permit+diag+outdate): [[(2, 65535), (3, 32768)], [(2, 65535)], [], []] - W (mask+norm): [[(2, 0.6666632756), (3, 0.3333367242)], [(2, 1)], [], []] - R (before): [0, 0, 0.5555544249, 0.1111122412] - C: [0, 0, 0.6666632756, 0] - W: [[(2, 0.6666632756)], [(2, 0.6666632756)], [], []] - Tv: [0.6666632756, 0.6666632756, 0, 0] - R (after): [0, 0, 0.4444421832, 0] - T: [0, 0, 0.799997558, 0] - I (=R): [0, 0, 1, 0] - B: [[(2, 65535), (3, 65535)], [(2, 65535), (3, 65535)], [], []] - B (outdatedmask): [[(2, 65535), (3, 65535)], [(2, 65535)], [], []] - B (mask+norm): [[(2, 0.5), (3, 1)], [(2, 0.5)], [], []] - ΔB: [[(2, 0.2222210916)], [(2, 0.2222210916)], [], []] - ΔB (norm): [[(2, 0.5)], [(2, 0.5)], [], []] - emaB: [[(2, 0.5), (3, 1)], [(2, 0.5)], [], []] - emaB (max-upscale): [[(2, 1), (3, 1)], [(2, 1)], [], []] - D: [0.5, 0.5, 0, 0] - nE: [0.25, 0.25, 0.5, 0] - E: [250000000, 250000000, 500000000, 0] - P: [0.25, 0.25, 0.5, 0] - P (u16): [32767, 32767, 65535, 0] */ - let bonds = SubtensorModule::get_bonds(netuid.into()); - assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, 0), 32767); // Note D = floor(0.5 * 65_535) - assert_eq!( - SubtensorModule::get_emission_for_uid(netuid, 0), - 250000000.into() - ); // Note E = 0.5 * 0.5 * 1_000_000_000 = 249311245 - assert_eq!(bonds[0][2], I32F32::from_num(65_535)); // floor(0.5*(2^16-1))/(2^16-1), then max-upscale - assert_eq!(bonds[0][3], I32F32::from_num(65_535)); // only uid0 has updated weights for new reg - }); -} - -/// Test the zero emission handling and fallback under zero effective weight conditions, to ensure non-zero effective emission. -#[test] -fn test_zero_weights() { - new_test_ext(1).execute_with(|| { - let sparse: bool = true; - let n: u16 = 2; - let netuid = NetUid::from(1); - let tempo: u16 = u16::MAX - 1; // high tempo to skip automatic epochs in on_initialize, use manual epochs instead - let mut block_number: u64 = 0; - let stake: u64 = 1; - add_network_disable_commit_reveal(netuid, tempo, 0); - SubtensorModule::set_max_allowed_uids(netuid, n); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - SubtensorModule::set_max_registrations_per_block(netuid, n); - SubtensorModule::set_target_registrations_per_interval(netuid, n); - SubtensorModule::set_min_allowed_weights(netuid, 0); - - // === Register [validator, server] - for key in 0..n as u64 { - add_balance_to_coldkey_account( - &U256::from(key), - ExistentialDeposit::get() + (SubtensorModule::get_network_min_lock() * 2.into()), - ); - let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( - netuid, - block_number, - key * 1_000_000, - &U256::from(key), - ); - assert_ok!(SubtensorModule::register( - RuntimeOrigin::signed(U256::from(key)), - netuid, - block_number, - nonce, - work, - U256::from(key), - U256::from(key) - )); - } - for validator in 0..(n / 2) as u64 { - add_balance_to_coldkey_account(&U256::from(validator), stake.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &U256::from(validator), - &U256::from(validator), - netuid, - stake.into(), - ); - } - assert_eq!(SubtensorModule::get_subnetwork_n(netuid), n); - - // === No weights - if sparse { - SubtensorModule::epoch(netuid, 1_000_000_000.into()); - } else { - SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); - } - /* current_block: 0; activity_cutoff: 5000; Last update: [0, 0]; Inactive: [false, false] - S: [1, 0]; S (mask): [1, 0]; S (mask+norm): [1, 0]; Block at registration: [0, 0] - W: [[], []]; W (diagmask): [[], []]; W (diag+outdatemask): [[], []]; W (mask+norm): [[], []] - R: [0, 0]; W (threshold): [[], []]; T: [0, 0]; C: [0.006693358, 0.006693358]; I: [0, 0] - B: [[], []]; B (mask+norm): [[], []]; - ΔB: [[], []]; ΔB (norm): [[], []]; emaB: [[], []]; D: [0, 0] - E: [1000000000, 0]; P: [1, 0] */ - for validator in 0..(n / 2) { - assert_eq!( - SubtensorModule::get_emission_for_uid(netuid, validator), - 1000000000.into() - ); // Note E = 1 * 1_000_000_000 - } - for server in (n / 2)..n { - assert_eq!( - SubtensorModule::get_emission_for_uid(netuid, server), - 0.into() - ); - // no stake - } - run_to_block(1); - block_number += 1; // run to next block to ensure weights are set on nodes after their registration block - - // === Self-weights only: set weights [srv->srv: 1] - for uid in ((n / 2) as u64)..n as u64 { - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(U256::from(uid)), - netuid, - vec![uid as u16], - vec![u16::MAX], - 0 - )); // server self-weight - } - if sparse { - SubtensorModule::epoch(netuid, 1_000_000_000.into()); - } else { - SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); - } - /* current_block: 1; activity_cutoff: 5000; Last update: [0, 1]; Inactive: [false, false] - S: [1, 0]; S (mask): [1, 0]; S (mask+norm): [1, 0]; Block at registration: [0, 0] - W: [[], [(1, 1)]] - W (diagmask): [[], []]; W (diag+outdatemask): [[], []]; W (mask+norm): [[], []] - R: [0, 0]; W (threshold): [[], []]; T: [0, 0]; C: [0.006693358, 0.006693358]; I: [0, 0] - B: [[], []]: B (mask+norm): [[], []] - ΔB: [[], []]; ΔB (norm): [[], []]; emaB: [[], []]; D: [0, 0] - E: [1000000000, 0]; P: [1, 0] */ - for validator in 0..(n / 2) { - assert_eq!( - SubtensorModule::get_emission_for_uid(netuid, validator), - 1000000000.into() - ); // Note E = 1 * 1_000_000_000 - } - for server in (n / 2)..n { - assert_eq!( - SubtensorModule::get_emission_for_uid(netuid, server), - 0.into() - ); - // no stake - } - run_to_block(2); - block_number += 1; - - // === Set weights [val->srv: 1/(n/2)] - for uid in 0..(n / 2) as u64 { - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(U256::from(uid)), - netuid, - ((n / 2)..n).collect(), - vec![u16::MAX / (n / 2); (n / 2) as usize], - 0 - )); - } - - // === Outdate weights by reregistering servers - for new_key in n..n + (n / 2) { - // register a new key while at max capacity, which means the least emission uid will be deregistered - add_balance_to_coldkey_account( - &U256::from(new_key), - ExistentialDeposit::get() + (SubtensorModule::get_network_min_lock() * 2.into()), - ); - let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( - netuid, - block_number, - new_key as u64 * 1_000_000, - &(U256::from(new_key)), - ); - assert_ok!(SubtensorModule::register( - RuntimeOrigin::signed(U256::from(new_key)), - netuid, - block_number, - nonce, - work, - U256::from(new_key), - U256::from(new_key) - )); - } - if sparse { - SubtensorModule::epoch(netuid, 1_000_000_000.into()); - } else { - SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); - } - /* current_block: 2; activity_cutoff: 5000; Last update: [2, 1]; Inactive: [false, false]; - S: [1, 0]; S (mask): [1, 0]; S (mask+norm): [1, 0]; Block at registration: [0, 2]; - W: [[(1, 1)], []]; W (diagmask): [[(1, 1)], []]; W (diag+outdatemask): [[], []]; W (mask+norm): [[], []]; - R: [0, 0]; W (threshold): [[], []]; T: [0, 0]; C: [0.006693358, 0.006693358]; I: [0, 0]; - B: [[], []]; B (mask+norm): [[], []]; - ΔB: [[], []]; ΔB (norm): [[], []]; emaB: [[], []]; D: [0, 0]; - E: [1000000000, 0]; P: [1, 0] */ - for validator in 0..(n / 2) { - assert_eq!( - SubtensorModule::get_emission_for_uid(netuid, validator), - 1000000000.into() - ); // Note E = 1 * 1_000_000_000 - } - for server in (n / 2)..n { - assert_eq!( - SubtensorModule::get_emission_for_uid(netuid, server), - 0.into() - ); - // no stake - } - run_to_block(3); - - // === Set new weights [val->srv: 1/(n/2)] to check that updated weights would produce non-zero incentive - for uid in 0..(n / 2) as u64 { - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(U256::from(uid)), - netuid, - ((n / 2)..n).collect(), - vec![u16::MAX / (n / 2); (n / 2) as usize], - 0 - )); - } - if sparse { - SubtensorModule::epoch(netuid, 1_000_000_000.into()); - } else { - SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); - } - /* current_block: 3; activity_cutoff: 5000; Last update: [3, 1]; Inactive: [false, false]; - S: [1, 0]; S (mask): [1, 0]; S (mask+norm): [1, 0]; Block at registration: [0, 2]; - W: [[(1, 1)], []]; W (diagmask): [[(1, 1)], []]; W (diag+outdatemask): [[(1, 1)], []]; W (mask+norm): [[(1, 1)], []]; - R: [0, 1]; W (threshold): [[(1, 1)], []]; T: [0, 1]; C: [0.006693358, 0.9933076561]; I: [0, 1]; - B: [[], []]; B (mask+norm): [[], []]; - ΔB: [[(1, 1)], []]; ΔB (norm): [[(1, 1)], []]; emaB: [[(1, 1)], []]; D: [1, 0]; emaB (max-upscale): [[(1, 1)], []] - E: [500000000, 500000000]; P: [0.5, 0.5] */ - for validator in 0..n { - assert_eq!( - SubtensorModule::get_emission_for_uid(netuid, validator), - (1000000000 / (n as u64)).into() - ); // Note E = 1/2 * 1_000_000_000 - } - }); -} - -// Test that recently/deregistered miner bonds are cleared before EMA. -#[test] -fn test_deregistered_miner_bonds() { - new_test_ext(1).execute_with(|| { - let sparse: bool = true; - let n: u16 = 4; - let netuid = NetUid::from(1); - let high_tempo: u16 = u16::MAX - 1; // high tempo to skip automatic epochs in on_initialize, use manual epochs instead - - let stake: TaoBalance = 1.into(); - add_network_disable_commit_reveal(netuid, high_tempo, 0); - SubtensorModule::set_max_allowed_uids(netuid, n); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - SubtensorModule::set_max_registrations_per_block(netuid, n); - SubtensorModule::set_target_registrations_per_interval(netuid, n); - SubtensorModule::set_min_allowed_weights(netuid, 0); - SubtensorModule::set_bonds_penalty(netuid, u16::MAX); - assert_eq!(SubtensorModule::get_registrations_this_block(netuid), 0); - - // === Register [validator1, validator2, server1, server2] - let block_number = System::block_number(); - for key in 0..n as u64 { - add_balance_to_coldkey_account( - &U256::from(key), - stake - + ExistentialDeposit::get() - + (SubtensorModule::get_network_min_lock() * 2.into()), - ); - let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( - netuid, - block_number, - key * 1_000_000, - &U256::from(key), - ); - assert_ok!(SubtensorModule::register( - RuntimeOrigin::signed(U256::from(key)), - netuid, - block_number, - nonce, - work, - U256::from(key), - U256::from(key) - )); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &U256::from(key), - &U256::from(key), - netuid, - AlphaBalance::from(stake.to_u64()), - ); - } - assert_eq!(SubtensorModule::get_subnetwork_n(netuid), n); - assert_eq!(SubtensorModule::get_registrations_this_block(netuid), 4); - - // === Issue validator permits - SubtensorModule::set_max_allowed_validators(netuid, n); - assert_eq!(SubtensorModule::get_max_allowed_validators(netuid), n); - SubtensorModule::epoch(netuid, 1_000_000_000.into()); // run first epoch to set allowed validators - assert_eq!(SubtensorModule::get_registrations_this_block(netuid), 4); - next_block(); // run to next block to ensure weights are set on nodes after their registration block - assert_eq!(SubtensorModule::get_registrations_this_block(netuid), 0); - - // === Set weights [val1->srv1: 2/3, val1->srv2: 1/3, val2->srv1: 2/3, val2->srv2: 1/3] - for uid in 0..(n / 2) as u64 { - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(U256::from(uid)), - netuid, - ((n / 2)..n).collect(), - vec![2 * (u16::MAX / 3), u16::MAX / 3], - 0 - )); - } - - // Set tempo high so we don't automatically run epochs - SubtensorModule::set_tempo_unchecked(netuid, high_tempo); - - // Run 2 blocks - next_block(); - next_block(); - - // set tempo to 2 blocks - SubtensorModule::set_tempo_unchecked(netuid, 2); - // Run epoch - if sparse { - SubtensorModule::epoch(netuid, 1_000_000_000.into()); - } else { - SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); - } - - // Check the bond values for the servers - let bonds = SubtensorModule::get_bonds(netuid.into()); - let bond_0_2 = bonds[0][2]; - let bond_0_3 = bonds[0][3]; - - // Non-zero bonds - assert!(bond_0_2 > 0); - assert!(bond_0_3 > 0); - - // Set tempo high so we don't automatically run epochs - SubtensorModule::set_tempo_unchecked(netuid, high_tempo); - - // Run one more block - next_block(); - - // === Dereg server2 at uid3 (least emission) + register new key over uid3 - let new_key: u64 = n as u64; // register a new key while at max capacity, which means the least incentive uid will be deregistered - let block_number = System::block_number(); - add_balance_to_coldkey_account( - &U256::from(new_key), - stake - + ExistentialDeposit::get() - + (SubtensorModule::get_network_min_lock() * 2.into()), - ); - let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( - netuid, - block_number, - 0, - &U256::from(new_key), - ); - assert_eq!(SubtensorModule::get_max_registrations_per_block(netuid), n); - assert_eq!(SubtensorModule::get_registrations_this_block(netuid), 0); - assert_ok!(SubtensorModule::register( - RuntimeOrigin::signed(U256::from(new_key)), - netuid, - block_number, - nonce, - work, - U256::from(new_key), - U256::from(new_key) - )); - let deregistered_uid: u16 = n - 1; // since uid=n-1 only recieved 1/3 of weight, it will get pruned first - assert_eq!( - U256::from(new_key), - SubtensorModule::get_hotkey_for_net_and_uid(netuid, deregistered_uid) - .expect("Not registered") - ); - - // Set weights again so they're active. - for uid in 0..(n / 2) as u64 { - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(U256::from(uid)), - netuid, - ((n / 2)..n).collect(), - vec![2 * (u16::MAX / 3), u16::MAX / 3], - 0 - )); - } - - // Run 1 block - next_block(); - // Assert block at registration happened after the last tempo - let block_at_registration = SubtensorModule::get_neuron_block_at_registration(netuid, 3); - let block_number = System::block_number(); - assert!( - block_at_registration >= block_number - 2, - "block at registration: {block_at_registration}, block number: {block_number}" - ); - - // set tempo to 2 blocks - SubtensorModule::set_tempo_unchecked(netuid, 2); - // Run epoch again. - if sparse { - SubtensorModule::epoch(netuid, 1_000_000_000.into()); - } else { - SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); - } - - // Check the bond values for the servers - let bonds = SubtensorModule::get_bonds(netuid.into()); - let bond_0_2_new = bonds[0][2]; - let bond_0_3_new = bonds[0][3]; - - // We expect the old bonds for server2, (uid3), to be reset. - // For server1, (uid2), the bond should be higher than before. - assert!( - bond_0_2_new >= bond_0_2, - "bond_0_2_new: {bond_0_2_new}, bond_0_2: {bond_0_2}" - ); - assert!( - bond_0_3_new <= bond_0_3, - "bond_0_3_new: {bond_0_3_new}, bond_0_3: {bond_0_3}" - ); - }); -} - -// Test that epoch assigns validator permits to highest stake uids that are over the stake threshold, varies uid interleaving and stake values. -#[test] -fn test_validator_permits() { - let netuid = NetUid::from(1); - let tempo: u16 = u16::MAX - 1; // high tempo to skip automatic epochs in on_initialize, use manual epochs instead - for interleave in 0..3 { - for (network_n, validators_n) in [(2, 1), (4, 2), (8, 4)] { - let min_stake = validators_n as u64; - for assignment in 0..=1 { - let (validators, servers) = - distribute_nodes(validators_n as usize, network_n, interleave as usize); - let correct: bool = true; - let mut stake: Vec = vec![0.into(); network_n]; - for validator in &validators { - stake[*validator as usize] = match assignment { - 1 => TaoBalance::from(*validator) + network_n.into(), - _ => 1.into(), - }; - } - for server in &servers { - stake[*server as usize] = match assignment { - 1 => TaoBalance::from(*server), - _ => 0.into(), - }; - } - new_test_ext(1).execute_with(|| { - let block_number: u64 = 0; - add_network(netuid, tempo, 0); - SubtensorModule::set_max_allowed_uids(netuid, network_n as u16); - assert_eq!( - SubtensorModule::get_max_allowed_uids(netuid), - network_n as u16 - ); - SubtensorModule::set_max_registrations_per_block(netuid, network_n as u16); - SubtensorModule::set_target_registrations_per_interval( - netuid, - network_n as u16, - ); - SubtensorModule::set_stake_threshold(min_stake); - - // === Register [validator1, validator2, server1, server2] - for key in 0..network_n as u64 { - add_balance_to_coldkey_account( - &U256::from(key), - stake[key as usize] - + ExistentialDeposit::get() - + SubtensorModule::get_network_min_lock(), - ); - let (nonce, work): (u64, Vec) = - SubtensorModule::create_work_for_block_number( - netuid, - block_number, - key * 1_000_000, - &U256::from(key), - ); - assert_ok!(SubtensorModule::register( - RuntimeOrigin::signed(U256::from(key)), - netuid, - block_number, - nonce, - work, - U256::from(key), - U256::from(key) - )); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &U256::from(key), - &U256::from(key), - netuid, - stake[key as usize].to_u64().into(), - ); - } - assert_eq!(SubtensorModule::get_subnetwork_n(netuid), network_n as u16); - - // === Issue validator permits - SubtensorModule::set_max_allowed_validators(netuid, validators_n as u16); - assert_eq!( - SubtensorModule::get_max_allowed_validators(netuid), - validators_n as u16 - ); - SubtensorModule::epoch(netuid, 1_000_000_000.into()); // run first epoch to set allowed validators - for validator in &validators { - assert_eq!( - stake[*validator as usize] >= TaoBalance::from(min_stake), - SubtensorModule::get_validator_permit_for_uid(netuid, *validator) - ); - } - for server in &servers { - assert_eq!( - !correct, - SubtensorModule::get_validator_permit_for_uid(netuid, *server) - ); - } - - // === Increase server stake above validators - for server in &servers { - add_balance_to_coldkey_account( - &(U256::from(*server as u64)), - (2 * network_n as u64).into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(*server as u64)), - &(U256::from(*server as u64)), - netuid, - (2 * network_n as u64).into(), - ); - } - - // === Update validator permits - run_to_block(1); - SubtensorModule::epoch(netuid, 1_000_000_000.into()); - - // === Check that servers now own permits instead of the validator uids - for validator in &validators { - assert_eq!( - !correct, - SubtensorModule::get_validator_permit_for_uid(netuid, *validator) - ); - } - for server in &servers { - assert_eq!( - (stake[*server as usize] - + (TaoBalance::from(2) * TaoBalance::from(network_n))) - >= TaoBalance::from(min_stake), - SubtensorModule::get_validator_permit_for_uid(netuid, *server) - ); - } - }); - } - } - } -} - -/// cargo test --package pallet-subtensor --lib -- tests::epoch::test_get_set_alpha --exact --show-output -#[test] -fn test_get_set_alpha() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let alpha_low: u16 = 1638_u16; - let alpha_high: u16 = u16::MAX - 10; - - let hotkey: U256 = U256::from(1); - let coldkey: U256 = U256::from(1 + 456); - let signer = RuntimeOrigin::signed(coldkey); - - // Enable Liquid Alpha and setup - SubtensorModule::set_liquid_alpha_enabled(netuid, true); - migrations::migrate_create_root_network::migrate_create_root_network::(); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_000_u64.into()); - assert_ok!(SubtensorModule::root_register(signer.clone(), hotkey,)); - - // Should fail as signer does not own the subnet - assert_err!( - SubtensorModule::do_set_alpha_values(signer.clone(), netuid, alpha_low, alpha_high), - DispatchError::BadOrigin - ); - - assert_ok!(SubtensorModule::register_network(signer.clone(), hotkey)); - SubtokenEnabled::::insert(netuid, true); - - let fee = ::SwapInterface::approx_fee_amount( - netuid.into(), - DefaultMinStake::::get(), - ); - - assert_ok!(SubtensorModule::add_stake( - signer.clone(), - hotkey, - netuid, - DefaultMinStake::::get() + fee * 2.into() - )); - - assert_ok!(SubtensorModule::do_set_alpha_values( - signer.clone(), - netuid, - alpha_low, - alpha_high - )); - let (grabbed_alpha_low, grabbed_alpha_high): (u16, u16) = - SubtensorModule::get_alpha_values(netuid); - - log::info!("alpha_low: {grabbed_alpha_low:?} alpha_high: {grabbed_alpha_high:?}"); - assert_eq!(grabbed_alpha_low, alpha_low); - assert_eq!(grabbed_alpha_high, alpha_high); - - // Convert the u16 values to decimal values - fn unnormalize_u16_to_float(normalized_value: u16) -> f32 { - const MAX_U16: u16 = 65535; - normalized_value as f32 / MAX_U16 as f32 - } - - let alpha_low_decimal = unnormalize_u16_to_float(alpha_low); - let alpha_high_decimal = unnormalize_u16_to_float(alpha_high); - - let (alpha_low_32, alpha_high_32) = SubtensorModule::get_alpha_values_32(netuid); - - let tolerance: f32 = 1e-6; // 0.000001 - - // Check if the values are equal to the sixth decimal - assert!( - (alpha_low_32.to_num::() - alpha_low_decimal).abs() < tolerance, - "alpha_low mismatch: {} != {}", - alpha_low_32.to_num::(), - alpha_low_decimal - ); - assert!( - (alpha_high_32.to_num::() - alpha_high_decimal).abs() < tolerance, - "alpha_high mismatch: {} != {}", - alpha_high_32.to_num::(), - alpha_high_decimal - ); - - // 1. Liquid alpha disabled - SubtensorModule::set_liquid_alpha_enabled(netuid, false); - assert_err!( - SubtensorModule::do_set_alpha_values(signer.clone(), netuid, alpha_low, alpha_high), - Error::::LiquidAlphaDisabled - ); - // Correct scenario after error - SubtensorModule::set_liquid_alpha_enabled(netuid, true); // Re-enable for further tests - assert_ok!(SubtensorModule::do_set_alpha_values( - signer.clone(), - netuid, - alpha_low, - alpha_high - )); - - // 2. Alpha high too low - let alpha_high_too_low = (u16::MAX as u32 / 40) as u16 - 1; // One less than the minimum acceptable value - assert_err!( - SubtensorModule::do_set_alpha_values( - signer.clone(), - netuid, - alpha_low, - alpha_high_too_low - ), - Error::::AlphaHighTooLow - ); - // Correct scenario after error - assert_ok!(SubtensorModule::do_set_alpha_values( - signer.clone(), - netuid, - alpha_low, - alpha_high - )); - - // 3. Alpha low too low or too high - let alpha_low_too_low = 0_u16; - assert_err!( - SubtensorModule::do_set_alpha_values( - signer.clone(), - netuid, - alpha_low_too_low, - alpha_high - ), - Error::::AlphaLowOutOfRange - ); - // Correct scenario after error - assert_ok!(SubtensorModule::do_set_alpha_values( - signer.clone(), - netuid, - alpha_low, - alpha_high - )); - - let alpha_low_too_high = alpha_high + 1; // alpha_low should be <= alpha_high - assert_err!( - SubtensorModule::do_set_alpha_values( - signer.clone(), - netuid, - alpha_low_too_high, - alpha_high - ), - Error::::AlphaLowOutOfRange - ); - // Correct scenario after error - assert_ok!(SubtensorModule::do_set_alpha_values( - signer.clone(), - netuid, - alpha_low, - alpha_high - )); - }); -} - -#[test] -fn test_blocks_since_last_step() { - new_test_ext(1).execute_with(|| { - System::set_block_number(0); - - let netuid = NetUid::from(1); - let tempo: u16 = 7200; - add_network(netuid, tempo, 0); - - let original_blocks: u64 = SubtensorModule::get_blocks_since_last_step(netuid); - - step_block(5); - - let new_blocks: u64 = SubtensorModule::get_blocks_since_last_step(netuid); - - assert!(new_blocks > original_blocks); - assert_eq!(new_blocks, 5); - - let blocks_to_step: u16 = SubtensorModule::blocks_until_next_auto_epoch( - netuid, - tempo, - SubtensorModule::get_current_block_as_u64(), - ) as u16 - + 10; - step_block(blocks_to_step); - - let post_blocks: u64 = SubtensorModule::get_blocks_since_last_step(netuid); - - assert_eq!(post_blocks, 10); - - let blocks_to_step: u16 = SubtensorModule::blocks_until_next_auto_epoch( - netuid, - tempo, - SubtensorModule::get_current_block_as_u64(), - ) as u16 - + 20; - step_block(blocks_to_step); - - let new_post_blocks: u64 = SubtensorModule::get_blocks_since_last_step(netuid); - - assert_eq!(new_post_blocks, 20); - - step_block(7); - - assert_eq!(SubtensorModule::get_blocks_since_last_step(netuid), 27); - }); -} - -/// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::epoch::test_can_set_self_weight_as_subnet_owner --exact --show-output -#[test] -fn test_can_set_self_weight_as_subnet_owner() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey: U256 = U256::from(1); - let subnet_owner_hotkey: U256 = U256::from(1 + 456); - - let other_hotkey: U256 = U256::from(2); - - let stake = 5_000_000_000_000_u64; // 5k TAO - let to_emit: u64 = 1_000_000_000_u64; // 1 TAO - - // Create subnet - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - // Register the other hotkey - register_ok_neuron(netuid, other_hotkey, subnet_owner_coldkey, 0); - - // Add stake to owner hotkey. - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &subnet_owner_hotkey, - &subnet_owner_coldkey, - netuid, - stake.into(), - ); - - // Give vpermits to owner hotkey ONLY - ValidatorPermit::::insert(netuid, vec![true, false]); - - // Set weight of 50% to each hotkey. - // This includes a self-weight - let fifty_percent: u16 = u16::MAX / 2; - Weights::::insert( - NetUidStorageIndex::from(netuid), - 0, - vec![(0, fifty_percent), (1, fifty_percent)], - ); - - step_block(1); - // Set updated so weights are valid - LastUpdate::::insert(NetUidStorageIndex::from(netuid), vec![2, 0]); - - // Run epoch - let hotkey_emission = SubtensorModule::epoch(netuid, to_emit.into()); - - // hotkey_emission is [(hotkey, incentive, dividend)] - assert_eq!(hotkey_emission.len(), 2); - assert!( - hotkey_emission - .iter() - .any(|(hk, _, _)| *hk == subnet_owner_hotkey) - ); - assert!(hotkey_emission.iter().any(|(hk, _, _)| *hk == other_hotkey)); - - log::debug!("hotkey_emission: {hotkey_emission:?}"); - // Both should have received incentive emission - assert!(hotkey_emission[0].1 > 0.into()); - assert!(hotkey_emission[1].1 > 0.into()); - - // Their incentive should be equal - assert_eq!(hotkey_emission[0].1, hotkey_emission[1].1); - }); -} - -#[test] -fn test_epoch_outputs_single_staker_registered_no_weights() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let high_tempo: u16 = u16::MAX - 1; // Don't run automatically. - add_network(netuid, high_tempo, 0); - - let hotkey = U256::from(1); - let coldkey = U256::from(2); - register_ok_neuron(netuid, hotkey, coldkey, 0); - // Give non-zero alpha - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - netuid, - 1.into(), - ); - - let pending_alpha = AlphaBalance::from(1_000_000_000); - let hotkey_emission = SubtensorModule::epoch(netuid, pending_alpha); - - let sum_incentives = hotkey_emission - .iter() - .map(|(_, incentive, _)| incentive) - .copied() - .fold(AlphaBalance::ZERO, |acc, x| acc + x); - let sum_dividends: AlphaBalance = hotkey_emission - .iter() - .map(|(_, _, dividend)| dividend) - .copied() - .fold(AlphaBalance::ZERO, |acc, x| acc + x); - - assert_abs_diff_eq!( - sum_incentives.saturating_add(sum_dividends), - pending_alpha, - epsilon = 1_000.into() - ); - }); -} - -// Map the retention graph for consensus guarantees with an single epoch on a graph with 512 nodes, -// of which the first 64 are validators, the graph is split into a major and minor set, each setting -// specific weight on itself and the complement on the other. -// -// ```import torch -// import matplotlib.pyplot as plt -// from matplotlib.pyplot import cm -// %matplotlib inline -// -// with open('finney_consensus_0.4.txt') as f: # test output saved to finney_consensus.txt -// retention_map = eval(f.read()) -// -// major_ratios = {} -// avg_weight_devs = {} -// for major_stake, major_weight, minor_weight, avg_weight_dev, major_ratio in retention_map: -// major_stake = f'{major_stake:.2f}' -// maj, min = int(round(50 * major_weight)), int(round(50 * minor_weight)) -// avg_weight_devs.setdefault(major_stake, torch.zeros((51, 51))) -// avg_weight_devs[major_stake][maj][min] = avg_weight_dev -// major_ratios.setdefault(major_stake, torch.zeros((51, 51))) -// major_ratios[major_stake][maj][min] = major_ratio -// -// _x = torch.linspace(0, 1, 51); _y = torch.linspace(0, 1, 51) -// x, y = torch.meshgrid(_x, _y, indexing='ij') -// -// fig = plt.figure(figsize=(6, 6), dpi=70); ax = fig.gca() -// ax.set_xticks(torch.arange(0, 1, 0.05)); ax.set_yticks(torch.arange(0, 1., 0.05)) -// ax.set_xticklabels([f'{_:.2f}'[1:] for _ in torch.arange(0, 1., 0.05)]) -// plt.grid(); plt.rc('grid', linestyle="dotted", color=[0.85, 0.85, 0.85]) -// -// isolate = ['0.60']; stakes = [0.51, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99] -// colors = cm.viridis(torch.linspace(0, 1, len(stakes) + 1)) -// for i, stake in enumerate(stakes): -// contours = plt.contour(x, y, major_ratios[f'{stake:.2f}'], levels=[0., stake], colors=[colors[i + 1]]) -// if f'{stake:.2f}' in isolate: -// contours.collections[1].set_linewidth(3) -// plt.clabel(contours, inline=True, fontsize=10) -// -// plt.title(f'Major emission [$stake_{{maj}}=emission_{{maj}}$ retention lines]') -// plt.ylabel('Minor self-weight'); plt.xlabel('Major self-weight'); plt.show() -// ``` -// #[test] -// fn _map_consensus_guarantees() { -// let netuid = NetUid::from(1); -// let network_n: u16 = 512; -// let validators_n: u16 = 64; -// let epochs: u16 = 1; -// let interleave = 0; -// let weight_stddev: I32F32 = fixed(0.4); -// let bonds_penalty: u16 = u16::MAX; -// println!("["); -// for _major_stake in vec![0.51, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99] { -// let major_stake: I32F32 = I32F32::from_num(_major_stake); -// for _major_weight in 0..51 { -// let major_weight: I32F32 = I32F32::from_num(50 - _major_weight) / I32F32::from_num(50); -// for _minor_weight in 0..51 { -// let minor_weight: I32F32 = -// I32F32::from_num(50 - _minor_weight) / I32F32::from_num(50); -// let ( -// validators, -// servers, -// major_validators, -// minor_validators, -// major_servers, -// minor_servers, -// stake, -// weights, -// avg_weight_dev, -// ) = split_graph( -// major_stake, -// major_weight, -// minor_weight, -// weight_stddev, -// validators_n as usize, -// network_n as usize, -// interleave as usize, -// ); -// -// new_test_ext(1).execute_with(|| { -// init_run_epochs(netuid, network_n, &validators, &servers, epochs, 1, true, &stake, true, &weights, true, false, 0, true, bonds_penalty); -// -// let mut major_emission: I64F64 = I64F64::from_num(0); -// let mut minor_emission: I64F64 = I64F64::from_num(0); -// for set in vec![major_validators, major_servers] { -// for uid in set { -// major_emission += I64F64::from_num(SubtensorModule::get_emission_for_uid( netuid, uid )); -// } -// } -// for set in vec![minor_validators, minor_servers] { -// for uid in set { -// minor_emission += I64F64::from_num(SubtensorModule::get_emission_for_uid( netuid, uid )); -// } -// } -// let major_ratio: I32F32 = I32F32::from_num(major_emission / (major_emission + minor_emission)); -// println!("[{major_stake}, {major_weight:.2}, {minor_weight:.2}, {avg_weight_dev:.3}, {major_ratio:.3}], "); -// }); -// } -// } -// } -// println!("]"); -// } - -// Helpers - -/// Asserts that two I32F32 values are approximately equal within a given epsilon. -/// -/// # Arguments -/// * `left` - The first value to compare. -/// * `right` - The second value to compare. -/// * `epsilon` - The maximum allowed difference between the two values. -pub fn assert_approx_eq(left: I32F32, right: I32F32, epsilon: I32F32) { - if (left - right).abs() > epsilon { - panic!( - "assertion failed: `(left ≈ right)`\n left: `{left:?}`,\n right: `{right:?}`,\n epsilon: `{epsilon:?}`" - ); - } -} - -// test Yuma 3 scenarios over a sequence of epochs. -fn setup_yuma_3_scenario(netuid: NetUid, n: u16, sparse: bool, max_stake: u64, stakes: Vec) { - let block_number = System::block_number(); - let tempo: u16 = 1; // high tempo to skip automatic epochs in on_initialize, use manual epochs instead - add_network_disable_commit_reveal(netuid, tempo, 0); - - SubtensorModule::set_max_allowed_uids(netuid, n); - assert_eq!(SubtensorModule::get_max_allowed_uids(netuid), n); - SubtensorModule::set_max_registrations_per_block(netuid, n); - SubtensorModule::set_target_registrations_per_interval(netuid, n); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - SubtensorModule::set_min_allowed_weights(netuid, 1); - SubtensorModule::set_bonds_penalty(netuid, 0); - SubtensorModule::set_alpha_sigmoid_steepness(netuid, 1000); - SubtensorModule::set_bonds_moving_average(netuid, 975_000); - - // === Register - for key in 0..n as u64 { - add_balance_to_coldkey_account( - &U256::from(key), - TaoBalance::from(max_stake) - + ExistentialDeposit::get() - + SubtensorModule::get_network_min_lock(), - ); - let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( - netuid, - block_number, - key * 1_000_000, - &U256::from(key), - ); - assert_ok!(SubtensorModule::register( - <::RuntimeOrigin>::signed(U256::from(key)), - netuid, - block_number, - nonce, - work, - U256::from(key), - U256::from(key) - )); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &U256::from(key), - &U256::from(key), - netuid, - stakes[key as usize].into(), - ); - } - assert_eq!(SubtensorModule::get_max_allowed_uids(netuid), n); - assert_eq!(SubtensorModule::get_subnetwork_n(netuid), n); - - // Enable Liquid Alpha - SubtensorModule::set_kappa(netuid, u16::MAX / 2); - SubtensorModule::set_liquid_alpha_enabled(netuid, true); - SubtensorModule::set_alpha_values_32(netuid, I32F32::from_num(0.1), I32F32::from_num(0.3)); - - // Enable Yuma3 - SubtensorModule::set_yuma3_enabled(netuid, true); - - // === Issue validator permits - SubtensorModule::set_max_allowed_validators(netuid, 3); - - // run first epoch to set allowed validators - // run to next block to ensure weights are set on nodes after their registration block - run_epoch(netuid, sparse); -} - -fn run_epoch(netuid: NetUid, sparse: bool) { - next_block_no_epoch(netuid); - if sparse { - SubtensorModule::epoch(netuid, 1_000_000_000.into()); - } else { - SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); - } -} - -fn run_epoch_and_check_bonds_dividends( - netuid: NetUid, - sparse: bool, - target_bonds: &[Vec], - target_dividends: &[f32], -) { - run_epoch(netuid, sparse); - let bonds = SubtensorModule::get_bonds_fixed_proportion(netuid.into()); - let dividends = SubtensorModule::get_dividends(netuid); - - let epsilon = I32F32::from_num(1e-3); - // Check the bonds - for (bond, target_bond) in bonds.iter().zip(target_bonds.iter()) { - // skip the 3 validators - for (b, t) in bond.iter().zip(target_bond.iter().skip(3)) { - assert_approx_eq(*b, fixed(*t), epsilon); - } - } - // Check the dividends - for (dividend, target_dividend) in dividends.iter().zip(target_dividends.iter()) { - assert_approx_eq( - u16_proportion_to_fixed(*dividend), - fixed(*target_dividend), - epsilon, - ); - } -} - -fn set_yuma_3_weights(netuid: NetUid, weights: Vec>, indices: Vec) { - for (uid, weight) in weights.iter().enumerate() { - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(U256::from(uid as u64)), - netuid, - indices.clone(), - weight.to_vec(), - 0 - )); - } -} - -#[test] -fn test_yuma_3_kappa_moves_first() { - for sparse in [true, false].iter() { - new_test_ext(1).execute_with(|| { - let n: u16 = 5; // 3 validators, 2 servers - let netuid = NetUid::from(1); - let max_stake: u64 = 8; - - // Validator A: kappa / Big validator (0.8) - moves first - // Validator B: Small eager validator (0.1) - moves second - // Validator C: Small lazy validator (0.1) - moves last - let stakes: Vec = vec![8, 1, 1, 0, 0]; - - setup_yuma_3_scenario(netuid, n, *sparse, max_stake, stakes); - let targets_bonds = [ - vec![ - vec![0.1013, 0.0000], - vec![0.1013, 0.0000], - vec![0.1013, 0.0000], - ], - vec![ - vec![0.0908, 0.1013], - vec![0.3697, 0.0000], - vec![0.3697, 0.0000], - ], - vec![ - vec![0.0815, 0.1924], - vec![0.3170, 0.1013], - vec![0.5580, 0.0000], - ], - vec![ - vec![0.0731, 0.2742], - vec![0.2765, 0.1924], - vec![0.4306, 0.1013], - ], - vec![ - vec![0.0656, 0.3478], - vec![0.2435, 0.2742], - vec![0.3589, 0.1924], - ], - vec![ - vec![0.0588, 0.4139], - vec![0.2157, 0.3478], - vec![0.3089, 0.2742], - ], - ]; - - let targets_dividends = [ - vec![0.8000, 0.1000, 0.1000, 0.0000, 0.0000], - vec![1.0000, 0.0000, 0.0000, 0.0000, 0.0000], - vec![0.9382, 0.0618, 0.0000, 0.0000, 0.0000], - vec![0.8819, 0.0773, 0.0407, 0.0000, 0.0000], - vec![0.8564, 0.0844, 0.0592, 0.0000, 0.0000], - vec![0.8418, 0.0884, 0.0697, 0.0000, 0.0000], - ]; - - for (epoch, (target_bonds, target_dividends)) in targets_bonds - .iter() - .zip(targets_dividends.iter()) - .enumerate() - { - match epoch { - 0 => { - // Initially, consensus is achieved by all Validators - set_yuma_3_weights(netuid, vec![vec![u16::MAX, 0]; 3], vec![3, 4]); - } - 1 => { - // Validator A -> Server 2 - // Validator B -> Server 1 - // Validator C -> Server 1 - set_yuma_3_weights( - netuid, - vec![vec![0, u16::MAX], vec![u16::MAX, 0], vec![u16::MAX, 0]], - vec![3, 4], - ); - } - 2 => { - // Validator A -> Server 2 - // Validator B -> Server 2 - // Validator C -> Server 1 - set_yuma_3_weights( - netuid, - vec![vec![0, u16::MAX], vec![0, u16::MAX], vec![u16::MAX, 0]], - vec![3, 4], - ); - } - 3 => { - // Subsequent epochs All validators -> Server 2 - set_yuma_3_weights(netuid, vec![vec![0, u16::MAX]; 3], vec![3, 4]); - } - _ => {} - }; - run_epoch_and_check_bonds_dividends( - netuid, - *sparse, - target_bonds, - target_dividends, - ); - } - }) - } -} - -#[test] -fn test_yuma_3_kappa_moves_second() { - for sparse in [true, false].iter() { - new_test_ext(1).execute_with(|| { - let n: u16 = 5; // 3 validators, 2 servers - let netuid = NetUid::from(1); - let max_stake: u64 = 8; - - // Validator A: kappa / Big validator (0.8) - moves second - // Validator B: Small eager validator (0.1) - moves first - // Validator C: Small lazy validator (0.1) - moves last - let stakes: Vec = vec![8, 1, 1, 0, 0]; - - setup_yuma_3_scenario(netuid, n, *sparse, max_stake, stakes); - let targets_bonds = [ - vec![ - vec![0.1013, 0.0000], - vec![0.1013, 0.0000], - vec![0.1013, 0.0000], - ], - vec![ - vec![0.1924, 0.0000], - vec![0.0908, 0.2987], - vec![0.1924, 0.0000], - ], - vec![ - vec![0.1715, 0.1013], - vec![0.0815, 0.3697], - vec![0.4336, 0.0000], - ], - vec![ - vec![0.1531, 0.1924], - vec![0.0731, 0.4336], - vec![0.3608, 0.1013], - ], - vec![ - vec![0.1369, 0.2742], - vec![0.0656, 0.4910], - vec![0.3103, 0.1924], - ], - vec![ - vec![0.1225, 0.3478], - vec![0.0588, 0.5426], - vec![0.2712, 0.2742], - ], - ]; - let targets_dividends = [ - vec![0.8000, 0.1000, 0.1000, 0.0000, 0.0000], - vec![0.8446, 0.0498, 0.1056, 0.0000, 0.0000], - vec![0.6868, 0.3132, 0.0000, 0.0000, 0.0000], - vec![0.7421, 0.2090, 0.0489, 0.0000, 0.0000], - vec![0.7625, 0.1706, 0.0669, 0.0000, 0.0000], - vec![0.7730, 0.1508, 0.0762, 0.0000, 0.0000], - ]; - - for (epoch, (target_bonds, target_dividends)) in targets_bonds - .iter() - .zip(targets_dividends.iter()) - .enumerate() - { - match epoch { - 0 => { - // Initially, consensus is achieved by all Validators - set_yuma_3_weights(netuid, vec![vec![u16::MAX, 0]; 3], vec![3, 4]); - } - 1 => { - // Validator A -> Server 1 - // Validator B -> Server 2 - // Validator C -> Server 1 - set_yuma_3_weights( - netuid, - vec![vec![u16::MAX, 0], vec![0, u16::MAX], vec![u16::MAX, 0]], - vec![3, 4], - ); - } - 2 => { - // Validator A -> Server 2 - // Validator B -> Server 2 - // Validator C -> Server 1 - set_yuma_3_weights( - netuid, - vec![vec![0, u16::MAX], vec![0, u16::MAX], vec![u16::MAX, 0]], - vec![3, 4], - ); - } - 3 => { - // Subsequent epochs All validators -> Server 2 - set_yuma_3_weights(netuid, vec![vec![0, u16::MAX]; 3], vec![3, 4]); - } - _ => {} - }; - run_epoch_and_check_bonds_dividends( - netuid, - *sparse, - target_bonds, - target_dividends, - ); - } - }) - } -} - -#[test] -fn test_yuma_3_kappa_moves_last() { - for sparse in [true, false].iter() { - new_test_ext(1).execute_with(|| { - let n: u16 = 5; // 3 validators, 2 servers - let netuid = NetUid::from(1); - let max_stake: u64 = 8; - - // Validator A: kappa / Big validator (0.8) - moves last - // Validator B: Small eager validator (0.1) - moves first - // Validator C: Small lazy validator (0.1) - moves second - let stakes: Vec = vec![8, 1, 1, 0, 0]; - - setup_yuma_3_scenario(netuid, n, *sparse, max_stake, stakes); - let targets_bonds = [ - vec![ - vec![0.1013, 0.0000], - vec![0.1013, 0.0000], - vec![0.1013, 0.0000], - ], - vec![ - vec![0.1924, 0.0000], - vec![0.0908, 0.2987], - vec![0.1924, 0.0000], - ], - vec![ - vec![0.2742, 0.0000], - vec![0.0815, 0.5081], - vec![0.1715, 0.2987], - ], - vec![ - vec![0.2416, 0.1013], - vec![0.0731, 0.5580], - vec![0.1531, 0.3697], - ], - vec![ - vec![0.2141, 0.1924], - vec![0.0656, 0.6028], - vec![0.1369, 0.4336], - ], - vec![ - vec![0.1903, 0.2742], - vec![0.0588, 0.6430], - vec![0.1225, 0.4910], - ], - ]; - let targets_dividends = [ - vec![0.8000, 0.1000, 0.1000, 0.0000, 0.0000], - vec![0.8446, 0.0498, 0.1056, 0.0000, 0.0000], - vec![0.8966, 0.0333, 0.0701, 0.0000, 0.0000], - vec![0.4663, 0.3210, 0.2127, 0.0000, 0.0000], - vec![0.5976, 0.2340, 0.1683, 0.0000, 0.0000], - vec![0.6592, 0.1932, 0.1475, 0.0000, 0.0000], - ]; - - for (epoch, (target_bonds, target_dividends)) in targets_bonds - .iter() - .zip(targets_dividends.iter()) - .enumerate() - { - match epoch { - 0 => { - // Initially, consensus is achieved by all Validators - set_yuma_3_weights(netuid, vec![vec![u16::MAX, 0]; 3], vec![3, 4]); - } - 1 => { - // Validator A -> Server 1 - // Validator B -> Server 2 - // Validator C -> Server 1 - set_yuma_3_weights( - netuid, - vec![vec![u16::MAX, 0], vec![0, u16::MAX], vec![u16::MAX, 0]], - vec![3, 4], - ); - } - 2 => { - // Validator A -> Server 1 - // Validator B -> Server 2 - // Validator C -> Server 2 - set_yuma_3_weights( - netuid, - vec![vec![u16::MAX, 0], vec![0, u16::MAX], vec![0, u16::MAX]], - vec![3, 4], - ); - } - 3 => { - // Subsequent epochs All validators -> Server 2 - set_yuma_3_weights(netuid, vec![vec![0, u16::MAX]; 3], vec![3, 4]); - } - _ => {} - }; - run_epoch_and_check_bonds_dividends( - netuid, - *sparse, - target_bonds, - target_dividends, - ); - } - }) - } -} - -#[test] -fn test_yuma_3_one_epoch_switch() { - for sparse in [true, false].iter() { - new_test_ext(1).execute_with(|| { - let n: u16 = 5; // 3 validators, 2 servers - let netuid = NetUid::from(1); - let max_stake: u64 = 8; - - // Equal stake validators - let stakes: Vec = vec![33, 33, 34, 0, 0]; - - setup_yuma_3_scenario(netuid, n, *sparse, max_stake, stakes); - - let targets_bonds = [ - vec![ - vec![0.1013, 0.0000], - vec![0.1013, 0.0000], - vec![0.1013, 0.0000], - ], - vec![ - vec![0.1924, 0.0000], - vec![0.1924, 0.0000], - vec![0.1924, 0.0000], - ], - vec![ - vec![0.2742, 0.0000], - vec![0.2742, 0.0000], - vec![0.1715, 0.2987], - ], - vec![ - vec![0.3478, 0.0000], - vec![0.3478, 0.0000], - vec![0.2554, 0.2618], - ], - vec![ - vec![0.4139, 0.0000], - vec![0.4139, 0.0000], - vec![0.3309, 0.2312], - ], - vec![ - vec![0.4733, 0.0000], - vec![0.4733, 0.0000], - vec![0.3987, 0.2051], - ], - ]; - let targets_dividends = [ - vec![0.3300, 0.3300, 0.3400, 0.0000, 0.0000], - vec![0.3300, 0.3300, 0.3400, 0.0000, 0.0000], - vec![0.3782, 0.3782, 0.2436, 0.0000, 0.0000], - vec![0.3628, 0.3628, 0.2745, 0.0000, 0.0000], - vec![0.3541, 0.3541, 0.2917, 0.0000, 0.0000], - vec![0.3487, 0.3487, 0.3026, 0.0000, 0.0000], - ]; - - for (epoch, (target_bonds, target_dividends)) in targets_bonds - .iter() - .zip(targets_dividends.iter()) - .enumerate() - { - match epoch { - 2 => { - // Validator A -> Server 1 - // Validator B -> Server 1 - // Validator C -> Server 2 - set_yuma_3_weights( - netuid, - vec![vec![u16::MAX, 0], vec![u16::MAX, 0], vec![0, u16::MAX]], - vec![3, 4], - ); - } - _ => { - // All validators -> Server 1 - set_yuma_3_weights(netuid, vec![vec![u16::MAX, 0]; 3], vec![3, 4]); - } - }; - run_epoch_and_check_bonds_dividends( - netuid, - *sparse, - target_bonds, - target_dividends, - ); - } - }) - } -} - -#[test] -fn test_yuma_3_liquid_alpha_disabled() { - for sparse in [true, false].iter() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let n: u16 = 5; // 3 validators, 2 servers - let max_stake: u64 = 8; - - // Equal stake validators - let stakes: Vec = vec![33, 33, 34, 0, 0]; - - setup_yuma_3_scenario(netuid, n, *sparse, max_stake, stakes); - - // disable liquid alpha - SubtensorModule::set_liquid_alpha_enabled(netuid, false); - - let targets_bonds = [ - vec![ - vec![0.0000, 0.0250, 0.0000], - vec![0.0000, 0.0250, 0.0000], - vec![0.0000, 0.0250, 0.0000], - ], - vec![ - vec![0.0000, 0.0494, 0.0000], - vec![0.0000, 0.0494, 0.0000], - vec![0.0000, 0.0494, 0.0000], - ], - vec![ - vec![0.0000, 0.0731, 0.0000], - vec![0.0000, 0.0731, 0.0000], - vec![0.0000, 0.0481, 0.0250], - ], - vec![ - vec![0.0000, 0.0963, 0.0000], - vec![0.0000, 0.0963, 0.0000], - vec![0.0000, 0.0719, 0.0244], - ], - vec![ - vec![0.0000, 0.1189, 0.0000], - vec![0.0000, 0.1189, 0.0000], - vec![0.0000, 0.0951, 0.0238], - ], - vec![ - vec![0.0000, 0.1409, 0.0000], - vec![0.0000, 0.1409, 0.0000], - vec![0.0000, 0.1178, 0.0232], - ], - ]; - let targets_dividends = [ - vec![0.3300, 0.3300, 0.3400, 0.0000, 0.0000], - vec![0.3300, 0.3300, 0.3400, 0.0000, 0.0000], - vec![0.3734, 0.3734, 0.2532, 0.0000, 0.0000], - vec![0.3611, 0.3611, 0.2779, 0.0000, 0.0000], - vec![0.3541, 0.3541, 0.2919, 0.0000, 0.0000], - vec![0.3495, 0.3495, 0.3009, 0.0000, 0.0000], - ]; - - for (epoch, (target_bonds, target_dividends)) in targets_bonds - .iter() - .zip(targets_dividends.iter()) - .enumerate() - { - match epoch { - 2 => { - // Validator A -> Server 1 - // Validator B -> Server 1 - // Validator C -> Server 2 - set_yuma_3_weights( - netuid, - vec![vec![u16::MAX, 0], vec![u16::MAX, 0], vec![0, u16::MAX]], - vec![3, 4], - ); - } - _ => { - // All validators -> Server 1 - set_yuma_3_weights(netuid, vec![vec![u16::MAX, 0]; 3], vec![3, 4]); - } - }; - run_epoch_and_check_bonds_dividends( - netuid, - *sparse, - target_bonds, - target_dividends, - ); - } - }) - } -} - -#[test] -fn test_yuma_3_stable_miner() { - for sparse in [true, false].iter() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let n: u16 = 6; // 3 validators, 3 servers - let max_stake: u64 = 8; - - // Validator A: kappa / Big validator (0.8) - // Validator B: Small eager validator (0.1) - // Validator C: Small lazy validator (0.1) - let stakes: Vec = vec![8, 1, 1, 0, 0, 0]; - - setup_yuma_3_scenario(netuid, n, *sparse, max_stake, stakes); - let targets_bonds = [ - vec![ - vec![0.0507, 0.0000, 0.0507], - vec![0.0507, 0.0000, 0.0507], - vec![0.0507, 0.0000, 0.0507], - ], - vec![ - vec![0.0962, 0.0000, 0.0962], - vec![0.0455, 0.1000, 0.0962], - vec![0.0962, 0.0000, 0.0962], - ], - vec![ - vec![0.0863, 0.0507, 0.1371], - vec![0.0408, 0.1405, 0.1371], - vec![0.1770, 0.0000, 0.1371], - ], - vec![ - vec![0.0774, 0.0962, 0.1739], - vec![0.0367, 0.1770, 0.1739], - vec![0.1579, 0.0507, 0.1739], - ], - vec![ - vec![0.0694, 0.1371, 0.2069], - vec![0.0329, 0.2097, 0.2069], - vec![0.1411, 0.0962, 0.2069], - ], - vec![ - vec![0.0623, 0.1739, 0.2366], - vec![0.0296, 0.2391, 0.2366], - vec![0.1263, 0.1371, 0.2366], - ], - ]; - let targets_dividends = [ - vec![0.8000, 0.1000, 0.1000, 0.0000, 0.0000, 0.0000], - vec![0.8226, 0.0745, 0.1028, 0.0000, 0.0000, 0.0000], - vec![0.7750, 0.1685, 0.0565, 0.0000, 0.0000, 0.0000], - vec![0.7864, 0.1372, 0.0764, 0.0000, 0.0000, 0.0000], - vec![0.7912, 0.1241, 0.0847, 0.0000, 0.0000, 0.0000], - vec![0.7937, 0.1173, 0.0890, 0.0000, 0.0000, 0.0000], - ]; - - for (epoch, (target_bonds, target_dividends)) in targets_bonds - .iter() - .zip(targets_dividends.iter()) - .enumerate() - { - match epoch { - 0 => { - // all validators 0.5 for first and third server - set_yuma_3_weights( - netuid, - vec![vec![u16::MAX / 2, 0, u16::MAX / 2]; 3], - vec![3, 4, 5], - ); - } - 1 => { - // one of small validators moves 0.5 to seconds server - set_yuma_3_weights( - netuid, - vec![ - vec![u16::MAX / 2, 0, u16::MAX / 2], - vec![0, u16::MAX / 2, u16::MAX / 2], - vec![u16::MAX / 2, 0, u16::MAX / 2], - ], - vec![3, 4, 5], - ); - } - 2 => { - // big validator follows - set_yuma_3_weights( - netuid, - vec![ - vec![0, u16::MAX / 2, u16::MAX / 2], - vec![0, u16::MAX / 2, u16::MAX / 2], - vec![u16::MAX / 2, 0, u16::MAX / 2], - ], - vec![3, 4, 5], - ); - } - 3 => { - // Subsequent epochs all validators have moves - set_yuma_3_weights( - netuid, - vec![vec![0, u16::MAX / 2, u16::MAX / 2]; 3], - vec![3, 4, 5], - ); - } - _ => {} - }; - run_epoch_and_check_bonds_dividends( - netuid, - *sparse, - target_bonds, - target_dividends, - ); - } - }) - } -} - -#[test] -fn test_yuma_3_bonds_reset() { - new_test_ext(1).execute_with(|| { - let sparse: bool = true; - let n: u16 = 5; // 3 validators, 2 servers - let netuid = NetUid::from(1); - let max_stake: u64 = 8; - - // "Case 8 - big vali moves late, then late" - // Big dishonest lazy vali. (0.8) - // Small eager-eager vali. (0.1) - // Small eager-eager vali 2. (0.1) - let stakes: Vec = vec![8, 1, 1, 0, 0]; - - setup_yuma_3_scenario(netuid, n, sparse, max_stake, stakes); - SubtensorModule::set_bonds_reset(netuid, true); - - // target bonds and dividends for specific epoch - let targets_dividends: std::collections::HashMap<_, _> = [ - (0, vec![0.8000, 0.1000, 0.1000, 0.0000, 0.0000]), - (1, vec![0.8944, 0.0528, 0.0528, 0.0000, 0.0000]), - (2, vec![0.5230, 0.2385, 0.2385, 0.0000, 0.0000]), - (19, vec![0.7919, 0.1040, 0.1040, 0.0000, 0.0000]), - (20, vec![0.7928, 0.1036, 0.1036, 0.0000, 0.0000]), - (21, vec![0.8467, 0.0766, 0.0766, 0.0000, 0.0000]), - (40, vec![0.7928, 0.1036, 0.1036, 0.0000, 0.0000]), - ] - .into_iter() - .collect(); - let targets_bonds: std::collections::HashMap<_, _> = [ - ( - 0, - vec![ - vec![0.1013, 0.0000], - vec![0.1013, 0.0000], - vec![0.1013, 0.0000], - ], - ), - ( - 1, - vec![ - vec![0.1924, 0.0000], - vec![0.0908, 0.2987], - vec![0.0908, 0.2987], - ], - ), - ( - 2, - vec![ - vec![0.1715, 0.1013], - vec![0.0815, 0.3697], - vec![0.0815, 0.3697], - ], - ), - ( - 19, - vec![ - vec![0.0269, 0.8539], - vec![0.0131, 0.8975], - vec![0.0131, 0.8975], - ], - ), - ( - 20, - vec![ - vec![0.0000, 0.8687], - vec![0.0000, 0.9079], - vec![0.0000, 0.9079], - ], - ), - ( - 21, - vec![ - vec![0.0000, 0.8820], - vec![0.2987, 0.6386], - vec![0.2987, 0.6386], - ], - ), - ( - 40, - vec![ - vec![0.8687, 0.0578], - vec![0.9079, 0.0523], - vec![0.9079, 0.0523], - ], - ), - ] - .into_iter() - .collect(); - - for epoch in 0..=40 { - match epoch { - 0 => { - // All validators -> Server 1 - set_yuma_3_weights(netuid, vec![vec![u16::MAX, 0]; 3], vec![3, 4]); - } - 1 => { - // validators B, C switch - // Validator A -> Server 1 - // Validator B -> Server 2 - // Validator C -> Server 2 - set_yuma_3_weights( - netuid, - vec![vec![u16::MAX, 0], vec![0, u16::MAX], vec![0, u16::MAX]], - vec![3, 4], - ); - } - (2..=20) => { - // validator A copies weights - // All validators -> Server 2 - set_yuma_3_weights(netuid, vec![vec![0, u16::MAX]; 3], vec![3, 4]); - if epoch == 20 { - let hotkey = SubtensorModule::get_hotkey_for_net_and_uid(netuid, 3) - .expect("Hotkey not found"); - let _ = SubtensorModule::do_reset_bonds(netuid.into(), &hotkey); - } - } - 21 => { - // validators B, C switch back - // Validator A -> Server 2 - // Validator B -> Server 1 - // Validator C -> Server 1 - set_yuma_3_weights( - netuid, - vec![vec![0, u16::MAX], vec![u16::MAX, 0], vec![u16::MAX, 0]], - vec![3, 4], - ); - } - _ => { - // validator A copies weights - // All validators -> Server 1 - set_yuma_3_weights(netuid, vec![vec![u16::MAX, 0]; 3], vec![3, 4]); - } - }; - - if let Some((target_dividend, target_bond)) = - targets_dividends.get(&epoch).zip(targets_bonds.get(&epoch)) - { - run_epoch_and_check_bonds_dividends(netuid, sparse, target_bond, target_dividend); - } else { - run_epoch(netuid, sparse); - } - } - }) -} - -#[test] -fn test_liquid_alpha_equal_values_against_itself() { - new_test_ext(1).execute_with(|| { - // check Liquid alpha disabled against Liquid Alpha enabled with alpha_low == alpha_high - let netuid: NetUid = NetUid::from(1); - let alpha_low = u16::MAX / 10; - let alpha_high = u16::MAX / 10; - let epsilon = I32F32::from_num(1e-3); - let weights: Vec> = vec_to_mat_fixed( - &[0., 0.1, 0., 0., 0.2, 0.4, 0., 0.3, 0.1, 0., 0.4, 0.5], - 4, - false, - ); - let bonds: Vec> = vec_to_mat_fixed( - &[0.1, 0.1, 0.5, 0., 0., 0.4, 0.5, 0.1, 0.1, 0., 0.4, 0.2], - 4, - false, - ); - let consensus: Vec = vec_to_fixed(&[0.3, 0.2, 0.1, 0.4]); - - // set both alpha values to 0.1 and bonds moving average to 0.9 - AlphaValues::::insert(netuid, (alpha_low, alpha_high)); - SubtensorModule::set_bonds_moving_average(netuid.into(), 900_000); - - // compute bonds with liquid alpha enabled - SubtensorModule::set_liquid_alpha_enabled(netuid.into(), true); - let new_bonds_liquid_alpha_on = - SubtensorModule::compute_bonds(netuid.into(), &weights, &bonds, &consensus); - - // compute bonds with liquid alpha disabled - SubtensorModule::set_liquid_alpha_enabled(netuid.into(), false); - let new_bonds_liquid_alpha_off = - SubtensorModule::compute_bonds(netuid.into(), &weights, &bonds, &consensus); - - assert_mat_compare( - &new_bonds_liquid_alpha_on, - &new_bonds_liquid_alpha_off, - epsilon, - ); - }); -} - -#[test] -fn test_epoch_masks_incoming_to_sniped_uid_prevents_inheritance() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(40); - let tempo: u16 = 10; - let reveal: u64 = 2; - - add_network(netuid, tempo, 0); - assert_ok!(SubtensorModule::set_reveal_period(netuid, reveal)); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_max_allowed_uids(netuid, 3); - SubtensorModule::set_target_registrations_per_interval(netuid, u16::MAX); - - /* Validator uid‑0 */ - let (val_hot, val_cold) = (U256::from(100), U256::from(200)); - register_ok_neuron(netuid, val_hot, val_cold, 0); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &val_hot, - &val_cold, - netuid, - 10_000.into(), - ); - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - - /* Miner uid‑1 (to be sniped later) */ - let (old_hot, old_cold) = (U256::from(101), U256::from(201)); - register_ok_neuron(netuid, old_hot, old_cold, 0); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &old_hot, - &old_cold, - netuid, - 100.into(), - ); - - /* filler uid‑2 */ - let (fill_hot, fill_cold) = (U256::from(102), U256::from(202)); - register_ok_neuron(netuid, fill_hot, fill_cold, 0); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &fill_hot, - &fill_cold, - netuid, - 5_000.into(), - ); - SubtensorModule::set_max_allowed_validators(netuid, 3); - - run_to_block(tempo as u64 * 2 + 1); - - /* commit, then move one block ahead so reg_block > commit_block */ - commit_dummy(val_hot, netuid); - run_to_block(System::block_number() + 1); - - /* validator weights uid‑1 */ - SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(val_hot), - netuid, - vec![1], - vec![u16::MAX], - 0 - )); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::epoch(netuid, 1_000.into()); - - /* register new miner (snipes) */ - let (new_hot, new_cold) = (U256::from(103), U256::from(203)); - register_ok_neuron(netuid, new_hot, new_cold, 0); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &new_hot, - &new_cold, - netuid, - 10_000.into(), - ); - let new_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &new_hot) - .expect("new miner gets UID"); - - run_to_block(System::block_number() + 1); - - /* validator refreshes vote (still inside window) */ - SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(val_hot), - netuid, - vec![0, new_uid], - vec![u16::MAX / 2, u16::MAX / 2], - 0 - )); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - - SubtensorModule::epoch(netuid, 1_000.into()); - assert_eq!(SubtensorModule::get_rank_for_uid(netuid, new_uid), 0); - assert_eq!( - SubtensorModule::get_incentive_for_uid(netuid.into(), new_uid), - 0 - ); - }); -} - -#[test] -fn test_epoch_no_mask_when_commit_reveal_disabled() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(32); - let tempo: u16 = 5; - add_network(netuid, tempo, 0); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); - - let (hot, cold) = (U256::from(1000), U256::from(1100)); - register_ok_neuron(netuid, hot, cold, 0); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hot, - &cold, - netuid, - 1_000.into(), - ); - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - - let (hot1, cold1) = (U256::from(1001), U256::from(1101)); - register_ok_neuron(netuid, hot1, cold1, 0); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hot1, - &cold1, - netuid, - 1_000.into(), - ); - - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(hot), - netuid, - vec![1], - vec![u16::MAX], - 0 - )); - - for _ in 0..3 { - SubtensorModule::epoch(netuid, 1.into()); - assert!( - !SubtensorModule::get_weights_sparse(netuid.into())[0].is_empty(), - "row visible when CR disabled" - ); - run_to_block(System::block_number() + tempo as u64 + 1); - } - }); -} - -#[test] -fn test_epoch_does_not_mask_outside_window_but_masks_inside() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(50); - let tempo: u16 = 8; - let reveal: u16 = 2; - - add_network(netuid, tempo, 0); - assert_ok!(SubtensorModule::set_reveal_period(netuid, reveal as u64)); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_target_registrations_per_interval(netuid, u16::MAX); - - /* validator uid‑0 */ - let (v_hot, v_cold) = (U256::from(2000), U256::from(2100)); - register_ok_neuron(netuid, v_hot, v_cold, 0); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &v_hot, - &v_cold, - netuid, - 10_000.into(), - ); - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - SubtensorModule::set_max_allowed_validators(netuid, 1); - - run_to_block(tempo as u64); - - /* first commit */ - commit_dummy(v_hot, netuid); - - /* UID‑1 — outside window */ - let (old_hot, old_cold) = (U256::from(2001), U256::from(2101)); - register_ok_neuron(netuid, old_hot, old_cold, 0); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &old_hot, - &old_cold, - netuid, - 1_000.into(), - ); - - /* let first commit expire for UID‑1 */ - for _ in 0..(reveal + 1) { - run_to_block(System::block_number() + tempo as u64); - } - - /* second commit — will mask UID‑2 & UID‑3 */ - commit_dummy(v_hot, netuid); - - /* ensure commit_block < reg_block for the new registrations */ - run_to_block(System::block_number() + 1); - - /* UID‑2, UID‑3 — inside window */ - let (mid_hot, mid_cold) = (U256::from(2002), U256::from(2102)); - register_ok_neuron(netuid, mid_hot, mid_cold, 0); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &mid_hot, - &mid_cold, - netuid, - 1_000.into(), - ); - - let (new_hot, new_cold) = (U256::from(2003), U256::from(2103)); - register_ok_neuron(netuid, new_hot, new_cold, 0); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &new_hot, - &new_cold, - netuid, - 1_000.into(), - ); - - run_to_block(System::block_number() + 1); // avoid out‑dated - - /* vote */ - SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(v_hot), - netuid, - vec![1, 2, 3], - vec![u16::MAX / 3, u16::MAX / 3, u16::MAX / 3], - 0 - )); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - - SubtensorModule::epoch(netuid, 1_000.into()); - - assert!( - SubtensorModule::get_incentive_for_uid(netuid.into(), 1) > 0, - "UID-1 (old) unmasked" - ); - assert_eq!( - SubtensorModule::get_incentive_for_uid(netuid.into(), 2), - 0, - "UID-2 (inside window) masked" - ); - assert_eq!( - SubtensorModule::get_incentive_for_uid(netuid.into(), 3), - 0, - "UID-3 (inside window) masked" - ); - }); -} - -// Test an epoch doesn't panic when LastUpdate size doesn't match to Weights size. -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::epoch::test_last_update_size_mismatch --exact --show-output --nocapture -#[test] -fn test_last_update_size_mismatch() { - new_test_ext(1).execute_with(|| { - log::info!("test_1_graph:"); - let netuid = NetUid::from(1); - let coldkey = U256::from(0); - let hotkey = U256::from(0); - let uid: u16 = 0; - let stake_amount: u64 = 1_000_000_000; - add_network_disable_commit_reveal(netuid, u16::MAX - 1, 0); - SubtensorModule::set_max_allowed_uids(netuid, 1); - add_balance_to_coldkey_account( - &coldkey, - TaoBalance::from(stake_amount) - + ExistentialDeposit::get() - + (SubtensorModule::get_network_min_lock() * 2.into()), - ); - register_ok_neuron(netuid, hotkey, coldkey, 1); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - stake_amount.into() - )); - - assert_eq!(SubtensorModule::get_subnetwork_n(netuid), 1); - run_to_block(1); // run to next block to ensure weights are set on nodes after their registration block - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(U256::from(uid)), - netuid, - vec![uid], - vec![u16::MAX], - 0 - )); - - // Set mismatching LastUpdate vector - LastUpdate::::insert(NetUidStorageIndex::from(netuid), vec![1, 1, 1]); - - SubtensorModule::epoch(netuid, 1_000_000_000.into()); - assert_eq!( - SubtensorModule::get_total_stake_for_hotkey(&hotkey), - stake_amount.into() - ); - assert_eq!(SubtensorModule::get_rank_for_uid(netuid, uid), 0); - assert_eq!(SubtensorModule::get_trust_for_uid(netuid, uid), 0); - assert_eq!(SubtensorModule::get_consensus_for_uid(netuid, uid), 0); - assert_eq!( - SubtensorModule::get_incentive_for_uid(netuid.into(), uid), - 0 - ); - assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, uid), 0); - }); -} - -#[test] -fn empty_ok() { - new_test_ext(1).execute_with(|| { - let netuid: NetUid = 155.into(); - assert!(Pallet::::is_epoch_input_state_consistent(netuid)); - }); -} - -#[test] -fn unique_hotkeys_and_uids_ok() { - new_test_ext(1).execute_with(|| { - let netuid: NetUid = 155.into(); - - // (netuid, uid) -> hotkey (AccountId = U256) - Keys::::insert(netuid, 0u16, U256::from(1u64)); - Keys::::insert(netuid, 1u16, U256::from(2u64)); - Keys::::insert(netuid, 2u16, U256::from(3u64)); - - assert!(Pallet::::is_epoch_input_state_consistent(netuid)); - }); -} - -#[test] -fn duplicate_hotkey_within_same_netuid_fails() { - new_test_ext(1).execute_with(|| { - let netuid: NetUid = 155.into(); - - // Same hotkey mapped from two different UIDs in the SAME netuid - let hk = U256::from(42u64); - Keys::::insert(netuid, 0u16, hk); - Keys::::insert(netuid, 1u16, U256::from(42u64)); // duplicate hotkey - - assert!(!Pallet::::is_epoch_input_state_consistent(netuid)); - }); -} - -#[test] -fn same_hotkey_across_different_netuids_is_ok() { - new_test_ext(1).execute_with(|| { - let net_a: NetUid = 10.into(); - let net_b: NetUid = 11.into(); - - // Same hotkey appears once in each netuid — each net checks independently. - let hk = U256::from(777u64); - Keys::::insert(net_a, 0u16, hk); - Keys::::insert(net_b, 0u16, hk); - - assert!(Pallet::::is_epoch_input_state_consistent(net_a)); - assert!(Pallet::::is_epoch_input_state_consistent(net_b)); - }); -} diff --git a/pallets/subtensor/src/tests/epoch/active_stake.rs b/pallets/subtensor/src/tests/epoch/active_stake.rs new file mode 100644 index 0000000000..06b34409dd --- /dev/null +++ b/pallets/subtensor/src/tests/epoch/active_stake.rs @@ -0,0 +1,237 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Active-stake filtering during epoch (validators without recent weights). + +use frame_support::assert_ok; +use sp_core::U256; +use substrate_fixed::types::I32F32; +use subtensor_runtime_common::{AlphaBalance, TaoBalance}; + +use super::super::mock::*; +use crate::*; + +// Test that epoch masks out inactive stake of validators with outdated weights beyond activity cutoff. +#[test] +fn test_active_stake() { + new_test_ext(1).execute_with(|| { + System::set_block_number(0); + let sparse: bool = true; + let n: u16 = 4; + let netuid = NetUid::from(1); + let tempo: u16 = 1; + let block_number: u64 = System::block_number(); + let stake: TaoBalance = 1.into(); + add_network_disable_commit_reveal(netuid, tempo, 0); + SubtensorModule::set_max_allowed_uids(netuid, n); + assert_eq!(SubtensorModule::get_max_allowed_uids(netuid), n); + SubtensorModule::set_max_registrations_per_block(netuid, n); + SubtensorModule::set_target_registrations_per_interval(netuid, n); + SubtensorModule::set_min_allowed_weights(netuid, 0); + + // === Register [validator1, validator2, server1, server2] + for key in 0..n as u64 { + add_balance_to_coldkey_account( + &U256::from(key), + stake + ExistentialDeposit::get() + SubtensorModule::get_network_min_lock(), + ); + let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( + netuid, + block_number, + key * 1_000_000, + &U256::from(key), + ); + assert_ok!(SubtensorModule::register( + RuntimeOrigin::signed(U256::from(key)), + netuid, + block_number, + nonce, + work, + U256::from(key), + U256::from(key) + )); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &U256::from(key), + &U256::from(key), + netuid, + AlphaBalance::from(stake.to_u64()), + ); + } + assert_eq!(SubtensorModule::get_max_allowed_uids(netuid), n); + assert_eq!(SubtensorModule::get_subnetwork_n(netuid), n); + + // === Issue validator permits + SubtensorModule::set_max_allowed_validators(netuid, n); + assert_eq!(SubtensorModule::get_max_allowed_validators(netuid), n); + SubtensorModule::epoch(netuid, 1_000_000_000.into()); // run first epoch to set allowed validators + next_block_no_epoch(netuid); // run to next block to ensure weights are set on nodes after their registration block + + // === Set weights [val1->srv1: 0.5, val1->srv2: 0.5, val2->srv1: 0.5, val2->srv2: 0.5] + for uid in 0..(n / 2) as u64 { + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(uid)), + netuid, + ((n / 2)..n).collect(), + vec![u16::MAX / (n / 2); (n / 2) as usize], + 0 + )); + } + if sparse { + SubtensorModule::epoch(netuid, 1_000_000_000.into()); + } else { + SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); + } + let bonds = SubtensorModule::get_bonds(netuid.into()); + for uid in 0..n { + // log::info!("\n{uid}" ); + // uid_stats(netuid, uid); + // log::info!("bonds: {:?}", bonds[uid as usize]); + if uid < n / 2 { + assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, uid), 32767); + // Note D = floor(0.5 * 65_535) + } + assert_eq!( + SubtensorModule::get_emission_for_uid(netuid, uid), + 250000000.into() + ); // Note E = 0.5 / (n/2) * 1_000_000_000 = 250_000_000 + } + for bond in bonds.iter().take((n / 2) as usize) { + // for on_validator in 0..(n / 2) as usize { + for i in bond.iter().take((n / 2) as usize) { + assert_eq!(*i, 0); + } + for i in bond.iter().take(n as usize).skip((n / 2) as usize) { + assert_eq!(*i, I32F32::from_num(65_535)); // floor(0.5*(2^16-1))/(2^16-1), then max-upscale to 65_535 + } + } + let activity_cutoff: u64 = SubtensorModule::get_activity_cutoff(netuid) as u64; + run_to_block_no_epoch(netuid, activity_cutoff + 2); // run to block where validator (uid 0, 1) weights become outdated + + // === Update uid 0 weights + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(0)), + netuid, + ((n / 2)..n).collect(), + vec![u16::MAX / (n / 2); (n / 2) as usize], + 0 + )); + if sparse { + SubtensorModule::epoch(netuid, 1_000_000_000.into()); + } else { + SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); + } + /* current_block: 5002; activity_cutoff: 5000 + Last update: [5002, 1, 0, 0]; Inactive: [false, true, true, true]; Block at registration: [0, 0, 0, 0] + S: [0.25, 0.25, 0.25, 0.25]; S (mask): [0.25, 0, 0, 0]; S (mask+norm): [1, 0, 0, 0] + validator_permits: [true, true, true, true]; max_allowed_validators: 4; new_validator_permits: [true, true, true, true] + W: [[(2, 0.4999923704), (3, 0.4999923704)], [(2, 0.4999923704), (3, 0.4999923704)], [], []] + W (permit): [[(2, 0.4999923704), (3, 0.4999923704)], [(2, 0.4999923704), (3, 0.4999923704)], [], []] + W (permit+diag): [[(2, 0.4999923704), (3, 0.4999923704)], [(2, 0.4999923704), (3, 0.4999923704)], [], []] + W (permit+diag+outdate): [[(2, 0.4999923704), (3, 0.4999923704)], [(2, 0.4999923704), (3, 0.4999923704)], [], []] + W (mask+norm): [[(2, 0.5), (3, 0.5)], [(2, 0.5), (3, 0.5)], [], []] + R: [0, 0, 0.5, 0.5] + W (threshold): [[(2, 1), (3, 1)], [(2, 1), (3, 1)], [], []] + T: [0, 0, 1, 1] + C: [0.006693358, 0.006693358, 0.9933076561, 0.9933076561] + I: [0, 0, 0.5, 0.5] + B: [[(2, 0.4999923704), (3, 0.4999923704)], [(2, 0.4999923704), (3, 0.4999923704)], [], []] + B (outdatedmask): [[(2, 0.4999923704), (3, 0.4999923704)], [(2, 0.4999923704), (3, 0.4999923704)], [], []] + B (mask+norm): [[(2, 0.5), (3, 0.5)], [(2, 0.5), (3, 0.5)], [], []] + ΔB: [[(2, 0.5), (3, 0.5)], [(2, 0), (3, 0)], [], []] + ΔB (norm): [[(2, 1), (3, 1)], [(2, 0), (3, 0)], [], []] + emaB: [[(2, 0.55), (3, 0.55)], [(2, 0.45), (3, 0.45)], [], []] + emaB (max-upscale): [[(2, 1), (3, 1)], [(2, 1), (3, 1)], [], []] + D: [0.55, 0.4499999997, 0, 0] + nE: [0.275, 0.2249999999, 0.25, 0.25] + E: [274999999, 224999999, 250000000, 250000000] + P: [0.275, 0.2249999999, 0.25, 0.25] + P (u16): [65535, 53619, 59577, 59577] */ + let bonds = SubtensorModule::get_bonds(netuid.into()); + assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, 0), 36044); // Note D = floor((0.5 * 0.9 + 0.1) * 65_535) + assert_eq!( + SubtensorModule::get_emission_for_uid(netuid, 0), + 274999999.into() + ); // Note E = 0.5 * 0.55 * 1_000_000_000 = 275_000_000 (discrepancy) + for server in ((n / 2) as usize)..n as usize { + assert_eq!(bonds[0][server], I32F32::from_num(65_535)); // floor(0.55*(2^16-1))/(2^16-1), then max-upscale + } + for validator in 1..(n / 2) { + assert_eq!( + SubtensorModule::get_dividends_for_uid(netuid, validator), + 29490 + ); // Note D = floor((0.5 * 0.9) * 65_535) + assert_eq!( + SubtensorModule::get_emission_for_uid(netuid, validator), + 224999999.into() + ); // Note E = 0.5 * 0.45 * 1_000_000_000 = 225_000_000 (discrepancy) + for server in ((n / 2) as usize)..n as usize { + assert_eq!(bonds[validator as usize][server], I32F32::from_num(53619)); + // floor(0.45*(2^16-1))/(2^16-1), then max-upscale + } + } + + // === Update uid 1 weights as well + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(1)), + netuid, + ((n / 2)..n).collect(), + vec![u16::MAX / (n / 2); (n / 2) as usize], + 0 + )); + run_to_block_no_epoch(netuid, activity_cutoff + 3); // run to block where validator (uid 0, 1) weights become outdated + if sparse { + SubtensorModule::epoch(netuid, 1_000_000_000.into()); + } else { + SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); + } + /* current_block: 5003; activity_cutoff: 5000 + Last update: [5002, 5002, 0, 0]; Inactive: [false, false, true, true]; Block at registration: [0, 0, 0, 0] + S: [0.25, 0.25, 0.25, 0.25]; S (mask): [0.25, 0.25, 0, 0]; S (mask+norm): [0.5, 0.5, 0, 0] + validator_permits: [true, true, true, true]; max_allowed_validators: 4; new_validator_permits: [true, true, true, true] + W: [[(2, 0.4999923704), (3, 0.4999923704)], [(2, 0.4999923704), (3, 0.4999923704)], [], []] + W (permit): [[(2, 0.4999923704), (3, 0.4999923704)], [(2, 0.4999923704), (3, 0.4999923704)], [], []] + W (permit+diag): [[(2, 0.4999923704), (3, 0.4999923704)], [(2, 0.4999923704), (3, 0.4999923704)], [], []] + W (permit+diag+outdate): [[(2, 0.4999923704), (3, 0.4999923704)], [(2, 0.4999923704), (3, 0.4999923704)], [], []] + W (mask+norm): [[(2, 0.5), (3, 0.5)], [(2, 0.5), (3, 0.5)], [], []] + R: [0, 0, 0.5, 0.5] + W (threshold): [[(2, 1), (3, 1)], [(2, 1), (3, 1)], [], []] + T: [0, 0, 1, 1] + C: [0.006693358, 0.006693358, 0.9933076561, 0.9933076561] + I: [0, 0, 0.5, 0.5] + B: [[(2, 65535), (3, 65535)], [(2, 53619), (3, 53619)], [], []] + B (outdatedmask): [[(2, 65535), (3, 65535)], [(2, 53619), (3, 53619)], [], []] + B (mask+norm): [[(2, 0.5500025176), (3, 0.5500025176)], [(2, 0.4499974821), (3, 0.4499974821)], [], []] + ΔB: [[(2, 0.25), (3, 0.25)], [(2, 0.25), (3, 0.25)], [], []] + ΔB (norm): [[(2, 0.5), (3, 0.5)], [(2, 0.5), (3, 0.5)], [], []] + emaB: [[(2, 0.545002266), (3, 0.545002266)], [(2, 0.4549977337), (3, 0.4549977337)], [], []] + emaB (max-upscale): [[(2, 1), (3, 1)], [(2, 0.8348547556), (3, 0.8348547556)], [], []] + D: [0.545002266, 0.4549977337, 0, 0] + nE: [0.272501133, 0.2274988669, 0.25, 0.25] + E: [272501132, 227498866, 250000000, 250000000] + P: [0.272501133, 0.2274988669, 0.25, 0.25] + P (u16): [65535, 54711, 60123, 60123] */ + let bonds = SubtensorModule::get_bonds(netuid.into()); + assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, 0), 35716); // Note D = floor((0.55 * 0.9 + 0.5 * 0.1) * 65_535) + assert_eq!( + SubtensorModule::get_emission_for_uid(netuid, 0), + 272501132.into() + ); // Note E = 0.5 * (0.55 * 0.9 + 0.5 * 0.1) * 1_000_000_000 = 272_500_000 (discrepancy) + for server in ((n / 2) as usize)..n as usize { + assert_eq!(bonds[0][server], I32F32::from_num(65_535)); // floor((0.55 * 0.9 + 0.5 * 0.1)*(2^16-1))/(2^16-1), then max-upscale + } + assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, 1), 29818); // Note D = floor((0.45 * 0.9 + 0.5 * 0.1) * 65_535) + assert_eq!( + SubtensorModule::get_emission_for_uid(netuid, 1), + 227498866.into() + ); // Note E = 0.5 * (0.45 * 0.9 + 0.5 * 0.1) * 1_000_000_000 = 227_500_000 (discrepancy) + for server in ((n / 2) as usize)..n as usize { + assert_eq!(bonds[1][server], I32F32::from_num(54712)); // floor((0.45 * 0.9 + 0.5 * 0.1)/(0.55 * 0.9 + 0.5 * 0.1)*(2^16-1)) + } + }); +} + +// Test that epoch masks out outdated weights and bonds of validators on deregistered servers. +// diff --git a/pallets/subtensor/src/tests/epoch/bonds.rs b/pallets/subtensor/src/tests/epoch/bonds.rs new file mode 100644 index 0000000000..8b25443245 --- /dev/null +++ b/pallets/subtensor/src/tests/epoch/bonds.rs @@ -0,0 +1,504 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Bond EMA / persistence across epochs and deregistered-miner bond cleanup. + +use frame_support::assert_ok; +use sp_core::U256; +use subtensor_runtime_common::{AlphaBalance, TaoBalance}; + +use super::super::mock::*; +use crate::*; + +// Test bonds exponential moving average over a sequence of epochs - no liquid alpha +#[test] +fn test_bonds() { + new_test_ext(1).execute_with(|| { + let sparse: bool = true; + let n: u16 = 8; + let netuid = NetUid::from(1); + let tempo: u16 = 1; + let max_stake: TaoBalance = 4.into(); + let stakes: Vec = vec![1, 2, 3, 4, 0, 0, 0, 0]; + let block_number = System::block_number(); + add_network_disable_commit_reveal(netuid, tempo, 0); + SubtensorModule::set_max_allowed_uids( netuid, n ); + assert_eq!(SubtensorModule::get_max_allowed_uids(netuid), n); + SubtensorModule::set_max_registrations_per_block( netuid, n ); + SubtensorModule::set_target_registrations_per_interval(netuid, n); + SubtensorModule::set_weights_set_rate_limit( netuid, 0 ); + SubtensorModule::set_min_allowed_weights( netuid, 1 ); + SubtensorModule::set_bonds_penalty(netuid, u16::MAX); + + + // === Register [validator1, validator2, validator3, validator4, server1, server2, server3, server4] + for key in 0..n as u64 { + add_balance_to_coldkey_account( + &U256::from(key), + max_stake + ExistentialDeposit::get() + SubtensorModule::get_network_min_lock() + ); + let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( netuid, block_number, key * 1_000_000, &U256::from(key)); + assert_ok!(SubtensorModule::register(<::RuntimeOrigin>::signed(U256::from(key)), netuid, block_number, nonce, work, U256::from(key), U256::from(key))); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( &U256::from(key), &U256::from(key), netuid, stakes[key as usize].into() ); + } + assert_eq!(SubtensorModule::get_max_allowed_uids(netuid), n); + assert_eq!(SubtensorModule::get_subnetwork_n(netuid), n); + + // === Issue validator permits + SubtensorModule::set_max_allowed_validators(netuid, n); + assert_eq!( SubtensorModule::get_max_allowed_validators(netuid), n); + SubtensorModule::epoch( netuid, 1_000_000_000 .into()); // run first epoch to set allowed validators + next_block_no_epoch(netuid); // run to next block to ensure weights are set on nodes after their registration block + + // === Set weights [val->srv1: 0.1, val->srv2: 0.2, val->srv3: 0.3, val->srv4: 0.4] + for uid in 0..(n/2) as u64 { + assert_ok!(SubtensorModule::set_weights(RuntimeOrigin::signed(U256::from(uid)), netuid, ((n/2)..n).collect(), vec![ u16::MAX/4, u16::MAX/2, (u16::MAX/4)*3, u16::MAX], 0)); + } + if sparse { SubtensorModule::epoch( netuid, 1_000_000_000 .into()); } + else { SubtensorModule::epoch_dense( netuid, 1_000_000_000 .into()); } + /* n: 8 + current_block: 1; activity_cutoff: 5000; Last update: [1, 1, 1, 1, 0, 0, 0, 0] + Inactive: [false, false, false, false, false, false, false, false] + Block at registration: [0, 0, 0, 0, 0, 0, 0, 0] + hotkeys: [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7)] + S: [0.0999999999, 0.2, 0.2999999998, 0.4, 0, 0, 0, 0] + validator_permits: [true, true, true, true, true, true, true, true] + max_allowed_validators: 8 + new_validator_permits: [true, true, true, true, true, true, true, true] + S: [0.0999999999, 0.2, 0.2999999998, 0.4, 0, 0, 0, 0] + W: [[(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] + W (permit): [[(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] + W (permit+diag): [[(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] + W (permit+diag+outdate): [[(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] + W (mask+norm): [[(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [], [], [], []] + R (before): [0, 0, 0, 0, 0.099997558, 0.2000012202, 0.2999926745, 0.4000085443] + C: [0, 0, 0, 0, 0.0999975584, 0.2000012207, 0.2999926754, 0.400008545] + W: [[(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [], [], [], []] + Tv: [0.9999999995, 0.9999999995, 0.9999999995, 0.9999999995, 0, 0, 0, 0] + R (after): [0, 0, 0, 0, 0.099997558, 0.2000012202, 0.2999926745, 0.4000085443] + T: [0, 0, 0, 0, 1, 1, 1, 1] + I (=R): [0, 0, 0, 0, 0.0999975582, 0.2000012207, 0.2999926752, 0.4000085455] + B: [[], [], [], [], [], [], [], []] + B (outdatedmask): [[], [], [], [], [], [], [], []] + B (mask+norm): [[], [], [], [], [], [], [], []] + ΔB: [[(4, 0.0099997558), (5, 0.020000122), (6, 0.0299992673), (7, 0.0400008543)], [(4, 0.0199995115), (5, 0.040000244), (6, 0.0599985349), (7, 0.0800017088)], [(4, 0.0299992673), (5, 0.060000366), (6, 0.0899978024), (7, 0.1200025633)], [(4, 0.0399990233), (5, 0.080000488), (6, 0.11999707), (7, 0.1600034179)], [], [], [], []] + ΔB (norm): [[(4, 0.0999999996), (5, 0.0999999999), (6, 0.0999999994), (7, 0.0999999996)], [(4, 0.1999999995), (5, 0.2), (6, 0.1999999997), (7, 0.1999999997)], [(4, 0.299999999), (5, 0.2999999998), (6, 0.3), (7, 0.3)], [(4, 0.4000000013), (5, 0.4), (6, 0.4000000004), (7, 0.4000000001)], [], [], [], []] + emaB: [[(4, 0.0999999982), (5, 0.0999999985), (6, 0.099999998), (7, 0.099999998)], [(4, 0.199999999), (5, 0.1999999995), (6, 0.1999999986), (7, 0.1999999986)], [(4, 0.2999999996), (5, 0.3000000003), (6, 0.3000000012), (7, 0.3000000012)], [(4, 0.4000000027), (5, 0.4000000013), (6, 0.4000000018), (7, 0.4000000018)], [], [], [], []] + D: [0.0999999978, 0.1999999983, 0.3000000012, 0.4000000022, 0, 0, 0, 0] + nE: [0.0499999989, 0.0999999992, 0.1500000006, 0.2000000011, 0.049998779, 0.1000006103, 0.1499963375, 0.2000042726] + E: [49999998, 99999999, 150000000, 200000001, 49998779, 100000610, 149996337, 200004272] + P: [0.0499999989, 0.0999999992, 0.1500000006, 0.2000000011, 0.049998779, 0.1000006103, 0.1499963375, 0.2000042726] + emaB: [[(4, 0.2499999937), (5, 0.2499999953), (6, 0.2499999937), (7, 0.2499999937)], [(4, 0.4999999942), (5, 0.499999997), (6, 0.4999999942), (7, 0.4999999942)], [(4, 0.7499999937), (5, 0.7499999981), (6, 0.7499999995), (7, 0.7499999995)], [(4, 1), (5, 1), (6, 1), (7, 1)], [], [], [], []] */ + let bonds = SubtensorModule::get_bonds( netuid.into() ); + assert_eq!(bonds[0][4], 16383); + assert_eq!(bonds[1][4], 32767); + assert_eq!(bonds[2][4], 49151); + assert_eq!(bonds[3][4], 65535); + + // === Set self-weight only on val1 + let uid = 0; + assert_ok!(SubtensorModule::set_weights(RuntimeOrigin::signed(U256::from(uid)), netuid, vec![uid], vec![u16::MAX], 0)); + next_block_no_epoch(netuid); + + if sparse { SubtensorModule::epoch( netuid, 1_000_000_000 .into()); } + else { SubtensorModule::epoch_dense( netuid, 1_000_000_000.into() ); } + /* n: 8 + current_block: 2 + activity_cutoff: 5000 + Last update: [1, 1, 1, 1, 0, 0, 0, 0] + Inactive: [false, false, false, false, false, false, false, false] + Block at registration: [0, 0, 0, 0, 0, 0, 0, 0] + hotkeys: [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7)] + S: [0.0999999999, 0.2, 0.2999999998, 0.4, 0, 0, 0, 0] + validator_permits: [true, true, true, true, true, true, true, true] + max_allowed_validators: 8 + new_validator_permits: [true, true, true, true, true, true, true, true] + S: [0.0999999999, 0.2, 0.2999999998, 0.4, 0, 0, 0, 0] + W: [[(0, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] + W (permit): [[(0, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] + W (permit+diag): [[], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] + W (permit+diag+outdate): [[], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] + W (mask+norm): [[], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [], [], [], []] + R (before): [0, 0, 0, 0, 0.0899978022, 0.1800010982, 0.2699934072, 0.36000769] + C: [0, 0, 0, 0, 0.0999975584, 0.2000012207, 0.2999926754, 0.400008545] + W: [[], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [], [], [], []] + Tv: [0, 0.9999999995, 0.9999999995, 0.9999999995, 0, 0, 0, 0] + R (after): [0, 0, 0, 0, 0.0899978022, 0.1800010982, 0.2699934072, 0.36000769] + T: [0, 0, 0, 0, 1, 1, 1, 1] + I (=R): [0, 0, 0, 0, 0.0999975582, 0.2000012207, 0.2999926754, 0.4000085455] + B: [[(4, 16383), (5, 16383), (6, 16383), (7, 16383)], [(4, 32767), (5, 32767), (6, 32767), (7, 32767)], [(4, 49151), (5, 49151), (6, 49151), (7, 49151)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] + B (outdatedmask): [[(4, 16383), (5, 16383), (6, 16383), (7, 16383)], [(4, 32767), (5, 32767), (6, 32767), (7, 32767)], [(4, 49151), (5, 49151), (6, 49151), (7, 49151)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] + B (mask+norm): [[(4, 0.0999963377), (5, 0.0999963377), (6, 0.0999963377), (7, 0.0999963377)], [(4, 0.1999987792), (5, 0.1999987792), (6, 0.1999987792), (7, 0.1999987792)], [(4, 0.3000012205), (5, 0.3000012205), (6, 0.3000012205), (7, 0.3000012205)], [(4, 0.400003662), (5, 0.400003662), (6, 0.400003662), (7, 0.400003662)], [], [], [], []] + ΔB: [[], [(4, 0.0199995115), (5, 0.040000244), (6, 0.0599985349), (7, 0.0800017088)], [(4, 0.0299992673), (5, 0.060000366), (6, 0.0899978024), (7, 0.1200025633)], [(4, 0.0399990233), (5, 0.080000488), (6, 0.11999707), (7, 0.1600034179)], [], [], [], []] + ΔB (norm): [[], [(4, 0.2222222215), (5, 0.222222222), (6, 0.2222222218), (7, 0.2222222218)], [(4, 0.3333333323), (5, 0.3333333333), (6, 0.3333333333), (7, 0.3333333333)], [(4, 0.4444444457), (5, 0.4444444443), (6, 0.4444444447), (7, 0.4444444445)], [], [], [], []] + emaB: [[(4, 0.0899967037), (5, 0.0899967037), (6, 0.0899967037), (7, 0.0899967037)], [(4, 0.2022211235), (5, 0.2022211235), (6, 0.2022211235), (7, 0.2022211235)], [(4, 0.3033344317), (5, 0.3033344317), (6, 0.3033344317), (7, 0.3033344317)], [(4, 0.4044477409), (5, 0.4044477406), (6, 0.4044477406), (7, 0.4044477406)], [], [], [], []] + D: [0.0899967032, 0.2022211233, 0.303334432, 0.404447741, 0, 0, 0, 0] + nE: [0.0449983515, 0.1011105615, 0.1516672159, 0.2022238704, 0.049998779, 0.1000006103, 0.1499963377, 0.2000042726] + E: [44998351, 101110561, 151667215, 202223870, 49998779, 100000610, 149996337, 200004272] + P: [0.0449983515, 0.1011105615, 0.1516672159, 0.2022238704, 0.049998779, 0.1000006103, 0.1499963377, 0.2000042726] + emaB: [[(4, 0.2225175085), (5, 0.2225175085), (6, 0.2225175085), (7, 0.2225175085)], [(4, 0.499993208), (5, 0.4999932083), (6, 0.4999932083), (7, 0.4999932083)], [(4, 0.7499966028), (5, 0.7499966032), (6, 0.7499966032), (7, 0.7499966032)], [(4, 1), (5, 1), (6, 1), (7, 1)], [], [], [], []] */ + let bonds = SubtensorModule::get_bonds( netuid.into() ); + assert_eq!(bonds[0][4], 14582); + assert_eq!(bonds[1][4], 32767); + assert_eq!(bonds[2][4], 49151); + assert_eq!(bonds[3][4], 65535); + + // === Set self-weight only on val2 + let uid = 1; + assert_ok!(SubtensorModule::set_weights(RuntimeOrigin::signed(U256::from(uid)), netuid, vec![uid], vec![u16::MAX], 0)); + next_block_no_epoch(netuid); + + if sparse { SubtensorModule::epoch( netuid, 1_000_000_000 .into()); } + else { SubtensorModule::epoch_dense( netuid, 1_000_000_000.into() ); } + /* current_block: 3 + W: [[(0, 65535)], [(1, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] + W (permit): [[(0, 65535)], [(1, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] + W (permit+diag): [[], [], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] + W (permit+diag+outdate): [[], [], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] + W (mask+norm): [[], [], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [], [], [], []] + R (before): [0, 0, 0, 0, 0.0699982906, 0.1400008542, 0.2099948723, 0.2800059812] + C: [0, 0, 0, 0, 0.0999975584, 0.2000012207, 0.2999926754, 0.400008545] + W: [[], [], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [], [], [], []] + Tv: [0, 0, 0.9999999995, 0.9999999995, 0, 0, 0, 0] + R (after): [0, 0, 0, 0, 0.0699982906, 0.1400008542, 0.2099948723, 0.2800059812] + T: [0, 0, 0, 0, 1, 1, 1, 1] + I (=R): [0, 0, 0, 0, 0.0999975582, 0.2000012207, 0.2999926754, 0.4000085455] + B: [[(4, 14582), (5, 14582), (6, 14582), (7, 14582)], [(4, 32767), (5, 32767), (6, 32767), (7, 32767)], [(4, 49151), (5, 49151), (6, 49151), (7, 49151)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] + B (outdatedmask): [[(4, 14582), (5, 14582), (6, 14582), (7, 14582)], [(4, 32767), (5, 32767), (6, 32767), (7, 32767)], [(4, 49151), (5, 49151), (6, 49151), (7, 49151)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] + B (mask+norm): [[(4, 0.0899929027), (5, 0.0899929027), (6, 0.0899929027), (7, 0.0899929027)], [(4, 0.2022217421), (5, 0.2022217421), (6, 0.2022217421), (7, 0.2022217421)], [(4, 0.303335699), (5, 0.303335699), (6, 0.303335699), (7, 0.303335699)], [(4, 0.404449656), (5, 0.404449656), (6, 0.404449656), (7, 0.404449656)], [], [], [], []] + ΔB: [[], [], [(4, 0.0299992673), (5, 0.060000366), (6, 0.0899978024), (7, 0.1200025633)], [(4, 0.0399990233), (5, 0.080000488), (6, 0.11999707), (7, 0.1600034179)], [], [], [], []] + ΔB (norm): [[], [], [(4, 0.428571427), (5, 0.4285714284), (6, 0.4285714284), (7, 0.4285714284)], [(4, 0.5714285728), (5, 0.5714285714), (6, 0.5714285714), (7, 0.5714285714)], [], [], [], []] + emaB: [[(4, 0.0809936123), (5, 0.0809936123), (6, 0.0809936123), (7, 0.0809936123)], [(4, 0.181999568), (5, 0.181999568), (6, 0.181999568), (7, 0.181999568)], [(4, 0.3158592717), (5, 0.315859272), (6, 0.315859272), (7, 0.315859272)], [(4, 0.4211475477), (5, 0.4211475474), (6, 0.4211475474), (7, 0.4211475474)], [], [], [], []] + D: [0.0809936118, 0.1819995677, 0.3158592721, 0.421147548, 0, 0, 0, 0] + nE: [0.040496806, 0.0909997837, 0.157929636, 0.2105737738, 0.049998779, 0.1000006103, 0.1499963377, 0.2000042726] + E: [40496805, 90999783, 157929636, 210573773, 49998779, 100000610, 149996337, 200004272] + P: [0.040496806, 0.0909997837, 0.157929636, 0.2105737738, 0.049998779, 0.1000006103, 0.1499963377, 0.2000042726] + emaB: [[(4, 0.192316476), (5, 0.192316476), (6, 0.192316476), (7, 0.192316476)], [(4, 0.4321515555), (5, 0.4321515558), (6, 0.4321515558), (7, 0.4321515558)], [(4, 0.7499967015), (5, 0.7499967027), (6, 0.7499967027), (7, 0.7499967027)], [(4, 1), (5, 1), (6, 1), (7, 1)], [], [], [], []] */ + let bonds = SubtensorModule::get_bonds( netuid.into() ); + assert_eq!(bonds[0][4], 12603); + assert_eq!(bonds[1][4], 28321); + assert_eq!(bonds[2][4], 49151); + assert_eq!(bonds[3][4], 65535); + + // === Set self-weight only on val3 + let uid = 2; + assert_ok!(SubtensorModule::set_weights(RuntimeOrigin::signed(U256::from(uid)), netuid, vec![uid], vec![u16::MAX], 0)); + next_block_no_epoch(netuid); + + if sparse { SubtensorModule::epoch( netuid, 1_000_000_000 .into()); } + else { SubtensorModule::epoch_dense( netuid, 1_000_000_000.into() ); } + /* current_block: 4 + W: [[(0, 65535)], [(1, 65535)], [(2, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] + W (permit): [[(0, 65535)], [(1, 65535)], [(2, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] + W (permit+diag): [[], [], [], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] + W (permit+diag+outdate): [[], [], [], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] + W (mask+norm): [[], [], [], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [], [], [], []] + R (before): [0, 0, 0, 0, 0.0399990233, 0.080000488, 0.11999707, 0.1600034179] + C: [0, 0, 0, 0, 0, 0, 0, 0] + W: [[], [], [], [], [], [], [], []] + Tv: [0, 0, 0, 0, 0, 0, 0, 0] + R (after): [0, 0, 0, 0, 0, 0, 0, 0] + T: [0, 0, 0, 0, 0, 0, 0, 0] + I (=R): [0, 0, 0, 0, 0, 0, 0, 0] + B: [[(4, 12603), (5, 12603), (6, 12603), (7, 12603)], [(4, 28321), (5, 28321), (6, 28321), (7, 28321)], [(4, 49151), (5, 49151), (6, 49151), (7, 49151)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] + B (outdatedmask): [[(4, 12603), (5, 12603), (6, 12603), (7, 12603)], [(4, 28321), (5, 28321), (6, 28321), (7, 28321)], [(4, 49151), (5, 49151), (6, 49151), (7, 49151)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] + B (mask+norm): [[(4, 0.0809909387), (5, 0.0809909387), (6, 0.0809909387), (7, 0.0809909387)], [(4, 0.1819998713), (5, 0.1819998713), (6, 0.1819998713), (7, 0.1819998713)], [(4, 0.3158601632), (5, 0.3158601632), (6, 0.3158601632), (7, 0.3158601632)], [(4, 0.4211490264), (5, 0.4211490264), (6, 0.4211490264), (7, 0.4211490264)], [], [], [], []] + ΔB: [[], [], [], [], [], [], [], []] + ΔB (norm): [[], [], [], [], [], [], [], []] + emaB: [[(4, 0.0809909385), (5, 0.0809909385), (6, 0.0809909385), (7, 0.0809909385)], [(4, 0.1819998713), (5, 0.1819998713), (6, 0.1819998713), (7, 0.1819998713)], [(4, 0.3158601632), (5, 0.3158601632), (6, 0.3158601632), (7, 0.3158601632)], [(4, 0.4211490266), (5, 0.4211490266), (6, 0.4211490266), (7, 0.4211490266)], [], [], [], []] + D: [0, 0, 0, 0, 0, 0, 0, 0] + nE: [0.0999999999, 0.2, 0.2999999998, 0.4, 0, 0, 0, 0] + E: [99999999, 199999999, 299999999, 399999999, 0, 0, 0, 0] + P: [0.0999999999, 0.2, 0.2999999998, 0.4, 0, 0, 0, 0] + emaB: [[(4, 0.1923094518), (5, 0.1923094518), (6, 0.1923094518), (7, 0.1923094518)], [(4, 0.4321507583), (5, 0.4321507583), (6, 0.4321507583), (7, 0.4321507583)], [(4, 0.7499961846), (5, 0.7499961846), (6, 0.7499961846), (7, 0.7499961846)], [(4, 1), (5, 1), (6, 1), (7, 1)], [], [], [], []] */ + let bonds = SubtensorModule::get_bonds( netuid.into() ); + assert_eq!(bonds[0][7], 12602); + assert_eq!(bonds[1][7], 28320); + assert_eq!(bonds[2][7], 49150); + assert_eq!(bonds[3][7], 65535); + + // === Set val3->srv4: 1 + assert_ok!(SubtensorModule::set_weights(RuntimeOrigin::signed(U256::from(2)), netuid, vec![7], vec![u16::MAX], 0)); + next_block_no_epoch(netuid); + + if sparse { SubtensorModule::epoch( netuid, 1_000_000_000 .into()); } + else { SubtensorModule::epoch_dense( netuid, 1_000_000_000.into() ); } + /* current_block: 5 + W: [[(0, 65535)], [(1, 65535)], [(7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] + W (permit): [[(0, 65535)], [(1, 65535)], [(7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] + W (permit+diag): [[], [], [(7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] + W (permit+diag+outdate): [[], [], [(7, 65535)], [(4, 16383), (5, 32767), (6, 49149), (7, 65535)], [], [], [], []] + W (mask+norm): [[], [], [(7, 1)], [(4, 0.0999975584), (5, 0.2000012207), (6, 0.2999926754), (7, 0.400008545)], [], [], [], []] + R (before): [0, 0, 0, 0, 0.0399990233, 0.080000488, 0.11999707, 0.4600034177] + C: [0, 0, 0, 0, 0, 0, 0, 0.400008545] + W: [[], [], [(7, 0.400008545)], [(7, 0.400008545)], [], [], [], []] + Tv: [0, 0, 0.400008545, 0.400008545, 0, 0, 0, 0] + R (after): [0, 0, 0, 0, 0, 0, 0, 0.2800059812] + T: [0, 0, 0, 0, 0, 0, 0, 0.6087041323] + I (=R): [0, 0, 0, 0, 0, 0, 0, 1] + B: [[(4, 12602), (5, 12602), (6, 12602), (7, 12602)], [(4, 28320), (5, 28320), (6, 28320), (7, 28320)], [(4, 49150), (5, 49150), (6, 49150), (7, 49150)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] + B (outdatedmask): [[(4, 12602), (5, 12602), (6, 12602), (7, 12602)], [(4, 28320), (5, 28320), (6, 28320), (7, 28320)], [(4, 49150), (5, 49150), (6, 49150), (7, 49150)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] + B (mask+norm): [[(4, 0.0809860737), (5, 0.0809860737), (6, 0.0809860737), (7, 0.0809860737)], [(4, 0.1819969537), (5, 0.1819969537), (6, 0.1819969537), (7, 0.1819969537)], [(4, 0.3158598263), (5, 0.3158598263), (6, 0.3158598263), (7, 0.3158598263)], [(4, 0.4211571459), (5, 0.4211571459), (6, 0.4211571459), (7, 0.4211571459)], [], [], [], []] + ΔB: [[], [], [(7, 0.1200025633)], [(7, 0.1600034179)], [], [], [], []] + ΔB (norm): [[], [], [(7, 0.4285714284)], [(7, 0.5714285714)], [], [], [], []] + emaB: [[(4, 0.0809860737), (5, 0.0809860737), (6, 0.0809860737), (7, 0.0728874663)], [(4, 0.1819969537), (5, 0.1819969537), (6, 0.1819969537), (7, 0.1637972582)], [(4, 0.3158598263), (5, 0.3158598263), (6, 0.3158598263), (7, 0.3271309866)], [(4, 0.421157146), (5, 0.421157146), (6, 0.421157146), (7, 0.4361842885)], [], [], [], []] + D: [0.0728874663, 0.1637972582, 0.3271309866, 0.4361842885, 0, 0, 0, 0] + nE: [0.0364437331, 0.081898629, 0.1635654932, 0.2180921442, 0, 0, 0, 0.5] + E: [36443733, 81898628, 163565493, 218092144, 0, 0, 0, 500000000] + P: [0.0364437331, 0.081898629, 0.1635654932, 0.2180921442, 0, 0, 0, 0.5] + emaB: [[(4, 0.1922941932), (5, 0.1922941932), (6, 0.1922941932), (7, 0.1671024568)], [(4, 0.4321354993), (5, 0.4321354993), (6, 0.4321354993), (7, 0.3755230587)], [(4, 0.7499809256), (5, 0.7499809256), (6, 0.7499809256), (7, 0.749983425)], [(4, 1), (5, 1), (6, 1), (7, 1)], [], [], [], []] */ + let bonds = SubtensorModule::get_bonds( netuid.into() ); + assert_eq!(bonds[0][7], 10951); + assert_eq!(bonds[1][7], 24609); + assert_eq!(bonds[2][7], 49150); + assert_eq!(bonds[3][7], 65535); + + next_block_no_epoch(netuid); + + if sparse { SubtensorModule::epoch( netuid, 1_000_000_000 .into()); } + else { SubtensorModule::epoch_dense( netuid, 1_000_000_000.into() ); } + /* current_block: 6 + B: [[(4, 12601), (5, 12601), (6, 12601), (7, 10951)], [(4, 28319), (5, 28319), (6, 28319), (7, 24609)], [(4, 49149), (5, 49149), (6, 49149), (7, 49150)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] + B (outdatedmask): [[(4, 12601), (5, 12601), (6, 12601), (7, 10951)], [(4, 28319), (5, 28319), (6, 28319), (7, 24609)], [(4, 49149), (5, 49149), (6, 49149), (7, 49150)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] + B (mask+norm): [[(4, 0.0809812085), (5, 0.0809812085), (6, 0.0809812085), (7, 0.0728876167)], [(4, 0.181994036), (5, 0.181994036), (6, 0.181994036), (7, 0.163792472)], [(4, 0.3158594894), (5, 0.3158594894), (6, 0.3158594894), (7, 0.3271323503)], [(4, 0.4211652656), (5, 0.4211652656), (6, 0.4211652656), (7, 0.4361875602)], [], [], [], []] + ΔB: [[], [], [(7, 0.1200025633)], [(7, 0.1600034179)], [], [], [], []] + ΔB (norm): [[], [], [(7, 0.4285714284)], [(7, 0.5714285714)], [], [], [], []] + emaB: [[(4, 0.0809812082), (5, 0.0809812082), (6, 0.0809812082), (7, 0.0655988548)], [(4, 0.181994036), (5, 0.181994036), (6, 0.181994036), (7, 0.1474132247)], [(4, 0.3158594896), (5, 0.3158594896), (6, 0.3158594896), (7, 0.3372762585)], [(4, 0.4211652658), (5, 0.4211652658), (6, 0.4211652658), (7, 0.4497116616)], [], [], [], []] + D: [0.0655988548, 0.1474132247, 0.3372762585, 0.4497116616, 0, 0, 0, 0] + nE: [0.0327994274, 0.0737066122, 0.1686381293, 0.2248558307, 0, 0, 0, 0.5] + E: [32799427, 73706612, 168638129, 224855830, 0, 0, 0, 500000000] + P: [0.0327994274, 0.0737066122, 0.1686381293, 0.2248558307, 0, 0, 0, 0.5] + emaB: [[(4, 0.1922789337), (5, 0.1922789337), (6, 0.1922789337), (7, 0.1458686984)], [(4, 0.4321202405), (5, 0.4321202405), (6, 0.4321202405), (7, 0.3277949789)], [(4, 0.749965667), (5, 0.749965667), (6, 0.749965667), (7, 0.74998335)], [(4, 1), (5, 1), (6, 1), (7, 1)], [], [], [], []] */ + let bonds = SubtensorModule::get_bonds( netuid.into() ); + assert_eq!(bonds[0][7], 9559); + assert_eq!(bonds[1][7], 21482); + assert_eq!(bonds[2][7], 49150); + assert_eq!(bonds[3][7], 65535); + + next_block_no_epoch(netuid); + + if sparse { SubtensorModule::epoch( netuid, 1_000_000_000 .into()); } + else { SubtensorModule::epoch_dense( netuid, 1_000_000_000.into() ); } + /* current_block: 7 + B: [[(4, 12600), (5, 12600), (6, 12600), (7, 9559)], [(4, 28318), (5, 28318), (6, 28318), (7, 21482)], [(4, 49148), (5, 49148), (6, 49148), (7, 49150)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] + B (outdatedmask): [[(4, 12600), (5, 12600), (6, 12600), (7, 9559)], [(4, 28318), (5, 28318), (6, 28318), (7, 21482)], [(4, 49148), (5, 49148), (6, 49148), (7, 49150)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] + B (mask+norm): [[(4, 0.0809763432), (5, 0.0809763432), (6, 0.0809763432), (7, 0.065595707)], [(4, 0.1819911182), (5, 0.1819911182), (6, 0.1819911182), (7, 0.1474136391)], [(4, 0.3158591525), (5, 0.3158591525), (6, 0.3158591525), (7, 0.337276807)], [(4, 0.4211733856), (5, 0.4211733856), (6, 0.4211733856), (7, 0.4497138464)], [], [], [], []] + ΔB: [[], [], [(7, 0.1200025633)], [(7, 0.1600034179)], [], [], [], []] + ΔB (norm): [[], [], [(7, 0.4285714284)], [(7, 0.5714285714)], [], [], [], []] + emaB: [[(4, 0.080976343), (5, 0.080976343), (6, 0.080976343), (7, 0.0590361361)], [(4, 0.181991118), (5, 0.181991118), (6, 0.181991118), (7, 0.1326722752)], [(4, 0.3158591525), (5, 0.3158591525), (6, 0.3158591525), (7, 0.3464062694)], [(4, 0.4211733858), (5, 0.4211733858), (6, 0.4211733858), (7, 0.4618853189)], [], [], [], []] + D: [0.0590361361, 0.1326722752, 0.3464062694, 0.4618853189, 0, 0, 0, 0] + nE: [0.029518068, 0.0663361375, 0.1732031347, 0.2309426593, 0, 0, 0, 0.5] + E: [29518068, 66336137, 173203134, 230942659, 0, 0, 0, 500000000] + P: [0.029518068, 0.0663361375, 0.1732031347, 0.2309426593, 0, 0, 0, 0.5] + emaB: [[(4, 0.192263675), (5, 0.192263675), (6, 0.192263675), (7, 0.1278155716)], [(4, 0.4321049813), (5, 0.4321049813), (6, 0.4321049813), (7, 0.2872407278)], [(4, 0.7499504078), (5, 0.7499504078), (6, 0.7499504078), (7, 0.7499832863)], [(4, 1), (5, 1), (6, 1), (7, 1)], [], [], [], []] */ + let bonds = SubtensorModule::get_bonds( netuid.into() ); + assert_eq!(bonds[0][7], 8376); + assert_eq!(bonds[1][7], 18824); + assert_eq!(bonds[2][7], 49150); + assert_eq!(bonds[3][7], 65535); + + next_block_no_epoch(netuid); + + if sparse { SubtensorModule::epoch( netuid, 1_000_000_000 .into()); } + else { SubtensorModule::epoch_dense( netuid, 1_000_000_000.into() ); } + /* current_block: 8 + B: [[(4, 12599), (5, 12599), (6, 12599), (7, 8376)], [(4, 28317), (5, 28317), (6, 28317), (7, 18824)], [(4, 49147), (5, 49147), (6, 49147), (7, 49150)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] + B (outdatedmask): [[(4, 12599), (5, 12599), (6, 12599), (7, 8376)], [(4, 28317), (5, 28317), (6, 28317), (7, 18824)], [(4, 49147), (5, 49147), (6, 49147), (7, 49150)], [(4, 65535), (5, 65535), (6, 65535), (7, 65535)], [], [], [], []] + B (mask+norm): [[(4, 0.0809714776), (5, 0.0809714776), (6, 0.0809714776), (7, 0.0590337245)], [(4, 0.1819882002), (5, 0.1819882002), (6, 0.1819882002), (7, 0.1326708249)], [(4, 0.3158588156), (5, 0.3158588156), (6, 0.3158588156), (7, 0.3464073015)], [(4, 0.421181506), (5, 0.421181506), (6, 0.421181506), (7, 0.4618881487)], [], [], [], []] + ΔB: [[], [], [(7, 0.1200025633)], [(7, 0.1600034179)], [], [], [], []] + ΔB (norm): [[], [], [(7, 0.4285714284)], [(7, 0.5714285714)], [], [], [], []] + emaB: [[(4, 0.0809714776), (5, 0.0809714776), (6, 0.0809714776), (7, 0.053130352)], [(4, 0.1819882002), (5, 0.1819882002), (6, 0.1819882002), (7, 0.1194037423)], [(4, 0.3158588156), (5, 0.3158588156), (6, 0.3158588156), (7, 0.3546237142)], [(4, 0.4211815062), (5, 0.4211815062), (6, 0.4211815062), (7, 0.472842191)], [], [], [], []] + D: [0.053130352, 0.1194037423, 0.3546237142, 0.472842191, 0, 0, 0, 0] + nE: [0.026565176, 0.0597018711, 0.177311857, 0.2364210954, 0, 0, 0, 0.5] + E: [26565175, 59701871, 177311856, 236421095, 0, 0, 0, 500000000] + P: [0.026565176, 0.0597018711, 0.177311857, 0.2364210954, 0, 0, 0, 0.5] + emaB: [[(4, 0.1922484161), (5, 0.1922484161), (6, 0.1922484161), (7, 0.1123638137)], [(4, 0.4320897225), (5, 0.4320897225), (6, 0.4320897225), (7, 0.2525234516)], [(4, 0.7499351487), (5, 0.7499351487), (6, 0.7499351487), (7, 0.7499832308)], [(4, 1), (5, 1), (6, 1), (7, 1)], [], [], [], []] */ + }); +} + +// Test that recently/deregistered miner bonds are cleared before EMA. +#[test] +fn test_deregistered_miner_bonds() { + new_test_ext(1).execute_with(|| { + let sparse: bool = true; + let n: u16 = 4; + let netuid = NetUid::from(1); + let high_tempo: u16 = u16::MAX - 1; // high tempo to skip automatic epochs in on_initialize, use manual epochs instead + + let stake: TaoBalance = 1.into(); + add_network_disable_commit_reveal(netuid, high_tempo, 0); + SubtensorModule::set_max_allowed_uids(netuid, n); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_max_registrations_per_block(netuid, n); + SubtensorModule::set_target_registrations_per_interval(netuid, n); + SubtensorModule::set_min_allowed_weights(netuid, 0); + SubtensorModule::set_bonds_penalty(netuid, u16::MAX); + assert_eq!(SubtensorModule::get_registrations_this_block(netuid), 0); + + // === Register [validator1, validator2, server1, server2] + let block_number = System::block_number(); + for key in 0..n as u64 { + add_balance_to_coldkey_account( + &U256::from(key), + stake + + ExistentialDeposit::get() + + (SubtensorModule::get_network_min_lock() * 2.into()), + ); + let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( + netuid, + block_number, + key * 1_000_000, + &U256::from(key), + ); + assert_ok!(SubtensorModule::register( + RuntimeOrigin::signed(U256::from(key)), + netuid, + block_number, + nonce, + work, + U256::from(key), + U256::from(key) + )); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &U256::from(key), + &U256::from(key), + netuid, + AlphaBalance::from(stake.to_u64()), + ); + } + assert_eq!(SubtensorModule::get_subnetwork_n(netuid), n); + assert_eq!(SubtensorModule::get_registrations_this_block(netuid), 4); + + // === Issue validator permits + SubtensorModule::set_max_allowed_validators(netuid, n); + assert_eq!(SubtensorModule::get_max_allowed_validators(netuid), n); + SubtensorModule::epoch(netuid, 1_000_000_000.into()); // run first epoch to set allowed validators + assert_eq!(SubtensorModule::get_registrations_this_block(netuid), 4); + next_block(); // run to next block to ensure weights are set on nodes after their registration block + assert_eq!(SubtensorModule::get_registrations_this_block(netuid), 0); + + // === Set weights [val1->srv1: 2/3, val1->srv2: 1/3, val2->srv1: 2/3, val2->srv2: 1/3] + for uid in 0..(n / 2) as u64 { + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(uid)), + netuid, + ((n / 2)..n).collect(), + vec![2 * (u16::MAX / 3), u16::MAX / 3], + 0 + )); + } + + // Set tempo high so we don't automatically run epochs + SubtensorModule::set_tempo_unchecked(netuid, high_tempo); + + // Run 2 blocks + next_block(); + next_block(); + + // set tempo to 2 blocks + SubtensorModule::set_tempo_unchecked(netuid, 2); + // Run epoch + if sparse { + SubtensorModule::epoch(netuid, 1_000_000_000.into()); + } else { + SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); + } + + // Check the bond values for the servers + let bonds = SubtensorModule::get_bonds(netuid.into()); + let bond_0_2 = bonds[0][2]; + let bond_0_3 = bonds[0][3]; + + // Non-zero bonds + assert!(bond_0_2 > 0); + assert!(bond_0_3 > 0); + + // Set tempo high so we don't automatically run epochs + SubtensorModule::set_tempo_unchecked(netuid, high_tempo); + + // Run one more block + next_block(); + + // === Dereg server2 at uid3 (least emission) + register new key over uid3 + let new_key: u64 = n as u64; // register a new key while at max capacity, which means the least incentive uid will be deregistered + let block_number = System::block_number(); + add_balance_to_coldkey_account( + &U256::from(new_key), + stake + + ExistentialDeposit::get() + + (SubtensorModule::get_network_min_lock() * 2.into()), + ); + let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( + netuid, + block_number, + 0, + &U256::from(new_key), + ); + assert_eq!(SubtensorModule::get_max_registrations_per_block(netuid), n); + assert_eq!(SubtensorModule::get_registrations_this_block(netuid), 0); + assert_ok!(SubtensorModule::register( + RuntimeOrigin::signed(U256::from(new_key)), + netuid, + block_number, + nonce, + work, + U256::from(new_key), + U256::from(new_key) + )); + let deregistered_uid: u16 = n - 1; // since uid=n-1 only recieved 1/3 of weight, it will get pruned first + assert_eq!( + U256::from(new_key), + SubtensorModule::get_hotkey_for_net_and_uid(netuid, deregistered_uid) + .expect("Not registered") + ); + + // Set weights again so they're active. + for uid in 0..(n / 2) as u64 { + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(uid)), + netuid, + ((n / 2)..n).collect(), + vec![2 * (u16::MAX / 3), u16::MAX / 3], + 0 + )); + } + + // Run 1 block + next_block(); + // Assert block at registration happened after the last tempo + let block_at_registration = SubtensorModule::get_neuron_block_at_registration(netuid, 3); + let block_number = System::block_number(); + assert!( + block_at_registration >= block_number - 2, + "block at registration: {block_at_registration}, block number: {block_number}" + ); + + // set tempo to 2 blocks + SubtensorModule::set_tempo_unchecked(netuid, 2); + // Run epoch again. + if sparse { + SubtensorModule::epoch(netuid, 1_000_000_000.into()); + } else { + SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); + } + + // Check the bond values for the servers + let bonds = SubtensorModule::get_bonds(netuid.into()); + let bond_0_2_new = bonds[0][2]; + let bond_0_3_new = bonds[0][3]; + + // We expect the old bonds for server2, (uid3), to be reset. + // For server1, (uid2), the bond should be higher than before. + assert!( + bond_0_2_new >= bond_0_2, + "bond_0_2_new: {bond_0_2_new}, bond_0_2: {bond_0_2}" + ); + assert!( + bond_0_3_new <= bond_0_3, + "bond_0_3_new: {bond_0_3_new}, bond_0_3: {bond_0_3}" + ); + }); +} diff --git a/pallets/subtensor/src/tests/epoch/epoch_input_state.rs b/pallets/subtensor/src/tests/epoch/epoch_input_state.rs new file mode 100644 index 0000000000..21a016c9f7 --- /dev/null +++ b/pallets/subtensor/src/tests/epoch/epoch_input_state.rs @@ -0,0 +1,124 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Epoch input consistency (`epoch_keys_have_unique_hotkeys`) and LastUpdate size mismatch. + +use frame_support::assert_ok; +use sp_core::U256; +use subtensor_runtime_common::{NetUidStorageIndex, TaoBalance}; + +use super::super::mock::*; +use crate::*; + +// Test an epoch doesn't panic when LastUpdate size doesn't match to Weights size. +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::epoch::epoch_input_state::test_last_update_size_mismatch --exact --show-output --nocapture +#[test] +fn test_last_update_size_mismatch() { + new_test_ext(1).execute_with(|| { + log::info!("test_1_graph:"); + let netuid = NetUid::from(1); + let coldkey = U256::from(0); + let hotkey = U256::from(0); + let uid: u16 = 0; + let stake_amount: u64 = 1_000_000_000; + add_network_disable_commit_reveal(netuid, u16::MAX - 1, 0); + SubtensorModule::set_max_allowed_uids(netuid, 1); + add_balance_to_coldkey_account( + &coldkey, + TaoBalance::from(stake_amount) + + ExistentialDeposit::get() + + (SubtensorModule::get_network_min_lock() * 2.into()), + ); + register_ok_neuron(netuid, hotkey, coldkey, 1); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + stake_amount.into() + )); + + assert_eq!(SubtensorModule::get_subnetwork_n(netuid), 1); + run_to_block(1); // run to next block to ensure weights are set on nodes after their registration block + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(uid)), + netuid, + vec![uid], + vec![u16::MAX], + 0 + )); + + // Set mismatching LastUpdate vector + LastUpdate::::insert(NetUidStorageIndex::from(netuid), vec![1, 1, 1]); + + SubtensorModule::epoch(netuid, 1_000_000_000.into()); + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey), + stake_amount.into() + ); + assert_eq!(SubtensorModule::get_rank_for_uid(netuid, uid), 0); + assert_eq!(SubtensorModule::get_trust_for_uid(netuid, uid), 0); + assert_eq!(SubtensorModule::get_consensus_for_uid(netuid, uid), 0); + assert_eq!( + SubtensorModule::get_incentive_for_uid(netuid.into(), uid), + 0 + ); + assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, uid), 0); + }); +} + +#[test] +fn empty_ok() { + new_test_ext(1).execute_with(|| { + let netuid: NetUid = 155.into(); + assert!(Pallet::::epoch_keys_have_unique_hotkeys(netuid)); + }); +} + +#[test] +fn unique_hotkeys_and_uids_ok() { + new_test_ext(1).execute_with(|| { + let netuid: NetUid = 155.into(); + + // (netuid, uid) -> hotkey (AccountId = U256) + Keys::::insert(netuid, 0u16, U256::from(1u64)); + Keys::::insert(netuid, 1u16, U256::from(2u64)); + Keys::::insert(netuid, 2u16, U256::from(3u64)); + + assert!(Pallet::::epoch_keys_have_unique_hotkeys(netuid)); + }); +} + +#[test] +fn duplicate_hotkey_within_same_netuid_fails() { + new_test_ext(1).execute_with(|| { + let netuid: NetUid = 155.into(); + + // Same hotkey mapped from two different UIDs in the SAME netuid + let hk = U256::from(42u64); + Keys::::insert(netuid, 0u16, hk); + Keys::::insert(netuid, 1u16, U256::from(42u64)); // duplicate hotkey + + assert!(!Pallet::::epoch_keys_have_unique_hotkeys(netuid)); + }); +} + +#[test] +fn same_hotkey_across_different_netuids_is_ok() { + new_test_ext(1).execute_with(|| { + let net_a: NetUid = 10.into(); + let net_b: NetUid = 11.into(); + + // Same hotkey appears once in each netuid — each net checks independently. + let hk = U256::from(777u64); + Keys::::insert(net_a, 0u16, hk); + Keys::::insert(net_b, 0u16, hk); + + assert!(Pallet::::epoch_keys_have_unique_hotkeys(net_a)); + assert!(Pallet::::epoch_keys_have_unique_hotkeys(net_b)); + }); +} diff --git a/pallets/subtensor/src/tests/epoch/epoch_outputs.rs b/pallets/subtensor/src/tests/epoch/epoch_outputs.rs new file mode 100644 index 0000000000..e6e6c1534e --- /dev/null +++ b/pallets/subtensor/src/tests/epoch/epoch_outputs.rs @@ -0,0 +1,158 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Epoch term outputs for minimal registered-stake topologies. + +use approx::assert_abs_diff_eq; +use sp_core::U256; +use subtensor_runtime_common::AlphaBalance; + +use super::super::mock::*; +use crate::*; + +#[test] +fn test_epoch_outputs_single_staker_registered_no_weights() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let high_tempo: u16 = u16::MAX - 1; // Don't run automatically. + add_network(netuid, high_tempo, 0); + + let hotkey = U256::from(1); + let coldkey = U256::from(2); + register_ok_neuron(netuid, hotkey, coldkey, 0); + // Give non-zero alpha + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + 1.into(), + ); + + let pending_alpha = AlphaBalance::from(1_000_000_000); + let hotkey_emission = SubtensorModule::epoch(netuid, pending_alpha); + + let sum_incentives = hotkey_emission + .iter() + .map(|(_, incentive, _)| incentive) + .copied() + .fold(AlphaBalance::ZERO, |acc, x| acc + x); + let sum_dividends: AlphaBalance = hotkey_emission + .iter() + .map(|(_, _, dividend)| dividend) + .copied() + .fold(AlphaBalance::ZERO, |acc, x| acc + x); + + assert_abs_diff_eq!( + sum_incentives.saturating_add(sum_dividends), + pending_alpha, + epsilon = 1_000.into() + ); + }); +} + +// Map the retention graph for consensus guarantees with an single epoch on a graph with 512 nodes, +// of which the first 64 are validators, the graph is split into a major and minor set, each setting +// specific weight on itself and the complement on the other. +// +// ```import torch +// import matplotlib.pyplot as plt +// from matplotlib.pyplot import cm +// %matplotlib inline +// +// with open('finney_consensus_0.4.txt') as f: # test output saved to finney_consensus.txt +// retention_map = eval(f.read()) +// +// major_ratios = {} +// avg_weight_devs = {} +// for major_stake, major_weight, minor_weight, avg_weight_dev, major_ratio in retention_map: +// major_stake = f'{major_stake:.2f}' +// maj, min = int(round(50 * major_weight)), int(round(50 * minor_weight)) +// avg_weight_devs.setdefault(major_stake, torch.zeros((51, 51))) +// avg_weight_devs[major_stake][maj][min] = avg_weight_dev +// major_ratios.setdefault(major_stake, torch.zeros((51, 51))) +// major_ratios[major_stake][maj][min] = major_ratio +// +// _x = torch.linspace(0, 1, 51); _y = torch.linspace(0, 1, 51) +// x, y = torch.meshgrid(_x, _y, indexing='ij') +// +// fig = plt.figure(figsize=(6, 6), dpi=70); ax = fig.gca() +// ax.set_xticks(torch.arange(0, 1, 0.05)); ax.set_yticks(torch.arange(0, 1., 0.05)) +// ax.set_xticklabels([f'{_:.2f}'[1:] for _ in torch.arange(0, 1., 0.05)]) +// plt.grid(); plt.rc('grid', linestyle="dotted", color=[0.85, 0.85, 0.85]) +// +// isolate = ['0.60']; stakes = [0.51, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99] +// colors = cm.viridis(torch.linspace(0, 1, len(stakes) + 1)) +// for i, stake in enumerate(stakes): +// contours = plt.contour(x, y, major_ratios[f'{stake:.2f}'], levels=[0., stake], colors=[colors[i + 1]]) +// if f'{stake:.2f}' in isolate: +// contours.collections[1].set_linewidth(3) +// plt.clabel(contours, inline=True, fontsize=10) +// +// plt.title(f'Major emission [$stake_{{maj}}=emission_{{maj}}$ retention lines]') +// plt.ylabel('Minor self-weight'); plt.xlabel('Major self-weight'); plt.show() +// ``` +// #[test] +// fn _map_consensus_guarantees() { +// let netuid = NetUid::from(1); +// let network_n: u16 = 512; +// let validators_n: u16 = 64; +// let epochs: u16 = 1; +// let interleave = 0; +// let weight_stddev: I32F32 = fixed(0.4); +// let bonds_penalty: u16 = u16::MAX; +// println!("["); +// for _major_stake in vec![0.51, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99] { +// let major_stake: I32F32 = I32F32::from_num(_major_stake); +// for _major_weight in 0..51 { +// let major_weight: I32F32 = I32F32::from_num(50 - _major_weight) / I32F32::from_num(50); +// for _minor_weight in 0..51 { +// let minor_weight: I32F32 = +// I32F32::from_num(50 - _minor_weight) / I32F32::from_num(50); +// let ( +// validators, +// servers, +// major_validators, +// minor_validators, +// major_servers, +// minor_servers, +// stake, +// weights, +// avg_weight_dev, +// ) = split_graph( +// major_stake, +// major_weight, +// minor_weight, +// weight_stddev, +// validators_n as usize, +// network_n as usize, +// interleave as usize, +// ); +// +// new_test_ext(1).execute_with(|| { +// init_run_epochs(netuid, network_n, &validators, &servers, epochs, 1, true, &stake, true, &weights, true, false, 0, true, bonds_penalty); +// +// let mut major_emission: I64F64 = I64F64::from_num(0); +// let mut minor_emission: I64F64 = I64F64::from_num(0); +// for set in vec![major_validators, major_servers] { +// for uid in set { +// major_emission += I64F64::from_num(SubtensorModule::get_emission_for_uid( netuid, uid )); +// } +// } +// for set in vec![minor_validators, minor_servers] { +// for uid in set { +// minor_emission += I64F64::from_num(SubtensorModule::get_emission_for_uid( netuid, uid )); +// } +// } +// let major_ratio: I32F32 = I32F32::from_num(major_emission / (major_emission + minor_emission)); +// println!("[{major_stake}, {major_weight:.2}, {minor_weight:.2}, {avg_weight_dev:.3}, {major_ratio:.3}], "); +// }); +// } +// } +// } +// println!("]"); +// } + +// Helpers diff --git a/pallets/subtensor/src/tests/epoch/epoch_timing.rs b/pallets/subtensor/src/tests/epoch/epoch_timing.rs new file mode 100644 index 0000000000..be7e89eee1 --- /dev/null +++ b/pallets/subtensor/src/tests/epoch/epoch_timing.rs @@ -0,0 +1,58 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Blocks-since-last-step bookkeeping around epoch. + +use super::super::mock::*; +use crate::*; + +#[test] +fn test_blocks_since_last_step() { + new_test_ext(1).execute_with(|| { + System::set_block_number(0); + + let netuid = NetUid::from(1); + let tempo: u16 = 7200; + add_network(netuid, tempo, 0); + + let original_blocks: u64 = SubtensorModule::get_blocks_since_last_step(netuid); + + step_block(5); + + let new_blocks: u64 = SubtensorModule::get_blocks_since_last_step(netuid); + + assert!(new_blocks > original_blocks); + assert_eq!(new_blocks, 5); + + let blocks_to_step: u16 = SubtensorModule::blocks_until_next_auto_epoch( + netuid, + tempo, + SubtensorModule::get_current_block_as_u64(), + ) as u16 + + 10; + step_block(blocks_to_step); + + let post_blocks: u64 = SubtensorModule::get_blocks_since_last_step(netuid); + + assert_eq!(post_blocks, 10); + + let blocks_to_step: u16 = SubtensorModule::blocks_until_next_auto_epoch( + netuid, + tempo, + SubtensorModule::get_current_block_as_u64(), + ) as u16 + + 20; + step_block(blocks_to_step); + + let new_post_blocks: u64 = SubtensorModule::get_blocks_since_last_step(netuid); + + assert_eq!(new_post_blocks, 20); + + step_block(7); + + assert_eq!(SubtensorModule::get_blocks_since_last_step(netuid), 27); + }); +} diff --git a/pallets/subtensor/src/tests/epoch/graph_epochs.rs b/pallets/subtensor/src/tests/epoch/graph_epochs.rs new file mode 100644 index 0000000000..756f0efd03 --- /dev/null +++ b/pallets/subtensor/src/tests/epoch/graph_epochs.rs @@ -0,0 +1,466 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Small and large bipartite graph epoch runs (`epoch` / `epoch_dense`). + +use frame_support::assert_ok; +use sp_core::U256; +use substrate_fixed::types::I32F32; +use subtensor_runtime_common::{AlphaBalance, TaoBalance}; + +use super::super::mock::*; +use super::helpers::{distribute_nodes, init_run_epochs}; +use crate::*; + +// Test an epoch on a graph with a single item. +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::epoch::graph_epochs::test_1_graph --exact --show-output --nocapture +#[test] +fn test_1_graph() { + new_test_ext(1).execute_with(|| { + log::info!("test_1_graph:"); + let netuid = NetUid::from(1); + let coldkey = U256::from(0); + let hotkey = U256::from(0); + let uid: u16 = 0; + let stake_amount: TaoBalance = 1_000_000_000.into(); + add_network_disable_commit_reveal(netuid, u16::MAX - 1, 0); // set higher tempo to avoid built-in epoch, then manual epoch instead + SubtensorModule::set_max_allowed_uids(netuid, 1); + add_balance_to_coldkey_account( + &coldkey, + stake_amount + ExistentialDeposit::get() + SubtensorModule::get_network_min_lock(), + ); + register_ok_neuron(netuid, hotkey, coldkey, 1); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + stake_amount.into() + )); + + assert_eq!(SubtensorModule::get_subnetwork_n(netuid), 1); + run_to_block(1); // run to next block to ensure weights are set on nodes after their registration block + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(uid)), + netuid, + vec![uid], + vec![u16::MAX], + 0 + )); + // SubtensorModule::set_weights_for_testing( netuid, i as u16, vec![ ( 0, u16::MAX )]); // doesn't set update status + // SubtensorModule::set_bonds_for_testing( netuid, uid, vec![ ( 0, u16::MAX )]); // rather, bonds are calculated in epoch + SubtensorModule::epoch(netuid, 1_000_000_000.into()); + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey), + stake_amount.into() + ); + assert_eq!(SubtensorModule::get_rank_for_uid(netuid, uid), 0); + assert_eq!(SubtensorModule::get_trust_for_uid(netuid, uid), 0); + assert_eq!(SubtensorModule::get_consensus_for_uid(netuid, uid), 0); + assert_eq!( + SubtensorModule::get_incentive_for_uid(netuid.into(), uid), + 0 + ); + assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, uid), 0); + }); +} +// Test an epoch on a graph with two items. +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::epoch::graph_epochs::test_10_graph --exact --show-output --nocapture +#[test] +fn test_10_graph() { + new_test_ext(1).execute_with(|| { + log::info!("test_10_graph"); + // Function for adding a nodes to the graph. + pub fn add_node(netuid: NetUid, coldkey: U256, hotkey: U256, uid: u16, stake_amount: u64) { + log::info!( + "+Add net:{:?} coldkey:{:?} hotkey:{:?} uid:{:?} stake_amount: {:?} subn: {:?}", + netuid, + coldkey, + hotkey, + uid, + stake_amount, + SubtensorModule::get_subnetwork_n(netuid), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + stake_amount.into(), + ); + SubtensorModule::append_neuron(netuid, &hotkey, 0); + assert_eq!(SubtensorModule::get_subnetwork_n(netuid) - 1, uid); + } + // Build the graph with 10 items + // each with 1 stake and self weights. + let n: usize = 10; + let netuid = NetUid::from(1); + add_network_disable_commit_reveal(netuid, u16::MAX - 1, 0); // set higher tempo to avoid built-in epoch, then manual epoch instead + SubtensorModule::set_max_allowed_uids(netuid, n as u16); + for i in 0..10 { + add_node(netuid, U256::from(i), U256::from(i), i as u16, 1) + } + assert_eq!(SubtensorModule::get_subnetwork_n(netuid), 10); + run_to_block(1); // run to next block to ensure weights are set on nodes after their registration block + for i in 0..10 { + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(i)), + netuid, + vec![i as u16], + vec![u16::MAX], + 0 + )); + } + // Run the epoch. + SubtensorModule::epoch(netuid, 1_000_000_000.into()); + // Check return values. + for i in 0..n { + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&(U256::from(i))), + TaoBalance::from(1) + ); + assert_eq!(SubtensorModule::get_rank_for_uid(netuid, i as u16), 0); + assert_eq!(SubtensorModule::get_trust_for_uid(netuid, i as u16), 0); + assert_eq!(SubtensorModule::get_consensus_for_uid(netuid, i as u16), 0); + assert_eq!( + SubtensorModule::get_incentive_for_uid(netuid.into(), i as u16), + 0 + ); + assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, i as u16), 0); + assert_eq!( + SubtensorModule::get_emission_for_uid(netuid, i as u16), + 99999999.into() + ); + } + }); +} + +// Test an epoch on a graph with 512 nodes, of which the first 64 are validators setting non-self weights, and the rest servers setting only self-weights. +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::epoch::graph_epochs::test_512_graph --exact --show-output --nocapture +#[test] +fn test_512_graph() { + let netuid = NetUid::from(1); + let network_n: u16 = 512; + let validators_n: u16 = 64; + let max_stake_per_validator: u64 = 328_125_000_000_000; // 21_000_000_000_000_000 / 64 + let epochs: u16 = 3; + log::info!("test_{network_n:?}_graph ({validators_n:?} validators)"); + for interleave in 0..3 { + for server_self in [false, true] { + // server-self weight off/on + let (validators, servers) = distribute_nodes( + validators_n as usize, + network_n as usize, + interleave as usize, + ); + let server: usize = servers[0] as usize; + let validator: usize = validators[0] as usize; + new_test_ext(1).execute_with(|| { + init_run_epochs( + netuid, + network_n, + &validators, + &servers, + epochs, + max_stake_per_validator, + server_self, + &[], + false, + &[], + false, + false, + 0, + false, + u16::MAX, + ); + let bonds = SubtensorModule::get_bonds(netuid.into()); + for uid in validators { + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&(U256::from(uid))), + max_stake_per_validator.into() + ); + assert_eq!(SubtensorModule::get_consensus_for_uid(netuid, uid), 0); + assert_eq!( + SubtensorModule::get_incentive_for_uid(netuid.into(), uid), + 0 + ); + assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, uid), 1023); // floor(1 / 64 * 65_535) + assert_eq!( + SubtensorModule::get_emission_for_uid(netuid, uid), + 7812500.into() + ); // 0.5 / 200 * 1_000_000_000 + assert_eq!(bonds[uid as usize][validator], 0.0); + assert_eq!(bonds[uid as usize][server], I32F32::from_num(65_535)); + } + for uid in servers { + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&(U256::from(uid))), + TaoBalance::ZERO + ); + assert_eq!(SubtensorModule::get_consensus_for_uid(netuid, uid), 146); + assert_eq!( + SubtensorModule::get_incentive_for_uid(netuid.into(), uid), + 146 + ); // floor(1 / (512 - 64) * 65_535) + assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, uid), 0); + assert_eq!( + SubtensorModule::get_emission_for_uid(netuid, uid), + 1116071.into() + ); // floor(0.5 / (512 - 64) * 1_000_000_000) + assert_eq!(bonds[uid as usize][validator], 0.0); + assert_eq!(bonds[uid as usize][server], 0.0); + } + }); + } + } +} + +// Test an epoch on a graph with 4096 nodes, of which the first 256 are validators setting random non-self weights, and the rest servers setting only self-weights. +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::epoch::graph_epochs::test_512_graph_random_weights --exact --show-output --nocapture +#[test] +fn test_512_graph_random_weights() { + let netuid = NetUid::from(1); + let network_n: u16 = 512; + let validators_n: u16 = 64; + let epochs: u16 = 1; + log::info!("test_{network_n:?}_graph_random_weights ({validators_n:?} validators)"); + for interleave in 0..3 { + // server-self weight off/on + for server_self in [false, true] { + for bonds_penalty in [0, u16::MAX / 2, u16::MAX] { + let (validators, servers) = distribute_nodes( + validators_n as usize, + network_n as usize, + interleave as usize, + ); + let server: usize = servers[0] as usize; + let validator: usize = validators[0] as usize; + let (mut rank, mut incentive, mut dividend, mut emission, mut bondv, mut bonds): ( + Vec, + Vec, + Vec, + Vec, + Vec, + Vec, + ) = (vec![], vec![], vec![], vec![], vec![], vec![]); + + // Dense epoch + new_test_ext(1).execute_with(|| { + init_run_epochs( + netuid, + network_n, + &validators, + &servers, + epochs, + 1, + server_self, + &[], + false, + &[], + false, + true, + interleave as u64, + false, + bonds_penalty, + ); + + let bond = SubtensorModule::get_bonds(netuid.into()); + for uid in 0..network_n { + rank.push(SubtensorModule::get_rank_for_uid(netuid, uid)); + incentive.push(SubtensorModule::get_incentive_for_uid(netuid.into(), uid)); + dividend.push(SubtensorModule::get_dividends_for_uid(netuid, uid)); + emission.push(SubtensorModule::get_emission_for_uid(netuid, uid)); + bondv.push(bond[uid as usize][validator]); + bonds.push(bond[uid as usize][server]); + } + }); + + // Sparse epoch (same random seed as dense) + new_test_ext(1).execute_with(|| { + init_run_epochs( + netuid, + network_n, + &validators, + &servers, + epochs, + 1, + server_self, + &[], + false, + &[], + false, + true, + interleave as u64, + true, + bonds_penalty, + ); + // Assert that dense and sparse epoch results are equal + let bond = SubtensorModule::get_bonds(netuid.into()); + for uid in 0..network_n { + assert_eq!( + SubtensorModule::get_rank_for_uid(netuid, uid), + rank[uid as usize] + ); + assert_eq!( + SubtensorModule::get_incentive_for_uid(netuid.into(), uid), + incentive[uid as usize] + ); + assert_eq!( + SubtensorModule::get_dividends_for_uid(netuid, uid), + dividend[uid as usize] + ); + assert_eq!( + SubtensorModule::get_emission_for_uid(netuid, uid), + emission[uid as usize] + ); + assert_eq!(bond[uid as usize][validator], bondv[uid as usize]); + assert_eq!(bond[uid as usize][server], bonds[uid as usize]); + } + }); + } + } + } +} + +// Test an epoch on a graph with 4096 nodes, of which the first 256 are validators setting non-self weights, and the rest servers setting only self-weights. +// #[test] +// fn test_4096_graph() { +// let netuid = NetUid::from(1); +// let network_n: u16 = 4096; +// let validators_n: u16 = 256; +// let epochs: u16 = 1; +// let max_stake_per_validator: u64 = 82_031_250_000_000; // 21_000_000_000_000_000 / 256 +// log::info!("test_{network_n:?}_graph ({validators_n:?} validators)"); +// for interleave in 0..3 { +// let (validators, servers) = distribute_nodes( +// validators_n as usize, +// network_n as usize, +// interleave as usize, +// ); +// let server: usize = servers[0] as usize; +// let validator: usize = validators[0] as usize; +// for server_self in [false, true] { +// // server-self weight off/on +// new_test_ext(1).execute_with(|| { +// init_run_epochs( +// netuid, +// network_n, +// &validators, +// &servers, +// epochs, +// max_stake_per_validator, +// server_self, +// &[], +// false, +// &[], +// false, +// false, +// 0, +// true, +// u16::MAX, +// ); +// let (total_stake, _, _) = SubtensorModule::get_stake_weights_for_network(netuid); +// assert_eq!(total_stake.iter().map(|s| s.to_num::()).sum::(), 21_000_000_000_000_000); +// let bonds = SubtensorModule::get_bonds(netuid); +// for uid in &validators { +// assert_eq!( +// SubtensorModule::get_total_stake_for_hotkey(&(U256::from(*uid as u64))), +// max_stake_per_validator +// ); +// assert_eq!(SubtensorModule::get_rank_for_uid(netuid, *uid), 0); +// assert_eq!(SubtensorModule::get_trust_for_uid(netuid, *uid), 0); +// assert_eq!(SubtensorModule::get_consensus_for_uid(netuid, *uid), 0); +// assert_eq!(SubtensorModule::get_incentive_for_uid(netuid, *uid), 0); +// assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, *uid), 255); // Note D = floor(1 / 256 * 65_535) +// assert_eq!(SubtensorModule::get_emission_for_uid(netuid, *uid), 1953125); // Note E = 0.5 / 256 * 1_000_000_000 = 1953125 +// assert_eq!(bonds[*uid as usize][validator], 0.0); +// assert_eq!( +// bonds[*uid as usize][server], +// I32F32::from_num(255) / I32F32::from_num(65_535) +// ); // Note B_ij = floor(1 / 256 * 65_535) / 65_535 +// } +// for uid in &servers { +// assert_eq!( +// SubtensorModule::get_total_stake_for_hotkey(&(U256::from(*uid as u64))), +// 0 +// ); +// assert_eq!(SubtensorModule::get_rank_for_uid(netuid, *uid), 17); // Note R = floor(1 / (4096 - 256) * 65_535) = 17 +// assert_eq!(SubtensorModule::get_trust_for_uid(netuid, *uid), 65535); +// assert_eq!(SubtensorModule::get_consensus_for_uid(netuid, *uid), 17); // Note C = floor(1 / (4096 - 256) * 65_535) = 17 +// assert_eq!(SubtensorModule::get_incentive_for_uid(netuid, *uid), 17); // Note I = floor(1 / (4096 - 256) * 65_535) = 17 +// assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, *uid), 0); +// assert_eq!(SubtensorModule::get_emission_for_uid(netuid, *uid), 130208); // Note E = floor(0.5 / (4096 - 256) * 1_000_000_000) = 130208 +// assert_eq!(bonds[*uid as usize][validator], 0.0); +// assert_eq!(bonds[*uid as usize][server], 0.0); +// } +// }); +// } +// } +// } + +// Test an epoch_sparse on a graph with 16384 nodes, of which the first 512 are validators setting non-self weights, and the rest servers setting only self-weights. +// #[test] +// fn test_16384_graph_sparse() { +// new_test_ext(1).execute_with(|| { +// let netuid = NetUid::from(1); +// let n: u16 = 16384; +// let validators_n: u16 = 512; +// let validators: Vec = (0..validators_n).collect(); +// let servers: Vec = (validators_n..n).collect(); +// let server: u16 = servers[0]; +// let epochs: u16 = 1; +// log::info!("test_{n:?}_graph ({validators_n:?} validators)"); +// init_run_epochs( +// netuid, +// n, +// &validators, +// &servers, +// epochs, +// 1, +// false, +// &[], +// false, +// &[], +// false, +// false, +// 0, +// true, +// u16::MAX, +// ); +// let bonds = SubtensorModule::get_bonds(netuid); +// for uid in validators { +// assert_eq!( +// SubtensorModule::get_total_stake_for_hotkey(&(U256::from(uid))), +// 1 +// ); +// assert_eq!(SubtensorModule::get_rank_for_uid(netuid, uid), 0); +// assert_eq!(SubtensorModule::get_trust_for_uid(netuid, uid), 0); +// assert_eq!(SubtensorModule::get_consensus_for_uid(netuid, uid), 438); // Note C = 0.0066928507 = (0.0066928507*65_535) = floor( 438.6159706245 ) +// assert_eq!(SubtensorModule::get_incentive_for_uid(netuid, uid), 0); +// assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, uid), 127); // Note D = floor(1 / 512 * 65_535) = 127 +// assert_eq!(SubtensorModule::get_emission_for_uid(netuid, uid), 976085); // Note E = 0.5 / 512 * 1_000_000_000 = 976_562 (discrepancy) +// assert_eq!(bonds[uid as usize][0], 0.0); +// assert_eq!( +// bonds[uid as usize][server as usize], +// I32F32::from_num(127) / I32F32::from_num(65_535) +// ); // Note B_ij = floor(1 / 512 * 65_535) / 65_535 = 127 / 65_535 +// } +// for uid in servers { +// assert_eq!( +// SubtensorModule::get_total_stake_for_hotkey(&(U256::from(uid))), +// 0 +// ); +// assert_eq!(SubtensorModule::get_rank_for_uid(netuid, uid), 4); // Note R = floor(1 / (16384 - 512) * 65_535) = 4 +// assert_eq!(SubtensorModule::get_trust_for_uid(netuid, uid), 65535); +// assert_eq!(SubtensorModule::get_consensus_for_uid(netuid, uid), 4); // Note C = floor(1 / (16384 - 512) * 65_535) = 4 +// assert_eq!(SubtensorModule::get_incentive_for_uid(netuid, uid), 4); // Note I = floor(1 / (16384 - 512) * 65_535) = 4 +// assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, uid), 0); +// assert_eq!(SubtensorModule::get_emission_for_uid(netuid, uid), 31517); // Note E = floor(0.5 / (16384 - 512) * 1_000_000_000) = 31502 (discrepancy) +// assert_eq!(bonds[uid as usize][0], 0.0); +// assert_eq!(bonds[uid as usize][server as usize], 0.0); +// } +// }); +// } diff --git a/pallets/subtensor/src/tests/epoch/helpers.rs b/pallets/subtensor/src/tests/epoch/helpers.rs new file mode 100644 index 0000000000..1c4271ef2b --- /dev/null +++ b/pallets/subtensor/src/tests/epoch/helpers.rs @@ -0,0 +1,548 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Shared fixtures and numeric helpers for epoch integration tests. + +use std::time::Instant; + +use frame_support::assert_ok; +use rand::{RngExt, SeedableRng, distr::Uniform, rngs::StdRng, seq::SliceRandom}; +use sp_core::U256; +use substrate_fixed::types::I32F32; +use subtensor_runtime_common::NetUidStorageIndex; + +use super::super::mock::*; +use crate::*; + +// Normalizes (sum to 1 except 0) the input vector directly in-place. +#[allow(dead_code)] +pub(super) fn inplace_normalize(x: &mut [I32F32]) { + let x_sum: I32F32 = x.iter().sum(); + if x_sum == I32F32::from_num(0.0_f32) { + return; + } + for i in x.iter_mut() { + *i /= x_sum; + } +} + +// Inplace normalize the passed positive integer weights so that they sum to u16 max value. +#[allow(dead_code)] +pub(super) fn normalize_weights(mut weights: Vec) -> Vec { + let sum: u64 = weights.iter().map(|x| *x as u64).sum(); + if sum == 0 { + return weights; + } + weights.iter_mut().for_each(|x| { + *x = (*x as u64 * u16::MAX as u64 / sum) as u16; + }); + weights +} + +// // Return as usize an I32F32 ratio of a usize input, avoiding the 0% and 100% extremes. +// fn non_extreme_fixed_ratio(ratio: I32F32, total: usize) -> usize { +// if total == 0 { +// return total; +// } +// let mut subset: usize = (ratio * I32F32::from_num(total)).to_num::(); +// if subset == 0 { +// subset = 1; +// } else if subset == total { +// subset = total - 1; +// } +// return subset; +// } + +// // Box-Muller Transform converting two uniform random samples to a normal random sample. +// fn normal(size: usize, rng: &mut StdRng, dist: &Uniform) -> Vec { +// let max: I32F32 = I32F32::from_num(u16::MAX); +// let two: I32F32 = I32F32::from_num(2); +// let eps: I32F32 = I32F32::from_num(0.000001); +// let pi: I32F32 = I32F32::from_num(PI); + +// let uniform_u16: Vec = (0..(2 * size)).map(|_| rng.sample(&dist)).collect(); +// let uniform: Vec = uniform_u16 +// .iter() +// .map(|&x| I32F32::from_num(x) / max) +// .collect(); +// let mut normal: Vec = vec![I32F32::from_num(0); size as usize]; + +// for i in 0..size { +// let u1: I32F32 = uniform[i] + eps; +// let u2: I32F32 = uniform[i + size] + eps; +// normal[i] = sqrt::(-two * ln::(u1).expect("")).expect("") +// * cos(two * pi * u2); +// } +// normal +// } + +// Returns validators and servers uids with either blockwise, regular, or random interleaving. +pub(super) fn distribute_nodes( + validators_n: usize, + network_n: usize, + interleave: usize, +) -> (Vec, Vec) { + let mut validators: Vec = vec![]; + let mut servers: Vec = vec![]; + + if interleave == 0 { + // blockwise [validator_block, server_block] + validators = (0..validators_n as u16).collect(); + servers = (validators_n as u16..network_n as u16).collect(); + } else if interleave == 1 { + // regular interleaving [val, srv, srv, ..., srv, val, srv, srv, ..., srv, val, srv, ..., srv] + (validators, servers) = (0..network_n as u16) + .collect::>() + .iter() + .partition(|&i| *i as usize % (network_n / validators_n) == 0); + } else if interleave == 2 { + // random interleaving + let mut permuted_uids: Vec = (0..network_n as u16).collect(); + permuted_uids.shuffle(&mut rand::rng()); + validators = permuted_uids[0..validators_n].into(); + servers = permuted_uids[validators_n..network_n].into(); + } + + (validators, servers) +} + +#[allow(dead_code)] +pub(super) fn uid_stats(netuid: NetUid, uid: u16) { + log::info!( + "stake: {:?}", + SubtensorModule::get_total_stake_for_hotkey(&(U256::from(uid))) + ); + log::info!("rank: {:?}", SubtensorModule::get_rank_for_uid(netuid, uid)); + log::info!( + "trust: {:?}", + SubtensorModule::get_trust_for_uid(netuid, uid) + ); + log::info!( + "consensus: {:?}", + SubtensorModule::get_consensus_for_uid(netuid, uid) + ); + log::info!( + "incentive: {:?}", + SubtensorModule::get_incentive_for_uid(NetUidStorageIndex::from(netuid), uid) + ); + log::info!( + "dividend: {:?}", + SubtensorModule::get_dividends_for_uid(netuid, uid) + ); + log::info!( + "emission: {:?}", + SubtensorModule::get_emission_for_uid(netuid, uid) + ); +} + +#[allow(clippy::too_many_arguments)] +pub(super) fn init_run_epochs( + netuid: NetUid, + n: u16, + validators: &[u16], + servers: &[u16], + epochs: u16, + stake_per_validator: u64, + server_self: bool, + input_stake: &[u64], + use_input_stake: bool, + input_weights: &[Vec<(u16, u16)>], + use_input_weights: bool, + random_weights: bool, + random_seed: u64, + sparse: bool, + bonds_penalty: u16, +) { + // === Create the network + add_network_disable_commit_reveal(netuid, u16::MAX - 1, 0); // set higher tempo to avoid built-in epoch, then manual epoch instead + + // === Set bonds penalty + SubtensorModule::set_bonds_penalty(netuid, bonds_penalty); + + // === Register uids + SubtensorModule::set_max_allowed_uids(netuid, n); + for key in 0..n { + let stake = if use_input_stake { + input_stake[key as usize] + } else if validators.contains(&key) { + stake_per_validator + } else { + // only validators receive stake + 0 + }; + + // let stake: u64 = 1; // alternative test: all nodes receive stake, should be same outcome, except stake + add_balance_to_coldkey_account(&(U256::from(key)), stake.into()); + SubtensorModule::append_neuron(netuid, &(U256::from(key)), 0); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &U256::from(key), + &U256::from(key), + netuid, + stake.into(), + ); + } + assert_eq!(SubtensorModule::get_subnetwork_n(netuid), n); + + // === Issue validator permits + SubtensorModule::set_max_allowed_validators(netuid, validators.len() as u16); + assert_eq!( + SubtensorModule::get_max_allowed_validators(netuid), + validators.len() as u16 + ); + SubtensorModule::epoch(netuid, 1_000_000_000.into()); // run first epoch to set allowed validators + run_to_block(1); // run to next block to ensure weights are set on nodes after their registration block + + // === Set weights + let mut rng = StdRng::seed_from_u64(random_seed); // constant seed so weights over multiple runs are equal + let range = Uniform::new(0, u16::MAX).unwrap(); + let mut weights: Vec = vec![u16::MAX / n; servers.len()]; + for uid in validators { + if random_weights { + weights = (0..servers.len()).map(|_| rng.sample(range)).collect(); + weights = normalize_weights(weights); + // assert_eq!(weights.iter().map(|x| *x as u64).sum::(), u16::MAX as u64); // normalized weight sum not always u16::MAX + } + if use_input_weights { + let sparse_weights = input_weights[*uid as usize].clone(); + weights = sparse_weights.iter().map(|(_, w)| *w).collect(); + let srvs: Vec = sparse_weights.iter().map(|(s, _)| *s).collect(); + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(*uid as u64)), + netuid, + srvs, + weights.clone(), + 0 + )); + } else { + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(*uid as u64)), + netuid, + servers.to_vec(), + weights.clone(), + 0 + )); + } + } + if server_self { + for uid in servers { + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(*uid as u64)), + netuid, + vec![*uid], + vec![u16::MAX], + 0 + )); // server self-weight + } + } + + // === Run the epochs. + log::info!("Start {epochs} epoch(s)"); + let start = Instant::now(); + for _ in 0..epochs { + if sparse { + SubtensorModule::epoch(netuid, 1_000_000_000.into()); + } else { + SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); + } + } + let duration = start.elapsed(); + log::info!("Time elapsed in (sparse={sparse}) epoch() is: {duration:?}"); + + // let bonds = SubtensorModule::get_bonds( netuid ); + // for (uid, node) in vec![ (validators[0], "validator"), (servers[0], "server") ] { + // log::info!("\n{node}" ); + // uid_stats(netuid, uid); + // log::info!("bonds: {:?} (on validator), {:?} (on server)", bonds[uid as usize][0], bonds[uid as usize][servers[0] as usize]); + // } +} + +// // Generate a random graph that is split into a major and minor set, each setting specific weight on itself and the complement on the other. +// fn split_graph( +// major_stake: I32F32, +// major_weight: I32F32, +// minor_weight: I32F32, +// weight_stddev: I32F32, +// validators_n: usize, +// network_n: usize, +// interleave: usize, +// ) -> ( +// Vec, +// Vec, +// Vec, +// Vec, +// Vec, +// Vec, +// Vec, +// Vec>, +// I32F32, +// ) { +// let servers_n: usize = network_n - validators_n; +// let major_servers_n: usize = non_extreme_fixed_ratio(major_stake, servers_n); +// let major_validators_n: usize = non_extreme_fixed_ratio(major_stake, validators_n); + +// let (validators, servers) = distribute_nodes(validators_n, network_n, interleave as usize); +// let major_validators: Vec = (0..major_validators_n).map(|i| validators[i]).collect(); +// let minor_validators: Vec = (major_validators_n..validators_n) +// .map(|i| validators[i]) +// .collect(); +// let major_servers: Vec = (0..major_servers_n).map(|i| servers[i]).collect(); +// let minor_servers: Vec = (major_servers_n..servers_n).map(|i| servers[i]).collect(); + +// let zero: I32F32 = I32F32::from_num(0); +// let one: I32F32 = I32F32::from_num(1); +// let stddev: I32F32 = I32F32::from_num(0.3); +// let total_stake: I64F64 = I64F64::from_num(21_000_000_000_000_000 as u64); +// let mut rng = StdRng::seed_from_u64(0); // constant seed so weights over multiple runs are equal +// let dist = Uniform::new(0, u16::MAX); + +// let mut stake: Vec = vec![0; network_n]; +// let mut stake_fixed: Vec = vec![zero; network_n]; +// for (ratio, vals) in vec![ +// (major_stake, &major_validators), +// (one - major_stake, &minor_validators), +// ] { +// let mut sample = normal(vals.len(), &mut rng, &dist) +// .iter() +// .map(|x: &I32F32| { +// let v: I32F32 = (stddev * x) + one; +// if v < zero { +// zero +// } else { +// v +// } +// }) +// .collect(); +// inplace_normalize(&mut sample); +// for (i, &val) in vals.iter().enumerate() { +// stake[val as usize] = +// (I64F64::from_num(ratio) * I64F64::from_num(sample[i]) * total_stake) +// .to_num::(); +// stake_fixed[val as usize] = +// I32F32::from_num(I64F64::from_num(ratio) * I64F64::from_num(sample[i])); +// } +// } + +// let mut weights: Vec> = vec![vec![]; network_n as usize]; +// let mut weights_fixed: Vec> = vec![vec![zero; network_n]; network_n]; +// for (first, second, vals) in vec![ +// (major_weight, one - major_weight, &major_validators), +// (one - minor_weight, minor_weight, &minor_validators), +// ] { +// for &val in vals { +// for (weight, srvs) in vec![(first, &major_servers), (second, &minor_servers)] { +// let mut sample: Vec = normal(srvs.len(), &mut rng, &dist) +// .iter() +// .map(|x: &I32F32| { +// let v: I32F32 = (weight_stddev * x) + one; +// if v < zero { +// zero +// } else { +// v +// } +// }) +// .collect(); +// inplace_normalize(&mut sample); + +// for (i, &srv) in srvs.iter().enumerate() { +// weights[val as usize].push((srv, fixed_proportion_to_u16(weight * sample[i]))); +// weights_fixed[val as usize][srv as usize] = weight * sample[i]; +// } +// } +// inplace_normalize(&mut weights_fixed[val as usize]); +// } +// } + +// inplace_normalize(&mut stake_fixed); + +// // Calculate stake-weighted mean per server +// let mut weight_mean: Vec = vec![zero; network_n]; +// for val in 0..network_n { +// if stake_fixed[val] > zero { +// for srv in 0..network_n { +// weight_mean[srv] += stake_fixed[val] * weights_fixed[val][srv]; +// } +// } +// } + +// // Calculate stake-weighted absolute standard deviation +// let mut weight_dev: Vec = vec![zero; network_n]; +// for val in 0..network_n { +// if stake_fixed[val] > zero { +// for srv in 0..network_n { +// weight_dev[srv] += +// stake_fixed[val] * (weight_mean[srv] - weights_fixed[val][srv]).abs(); +// } +// } +// } + +// // Calculate rank-weighted mean of weight_dev +// let avg_weight_dev: I32F32 = +// weight_dev.iter().sum::() / weight_mean.iter().sum::(); + +// ( +// validators, +// servers, +// major_validators, +// minor_validators, +// major_servers, +// minor_servers, +// stake, +// weights, +// avg_weight_dev, +// ) +// } + +// Test consensus guarantees with an epoch on a graph with 4096 nodes, of which the first 128 are validators, the graph is split into a major and minor set, each setting specific weight on itself and the complement on the other. Asserts that the major emission ratio >= major stake ratio. +// #[test] +// fn test_consensus_guarantees() { +// let netuid = NetUid::from(0); +// let network_n: u16 = 512; +// let validators_n: u16 = 64; +// let epochs: u16 = 1; +// let interleave = 2; +// log::info!("test_consensus_guarantees ({network_n:?}, {validators_n:?} validators)"); +// for (major_stake, major_weight, minor_weight, weight_stddev, bonds_penalty) in vec![ +// (0.51, 1., 1., 0.001, u16::MAX), +// (0.51, 0.03, 0., 0.001, u16::MAX), +// (0.51, 0.51, 0.49, 0.001, u16::MAX), +// (0.51, 0.51, 1., 0.001, u16::MAX), +// (0.51, 0.61, 0.8, 0.1, u16::MAX), +// (0.6, 0.67, 0.65, 0.2, u16::MAX), +// (0.6, 0.74, 0.77, 0.4, u16::MAX), +// (0.6, 0.76, 0.8, 0.4, u16::MAX), +// (0.6, 0.73, 1., 0.4, u16::MAX), // bonds_penalty = 100% +// (0.6, 0.74, 1., 0.4, 55800), // bonds_penalty = 85% +// (0.6, 0.76, 1., 0.4, 43690), // bonds_penalty = 66% +// (0.6, 0.78, 1., 0.4, 21845), // bonds_penalty = 33% +// (0.6, 0.79, 1., 0.4, 0), // bonds_penalty = 0% +// (0.6, 0.92, 1., 0.4, u16::MAX), +// (0.6, 0.94, 1., 0.4, u16::MAX), +// (0.65, 0.78, 0.85, 0.6, u16::MAX), +// (0.7, 0.81, 0.85, 0.8, u16::MAX), +// (0.7, 0.83, 0.85, 1., u16::MAX), +// ] { +// let ( +// validators, +// servers, +// major_validators, +// minor_validators, +// major_servers, +// minor_servers, +// stake, +// weights, +// _avg_weight_dev, +// ) = split_graph( +// fixed(major_stake), +// fixed(major_weight), +// fixed(minor_weight), +// fixed(weight_stddev), +// validators_n as usize, +// network_n as usize, +// interleave as usize, +// ); + +// new_test_ext(1).execute_with(|| { +// init_run_epochs( +// netuid, +// network_n, +// &validators, +// &servers, +// epochs, +// 1, +// true, +// &stake, +// true, +// &weights, +// true, +// false, +// 0, +// false, +// bonds_penalty +// ); + +// let mut major_emission: I64F64 = I64F64::from_num(0); +// let mut minor_emission: I64F64 = I64F64::from_num(0); +// for set in vec![major_validators, major_servers] { +// for uid in set { +// major_emission += +// I64F64::from_num(SubtensorModule::get_emission_for_uid(netuid, uid)); +// } +// } +// for set in vec![minor_validators, minor_servers] { +// for uid in set { +// minor_emission += +// I64F64::from_num(SubtensorModule::get_emission_for_uid(netuid, uid)); +// } +// } +// let major_ratio: I32F32 = +// I32F32::from_num(major_emission / (major_emission + minor_emission)); +// assert!(major_stake <= major_ratio); +// }); +// } +// } + +// Test an epoch on an empty graph. +// #[test] +// fn test_overflow() { +// new_test_ext(1).execute_with(|| { +// log::info!("test_overflow:"); +// let netuid = NetUid::from(1); +// add_network(netuid, 1, 0); +// SubtensorModule::set_max_allowed_uids(netuid, 3); +// SubtensorModule::increase_stake_on_coldkey_hotkey_account( +// &U256::from(0), +// &U256::from(0), +// 10, +// ); +// SubtensorModule::increase_stake_on_coldkey_hotkey_account( +// &U256::from(1), +// &U256::from(1), +// 10, +// ); +// SubtensorModule::increase_stake_on_coldkey_hotkey_account( +// &U256::from(2), +// &U256::from(2), +// 10, +// ); +// SubtensorModule::append_neuron(netuid, &U256::from(0), 0); +// SubtensorModule::append_neuron(netuid, &U256::from(1), 0); +// SubtensorModule::append_neuron(netuid, &U256::from(2), 0); +// SubtensorModule::set_validator_permit_for_uid(0, 0, true); +// SubtensorModule::set_validator_permit_for_uid(0, 1, true); +// SubtensorModule::set_validator_permit_for_uid(0, 2, true); +// assert_ok!(SubtensorModule::set_weights( +// RuntimeOrigin::signed(U256::from(0)), +// netuid, +// vec![0, 1, 2], +// vec![u16::MAX / 3, u16::MAX / 3, u16::MAX], +// 0 +// )); +// assert_ok!(SubtensorModule::set_weights( +// RuntimeOrigin::signed(U256::from(1)), +// netuid, +// vec![1, 2], +// vec![u16::MAX / 2, u16::MAX / 2], +// 0 +// )); +// assert_ok!(SubtensorModule::set_weights( +// RuntimeOrigin::signed(U256::from(2)), +// netuid, +// vec![2], +// vec![u16::MAX], +// 0 +// )); +// SubtensorModule::epoch(0, u64::MAX); +// }); +// } + +// Test an epoch on an empty graph. +// #[test] +// fn test_nill_epoch_subtensor() { +// new_test_ext(1).execute_with(|| { +// log::info!("test_nill_epoch:"); +// SubtensorModule::epoch(0, 0); +// }); +// } diff --git a/pallets/subtensor/src/tests/epoch/liquid_alpha.rs b/pallets/subtensor/src/tests/epoch/liquid_alpha.rs new file mode 100644 index 0000000000..f170f170e7 --- /dev/null +++ b/pallets/subtensor/src/tests/epoch/liquid_alpha.rs @@ -0,0 +1,254 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Liquid alpha hyperparameters and equal-alpha self-consistency. + +use frame_support::{assert_err, assert_ok}; +use sp_core::U256; +use substrate_fixed::types::I32F32; +use subtensor_runtime_common::TaoBalance; +use subtensor_swap_interface::SwapHandler; + +use super::super::mock::*; +use crate::tests::math::{assert_mat_compare, vec_to_fixed, vec_to_mat_fixed}; +use crate::*; + +#[test] +fn test_set_alpha_disabled() { + new_test_ext(1).execute_with(|| { + let hotkey = U256::from(1); + let coldkey = U256::from(1 + 456); + let netuid = add_dynamic_network(&hotkey, &coldkey); + let signer = RuntimeOrigin::signed(coldkey); + + // Enable Liquid Alpha and setup + SubtensorModule::set_liquid_alpha_enabled(netuid, true); + migrations::migrate_create_root_network::migrate_create_root_network::(); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_000_u64.into()); + assert_ok!(SubtensorModule::root_register(signer.clone(), hotkey,)); + let fee = ::SwapInterface::approx_fee_amount( + netuid.into(), + DefaultMinStake::::get(), + ); + assert_ok!(SubtensorModule::add_stake( + signer.clone(), + hotkey, + netuid, + TaoBalance::from(5) * DefaultMinStake::::get() + fee + )); + // Only owner can set alpha values + assert_ok!(SubtensorModule::register_network(signer.clone(), hotkey)); + + // Explicitly set to false + SubtensorModule::set_liquid_alpha_enabled(netuid, false); + assert_err!( + SubtensorModule::do_set_alpha_values(signer.clone(), netuid, 1638_u16, u16::MAX), + Error::::LiquidAlphaDisabled + ); + + SubtensorModule::set_liquid_alpha_enabled(netuid, true); + assert_ok!(SubtensorModule::do_set_alpha_values( + signer.clone(), + netuid, + 1638_u16, + u16::MAX + )); + }); +} + +/// cargo test --package pallet-subtensor --lib -- tests::epoch::liquid_alpha::test_get_set_alpha --exact --show-output +#[test] +fn test_get_set_alpha() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let alpha_low: u16 = 1638_u16; + let alpha_high: u16 = u16::MAX - 10; + + let hotkey: U256 = U256::from(1); + let coldkey: U256 = U256::from(1 + 456); + let signer = RuntimeOrigin::signed(coldkey); + + // Enable Liquid Alpha and setup + SubtensorModule::set_liquid_alpha_enabled(netuid, true); + migrations::migrate_create_root_network::migrate_create_root_network::(); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_000_u64.into()); + assert_ok!(SubtensorModule::root_register(signer.clone(), hotkey,)); + + // Should fail as signer does not own the subnet + assert_err!( + SubtensorModule::do_set_alpha_values(signer.clone(), netuid, alpha_low, alpha_high), + DispatchError::BadOrigin + ); + + assert_ok!(SubtensorModule::register_network(signer.clone(), hotkey)); + SubtokenEnabled::::insert(netuid, true); + + let fee = ::SwapInterface::approx_fee_amount( + netuid.into(), + DefaultMinStake::::get(), + ); + + assert_ok!(SubtensorModule::add_stake( + signer.clone(), + hotkey, + netuid, + DefaultMinStake::::get() + fee * 2.into() + )); + + assert_ok!(SubtensorModule::do_set_alpha_values( + signer.clone(), + netuid, + alpha_low, + alpha_high + )); + let (grabbed_alpha_low, grabbed_alpha_high): (u16, u16) = + SubtensorModule::get_alpha_values(netuid); + + log::info!("alpha_low: {grabbed_alpha_low:?} alpha_high: {grabbed_alpha_high:?}"); + assert_eq!(grabbed_alpha_low, alpha_low); + assert_eq!(grabbed_alpha_high, alpha_high); + + // Convert the u16 values to decimal values + fn unnormalize_u16_to_float(normalized_value: u16) -> f32 { + const MAX_U16: u16 = 65535; + normalized_value as f32 / MAX_U16 as f32 + } + + let alpha_low_decimal = unnormalize_u16_to_float(alpha_low); + let alpha_high_decimal = unnormalize_u16_to_float(alpha_high); + + let (alpha_low_32, alpha_high_32) = SubtensorModule::get_alpha_values_32(netuid); + + let tolerance: f32 = 1e-6; // 0.000001 + + // Check if the values are equal to the sixth decimal + assert!( + (alpha_low_32.to_num::() - alpha_low_decimal).abs() < tolerance, + "alpha_low mismatch: {} != {}", + alpha_low_32.to_num::(), + alpha_low_decimal + ); + assert!( + (alpha_high_32.to_num::() - alpha_high_decimal).abs() < tolerance, + "alpha_high mismatch: {} != {}", + alpha_high_32.to_num::(), + alpha_high_decimal + ); + + // 1. Liquid alpha disabled + SubtensorModule::set_liquid_alpha_enabled(netuid, false); + assert_err!( + SubtensorModule::do_set_alpha_values(signer.clone(), netuid, alpha_low, alpha_high), + Error::::LiquidAlphaDisabled + ); + // Correct scenario after error + SubtensorModule::set_liquid_alpha_enabled(netuid, true); // Re-enable for further tests + assert_ok!(SubtensorModule::do_set_alpha_values( + signer.clone(), + netuid, + alpha_low, + alpha_high + )); + + // 2. Alpha high too low + let alpha_high_too_low = (u16::MAX as u32 / 40) as u16 - 1; // One less than the minimum acceptable value + assert_err!( + SubtensorModule::do_set_alpha_values( + signer.clone(), + netuid, + alpha_low, + alpha_high_too_low + ), + Error::::AlphaHighTooLow + ); + // Correct scenario after error + assert_ok!(SubtensorModule::do_set_alpha_values( + signer.clone(), + netuid, + alpha_low, + alpha_high + )); + + // 3. Alpha low too low or too high + let alpha_low_too_low = 0_u16; + assert_err!( + SubtensorModule::do_set_alpha_values( + signer.clone(), + netuid, + alpha_low_too_low, + alpha_high + ), + Error::::AlphaLowOutOfRange + ); + // Correct scenario after error + assert_ok!(SubtensorModule::do_set_alpha_values( + signer.clone(), + netuid, + alpha_low, + alpha_high + )); + + let alpha_low_too_high = alpha_high + 1; // alpha_low should be <= alpha_high + assert_err!( + SubtensorModule::do_set_alpha_values( + signer.clone(), + netuid, + alpha_low_too_high, + alpha_high + ), + Error::::AlphaLowOutOfRange + ); + // Correct scenario after error + assert_ok!(SubtensorModule::do_set_alpha_values( + signer.clone(), + netuid, + alpha_low, + alpha_high + )); + }); +} + +#[test] +fn test_liquid_alpha_equal_values_against_itself() { + new_test_ext(1).execute_with(|| { + // check Liquid alpha disabled against Liquid Alpha enabled with alpha_low == alpha_high + let netuid: NetUid = NetUid::from(1); + let alpha_low = u16::MAX / 10; + let alpha_high = u16::MAX / 10; + let epsilon = I32F32::from_num(1e-3); + let weights: Vec> = vec_to_mat_fixed( + &[0., 0.1, 0., 0., 0.2, 0.4, 0., 0.3, 0.1, 0., 0.4, 0.5], + 4, + false, + ); + let bonds: Vec> = vec_to_mat_fixed( + &[0.1, 0.1, 0.5, 0., 0., 0.4, 0.5, 0.1, 0.1, 0., 0.4, 0.2], + 4, + false, + ); + let consensus: Vec = vec_to_fixed(&[0.3, 0.2, 0.1, 0.4]); + + // set both alpha values to 0.1 and bonds moving average to 0.9 + AlphaValues::::insert(netuid, (alpha_low, alpha_high)); + SubtensorModule::set_bonds_moving_average(netuid.into(), 900_000); + + // compute bonds with liquid alpha enabled + SubtensorModule::set_liquid_alpha_enabled(netuid.into(), true); + let new_bonds_liquid_alpha_on = + SubtensorModule::compute_bonds(netuid.into(), &weights, &bonds, &consensus); + + // compute bonds with liquid alpha disabled + SubtensorModule::set_liquid_alpha_enabled(netuid.into(), false); + let new_bonds_liquid_alpha_off = + SubtensorModule::compute_bonds(netuid.into(), &weights, &bonds, &consensus); + + assert_mat_compare( + &new_bonds_liquid_alpha_on, + &new_bonds_liquid_alpha_off, + epsilon, + ); + }); +} diff --git a/pallets/subtensor/src/tests/epoch/mod.rs b/pallets/subtensor/src/tests/epoch/mod.rs new file mode 100644 index 0000000000..ca7d5af81c --- /dev/null +++ b/pallets/subtensor/src/tests/epoch/mod.rs @@ -0,0 +1,41 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Integration tests for [`crate::epoch`] (`run_epoch`, bonds EMA / liquid alpha, weight loaders). +//! +//! Split from the former monolithic `tests/epoch.rs` into concept-named modules. +//! +//! ## Search anchors +//! +//! | Module | Owns | +//! |--------|------| +//! | [`helpers`] | `init_run_epochs`, node distribution, normalize helpers | +//! | [`graph_epochs`] | 1/10/512-node graph epoch runs | +//! | [`bonds`] | bond accumulation and deregistered-miner bonds | +//! | [`liquid_alpha`] | liquid alpha get/set and equal-alpha checks | +//! | [`active_stake`] | active-stake filtering | +//! | [`weight_activity`] | outdated / zero weights | +//! | [`validator_permits`] | validator permit issuance | +//! | [`epoch_timing`] | blocks since last step | +//! | [`self_weight`] | subnet-owner self-weight | +//! | [`epoch_outputs`] | minimal topology epoch outputs | +//! | [`yuma_3`] | Yuma3 kappa / bonds / liquid-alpha scenarios | +//! | [`snipe_weight_mask`] | sniped-UID weight masking | +//! | [`epoch_input_state`] | input consistency + LastUpdate mismatch | + +mod active_stake; +mod bonds; +mod epoch_input_state; +mod epoch_outputs; +mod epoch_timing; +mod graph_epochs; +mod helpers; +mod liquid_alpha; +mod self_weight; +mod snipe_weight_mask; +mod validator_permits; +mod weight_activity; +mod yuma_3; diff --git a/pallets/subtensor/src/tests/epoch/self_weight.rs b/pallets/subtensor/src/tests/epoch/self_weight.rs new file mode 100644 index 0000000000..4e963d7f74 --- /dev/null +++ b/pallets/subtensor/src/tests/epoch/self_weight.rs @@ -0,0 +1,77 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Subnet-owner self-weight allowance during epoch weight setting. + +use sp_core::U256; +use subtensor_runtime_common::NetUidStorageIndex; + +use super::super::mock::*; +use crate::*; + +/// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::epoch::self_weight::test_can_set_self_weight_as_subnet_owner --exact --show-output +#[test] +fn test_can_set_self_weight_as_subnet_owner() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey: U256 = U256::from(1); + let subnet_owner_hotkey: U256 = U256::from(1 + 456); + + let other_hotkey: U256 = U256::from(2); + + let stake = 5_000_000_000_000_u64; // 5k TAO + let to_emit: u64 = 1_000_000_000_u64; // 1 TAO + + // Create subnet + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + // Register the other hotkey + register_ok_neuron(netuid, other_hotkey, subnet_owner_coldkey, 0); + + // Add stake to owner hotkey. + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &subnet_owner_hotkey, + &subnet_owner_coldkey, + netuid, + stake.into(), + ); + + // Give vpermits to owner hotkey ONLY + ValidatorPermit::::insert(netuid, vec![true, false]); + + // Set weight of 50% to each hotkey. + // This includes a self-weight + let fifty_percent: u16 = u16::MAX / 2; + Weights::::insert( + NetUidStorageIndex::from(netuid), + 0, + vec![(0, fifty_percent), (1, fifty_percent)], + ); + + step_block(1); + // Set updated so weights are valid + LastUpdate::::insert(NetUidStorageIndex::from(netuid), vec![2, 0]); + + // Run epoch + let hotkey_emission = SubtensorModule::epoch(netuid, to_emit.into()); + + // hotkey_emission is [(hotkey, incentive, dividend)] + assert_eq!(hotkey_emission.len(), 2); + assert!( + hotkey_emission + .iter() + .any(|(hk, _, _)| *hk == subnet_owner_hotkey) + ); + assert!(hotkey_emission.iter().any(|(hk, _, _)| *hk == other_hotkey)); + + log::debug!("hotkey_emission: {hotkey_emission:?}"); + // Both should have received incentive emission + assert!(hotkey_emission[0].1 > 0.into()); + assert!(hotkey_emission[1].1 > 0.into()); + + // Their incentive should be equal + assert_eq!(hotkey_emission[0].1, hotkey_emission[1].1); + }); +} diff --git a/pallets/subtensor/src/tests/epoch/snipe_weight_mask.rs b/pallets/subtensor/src/tests/epoch/snipe_weight_mask.rs new file mode 100644 index 0000000000..a7233692bd --- /dev/null +++ b/pallets/subtensor/src/tests/epoch/snipe_weight_mask.rs @@ -0,0 +1,260 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Commit-reveal sniped-UID incoming weight masking during epoch. + +use frame_support::assert_ok; +use sp_core::U256; + +use super::super::mock::*; +use crate::*; + +#[test] +fn test_epoch_masks_incoming_to_sniped_uid_prevents_inheritance() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(40); + let tempo: u16 = 10; + let reveal: u64 = 2; + + add_network(netuid, tempo, 0); + assert_ok!(SubtensorModule::set_reveal_period(netuid, reveal)); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + SubtensorModule::set_max_allowed_uids(netuid, 3); + SubtensorModule::set_target_registrations_per_interval(netuid, u16::MAX); + + /* Validator uid‑0 */ + let (val_hot, val_cold) = (U256::from(100), U256::from(200)); + register_ok_neuron(netuid, val_hot, val_cold, 0); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &val_hot, + &val_cold, + netuid, + 10_000.into(), + ); + SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); + + /* Miner uid‑1 (to be sniped later) */ + let (old_hot, old_cold) = (U256::from(101), U256::from(201)); + register_ok_neuron(netuid, old_hot, old_cold, 0); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &old_hot, + &old_cold, + netuid, + 100.into(), + ); + + /* filler uid‑2 */ + let (fill_hot, fill_cold) = (U256::from(102), U256::from(202)); + register_ok_neuron(netuid, fill_hot, fill_cold, 0); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &fill_hot, + &fill_cold, + netuid, + 5_000.into(), + ); + SubtensorModule::set_max_allowed_validators(netuid, 3); + + run_to_block(tempo as u64 * 2 + 1); + + /* commit, then move one block ahead so reg_block > commit_block */ + commit_dummy(val_hot, netuid); + run_to_block(System::block_number() + 1); + + /* validator weights uid‑1 */ + SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(val_hot), + netuid, + vec![1], + vec![u16::MAX], + 0 + )); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + SubtensorModule::epoch(netuid, 1_000.into()); + + /* register new miner (snipes) */ + let (new_hot, new_cold) = (U256::from(103), U256::from(203)); + register_ok_neuron(netuid, new_hot, new_cold, 0); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &new_hot, + &new_cold, + netuid, + 10_000.into(), + ); + let new_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &new_hot) + .expect("new miner gets UID"); + + run_to_block(System::block_number() + 1); + + /* validator refreshes vote (still inside window) */ + SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(val_hot), + netuid, + vec![0, new_uid], + vec![u16::MAX / 2, u16::MAX / 2], + 0 + )); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + + SubtensorModule::epoch(netuid, 1_000.into()); + assert_eq!(SubtensorModule::get_rank_for_uid(netuid, new_uid), 0); + assert_eq!( + SubtensorModule::get_incentive_for_uid(netuid.into(), new_uid), + 0 + ); + }); +} + +#[test] +fn test_epoch_no_mask_when_commit_reveal_disabled() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(32); + let tempo: u16 = 5; + add_network(netuid, tempo, 0); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); + + let (hot, cold) = (U256::from(1000), U256::from(1100)); + register_ok_neuron(netuid, hot, cold, 0); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hot, + &cold, + netuid, + 1_000.into(), + ); + SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); + + let (hot1, cold1) = (U256::from(1001), U256::from(1101)); + register_ok_neuron(netuid, hot1, cold1, 0); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hot1, + &cold1, + netuid, + 1_000.into(), + ); + + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(hot), + netuid, + vec![1], + vec![u16::MAX], + 0 + )); + + for _ in 0..3 { + SubtensorModule::epoch(netuid, 1.into()); + assert!( + !SubtensorModule::unnormalized_weights_sparse(netuid.into())[0].is_empty(), + "row visible when CR disabled" + ); + run_to_block(System::block_number() + tempo as u64 + 1); + } + }); +} + +#[test] +fn test_epoch_does_not_mask_outside_window_but_masks_inside() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(50); + let tempo: u16 = 8; + let reveal: u16 = 2; + + add_network(netuid, tempo, 0); + assert_ok!(SubtensorModule::set_reveal_period(netuid, reveal as u64)); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + SubtensorModule::set_target_registrations_per_interval(netuid, u16::MAX); + + /* validator uid‑0 */ + let (v_hot, v_cold) = (U256::from(2000), U256::from(2100)); + register_ok_neuron(netuid, v_hot, v_cold, 0); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &v_hot, + &v_cold, + netuid, + 10_000.into(), + ); + SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); + SubtensorModule::set_max_allowed_validators(netuid, 1); + + run_to_block(tempo as u64); + + /* first commit */ + commit_dummy(v_hot, netuid); + + /* UID‑1 — outside window */ + let (old_hot, old_cold) = (U256::from(2001), U256::from(2101)); + register_ok_neuron(netuid, old_hot, old_cold, 0); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &old_hot, + &old_cold, + netuid, + 1_000.into(), + ); + + /* let first commit expire for UID‑1 */ + for _ in 0..(reveal + 1) { + run_to_block(System::block_number() + tempo as u64); + } + + /* second commit — will mask UID‑2 & UID‑3 */ + commit_dummy(v_hot, netuid); + + /* ensure commit_block < reg_block for the new registrations */ + run_to_block(System::block_number() + 1); + + /* UID‑2, UID‑3 — inside window */ + let (mid_hot, mid_cold) = (U256::from(2002), U256::from(2102)); + register_ok_neuron(netuid, mid_hot, mid_cold, 0); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &mid_hot, + &mid_cold, + netuid, + 1_000.into(), + ); + + let (new_hot, new_cold) = (U256::from(2003), U256::from(2103)); + register_ok_neuron(netuid, new_hot, new_cold, 0); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &new_hot, + &new_cold, + netuid, + 1_000.into(), + ); + + run_to_block(System::block_number() + 1); // avoid out‑dated + + /* vote */ + SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(v_hot), + netuid, + vec![1, 2, 3], + vec![u16::MAX / 3, u16::MAX / 3, u16::MAX / 3], + 0 + )); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + + SubtensorModule::epoch(netuid, 1_000.into()); + + assert!( + SubtensorModule::get_incentive_for_uid(netuid.into(), 1) > 0, + "UID-1 (old) unmasked" + ); + assert_eq!( + SubtensorModule::get_incentive_for_uid(netuid.into(), 2), + 0, + "UID-2 (inside window) masked" + ); + assert_eq!( + SubtensorModule::get_incentive_for_uid(netuid.into(), 3), + 0, + "UID-3 (inside window) masked" + ); + }); +} diff --git a/pallets/subtensor/src/tests/epoch/validator_permits.rs b/pallets/subtensor/src/tests/epoch/validator_permits.rs new file mode 100644 index 0000000000..1d34d152f0 --- /dev/null +++ b/pallets/subtensor/src/tests/epoch/validator_permits.rs @@ -0,0 +1,147 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Max-allowed-validators / permit issuance via epoch. + +use frame_support::assert_ok; +use sp_core::U256; +use subtensor_runtime_common::TaoBalance; + +use super::super::mock::*; +use super::helpers::distribute_nodes; +use crate::*; + +// Test that epoch assigns validator permits to highest stake uids that are over the stake threshold, varies uid interleaving and stake values. +#[test] +fn test_validator_permits() { + let netuid = NetUid::from(1); + let tempo: u16 = u16::MAX - 1; // high tempo to skip automatic epochs in on_initialize, use manual epochs instead + for interleave in 0..3 { + for (network_n, validators_n) in [(2, 1), (4, 2), (8, 4)] { + let min_stake = validators_n as u64; + for assignment in 0..=1 { + let (validators, servers) = + distribute_nodes(validators_n as usize, network_n, interleave as usize); + let correct: bool = true; + let mut stake: Vec = vec![0.into(); network_n]; + for validator in &validators { + stake[*validator as usize] = match assignment { + 1 => TaoBalance::from(*validator) + network_n.into(), + _ => 1.into(), + }; + } + for server in &servers { + stake[*server as usize] = match assignment { + 1 => TaoBalance::from(*server), + _ => 0.into(), + }; + } + new_test_ext(1).execute_with(|| { + let block_number: u64 = 0; + add_network(netuid, tempo, 0); + SubtensorModule::set_max_allowed_uids(netuid, network_n as u16); + assert_eq!( + SubtensorModule::get_max_allowed_uids(netuid), + network_n as u16 + ); + SubtensorModule::set_max_registrations_per_block(netuid, network_n as u16); + SubtensorModule::set_target_registrations_per_interval( + netuid, + network_n as u16, + ); + SubtensorModule::set_stake_threshold(min_stake); + + // === Register [validator1, validator2, server1, server2] + for key in 0..network_n as u64 { + add_balance_to_coldkey_account( + &U256::from(key), + stake[key as usize] + + ExistentialDeposit::get() + + SubtensorModule::get_network_min_lock(), + ); + let (nonce, work): (u64, Vec) = + SubtensorModule::create_work_for_block_number( + netuid, + block_number, + key * 1_000_000, + &U256::from(key), + ); + assert_ok!(SubtensorModule::register( + RuntimeOrigin::signed(U256::from(key)), + netuid, + block_number, + nonce, + work, + U256::from(key), + U256::from(key) + )); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &U256::from(key), + &U256::from(key), + netuid, + stake[key as usize].to_u64().into(), + ); + } + assert_eq!(SubtensorModule::get_subnetwork_n(netuid), network_n as u16); + + // === Issue validator permits + SubtensorModule::set_max_allowed_validators(netuid, validators_n as u16); + assert_eq!( + SubtensorModule::get_max_allowed_validators(netuid), + validators_n as u16 + ); + SubtensorModule::epoch(netuid, 1_000_000_000.into()); // run first epoch to set allowed validators + for validator in &validators { + assert_eq!( + stake[*validator as usize] >= TaoBalance::from(min_stake), + SubtensorModule::get_validator_permit_for_uid(netuid, *validator) + ); + } + for server in &servers { + assert_eq!( + !correct, + SubtensorModule::get_validator_permit_for_uid(netuid, *server) + ); + } + + // === Increase server stake above validators + for server in &servers { + add_balance_to_coldkey_account( + &(U256::from(*server as u64)), + (2 * network_n as u64).into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(*server as u64)), + &(U256::from(*server as u64)), + netuid, + (2 * network_n as u64).into(), + ); + } + + // === Update validator permits + run_to_block(1); + SubtensorModule::epoch(netuid, 1_000_000_000.into()); + + // === Check that servers now own permits instead of the validator uids + for validator in &validators { + assert_eq!( + !correct, + SubtensorModule::get_validator_permit_for_uid(netuid, *validator) + ); + } + for server in &servers { + assert_eq!( + (stake[*server as usize] + + (TaoBalance::from(2) * TaoBalance::from(network_n))) + >= TaoBalance::from(min_stake), + SubtensorModule::get_validator_permit_for_uid(netuid, *server) + ); + } + }); + } + } + } +} diff --git a/pallets/subtensor/src/tests/epoch/weight_activity.rs b/pallets/subtensor/src/tests/epoch/weight_activity.rs new file mode 100644 index 0000000000..93f301faf6 --- /dev/null +++ b/pallets/subtensor/src/tests/epoch/weight_activity.rs @@ -0,0 +1,424 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Outdated and zero weight rows and their effect on epoch outputs. + +use frame_support::assert_ok; +use sp_core::U256; +use substrate_fixed::types::I32F32; +use subtensor_runtime_common::{AlphaBalance, TaoBalance}; + +use super::super::mock::*; +use crate::*; + +#[test] +fn test_outdated_weights() { + new_test_ext(1).execute_with(|| { + let sparse: bool = true; + let n: u16 = 4; + let netuid = NetUid::from(1); + let tempo: u16 = 0; + let mut block_number: u64 = System::block_number(); + let stake: TaoBalance = 1.into(); + add_network_disable_commit_reveal(netuid, tempo, 0); + SubtensorModule::set_max_allowed_uids(netuid, n); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_max_registrations_per_block(netuid, n); + SubtensorModule::set_target_registrations_per_interval(netuid, n); + SubtensorModule::set_min_allowed_weights(netuid, 0); + SubtensorModule::set_bonds_penalty(netuid, u16::MAX); + assert_eq!(SubtensorModule::get_registrations_this_block(netuid), 0); + + // === Register [validator1, validator2, server1, server2] + for key in 0..n as u64 { + add_balance_to_coldkey_account( + &U256::from(key), + stake + + ExistentialDeposit::get() + + (SubtensorModule::get_network_min_lock() * 2.into()), + ); + let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( + netuid, + block_number, + key * 1_000_000, + &U256::from(key), + ); + assert_ok!(SubtensorModule::register( + RuntimeOrigin::signed(U256::from(key)), + netuid, + block_number, + nonce, + work, + U256::from(key), + U256::from(key) + )); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &U256::from(key), + &U256::from(key), + netuid, + AlphaBalance::from(stake.to_u64()), + ); + } + assert_eq!(SubtensorModule::get_subnetwork_n(netuid), n); + assert_eq!(SubtensorModule::get_registrations_this_block(netuid), 4); + + // === Issue validator permits + SubtensorModule::set_max_allowed_validators(netuid, n); + assert_eq!(SubtensorModule::get_max_allowed_validators(netuid), n); + SubtensorModule::epoch(netuid, 1_000_000_000.into()); // run first epoch to set allowed validators + assert_eq!(SubtensorModule::get_registrations_this_block(netuid), 4); + block_number = next_block_no_epoch(netuid); // run to next block to ensure weights are set on nodes after their registration block + assert_eq!(SubtensorModule::get_registrations_this_block(netuid), 0); + + // === Set weights [val1->srv1: 2/3, val1->srv2: 1/3, val2->srv1: 2/3, val2->srv2: 1/3, srv1->srv1: 1, srv2->srv2: 1] + for uid in 0..(n / 2) as u64 { + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(uid)), + netuid, + ((n / 2)..n).collect(), + vec![2 * (u16::MAX / 3), u16::MAX / 3], + 0 + )); + } + for uid in ((n / 2) as u64)..n as u64 { + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(uid)), + netuid, + vec![uid as u16], + vec![u16::MAX], + 0 + )); // server self-weight + } + if sparse { + SubtensorModule::epoch(netuid, 1_000_000_000.into()); + } else { + SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); + } + /* current_block: 1; activity_cutoff: 5000 + Last update: [1, 1, 1, 1]; Inactive: [false, false, false, false]; Block at registration: [0, 0, 0, 0] + S: [0.25, 0.25, 0.25, 0.25]; S (mask): [0.25, 0.25, 0.25, 0.25]; S (mask+norm): [0.25, 0.25, 0.25, 0.25] + validator_permits: [true, true, true, true]; max_allowed_validators: 4; new_validator_permits: [true, true, true, true] + W: [[(2, 65535), (3, 32768)], [(2, 65535), (3, 32768)], [(2, 65535)], [(3, 65535)]] + W (permit): [[(2, 65535), (3, 32768)], [(2, 65535), (3, 32768)], [(2, 65535)], [(3, 65535)]] + W (permit+diag): [[(2, 65535), (3, 32768)], [(2, 65535), (3, 32768)], [], []] + W (permit+diag+outdate): [[(2, 65535), (3, 32768)], [(2, 65535), (3, 32768)], [], []] + W (mask+norm): [[(2, 0.6666632756), (3, 0.3333367242)], [(2, 0.6666632756), (3, 0.3333367242)], [], []] + R (before): [0, 0, 0.3333316376, 0.166668362] + C: [0, 0, 0.6666632756, 0.3333367242] + W: [[(2, 0.6666632756), (3, 0.3333367242)], [(2, 0.6666632756), (3, 0.3333367242)], [], []] + Tv: [0.9999999998, 0.9999999998, 0, 0] + R (after): [0, 0, 0.3333316376, 0.166668362] + T: [0, 0, 1, 1] + I (=R): [0, 0, 0.6666632756, 0.3333367242] + B: [[], [], [], []] + B (outdatedmask): [[], [], [], []] + B (mask+norm): [[], [], [], []] + ΔB: [[(2, 0.1666658188), (3, 0.083334181)], [(2, 0.1666658188), (3, 0.083334181)], [], []] + ΔB (norm): [[(2, 0.5), (3, 0.5)], [(2, 0.5), (3, 0.5)], [], []] + emaB: [[(2, 0.5), (3, 0.5)], [(2, 0.5), (3, 0.5)], [], []] + D: [0.5, 0.5, 0, 0] + nE: [0.25, 0.25, 0.3333316378, 0.166668362] + E: [250000000, 250000000, 333331637, 166668361] + P: [0.25, 0.25, 0.3333316378, 0.166668362] + P (u16): [49151, 49151, 65535, 32767] */ + + // === Dereg server2 at uid3 (least emission) + register new key over uid3 + let new_key: u64 = n as u64; // register a new key while at max capacity, which means the least incentive uid will be deregistered + add_balance_to_coldkey_account( + &U256::from(new_key), + stake + + ExistentialDeposit::get() + + (SubtensorModule::get_network_min_lock() * 2.into()), + ); + let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( + netuid, + block_number, + 0, + &U256::from(new_key), + ); + assert_eq!(System::block_number(), block_number); + assert_eq!(SubtensorModule::get_max_registrations_per_block(netuid), n); + assert_eq!(SubtensorModule::get_registrations_this_block(netuid), 0); + assert_ok!(SubtensorModule::register( + RuntimeOrigin::signed(U256::from(new_key)), + netuid, + block_number, + nonce, + work, + U256::from(new_key), + U256::from(new_key) + )); + let deregistered_uid: u16 = n - 1; // since uid=n-1 only recieved 1/3 of weight, it will get pruned first + assert_eq!( + U256::from(new_key), + SubtensorModule::get_hotkey_for_net_and_uid(netuid, deregistered_uid) + .expect("Not registered") + ); + next_block_no_epoch(netuid); // run to next block to outdate weights and bonds set on deregistered uid + + // === Update weights from only uid=0 + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(0)), + netuid, + ((n / 2)..n).collect(), + vec![2 * (u16::MAX / 3), u16::MAX / 3], + 0 + )); + if sparse { + SubtensorModule::epoch(netuid, 1_000_000_000.into()); + } else { + SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); + } + /* current_block: 2; activity_cutoff: 5000 + Last update: [2, 1, 1, 1]; Inactive: [false, false, false, false]; Block at registration: [0, 0, 0, 1] + S: [0.3333333333, 0.3333333333, 0.3333333333, 0] + S (mask): [0.3333333333, 0.3333333333, 0.3333333333, 0] + S (mask+norm): [0.3333333333, 0.3333333333, 0.3333333333, 0] + validator_permits: [true, true, true, false]; max_allowed_validators: 4; new_validator_permits: [true, true, true, true] + W: [[(2, 65535), (3, 32768)], [(2, 65535), (3, 32768)], [(2, 65535)], [(3, 65535)]] + W (permit): [[(2, 65535), (3, 32768)], [(2, 65535), (3, 32768)], [(2, 65535)], [(3, 65535)]] + W (permit+diag): [[(2, 65535), (3, 32768)], [(2, 65535), (3, 32768)], [], []] + W (permit+diag+outdate): [[(2, 65535), (3, 32768)], [(2, 65535)], [], []] + W (mask+norm): [[(2, 0.6666632756), (3, 0.3333367242)], [(2, 1)], [], []] + R (before): [0, 0, 0.5555544249, 0.1111122412] + C: [0, 0, 0.6666632756, 0] + W: [[(2, 0.6666632756)], [(2, 0.6666632756)], [], []] + Tv: [0.6666632756, 0.6666632756, 0, 0] + R (after): [0, 0, 0.4444421832, 0] + T: [0, 0, 0.799997558, 0] + I (=R): [0, 0, 1, 0] + B: [[(2, 65535), (3, 65535)], [(2, 65535), (3, 65535)], [], []] + B (outdatedmask): [[(2, 65535), (3, 65535)], [(2, 65535)], [], []] + B (mask+norm): [[(2, 0.5), (3, 1)], [(2, 0.5)], [], []] + ΔB: [[(2, 0.2222210916)], [(2, 0.2222210916)], [], []] + ΔB (norm): [[(2, 0.5)], [(2, 0.5)], [], []] + emaB: [[(2, 0.5), (3, 1)], [(2, 0.5)], [], []] + emaB (max-upscale): [[(2, 1), (3, 1)], [(2, 1)], [], []] + D: [0.5, 0.5, 0, 0] + nE: [0.25, 0.25, 0.5, 0] + E: [250000000, 250000000, 500000000, 0] + P: [0.25, 0.25, 0.5, 0] + P (u16): [32767, 32767, 65535, 0] */ + let bonds = SubtensorModule::get_bonds(netuid.into()); + assert_eq!(SubtensorModule::get_dividends_for_uid(netuid, 0), 32767); // Note D = floor(0.5 * 65_535) + assert_eq!( + SubtensorModule::get_emission_for_uid(netuid, 0), + 250000000.into() + ); // Note E = 0.5 * 0.5 * 1_000_000_000 = 249311245 + assert_eq!(bonds[0][2], I32F32::from_num(65_535)); // floor(0.5*(2^16-1))/(2^16-1), then max-upscale + assert_eq!(bonds[0][3], I32F32::from_num(65_535)); // only uid0 has updated weights for new reg + }); +} + +/// Test the zero emission handling and fallback under zero effective weight conditions, to ensure non-zero effective emission. +#[test] +fn test_zero_weights() { + new_test_ext(1).execute_with(|| { + let sparse: bool = true; + let n: u16 = 2; + let netuid = NetUid::from(1); + let tempo: u16 = u16::MAX - 1; // high tempo to skip automatic epochs in on_initialize, use manual epochs instead + let mut block_number: u64 = 0; + let stake: u64 = 1; + add_network_disable_commit_reveal(netuid, tempo, 0); + SubtensorModule::set_max_allowed_uids(netuid, n); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_max_registrations_per_block(netuid, n); + SubtensorModule::set_target_registrations_per_interval(netuid, n); + SubtensorModule::set_min_allowed_weights(netuid, 0); + + // === Register [validator, server] + for key in 0..n as u64 { + add_balance_to_coldkey_account( + &U256::from(key), + ExistentialDeposit::get() + (SubtensorModule::get_network_min_lock() * 2.into()), + ); + let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( + netuid, + block_number, + key * 1_000_000, + &U256::from(key), + ); + assert_ok!(SubtensorModule::register( + RuntimeOrigin::signed(U256::from(key)), + netuid, + block_number, + nonce, + work, + U256::from(key), + U256::from(key) + )); + } + for validator in 0..(n / 2) as u64 { + add_balance_to_coldkey_account(&U256::from(validator), stake.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &U256::from(validator), + &U256::from(validator), + netuid, + stake.into(), + ); + } + assert_eq!(SubtensorModule::get_subnetwork_n(netuid), n); + + // === No weights + if sparse { + SubtensorModule::epoch(netuid, 1_000_000_000.into()); + } else { + SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); + } + /* current_block: 0; activity_cutoff: 5000; Last update: [0, 0]; Inactive: [false, false] + S: [1, 0]; S (mask): [1, 0]; S (mask+norm): [1, 0]; Block at registration: [0, 0] + W: [[], []]; W (diagmask): [[], []]; W (diag+outdatemask): [[], []]; W (mask+norm): [[], []] + R: [0, 0]; W (threshold): [[], []]; T: [0, 0]; C: [0.006693358, 0.006693358]; I: [0, 0] + B: [[], []]; B (mask+norm): [[], []]; + ΔB: [[], []]; ΔB (norm): [[], []]; emaB: [[], []]; D: [0, 0] + E: [1000000000, 0]; P: [1, 0] */ + for validator in 0..(n / 2) { + assert_eq!( + SubtensorModule::get_emission_for_uid(netuid, validator), + 1000000000.into() + ); // Note E = 1 * 1_000_000_000 + } + for server in (n / 2)..n { + assert_eq!( + SubtensorModule::get_emission_for_uid(netuid, server), + 0.into() + ); + // no stake + } + run_to_block(1); + block_number += 1; // run to next block to ensure weights are set on nodes after their registration block + + // === Self-weights only: set weights [srv->srv: 1] + for uid in ((n / 2) as u64)..n as u64 { + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(uid)), + netuid, + vec![uid as u16], + vec![u16::MAX], + 0 + )); // server self-weight + } + if sparse { + SubtensorModule::epoch(netuid, 1_000_000_000.into()); + } else { + SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); + } + /* current_block: 1; activity_cutoff: 5000; Last update: [0, 1]; Inactive: [false, false] + S: [1, 0]; S (mask): [1, 0]; S (mask+norm): [1, 0]; Block at registration: [0, 0] + W: [[], [(1, 1)]] + W (diagmask): [[], []]; W (diag+outdatemask): [[], []]; W (mask+norm): [[], []] + R: [0, 0]; W (threshold): [[], []]; T: [0, 0]; C: [0.006693358, 0.006693358]; I: [0, 0] + B: [[], []]: B (mask+norm): [[], []] + ΔB: [[], []]; ΔB (norm): [[], []]; emaB: [[], []]; D: [0, 0] + E: [1000000000, 0]; P: [1, 0] */ + for validator in 0..(n / 2) { + assert_eq!( + SubtensorModule::get_emission_for_uid(netuid, validator), + 1000000000.into() + ); // Note E = 1 * 1_000_000_000 + } + for server in (n / 2)..n { + assert_eq!( + SubtensorModule::get_emission_for_uid(netuid, server), + 0.into() + ); + // no stake + } + run_to_block(2); + block_number += 1; + + // === Set weights [val->srv: 1/(n/2)] + for uid in 0..(n / 2) as u64 { + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(uid)), + netuid, + ((n / 2)..n).collect(), + vec![u16::MAX / (n / 2); (n / 2) as usize], + 0 + )); + } + + // === Outdate weights by reregistering servers + for new_key in n..n + (n / 2) { + // register a new key while at max capacity, which means the least emission uid will be deregistered + add_balance_to_coldkey_account( + &U256::from(new_key), + ExistentialDeposit::get() + (SubtensorModule::get_network_min_lock() * 2.into()), + ); + let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( + netuid, + block_number, + new_key as u64 * 1_000_000, + &(U256::from(new_key)), + ); + assert_ok!(SubtensorModule::register( + RuntimeOrigin::signed(U256::from(new_key)), + netuid, + block_number, + nonce, + work, + U256::from(new_key), + U256::from(new_key) + )); + } + if sparse { + SubtensorModule::epoch(netuid, 1_000_000_000.into()); + } else { + SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); + } + /* current_block: 2; activity_cutoff: 5000; Last update: [2, 1]; Inactive: [false, false]; + S: [1, 0]; S (mask): [1, 0]; S (mask+norm): [1, 0]; Block at registration: [0, 2]; + W: [[(1, 1)], []]; W (diagmask): [[(1, 1)], []]; W (diag+outdatemask): [[], []]; W (mask+norm): [[], []]; + R: [0, 0]; W (threshold): [[], []]; T: [0, 0]; C: [0.006693358, 0.006693358]; I: [0, 0]; + B: [[], []]; B (mask+norm): [[], []]; + ΔB: [[], []]; ΔB (norm): [[], []]; emaB: [[], []]; D: [0, 0]; + E: [1000000000, 0]; P: [1, 0] */ + for validator in 0..(n / 2) { + assert_eq!( + SubtensorModule::get_emission_for_uid(netuid, validator), + 1000000000.into() + ); // Note E = 1 * 1_000_000_000 + } + for server in (n / 2)..n { + assert_eq!( + SubtensorModule::get_emission_for_uid(netuid, server), + 0.into() + ); + // no stake + } + run_to_block(3); + + // === Set new weights [val->srv: 1/(n/2)] to check that updated weights would produce non-zero incentive + for uid in 0..(n / 2) as u64 { + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(uid)), + netuid, + ((n / 2)..n).collect(), + vec![u16::MAX / (n / 2); (n / 2) as usize], + 0 + )); + } + if sparse { + SubtensorModule::epoch(netuid, 1_000_000_000.into()); + } else { + SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); + } + /* current_block: 3; activity_cutoff: 5000; Last update: [3, 1]; Inactive: [false, false]; + S: [1, 0]; S (mask): [1, 0]; S (mask+norm): [1, 0]; Block at registration: [0, 2]; + W: [[(1, 1)], []]; W (diagmask): [[(1, 1)], []]; W (diag+outdatemask): [[(1, 1)], []]; W (mask+norm): [[(1, 1)], []]; + R: [0, 1]; W (threshold): [[(1, 1)], []]; T: [0, 1]; C: [0.006693358, 0.9933076561]; I: [0, 1]; + B: [[], []]; B (mask+norm): [[], []]; + ΔB: [[(1, 1)], []]; ΔB (norm): [[(1, 1)], []]; emaB: [[(1, 1)], []]; D: [1, 0]; emaB (max-upscale): [[(1, 1)], []] + E: [500000000, 500000000]; P: [0.5, 0.5] */ + for validator in 0..n { + assert_eq!( + SubtensorModule::get_emission_for_uid(netuid, validator), + (1000000000 / (n as u64)).into() + ); // Note E = 1/2 * 1_000_000_000 + } + }); +} diff --git a/pallets/subtensor/src/tests/epoch/yuma_3.rs b/pallets/subtensor/src/tests/epoch/yuma_3.rs new file mode 100644 index 0000000000..4b18193cc7 --- /dev/null +++ b/pallets/subtensor/src/tests/epoch/yuma_3.rs @@ -0,0 +1,886 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Yuma3 / liquid-alpha bond and dividend trajectories across epochs. + +use frame_support::assert_ok; +use sp_core::U256; +use substrate_fixed::types::I32F32; +use subtensor_runtime_common::TaoBalance; + +use super::super::mock::*; +use crate::epoch::math::{fixed, u16_proportion_to_fixed}; +use crate::*; + +/// Asserts that two I32F32 values are approximately equal within a given epsilon. +/// +/// # Arguments +/// * `left` - The first value to compare. +/// * `right` - The second value to compare. +/// * `epsilon` - The maximum allowed difference between the two values. +pub(super) fn assert_approx_eq(left: I32F32, right: I32F32, epsilon: I32F32) { + if (left - right).abs() > epsilon { + panic!( + "assertion failed: `(left ≈ right)`\n left: `{left:?}`,\n right: `{right:?}`,\n epsilon: `{epsilon:?}`" + ); + } +} + +// test Yuma 3 scenarios over a sequence of epochs. +fn setup_yuma_3_scenario(netuid: NetUid, n: u16, sparse: bool, max_stake: u64, stakes: Vec) { + let block_number = System::block_number(); + let tempo: u16 = 1; // high tempo to skip automatic epochs in on_initialize, use manual epochs instead + add_network_disable_commit_reveal(netuid, tempo, 0); + + SubtensorModule::set_max_allowed_uids(netuid, n); + assert_eq!(SubtensorModule::get_max_allowed_uids(netuid), n); + SubtensorModule::set_max_registrations_per_block(netuid, n); + SubtensorModule::set_target_registrations_per_interval(netuid, n); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_min_allowed_weights(netuid, 1); + SubtensorModule::set_bonds_penalty(netuid, 0); + SubtensorModule::set_alpha_sigmoid_steepness(netuid, 1000); + SubtensorModule::set_bonds_moving_average(netuid, 975_000); + + // === Register + for key in 0..n as u64 { + add_balance_to_coldkey_account( + &U256::from(key), + TaoBalance::from(max_stake) + + ExistentialDeposit::get() + + SubtensorModule::get_network_min_lock(), + ); + let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( + netuid, + block_number, + key * 1_000_000, + &U256::from(key), + ); + assert_ok!(SubtensorModule::register( + <::RuntimeOrigin>::signed(U256::from(key)), + netuid, + block_number, + nonce, + work, + U256::from(key), + U256::from(key) + )); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &U256::from(key), + &U256::from(key), + netuid, + stakes[key as usize].into(), + ); + } + assert_eq!(SubtensorModule::get_max_allowed_uids(netuid), n); + assert_eq!(SubtensorModule::get_subnetwork_n(netuid), n); + + // Enable Liquid Alpha + SubtensorModule::set_kappa(netuid, u16::MAX / 2); + SubtensorModule::set_liquid_alpha_enabled(netuid, true); + SubtensorModule::set_alpha_values_32(netuid, I32F32::from_num(0.1), I32F32::from_num(0.3)); + + // Enable Yuma3 + SubtensorModule::set_yuma3_enabled(netuid, true); + + // === Issue validator permits + SubtensorModule::set_max_allowed_validators(netuid, 3); + + // run first epoch to set allowed validators + // run to next block to ensure weights are set on nodes after their registration block + run_epoch(netuid, sparse); +} + +fn run_epoch(netuid: NetUid, sparse: bool) { + next_block_no_epoch(netuid); + if sparse { + SubtensorModule::epoch(netuid, 1_000_000_000.into()); + } else { + SubtensorModule::epoch_dense(netuid, 1_000_000_000.into()); + } +} + +fn run_epoch_and_check_bonds_dividends( + netuid: NetUid, + sparse: bool, + target_bonds: &[Vec], + target_dividends: &[f32], +) { + run_epoch(netuid, sparse); + let bonds = SubtensorModule::get_bonds_fixed_proportion(netuid.into()); + let dividends = SubtensorModule::get_dividends(netuid); + + let epsilon = I32F32::from_num(1e-3); + // Check the bonds + for (bond, target_bond) in bonds.iter().zip(target_bonds.iter()) { + // skip the 3 validators + for (b, t) in bond.iter().zip(target_bond.iter().skip(3)) { + assert_approx_eq(*b, fixed(*t), epsilon); + } + } + // Check the dividends + for (dividend, target_dividend) in dividends.iter().zip(target_dividends.iter()) { + assert_approx_eq( + u16_proportion_to_fixed(*dividend), + fixed(*target_dividend), + epsilon, + ); + } +} + +fn set_yuma_3_weights(netuid: NetUid, weights: Vec>, indices: Vec) { + for (uid, weight) in weights.iter().enumerate() { + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(uid as u64)), + netuid, + indices.clone(), + weight.to_vec(), + 0 + )); + } +} + +#[test] +fn test_yuma_3_kappa_moves_first() { + for sparse in [true, false].iter() { + new_test_ext(1).execute_with(|| { + let n: u16 = 5; // 3 validators, 2 servers + let netuid = NetUid::from(1); + let max_stake: u64 = 8; + + // Validator A: kappa / Big validator (0.8) - moves first + // Validator B: Small eager validator (0.1) - moves second + // Validator C: Small lazy validator (0.1) - moves last + let stakes: Vec = vec![8, 1, 1, 0, 0]; + + setup_yuma_3_scenario(netuid, n, *sparse, max_stake, stakes); + let targets_bonds = [ + vec![ + vec![0.1013, 0.0000], + vec![0.1013, 0.0000], + vec![0.1013, 0.0000], + ], + vec![ + vec![0.0908, 0.1013], + vec![0.3697, 0.0000], + vec![0.3697, 0.0000], + ], + vec![ + vec![0.0815, 0.1924], + vec![0.3170, 0.1013], + vec![0.5580, 0.0000], + ], + vec![ + vec![0.0731, 0.2742], + vec![0.2765, 0.1924], + vec![0.4306, 0.1013], + ], + vec![ + vec![0.0656, 0.3478], + vec![0.2435, 0.2742], + vec![0.3589, 0.1924], + ], + vec![ + vec![0.0588, 0.4139], + vec![0.2157, 0.3478], + vec![0.3089, 0.2742], + ], + ]; + + let targets_dividends = [ + vec![0.8000, 0.1000, 0.1000, 0.0000, 0.0000], + vec![1.0000, 0.0000, 0.0000, 0.0000, 0.0000], + vec![0.9382, 0.0618, 0.0000, 0.0000, 0.0000], + vec![0.8819, 0.0773, 0.0407, 0.0000, 0.0000], + vec![0.8564, 0.0844, 0.0592, 0.0000, 0.0000], + vec![0.8418, 0.0884, 0.0697, 0.0000, 0.0000], + ]; + + for (epoch, (target_bonds, target_dividends)) in targets_bonds + .iter() + .zip(targets_dividends.iter()) + .enumerate() + { + match epoch { + 0 => { + // Initially, consensus is achieved by all Validators + set_yuma_3_weights(netuid, vec![vec![u16::MAX, 0]; 3], vec![3, 4]); + } + 1 => { + // Validator A -> Server 2 + // Validator B -> Server 1 + // Validator C -> Server 1 + set_yuma_3_weights( + netuid, + vec![vec![0, u16::MAX], vec![u16::MAX, 0], vec![u16::MAX, 0]], + vec![3, 4], + ); + } + 2 => { + // Validator A -> Server 2 + // Validator B -> Server 2 + // Validator C -> Server 1 + set_yuma_3_weights( + netuid, + vec![vec![0, u16::MAX], vec![0, u16::MAX], vec![u16::MAX, 0]], + vec![3, 4], + ); + } + 3 => { + // Subsequent epochs All validators -> Server 2 + set_yuma_3_weights(netuid, vec![vec![0, u16::MAX]; 3], vec![3, 4]); + } + _ => {} + }; + run_epoch_and_check_bonds_dividends( + netuid, + *sparse, + target_bonds, + target_dividends, + ); + } + }) + } +} + +#[test] +fn test_yuma_3_kappa_moves_second() { + for sparse in [true, false].iter() { + new_test_ext(1).execute_with(|| { + let n: u16 = 5; // 3 validators, 2 servers + let netuid = NetUid::from(1); + let max_stake: u64 = 8; + + // Validator A: kappa / Big validator (0.8) - moves second + // Validator B: Small eager validator (0.1) - moves first + // Validator C: Small lazy validator (0.1) - moves last + let stakes: Vec = vec![8, 1, 1, 0, 0]; + + setup_yuma_3_scenario(netuid, n, *sparse, max_stake, stakes); + let targets_bonds = [ + vec![ + vec![0.1013, 0.0000], + vec![0.1013, 0.0000], + vec![0.1013, 0.0000], + ], + vec![ + vec![0.1924, 0.0000], + vec![0.0908, 0.2987], + vec![0.1924, 0.0000], + ], + vec![ + vec![0.1715, 0.1013], + vec![0.0815, 0.3697], + vec![0.4336, 0.0000], + ], + vec![ + vec![0.1531, 0.1924], + vec![0.0731, 0.4336], + vec![0.3608, 0.1013], + ], + vec![ + vec![0.1369, 0.2742], + vec![0.0656, 0.4910], + vec![0.3103, 0.1924], + ], + vec![ + vec![0.1225, 0.3478], + vec![0.0588, 0.5426], + vec![0.2712, 0.2742], + ], + ]; + let targets_dividends = [ + vec![0.8000, 0.1000, 0.1000, 0.0000, 0.0000], + vec![0.8446, 0.0498, 0.1056, 0.0000, 0.0000], + vec![0.6868, 0.3132, 0.0000, 0.0000, 0.0000], + vec![0.7421, 0.2090, 0.0489, 0.0000, 0.0000], + vec![0.7625, 0.1706, 0.0669, 0.0000, 0.0000], + vec![0.7730, 0.1508, 0.0762, 0.0000, 0.0000], + ]; + + for (epoch, (target_bonds, target_dividends)) in targets_bonds + .iter() + .zip(targets_dividends.iter()) + .enumerate() + { + match epoch { + 0 => { + // Initially, consensus is achieved by all Validators + set_yuma_3_weights(netuid, vec![vec![u16::MAX, 0]; 3], vec![3, 4]); + } + 1 => { + // Validator A -> Server 1 + // Validator B -> Server 2 + // Validator C -> Server 1 + set_yuma_3_weights( + netuid, + vec![vec![u16::MAX, 0], vec![0, u16::MAX], vec![u16::MAX, 0]], + vec![3, 4], + ); + } + 2 => { + // Validator A -> Server 2 + // Validator B -> Server 2 + // Validator C -> Server 1 + set_yuma_3_weights( + netuid, + vec![vec![0, u16::MAX], vec![0, u16::MAX], vec![u16::MAX, 0]], + vec![3, 4], + ); + } + 3 => { + // Subsequent epochs All validators -> Server 2 + set_yuma_3_weights(netuid, vec![vec![0, u16::MAX]; 3], vec![3, 4]); + } + _ => {} + }; + run_epoch_and_check_bonds_dividends( + netuid, + *sparse, + target_bonds, + target_dividends, + ); + } + }) + } +} + +#[test] +fn test_yuma_3_kappa_moves_last() { + for sparse in [true, false].iter() { + new_test_ext(1).execute_with(|| { + let n: u16 = 5; // 3 validators, 2 servers + let netuid = NetUid::from(1); + let max_stake: u64 = 8; + + // Validator A: kappa / Big validator (0.8) - moves last + // Validator B: Small eager validator (0.1) - moves first + // Validator C: Small lazy validator (0.1) - moves second + let stakes: Vec = vec![8, 1, 1, 0, 0]; + + setup_yuma_3_scenario(netuid, n, *sparse, max_stake, stakes); + let targets_bonds = [ + vec![ + vec![0.1013, 0.0000], + vec![0.1013, 0.0000], + vec![0.1013, 0.0000], + ], + vec![ + vec![0.1924, 0.0000], + vec![0.0908, 0.2987], + vec![0.1924, 0.0000], + ], + vec![ + vec![0.2742, 0.0000], + vec![0.0815, 0.5081], + vec![0.1715, 0.2987], + ], + vec![ + vec![0.2416, 0.1013], + vec![0.0731, 0.5580], + vec![0.1531, 0.3697], + ], + vec![ + vec![0.2141, 0.1924], + vec![0.0656, 0.6028], + vec![0.1369, 0.4336], + ], + vec![ + vec![0.1903, 0.2742], + vec![0.0588, 0.6430], + vec![0.1225, 0.4910], + ], + ]; + let targets_dividends = [ + vec![0.8000, 0.1000, 0.1000, 0.0000, 0.0000], + vec![0.8446, 0.0498, 0.1056, 0.0000, 0.0000], + vec![0.8966, 0.0333, 0.0701, 0.0000, 0.0000], + vec![0.4663, 0.3210, 0.2127, 0.0000, 0.0000], + vec![0.5976, 0.2340, 0.1683, 0.0000, 0.0000], + vec![0.6592, 0.1932, 0.1475, 0.0000, 0.0000], + ]; + + for (epoch, (target_bonds, target_dividends)) in targets_bonds + .iter() + .zip(targets_dividends.iter()) + .enumerate() + { + match epoch { + 0 => { + // Initially, consensus is achieved by all Validators + set_yuma_3_weights(netuid, vec![vec![u16::MAX, 0]; 3], vec![3, 4]); + } + 1 => { + // Validator A -> Server 1 + // Validator B -> Server 2 + // Validator C -> Server 1 + set_yuma_3_weights( + netuid, + vec![vec![u16::MAX, 0], vec![0, u16::MAX], vec![u16::MAX, 0]], + vec![3, 4], + ); + } + 2 => { + // Validator A -> Server 1 + // Validator B -> Server 2 + // Validator C -> Server 2 + set_yuma_3_weights( + netuid, + vec![vec![u16::MAX, 0], vec![0, u16::MAX], vec![0, u16::MAX]], + vec![3, 4], + ); + } + 3 => { + // Subsequent epochs All validators -> Server 2 + set_yuma_3_weights(netuid, vec![vec![0, u16::MAX]; 3], vec![3, 4]); + } + _ => {} + }; + run_epoch_and_check_bonds_dividends( + netuid, + *sparse, + target_bonds, + target_dividends, + ); + } + }) + } +} + +#[test] +fn test_yuma_3_one_epoch_switch() { + for sparse in [true, false].iter() { + new_test_ext(1).execute_with(|| { + let n: u16 = 5; // 3 validators, 2 servers + let netuid = NetUid::from(1); + let max_stake: u64 = 8; + + // Equal stake validators + let stakes: Vec = vec![33, 33, 34, 0, 0]; + + setup_yuma_3_scenario(netuid, n, *sparse, max_stake, stakes); + + let targets_bonds = [ + vec![ + vec![0.1013, 0.0000], + vec![0.1013, 0.0000], + vec![0.1013, 0.0000], + ], + vec![ + vec![0.1924, 0.0000], + vec![0.1924, 0.0000], + vec![0.1924, 0.0000], + ], + vec![ + vec![0.2742, 0.0000], + vec![0.2742, 0.0000], + vec![0.1715, 0.2987], + ], + vec![ + vec![0.3478, 0.0000], + vec![0.3478, 0.0000], + vec![0.2554, 0.2618], + ], + vec![ + vec![0.4139, 0.0000], + vec![0.4139, 0.0000], + vec![0.3309, 0.2312], + ], + vec![ + vec![0.4733, 0.0000], + vec![0.4733, 0.0000], + vec![0.3987, 0.2051], + ], + ]; + let targets_dividends = [ + vec![0.3300, 0.3300, 0.3400, 0.0000, 0.0000], + vec![0.3300, 0.3300, 0.3400, 0.0000, 0.0000], + vec![0.3782, 0.3782, 0.2436, 0.0000, 0.0000], + vec![0.3628, 0.3628, 0.2745, 0.0000, 0.0000], + vec![0.3541, 0.3541, 0.2917, 0.0000, 0.0000], + vec![0.3487, 0.3487, 0.3026, 0.0000, 0.0000], + ]; + + for (epoch, (target_bonds, target_dividends)) in targets_bonds + .iter() + .zip(targets_dividends.iter()) + .enumerate() + { + match epoch { + 2 => { + // Validator A -> Server 1 + // Validator B -> Server 1 + // Validator C -> Server 2 + set_yuma_3_weights( + netuid, + vec![vec![u16::MAX, 0], vec![u16::MAX, 0], vec![0, u16::MAX]], + vec![3, 4], + ); + } + _ => { + // All validators -> Server 1 + set_yuma_3_weights(netuid, vec![vec![u16::MAX, 0]; 3], vec![3, 4]); + } + }; + run_epoch_and_check_bonds_dividends( + netuid, + *sparse, + target_bonds, + target_dividends, + ); + } + }) + } +} + +#[test] +fn test_yuma_3_liquid_alpha_disabled() { + for sparse in [true, false].iter() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let n: u16 = 5; // 3 validators, 2 servers + let max_stake: u64 = 8; + + // Equal stake validators + let stakes: Vec = vec![33, 33, 34, 0, 0]; + + setup_yuma_3_scenario(netuid, n, *sparse, max_stake, stakes); + + // disable liquid alpha + SubtensorModule::set_liquid_alpha_enabled(netuid, false); + + let targets_bonds = [ + vec![ + vec![0.0000, 0.0250, 0.0000], + vec![0.0000, 0.0250, 0.0000], + vec![0.0000, 0.0250, 0.0000], + ], + vec![ + vec![0.0000, 0.0494, 0.0000], + vec![0.0000, 0.0494, 0.0000], + vec![0.0000, 0.0494, 0.0000], + ], + vec![ + vec![0.0000, 0.0731, 0.0000], + vec![0.0000, 0.0731, 0.0000], + vec![0.0000, 0.0481, 0.0250], + ], + vec![ + vec![0.0000, 0.0963, 0.0000], + vec![0.0000, 0.0963, 0.0000], + vec![0.0000, 0.0719, 0.0244], + ], + vec![ + vec![0.0000, 0.1189, 0.0000], + vec![0.0000, 0.1189, 0.0000], + vec![0.0000, 0.0951, 0.0238], + ], + vec![ + vec![0.0000, 0.1409, 0.0000], + vec![0.0000, 0.1409, 0.0000], + vec![0.0000, 0.1178, 0.0232], + ], + ]; + let targets_dividends = [ + vec![0.3300, 0.3300, 0.3400, 0.0000, 0.0000], + vec![0.3300, 0.3300, 0.3400, 0.0000, 0.0000], + vec![0.3734, 0.3734, 0.2532, 0.0000, 0.0000], + vec![0.3611, 0.3611, 0.2779, 0.0000, 0.0000], + vec![0.3541, 0.3541, 0.2919, 0.0000, 0.0000], + vec![0.3495, 0.3495, 0.3009, 0.0000, 0.0000], + ]; + + for (epoch, (target_bonds, target_dividends)) in targets_bonds + .iter() + .zip(targets_dividends.iter()) + .enumerate() + { + match epoch { + 2 => { + // Validator A -> Server 1 + // Validator B -> Server 1 + // Validator C -> Server 2 + set_yuma_3_weights( + netuid, + vec![vec![u16::MAX, 0], vec![u16::MAX, 0], vec![0, u16::MAX]], + vec![3, 4], + ); + } + _ => { + // All validators -> Server 1 + set_yuma_3_weights(netuid, vec![vec![u16::MAX, 0]; 3], vec![3, 4]); + } + }; + run_epoch_and_check_bonds_dividends( + netuid, + *sparse, + target_bonds, + target_dividends, + ); + } + }) + } +} + +#[test] +fn test_yuma_3_stable_miner() { + for sparse in [true, false].iter() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let n: u16 = 6; // 3 validators, 3 servers + let max_stake: u64 = 8; + + // Validator A: kappa / Big validator (0.8) + // Validator B: Small eager validator (0.1) + // Validator C: Small lazy validator (0.1) + let stakes: Vec = vec![8, 1, 1, 0, 0, 0]; + + setup_yuma_3_scenario(netuid, n, *sparse, max_stake, stakes); + let targets_bonds = [ + vec![ + vec![0.0507, 0.0000, 0.0507], + vec![0.0507, 0.0000, 0.0507], + vec![0.0507, 0.0000, 0.0507], + ], + vec![ + vec![0.0962, 0.0000, 0.0962], + vec![0.0455, 0.1000, 0.0962], + vec![0.0962, 0.0000, 0.0962], + ], + vec![ + vec![0.0863, 0.0507, 0.1371], + vec![0.0408, 0.1405, 0.1371], + vec![0.1770, 0.0000, 0.1371], + ], + vec![ + vec![0.0774, 0.0962, 0.1739], + vec![0.0367, 0.1770, 0.1739], + vec![0.1579, 0.0507, 0.1739], + ], + vec![ + vec![0.0694, 0.1371, 0.2069], + vec![0.0329, 0.2097, 0.2069], + vec![0.1411, 0.0962, 0.2069], + ], + vec![ + vec![0.0623, 0.1739, 0.2366], + vec![0.0296, 0.2391, 0.2366], + vec![0.1263, 0.1371, 0.2366], + ], + ]; + let targets_dividends = [ + vec![0.8000, 0.1000, 0.1000, 0.0000, 0.0000, 0.0000], + vec![0.8226, 0.0745, 0.1028, 0.0000, 0.0000, 0.0000], + vec![0.7750, 0.1685, 0.0565, 0.0000, 0.0000, 0.0000], + vec![0.7864, 0.1372, 0.0764, 0.0000, 0.0000, 0.0000], + vec![0.7912, 0.1241, 0.0847, 0.0000, 0.0000, 0.0000], + vec![0.7937, 0.1173, 0.0890, 0.0000, 0.0000, 0.0000], + ]; + + for (epoch, (target_bonds, target_dividends)) in targets_bonds + .iter() + .zip(targets_dividends.iter()) + .enumerate() + { + match epoch { + 0 => { + // all validators 0.5 for first and third server + set_yuma_3_weights( + netuid, + vec![vec![u16::MAX / 2, 0, u16::MAX / 2]; 3], + vec![3, 4, 5], + ); + } + 1 => { + // one of small validators moves 0.5 to seconds server + set_yuma_3_weights( + netuid, + vec![ + vec![u16::MAX / 2, 0, u16::MAX / 2], + vec![0, u16::MAX / 2, u16::MAX / 2], + vec![u16::MAX / 2, 0, u16::MAX / 2], + ], + vec![3, 4, 5], + ); + } + 2 => { + // big validator follows + set_yuma_3_weights( + netuid, + vec![ + vec![0, u16::MAX / 2, u16::MAX / 2], + vec![0, u16::MAX / 2, u16::MAX / 2], + vec![u16::MAX / 2, 0, u16::MAX / 2], + ], + vec![3, 4, 5], + ); + } + 3 => { + // Subsequent epochs all validators have moves + set_yuma_3_weights( + netuid, + vec![vec![0, u16::MAX / 2, u16::MAX / 2]; 3], + vec![3, 4, 5], + ); + } + _ => {} + }; + run_epoch_and_check_bonds_dividends( + netuid, + *sparse, + target_bonds, + target_dividends, + ); + } + }) + } +} + +#[test] +fn test_yuma_3_bonds_reset() { + new_test_ext(1).execute_with(|| { + let sparse: bool = true; + let n: u16 = 5; // 3 validators, 2 servers + let netuid = NetUid::from(1); + let max_stake: u64 = 8; + + // "Case 8 - big vali moves late, then late" + // Big dishonest lazy vali. (0.8) + // Small eager-eager vali. (0.1) + // Small eager-eager vali 2. (0.1) + let stakes: Vec = vec![8, 1, 1, 0, 0]; + + setup_yuma_3_scenario(netuid, n, sparse, max_stake, stakes); + SubtensorModule::set_bonds_reset(netuid, true); + + // target bonds and dividends for specific epoch + let targets_dividends: std::collections::HashMap<_, _> = [ + (0, vec![0.8000, 0.1000, 0.1000, 0.0000, 0.0000]), + (1, vec![0.8944, 0.0528, 0.0528, 0.0000, 0.0000]), + (2, vec![0.5230, 0.2385, 0.2385, 0.0000, 0.0000]), + (19, vec![0.7919, 0.1040, 0.1040, 0.0000, 0.0000]), + (20, vec![0.7928, 0.1036, 0.1036, 0.0000, 0.0000]), + (21, vec![0.8467, 0.0766, 0.0766, 0.0000, 0.0000]), + (40, vec![0.7928, 0.1036, 0.1036, 0.0000, 0.0000]), + ] + .into_iter() + .collect(); + let targets_bonds: std::collections::HashMap<_, _> = [ + ( + 0, + vec![ + vec![0.1013, 0.0000], + vec![0.1013, 0.0000], + vec![0.1013, 0.0000], + ], + ), + ( + 1, + vec![ + vec![0.1924, 0.0000], + vec![0.0908, 0.2987], + vec![0.0908, 0.2987], + ], + ), + ( + 2, + vec![ + vec![0.1715, 0.1013], + vec![0.0815, 0.3697], + vec![0.0815, 0.3697], + ], + ), + ( + 19, + vec![ + vec![0.0269, 0.8539], + vec![0.0131, 0.8975], + vec![0.0131, 0.8975], + ], + ), + ( + 20, + vec![ + vec![0.0000, 0.8687], + vec![0.0000, 0.9079], + vec![0.0000, 0.9079], + ], + ), + ( + 21, + vec![ + vec![0.0000, 0.8820], + vec![0.2987, 0.6386], + vec![0.2987, 0.6386], + ], + ), + ( + 40, + vec![ + vec![0.8687, 0.0578], + vec![0.9079, 0.0523], + vec![0.9079, 0.0523], + ], + ), + ] + .into_iter() + .collect(); + + for epoch in 0..=40 { + match epoch { + 0 => { + // All validators -> Server 1 + set_yuma_3_weights(netuid, vec![vec![u16::MAX, 0]; 3], vec![3, 4]); + } + 1 => { + // validators B, C switch + // Validator A -> Server 1 + // Validator B -> Server 2 + // Validator C -> Server 2 + set_yuma_3_weights( + netuid, + vec![vec![u16::MAX, 0], vec![0, u16::MAX], vec![0, u16::MAX]], + vec![3, 4], + ); + } + (2..=20) => { + // validator A copies weights + // All validators -> Server 2 + set_yuma_3_weights(netuid, vec![vec![0, u16::MAX]; 3], vec![3, 4]); + if epoch == 20 { + let hotkey = SubtensorModule::get_hotkey_for_net_and_uid(netuid, 3) + .expect("Hotkey not found"); + let _ = SubtensorModule::reset_bonds_column_for_hotkey(netuid.into(), &hotkey); + } + } + 21 => { + // validators B, C switch back + // Validator A -> Server 2 + // Validator B -> Server 1 + // Validator C -> Server 1 + set_yuma_3_weights( + netuid, + vec![vec![0, u16::MAX], vec![u16::MAX, 0], vec![u16::MAX, 0]], + vec![3, 4], + ); + } + _ => { + // validator A copies weights + // All validators -> Server 1 + set_yuma_3_weights(netuid, vec![vec![u16::MAX, 0]; 3], vec![3, 4]); + } + }; + + if let Some((target_dividend, target_bond)) = + targets_dividends.get(&epoch).zip(targets_bonds.get(&epoch)) + { + run_epoch_and_check_bonds_dividends(netuid, sparse, target_bond, target_dividend); + } else { + run_epoch(netuid, sparse); + } + } + }) +} diff --git a/pallets/subtensor/src/tests/epoch_logs.rs b/pallets/subtensor/src/tests/epoch_logs.rs index 265add2802..e2570852dc 100644 --- a/pallets/subtensor/src/tests/epoch_logs.rs +++ b/pallets/subtensor/src/tests/epoch_logs.rs @@ -1,3 +1,7 @@ +//! Epoch behavior asserted via captured tracing logs ([`crate::epoch`]). +//! +//! Covers inactive/permit masks, Yuma bonds pipelines, and multi-mechanism weights. + #![allow( clippy::arithmetic_side_effects, clippy::indexing_slicing, diff --git a/pallets/subtensor/src/tests/evm.rs b/pallets/subtensor/src/tests/evm.rs index d2e98779d4..43f8b3b095 100644 --- a/pallets/subtensor/src/tests/evm.rs +++ b/pallets/subtensor/src/tests/evm.rs @@ -1,3 +1,7 @@ +//! Tests for [`crate::Pallet::associate_evm_key`]. +//! +//! Covers ownership/registration guards, hash verification, rate limits, and address index caps. + #![allow( clippy::arithmetic_side_effects, clippy::expect_used, diff --git a/pallets/subtensor/src/tests/hotkey_lineage.rs b/pallets/subtensor/src/tests/hotkey_lineage.rs index c21466cace..121ad51814 100644 --- a/pallets/subtensor/src/tests/hotkey_lineage.rs +++ b/pallets/subtensor/src/tests/hotkey_lineage.rs @@ -1,3 +1,7 @@ +//! Tests for hotkey-swap lineage recording ([`crate::swap::hotkey_lineage`]). +//! +//! Verifies tip/chain updates and rollback when a swap fails mid-flight. + #![allow(clippy::unwrap_used, clippy::expect_used)] use frame_support::{assert_noop, assert_ok}; @@ -22,7 +26,7 @@ fn test_hotkey_swap_records_lineage_on_subnet_only() { add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( + assert_ok!(SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &h0, &h1, @@ -52,7 +56,7 @@ fn test_hotkey_swap_lineage_chain_and_tip() { add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get() + 1); - assert_ok!(SubtensorModule::do_swap_hotkey( + assert_ok!(SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &h0, &h1, @@ -62,7 +66,7 @@ fn test_hotkey_swap_lineage_chain_and_tip() { // Cooldown is strict `<`: need interval + 1 after the recorded swap block. System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get() + 1); - assert_ok!(SubtensorModule::do_swap_hotkey( + assert_ok!(SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &h1, &h2, @@ -90,7 +94,7 @@ fn test_hotkey_swap_all_subnets_records_lineage_on_each() { add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( + assert_ok!(SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &h0, &h1, @@ -139,7 +143,7 @@ fn test_all_subnets_swap_records_lineage_for_residual_collateral() { )); System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get() + 1); - assert_ok!(SubtensorModule::do_swap_hotkey( + assert_ok!(SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &h0, &h1, @@ -196,7 +200,7 @@ fn test_bonded_hotkey_swap_migrates_collateral_keep_stake_blocked() { System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); assert_noop!( - SubtensorModule::do_swap_hotkey( + SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &h0, &h1, @@ -206,7 +210,7 @@ fn test_bonded_hotkey_swap_migrates_collateral_keep_stake_blocked() { Error::::KeepStakeBlockedByCollateral ); - assert_ok!(SubtensorModule::do_swap_hotkey( + assert_ok!(SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &h0, &h1, @@ -225,7 +229,7 @@ fn test_bonded_hotkey_swap_migrates_collateral_keep_stake_blocked() { SubtensorModule::set_validator_permit_for_uid(netuid, uid, true); System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get() + 1); assert_noop!( - SubtensorModule::do_swap_hotkey( + SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &h1, &h2, @@ -289,7 +293,7 @@ fn test_bonded_hotkey_swap_renames_index_at_cap() { ); System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( + assert_ok!(SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &h0, &h1, @@ -352,7 +356,7 @@ fn test_unindexed_collateral_at_full_cap_rolls_back_hotkey_swap() { System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); assert_noop!( - SubtensorModule::do_swap_hotkey( + SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &h0, &h1, @@ -384,7 +388,7 @@ fn test_hotkey_lineage_reverse_swap_does_not_cycle() { add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get() + 1); - assert_ok!(SubtensorModule::do_swap_hotkey( + assert_ok!(SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &h0, &h1, @@ -394,7 +398,7 @@ fn test_hotkey_lineage_reverse_swap_does_not_cycle() { assert_eq!(HotkeySuccessor::::get(netuid, h0), Some(h1)); System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get() + 1); - assert_ok!(SubtensorModule::do_swap_hotkey( + assert_ok!(SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &h1, &h0, @@ -423,7 +427,7 @@ fn test_reregister_clears_stale_successor_for_tip() { add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get() + 1); - assert_ok!(SubtensorModule::do_swap_hotkey( + assert_ok!(SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &h0, &h1, @@ -440,7 +444,7 @@ fn test_reregister_clears_stale_successor_for_tip() { assert!(SubtensorModule::same_hotkey_lineage(netuid, &h0, &h1)); System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get() + 1); - assert_ok!(SubtensorModule::do_swap_hotkey( + assert_ok!(SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &h0, &h2, @@ -464,7 +468,7 @@ fn test_dissolve_clears_hotkey_lineage_maps() { add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get() + 1); - assert_ok!(SubtensorModule::do_swap_hotkey( + assert_ok!(SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &h0, &h1, diff --git a/pallets/subtensor/src/tests/leasing.rs b/pallets/subtensor/src/tests/leasing.rs index 0bc00d139d..878747cb0b 100644 --- a/pallets/subtensor/src/tests/leasing.rs +++ b/pallets/subtensor/src/tests/leasing.rs @@ -1,8 +1,13 @@ +//! Tests for subnet leasing ([`crate::subnets::leasing`]). +//! +//! Covers lease creation, payments, takeover, and lease-end cleanup. + #![allow( clippy::arithmetic_side_effects, clippy::unwrap_used, clippy::indexing_slicing )] + use super::mock::*; use crate::{subnets::leasing::SubnetLeaseOf, *}; use frame_support::{StorageDoubleMap, assert_err, assert_ok}; diff --git a/pallets/subtensor/src/tests/locks.rs b/pallets/subtensor/src/tests/locks.rs deleted file mode 100644 index cbf87a7901..0000000000 --- a/pallets/subtensor/src/tests/locks.rs +++ /dev/null @@ -1,5087 +0,0 @@ -#![allow( - clippy::arithmetic_side_effects, - clippy::expect_used, - clippy::indexing_slicing, - clippy::unwrap_used -)] - -use approx::assert_abs_diff_eq; -use frame_support::dispatch::{GetDispatchInfo, Pays}; -use frame_support::weights::Weight; -use frame_support::{assert_noop, assert_ok}; -use safe_math::FixedExt; -use sp_core::U256; -use substrate_fixed::types::U64F64; -use subtensor_runtime_common::{AlphaBalance, NetUidStorageIndex, TaoBalance}; -use subtensor_swap_interface::SwapHandler; - -use super::mock::*; -use crate::staking::lock::{ConvictionModel, LockState}; -use crate::*; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -fn setup_subnet_with_stake( - coldkey: U256, - hotkey: U256, - stake_tao: u64, -) -> subtensor_runtime_common::NetUid { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - let amount: TaoBalance = (stake_tao).into(); - setup_reserves( - netuid, - (stake_tao * 1_000_000).into(), - (stake_tao * 10_000_000).into(), - ); - - assert_ok!(SubtensorModule::create_account_if_non_existent( - &coldkey, &hotkey - )); - add_balance_to_coldkey_account(&coldkey, amount); - SubtensorModule::stake_into_subnet( - &hotkey, - &coldkey, - netuid, - amount, - ::SwapInterface::max_price(), - false, - ) - .unwrap(); - DecayingLock::::insert(coldkey, netuid, false); - - netuid -} - -fn get_alpha( - hotkey: &U256, - coldkey: &U256, - netuid: subtensor_runtime_common::NetUid, -) -> AlphaBalance { - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid) -} - -fn roll_forward_lock( - lock: LockState, - now: u64, - owner_lock: bool, - perpetual_lock: bool, -) -> LockState { - ConvictionModel::roll_forward_lock( - lock, - now, - UnlockRate::::get(), - MaturityRate::::get(), - owner_lock, - perpetual_lock, - ) - .0 -} - -fn roll_forward_individual_lock( - coldkey: &U256, - netuid: subtensor_runtime_common::NetUid, - hotkey: &U256, - lock: LockState, - now: u64, -) -> LockState { - roll_forward_lock( - lock, - now, - hotkey == &SubnetOwnerHotkey::::get(netuid), - DecayingLock::::get(coldkey, netuid) == Some(false), - ) -} - -#[test] -fn test_account_flags_default_to_zero_and_reject_locked_alpha_setter_pays_fee() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - - assert_eq!(AccountFlags::::get(coldkey), 0); - assert!(!AccountFlags::::contains_key(coldkey)); - assert!(SubtensorModule::account_rejects_locked_alpha(&coldkey)); - - let call = - RuntimeCall::SubtensorModule(crate::Call::set_reject_locked_alpha { enabled: true }); - assert_eq!(call.get_dispatch_info().pays_fee, Pays::Yes); - - assert_ok!(SubtensorModule::set_reject_locked_alpha( - RuntimeOrigin::signed(coldkey), - false, - )); - assert_eq!( - AccountFlags::::get(coldkey), - ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA - ); - assert!(AccountFlags::::contains_key(coldkey)); - assert!(!SubtensorModule::account_rejects_locked_alpha(&coldkey)); - - assert_ok!(SubtensorModule::set_reject_locked_alpha( - RuntimeOrigin::signed(coldkey), - true, - )); - assert_eq!(AccountFlags::::get(coldkey), 0); - assert!(!AccountFlags::::contains_key(coldkey)); - assert!(SubtensorModule::account_rejects_locked_alpha(&coldkey)); - }); -} - -fn roll_forward_hotkey_lock(lock: LockState, now: u64) -> LockState { - roll_forward_lock(lock, now, false, true) -} - -fn roll_forward_decaying_hotkey_lock(lock: LockState, now: u64) -> LockState { - roll_forward_lock(lock, now, false, false) -} - -// ========================================================================= -// GROUP 1: Green-path — basic lock creation -// ========================================================================= - -#[test] -fn test_lock_stake_creates_new_lock() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - let alpha = get_alpha(&hotkey, &coldkey, netuid); - let lock_amount = alpha.to_u64() / 2; - - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - lock_amount.into(), - )); - - let lock = Lock::::get((coldkey, netuid, hotkey)).expect("Lock should exist"); - assert_eq!(lock.locked_mass, lock_amount.into()); - assert_eq!(lock.conviction, U64F64::from_num(0)); - assert_eq!( - lock.last_update, - SubtensorModule::get_current_block_as_u64() - ); - - // Hotkey lock should also be created - let hotkey_lock = HotkeyLock::::get(netuid, hotkey); - assert!(hotkey_lock.is_some()); - assert_eq!(hotkey_lock.unwrap().locked_mass, lock_amount.into()); - }); -} - -#[test] -fn test_lock_stake_defaults_to_decaying_lock() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - DecayingLock::::remove(coldkey, netuid); - - let lock_amount: AlphaBalance = 5000u64.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - lock_amount, - )); - - assert!(DecayingLock::::get(coldkey, netuid).is_none()); - assert!(HotkeyLock::::get(netuid, hotkey).is_none()); - - let decaying_hotkey_lock = DecayingHotkeyLock::::get(netuid, hotkey) - .expect("default lock should use decaying aggregate"); - assert_eq!(decaying_hotkey_lock.locked_mass, lock_amount); - }); -} - -#[test] -fn test_lock_stake_by_subnet_owner_coldkey_gets_immediate_conviction() { - new_test_ext(1).execute_with(|| { - let owner_coldkey = U256::from(1); - let owner_hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(owner_coldkey, owner_hotkey, 300_000_000_000); - SubnetOwner::::insert(netuid, owner_coldkey); - SubnetOwnerHotkey::::insert(netuid, owner_hotkey); - - let lock_amount: AlphaBalance = 5000u64.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &owner_coldkey, - netuid, - &owner_hotkey, - lock_amount, - )); - - let lock = Lock::::get((owner_coldkey, netuid, owner_hotkey)) - .expect("lock to owner hotkey should exist"); - assert_eq!(lock.locked_mass, lock_amount); - assert_eq!(lock.conviction, U64F64::saturating_from_num(5000)); - let owner_lock = OwnerLock::::get(netuid).expect("owner lock should exist"); - assert_eq!(owner_lock.locked_mass, lock_amount); - assert_eq!(owner_lock.conviction, U64F64::saturating_from_num(5000)); - }); -} - -#[test] -fn test_lock_to_subnet_owner_hotkey_gets_immediate_conviction_for_non_owner_coldkey() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let staker_hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, staker_hotkey, 300_000_000_000); - let owner_hotkey = SubnetOwnerHotkey::::get(netuid); - - let lock_amount: AlphaBalance = 5000u64.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &owner_hotkey, - lock_amount, - )); - - let lock = Lock::::get((coldkey, netuid, owner_hotkey)) - .expect("lock to owner hotkey should exist"); - assert_eq!(lock.locked_mass, lock_amount); - assert_eq!(lock.conviction, U64F64::saturating_from_num(5000)); - - let owner_lock = OwnerLock::::get(netuid).expect("owner lock should exist"); - assert_eq!(owner_lock.locked_mass, lock_amount); - assert_eq!(owner_lock.conviction, U64F64::saturating_from_num(5000)); - assert!( - HotkeyLock::::get(netuid, owner_hotkey).is_none(), - "lock to owner hotkey should use OwnerLock, not HotkeyLock" - ); - }); -} - -#[test] -fn test_decaying_lock_to_subnet_owner_hotkey_keeps_decaying_mass() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let staker_hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, staker_hotkey, 300_000_000_000); - let owner_hotkey = SubnetOwnerHotkey::::get(netuid); - - assert_ok!(SubtensorModule::do_set_perpetual_lock( - &coldkey, netuid, false, - )); - - let lock_amount: AlphaBalance = 5000u64.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &owner_hotkey, - lock_amount, - )); - - step_block(1_000); - let now = SubtensorModule::get_current_block_as_u64(); - let rolled = roll_forward_individual_lock( - &coldkey, - netuid, - &owner_hotkey, - Lock::::get((coldkey, netuid, owner_hotkey)).unwrap(), - now, - ); - - assert!(rolled.locked_mass < lock_amount); - assert_eq!( - rolled.conviction, - U64F64::saturating_from_num(u64::from(rolled.locked_mass)) - ); - assert_eq!( - SubtensorModule::hotkey_conviction(&owner_hotkey, netuid), - rolled.conviction - ); - assert!( - OwnerLock::::get(netuid).is_none(), - "decaying lock to owner hotkey should not use perpetual OwnerLock" - ); - assert!( - DecayingOwnerLock::::get(netuid).is_some(), - "decaying lock to owner hotkey should use DecayingOwnerLock" - ); - }); -} - -#[test] -fn test_lock_by_subnet_owner_coldkey_to_non_owner_hotkey_matures_normally() { - new_test_ext(1).execute_with(|| { - let owner_coldkey = U256::from(1); - let non_owner_hotkey = U256::from(2); - let owner_hotkey = U256::from(3); - let netuid = setup_subnet_with_stake(owner_coldkey, non_owner_hotkey, 300_000_000_000); - assert_ok!(SubtensorModule::create_account_if_non_existent( - &owner_coldkey, - &owner_hotkey - )); - SubnetOwner::::insert(netuid, owner_coldkey); - SubnetOwnerHotkey::::insert(netuid, owner_hotkey); - - let lock_amount: AlphaBalance = 5000u64.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &owner_coldkey, - netuid, - &non_owner_hotkey, - lock_amount, - )); - - let lock = Lock::::get((owner_coldkey, netuid, non_owner_hotkey)) - .expect("lock to non-owner hotkey should exist"); - assert_eq!(lock.locked_mass, lock_amount); - assert_eq!(lock.conviction, U64F64::saturating_from_num(0)); - assert!( - OwnerLock::::get(netuid).is_none(), - "owner coldkey lock to a non-owner hotkey should not use OwnerLock" - ); - - let hotkey_lock = - HotkeyLock::::get(netuid, non_owner_hotkey).expect("hotkey lock should exist"); - assert_eq!(hotkey_lock.locked_mass, lock_amount); - assert_eq!(hotkey_lock.conviction, U64F64::saturating_from_num(0)); - }); -} - -#[test] -fn test_lock_stake_topup_by_subnet_owner_coldkey_gets_immediate_conviction() { - new_test_ext(1).execute_with(|| { - let owner_coldkey = U256::from(1); - let owner_hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(owner_coldkey, owner_hotkey, 100_000_000_000); - SubnetOwner::::insert(netuid, owner_coldkey); - SubnetOwnerHotkey::::insert(netuid, owner_hotkey); - - let first_lock: AlphaBalance = 5000u64.into(); - let second_lock: AlphaBalance = 7000u64.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &owner_coldkey, - netuid, - &owner_hotkey, - first_lock, - )); - assert_ok!(SubtensorModule::do_lock_stake( - &owner_coldkey, - netuid, - &owner_hotkey, - second_lock, - )); - - let expected_locked = first_lock + second_lock; - let lock = Lock::::get((owner_coldkey, netuid, owner_hotkey)) - .expect("lock to owner hotkey should exist"); - assert_eq!(lock.locked_mass, expected_locked); - assert_eq!( - lock.conviction, - U64F64::saturating_from_num(u64::from(expected_locked)) - ); - - let owner_lock = OwnerLock::::get(netuid).expect("owner lock should exist"); - assert_eq!(owner_lock.locked_mass, expected_locked); - assert_eq!( - owner_lock.conviction, - U64F64::saturating_from_num(u64::from(expected_locked)) - ); - }); -} - -#[test] -fn test_set_perpetual_lock_toggles_owner_lock_decay() { - new_test_ext(1).execute_with(|| { - let owner_coldkey = U256::from(1); - let owner_hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(owner_coldkey, owner_hotkey, 100_000_000_000); - SubnetOwner::::insert(netuid, owner_coldkey); - SubnetOwnerHotkey::::insert(netuid, owner_hotkey); - - let lock_amount: AlphaBalance = 5000u64.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &owner_coldkey, - netuid, - &owner_hotkey, - lock_amount, - )); - - assert_ok!(SubtensorModule::set_perpetual_lock( - RuntimeOrigin::signed(owner_coldkey), - netuid, - true, - )); - step_block(100); - assert_eq!( - SubtensorModule::get_current_locked(&owner_coldkey, netuid), - lock_amount - ); - - assert_ok!(SubtensorModule::set_perpetual_lock( - RuntimeOrigin::signed(owner_coldkey), - netuid, - false, - )); - step_block(100); - assert!(SubtensorModule::get_current_locked(&owner_coldkey, netuid) < lock_amount); - }); -} - -#[test] -fn test_set_perpetual_lock_is_per_coldkey_and_rolls_lock_at_boundary() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 300_000_000_000); - - let lock_amount: AlphaBalance = 5000u64.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - lock_amount, - )); - - assert_ok!(SubtensorModule::set_perpetual_lock( - RuntimeOrigin::signed(coldkey), - netuid, - false, - )); - System::set_block_number(System::block_number() + UnlockRate::::get() / 10); - assert_ok!(SubtensorModule::set_perpetual_lock( - RuntimeOrigin::signed(coldkey), - netuid, - true, - )); - - let locked_at_boundary = SubtensorModule::get_current_locked(&coldkey, netuid); - assert!(locked_at_boundary < lock_amount); - - System::set_block_number(System::block_number() + UnlockRate::::get() / 10); - assert_eq!( - SubtensorModule::get_current_locked(&coldkey, netuid), - locked_at_boundary - ); - - assert_ok!(SubtensorModule::set_perpetual_lock( - RuntimeOrigin::signed(coldkey), - netuid, - false, - )); - System::set_block_number(System::block_number() + UnlockRate::::get() / 10); - assert!(SubtensorModule::get_current_locked(&coldkey, netuid) < locked_at_boundary); - }); -} - -#[test] -fn test_mixed_perpetual_and_decaying_non_owner_locks_same_hotkey_update_aggregates() { - new_test_ext(1).execute_with(|| { - let perpetual_coldkey = U256::from(1); - let decaying_coldkey = U256::from(3); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(perpetual_coldkey, hotkey, 100_000_000_000); - - assert_ok!(SubtensorModule::create_account_if_non_existent( - &decaying_coldkey, - &hotkey - )); - add_balance_to_coldkey_account(&decaying_coldkey, 100_000_000_000u64.into()); - SubtensorModule::stake_into_subnet( - &hotkey, - &decaying_coldkey, - netuid, - 100_000_000_000u64.into(), - ::SwapInterface::max_price(), - false, - ) - .unwrap(); - - let lock_amount: AlphaBalance = 10_000u64.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &perpetual_coldkey, - netuid, - &hotkey, - lock_amount, - )); - assert_ok!(SubtensorModule::do_lock_stake( - &decaying_coldkey, - netuid, - &hotkey, - lock_amount, - )); - assert_ok!(SubtensorModule::do_set_perpetual_lock( - &decaying_coldkey, - netuid, - false, - )); - - step_block(1_000); - let now = SubtensorModule::get_current_block_as_u64(); - - let perpetual_lock = roll_forward_individual_lock( - &perpetual_coldkey, - netuid, - &hotkey, - Lock::::get((perpetual_coldkey, netuid, hotkey)).unwrap(), - now, - ); - let decaying_lock = roll_forward_individual_lock( - &decaying_coldkey, - netuid, - &hotkey, - Lock::::get((decaying_coldkey, netuid, hotkey)).unwrap(), - now, - ); - let perpetual_hotkey_lock = - roll_forward_hotkey_lock(HotkeyLock::::get(netuid, hotkey).unwrap(), now); - let decaying_hotkey_lock = roll_forward_decaying_hotkey_lock( - DecayingHotkeyLock::::get(netuid, hotkey).unwrap(), - now, - ); - - assert_eq!(perpetual_lock.locked_mass, lock_amount); - assert_eq!(perpetual_hotkey_lock.locked_mass, lock_amount); - assert!(decaying_lock.locked_mass < lock_amount); - assert_eq!(decaying_hotkey_lock.locked_mass, decaying_lock.locked_mass); - assert_eq!( - SubtensorModule::hotkey_conviction(&hotkey, netuid), - perpetual_hotkey_lock - .conviction - .saturating_add(decaying_hotkey_lock.conviction) - ); - }); -} - -#[test] -#[ignore] -fn plot_perpetual_decay_perpetual_lock_curve() { - new_test_ext(1).execute_with(|| { - const ALPHA: u64 = 1_000_000_000; - const ALPHA_F64: f64 = ALPHA as f64; - - let owner_coldkey = U256::from(1); - let owner_hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(owner_coldkey, owner_hotkey, 300_000_000_000); - SubnetOwner::::insert(netuid, owner_coldkey); - SubnetOwnerHotkey::::insert(netuid, owner_hotkey); - MaturityRate::::put(300u64); - UnlockRate::::put(200u64); - - let lock_amount: AlphaBalance = (1_000u64 * ALPHA).into(); - assert_ok!(SubtensorModule::do_lock_stake( - &owner_coldkey, - netuid, - &owner_hotkey, - lock_amount, - )); - assert_ok!(SubtensorModule::do_set_perpetual_lock( - &owner_coldkey, - netuid, - true, - )); - - println!("block,locked_mass,conviction"); - for block in 0..=2_000u64 { - System::set_block_number(block); - - if block == 1_000 { - assert_ok!(SubtensorModule::do_set_perpetual_lock( - &owner_coldkey, - netuid, - false, - )); - } else if block == 1_200 { - assert_ok!(SubtensorModule::do_set_perpetual_lock( - &owner_coldkey, - netuid, - true, - )); - } - - let lock = Lock::::get((owner_coldkey, netuid, owner_hotkey)).unwrap(); - let rolled = - roll_forward_individual_lock(&owner_coldkey, netuid, &owner_hotkey, lock, block); - SubtensorModule::insert_lock_state( - &owner_coldkey, - netuid, - &owner_hotkey, - rolled.clone(), - ); - SubtensorModule::insert_owner_lock_state(netuid, rolled.clone()); - println!( - "{},{},{}", - block, - u64::from(rolled.locked_mass) as f64 / ALPHA_F64, - rolled.conviction.to_num::() / ALPHA_F64 - ); - } - }); -} - -#[test] -#[ignore] -fn plot_decaying_non_owner_lock_curve() { - new_test_ext(1).execute_with(|| { - const ALPHA: u64 = 1_000_000_000; - const ALPHA_F64: f64 = ALPHA as f64; - - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 300_000_000_000); - MaturityRate::::put(300u64); - UnlockRate::::put(200u64); - System::set_block_number(0); - - let lock_amount: AlphaBalance = (1_000u64 * ALPHA).into(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - lock_amount, - )); - assert_ok!(SubtensorModule::do_set_perpetual_lock( - &coldkey, netuid, false, - )); - - println!("block,locked_mass,conviction"); - for block in 0..=2_000u64 { - System::set_block_number(block); - - let lock = Lock::::get((coldkey, netuid, hotkey)).unwrap(); - let rolled = roll_forward_individual_lock(&coldkey, netuid, &hotkey, lock, block); - SubtensorModule::insert_lock_state(&coldkey, netuid, &hotkey, rolled.clone()); - SubtensorModule::insert_hotkey_lock_state(netuid, &hotkey, rolled.clone()); - println!( - "{},{},{}", - block, - u64::from(rolled.locked_mass) as f64 / ALPHA_F64, - rolled.conviction.to_num::() / ALPHA_F64 - ); - } - }); -} - -#[test] -#[ignore] -fn plot_perpetual_decay_perpetual_non_owner_lock_curve() { - new_test_ext(1).execute_with(|| { - const ALPHA: u64 = 1_000_000_000; - const ALPHA_F64: f64 = ALPHA as f64; - - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 1_000_000_000_000); - MaturityRate::::put(300u64); - UnlockRate::::put(200u64); - System::set_block_number(0); - - let lock_amount: AlphaBalance = (1_000u64 * ALPHA).into(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - lock_amount, - )); - assert_ok!(SubtensorModule::do_set_perpetual_lock( - &coldkey, netuid, true, - )); - - println!("block,locked_mass,conviction"); - for block in 0..=2_000u64 { - System::set_block_number(block); - - if block == 1_000 { - assert_ok!(SubtensorModule::do_set_perpetual_lock( - &coldkey, netuid, false, - )); - } else if block == 1_200 { - assert_ok!(SubtensorModule::do_set_perpetual_lock( - &coldkey, netuid, true, - )); - } - - let lock = Lock::::get((coldkey, netuid, hotkey)).unwrap(); - let rolled = roll_forward_individual_lock(&coldkey, netuid, &hotkey, lock, block); - SubtensorModule::insert_lock_state(&coldkey, netuid, &hotkey, rolled.clone()); - if DecayingLock::::get(coldkey, netuid) == Some(false) { - SubtensorModule::insert_hotkey_lock_state(netuid, &hotkey, rolled.clone()); - } else { - SubtensorModule::insert_decaying_hotkey_lock_state(netuid, &hotkey, rolled.clone()); - } - println!( - "{},{},{}", - block, - u64::from(rolled.locked_mass) as f64 / ALPHA_F64, - rolled.conviction.to_num::() / ALPHA_F64 - ); - - // Add more lock (emulate owner auto-lock) - let auto_lock_amount: AlphaBalance = 200_000_000_u64.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - auto_lock_amount, - )); - } - }); -} - -#[test] -fn test_lock_stake_emits_event() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - let lock_amount: u64 = 1000; - - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - lock_amount.into(), - )); - - System::assert_last_event( - Event::StakeLocked { - coldkey, - hotkey, - netuid, - amount: lock_amount.into(), - } - .into(), - ); - }); -} - -#[test] -fn test_lock_stake_full_amount() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - let total_alpha = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - assert!(!total_alpha.is_zero()); - - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - total_alpha, - )); - - let lock = Lock::::get((coldkey, netuid, hotkey)).unwrap(); - assert_eq!(lock.locked_mass, total_alpha); - }); -} - -// ========================================================================= -// GROUP 2: Green-path — lock queries -// ========================================================================= - -#[test] -fn test_get_current_locked_no_lock() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let netuid = subtensor_runtime_common::NetUid::from(1); - assert_eq!( - SubtensorModule::get_current_locked(&coldkey, netuid), - AlphaBalance::ZERO - ); - }); -} - -#[test] -fn test_get_conviction_no_lock() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let netuid = subtensor_runtime_common::NetUid::from(1); - assert_eq!( - SubtensorModule::get_conviction(&coldkey, netuid), - U64F64::from_num(0) - ); - }); -} - -#[test] -fn test_get_coldkey_lock_rolls_forward() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - 5000u64.into(), - )); - - let initial_lock = - SubtensorModule::get_coldkey_lock(&coldkey, netuid).expect("coldkey lock should exist"); - assert_eq!(initial_lock.conviction, U64F64::from_num(0)); - - step_block(1000); - - let rolled_lock = - SubtensorModule::get_coldkey_lock(&coldkey, netuid).expect("coldkey lock should exist"); - assert_eq!(rolled_lock.locked_mass, initial_lock.locked_mass); - assert!(rolled_lock.conviction > initial_lock.conviction); - assert_eq!( - rolled_lock.last_update, - SubtensorModule::get_current_block_as_u64() - ); - }); -} - -#[test] -fn test_get_coldkey_lock_no_lock() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let netuid = subtensor_runtime_common::NetUid::from(1); - - assert!(SubtensorModule::get_coldkey_lock(&coldkey, netuid).is_none()); - }); -} - -#[test] -fn test_available_to_unstake_no_lock() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - let available = SubtensorModule::available_to_unstake(&coldkey, netuid); - assert_eq!(available, total); - }); -} - -#[test] -fn test_available_to_unstake_with_lock() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - let lock_amount = total / 2.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - lock_amount, - )); - - let available = SubtensorModule::available_to_unstake(&coldkey, netuid); - assert_eq!(available, total - lock_amount); - }); -} - -#[test] -fn test_available_to_unstake_fully_locked() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, netuid, &hotkey, total, - )); - - let available = SubtensorModule::available_to_unstake(&coldkey, netuid); - assert_eq!(available, AlphaBalance::ZERO); - }); -} - -#[test] -fn test_stake_availability_for_coldkeys_empty_coldkeys() { - new_test_ext(1).execute_with(|| { - let result = SubtensorModule::get_stake_availability_for_coldkeys(Vec::new(), None); - assert!(result.is_empty()); - }); -} - -#[test] -fn test_stake_availability_for_coldkeys_empty_netuids() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let result = - SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(Vec::new())); - assert_eq!(result.len(), 1); - assert!(result.contains_key(&coldkey)); - assert!(result.get(&coldkey).unwrap().is_empty()); - }); -} - -#[test] -fn test_stake_availability_for_coldkeys_filters_empty_rows() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - let result = - SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(vec![netuid])); - - assert_eq!(result.len(), 1); - assert!(result.contains_key(&coldkey)); - assert!(result.get(&coldkey).unwrap().is_empty()); - }); -} - -#[test] -fn test_stake_availability_for_coldkeys_stake_without_lock() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - - let result = - SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(vec![netuid])); - - assert_eq!(result.len(), 1); - let availability = result.get(&coldkey).unwrap().get(&netuid).unwrap(); - assert_eq!(availability.total(), total); - assert_eq!(availability.locked(), AlphaBalance::ZERO); - assert_eq!(availability.available(), total); - }); -} - -#[test] -fn test_stake_availability_for_coldkeys_partial_lock() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - let lock_amount = total / 2.into(); - - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - lock_amount, - )); - - let result = - SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(vec![netuid])); - let availability = result.get(&coldkey).unwrap().get(&netuid).unwrap(); - - assert_eq!(availability.total(), total); - assert_eq!( - availability.locked(), - SubtensorModule::get_current_locked(&coldkey, netuid) - ); - assert_eq!(availability.available(), total - availability.locked()); - }); -} - -#[test] -fn test_stake_availability_for_coldkeys_fully_locked() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, netuid, &hotkey, total, - )); - - let result = - SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(vec![netuid])); - let availability = result.get(&coldkey).unwrap().get(&netuid).unwrap(); - - assert_eq!(availability.total(), total); - assert_eq!(availability.locked(), total); - assert_eq!(availability.available(), AlphaBalance::ZERO); - }); -} - -#[test] -fn test_stake_availability_for_coldkeys_preserves_coldkey_grouping() { - new_test_ext(1).execute_with(|| { - let coldkey_a = U256::from(1); - let hotkey_a = U256::from(2); - let coldkey_b = U256::from(3); - let hotkey_b = U256::from(4); - let netuid_a = setup_subnet_with_stake(coldkey_a, hotkey_a, 100_000_000_000); - let netuid_b = setup_subnet_with_stake(coldkey_b, hotkey_b, 100_000_000_000); - - let result = SubtensorModule::get_stake_availability_for_coldkeys( - vec![coldkey_a, coldkey_b], - Some(vec![netuid_a, netuid_b]), - ); - - assert_eq!(result.len(), 2); - assert_eq!(result.get(&coldkey_a).unwrap().len(), 1); - assert!(result.get(&coldkey_a).unwrap().contains_key(&netuid_a)); - assert_eq!(result.get(&coldkey_b).unwrap().len(), 1); - assert!(result.get(&coldkey_b).unwrap().contains_key(&netuid_b)); - }); -} - -#[test] -fn test_stake_availability_for_coldkeys_none_netuids_uses_all_subnets() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - let result = SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], None); - - assert_eq!(result.len(), 1); - assert!(result.get(&coldkey).unwrap().contains_key(&netuid)); - }); -} - -#[test] -fn test_stake_availability_for_coldkeys_one_coldkey_two_subnets() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey_a = U256::from(2); - let hotkey_b = U256::from(3); - let netuid_a = setup_subnet_with_stake(coldkey, hotkey_a, 100_000_000_000); - let netuid_b = setup_subnet_with_stake(coldkey, hotkey_b, 100_000_000_000); - let total_a = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid_a); - let total_b = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid_b); - - let result = SubtensorModule::get_stake_availability_for_coldkeys( - vec![coldkey], - Some(vec![netuid_a, netuid_b]), - ); - - assert_eq!(result.len(), 1); - let subnets = result.get(&coldkey).unwrap(); - assert_eq!(subnets.len(), 2); - assert!(subnets.contains_key(&netuid_a)); - assert!(subnets.contains_key(&netuid_b)); - - let row_a = subnets.get(&netuid_a).unwrap(); - assert_eq!(row_a.total(), total_a); - assert_eq!(row_a.locked(), AlphaBalance::ZERO); - assert_eq!(row_a.available(), total_a); - - let row_b = subnets.get(&netuid_b).unwrap(); - assert_eq!(row_b.total(), total_b); - assert_eq!(row_b.locked(), AlphaBalance::ZERO); - assert_eq!(row_b.available(), total_b); - }); -} - -#[test] -fn test_stake_availability_for_coldkeys_filters_to_requested_netuid() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey_a = U256::from(2); - let hotkey_b = U256::from(3); - let netuid_a = setup_subnet_with_stake(coldkey, hotkey_a, 100_000_000_000); - let netuid_b = setup_subnet_with_stake(coldkey, hotkey_b, 100_000_000_000); - - let result = SubtensorModule::get_stake_availability_for_coldkeys( - vec![coldkey], - Some(vec![netuid_b]), - ); - - assert_eq!(result.len(), 1); - let subnets = result.get(&coldkey).unwrap(); - assert_eq!(subnets.len(), 1); - assert!(subnets.contains_key(&netuid_b)); - assert!(!subnets.contains_key(&netuid_a)); - }); -} - -#[test] -fn test_stake_availability_for_coldkeys_dedups_netuids() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - let result = SubtensorModule::get_stake_availability_for_coldkeys( - vec![coldkey], - Some(vec![netuid, netuid]), - ); - - assert_eq!(result.len(), 1); - assert_eq!(result.get(&coldkey).unwrap().len(), 1); - assert!(result.get(&coldkey).unwrap().contains_key(&netuid)); - }); -} - -#[test] -fn test_stake_availability_for_coldkeys_skips_nonexistent_netuid() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - let nonexistent = subtensor_runtime_common::NetUid::from(99); - - let result = SubtensorModule::get_stake_availability_for_coldkeys( - vec![coldkey], - Some(vec![nonexistent]), - ); - assert_eq!(result.len(), 1); - assert!(result.get(&coldkey).unwrap().is_empty()); - - // Mix real + fake requires at least two subnets on chain so len(requested) <= subnet_count. - let subnet_owner_coldkey = U256::from(2001); - let subnet_owner_hotkey = U256::from(2002); - let _other_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - let result = SubtensorModule::get_stake_availability_for_coldkeys( - vec![coldkey], - Some(vec![netuid, nonexistent]), - ); - assert_eq!(result.len(), 1); - let subnets = result.get(&coldkey).unwrap(); - assert_eq!(subnets.len(), 1); - assert!(subnets.contains_key(&netuid)); - assert!(!subnets.contains_key(&nonexistent)); - }); -} - -#[test] -fn test_stake_availability_for_coldkeys_rejects_oversized_netuid_list() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - let subnet_count = SubtensorModule::get_all_subnet_netuids().len(); - let requested: Vec = (0..=subnet_count as u16) - .map(subtensor_runtime_common::NetUid::from) - .collect(); - - let result = - SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(requested)); - assert_eq!(result.len(), 1); - assert!(result.contains_key(&coldkey)); - assert!(result.get(&coldkey).unwrap().is_empty()); - - let result = - SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(vec![netuid])); - assert_eq!(result.get(&coldkey).unwrap().len(), 1); - assert!(result.get(&coldkey).unwrap().contains_key(&netuid)); - }); -} - -#[test] -fn test_stake_availability_for_coldkeys_uses_rolled_forward_lock() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - let lock_amount = total / 2.into(); - - DecayingLock::::remove(coldkey, netuid); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - lock_amount, - )); - let raw_lock = Lock::::get((coldkey, netuid, hotkey)).unwrap(); - - step_block(1000); - - let result = - SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(vec![netuid])); - let availability = result.get(&coldkey).unwrap().get(&netuid).unwrap(); - let rolled_locked = SubtensorModule::get_current_locked(&coldkey, netuid); - - assert!(rolled_locked < raw_lock.locked_mass); - assert_eq!(availability.locked(), rolled_locked); - assert_eq!(availability.available(), total - rolled_locked); - }); -} - -// ========================================================================= -// GROUP 3: Incremental locks (top-up) -// ========================================================================= - -#[test] -fn test_lock_stake_topup() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - let first_lock = 1000u64; - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - first_lock.into() - )); - - step_block(100); - - let second_lock = 500u64; - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - second_lock.into() - )); - - let lock = Lock::::get((coldkey, netuid, hotkey)).unwrap(); - // locked_mass should be decayed(first_lock) + second_lock - // Since tau is large (216000), decay over 100 blocks is small; locked_mass ~ 1000 + 500 - assert!(lock.locked_mass > 1490.into()); - assert!(lock.locked_mass < 1501.into()); - // conviction should have grown from the time the first lock was active - assert!(lock.conviction > U64F64::from_num(0)); - assert_eq!( - lock.last_update, - SubtensorModule::get_current_block_as_u64() - ); - - // Hotkey lock should also be created - let hotkey_lock = HotkeyLock::::get(netuid, hotkey).unwrap(); - assert!(hotkey_lock.locked_mass > 1490.into()); - assert_eq!(hotkey_lock.locked_mass, lock.locked_mass); - assert!(hotkey_lock.conviction > U64F64::from_num(0)); - }); -} - -#[test] -fn test_lock_stake_topup_multiple_times() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - let chunk = 500u64.into(); - - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, netuid, &hotkey, chunk - )); - step_block(50); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, netuid, &hotkey, chunk - )); - step_block(50); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, netuid, &hotkey, chunk - )); - - let lock = Lock::::get((coldkey, netuid, hotkey)).unwrap(); - // After three top-ups with small decay, should be close to 1500 - assert!(lock.locked_mass > 1490.into()); - assert!(lock.locked_mass <= 1500.into()); - assert!(lock.conviction > U64F64::from_num(0)); - - // Hotkey lock should also be updated - let hotkey_lock = HotkeyLock::::get(netuid, hotkey).unwrap(); - assert!(hotkey_lock.locked_mass > 1490.into()); - assert_eq!(hotkey_lock.locked_mass, lock.locked_mass); - assert!(hotkey_lock.conviction > U64F64::from_num(0)); - }); -} - -#[test] -fn test_lock_stake_topup_same_block() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - let first = 1000u64.into(); - let second = 500u64.into(); - - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, netuid, &hotkey, first - )); - // No block advancement — same block top-up - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, netuid, &hotkey, second - )); - - let lock = Lock::::get((coldkey, netuid, hotkey)).unwrap(); - // dt=0 means no decay, simple addition - assert_eq!(lock.locked_mass, first + second); - assert_eq!(lock.conviction, U64F64::from_num(0)); - - // Hotkey lock should also be updated - let hotkey_lock = HotkeyLock::::get(netuid, hotkey).unwrap(); - assert_eq!(hotkey_lock.locked_mass, first + second); - assert_eq!(hotkey_lock.conviction, U64F64::from_num(0)); - }); -} - -#[test] -fn test_locking_coldkeys_added_once_by_lock_stake() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - 100u64.into(), - )); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - 50u64.into(), - )); - - assert!(LockingColdkeys::::contains_key(( - netuid, hotkey, coldkey - ))); - assert_eq!( - LockingColdkeys::::iter_prefix((netuid, hotkey)).count(), - 1 - ); - }); -} - -#[test] -fn test_locking_coldkeys_removed_when_lock_is_fully_reduced() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - let amount = 100u64.into(); - - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, netuid, &hotkey, amount - )); - assert!(LockingColdkeys::::contains_key(( - netuid, hotkey, coldkey - ))); - - SubtensorModule::force_reduce_lock(&coldkey, netuid, amount); - - assert!(Lock::::get((coldkey, netuid, hotkey)).is_none()); - assert!(!LockingColdkeys::::contains_key(( - netuid, hotkey, coldkey - ))); - }); -} - -#[test] -fn test_lock_state_is_zero_uses_dust_threshold() { - let below_threshold = LockState { - locked_mass: AlphaBalance::from(99u64), - conviction: U64F64::from_num(99), - last_update: 0, - }; - let locked_mass_at_threshold = LockState { - locked_mass: AlphaBalance::from(100u64), - conviction: U64F64::from_num(99), - last_update: 0, - }; - let conviction_at_threshold = LockState { - locked_mass: AlphaBalance::from(99u64), - conviction: U64F64::from_num(100), - last_update: 0, - }; - - assert!(below_threshold.is_zero()); - assert!(!locked_mass_at_threshold.is_zero()); - assert!(!conviction_at_threshold.is_zero()); -} - -// ========================================================================= -// GROUP 4: Lock rejection cases -// ========================================================================= - -#[test] -fn test_lock_stake_zero_amount() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - assert_noop!( - SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, AlphaBalance::ZERO,), - Error::::AmountTooLow - ); - }); -} - -#[test] -fn test_lock_stake_exceeds_total_alpha() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - let too_much = total + 1.into(); - - assert_noop!( - SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, too_much), - Error::::InsufficientStakeForLock - ); - }); -} - -#[test] -fn test_lock_stake_wrong_hotkey() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey_a = U256::from(2); - let hotkey_b = U256::from(3); - let netuid = setup_subnet_with_stake(coldkey, hotkey_a, 100_000_000_000); - assert_ok!(SubtensorModule::create_account_if_non_existent( - &coldkey, &hotkey_b - )); - - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey_a, - 1000u64.into(), - )); - - assert_noop!( - SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey_b, 500u64.into(),), - Error::::LockHotkeyMismatch - ); - }); -} - -#[test] -fn test_lock_stake_topup_exceeds_total() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - // Lock 80% initially - let initial = total * 8.into() / 10.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, netuid, &hotkey, initial - )); - - // Try to top up the remaining 30% (exceeds total by 10%) - let topup = total * 3.into() / 10.into(); - assert_noop!( - SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, topup), - Error::::InsufficientStakeForLock - ); - }); -} - -// ========================================================================= -// GROUP 5: ConvictionModel roll-forward math -// ========================================================================= - -#[test] -fn test_exp_decay_zero_dt() { - new_test_ext(1).execute_with(|| { - let result = ConvictionModel::exp_decay(0, 216000); - assert_eq!(result, U64F64::from_num(1)); - }); -} - -#[test] -fn test_exp_decay_zero_tau() { - new_test_ext(1).execute_with(|| { - let result = ConvictionModel::exp_decay(1000, 0); - assert_eq!(result, U64F64::from_num(0)); - }); -} - -#[test] -fn test_exp_decay_one_tau() { - new_test_ext(1).execute_with(|| { - let tau = 216000u64; - let result = ConvictionModel::exp_decay(tau, tau); - // exp(-1) ~= 0.36787944 - let expected = U64F64::from_num(0.36787944f64); - let diff = if result > expected { - result - expected - } else { - expected - result - }; - assert!(diff < U64F64::from_num(0.001)); - }); -} - -#[test] -fn test_exp_decay_clamps_large_dt_to_min_ratio() { - new_test_ext(1).execute_with(|| { - let tau = 216000u64; - let clamped_result = ConvictionModel::exp_decay(40 * tau, tau); - let oversized_result = ConvictionModel::exp_decay(100 * tau, tau); - - let diff = if oversized_result > clamped_result { - oversized_result - clamped_result - } else { - clamped_result - oversized_result - }; - - assert!(diff < U64F64::from_num(0.000000001)); - assert!(oversized_result > U64F64::from_num(0)); - }); -} - -#[test] -fn test_roll_forward_individual_lock_uses_lock_owner_and_decay_mode() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - let owner_hotkey = SubnetOwnerHotkey::::get(netuid); - DecayingLock::::remove(coldkey, netuid); - - let lock = LockState { - locked_mass: 10_000u64.into(), - conviction: U64F64::from_num(0), - last_update: 0, - }; - let now = 1_000u64; - - let rolled = - roll_forward_individual_lock(&coldkey, netuid, &owner_hotkey, lock.clone(), now); - let expected = ConvictionModel::roll_forward_lock( - lock, - now, - UnlockRate::::get(), - MaturityRate::::get(), - true, - false, - ) - .0; - - assert_eq!(rolled, expected); - }); -} - -#[test] -fn test_roll_forward_hotkey_lock_uses_perpetual_general_mode() { - new_test_ext(1).execute_with(|| { - let lock = LockState { - locked_mass: 10_000u64.into(), - conviction: U64F64::from_num(0), - last_update: 0, - }; - let now = 1_000u64; - - let rolled = roll_forward_hotkey_lock(lock.clone(), now); - let expected = ConvictionModel::roll_forward_lock( - lock, - now, - UnlockRate::::get(), - MaturityRate::::get(), - false, - true, - ) - .0; - - assert_eq!(rolled, expected); - }); -} - -#[test] -fn test_roll_forward_decaying_hotkey_lock_uses_decaying_general_mode() { - new_test_ext(1).execute_with(|| { - let lock = LockState { - locked_mass: 10_000u64.into(), - conviction: U64F64::from_num(0), - last_update: 0, - }; - let now = 1_000u64; - - let rolled = roll_forward_decaying_hotkey_lock(lock.clone(), now); - let expected = ConvictionModel::roll_forward_lock( - lock, - now, - UnlockRate::::get(), - MaturityRate::::get(), - false, - false, - ) - .0; - - assert_eq!(rolled, expected); - }); -} - -#[test] -fn test_roll_forward_locked_mass_decays() { - new_test_ext(1).execute_with(|| { - let lock_amount = 10000u64; - let lock = LockState { - locked_mass: lock_amount.into(), - conviction: U64F64::from_num(0), - last_update: 0, - }; - let rolled = roll_forward_lock(lock, UnlockRate::::get(), false, false); - - assert!(rolled.locked_mass < lock_amount.into()); - assert!(rolled.locked_mass > AlphaBalance::ZERO); - }); -} - -#[test] -fn test_roll_forward_conviction_uses_unequal_rate_closed_form() { - new_test_ext(1).execute_with(|| { - let locked_mass = 10_000u64; - let dt = 10_000u64; - let unlock_rate = 200_000u64; - let maturity_rate = 240_000u64; - UnlockRate::::set(unlock_rate); - MaturityRate::::set(maturity_rate); - assert_ne!(unlock_rate, maturity_rate); - - let lock = LockState { - locked_mass: locked_mass.into(), - conviction: U64F64::from_num(0), - last_update: 0, - }; - let rolled = roll_forward_lock(lock, dt, false, false); - - let unlock_decay = ConvictionModel::exp_decay(dt, unlock_rate); - let maturity_decay = ConvictionModel::exp_decay(dt, maturity_rate); - let gamma = U64F64::from_num(unlock_rate) - .saturating_mul(maturity_decay.saturating_sub(unlock_decay)) - .safe_div(U64F64::from_num(maturity_rate.saturating_sub(unlock_rate))); - let expected = U64F64::from_num(locked_mass).saturating_mul(gamma); - - assert_abs_diff_eq!( - rolled.conviction.to_num::(), - expected.to_num::(), - epsilon = 0.0000001 - ); - }); -} - -#[test] -fn test_roll_forward_adjacent_large_rates_and_large_mass_match_f64_closed_form() { - new_test_ext(1).execute_with(|| { - let unlock_rate = 1_142_108u64; - let maturity_rate = unlock_rate + 1; - let locked_mass = 21_000_000_000_000_000u64; - let dt = unlock_rate; - UnlockRate::::put(unlock_rate); - MaturityRate::::put(maturity_rate); - - let lock = LockState { - locked_mass: locked_mass.into(), - conviction: U64F64::from_num(0), - last_update: 0, - }; - let rolled = roll_forward_lock(lock, dt, false, false); - - let decay_x = (-(dt as f64) / unlock_rate as f64).exp(); - let decay_z = (-(dt as f64) / maturity_rate as f64).exp(); - let gamma = - unlock_rate as f64 * (decay_x - decay_z) / (unlock_rate as f64 - maturity_rate as f64); - let expected_conviction = locked_mass as f64 * gamma; - let expected_locked_mass = locked_mass as f64 * decay_x; - - assert_abs_diff_eq!( - rolled.conviction.to_num::(), - expected_conviction, - epsilon = 50_000.0 - ); - assert_abs_diff_eq!( - u64::from(rolled.locked_mass) as f64, - expected_locked_mass, - epsilon = 2_000.0 - ); - }); -} - -#[test] -fn test_roll_forward_scales_linearly_with_locked_mass() { - new_test_ext(1).execute_with(|| { - let dt = 25_000u64; - let base_mass = 10_000u64; - let base = LockState { - locked_mass: base_mass.into(), - conviction: U64F64::from_num(0), - last_update: 0, - }; - let double = LockState { - locked_mass: (base_mass * 2).into(), - conviction: U64F64::from_num(0), - last_update: 0, - }; - - let rolled_base = roll_forward_lock(base, dt, false, false); - let rolled_double = roll_forward_lock(double, dt, false, false); - - assert_abs_diff_eq!( - u64::from(rolled_double.locked_mass) as f64, - (u64::from(rolled_base.locked_mass) * 2) as f64, - epsilon = 1.0 - ); - assert_abs_diff_eq!( - rolled_double.conviction.to_num::(), - rolled_base.conviction.to_num::() * 2.0, - epsilon = 0.0000001 - ); - }); -} - -#[test] -fn test_roll_forward_chunked_update_matches_single_update() { - new_test_ext(1).execute_with(|| { - let lock = LockState { - locked_mass: 1_000_000_000u64.into(), - conviction: U64F64::from_num(0), - last_update: 0, - }; - let mid = 10_000u64; - let end = 20_000u64; - - let rolled_once = roll_forward_lock(lock.clone(), end, false, false); - let rolled_twice = roll_forward_lock( - roll_forward_lock(lock, mid, false, false), - end, - false, - false, - ); - - assert_abs_diff_eq!( - u64::from(rolled_twice.locked_mass) as f64, - u64::from(rolled_once.locked_mass) as f64, - epsilon = 1.0 - ); - assert_abs_diff_eq!( - rolled_twice.conviction.to_num::(), - rolled_once.conviction.to_num::(), - epsilon = 0.1 - ); - }); -} - -#[test] -fn test_roll_forward_conviction_stays_below_original_mass_for_one_shot_lock() { - new_test_ext(1).execute_with(|| { - let locked_mass = 10_000u64; - let lock = LockState { - locked_mass: locked_mass.into(), - conviction: U64F64::from_num(0), - last_update: 0, - }; - let cap = U64F64::from_num(locked_mass); - - for dt in [ - 1_000u64, - 10_000u64, - UnlockRate::::get(), - MaturityRate::::get(), - MaturityRate::::get().saturating_mul(5), - ] { - let rolled = roll_forward_lock(lock.clone(), dt, false, false); - assert!(rolled.conviction <= cap); - } - }); -} - -#[test] -fn test_roll_forward_decaying_conviction_peak_is_below_original_lock() { - new_test_ext(1).execute_with(|| { - UnlockRate::::set(200_000u64); - MaturityRate::::set(240_000u64); - - let locked_mass = 10_000u64; - let unlock_rate = UnlockRate::::get() as f64; - let maturity_rate = MaturityRate::::get() as f64; - assert_ne!(unlock_rate, maturity_rate); - - let peak_block = ((unlock_rate * maturity_rate) / (unlock_rate - maturity_rate) - * (unlock_rate / maturity_rate).ln()) - .round() as u64; - let lock = LockState { - locked_mass: locked_mass.into(), - conviction: U64F64::from_num(0), - last_update: 0, - }; - - let rolled = roll_forward_lock(lock, peak_block, false, false); - - assert!(rolled.conviction < U64F64::from_num(locked_mass)); - }); -} - -#[test] -fn test_roll_forward_perpetual_mass_does_not_decay_and_conviction_matures() { - new_test_ext(1).execute_with(|| { - let locked_mass = 10_000u64; - let lock = LockState { - locked_mass: locked_mass.into(), - conviction: U64F64::from_num(0), - last_update: 0, - }; - - let rolled = roll_forward_lock(lock, MaturityRate::::get(), false, true); - - assert_eq!(rolled.locked_mass, locked_mass.into()); - assert!(rolled.conviction > U64F64::from_num(0)); - assert!(rolled.conviction < U64F64::from_num(locked_mass)); - }); -} - -#[test] -fn test_roll_forward_perpetual_conviction_never_exceeds_lock() { - new_test_ext(1).execute_with(|| { - let locked_mass = 10_000u64; - let lock = LockState { - locked_mass: locked_mass.into(), - conviction: U64F64::from_num(0), - last_update: 0, - }; - - for dt in [ - 1u64, - 1_000u64, - MaturityRate::::get(), - MaturityRate::::get().saturating_mul(10), - MaturityRate::::get().saturating_mul(1_000), - ] { - let rolled = roll_forward_lock(lock.clone(), dt, false, true); - assert_eq!(rolled.locked_mass, locked_mass.into()); - assert!(rolled.conviction <= U64F64::from_num(locked_mass)); - } - }); -} - -#[test] -fn test_roll_forward_conviction_converges_to_zero() { - new_test_ext(1).execute_with(|| { - let lock_amount = 10000u64; - let lock = LockState { - locked_mass: lock_amount.into(), - conviction: U64F64::from_num(0), - last_update: 0, - }; - - let c0 = lock.conviction; - assert_eq!(c0, U64F64::from_num(0)); - - let rolled = roll_forward_lock(lock.clone(), 100, false, false); - let c1 = rolled.conviction; - assert!(c1 > U64F64::from_num(0)); - - let rolled = roll_forward_lock(lock.clone(), 1_100, false, false); - let c2 = rolled.conviction; - assert!(c2 > c1); - - let tau = MaturityRate::::get(); - let c_late = roll_forward_lock(lock, tau * 1000, false, false).conviction; - assert_abs_diff_eq!(c_late.to_num::(), 0., epsilon = 0.0000001); - }); -} - -#[test] -fn test_roll_forward_normalizes_dust_to_zero() { - new_test_ext(1).execute_with(|| { - let lock = LockState { - locked_mass: 99u64.into(), - conviction: U64F64::from_num(99), - last_update: 100, - }; - - let rolled = roll_forward_lock(lock, 100, false, false); - - assert_eq!(rolled.locked_mass, AlphaBalance::ZERO); - assert_eq!(rolled.conviction, U64F64::from_num(0)); - assert_eq!(rolled.last_update, 100); - }); -} - -#[test] -fn test_roll_forward_no_change_when_now_equals_last_update() { - new_test_ext(1).execute_with(|| { - let lock = LockState { - locked_mass: 5000.into(), - conviction: U64F64::from_num(1234), - last_update: 100, - }; - let rolled = roll_forward_lock(lock.clone(), 100, false, false); - assert_eq!(rolled.locked_mass, lock.locked_mass); - assert_eq!(rolled.conviction, lock.conviction); - assert_eq!(rolled.last_update, 100); - }); -} - -// ========================================================================= -// GROUP 6: Unstake invariant enforcement -// ========================================================================= - -#[test] -fn test_unstake_allowed_when_no_lock() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - let alpha = get_alpha(&hotkey, &coldkey, netuid); - assert!(alpha > AlphaBalance::ZERO); - - assert_ok!(SubtensorModule::do_remove_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - alpha, - )); - }); -} - -#[test] -fn test_unstake_allowed_up_to_available() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - let lock_amount = total / 2.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - lock_amount - )); - - // Unstake the unlocked half - let alpha = get_alpha(&hotkey, &coldkey, netuid); - let available_alpha: u64 = (alpha.to_u64()) / 2; - // Need to step a block to pass rate limiter - step_block(1); - assert_ok!(SubtensorModule::do_remove_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - available_alpha.into(), - )); - }); -} - -#[test] -fn test_unstake_rolls_forward_existing_lock() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - let lock_amount = AlphaBalance::from(1_000_000_000u64); - - DecayingLock::::remove(coldkey, netuid); - let lock_block = SubtensorModule::get_current_block_as_u64(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - lock_amount, - )); - - step_block(100); - let now = SubtensorModule::get_current_block_as_u64(); - let expected = roll_forward_decaying_hotkey_lock( - LockState { - locked_mass: lock_amount, - conviction: U64F64::from_num(0), - last_update: lock_block, - }, - now, - ); - - assert_ok!(SubtensorModule::do_remove_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - lock_amount, - )); - - assert_eq!( - Lock::::get((coldkey, netuid, hotkey)).expect("lock should remain"), - expected - ); - let aggregate = - DecayingHotkeyLock::::get(netuid, hotkey).expect("aggregate should remain"); - assert_eq!(aggregate.locked_mass, expected.locked_mass); - assert_eq!(aggregate.last_update, now); - }); -} - -#[test] -fn test_unstake_roll_forward_collects_decaying_lock_dust_from_hotkey_aggregate() { - new_test_ext(1).execute_with(|| { - const ONE_ALPHA: u64 = 1_000_000_000; - const DUST_ALPHA: u64 = 100; - const STAKE_TAO_RAO: u64 = 1_000 * 1_000_000_000; - - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let coldkey_1 = U256::from(2001); - let coldkey_2 = U256::from(2002); - let hotkey_1 = U256::from(3001); - let hotkey_2 = U256::from(3002); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - setup_reserves( - netuid, - (STAKE_TAO_RAO * 1_000).into(), - (STAKE_TAO_RAO * 10_000).into(), - ); - assert_ok!(SubtensorModule::create_account_if_non_existent( - &coldkey_1, &hotkey_1 - )); - assert_ok!(SubtensorModule::create_account_if_non_existent( - &coldkey_1, &hotkey_2 - )); - - for coldkey in [coldkey_1, coldkey_2] { - add_balance_to_coldkey_account(&coldkey, STAKE_TAO_RAO.into()); - SubtensorModule::stake_into_subnet( - &hotkey_1, - &coldkey, - netuid, - STAKE_TAO_RAO.into(), - ::SwapInterface::max_price(), - false, - ) - .unwrap(); - } - - let lock_block = SubtensorModule::get_current_block_as_u64(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey_1, - netuid, - &hotkey_2, - ONE_ALPHA.into(), - )); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey_2, - netuid, - &hotkey_2, - DUST_ALPHA.into(), - )); - - assert_eq!( - DecayingHotkeyLock::::get(netuid, hotkey_2) - .expect("decaying aggregate should exist") - .locked_mass, - AlphaBalance::from(ONE_ALPHA + DUST_ALPHA) - ); - - step_block(100); - let now = SubtensorModule::get_current_block_as_u64(); - let rolled_large_lock = roll_forward_decaying_hotkey_lock( - LockState { - locked_mass: ONE_ALPHA.into(), - conviction: U64F64::from_num(0), - last_update: lock_block, - }, - now, - ); - - assert_ok!(SubtensorModule::do_remove_stake( - RuntimeOrigin::signed(coldkey_1), - hotkey_1, - netuid, - ONE_ALPHA.into(), - )); - assert_eq!( - Lock::::get((coldkey_1, netuid, hotkey_2)).expect("coldkey1 lock should remain"), - rolled_large_lock - ); - assert_eq!( - DecayingHotkeyLock::::get(netuid, hotkey_2) - .expect("decaying aggregate should remain") - .locked_mass, - rolled_large_lock - .locked_mass - .saturating_add(AlphaBalance::from(DUST_ALPHA)) - ); - - assert_ok!(SubtensorModule::do_remove_stake( - RuntimeOrigin::signed(coldkey_2), - hotkey_1, - netuid, - ONE_ALPHA.into(), - )); - assert_eq!( - DecayingHotkeyLock::::get(netuid, hotkey_2) - .expect("decaying aggregate should remain") - .locked_mass, - rolled_large_lock.locked_mass - ); - }); -} - -#[test] -fn test_unstake_blocked_by_lock() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - // Lock the entire amount - assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, total)); - - step_block(1); - - let alpha = get_alpha(&hotkey, &coldkey, netuid); - assert_noop!( - SubtensorModule::do_remove_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - alpha, - ), - Error::::StakeUnavailable - ); - }); -} - -// ========================================================================= -// GROUP 7: Move/transfer invariant enforcement -// ========================================================================= - -#[test] -fn test_move_stake_same_coldkey_same_subnet_allowed() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey_a = U256::from(2); - let hotkey_b = U256::from(3); - let netuid = setup_subnet_with_stake(coldkey, hotkey_a, 100_000_000_000); - - assert_ok!(SubtensorModule::create_account_if_non_existent( - &coldkey, &hotkey_b - )); - - let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - // Lock the full amount to hotkey_a - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, netuid, &hotkey_a, total - )); - - // Move from hotkey_a to hotkey_b on same subnet — total coldkey alpha unchanged - let alpha = get_alpha(&hotkey_a, &coldkey, netuid); - let move_amount = alpha / 2.into(); - assert_ok!(SubtensorModule::do_move_stake( - RuntimeOrigin::signed(coldkey), - hotkey_a, - hotkey_b, - netuid, - netuid, - move_amount, - )); - }); -} - -#[test] -fn test_do_transfer_stake_same_subnet_transfers_lock_to_destination_coldkey() { - new_test_ext(1).execute_with(|| { - let coldkey_sender = U256::from(1); - let coldkey_receiver = U256::from(5); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey_sender, hotkey, 100_000_000_000); - DecayingLock::::insert(coldkey_receiver, netuid, false); - assert_ok!(SubtensorModule::set_reject_locked_alpha( - RuntimeOrigin::signed(coldkey_receiver), - false, - )); - - let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_sender, netuid); - let lock_half = total / 2.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey_sender, - netuid, - &hotkey, - lock_half, - )); - - let sender_lock_before = - Lock::::get((coldkey_sender, netuid, hotkey)).expect("sender lock should exist"); - let hotkey_lock_before = - HotkeyLock::::get(netuid, hotkey).expect("hotkey lock should exist"); - - step_block(1); - - let transfer_amount = total; - assert_ok!(SubtensorModule::do_transfer_stake( - RuntimeOrigin::signed(coldkey_sender), - coldkey_receiver, - hotkey, - netuid, - netuid, - transfer_amount, - )); - - let expected_sender_lock = roll_forward_lock( - sender_lock_before, - SubtensorModule::get_current_block_as_u64(), - false, - true, - ); - - assert!(Lock::::get((coldkey_sender, netuid, hotkey)).is_none()); - - let receiver_lock = Lock::::get((coldkey_receiver, netuid, hotkey)) - .expect("receiver lock should exist after transfer"); - assert_eq!(receiver_lock.locked_mass, expected_sender_lock.locked_mass); - assert!(receiver_lock.conviction > U64F64::from_num(0)); - assert!(receiver_lock.conviction <= expected_sender_lock.conviction); - - let hotkey_lock_after = - HotkeyLock::::get(netuid, hotkey).expect("hotkey lock should remain"); - let expected_hotkey_lock = roll_forward_lock( - hotkey_lock_before, - SubtensorModule::get_current_block_as_u64(), - false, - true, - ); - assert_eq!( - hotkey_lock_after.locked_mass, - expected_hotkey_lock.locked_mass - ); - }); -} - -// Regression test: a same-subnet transfer that changes the hotkey must move the -// individual lock and the aggregate lock to the destination hotkey. Before the -// fix the recipient's lock (and aggregate conviction) stayed on the origin -// hotkey while the stake landed on the destination hotkey. -#[test] -fn test_do_transfer_stake_and_hotkey_same_subnet_moves_lock_to_destination_hotkey() { - new_test_ext(1).execute_with(|| { - let coldkey_sender = U256::from(1); - let coldkey_receiver = U256::from(5); - let origin_hotkey = U256::from(2); - let destination_hotkey = U256::from(6); - let netuid = setup_subnet_with_stake(coldkey_sender, origin_hotkey, 100_000_000_000); - - // The destination hotkey is owned by the receiving coldkey, so origin and - // destination hotkeys have different owners. - assert_ok!(SubtensorModule::create_account_if_non_existent( - &coldkey_receiver, - &destination_hotkey - )); - DecayingLock::::insert(coldkey_receiver, netuid, false); - assert_ok!(SubtensorModule::set_reject_locked_alpha( - RuntimeOrigin::signed(coldkey_receiver), - false, - )); - - let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_sender, netuid); - let lock_half = total / 2.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey_sender, - netuid, - &origin_hotkey, - lock_half, - )); - - let sender_lock_before = Lock::::get((coldkey_sender, netuid, origin_hotkey)) - .expect("sender lock should exist"); - - step_block(1); - - // Transfer the whole position (unlocked and locked halves) to the - // destination coldkey and hotkey. - assert_ok!(SubtensorModule::do_transfer_stake_and_hotkey( - RuntimeOrigin::signed(coldkey_sender), - coldkey_receiver, - origin_hotkey, - destination_hotkey, - netuid, - netuid, - total, - )); - - let expected_sender_lock = roll_forward_lock( - sender_lock_before, - SubtensorModule::get_current_block_as_u64(), - false, - true, - ); - - // The sender's lock is fully transferred away. - assert!(Lock::::get((coldkey_sender, netuid, origin_hotkey)).is_none()); - - // The receiver's lock follows the stake to the destination hotkey and - // does not stay stranded on the origin hotkey. - assert!(Lock::::get((coldkey_receiver, netuid, origin_hotkey)).is_none()); - let receiver_lock = Lock::::get((coldkey_receiver, netuid, destination_hotkey)) - .expect("receiver lock should exist on the destination hotkey"); - assert_eq!(receiver_lock.locked_mass, expected_sender_lock.locked_mass); - - // The hotkeys are owned by different coldkeys, so the transferred - // conviction is forfeited, mirroring do_move_lock. - assert_eq!(receiver_lock.conviction, U64F64::from_num(0)); - - // The aggregate lock moves off the origin hotkey and onto the destination hotkey. - assert!( - HotkeyLock::::get(netuid, origin_hotkey) - .map(|lock| lock.locked_mass) - .unwrap_or(AlphaBalance::ZERO) - .is_zero() - ); - let destination_hotkey_lock = HotkeyLock::::get(netuid, destination_hotkey) - .expect("destination hotkey aggregate lock should exist"); - assert_eq!( - destination_hotkey_lock.locked_mass, - expected_sender_lock.locked_mass - ); - }); -} - -// When origin and destination hotkeys share an owning coldkey, the transferred -// conviction follows the lock to the destination hotkey instead of being forfeited. -#[test] -fn test_do_transfer_stake_and_hotkey_same_owner_preserves_conviction() { - new_test_ext(1).execute_with(|| { - let coldkey_sender = U256::from(1); - let coldkey_receiver = U256::from(5); - let origin_hotkey = U256::from(2); - let destination_hotkey = U256::from(6); - let netuid = setup_subnet_with_stake(coldkey_sender, origin_hotkey, 100_000_000_000); - - // Both hotkeys are owned by the sending coldkey. - assert_ok!(SubtensorModule::create_account_if_non_existent( - &coldkey_sender, - &destination_hotkey - )); - DecayingLock::::insert(coldkey_receiver, netuid, false); - assert_ok!(SubtensorModule::set_reject_locked_alpha( - RuntimeOrigin::signed(coldkey_receiver), - false, - )); - - let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_sender, netuid); - let lock_half = total / 2.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey_sender, - netuid, - &origin_hotkey, - lock_half, - )); - - let sender_lock_before = Lock::::get((coldkey_sender, netuid, origin_hotkey)) - .expect("sender lock should exist"); - - step_block(1); - - assert_ok!(SubtensorModule::do_transfer_stake_and_hotkey( - RuntimeOrigin::signed(coldkey_sender), - coldkey_receiver, - origin_hotkey, - destination_hotkey, - netuid, - netuid, - total, - )); - - let expected_sender_lock = roll_forward_lock( - sender_lock_before, - SubtensorModule::get_current_block_as_u64(), - false, - true, - ); - - let receiver_lock = Lock::::get((coldkey_receiver, netuid, destination_hotkey)) - .expect("receiver lock should exist on the destination hotkey"); - assert_eq!(receiver_lock.locked_mass, expected_sender_lock.locked_mass); - - // Same-owner hotkey change: the conviction moved with the lock. - assert!(receiver_lock.conviction > U64F64::from_num(0)); - assert!(receiver_lock.conviction <= expected_sender_lock.conviction); - }); -} - -// The LockHotkeyMismatch guard is checked against the hotkey the stake lands on: -// a recipient with an existing lock can only receive locked alpha onto that same -// hotkey, and transfers targeting any other hotkey are rejected. -#[test] -fn test_do_transfer_stake_and_hotkey_locked_requires_destination_match_receiver_lock() { - new_test_ext(1).execute_with(|| { - let coldkey_sender = U256::from(1); - let coldkey_receiver = U256::from(5); - let origin_hotkey = U256::from(2); - let receiver_lock_hotkey = U256::from(6); - let other_hotkey = U256::from(7); - let netuid = setup_subnet_with_stake(coldkey_sender, origin_hotkey, 100_000_000_000); - - assert_ok!(SubtensorModule::create_account_if_non_existent( - &coldkey_receiver, - &receiver_lock_hotkey - )); - assert_ok!(SubtensorModule::create_account_if_non_existent( - &coldkey_receiver, - &other_hotkey - )); - DecayingLock::::insert(coldkey_receiver, netuid, false); - assert_ok!(SubtensorModule::set_reject_locked_alpha( - RuntimeOrigin::signed(coldkey_receiver), - false, - )); - - // The receiver already has an active lock on receiver_lock_hotkey. - let receiver_locked = AlphaBalance::from(1_000_000u64); - SubtensorModule::insert_lock_state( - &coldkey_receiver, - netuid, - &receiver_lock_hotkey, - LockState { - locked_mass: receiver_locked, - conviction: U64F64::from_num(0), - last_update: SubtensorModule::get_current_block_as_u64(), - }, - ); - - let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_sender, netuid); - let lock_half = total / 2.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey_sender, - netuid, - &origin_hotkey, - lock_half, - )); - let sender_lock_before = Lock::::get((coldkey_sender, netuid, origin_hotkey)) - .expect("sender lock should exist"); - - step_block(1); - - // Locked alpha targeting a hotkey other than the receiver's lock hotkey fails. - assert_noop!( - SubtensorModule::do_transfer_stake_and_hotkey( - RuntimeOrigin::signed(coldkey_sender), - coldkey_receiver, - origin_hotkey, - other_hotkey, - netuid, - netuid, - total, - ), - Error::::LockHotkeyMismatch - ); - - // Targeting the receiver's lock hotkey succeeds even though it differs - // from the origin hotkey (the pre-fix check compared against the origin - // hotkey and would have rejected this). - assert_ok!(SubtensorModule::do_transfer_stake_and_hotkey( - RuntimeOrigin::signed(coldkey_sender), - coldkey_receiver, - origin_hotkey, - receiver_lock_hotkey, - netuid, - netuid, - total, - )); - - let expected_sender_lock = roll_forward_lock( - sender_lock_before, - SubtensorModule::get_current_block_as_u64(), - false, - true, - ); - let receiver_lock = Lock::::get((coldkey_receiver, netuid, receiver_lock_hotkey)) - .expect("receiver lock should exist on its lock hotkey"); - assert_eq!( - receiver_lock.locked_mass, - receiver_locked.saturating_add(expected_sender_lock.locked_mass) - ); - }); -} - -#[test] -fn test_move_stake_cross_subnet_blocked_by_lock() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid_a = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - let subnet_owner2_ck = U256::from(2001); - let subnet_owner2_hk = U256::from(2002); - let netuid_b = add_dynamic_network(&subnet_owner2_hk, &subnet_owner2_ck); - setup_reserves( - netuid_b, - (100_000_000_000u64 * 1_000_000).into(), - (100_000_000_000u64 * 10_000_000).into(), - ); - - let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid_a); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, netuid_a, &hotkey, total - )); - - step_block(1); - - let alpha = get_alpha(&hotkey, &coldkey, netuid_a); - assert_noop!( - SubtensorModule::do_move_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - hotkey, - netuid_a, - netuid_b, - alpha, - ), - Error::::StakeUnavailable - ); - }); -} - -#[test] -fn test_do_transfer_stake_rejects_locked_alpha_to_flagged_destination() { - new_test_ext(1).execute_with(|| { - let coldkey_sender = U256::from(1); - let coldkey_receiver = U256::from(5); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey_sender, hotkey, 100_000_000_000); - - let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_sender, netuid); - let lock_half = total / 2.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey_sender, - netuid, - &hotkey, - lock_half, - )); - assert_ok!(SubtensorModule::set_reject_locked_alpha( - RuntimeOrigin::signed(coldkey_receiver), - true, - )); - - let sender_lock_before = - Lock::::get((coldkey_sender, netuid, hotkey)).expect("sender lock should exist"); - let sender_alpha_before = - SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_sender, netuid); - let receiver_alpha_before = - SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_receiver, netuid); - - assert_noop!( - SubtensorModule::do_transfer_stake( - RuntimeOrigin::signed(coldkey_sender), - coldkey_receiver, - hotkey, - netuid, - netuid, - total, - ), - Error::::AccountRejectsLockedAlpha - ); - - assert_eq!( - Lock::::get((coldkey_sender, netuid, hotkey)), - Some(sender_lock_before) - ); - assert!(Lock::::get((coldkey_receiver, netuid, hotkey)).is_none()); - assert_eq!( - SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_sender, netuid), - sender_alpha_before - ); - assert_eq!( - SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_receiver, netuid), - receiver_alpha_before - ); - }); -} - -#[test] -fn test_do_transfer_stake_allows_unlocked_alpha_to_flagged_destination() { - new_test_ext(1).execute_with(|| { - let coldkey_sender = U256::from(1); - let coldkey_receiver = U256::from(5); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey_sender, hotkey, 100_000_000_000); - - let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_sender, netuid); - let lock_half = total / 2.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey_sender, - netuid, - &hotkey, - lock_half, - )); - assert_ok!(SubtensorModule::set_reject_locked_alpha( - RuntimeOrigin::signed(coldkey_receiver), - true, - )); - - let unlocked_transfer = lock_half / 2.into(); - assert_ok!(SubtensorModule::do_transfer_stake( - RuntimeOrigin::signed(coldkey_sender), - coldkey_receiver, - hotkey, - netuid, - netuid, - unlocked_transfer, - )); - - assert!(Lock::::get((coldkey_receiver, netuid, hotkey)).is_none()); - assert_eq!( - SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_receiver, netuid), - unlocked_transfer - ); - }); -} - -#[test] -fn test_transfer_stake_cross_coldkey_allowed_partial() { - new_test_ext(1).execute_with(|| { - let coldkey_sender = U256::from(1); - let coldkey_receiver = U256::from(5); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey_sender, hotkey, 100_000_000_000); - - let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_sender, netuid); - let lock_half = total / 2.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey_sender, - netuid, - &hotkey, - lock_half, - )); - - let sender_lock_before = - Lock::::get((coldkey_sender, netuid, hotkey)).expect("sender lock should exist"); - - step_block(1); - - // Transfer the unlocked portion - let alpha = get_alpha(&hotkey, &coldkey_sender, netuid); - let transfer_amount = alpha / 4.into(); // well within the unlocked half - assert_ok!(SubtensorModule::do_transfer_stake( - RuntimeOrigin::signed(coldkey_sender), - coldkey_receiver, - hotkey, - netuid, - netuid, - transfer_amount, - )); - - let sender_lock_after = - Lock::::get((coldkey_sender, netuid, hotkey)).expect("sender lock should remain"); - assert_eq!( - sender_lock_after.locked_mass, - roll_forward_lock(sender_lock_before, 2, false, true).locked_mass - ); - assert!(Lock::::get((coldkey_receiver, netuid, hotkey)).is_none()); - }); -} - -// ========================================================================= -// GROUP 8: Multi-subnet locks -// ========================================================================= - -#[test] -fn test_lock_on_multiple_subnets() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey_a = U256::from(2); - let hotkey_b = U256::from(3); - - let netuid_a = setup_subnet_with_stake(coldkey, hotkey_a, 100_000_000_000); - - let subnet_owner2_ck = U256::from(2001); - let subnet_owner2_hk = U256::from(2002); - let netuid_b = add_dynamic_network(&subnet_owner2_hk, &subnet_owner2_ck); - setup_reserves( - netuid_b, - (100_000_000_000u64 * 1_000_000).into(), - (100_000_000_000u64 * 10_000_000).into(), - ); - assert_ok!(SubtensorModule::create_account_if_non_existent( - &coldkey, &hotkey_b - )); - add_balance_to_coldkey_account(&coldkey, 100_000_000_000u64.into()); - SubtensorModule::stake_into_subnet( - &hotkey_b, - &coldkey, - netuid_b, - 100_000_000_000u64.into(), - ::SwapInterface::max_price(), - false, - ) - .unwrap(); - DecayingLock::::insert(coldkey, netuid_b, false); - - // Lock on subnet A to hotkey_a - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid_a, - &hotkey_a, - 1000u64.into(), - )); - - // Lock on subnet B to hotkey_b (different hotkey is fine — different subnet) - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid_b, - &hotkey_b, - 2000u64.into(), - )); - - let lock_a = Lock::::get((coldkey, netuid_a, hotkey_a)).unwrap(); - let lock_b = Lock::::get((coldkey, netuid_b, hotkey_b)).unwrap(); - assert_eq!(lock_a.locked_mass, 1000u64.into()); - assert_eq!(lock_b.locked_mass, 2000u64.into()); - - // Hotkey locks should also be separate - let hotkey_lock_a = HotkeyLock::::get(netuid_a, hotkey_a).unwrap(); - let hotkey_lock_b = HotkeyLock::::get(netuid_b, hotkey_b).unwrap(); - assert_eq!(hotkey_lock_a.locked_mass, 1000u64.into()); - assert_eq!(hotkey_lock_b.locked_mass, 2000u64.into()); - }); -} - -#[test] -fn test_unstake_one_subnet_does_not_affect_other() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid_a = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - // Lock on subnet A - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid_a, - &hotkey, - 5000u64.into(), - )); - - // Subnet B — no lock, just stake - let subnet_owner2_ck = U256::from(2001); - let subnet_owner2_hk = U256::from(2002); - let netuid_b = add_dynamic_network(&subnet_owner2_hk, &subnet_owner2_ck); - setup_reserves( - netuid_b, - (100_000_000_000u64 * 1_000_000).into(), - (100_000_000_000u64 * 10_000_000).into(), - ); - assert_ok!(SubtensorModule::create_account_if_non_existent( - &coldkey, &hotkey - )); - add_balance_to_coldkey_account(&coldkey, 100_000_000_000u64.into()); - SubtensorModule::stake_into_subnet( - &hotkey, - &coldkey, - netuid_b, - 100_000_000_000u64.into(), - ::SwapInterface::max_price(), - false, - ) - .unwrap(); - - step_block(1); - - // Unstake from subnet B — should succeed (no lock there) - let alpha_b = get_alpha(&hotkey, &coldkey, netuid_b); - assert_ok!(SubtensorModule::do_remove_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid_b, - alpha_b, - )); - - // Lock on subnet A unaffected - let lock_a = Lock::::get((coldkey, netuid_a, hotkey)).unwrap(); - assert_eq!(lock_a.locked_mass, 5000u64.into()); - - // Hotkey lock on subnet A also unaffected - let hotkey_lock_a = HotkeyLock::::get(netuid_a, hotkey).unwrap(); - assert_eq!(hotkey_lock_a.locked_mass, 5000u64.into()); - }); -} - -// ========================================================================= -// GROUP 9: Hotkey conviction and subnet king -// ========================================================================= - -#[test] -fn test_hotkey_conviction_single_locker() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - 5000u64.into(), - )); - - // Initially conviction is 0 (just created) - let c = SubtensorModule::hotkey_conviction(&hotkey, netuid); - assert_eq!(c, U64F64::from_num(0)); - - // After time, conviction grows - step_block(1000); - let c = SubtensorModule::hotkey_conviction(&hotkey, netuid); - assert!(c > U64F64::from_num(0)); - }); -} - -#[test] -fn test_hotkey_conviction_multiple_lockers() { - new_test_ext(1).execute_with(|| { - let coldkey1 = U256::from(1); - let coldkey2 = U256::from(5); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey1, hotkey, 100_000_000_000); - - // Also give coldkey2 stake on same hotkey - add_balance_to_coldkey_account(&coldkey2, 100_000_000_000u64.into()); - assert_ok!(SubtensorModule::create_account_if_non_existent( - &coldkey2, &hotkey - )); - SubtensorModule::stake_into_subnet( - &hotkey, - &coldkey2, - netuid, - 50_000_000_000u64.into(), - ::SwapInterface::max_price(), - false, - ) - .unwrap(); - - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey1, - netuid, - &hotkey, - 3000u64.into(), - )); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey2, - netuid, - &hotkey, - 2000u64.into(), - )); - - step_block(500); - - let total_conviction = SubtensorModule::hotkey_conviction(&hotkey, netuid); - let c1 = SubtensorModule::get_conviction(&coldkey1, netuid); - let c2 = SubtensorModule::get_conviction(&coldkey2, netuid); - - // Total conviction should be approximately sum of individual convictions - let diff = if total_conviction > (c1 + c2) { - total_conviction - (c1 + c2) - } else { - (c1 + c2) - total_conviction - }; - assert!(diff < U64F64::from_num(1)); - }); -} - -#[test] -fn test_mixed_perpetual_owner_and_decaying_non_owner_locks_roll_forward() { - new_test_ext(1).execute_with(|| { - let owner_coldkey = U256::from(1001); - let owner_hotkey = U256::from(1002); - let staker_coldkey = U256::from(1); - let staker_hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(staker_coldkey, staker_hotkey, 100_000_000_000); - - add_balance_to_coldkey_account(&owner_coldkey, 100_000_000_000u64.into()); - assert_ok!(SubtensorModule::create_account_if_non_existent( - &owner_coldkey, - &owner_hotkey - )); - SubtensorModule::stake_into_subnet( - &owner_hotkey, - &owner_coldkey, - netuid, - 100_000_000_000u64.into(), - ::SwapInterface::max_price(), - false, - ) - .unwrap(); - - let owner_lock_amount = AlphaBalance::from(10_000u64); - let staker_lock_amount = AlphaBalance::from(20_000u64); - assert_ok!(SubtensorModule::do_lock_stake( - &owner_coldkey, - netuid, - &owner_hotkey, - owner_lock_amount, - )); - assert_ok!(SubtensorModule::do_lock_stake( - &staker_coldkey, - netuid, - &staker_hotkey, - staker_lock_amount, - )); - assert_ok!(SubtensorModule::do_set_perpetual_lock( - &owner_coldkey, - netuid, - true, - )); - - System::set_block_number(System::block_number() + UnlockRate::::get()); - - let owner_lock = roll_forward_lock( - OwnerLock::::get(netuid).unwrap(), - SubtensorModule::get_current_block_as_u64(), - true, - true, - ); - let staker_lock = roll_forward_lock( - HotkeyLock::::get(netuid, staker_hotkey).unwrap(), - SubtensorModule::get_current_block_as_u64(), - false, - false, - ); - - assert_eq!(owner_lock.locked_mass, owner_lock_amount); - assert_eq!( - owner_lock.conviction, - U64F64::from_num(u64::from(owner_lock_amount)) - ); - assert!(staker_lock.locked_mass < staker_lock_amount); - assert!(staker_lock.conviction > U64F64::from_num(0)); - }); -} - -#[test] -fn test_total_conviction_equals_sum_of_participating_aggregate_convictions() { - new_test_ext(1).execute_with(|| { - let owner_coldkey = U256::from(1001); - let owner_hotkey = U256::from(1002); - let staker_coldkey = U256::from(1); - let staker_hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(staker_coldkey, staker_hotkey, 100_000_000_000); - - add_balance_to_coldkey_account(&owner_coldkey, 100_000_000_000u64.into()); - assert_ok!(SubtensorModule::create_account_if_non_existent( - &owner_coldkey, - &owner_hotkey - )); - SubtensorModule::stake_into_subnet( - &owner_hotkey, - &owner_coldkey, - netuid, - 100_000_000_000u64.into(), - ::SwapInterface::max_price(), - false, - ) - .unwrap(); - - assert_ok!(SubtensorModule::do_lock_stake( - &owner_coldkey, - netuid, - &owner_hotkey, - 10_000u64.into(), - )); - assert_ok!(SubtensorModule::do_lock_stake( - &staker_coldkey, - netuid, - &staker_hotkey, - 20_000u64.into(), - )); - assert_ok!(SubtensorModule::do_set_perpetual_lock( - &owner_coldkey, - netuid, - true, - )); - - step_block(1_000); - - let owner_conviction = SubtensorModule::hotkey_conviction(&owner_hotkey, netuid); - let staker_conviction = SubtensorModule::hotkey_conviction(&staker_hotkey, netuid); - let expected = owner_conviction.saturating_add(staker_conviction); - let total = SubtensorModule::get_total_conviction(netuid); - let diff = if total > expected { - total - expected - } else { - expected - total - }; - - assert!(diff < U64F64::from_num(1)); - }); -} - -#[test] -fn test_total_conviction_equals_sum_of_individual_lock_convictions_for_many_lockers() { - new_test_ext(1).execute_with(|| { - let first_coldkey = U256::from(1); - let first_hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(first_coldkey, first_hotkey, 100_000_000_000); - - let mut lockers = vec![(first_coldkey, first_hotkey)]; - for i in 1..10u64 { - let coldkey = U256::from(10 + i); - let hotkey = U256::from(100 + (i % 3)); - add_balance_to_coldkey_account(&coldkey, 100_000_000_000u64.into()); - assert_ok!(SubtensorModule::create_account_if_non_existent( - &coldkey, &hotkey - )); - SubtensorModule::stake_into_subnet( - &hotkey, - &coldkey, - netuid, - 50_000_000_000u64.into(), - ::SwapInterface::max_price(), - false, - ) - .unwrap(); - lockers.push((coldkey, hotkey)); - } - - for (index, (coldkey, hotkey)) in lockers.iter().enumerate() { - assert_ok!(SubtensorModule::do_lock_stake( - coldkey, - netuid, - hotkey, - AlphaBalance::from(1_000u64 + index as u64), - )); - } - - step_block(1_000); - - let now = SubtensorModule::get_current_block_as_u64(); - let individual_sum = Lock::::iter() - .filter(|((_coldkey, lock_netuid, _hotkey), _lock)| *lock_netuid == netuid) - .map(|((coldkey, _netuid, hotkey), lock)| { - roll_forward_individual_lock(&coldkey, netuid, &hotkey, lock, now).conviction - }) - .fold(U64F64::from_num(0), |acc, conviction| { - acc.saturating_add(conviction) - }); - let total = SubtensorModule::get_total_conviction(netuid); - let diff = if total > individual_sum { - total - individual_sum - } else { - individual_sum - total - }; - - assert!(diff < U64F64::from_num(1)); - }); -} - -#[test] -fn test_subnet_king_single_hotkey() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - 5000u64.into(), - )); - - step_block(100); - - let king = SubtensorModule::subnet_king(netuid); - assert_eq!(king, Some(hotkey)); - }); -} - -#[test] -fn test_subnet_king_highest_conviction_wins() { - new_test_ext(1).execute_with(|| { - let coldkey1 = U256::from(1); - let coldkey2 = U256::from(5); - let hotkey_a = U256::from(2); - let hotkey_b = U256::from(3); - - let netuid = setup_subnet_with_stake(coldkey1, hotkey_a, 100_000_000_000); - - add_balance_to_coldkey_account(&coldkey2, 100_000_000_000u64.into()); - assert_ok!(SubtensorModule::create_account_if_non_existent( - &coldkey2, &hotkey_b - )); - SubtensorModule::stake_into_subnet( - &hotkey_b, - &coldkey2, - netuid, - 50_000_000_000u64.into(), - ::SwapInterface::max_price(), - false, - ) - .unwrap(); - - // coldkey1 locks more to hotkey_a - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey1, - netuid, - &hotkey_a, - 8000u64.into(), - )); - // coldkey2 locks less to hotkey_b - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey2, - netuid, - &hotkey_b, - 2000u64.into(), - )); - - step_block(500); - - let king = SubtensorModule::subnet_king(netuid); - assert_eq!(king, Some(hotkey_a)); - }); -} - -#[test] -fn test_subnet_king_no_locks() { - new_test_ext(1).execute_with(|| { - let netuid = subtensor_runtime_common::NetUid::from(99); - let king = SubtensorModule::subnet_king(netuid); - assert_eq!(king, None); - }); -} - -#[test] -fn test_change_subnet_owner_if_needed_reassigns_to_subnet_king() { - new_test_ext(1).execute_with(|| { - // Start with the subnet's existing owner, then create a different hotkey owner - // that can become subnet king. - let old_owner_coldkey = U256::from(1); - let old_owner_hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(old_owner_coldkey, old_owner_hotkey, 100_000_000_000); - SubnetOwner::::insert(netuid, old_owner_coldkey); - SubnetOwnerHotkey::::insert(netuid, old_owner_hotkey); - - let new_owner_coldkey = U256::from(5); - let king_hotkey = U256::from(6); - assert_ok!(SubtensorModule::create_account_if_non_existent( - &new_owner_coldkey, - &king_hotkey - )); - - // Make the subnet old enough and set alpha out so 1_000 conviction is exactly - // the 10% minimum required to trigger reassignment. - let now = crate::staking::lock::ONE_YEAR + 1; - System::set_block_number(now); - NetworkRegisteredAt::::insert(netuid, 1); - SubnetAlphaOut::::insert(netuid, AlphaBalance::from(10_000u64)); - - // Seed matching individual and aggregate lock rows for the future king. - let locked_mass = AlphaBalance::from(1_000u64); - Lock::::insert( - (new_owner_coldkey, netuid, king_hotkey), - LockState { - locked_mass, - conviction: U64F64::from_num(1_000), - last_update: now, - }, - ); - HotkeyLock::::insert( - netuid, - king_hotkey, - LockState { - locked_mass, - conviction: U64F64::from_num(1_000), - last_update: now, - }, - ); - - // Reassignment should select the king hotkey and its owning coldkey. - SubtensorModule::change_subnet_owner_if_needed(netuid); - - assert_eq!(SubnetOwner::::get(netuid), new_owner_coldkey); - assert_eq!(SubnetOwnerHotkey::::get(netuid), king_hotkey); - - // The new owner's aggregate conviction is progressed to locked mass. - let owner_lock = Lock::::get((new_owner_coldkey, netuid, king_hotkey)).unwrap(); - assert_eq!(owner_lock.conviction, U64F64::from_num(1_000)); - - let king_lock = OwnerLock::::get(netuid).unwrap(); - assert_eq!(king_lock.conviction, U64F64::from_num(1_000)); - }); -} - -#[test] -fn test_run_coinbase_reassigns_subnet_owner_by_conviction_on_epoch() { - new_test_ext(1).execute_with(|| { - let old_owner_coldkey = U256::from(1); - let old_owner_hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(old_owner_coldkey, old_owner_hotkey, 100_000_000_000); - SubnetOwner::::insert(netuid, old_owner_coldkey); - SubnetOwnerHotkey::::insert(netuid, old_owner_hotkey); - - let new_owner_coldkey = U256::from(5); - let king_hotkey = U256::from(6); - assert_ok!(SubtensorModule::create_account_if_non_existent( - &new_owner_coldkey, - &king_hotkey - )); - - let now = crate::staking::lock::ONE_YEAR + 1; - System::set_block_number(now); - NetworkRegisteredAt::::insert(netuid, 1); - SubnetAlphaOut::::insert(netuid, AlphaBalance::from(10_000u64)); - SubtensorModule::set_tempo_unchecked(netuid, 1); - LastEpochBlock::::insert(netuid, now.saturating_sub(1)); - PendingEpochAt::::insert(netuid, 0); - - let locked_mass = AlphaBalance::from(1_000u64); - Lock::::insert( - (new_owner_coldkey, netuid, king_hotkey), - LockState { - locked_mass, - conviction: U64F64::from_num(1_000), - last_update: now, - }, - ); - HotkeyLock::::insert( - netuid, - king_hotkey, - LockState { - locked_mass, - conviction: U64F64::from_num(1_000), - last_update: now, - }, - ); - - assert_eq!(SubnetOwner::::get(netuid), old_owner_coldkey); - assert_eq!(SubnetOwnerHotkey::::get(netuid), old_owner_hotkey); - - SubtensorModule::run_coinbase(SubtensorModule::mint_tao(0.into())); - - assert_eq!(SubnetOwner::::get(netuid), new_owner_coldkey); - assert_eq!(SubnetOwnerHotkey::::get(netuid), king_hotkey); - assert_eq!(LastEpochBlock::::get(netuid), now); - }); -} - -#[test] -fn test_change_subnet_owner_rebuilds_old_owner_hotkey_by_lock_mode() { - new_test_ext(1).execute_with(|| { - let old_owner_coldkey = U256::from(1); - let old_owner_hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(old_owner_coldkey, old_owner_hotkey, 100_000_000_000); - SubnetOwner::::insert(netuid, old_owner_coldkey); - SubnetOwnerHotkey::::insert(netuid, old_owner_hotkey); - - let perpetual_coldkey = U256::from(3); - let decaying_coldkey = U256::from(4); - let king_coldkey = U256::from(5); - let king_hotkey = U256::from(6); - assert_ok!(SubtensorModule::create_account_if_non_existent( - &king_coldkey, - &king_hotkey - )); - register_ok_neuron(netuid, king_hotkey, king_coldkey, 0); - - let now = crate::staking::lock::ONE_YEAR + 1; - System::set_block_number(now); - NetworkRegisteredAt::::insert(netuid, 1); - SubnetAlphaOut::::insert(netuid, AlphaBalance::from(10_000u64)); - DecayingLock::::insert(perpetual_coldkey, netuid, false); - - Lock::::insert( - (perpetual_coldkey, netuid, old_owner_hotkey), - LockState { - locked_mass: 400u64.into(), - conviction: U64F64::from_num(400), - last_update: now, - }, - ); - Lock::::insert( - (decaying_coldkey, netuid, old_owner_hotkey), - LockState { - locked_mass: 300u64.into(), - conviction: U64F64::from_num(300), - last_update: now, - }, - ); - OwnerLock::::insert( - netuid, - LockState { - locked_mass: 400u64.into(), - conviction: U64F64::from_num(400), - last_update: now, - }, - ); - DecayingOwnerLock::::insert( - netuid, - LockState { - locked_mass: 300u64.into(), - conviction: U64F64::from_num(300), - last_update: now, - }, - ); - Lock::::insert( - (king_coldkey, netuid, king_hotkey), - LockState { - locked_mass: 1_000u64.into(), - conviction: U64F64::from_num(1_000), - last_update: now, - }, - ); - HotkeyLock::::insert( - netuid, - king_hotkey, - LockState { - locked_mass: 1_000u64.into(), - conviction: U64F64::from_num(1_000), - last_update: now, - }, - ); - - SubtensorModule::change_subnet_owner_if_needed(netuid); - - assert_eq!(SubnetOwnerHotkey::::get(netuid), king_hotkey); - assert_eq!( - HotkeyLock::::get(netuid, old_owner_hotkey) - .unwrap() - .locked_mass, - 400u64.into() - ); - assert_eq!( - DecayingHotkeyLock::::get(netuid, old_owner_hotkey) - .unwrap() - .locked_mass, - 300u64.into() - ); - assert_eq!( - OwnerLock::::get(netuid).unwrap().locked_mass, - 1_000u64.into() - ); - }); -} - -#[test] -fn test_swap_hotkey_locks_moves_owner_hotkey_aggregate_to_owner_lock() { - new_test_ext(1).execute_with(|| { - let owner_coldkey = U256::from(1); - let old_owner_hotkey = U256::from(2); - let new_owner_hotkey = U256::from(3); - let locking_coldkey = U256::from(4); - let netuid = setup_subnet_with_stake(owner_coldkey, old_owner_hotkey, 100_000_000_000); - SubnetOwner::::insert(netuid, owner_coldkey); - SubnetOwnerHotkey::::insert(netuid, old_owner_hotkey); - - assert_ok!(SubtensorModule::create_account_if_non_existent( - &owner_coldkey, - &new_owner_hotkey - )); - - let now = SubtensorModule::get_current_block_as_u64(); - Lock::::insert( - (locking_coldkey, netuid, old_owner_hotkey), - LockState { - locked_mass: 500u64.into(), - conviction: U64F64::from_num(500), - last_update: now, - }, - ); - SubtensorModule::add_locking_coldkey(&old_owner_hotkey, netuid, &locking_coldkey); - OwnerLock::::insert( - netuid, - LockState { - locked_mass: 500u64.into(), - conviction: U64F64::from_num(500), - last_update: now, - }, - ); - - SubtensorModule::swap_hotkey_locks(&old_owner_hotkey, &new_owner_hotkey); - - assert!(Lock::::get((locking_coldkey, netuid, old_owner_hotkey)).is_none()); - assert!(Lock::::get((locking_coldkey, netuid, new_owner_hotkey)).is_some()); - assert!(HotkeyLock::::get(netuid, new_owner_hotkey).is_none()); - assert!(DecayingHotkeyLock::::get(netuid, new_owner_hotkey).is_none()); - assert_eq!( - OwnerLock::::get(netuid).unwrap().locked_mass, - 500u64.into() - ); - assert!(!LockingColdkeys::::contains_key(( - netuid, - old_owner_hotkey, - locking_coldkey - ))); - assert!(LockingColdkeys::::contains_key(( - netuid, - new_owner_hotkey, - locking_coldkey - ))); - }); -} - -#[test] -fn test_change_subnet_owner_if_needed_does_not_reassign_when_required_condition_is_missing() { - let assert_owner_unchanged = - |alpha_out: u64, registered_at: u64, owner_conviction: u64, king_conviction: u64| { - new_test_ext(1).execute_with(|| { - let owner_coldkey = U256::from(1001); - let owner_hotkey = U256::from(1002); - let staker_coldkey = U256::from(1); - let staker_hotkey = U256::from(2); - let netuid = - setup_subnet_with_stake(staker_coldkey, staker_hotkey, 100_000_000_000); - - let king_coldkey = U256::from(5); - let king_hotkey = U256::from(6); - assert_ok!(SubtensorModule::create_account_if_non_existent( - &king_coldkey, - &king_hotkey - )); - - let now = crate::staking::lock::ONE_YEAR + 10; - System::set_block_number(now); - NetworkRegisteredAt::::insert(netuid, registered_at); - SubnetAlphaOut::::insert(netuid, AlphaBalance::from(alpha_out)); - - let locked_mass = AlphaBalance::from(1_000u64); - HotkeyLock::::insert( - netuid, - owner_hotkey, - LockState { - locked_mass, - conviction: U64F64::from_num(owner_conviction), - last_update: now, - }, - ); - HotkeyLock::::insert( - netuid, - king_hotkey, - LockState { - locked_mass, - conviction: U64F64::from_num(king_conviction), - last_update: now, - }, - ); - - SubtensorModule::change_subnet_owner_if_needed(netuid); - - assert_eq!(SubnetOwner::::get(netuid), owner_coldkey); - assert_eq!(SubnetOwnerHotkey::::get(netuid), owner_hotkey); - }); - }; - - // Missing condition 1: total conviction is below 10% of SubnetAlphaOut. - assert_owner_unchanged(30_000, 1, 500, 1_000); - - // Missing condition 2: subnet is younger than one year. - assert_owner_unchanged(20_000, crate::staking::lock::ONE_YEAR, 500, 1_000); - - // Missing condition 3: challenger is not the subnet king because owner's conviction is higher. - assert_owner_unchanged(20_000, 1, 2_000, 1_000); -} - -// ========================================================================= -// GROUP 10: Lock force-reduction -// ========================================================================= - -#[test] -fn test_reduce_lock_removes_dust() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - let lock_amount = AlphaBalance::from(50u64); - - // Lock a small amount - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - lock_amount, - )); - - // Advance many taus so everything decays well below dust (100) - let tau = UnlockRate::::get(); - let target = System::block_number() + tau * 50; - System::set_block_number(target); - - // Remove full lock amount - SubtensorModule::force_reduce_lock(&coldkey, netuid, lock_amount); - - assert!(Lock::::get((coldkey, netuid, hotkey)).is_none()); - assert!(HotkeyLock::::get(netuid, hotkey).is_none()); - }); -} - -#[test] -fn test_reduce_lock_partial_reduction() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - let lock_amount = AlphaBalance::from(1_000u64); - let reduce_amount = AlphaBalance::from(400u64); - let now = SubtensorModule::get_current_block_as_u64(); - - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - lock_amount, - )); - - let conviction = U64F64::from_num(1_000); - Lock::::insert( - (coldkey, netuid, hotkey), - LockState { - locked_mass: lock_amount, - conviction, - last_update: now, - }, - ); - HotkeyLock::::insert( - netuid, - hotkey, - LockState { - locked_mass: lock_amount, - conviction, - last_update: now, - }, - ); - - SubtensorModule::force_reduce_lock(&coldkey, netuid, reduce_amount); - - let lock = Lock::::get((coldkey, netuid, hotkey)).expect("lock should remain"); - assert_eq!(lock.locked_mass, 600u64.into()); - assert_abs_diff_eq!( - lock.conviction.to_num::(), - 600., - epsilon = 0.0000000001 - ); - - let hotkey_lock = - HotkeyLock::::get(netuid, hotkey).expect("hotkey lock should remain"); - assert_eq!(hotkey_lock.locked_mass, 600u64.into()); - assert_abs_diff_eq!( - hotkey_lock.conviction.to_num::(), - 600., - epsilon = 0.0000000001 - ); - }); -} - -#[test] -fn test_reduce_lock_no_lock() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let netuid = subtensor_runtime_common::NetUid::from(1); - // Should be a no-op, no panic - SubtensorModule::force_reduce_lock(&coldkey, netuid, 100u64.into()); - assert!( - Lock::::iter_prefix((coldkey, netuid)) - .next() - .is_none() - ); - }); -} - -#[test] -fn test_reduce_lock_two_coldkeys() { - new_test_ext(1).execute_with(|| { - let coldkey1 = U256::from(1); - let coldkey2 = U256::from(3); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey1, hotkey, 100_000_000_000); - - // Add stake on coldkey 2 - add_balance_to_coldkey_account(&coldkey2, 100_000_000_000u64.into()); - assert_ok!(SubtensorModule::create_account_if_non_existent( - &coldkey2, &hotkey - )); - SubtensorModule::stake_into_subnet( - &hotkey, - &coldkey2, - netuid, - 100_000_000_000u64.into(), - ::SwapInterface::max_price(), - false, - ) - .unwrap(); - DecayingLock::::insert(coldkey2, netuid, false); - - // Mock a non-zero conviction for both coldkeys - let lock1 = Lock::::get((coldkey1, netuid, hotkey)).unwrap_or(LockState { - locked_mass: 0.into(), - conviction: U64F64::from_num(1234), - last_update: System::block_number(), - }); - let lock2 = Lock::::get((coldkey2, netuid, hotkey)).unwrap_or(LockState { - locked_mass: 0.into(), - conviction: U64F64::from_num(1234), - last_update: System::block_number(), - }); - Lock::::insert((coldkey1, netuid, hotkey), lock1); - Lock::::insert((coldkey2, netuid, hotkey), lock2); - HotkeyLock::::insert( - netuid, - hotkey, - LockState { - locked_mass: 0.into(), - conviction: U64F64::from_num(1234 * 2), - last_update: System::block_number(), - }, - ); - - // Lock a small amount from both coldkeys - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey1, - netuid, - &hotkey, - 50u64.into(), - )); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey2, - netuid, - &hotkey, - 50u64.into(), - )); - - SubtensorModule::force_reduce_lock(&coldkey1, netuid, 50u64.into()); - - // Should only clean up coldkey1's lock, not coldkey2's - assert!( - Lock::::iter_prefix((coldkey1, netuid)) - .next() - .is_none() - ); - assert!(Lock::::get((coldkey2, netuid, hotkey)).is_some()); - - // Hotkey lock should reduce according to coldkey1 lock - let hotkey_lock = HotkeyLock::::get(netuid, hotkey).unwrap(); - assert_eq!(hotkey_lock.locked_mass, 50u64.into()); - - // Conviction should be reduced by coldkey1's lock conviction, - // but not fully reset because coldkey2 still has a lock - assert!(hotkey_lock.conviction == U64F64::from_num(1234)); - }); -} - -#[test] -fn test_force_reduce_lock_does_not_over_reduce_hotkey_lock() { - new_test_ext(1).execute_with(|| { - let coldkey1 = U256::from(1); - let coldkey2 = U256::from(3); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey1, hotkey, 100_000_000_000); - let now = SubtensorModule::get_current_block_as_u64(); - - Lock::::insert( - (coldkey1, netuid, hotkey), - LockState { - locked_mass: 1_000u64.into(), - conviction: U64F64::from_num(1_000), - last_update: now, - }, - ); - Lock::::insert( - (coldkey2, netuid, hotkey), - LockState { - locked_mass: 5_000u64.into(), - conviction: U64F64::from_num(2_000), - last_update: now, - }, - ); - HotkeyLock::::insert( - netuid, - hotkey, - LockState { - locked_mass: 6_000u64.into(), - conviction: U64F64::from_num(3_000), - last_update: now, - }, - ); - - SubtensorModule::force_reduce_lock(&coldkey1, netuid, 2_000u64.into()); - - assert!(Lock::::get((coldkey1, netuid, hotkey)).is_none()); - assert!(Lock::::get((coldkey2, netuid, hotkey)).is_some()); - - let hotkey_lock = - HotkeyLock::::get(netuid, hotkey).expect("hotkey lock should remain"); - assert_eq!(hotkey_lock.locked_mass, 5_000u64.into()); - assert_eq!(hotkey_lock.conviction, U64F64::from_num(2_000)); - }); -} - -// ========================================================================= -// GROUP 11: Coldkey swap interaction -// ========================================================================= - -#[test] -fn test_coldkey_swap_swaps_lock() { - new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); - let new_coldkey = U256::from(10); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(old_coldkey, hotkey, 100_000_000_000); - - assert_ok!(SubtensorModule::do_lock_stake( - &old_coldkey, - netuid, - &hotkey, - 5000u64.into(), - )); - assert_ok!(SubtensorModule::set_reject_locked_alpha( - RuntimeOrigin::signed(new_coldkey), - false, - )); - - // Perform coldkey swap - assert_ok!(SubtensorModule::do_swap_coldkey(&old_coldkey, &new_coldkey)); - - // Lock removed on old coldkey - assert!( - Lock::::iter_prefix((old_coldkey, netuid)) - .next() - .is_none() - ); - assert!(!DecayingLock::::contains_key(old_coldkey, netuid)); - // New coldkey now has the lock - assert!(Lock::::get((new_coldkey, netuid, hotkey)).is_some()); - assert_eq!(DecayingLock::::get(new_coldkey, netuid), Some(false)); - assert!(HotkeyLock::::contains_key(netuid, hotkey)); - assert!(!DecayingHotkeyLock::::contains_key(netuid, hotkey)); - }); -} - -#[test] -fn test_coldkey_swap_lock_blocks_unstake() { - new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); - let new_coldkey = U256::from(10); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(old_coldkey, hotkey, 100_000_000_000); - - let total = SubtensorModule::total_coldkey_alpha_on_subnet(&old_coldkey, netuid); - assert_ok!(SubtensorModule::do_lock_stake( - &old_coldkey, - netuid, - &hotkey, - total, - )); - assert_ok!(SubtensorModule::set_reject_locked_alpha( - RuntimeOrigin::signed(new_coldkey), - false, - )); - - // Swap coldkey - assert_ok!(SubtensorModule::do_swap_coldkey(&old_coldkey, &new_coldkey)); - - step_block(1); - - // New coldkey should not be able to unstake - let alpha = get_alpha(&hotkey, &new_coldkey, netuid); - assert!(alpha > AlphaBalance::ZERO); - assert_noop!( - SubtensorModule::do_remove_stake( - RuntimeOrigin::signed(new_coldkey), - hotkey, - netuid, - alpha, - ), - Error::::StakeUnavailable - ); - }); -} - -#[test] -// Conviction-only destination lock state is not active, so direct coldkey lock transfer is allowed. -fn test_coldkey_swap_allows_destination_conviction_only_lock() { - new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); - let new_coldkey = U256::from(10); - let old_hotkey = U256::from(2); - let new_hotkey = U256::from(20); - let netuid = subtensor_runtime_common::NetUid::from(1); - - let old_conviction = U64F64::from_num(777); - let new_conviction = U64F64::from_num(111); - - SubtensorModule::insert_lock_state( - &old_coldkey, - netuid, - &old_hotkey, - LockState { - locked_mass: AlphaBalance::ZERO, - conviction: old_conviction, - last_update: SubtensorModule::get_current_block_as_u64(), - }, - ); - DecayingLock::::insert(old_coldkey, netuid, false); - SubtensorModule::insert_lock_state( - &new_coldkey, - netuid, - &new_hotkey, - LockState { - locked_mass: AlphaBalance::ZERO, - conviction: new_conviction, - last_update: SubtensorModule::get_current_block_as_u64(), - }, - ); - - assert_ok!(SubtensorModule::swap_coldkey_locks( - &old_coldkey, - &new_coldkey - )); - - assert!( - Lock::::iter_prefix((old_coldkey, netuid)) - .next() - .is_none() - ); - assert!(Lock::::get((new_coldkey, netuid, new_hotkey)).is_some()); - - let swapped_lock = Lock::::get((new_coldkey, netuid, old_hotkey)) - .expect("source lock should be transferred"); - assert_eq!(swapped_lock.locked_mass, AlphaBalance::ZERO); - assert_eq!(swapped_lock.conviction, old_conviction); - assert_eq!(Lock::::iter_prefix((new_coldkey, netuid)).count(), 2); - assert!(DecayingLock::::get(old_coldkey, netuid).is_none()); - assert_eq!(DecayingLock::::get(new_coldkey, netuid), Some(false)); - }); -} - -#[test] -// When the destination already has an active lock, coldkey lock transfer should fail -// before mutating either coldkey's lock state. -fn test_coldkey_swap_rejects_destination_lock() { - new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); - let new_coldkey = U256::from(10); - let old_hotkey = U256::from(2); - let new_hotkey = U256::from(20); - let netuid = subtensor_runtime_common::NetUid::from(1); - - let old_locked = AlphaBalance::from(7_000u64); - let old_conviction = U64F64::from_num(77); - - let new_locked = AlphaBalance::from(999u64); - let new_conviction = U64F64::from_num(11); - - SubtensorModule::insert_lock_state( - &old_coldkey, - netuid, - &old_hotkey, - LockState { - locked_mass: old_locked, - conviction: old_conviction, - last_update: SubtensorModule::get_current_block_as_u64(), - }, - ); - SubtensorModule::insert_lock_state( - &new_coldkey, - netuid, - &new_hotkey, - LockState { - locked_mass: new_locked, - conviction: new_conviction, - last_update: SubtensorModule::get_current_block_as_u64(), - }, - ); - - assert_noop!( - SubtensorModule::swap_coldkey_locks(&old_coldkey, &new_coldkey), - Error::::ActiveLockExists - ); - - let source_lock = Lock::::get((old_coldkey, netuid, old_hotkey)) - .expect("source lock should remain after failed transfer"); - assert_eq!(source_lock.locked_mass, old_locked); - assert_eq!(source_lock.conviction, old_conviction); - let destination_lock = Lock::::get((new_coldkey, netuid, new_hotkey)) - .expect("destination lock should remain after failed transfer"); - assert_eq!(destination_lock.locked_mass, new_locked); - assert_eq!(destination_lock.conviction, new_conviction); - assert!( - Lock::::get((new_coldkey, netuid, old_hotkey)).is_none(), - "source lock should not be inserted under destination coldkey" - ); - assert_eq!(Lock::::iter_prefix((new_coldkey, netuid)).count(), 1); - }); -} - -#[test] -fn test_coldkey_swap_rejects_locked_alpha_to_flagged_destination() { - new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); - let new_coldkey = U256::from(10); - let old_hotkey = U256::from(2); - let netuid = subtensor_runtime_common::NetUid::from(1); - - let old_locked = AlphaBalance::from(7_000u64); - let old_conviction = U64F64::from_num(77); - - SubtensorModule::insert_lock_state( - &old_coldkey, - netuid, - &old_hotkey, - LockState { - locked_mass: old_locked, - conviction: old_conviction, - last_update: SubtensorModule::get_current_block_as_u64(), - }, - ); - DecayingLock::::insert(old_coldkey, netuid, false); - assert_ok!(SubtensorModule::set_reject_locked_alpha( - RuntimeOrigin::signed(new_coldkey), - true, - )); - - assert_noop!( - SubtensorModule::swap_coldkey_locks(&old_coldkey, &new_coldkey), - Error::::AccountRejectsLockedAlpha - ); - - let source_lock = Lock::::get((old_coldkey, netuid, old_hotkey)) - .expect("source lock should remain after failed transfer"); - assert_eq!(source_lock.locked_mass, old_locked); - assert_eq!(source_lock.conviction, old_conviction); - assert!( - Lock::::iter_prefix((new_coldkey, netuid)) - .next() - .is_none() - ); - assert_eq!(DecayingLock::::get(old_coldkey, netuid), Some(false)); - assert!(DecayingLock::::get(new_coldkey, netuid).is_none()); - }); -} - -#[test] -// The public coldkey swap extrinsic runs inside a storage layer, so a late failure rolls back the earlier writes. -fn test_failed_coldkey_swap_extrinsic_rolls_back_state_changes() { - new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); - let old_hotkey = U256::from(2); - let new_coldkey = U256::from(3); - let blocked_hotkey = U256::from(4); - let netuid = setup_subnet_with_stake(old_coldkey, old_hotkey, 100_000_000_000); - - let original_stake = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &old_hotkey, - &old_coldkey, - netuid, - ); - assert!(!original_stake.is_zero()); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &old_hotkey, - &new_coldkey, - netuid - ), - AlphaBalance::ZERO - ); - - // Seed a lock directly on the destination coldkey so the swap reaches ActiveLockExists - // without tripping the earlier "already associated" guard. - SubtensorModule::insert_lock_state( - &new_coldkey, - netuid, - &blocked_hotkey, - LockState { - locked_mass: 1_000u64.into(), - conviction: U64F64::from_num(0), - last_update: SubtensorModule::get_current_block_as_u64(), - }, - ); - - assert_noop!( - SubtensorModule::swap_coldkey( - RuntimeOrigin::root(), - old_coldkey, - new_coldkey, - TaoBalance::ZERO, - ), - Error::::ActiveLockExists - ); - - // The failed extrinsic should roll back the earlier stake transfer. - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &old_hotkey, - &old_coldkey, - netuid - ), - original_stake - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &old_hotkey, - &new_coldkey, - netuid - ), - AlphaBalance::ZERO - ); - }); -} - -// ========================================================================= -// GROUP 12: Hotkey swap interaction -// ========================================================================= - -#[test] -fn test_hotkey_swap_swaps_locks_and_convictions() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let old_hotkey = U256::from(2); - let new_hotkey = U256::from(20); - let netuid = setup_subnet_with_stake(coldkey, old_hotkey, 100_000_000_000); - Owner::::insert(old_hotkey, coldkey); - Owner::::insert(new_hotkey, coldkey); - - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &old_hotkey, - 5000u64.into(), - )); - assert!(LockingColdkeys::::contains_key(( - netuid, old_hotkey, coldkey - ))); - assert_eq!( - LockingColdkeys::::iter_prefix((netuid, old_hotkey)).count(), - 1 - ); - - // Mock a non-zero conviction - let mut lock = Lock::::get((coldkey, netuid, old_hotkey)).unwrap(); - lock.conviction = U64F64::from_num(1234); - Lock::::insert((coldkey, netuid, old_hotkey), lock); - let mut hotkey_lock = HotkeyLock::::get(netuid, old_hotkey).unwrap(); - hotkey_lock.conviction = U64F64::from_num(1234); - HotkeyLock::::insert(netuid, old_hotkey, hotkey_lock); - - // Perform hotkey swap - let mut weight = Weight::zero(); - assert_ok!(SubtensorModule::perform_hotkey_swap_on_all_subnets( - &old_hotkey, - &new_hotkey, - &coldkey, - &mut weight, - false - )); - - // Lock references new_hotkey, conviction is not reset - let lock = Lock::::get((coldkey, netuid, new_hotkey)).unwrap(); - assert_eq!(lock.locked_mass, 5000u64.into()); - assert!(lock.conviction > U64F64::from_num(0)); - assert!(!LockingColdkeys::::contains_key(( - netuid, old_hotkey, coldkey - ))); - assert!(LockingColdkeys::::contains_key(( - netuid, new_hotkey, coldkey - ))); - - // Hotkey lock data also updated, conviction is not reset - let hotkey_lock = HotkeyLock::::get(netuid, new_hotkey).unwrap(); - assert_eq!(hotkey_lock.locked_mass, 5000u64.into()); - assert!(hotkey_lock.conviction > U64F64::from_num(0)); - - // Trying to top up to new_hotkey works - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &new_hotkey, - 100u64.into() - )); - - // Trying to top up to old_hotkey fails (old_hotkey is no longer associated with coldkey) - assert_noop!( - SubtensorModule::do_lock_stake(&coldkey, netuid, &old_hotkey, 100u64.into()), - Error::::HotKeyAccountNotExists - ); - }); -} - -// ========================================================================= -// GROUP 13: Lock extrinsic via dispatch -// ========================================================================= - -#[test] -fn test_lock_stake_extrinsic() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - let lock_amount: u64 = 5000; - assert_ok!(SubtensorModule::lock_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - lock_amount.into(), - )); - - let lock = Lock::::get((coldkey, netuid, hotkey)).expect("Lock should exist"); - assert_eq!(lock.locked_mass, lock_amount.into()); - assert_eq!(lock.conviction, U64F64::from_num(0)); - - // Hotkey lock should also be updated - let hotkey_lock = - HotkeyLock::::get(netuid, hotkey).expect("Hotkey lock should exist"); - assert_eq!(hotkey_lock.locked_mass, lock_amount.into()); - assert_eq!(hotkey_lock.conviction, U64F64::from_num(0)); - }); -} - -// ========================================================================= -// GROUP 14: Recycle/burn alpha checks against lock -// ========================================================================= - -#[test] -fn test_recycle_alpha_checks_lock() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, total)); - - step_block(1); - - // Unstake should be blocked - let alpha = get_alpha(&hotkey, &coldkey, netuid); - assert_noop!( - SubtensorModule::do_remove_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - alpha, - ), - Error::::StakeUnavailable - ); - - // recycle_alpha checks lock and should fail if it would reduce alpha below locked amount - let recycle_amount = alpha / 2.into(); - assert_noop!( - SubtensorModule::do_recycle_alpha( - RuntimeOrigin::signed(coldkey), - hotkey, - recycle_amount, - netuid, - ), - Error::::StakeUnavailable - ); - - // Alpha is not below locked_mass - let total_after = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - let locked = SubtensorModule::get_current_locked(&coldkey, netuid); - assert!(total_after >= locked); - }); -} - -#[test] -fn test_burn_alpha_checks_lock() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, netuid, &hotkey, total - )); - - step_block(1); - - // burn_alpha checks lock and should fail if it would reduce alpha below locked amount - let alpha = get_alpha(&hotkey, &coldkey, netuid); - let burn_amount = alpha / 2.into(); - assert_noop!( - SubtensorModule::do_burn_alpha( - RuntimeOrigin::signed(coldkey), - hotkey, - burn_amount, - netuid, - ), - Error::::StakeUnavailable - ); - - // Alpha is not below locked_mass - let total_after = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - let locked = SubtensorModule::get_current_locked(&coldkey, netuid); - assert!(total_after >= locked); - }); -} - -// ========================================================================= -// GROUP 15: Subnet dissolution -// ========================================================================= - -#[test] -fn test_subnet_dissolution_orphans_locks() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - 5000u64.into(), - )); - assert!(Lock::::get((coldkey, netuid, hotkey)).is_some()); - - // Dissolve the subnet - assert_ok!(SubtensorModule::do_dissolve_network(netuid)); - run_block_idle(); - - // All Alpha entries are gone - assert_eq!( - SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid), - AlphaBalance::ZERO - ); - - // Lock entries are not orphaned - let lock = Lock::::get((coldkey, netuid, hotkey)); - assert!(lock.is_none()); - - // Hotkey lock is also removed - let hotkey_lock = HotkeyLock::::get(netuid, hotkey); - assert!(hotkey_lock.is_none()); - }); -} - -#[test] -fn test_subnet_dissolution_and_netuid_reuse() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey_old = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey_old, 100_000_000_000); - - // Lock on the old subnet - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey_old, - 5000u64.into(), - )); - - // Dissolve old subnet - assert_ok!(SubtensorModule::do_dissolve_network(netuid)); - run_block_idle(); - - // No stale lock from old subnet remains - let stale_lock = Lock::::get((coldkey, netuid, hotkey_old)); - assert!(stale_lock.is_none()); - - // No stale hotkey lock remains - let stale_hotkey_lock = HotkeyLock::::get(netuid, hotkey_old); - assert!(stale_hotkey_lock.is_none()); - }); -} - -// ========================================================================= -// GROUP 16: Clear small nomination checks lock -// ========================================================================= - -#[test] -fn test_clear_small_nomination_checks_lock() { - new_test_ext(1).execute_with(|| { - let owner_coldkey = U256::from(100); - let owner_hotkey = U256::from(101); - let netuid = setup_subnet_with_stake(owner_coldkey, owner_hotkey, 100_000_000_000); - - // Set up a nominator (different coldkey, does NOT own the hotkey) - let nominator = U256::from(200); - add_balance_to_coldkey_account(&nominator, 100_000_000_000u64.into()); - assert_ok!(SubtensorModule::create_account_if_non_existent( - &nominator, - &owner_hotkey - )); - SubtensorModule::stake_into_subnet( - &owner_hotkey, - &nominator, - netuid, - 50_000_000_000u64.into(), - ::SwapInterface::max_price(), - false, - ) - .unwrap(); - - let nominator_alpha = get_alpha(&owner_hotkey, &nominator, netuid); - assert!(nominator_alpha > AlphaBalance::ZERO); - - // Nominator locks their full stake - let nominator_total = SubtensorModule::total_coldkey_alpha_on_subnet(&nominator, netuid); - assert_ok!(SubtensorModule::do_lock_stake( - &nominator, - netuid, - &owner_hotkey, - nominator_total, - )); - - // Set a high nominator min stake so the current stake is "small" - SubtensorModule::set_nominator_min_required_stake(u64::MAX); - - // clear_small_nomination removes the lock and unstakes alpha - SubtensorModule::clear_small_nomination_if_required(&owner_hotkey, &nominator, netuid); - - // Nominator alpha has been removed despite lock - let nominator_alpha_after = get_alpha(&owner_hotkey, &nominator, netuid); - assert_eq!(nominator_alpha_after, AlphaBalance::ZERO); - - // Lock entry doesn't exist anymore - assert!( - Lock::::iter_prefix((nominator, netuid)) - .next() - .is_none() - ); - - // Hotkey lock should also be removed - let hotkey_lock = HotkeyLock::::get(netuid, owner_hotkey); - assert!(hotkey_lock.is_none()); - }); -} - -#[test] -// If one coldkey has a large nomination on one hotkey and a tiny nomination on another, -// clearing the tiny nomination should reduce the lock state only by that tiny alpha amount. -fn test_clear_small_nomination_reduces_only_tiny_amount_from_lock_state() { - new_test_ext(1).execute_with(|| { - // Large stake, subnet owner, and large lock receiver - let coldkey_large = U256::from(100); - let hotkey_large = U256::from(101); - let netuid = setup_subnet_with_stake(coldkey_large, hotkey_large, 100_000_000_000); - - let coldkey_tiny = U256::from(102); - let hotkey_tiny = U256::from(103); - assert_ok!(SubtensorModule::create_account_if_non_existent( - &coldkey_tiny, - &hotkey_tiny - )); - - // Coldkey that is going to stake and lock - let nominator = U256::from(200); - let large_tao = TaoBalance::from(50_000_000_000u64); - let tiny_tao = TaoBalance::from(1_000_000u64); - add_balance_to_coldkey_account(&nominator, large_tao + tiny_tao); - - // Create one large nomination and one tiny nomination on the same subnet. - SubtensorModule::stake_into_subnet( - &hotkey_large, - &nominator, - netuid, - large_tao, - ::SwapInterface::max_price(), - false, - ) - .unwrap(); - SubtensorModule::stake_into_subnet( - &hotkey_tiny, - &nominator, - netuid, - tiny_tao, - ::SwapInterface::max_price(), - false, - ) - .unwrap(); - DecayingLock::::insert(nominator, netuid, false); - - let large_alpha_before = get_alpha(&hotkey_large, &nominator, netuid); - let tiny_alpha_before = get_alpha(&hotkey_tiny, &nominator, netuid); - assert!(large_alpha_before > tiny_alpha_before); - - // Lock against the large nomination hotkey and seed non-zero unlocked_mass + conviction - // so we can verify each field is reduced only by the tiny nomination's alpha amount. - let total_before = SubtensorModule::total_coldkey_alpha_on_subnet(&nominator, netuid); - assert_ok!(SubtensorModule::do_lock_stake( - &nominator, - netuid, - &hotkey_large, - total_before, - )); - - let conviction_before = U64F64::from_num(tiny_alpha_before.to_u64() + 2_000); - let last_update = SubtensorModule::get_current_block_as_u64(); - Lock::::insert( - (nominator, netuid, hotkey_large), - LockState { - locked_mass: total_before, - conviction: conviction_before, - last_update, - }, - ); - HotkeyLock::::insert( - netuid, - hotkey_large, - LockState { - locked_mass: total_before, - conviction: conviction_before, - last_update, - }, - ); - - // Force the tiny nomination to qualify as "small" and clear only that nomination. - SubtensorModule::set_nominator_min_required_stake(u64::MAX); - SubtensorModule::clear_small_nomination_if_required(&hotkey_tiny, &nominator, netuid); - - // The large nomination stays, the tiny one is removed. - let large_alpha_after = get_alpha(&hotkey_large, &nominator, netuid); - let tiny_alpha_after = get_alpha(&hotkey_tiny, &nominator, netuid); - assert_eq!(large_alpha_after, large_alpha_before); - assert!(!large_alpha_after.is_zero()); - assert_eq!(tiny_alpha_after, AlphaBalance::ZERO); - - // Only the tiny alpha amount should be shaved off the coldkey lock state. - // Conviction is reduced proportionally - let lock_after = Lock::::get((nominator, netuid, hotkey_large)).unwrap(); - assert!(!lock_after.locked_mass.is_zero()); - assert_eq!(lock_after.locked_mass, total_before - tiny_alpha_before); - assert!(lock_after.conviction != U64F64::from_num(0)); - let expected_conviction = conviction_before.to_num::() - * (1. - u64::from(tiny_alpha_before) as f64 / u64::from(total_before) as f64); - assert_abs_diff_eq!( - lock_after.conviction.to_num::(), - expected_conviction, - epsilon = expected_conviction / 1000000. - ); - - // The aggregate hotkey lock on the locked hotkey should also only shrink by the tiny amount. - let hotkey_lock_after = HotkeyLock::::get(netuid, hotkey_large).unwrap(); - assert_eq!( - hotkey_lock_after.locked_mass, - total_before - tiny_alpha_before - ); - assert_abs_diff_eq!( - hotkey_lock_after.conviction.to_num::(), - expected_conviction, - epsilon = expected_conviction / 1000000. - ); - }); -} - -// ========================================================================= -// GROUP 17: Emission interaction -// ========================================================================= - -#[test] -fn test_emissions_do_not_break_lock_invariant() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - let total_alpha_before = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - total_alpha_before - )); - - // Simulate emission: directly increase alpha for the hotkey on subnet - // This increases the pool value for all share holders (including our coldkey) - let emission_amount: AlphaBalance = 10_000_000u64.into(); - SubtensorModule::increase_stake_for_hotkey_on_subnet(&hotkey, netuid, emission_amount); - - // After emission, total alpha should increase by emission_amount - let total_alpha_after = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - assert_eq!(total_alpha_after, total_alpha_before + emission_amount); - - // Lock invariant still holds: total_alpha >= locked_mass - let locked = SubtensorModule::get_current_locked(&coldkey, netuid); - assert!(total_alpha_after >= locked); - - // Available becomes emission_amount - let available = SubtensorModule::available_to_unstake(&coldkey, netuid); - assert_eq!(available, emission_amount); - }); -} - -#[test] -fn test_epoch_distribution_auto_locks_owner_cut() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let validator_coldkey = U256::from(1); - let validator_hotkey = U256::from(2); - let miner_coldkey = U256::from(5); - let miner_hotkey = U256::from(6); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - let subnet_tempo = 10; - let stake = 100_000_000_000u64; - - SubtensorModule::set_tempo_unchecked(netuid, subnet_tempo); - SubtensorModule::set_ck_burn(0); - setup_reserves(netuid, (stake * 10_000).into(), (stake * 10_000).into()); - - register_ok_neuron(netuid, validator_hotkey, validator_coldkey, 0); - register_ok_neuron(netuid, miner_hotkey, miner_coldkey, 1); - - add_balance_to_coldkey_account( - &validator_coldkey, - TaoBalance::from(stake) + ExistentialDeposit::get(), - ); - - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(validator_coldkey), - validator_hotkey, - netuid, - stake.into() - )); - - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - SubtensorModule::set_max_allowed_validators(netuid, 1); - step_block(subnet_tempo); - SubnetOwnerCut::::set(u16::MAX / 10); - OwnerCutAutoLockEnabled::::insert(netuid, true); - - let owner_uid = - SubtensorModule::get_uid_for_net_and_hotkey(netuid, &subnet_owner_hotkey).unwrap(); - let validator_uid = - SubtensorModule::get_uid_for_net_and_hotkey(netuid, &validator_hotkey).unwrap(); - let miner_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &miner_hotkey).unwrap(); - let uid_count = [ - owner_uid as usize, - validator_uid as usize, - miner_uid as usize, - ] - .into_iter() - .max() - .unwrap() - + 1; - - // Setup YUMA so that the next epoch produces non-zero subnet emissions. - Weights::::insert( - NetUidStorageIndex::from(netuid), - validator_uid, - vec![(miner_uid, 0xFFFF)], - ); - BlockAtRegistration::::set(netuid, owner_uid, 1); - BlockAtRegistration::::set(netuid, validator_uid, 1); - BlockAtRegistration::::set(netuid, miner_uid, 1); - LastUpdate::::set(NetUidStorageIndex::from(netuid), vec![2; uid_count]); - Kappa::::set(netuid, u16::MAX / 5); - ActivityCutoff::::set(netuid, u16::MAX); - let mut validator_permit = vec![false; uid_count]; - validator_permit[validator_uid as usize] = true; - ValidatorPermit::::insert(netuid, validator_permit); - - let owner_stake_before = get_alpha(&subnet_owner_hotkey, &subnet_owner_coldkey, netuid); - assert!( - Lock::::iter_prefix((subnet_owner_coldkey, netuid)) - .next() - .is_none() - ); - - // Advance to the next epoch so owner cut is distributed and auto-locked. - step_epochs(1, netuid); - - let owner_stake_after = get_alpha(&subnet_owner_hotkey, &subnet_owner_coldkey, netuid); - let owner_cut_locked = owner_stake_after - owner_stake_before; - assert!(owner_cut_locked > AlphaBalance::ZERO); - - let owner_lock = Lock::::get((subnet_owner_coldkey, netuid, subnet_owner_hotkey)) - .expect("owner cut should be auto-locked to the subnet owner's hotkey"); - assert_eq!(owner_lock.locked_mass, owner_cut_locked); - }); -} - -#[test] -fn test_auto_lock_owner_cut_is_disabled_by_default_and_can_be_enabled() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let netuid = - setup_subnet_with_stake(subnet_owner_coldkey, subnet_owner_hotkey, 100_000_000_000); - let owner_cut: AlphaBalance = 10_000_000u64.into(); - - assert!(!SubtensorModule::get_owner_cut_auto_lock_enabled(netuid)); - SubtensorModule::auto_lock_owner_cut(netuid, owner_cut); - - assert!( - Lock::::iter_prefix((subnet_owner_coldkey, netuid)) - .next() - .is_none() - ); - - OwnerCutAutoLockEnabled::::insert(netuid, true); - assert!(SubtensorModule::get_owner_cut_auto_lock_enabled(netuid)); - SubtensorModule::auto_lock_owner_cut(netuid, owner_cut); - - let owner_lock = Lock::::get((subnet_owner_coldkey, netuid, subnet_owner_hotkey)) - .expect("owner cut should be auto-locked when enabled"); - assert_eq!(owner_lock.locked_mass, owner_cut); - }); -} - -// ========================================================================= -// GROUP 18: Neuron replacement -// ========================================================================= - -#[test] -fn test_neuron_replacement_does_not_affect_lock() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - - // Register the hotkey as a neuron - register_ok_neuron(netuid, hotkey, coldkey, 0); - - let lock_amount = 5000u64.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey, - lock_amount - )); - assert_ok!(SubtensorModule::do_set_perpetual_lock( - &coldkey, netuid, false, - )); - - let total_before = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - let locked_before = SubtensorModule::get_current_locked(&coldkey, netuid); - - // Replace the neuron with a different hotkey - let new_hotkey = U256::from(99); - let uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey).unwrap(); - SubtensorModule::replace_neuron( - netuid, - uid, - &new_hotkey, - SubtensorModule::get_current_block_as_u64(), - ); - - // Alpha and lock should be unaffected by neuron replacement - let total_after = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); - let locked_after = SubtensorModule::get_current_locked(&coldkey, netuid); - - assert_eq!(total_after, total_before); - assert_eq!(locked_after, locked_before); - - // Lock still references original hotkey - assert!(Lock::::get((coldkey, netuid, hotkey)).is_some()); - - // Aggregate lock still references original hotkey - assert!(DecayingHotkeyLock::::get(netuid, hotkey).is_some()); - }); -} - -// ========================================================================= -// GROUP 19: Moving lock -// ========================================================================= - -#[test] -fn test_moving_lock() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey_origin = U256::from(2); - let hotkey_destination = U256::from(3); - let netuid = setup_subnet_with_stake(coldkey, hotkey_origin, 100_000_000_000); - assert_ok!(SubtensorModule::create_account_if_non_existent( - &coldkey, - &hotkey_destination - )); - - let lock_amount = 5000u64.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey_origin, - lock_amount - )); - - // Mock a non-zero conviction - let mut lock = Lock::::get((coldkey, netuid, hotkey_origin)).unwrap(); - lock.conviction = U64F64::from_num(1234); - Lock::::insert((coldkey, netuid, hotkey_origin), lock); - let mut hotkey_lock = HotkeyLock::::get(netuid, hotkey_origin).unwrap(); - hotkey_lock.conviction = U64F64::from_num(1234); - HotkeyLock::::insert(netuid, hotkey_origin, hotkey_lock); - - assert_ok!(SubtensorModule::move_lock( - RuntimeOrigin::signed(coldkey), - hotkey_destination, - netuid, - )); - let lock = Lock::::get((coldkey, netuid, hotkey_destination)).unwrap(); - assert_eq!(lock.locked_mass, lock_amount); - assert_eq!(lock.conviction, U64F64::from_num(1234)); - - // Hotkey lock is removed on origin and added on destination - assert!(HotkeyLock::::get(netuid, hotkey_origin).is_none()); - let hotkey_lock_destination_after = - HotkeyLock::::get(netuid, hotkey_destination).unwrap(); - assert_eq!(hotkey_lock_destination_after.locked_mass, lock_amount); - - // Conviction is not reset because owner is the same for origin and destination - // hotkeys - assert_eq!( - hotkey_lock_destination_after.conviction, - U64F64::from_num(1234) - ); - }); -} - -#[test] -fn test_moving_lock_to_subnet_owner_hotkey_gets_owner_conviction_for_non_owner_coldkey() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey_origin = U256::from(2); - let netuid = setup_subnet_with_stake(coldkey, hotkey_origin, 100_000_000_000); - let owner_hotkey = SubnetOwnerHotkey::::get(netuid); - - let lock_amount = 5000u64.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &hotkey_origin, - lock_amount - )); - - assert_ok!(SubtensorModule::move_lock( - RuntimeOrigin::signed(coldkey), - owner_hotkey, - netuid, - )); - - let lock = Lock::::get((coldkey, netuid, owner_hotkey)).unwrap(); - assert_eq!(lock.locked_mass, lock_amount); - assert_eq!(lock.conviction, U64F64::from_num(5000)); - - assert!( - HotkeyLock::::get(netuid, owner_hotkey).is_none(), - "lock moved to owner hotkey should use OwnerLock" - ); - let owner_lock = OwnerLock::::get(netuid).unwrap(); - assert_eq!(owner_lock.locked_mass, lock_amount); - assert_eq!(owner_lock.conviction, U64F64::from_num(5000)); - }); -} - -#[test] -fn test_moving_partial_lock() { - new_test_ext(1).execute_with(|| { - let coldkey1 = U256::from(1); - let coldkey2 = U256::from(2); - let hotkey_origin = U256::from(3); - let hotkey_destination = U256::from(4); - let netuid = setup_subnet_with_stake(coldkey1, hotkey_origin, 100_000_000_000); - - // Make hotkey_origin and hotkey_destination owned by different coldkeys - assert_ok!(SubtensorModule::create_account_if_non_existent( - &coldkey1, - &hotkey_origin - )); - assert_ok!(SubtensorModule::create_account_if_non_existent( - &coldkey2, - &hotkey_destination - )); - - // Add coldkey2 stake - add_balance_to_coldkey_account(&coldkey2, 100_000_000_000u64.into()); - SubtensorModule::stake_into_subnet( - &hotkey_origin, - &coldkey2, - netuid, - 50_000_000_000u64.into(), - ::SwapInterface::max_price(), - false, - ) - .unwrap(); - DecayingLock::::insert(coldkey2, netuid, false); - - let lock_amount = 5000u64.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey1, - netuid, - &hotkey_origin, - lock_amount - )); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey2, - netuid, - &hotkey_origin, - lock_amount - )); - - // Mock a non-zero conviction - let mut lock1 = Lock::::get((coldkey1, netuid, hotkey_origin)).unwrap(); - lock1.conviction = U64F64::from_num(1000); - Lock::::insert((coldkey1, netuid, hotkey_origin), lock1); - let mut lock2 = Lock::::get((coldkey2, netuid, hotkey_origin)).unwrap(); - lock2.conviction = U64F64::from_num(1000); - Lock::::insert((coldkey2, netuid, hotkey_origin), lock2); - let mut hotkey_lock = HotkeyLock::::get(netuid, hotkey_origin).unwrap(); - hotkey_lock.conviction = U64F64::from_num(2000); - HotkeyLock::::insert(netuid, hotkey_origin, hotkey_lock); - - // Move lock for coldkey1 to hotkey_destination, coldkey2's lock should be unaffected - assert_ok!(SubtensorModule::move_lock( - RuntimeOrigin::signed(coldkey1), - hotkey_destination, - netuid, - )); - let lock1_after = Lock::::get((coldkey1, netuid, hotkey_destination)).unwrap(); - let lock2_after = Lock::::get((coldkey2, netuid, hotkey_origin)).unwrap(); - assert_eq!(lock1_after.locked_mass, lock_amount); - assert_eq!(lock1_after.conviction, U64F64::from_num(0)); - assert_eq!(lock2_after.locked_mass, lock_amount); - assert_eq!(lock2_after.conviction, U64F64::from_num(1000)); - - // Hotkey lock is removed on origin and added on destination - let hotkey_lock_origin_after = HotkeyLock::::get(netuid, hotkey_origin).unwrap(); - let hotkey_lock_destination_after = - HotkeyLock::::get(netuid, hotkey_destination).unwrap(); - assert_eq!(hotkey_lock_origin_after.locked_mass, lock_amount); - assert_eq!(hotkey_lock_origin_after.conviction, U64F64::from_num(1000)); - assert_eq!(hotkey_lock_destination_after.locked_mass, lock_amount); - assert_eq!( - hotkey_lock_destination_after.conviction, - U64F64::from_num(0) - ); - }); -} - -#[test] -fn test_moving_partial_lock_same_owners() { - new_test_ext(1).execute_with(|| { - let coldkey1 = U256::from(1); - let coldkey2 = U256::from(2); - let hotkey_origin = U256::from(3); - let hotkey_destination = U256::from(4); - let netuid = setup_subnet_with_stake(coldkey1, hotkey_origin, 100_000_000_000); - - // Add coldkey2 stake - add_balance_to_coldkey_account(&coldkey2, 100_000_000_000u64.into()); - - // Make hotkey_origin and hotkey_destination both owned by coldkey1 - assert_ok!(SubtensorModule::create_account_if_non_existent( - &coldkey1, - &hotkey_origin - )); - assert_ok!(SubtensorModule::create_account_if_non_existent( - &coldkey1, - &hotkey_destination - )); - SubtensorModule::stake_into_subnet( - &hotkey_origin, - &coldkey2, - netuid, - 50_000_000_000u64.into(), - ::SwapInterface::max_price(), - false, - ) - .unwrap(); - DecayingLock::::insert(coldkey2, netuid, false); - - let lock_amount = 5000u64.into(); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey1, - netuid, - &hotkey_origin, - lock_amount - )); - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey2, - netuid, - &hotkey_origin, - lock_amount - )); - - // Mock a non-zero conviction - let mut lock1 = Lock::::get((coldkey1, netuid, hotkey_origin)).unwrap(); - lock1.conviction = U64F64::from_num(1000); - Lock::::insert((coldkey1, netuid, hotkey_origin), lock1); - let mut lock2 = Lock::::get((coldkey2, netuid, hotkey_origin)).unwrap(); - lock2.conviction = U64F64::from_num(1000); - Lock::::insert((coldkey2, netuid, hotkey_origin), lock2); - let mut hotkey_lock = HotkeyLock::::get(netuid, hotkey_origin).unwrap(); - hotkey_lock.conviction = U64F64::from_num(2000); - HotkeyLock::::insert(netuid, hotkey_origin, hotkey_lock); - - // Move lock for coldkey1 to hotkey_destination, coldkey2's lock should be unaffected - assert_ok!(SubtensorModule::move_lock( - RuntimeOrigin::signed(coldkey1), - hotkey_destination, - netuid, - )); - let lock1_after = Lock::::get((coldkey1, netuid, hotkey_destination)).unwrap(); - let lock2_after = Lock::::get((coldkey2, netuid, hotkey_origin)).unwrap(); - assert_eq!(lock1_after.locked_mass, lock_amount); - assert_eq!(lock1_after.conviction, U64F64::from_num(1000)); - assert_eq!(lock2_after.locked_mass, lock_amount); - assert_eq!(lock2_after.conviction, U64F64::from_num(1000)); - - // Hotkey lock is moved to destination with conviction - let hotkey_lock_origin_after = HotkeyLock::::get(netuid, hotkey_origin).unwrap(); - let hotkey_lock_destination_after = - HotkeyLock::::get(netuid, hotkey_destination).unwrap(); - assert_eq!(hotkey_lock_origin_after.locked_mass, lock_amount); - assert_eq!(hotkey_lock_origin_after.conviction, U64F64::from_num(1000)); - assert_eq!(hotkey_lock_destination_after.locked_mass, lock_amount); - assert_eq!( - hotkey_lock_destination_after.conviction, - U64F64::from_num(1000) - ); - }); -} - -#[test] -fn test_hotkey_swap_moves_lock_and_conviction_to_new_hotkey() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let old_hotkey = U256::from(2); - let new_hotkey = U256::from(3); - let netuid = setup_subnet_with_stake(coldkey, old_hotkey, 100_000_000_000); - let lock_amount: AlphaBalance = 5000u64.into(); - let conviction = U64F64::from_num(1000); - - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &old_hotkey, - lock_amount, - )); - - let mut lock = Lock::::get((coldkey, netuid, old_hotkey)).unwrap(); - lock.conviction = conviction; - Lock::::insert((coldkey, netuid, old_hotkey), lock); - - let mut hotkey_lock = HotkeyLock::::get(netuid, old_hotkey).unwrap(); - hotkey_lock.conviction = conviction; - HotkeyLock::::insert(netuid, old_hotkey, hotkey_lock); - - add_balance_to_coldkey_account( - &coldkey, - (SubtensorModule::get_key_swap_cost() + 1000.into()).into(), - ); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - None, - false, - )); - - assert!(Lock::::get((coldkey, netuid, old_hotkey)).is_none()); - assert!(HotkeyLock::::get(netuid, old_hotkey).is_none()); - - let moved_lock = Lock::::get((coldkey, netuid, new_hotkey)).unwrap(); - assert_eq!(moved_lock.locked_mass, lock_amount); - assert_eq!(moved_lock.conviction, conviction); - - let moved_hotkey_lock = HotkeyLock::::get(netuid, new_hotkey).unwrap(); - assert_eq!(moved_hotkey_lock.locked_mass, lock_amount); - assert_eq!(moved_hotkey_lock.conviction, conviction); - assert_eq!( - SubtensorModule::hotkey_conviction(&new_hotkey, netuid), - conviction - ); - }); -} - -#[test] -fn test_swap_hotkey_v2_on_subnet_moves_lock_and_conviction_to_new_hotkey() { - new_test_ext(100).execute_with(|| { - let coldkey = U256::from(1); - let old_hotkey = U256::from(2); - let new_hotkey = U256::from(3); - let netuid = setup_subnet_with_stake(coldkey, old_hotkey, 100_000_000_000); - let lock_amount: AlphaBalance = 5000u64.into(); - let conviction = U64F64::from_num(1000); - - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &old_hotkey, - lock_amount, - )); - - let mut lock = Lock::::get((coldkey, netuid, old_hotkey)).unwrap(); - lock.conviction = conviction; - Lock::::insert((coldkey, netuid, old_hotkey), lock); - - let mut hotkey_lock = HotkeyLock::::get(netuid, old_hotkey).unwrap(); - hotkey_lock.conviction = conviction; - HotkeyLock::::insert(netuid, old_hotkey, hotkey_lock); - - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000u64.into()); - assert_ok!(SubtensorModule::swap_hotkey_v2( - RuntimeOrigin::signed(coldkey), - old_hotkey, - new_hotkey, - Some(netuid), - false, - )); - - assert!(Lock::::get((coldkey, netuid, old_hotkey)).is_none()); - assert!(HotkeyLock::::get(netuid, old_hotkey).is_none()); - - let moved_lock = Lock::::get((coldkey, netuid, new_hotkey)).unwrap(); - assert_eq!(moved_lock.locked_mass, lock_amount); - assert_eq!(moved_lock.conviction, conviction); - - let moved_hotkey_lock = HotkeyLock::::get(netuid, new_hotkey).unwrap(); - assert_eq!(moved_hotkey_lock.locked_mass, lock_amount); - assert_eq!(moved_hotkey_lock.conviction, conviction); - assert_eq!( - SubtensorModule::hotkey_conviction(&new_hotkey, netuid), - conviction - ); - }); -} - -#[test] -fn test_swap_hotkey_v2_on_subnet_does_not_move_locks_on_other_subnets() { - new_test_ext(100).execute_with(|| { - let coldkey = U256::from(1); - let old_hotkey = U256::from(2); - let new_hotkey = U256::from(3); - let swapped_netuid = setup_subnet_with_stake(coldkey, old_hotkey, 100_000_000_000); - let untouched_netuid = setup_subnet_with_stake(coldkey, old_hotkey, 100_000_000_000); - let lock_amount: AlphaBalance = 5000u64.into(); - let conviction = U64F64::from_num(1000); - - for netuid in [swapped_netuid, untouched_netuid] { - assert_ok!(SubtensorModule::do_lock_stake( - &coldkey, - netuid, - &old_hotkey, - lock_amount, - )); - - let mut lock = Lock::::get((coldkey, netuid, old_hotkey)).unwrap(); - lock.conviction = conviction; - Lock::::insert((coldkey, netuid, old_hotkey), lock); - - let mut hotkey_lock = HotkeyLock::::get(netuid, old_hotkey).unwrap(); - hotkey_lock.conviction = conviction; - HotkeyLock::::insert(netuid, old_hotkey, hotkey_lock); - } - - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000u64.into()); - assert_ok!(SubtensorModule::swap_hotkey_v2( - RuntimeOrigin::signed(coldkey), - old_hotkey, - new_hotkey, - Some(swapped_netuid), - false, - )); - - assert!(Lock::::get((coldkey, swapped_netuid, old_hotkey)).is_none()); - assert!(HotkeyLock::::get(swapped_netuid, old_hotkey).is_none()); - assert_eq!( - Lock::::get((coldkey, swapped_netuid, new_hotkey)) - .unwrap() - .conviction, - conviction - ); - assert_eq!( - HotkeyLock::::get(swapped_netuid, new_hotkey) - .unwrap() - .conviction, - conviction - ); - - let untouched_lock = Lock::::get((coldkey, untouched_netuid, old_hotkey)).unwrap(); - assert_eq!(untouched_lock.locked_mass, lock_amount); - assert_eq!(untouched_lock.conviction, conviction); - assert!(Lock::::get((coldkey, untouched_netuid, new_hotkey)).is_none()); - - let untouched_hotkey_lock = HotkeyLock::::get(untouched_netuid, old_hotkey).unwrap(); - assert_eq!(untouched_hotkey_lock.locked_mass, lock_amount); - assert_eq!(untouched_hotkey_lock.conviction, conviction); - assert!(HotkeyLock::::get(untouched_netuid, new_hotkey).is_none()); - }); -} diff --git a/pallets/subtensor/src/tests/locks/account_flags_reject_locked_alpha.rs b/pallets/subtensor/src/tests/locks/account_flags_reject_locked_alpha.rs new file mode 100644 index 0000000000..f6d68a2b31 --- /dev/null +++ b/pallets/subtensor/src/tests/locks/account_flags_reject_locked_alpha.rs @@ -0,0 +1,43 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! AccountFlags reject-locked-alpha defaults and fee path. + +use super::prelude::*; + +#[test] +fn test_account_flags_default_to_zero_and_reject_locked_alpha_setter_pays_fee() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + + assert_eq!(AccountFlags::::get(coldkey), 0); + assert!(!AccountFlags::::contains_key(coldkey)); + assert!(SubtensorModule::account_rejects_locked_alpha(&coldkey)); + + let call = + RuntimeCall::SubtensorModule(crate::Call::set_reject_locked_alpha { enabled: true }); + assert_eq!(call.get_dispatch_info().pays_fee, Pays::Yes); + + assert_ok!(SubtensorModule::set_reject_locked_alpha( + RuntimeOrigin::signed(coldkey), + false, + )); + assert_eq!( + AccountFlags::::get(coldkey), + ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA + ); + assert!(AccountFlags::::contains_key(coldkey)); + assert!(!SubtensorModule::account_rejects_locked_alpha(&coldkey)); + + assert_ok!(SubtensorModule::set_reject_locked_alpha( + RuntimeOrigin::signed(coldkey), + true, + )); + assert_eq!(AccountFlags::::get(coldkey), 0); + assert!(!AccountFlags::::contains_key(coldkey)); + assert!(SubtensorModule::account_rejects_locked_alpha(&coldkey)); + }); +} diff --git a/pallets/subtensor/src/tests/locks/clear_small_nomination_lock.rs b/pallets/subtensor/src/tests/locks/clear_small_nomination_lock.rs new file mode 100644 index 0000000000..2a3df6f731 --- /dev/null +++ b/pallets/subtensor/src/tests/locks/clear_small_nomination_lock.rs @@ -0,0 +1,190 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Clear small nomination checks lock. + +use super::helpers::*; +use super::prelude::*; + +// ========================================================================= +// GROUP 16: Clear small nomination checks lock +// ========================================================================= + +#[test] +fn test_clear_small_nomination_checks_lock() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(100); + let owner_hotkey = U256::from(101); + let netuid = setup_subnet_with_stake(owner_coldkey, owner_hotkey, 100_000_000_000); + + // Set up a nominator (different coldkey, does NOT own the hotkey) + let nominator = U256::from(200); + add_balance_to_coldkey_account(&nominator, 100_000_000_000u64.into()); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &nominator, + &owner_hotkey + )); + SubtensorModule::stake_into_subnet( + &owner_hotkey, + &nominator, + netuid, + 50_000_000_000u64.into(), + ::SwapInterface::max_price(), + false, + ) + .unwrap(); + + let nominator_alpha = get_alpha(&owner_hotkey, &nominator, netuid); + assert!(nominator_alpha > AlphaBalance::ZERO); + + // Nominator locks their full stake + let nominator_total = SubtensorModule::total_coldkey_alpha_on_subnet(&nominator, netuid); + assert_ok!(SubtensorModule::do_lock_stake( + &nominator, + netuid, + &owner_hotkey, + nominator_total, + )); + + // Set a high nominator min stake so the current stake is "small" + SubtensorModule::set_nominator_min_required_stake(u64::MAX); + + // clear_small_nomination removes the lock and unstakes alpha + SubtensorModule::clear_small_nomination_if_required(&owner_hotkey, &nominator, netuid); + + // Nominator alpha has been removed despite lock + let nominator_alpha_after = get_alpha(&owner_hotkey, &nominator, netuid); + assert_eq!(nominator_alpha_after, AlphaBalance::ZERO); + + // Lock entry doesn't exist anymore + assert!( + Lock::::iter_prefix((nominator, netuid)) + .next() + .is_none() + ); + + // Hotkey lock should also be removed + let hotkey_lock = HotkeyLock::::get(netuid, owner_hotkey); + assert!(hotkey_lock.is_none()); + }); +} + +#[test] +// If one coldkey has a large nomination on one hotkey and a tiny nomination on another, +// clearing the tiny nomination should reduce the lock state only by that tiny alpha amount. +fn test_clear_small_nomination_reduces_only_tiny_amount_from_lock_state() { + new_test_ext(1).execute_with(|| { + // Large stake, subnet owner, and large lock receiver + let coldkey_large = U256::from(100); + let hotkey_large = U256::from(101); + let netuid = setup_subnet_with_stake(coldkey_large, hotkey_large, 100_000_000_000); + + let coldkey_tiny = U256::from(102); + let hotkey_tiny = U256::from(103); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &coldkey_tiny, + &hotkey_tiny + )); + + // Coldkey that is going to stake and lock + let nominator = U256::from(200); + let large_tao = TaoBalance::from(50_000_000_000u64); + let tiny_tao = TaoBalance::from(1_000_000u64); + add_balance_to_coldkey_account(&nominator, large_tao + tiny_tao); + + // Create one large nomination and one tiny nomination on the same subnet. + SubtensorModule::stake_into_subnet( + &hotkey_large, + &nominator, + netuid, + large_tao, + ::SwapInterface::max_price(), + false, + ) + .unwrap(); + SubtensorModule::stake_into_subnet( + &hotkey_tiny, + &nominator, + netuid, + tiny_tao, + ::SwapInterface::max_price(), + false, + ) + .unwrap(); + DecayingLock::::insert(nominator, netuid, false); + + let large_alpha_before = get_alpha(&hotkey_large, &nominator, netuid); + let tiny_alpha_before = get_alpha(&hotkey_tiny, &nominator, netuid); + assert!(large_alpha_before > tiny_alpha_before); + + // Lock against the large nomination hotkey and seed non-zero unlocked_mass + conviction + // so we can verify each field is reduced only by the tiny nomination's alpha amount. + let total_before = SubtensorModule::total_coldkey_alpha_on_subnet(&nominator, netuid); + assert_ok!(SubtensorModule::do_lock_stake( + &nominator, + netuid, + &hotkey_large, + total_before, + )); + + let conviction_before = U64F64::from_num(tiny_alpha_before.to_u64() + 2_000); + let last_update = SubtensorModule::get_current_block_as_u64(); + Lock::::insert( + (nominator, netuid, hotkey_large), + LockState { + locked_mass: total_before, + conviction: conviction_before, + last_update, + }, + ); + HotkeyLock::::insert( + netuid, + hotkey_large, + LockState { + locked_mass: total_before, + conviction: conviction_before, + last_update, + }, + ); + + // Force the tiny nomination to qualify as "small" and clear only that nomination. + SubtensorModule::set_nominator_min_required_stake(u64::MAX); + SubtensorModule::clear_small_nomination_if_required(&hotkey_tiny, &nominator, netuid); + + // The large nomination stays, the tiny one is removed. + let large_alpha_after = get_alpha(&hotkey_large, &nominator, netuid); + let tiny_alpha_after = get_alpha(&hotkey_tiny, &nominator, netuid); + assert_eq!(large_alpha_after, large_alpha_before); + assert!(!large_alpha_after.is_zero()); + assert_eq!(tiny_alpha_after, AlphaBalance::ZERO); + + // Only the tiny alpha amount should be shaved off the coldkey lock state. + // Conviction is reduced proportionally + let lock_after = Lock::::get((nominator, netuid, hotkey_large)).unwrap(); + assert!(!lock_after.locked_mass.is_zero()); + assert_eq!(lock_after.locked_mass, total_before - tiny_alpha_before); + assert!(lock_after.conviction != U64F64::from_num(0)); + let expected_conviction = conviction_before.to_num::() + * (1. - u64::from(tiny_alpha_before) as f64 / u64::from(total_before) as f64); + assert_abs_diff_eq!( + lock_after.conviction.to_num::(), + expected_conviction, + epsilon = expected_conviction / 1000000. + ); + + // The aggregate hotkey lock on the locked hotkey should also only shrink by the tiny amount. + let hotkey_lock_after = HotkeyLock::::get(netuid, hotkey_large).unwrap(); + assert_eq!( + hotkey_lock_after.locked_mass, + total_before - tiny_alpha_before + ); + assert_abs_diff_eq!( + hotkey_lock_after.conviction.to_num::(), + expected_conviction, + epsilon = expected_conviction / 1000000. + ); + }); +} diff --git a/pallets/subtensor/src/tests/locks/coldkey_swap_lock.rs b/pallets/subtensor/src/tests/locks/coldkey_swap_lock.rs new file mode 100644 index 0000000000..2e9b7e8c03 --- /dev/null +++ b/pallets/subtensor/src/tests/locks/coldkey_swap_lock.rs @@ -0,0 +1,321 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Coldkey swap interaction. + +use super::helpers::*; +use super::prelude::*; + +// ========================================================================= +// GROUP 11: Coldkey swap interaction +// ========================================================================= + +#[test] +fn test_coldkey_swap_swaps_lock() { + new_test_ext(1).execute_with(|| { + let old_coldkey = U256::from(1); + let new_coldkey = U256::from(10); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(old_coldkey, hotkey, 100_000_000_000); + + assert_ok!(SubtensorModule::do_lock_stake( + &old_coldkey, + netuid, + &hotkey, + 5000u64.into(), + )); + assert_ok!(SubtensorModule::set_reject_locked_alpha( + RuntimeOrigin::signed(new_coldkey), + false, + )); + + // Perform coldkey swap + assert_ok!(SubtensorModule::perform_coldkey_swap(&old_coldkey, &new_coldkey)); + + // Lock removed on old coldkey + assert!( + Lock::::iter_prefix((old_coldkey, netuid)) + .next() + .is_none() + ); + assert!(!DecayingLock::::contains_key(old_coldkey, netuid)); + // New coldkey now has the lock + assert!(Lock::::get((new_coldkey, netuid, hotkey)).is_some()); + assert_eq!(DecayingLock::::get(new_coldkey, netuid), Some(false)); + assert!(HotkeyLock::::contains_key(netuid, hotkey)); + assert!(!DecayingHotkeyLock::::contains_key(netuid, hotkey)); + }); +} + +#[test] +fn test_coldkey_swap_lock_blocks_unstake() { + new_test_ext(1).execute_with(|| { + let old_coldkey = U256::from(1); + let new_coldkey = U256::from(10); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(old_coldkey, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&old_coldkey, netuid); + assert_ok!(SubtensorModule::do_lock_stake( + &old_coldkey, + netuid, + &hotkey, + total, + )); + assert_ok!(SubtensorModule::set_reject_locked_alpha( + RuntimeOrigin::signed(new_coldkey), + false, + )); + + // Swap coldkey + assert_ok!(SubtensorModule::perform_coldkey_swap(&old_coldkey, &new_coldkey)); + + step_block(1); + + // New coldkey should not be able to unstake + let alpha = get_alpha(&hotkey, &new_coldkey, netuid); + assert!(alpha > AlphaBalance::ZERO); + assert_noop!( + SubtensorModule::do_remove_stake( + RuntimeOrigin::signed(new_coldkey), + hotkey, + netuid, + alpha, + ), + Error::::StakeUnavailable + ); + }); +} + +#[test] +// Conviction-only destination lock state is not active, so direct coldkey lock transfer is allowed. +fn test_coldkey_swap_allows_destination_conviction_only_lock() { + new_test_ext(1).execute_with(|| { + let old_coldkey = U256::from(1); + let new_coldkey = U256::from(10); + let old_hotkey = U256::from(2); + let new_hotkey = U256::from(20); + let netuid = subtensor_runtime_common::NetUid::from(1); + + let old_conviction = U64F64::from_num(777); + let new_conviction = U64F64::from_num(111); + + SubtensorModule::insert_lock_state( + &old_coldkey, + netuid, + &old_hotkey, + LockState { + locked_mass: AlphaBalance::ZERO, + conviction: old_conviction, + last_update: SubtensorModule::get_current_block_as_u64(), + }, + ); + DecayingLock::::insert(old_coldkey, netuid, false); + SubtensorModule::insert_lock_state( + &new_coldkey, + netuid, + &new_hotkey, + LockState { + locked_mass: AlphaBalance::ZERO, + conviction: new_conviction, + last_update: SubtensorModule::get_current_block_as_u64(), + }, + ); + + assert_ok!(SubtensorModule::swap_coldkey_locks( + &old_coldkey, + &new_coldkey + )); + + assert!( + Lock::::iter_prefix((old_coldkey, netuid)) + .next() + .is_none() + ); + assert!(Lock::::get((new_coldkey, netuid, new_hotkey)).is_some()); + + let swapped_lock = Lock::::get((new_coldkey, netuid, old_hotkey)) + .expect("source lock should be transferred"); + assert_eq!(swapped_lock.locked_mass, AlphaBalance::ZERO); + assert_eq!(swapped_lock.conviction, old_conviction); + assert_eq!(Lock::::iter_prefix((new_coldkey, netuid)).count(), 2); + assert!(DecayingLock::::get(old_coldkey, netuid).is_none()); + assert_eq!(DecayingLock::::get(new_coldkey, netuid), Some(false)); + }); +} + +#[test] +// When the destination already has an active lock, coldkey lock transfer should fail +// before mutating either coldkey's lock state. +fn test_coldkey_swap_rejects_destination_lock() { + new_test_ext(1).execute_with(|| { + let old_coldkey = U256::from(1); + let new_coldkey = U256::from(10); + let old_hotkey = U256::from(2); + let new_hotkey = U256::from(20); + let netuid = subtensor_runtime_common::NetUid::from(1); + + let old_locked = AlphaBalance::from(7_000u64); + let old_conviction = U64F64::from_num(77); + + let new_locked = AlphaBalance::from(999u64); + let new_conviction = U64F64::from_num(11); + + SubtensorModule::insert_lock_state( + &old_coldkey, + netuid, + &old_hotkey, + LockState { + locked_mass: old_locked, + conviction: old_conviction, + last_update: SubtensorModule::get_current_block_as_u64(), + }, + ); + SubtensorModule::insert_lock_state( + &new_coldkey, + netuid, + &new_hotkey, + LockState { + locked_mass: new_locked, + conviction: new_conviction, + last_update: SubtensorModule::get_current_block_as_u64(), + }, + ); + + assert_noop!( + SubtensorModule::swap_coldkey_locks(&old_coldkey, &new_coldkey), + Error::::ActiveLockExists + ); + + let source_lock = Lock::::get((old_coldkey, netuid, old_hotkey)) + .expect("source lock should remain after failed transfer"); + assert_eq!(source_lock.locked_mass, old_locked); + assert_eq!(source_lock.conviction, old_conviction); + let destination_lock = Lock::::get((new_coldkey, netuid, new_hotkey)) + .expect("destination lock should remain after failed transfer"); + assert_eq!(destination_lock.locked_mass, new_locked); + assert_eq!(destination_lock.conviction, new_conviction); + assert!( + Lock::::get((new_coldkey, netuid, old_hotkey)).is_none(), + "source lock should not be inserted under destination coldkey" + ); + assert_eq!(Lock::::iter_prefix((new_coldkey, netuid)).count(), 1); + }); +} + +#[test] +fn test_coldkey_swap_rejects_locked_alpha_to_flagged_destination() { + new_test_ext(1).execute_with(|| { + let old_coldkey = U256::from(1); + let new_coldkey = U256::from(10); + let old_hotkey = U256::from(2); + let netuid = subtensor_runtime_common::NetUid::from(1); + + let old_locked = AlphaBalance::from(7_000u64); + let old_conviction = U64F64::from_num(77); + + SubtensorModule::insert_lock_state( + &old_coldkey, + netuid, + &old_hotkey, + LockState { + locked_mass: old_locked, + conviction: old_conviction, + last_update: SubtensorModule::get_current_block_as_u64(), + }, + ); + DecayingLock::::insert(old_coldkey, netuid, false); + assert_ok!(SubtensorModule::set_reject_locked_alpha( + RuntimeOrigin::signed(new_coldkey), + true, + )); + + assert_noop!( + SubtensorModule::swap_coldkey_locks(&old_coldkey, &new_coldkey), + Error::::AccountRejectsLockedAlpha + ); + + let source_lock = Lock::::get((old_coldkey, netuid, old_hotkey)) + .expect("source lock should remain after failed transfer"); + assert_eq!(source_lock.locked_mass, old_locked); + assert_eq!(source_lock.conviction, old_conviction); + assert!( + Lock::::iter_prefix((new_coldkey, netuid)) + .next() + .is_none() + ); + assert_eq!(DecayingLock::::get(old_coldkey, netuid), Some(false)); + assert!(DecayingLock::::get(new_coldkey, netuid).is_none()); + }); +} + +#[test] +// The public coldkey swap extrinsic runs inside a storage layer, so a late failure rolls back the earlier writes. +fn test_failed_coldkey_swap_extrinsic_rolls_back_state_changes() { + new_test_ext(1).execute_with(|| { + let old_coldkey = U256::from(1); + let old_hotkey = U256::from(2); + let new_coldkey = U256::from(3); + let blocked_hotkey = U256::from(4); + let netuid = setup_subnet_with_stake(old_coldkey, old_hotkey, 100_000_000_000); + + let original_stake = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &old_hotkey, + &old_coldkey, + netuid, + ); + assert!(!original_stake.is_zero()); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &old_hotkey, + &new_coldkey, + netuid + ), + AlphaBalance::ZERO + ); + + // Seed a lock directly on the destination coldkey so the swap reaches ActiveLockExists + // without tripping the earlier "already associated" guard. + SubtensorModule::insert_lock_state( + &new_coldkey, + netuid, + &blocked_hotkey, + LockState { + locked_mass: 1_000u64.into(), + conviction: U64F64::from_num(0), + last_update: SubtensorModule::get_current_block_as_u64(), + }, + ); + + assert_noop!( + SubtensorModule::swap_coldkey( + RuntimeOrigin::root(), + old_coldkey, + new_coldkey, + TaoBalance::ZERO, + ), + Error::::ActiveLockExists + ); + + // The failed extrinsic should roll back the earlier stake transfer. + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &old_hotkey, + &old_coldkey, + netuid + ), + original_stake + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &old_hotkey, + &new_coldkey, + netuid + ), + AlphaBalance::ZERO + ); + }); +} diff --git a/pallets/subtensor/src/tests/locks/conviction_roll_forward.rs b/pallets/subtensor/src/tests/locks/conviction_roll_forward.rs new file mode 100644 index 0000000000..98f6b08515 --- /dev/null +++ b/pallets/subtensor/src/tests/locks/conviction_roll_forward.rs @@ -0,0 +1,447 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! ConvictionModel roll-forward math. + +use super::helpers::*; +use super::prelude::*; + +// ========================================================================= +// GROUP 5: ConvictionModel roll-forward math +// ========================================================================= + +#[test] +fn test_exp_decay_zero_dt() { + new_test_ext(1).execute_with(|| { + let result = ConvictionModel::exp_decay(0, 216000); + assert_eq!(result, U64F64::from_num(1)); + }); +} + +#[test] +fn test_exp_decay_zero_tau() { + new_test_ext(1).execute_with(|| { + let result = ConvictionModel::exp_decay(1000, 0); + assert_eq!(result, U64F64::from_num(0)); + }); +} + +#[test] +fn test_exp_decay_one_tau() { + new_test_ext(1).execute_with(|| { + let tau = 216000u64; + let result = ConvictionModel::exp_decay(tau, tau); + // exp(-1) ~= 0.36787944 + let expected = U64F64::from_num(0.36787944f64); + let diff = if result > expected { + result - expected + } else { + expected - result + }; + assert!(diff < U64F64::from_num(0.001)); + }); +} + +#[test] +fn test_exp_decay_clamps_large_dt_to_min_ratio() { + new_test_ext(1).execute_with(|| { + let tau = 216000u64; + let clamped_result = ConvictionModel::exp_decay(40 * tau, tau); + let oversized_result = ConvictionModel::exp_decay(100 * tau, tau); + + let diff = if oversized_result > clamped_result { + oversized_result - clamped_result + } else { + clamped_result - oversized_result + }; + + assert!(diff < U64F64::from_num(0.000000001)); + assert!(oversized_result > U64F64::from_num(0)); + }); +} + +#[test] +fn test_roll_forward_individual_lock_uses_lock_owner_and_decay_mode() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + let owner_hotkey = SubnetOwnerHotkey::::get(netuid); + DecayingLock::::remove(coldkey, netuid); + + let lock = LockState { + locked_mass: 10_000u64.into(), + conviction: U64F64::from_num(0), + last_update: 0, + }; + let now = 1_000u64; + + let rolled = + roll_forward_individual_lock(&coldkey, netuid, &owner_hotkey, lock.clone(), now); + let expected = ConvictionModel::roll_forward_lock( + lock, + now, + UnlockRate::::get(), + MaturityRate::::get(), + true, + false, + ) + .0; + + assert_eq!(rolled, expected); + }); +} + +#[test] +fn test_roll_forward_hotkey_lock_uses_perpetual_general_mode() { + new_test_ext(1).execute_with(|| { + let lock = LockState { + locked_mass: 10_000u64.into(), + conviction: U64F64::from_num(0), + last_update: 0, + }; + let now = 1_000u64; + + let rolled = roll_forward_hotkey_lock(lock.clone(), now); + let expected = ConvictionModel::roll_forward_lock( + lock, + now, + UnlockRate::::get(), + MaturityRate::::get(), + false, + true, + ) + .0; + + assert_eq!(rolled, expected); + }); +} + +#[test] +fn test_roll_forward_decaying_hotkey_lock_uses_decaying_general_mode() { + new_test_ext(1).execute_with(|| { + let lock = LockState { + locked_mass: 10_000u64.into(), + conviction: U64F64::from_num(0), + last_update: 0, + }; + let now = 1_000u64; + + let rolled = roll_forward_decaying_hotkey_lock(lock.clone(), now); + let expected = ConvictionModel::roll_forward_lock( + lock, + now, + UnlockRate::::get(), + MaturityRate::::get(), + false, + false, + ) + .0; + + assert_eq!(rolled, expected); + }); +} + +#[test] +fn test_roll_forward_locked_mass_decays() { + new_test_ext(1).execute_with(|| { + let lock_amount = 10000u64; + let lock = LockState { + locked_mass: lock_amount.into(), + conviction: U64F64::from_num(0), + last_update: 0, + }; + let rolled = roll_forward_lock(lock, UnlockRate::::get(), false, false); + + assert!(rolled.locked_mass < lock_amount.into()); + assert!(rolled.locked_mass > AlphaBalance::ZERO); + }); +} + +#[test] +fn test_roll_forward_conviction_uses_unequal_rate_closed_form() { + new_test_ext(1).execute_with(|| { + let locked_mass = 10_000u64; + let dt = 10_000u64; + let unlock_rate = 200_000u64; + let maturity_rate = 240_000u64; + UnlockRate::::set(unlock_rate); + MaturityRate::::set(maturity_rate); + assert_ne!(unlock_rate, maturity_rate); + + let lock = LockState { + locked_mass: locked_mass.into(), + conviction: U64F64::from_num(0), + last_update: 0, + }; + let rolled = roll_forward_lock(lock, dt, false, false); + + let unlock_decay = ConvictionModel::exp_decay(dt, unlock_rate); + let maturity_decay = ConvictionModel::exp_decay(dt, maturity_rate); + let gamma = U64F64::from_num(unlock_rate) + .saturating_mul(maturity_decay.saturating_sub(unlock_decay)) + .safe_div(U64F64::from_num(maturity_rate.saturating_sub(unlock_rate))); + let expected = U64F64::from_num(locked_mass).saturating_mul(gamma); + + assert_abs_diff_eq!( + rolled.conviction.to_num::(), + expected.to_num::(), + epsilon = 0.0000001 + ); + }); +} + +#[test] +fn test_roll_forward_adjacent_large_rates_and_large_mass_match_f64_closed_form() { + new_test_ext(1).execute_with(|| { + let unlock_rate = 1_142_108u64; + let maturity_rate = unlock_rate + 1; + let locked_mass = 21_000_000_000_000_000u64; + let dt = unlock_rate; + UnlockRate::::put(unlock_rate); + MaturityRate::::put(maturity_rate); + + let lock = LockState { + locked_mass: locked_mass.into(), + conviction: U64F64::from_num(0), + last_update: 0, + }; + let rolled = roll_forward_lock(lock, dt, false, false); + + let decay_x = (-(dt as f64) / unlock_rate as f64).exp(); + let decay_z = (-(dt as f64) / maturity_rate as f64).exp(); + let gamma = + unlock_rate as f64 * (decay_x - decay_z) / (unlock_rate as f64 - maturity_rate as f64); + let expected_conviction = locked_mass as f64 * gamma; + let expected_locked_mass = locked_mass as f64 * decay_x; + + assert_abs_diff_eq!( + rolled.conviction.to_num::(), + expected_conviction, + epsilon = 50_000.0 + ); + assert_abs_diff_eq!( + u64::from(rolled.locked_mass) as f64, + expected_locked_mass, + epsilon = 2_000.0 + ); + }); +} + +#[test] +fn test_roll_forward_scales_linearly_with_locked_mass() { + new_test_ext(1).execute_with(|| { + let dt = 25_000u64; + let base_mass = 10_000u64; + let base = LockState { + locked_mass: base_mass.into(), + conviction: U64F64::from_num(0), + last_update: 0, + }; + let double = LockState { + locked_mass: (base_mass * 2).into(), + conviction: U64F64::from_num(0), + last_update: 0, + }; + + let rolled_base = roll_forward_lock(base, dt, false, false); + let rolled_double = roll_forward_lock(double, dt, false, false); + + assert_abs_diff_eq!( + u64::from(rolled_double.locked_mass) as f64, + (u64::from(rolled_base.locked_mass) * 2) as f64, + epsilon = 1.0 + ); + assert_abs_diff_eq!( + rolled_double.conviction.to_num::(), + rolled_base.conviction.to_num::() * 2.0, + epsilon = 0.0000001 + ); + }); +} + +#[test] +fn test_roll_forward_chunked_update_matches_single_update() { + new_test_ext(1).execute_with(|| { + let lock = LockState { + locked_mass: 1_000_000_000u64.into(), + conviction: U64F64::from_num(0), + last_update: 0, + }; + let mid = 10_000u64; + let end = 20_000u64; + + let rolled_once = roll_forward_lock(lock.clone(), end, false, false); + let rolled_twice = roll_forward_lock( + roll_forward_lock(lock, mid, false, false), + end, + false, + false, + ); + + assert_abs_diff_eq!( + u64::from(rolled_twice.locked_mass) as f64, + u64::from(rolled_once.locked_mass) as f64, + epsilon = 1.0 + ); + assert_abs_diff_eq!( + rolled_twice.conviction.to_num::(), + rolled_once.conviction.to_num::(), + epsilon = 0.1 + ); + }); +} + +#[test] +fn test_roll_forward_conviction_stays_below_original_mass_for_one_shot_lock() { + new_test_ext(1).execute_with(|| { + let locked_mass = 10_000u64; + let lock = LockState { + locked_mass: locked_mass.into(), + conviction: U64F64::from_num(0), + last_update: 0, + }; + let cap = U64F64::from_num(locked_mass); + + for dt in [ + 1_000u64, + 10_000u64, + UnlockRate::::get(), + MaturityRate::::get(), + MaturityRate::::get().saturating_mul(5), + ] { + let rolled = roll_forward_lock(lock.clone(), dt, false, false); + assert!(rolled.conviction <= cap); + } + }); +} + +#[test] +fn test_roll_forward_decaying_conviction_peak_is_below_original_lock() { + new_test_ext(1).execute_with(|| { + UnlockRate::::set(200_000u64); + MaturityRate::::set(240_000u64); + + let locked_mass = 10_000u64; + let unlock_rate = UnlockRate::::get() as f64; + let maturity_rate = MaturityRate::::get() as f64; + assert_ne!(unlock_rate, maturity_rate); + + let peak_block = ((unlock_rate * maturity_rate) / (unlock_rate - maturity_rate) + * (unlock_rate / maturity_rate).ln()) + .round() as u64; + let lock = LockState { + locked_mass: locked_mass.into(), + conviction: U64F64::from_num(0), + last_update: 0, + }; + + let rolled = roll_forward_lock(lock, peak_block, false, false); + + assert!(rolled.conviction < U64F64::from_num(locked_mass)); + }); +} + +#[test] +fn test_roll_forward_perpetual_mass_does_not_decay_and_conviction_matures() { + new_test_ext(1).execute_with(|| { + let locked_mass = 10_000u64; + let lock = LockState { + locked_mass: locked_mass.into(), + conviction: U64F64::from_num(0), + last_update: 0, + }; + + let rolled = roll_forward_lock(lock, MaturityRate::::get(), false, true); + + assert_eq!(rolled.locked_mass, locked_mass.into()); + assert!(rolled.conviction > U64F64::from_num(0)); + assert!(rolled.conviction < U64F64::from_num(locked_mass)); + }); +} + +#[test] +fn test_roll_forward_perpetual_conviction_never_exceeds_lock() { + new_test_ext(1).execute_with(|| { + let locked_mass = 10_000u64; + let lock = LockState { + locked_mass: locked_mass.into(), + conviction: U64F64::from_num(0), + last_update: 0, + }; + + for dt in [ + 1u64, + 1_000u64, + MaturityRate::::get(), + MaturityRate::::get().saturating_mul(10), + MaturityRate::::get().saturating_mul(1_000), + ] { + let rolled = roll_forward_lock(lock.clone(), dt, false, true); + assert_eq!(rolled.locked_mass, locked_mass.into()); + assert!(rolled.conviction <= U64F64::from_num(locked_mass)); + } + }); +} + +#[test] +fn test_roll_forward_conviction_converges_to_zero() { + new_test_ext(1).execute_with(|| { + let lock_amount = 10000u64; + let lock = LockState { + locked_mass: lock_amount.into(), + conviction: U64F64::from_num(0), + last_update: 0, + }; + + let c0 = lock.conviction; + assert_eq!(c0, U64F64::from_num(0)); + + let rolled = roll_forward_lock(lock.clone(), 100, false, false); + let c1 = rolled.conviction; + assert!(c1 > U64F64::from_num(0)); + + let rolled = roll_forward_lock(lock.clone(), 1_100, false, false); + let c2 = rolled.conviction; + assert!(c2 > c1); + + let tau = MaturityRate::::get(); + let c_late = roll_forward_lock(lock, tau * 1000, false, false).conviction; + assert_abs_diff_eq!(c_late.to_num::(), 0., epsilon = 0.0000001); + }); +} + +#[test] +fn test_roll_forward_normalizes_dust_to_zero() { + new_test_ext(1).execute_with(|| { + let lock = LockState { + locked_mass: 99u64.into(), + conviction: U64F64::from_num(99), + last_update: 100, + }; + + let rolled = roll_forward_lock(lock, 100, false, false); + + assert_eq!(rolled.locked_mass, AlphaBalance::ZERO); + assert_eq!(rolled.conviction, U64F64::from_num(0)); + assert_eq!(rolled.last_update, 100); + }); +} + +#[test] +fn test_roll_forward_no_change_when_now_equals_last_update() { + new_test_ext(1).execute_with(|| { + let lock = LockState { + locked_mass: 5000.into(), + conviction: U64F64::from_num(1234), + last_update: 100, + }; + let rolled = roll_forward_lock(lock.clone(), 100, false, false); + assert_eq!(rolled.locked_mass, lock.locked_mass); + assert_eq!(rolled.conviction, lock.conviction); + assert_eq!(rolled.last_update, 100); + }); +} diff --git a/pallets/subtensor/src/tests/locks/emission_lock.rs b/pallets/subtensor/src/tests/locks/emission_lock.rs new file mode 100644 index 0000000000..143c74505a --- /dev/null +++ b/pallets/subtensor/src/tests/locks/emission_lock.rs @@ -0,0 +1,165 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Emission interaction. + +use super::helpers::*; +use super::prelude::*; + +// ========================================================================= +// GROUP 17: Emission interaction +// ========================================================================= + +#[test] +fn test_emissions_do_not_break_lock_invariant() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total_alpha_before = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + total_alpha_before + )); + + // Simulate emission: directly increase alpha for the hotkey on subnet + // This increases the pool value for all share holders (including our coldkey) + let emission_amount: AlphaBalance = 10_000_000u64.into(); + SubtensorModule::increase_stake_for_hotkey_on_subnet(&hotkey, netuid, emission_amount); + + // After emission, total alpha should increase by emission_amount + let total_alpha_after = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + assert_eq!(total_alpha_after, total_alpha_before + emission_amount); + + // Lock invariant still holds: total_alpha >= locked_mass + let locked = SubtensorModule::get_current_locked(&coldkey, netuid); + assert!(total_alpha_after >= locked); + + // Available becomes emission_amount + let available = SubtensorModule::available_to_unstake(&coldkey, netuid); + assert_eq!(available, emission_amount); + }); +} + +#[test] +fn test_epoch_distribution_auto_locks_owner_cut() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let validator_coldkey = U256::from(1); + let validator_hotkey = U256::from(2); + let miner_coldkey = U256::from(5); + let miner_hotkey = U256::from(6); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + let subnet_tempo = 10; + let stake = 100_000_000_000u64; + + SubtensorModule::set_tempo_unchecked(netuid, subnet_tempo); + SubtensorModule::set_ck_burn(0); + setup_reserves(netuid, (stake * 10_000).into(), (stake * 10_000).into()); + + register_ok_neuron(netuid, validator_hotkey, validator_coldkey, 0); + register_ok_neuron(netuid, miner_hotkey, miner_coldkey, 1); + + add_balance_to_coldkey_account( + &validator_coldkey, + TaoBalance::from(stake) + ExistentialDeposit::get(), + ); + + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(validator_coldkey), + validator_hotkey, + netuid, + stake.into() + )); + + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_max_allowed_validators(netuid, 1); + step_block(subnet_tempo); + SubnetOwnerCut::::set(u16::MAX / 10); + OwnerCutAutoLockEnabled::::insert(netuid, true); + + let owner_uid = + SubtensorModule::get_uid_for_net_and_hotkey(netuid, &subnet_owner_hotkey).unwrap(); + let validator_uid = + SubtensorModule::get_uid_for_net_and_hotkey(netuid, &validator_hotkey).unwrap(); + let miner_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &miner_hotkey).unwrap(); + let uid_count = [ + owner_uid as usize, + validator_uid as usize, + miner_uid as usize, + ] + .into_iter() + .max() + .unwrap() + + 1; + + // Setup YUMA so that the next epoch produces non-zero subnet emissions. + Weights::::insert( + NetUidStorageIndex::from(netuid), + validator_uid, + vec![(miner_uid, 0xFFFF)], + ); + BlockAtRegistration::::set(netuid, owner_uid, 1); + BlockAtRegistration::::set(netuid, validator_uid, 1); + BlockAtRegistration::::set(netuid, miner_uid, 1); + LastUpdate::::set(NetUidStorageIndex::from(netuid), vec![2; uid_count]); + Kappa::::set(netuid, u16::MAX / 5); + ActivityCutoff::::set(netuid, u16::MAX); + let mut validator_permit = vec![false; uid_count]; + validator_permit[validator_uid as usize] = true; + ValidatorPermit::::insert(netuid, validator_permit); + + let owner_stake_before = get_alpha(&subnet_owner_hotkey, &subnet_owner_coldkey, netuid); + assert!( + Lock::::iter_prefix((subnet_owner_coldkey, netuid)) + .next() + .is_none() + ); + + // Advance to the next epoch so owner cut is distributed and auto-locked. + step_epochs(1, netuid); + + let owner_stake_after = get_alpha(&subnet_owner_hotkey, &subnet_owner_coldkey, netuid); + let owner_cut_locked = owner_stake_after - owner_stake_before; + assert!(owner_cut_locked > AlphaBalance::ZERO); + + let owner_lock = Lock::::get((subnet_owner_coldkey, netuid, subnet_owner_hotkey)) + .expect("owner cut should be auto-locked to the subnet owner's hotkey"); + assert_eq!(owner_lock.locked_mass, owner_cut_locked); + }); +} + +#[test] +fn test_auto_lock_owner_cut_is_disabled_by_default_and_can_be_enabled() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = + setup_subnet_with_stake(subnet_owner_coldkey, subnet_owner_hotkey, 100_000_000_000); + let owner_cut: AlphaBalance = 10_000_000u64.into(); + + assert!(!SubtensorModule::get_owner_cut_auto_lock_enabled(netuid)); + SubtensorModule::auto_lock_owner_cut(netuid, owner_cut); + + assert!( + Lock::::iter_prefix((subnet_owner_coldkey, netuid)) + .next() + .is_none() + ); + + OwnerCutAutoLockEnabled::::insert(netuid, true); + assert!(SubtensorModule::get_owner_cut_auto_lock_enabled(netuid)); + SubtensorModule::auto_lock_owner_cut(netuid, owner_cut); + + let owner_lock = Lock::::get((subnet_owner_coldkey, netuid, subnet_owner_hotkey)) + .expect("owner cut should be auto-locked when enabled"); + assert_eq!(owner_lock.locked_mass, owner_cut); + }); +} diff --git a/pallets/subtensor/src/tests/locks/force_reduce_lock.rs b/pallets/subtensor/src/tests/locks/force_reduce_lock.rs new file mode 100644 index 0000000000..79e8e3baed --- /dev/null +++ b/pallets/subtensor/src/tests/locks/force_reduce_lock.rs @@ -0,0 +1,243 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Lock force-reduction. + +use super::helpers::*; +use super::prelude::*; + +// ========================================================================= +// GROUP 10: Lock force-reduction +// ========================================================================= + +#[test] +fn test_reduce_lock_removes_dust() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + let lock_amount = AlphaBalance::from(50u64); + + // Lock a small amount + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount, + )); + + // Advance many taus so everything decays well below dust (100) + let tau = UnlockRate::::get(); + let target = System::block_number() + tau * 50; + System::set_block_number(target); + + // Remove full lock amount + SubtensorModule::force_reduce_lock(&coldkey, netuid, lock_amount); + + assert!(Lock::::get((coldkey, netuid, hotkey)).is_none()); + assert!(HotkeyLock::::get(netuid, hotkey).is_none()); + }); +} + +#[test] +fn test_reduce_lock_partial_reduction() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + let lock_amount = AlphaBalance::from(1_000u64); + let reduce_amount = AlphaBalance::from(400u64); + let now = SubtensorModule::get_current_block_as_u64(); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount, + )); + + let conviction = U64F64::from_num(1_000); + Lock::::insert( + (coldkey, netuid, hotkey), + LockState { + locked_mass: lock_amount, + conviction, + last_update: now, + }, + ); + HotkeyLock::::insert( + netuid, + hotkey, + LockState { + locked_mass: lock_amount, + conviction, + last_update: now, + }, + ); + + SubtensorModule::force_reduce_lock(&coldkey, netuid, reduce_amount); + + let lock = Lock::::get((coldkey, netuid, hotkey)).expect("lock should remain"); + assert_eq!(lock.locked_mass, 600u64.into()); + assert_abs_diff_eq!( + lock.conviction.to_num::(), + 600., + epsilon = 0.0000000001 + ); + + let hotkey_lock = + HotkeyLock::::get(netuid, hotkey).expect("hotkey lock should remain"); + assert_eq!(hotkey_lock.locked_mass, 600u64.into()); + assert_abs_diff_eq!( + hotkey_lock.conviction.to_num::(), + 600., + epsilon = 0.0000000001 + ); + }); +} + +#[test] +fn test_reduce_lock_no_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let netuid = subtensor_runtime_common::NetUid::from(1); + // Should be a no-op, no panic + SubtensorModule::force_reduce_lock(&coldkey, netuid, 100u64.into()); + assert!( + Lock::::iter_prefix((coldkey, netuid)) + .next() + .is_none() + ); + }); +} + +#[test] +fn test_reduce_lock_two_coldkeys() { + new_test_ext(1).execute_with(|| { + let coldkey1 = U256::from(1); + let coldkey2 = U256::from(3); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey1, hotkey, 100_000_000_000); + + // Add stake on coldkey 2 + add_balance_to_coldkey_account(&coldkey2, 100_000_000_000u64.into()); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &coldkey2, &hotkey + )); + SubtensorModule::stake_into_subnet( + &hotkey, + &coldkey2, + netuid, + 100_000_000_000u64.into(), + ::SwapInterface::max_price(), + false, + ) + .unwrap(); + DecayingLock::::insert(coldkey2, netuid, false); + + // Mock a non-zero conviction for both coldkeys + let lock1 = Lock::::get((coldkey1, netuid, hotkey)).unwrap_or(LockState { + locked_mass: 0.into(), + conviction: U64F64::from_num(1234), + last_update: System::block_number(), + }); + let lock2 = Lock::::get((coldkey2, netuid, hotkey)).unwrap_or(LockState { + locked_mass: 0.into(), + conviction: U64F64::from_num(1234), + last_update: System::block_number(), + }); + Lock::::insert((coldkey1, netuid, hotkey), lock1); + Lock::::insert((coldkey2, netuid, hotkey), lock2); + HotkeyLock::::insert( + netuid, + hotkey, + LockState { + locked_mass: 0.into(), + conviction: U64F64::from_num(1234 * 2), + last_update: System::block_number(), + }, + ); + + // Lock a small amount from both coldkeys + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey1, + netuid, + &hotkey, + 50u64.into(), + )); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey2, + netuid, + &hotkey, + 50u64.into(), + )); + + SubtensorModule::force_reduce_lock(&coldkey1, netuid, 50u64.into()); + + // Should only clean up coldkey1's lock, not coldkey2's + assert!( + Lock::::iter_prefix((coldkey1, netuid)) + .next() + .is_none() + ); + assert!(Lock::::get((coldkey2, netuid, hotkey)).is_some()); + + // Hotkey lock should reduce according to coldkey1 lock + let hotkey_lock = HotkeyLock::::get(netuid, hotkey).unwrap(); + assert_eq!(hotkey_lock.locked_mass, 50u64.into()); + + // Conviction should be reduced by coldkey1's lock conviction, + // but not fully reset because coldkey2 still has a lock + assert!(hotkey_lock.conviction == U64F64::from_num(1234)); + }); +} + +#[test] +fn test_force_reduce_lock_does_not_over_reduce_hotkey_lock() { + new_test_ext(1).execute_with(|| { + let coldkey1 = U256::from(1); + let coldkey2 = U256::from(3); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey1, hotkey, 100_000_000_000); + let now = SubtensorModule::get_current_block_as_u64(); + + Lock::::insert( + (coldkey1, netuid, hotkey), + LockState { + locked_mass: 1_000u64.into(), + conviction: U64F64::from_num(1_000), + last_update: now, + }, + ); + Lock::::insert( + (coldkey2, netuid, hotkey), + LockState { + locked_mass: 5_000u64.into(), + conviction: U64F64::from_num(2_000), + last_update: now, + }, + ); + HotkeyLock::::insert( + netuid, + hotkey, + LockState { + locked_mass: 6_000u64.into(), + conviction: U64F64::from_num(3_000), + last_update: now, + }, + ); + + SubtensorModule::force_reduce_lock(&coldkey1, netuid, 2_000u64.into()); + + assert!(Lock::::get((coldkey1, netuid, hotkey)).is_none()); + assert!(Lock::::get((coldkey2, netuid, hotkey)).is_some()); + + let hotkey_lock = + HotkeyLock::::get(netuid, hotkey).expect("hotkey lock should remain"); + assert_eq!(hotkey_lock.locked_mass, 5_000u64.into()); + assert_eq!(hotkey_lock.conviction, U64F64::from_num(2_000)); + }); +} diff --git a/pallets/subtensor/src/tests/locks/helpers.rs b/pallets/subtensor/src/tests/locks/helpers.rs new file mode 100644 index 0000000000..ba4e125c10 --- /dev/null +++ b/pallets/subtensor/src/tests/locks/helpers.rs @@ -0,0 +1,98 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Setup and roll-forward fixtures for stake-lock unit tests. + +use frame_support::assert_ok; +use sp_core::U256; +use subtensor_runtime_common::{AlphaBalance, TaoBalance}; +use subtensor_swap_interface::SwapHandler; + +use super::super::mock::*; +use crate::staking::lock::{ConvictionModel, LockState}; +use crate::*; + +pub(super) fn setup_subnet_with_stake( + coldkey: U256, + hotkey: U256, + stake_tao: u64, +) -> subtensor_runtime_common::NetUid { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + let amount: TaoBalance = (stake_tao).into(); + setup_reserves( + netuid, + (stake_tao * 1_000_000).into(), + (stake_tao * 10_000_000).into(), + ); + + assert_ok!(SubtensorModule::create_account_if_non_existent( + &coldkey, &hotkey + )); + add_balance_to_coldkey_account(&coldkey, amount); + SubtensorModule::stake_into_subnet( + &hotkey, + &coldkey, + netuid, + amount, + ::SwapInterface::max_price(), + false, + ) + .unwrap(); + DecayingLock::::insert(coldkey, netuid, false); + + netuid +} + +pub(super) fn get_alpha( + hotkey: &U256, + coldkey: &U256, + netuid: subtensor_runtime_common::NetUid, +) -> AlphaBalance { + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid) +} + +pub(super) fn roll_forward_lock( + lock: LockState, + now: u64, + owner_lock: bool, + perpetual_lock: bool, +) -> LockState { + ConvictionModel::roll_forward_lock( + lock, + now, + UnlockRate::::get(), + MaturityRate::::get(), + owner_lock, + perpetual_lock, + ) + .0 +} + +pub(super) fn roll_forward_individual_lock( + coldkey: &U256, + netuid: subtensor_runtime_common::NetUid, + hotkey: &U256, + lock: LockState, + now: u64, +) -> LockState { + roll_forward_lock( + lock, + now, + hotkey == &SubnetOwnerHotkey::::get(netuid), + DecayingLock::::get(coldkey, netuid) == Some(false), + ) +} + +pub(super) fn roll_forward_hotkey_lock(lock: LockState, now: u64) -> LockState { + roll_forward_lock(lock, now, false, true) +} + +pub(super) fn roll_forward_decaying_hotkey_lock(lock: LockState, now: u64) -> LockState { + roll_forward_lock(lock, now, false, false) +} diff --git a/pallets/subtensor/src/tests/locks/hotkey_conviction_subnet_king.rs b/pallets/subtensor/src/tests/locks/hotkey_conviction_subnet_king.rs new file mode 100644 index 0000000000..aa7b45e3e5 --- /dev/null +++ b/pallets/subtensor/src/tests/locks/hotkey_conviction_subnet_king.rs @@ -0,0 +1,682 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Hotkey conviction and subnet king. + +use super::helpers::*; +use super::prelude::*; + +// ========================================================================= +// GROUP 9: Hotkey conviction and subnet king +// ========================================================================= + +#[test] +fn test_hotkey_conviction_single_locker() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + 5000u64.into(), + )); + + // Initially conviction is 0 (just created) + let c = SubtensorModule::hotkey_conviction(&hotkey, netuid); + assert_eq!(c, U64F64::from_num(0)); + + // After time, conviction grows + step_block(1000); + let c = SubtensorModule::hotkey_conviction(&hotkey, netuid); + assert!(c > U64F64::from_num(0)); + }); +} + +#[test] +fn test_hotkey_conviction_multiple_lockers() { + new_test_ext(1).execute_with(|| { + let coldkey1 = U256::from(1); + let coldkey2 = U256::from(5); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey1, hotkey, 100_000_000_000); + + // Also give coldkey2 stake on same hotkey + add_balance_to_coldkey_account(&coldkey2, 100_000_000_000u64.into()); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &coldkey2, &hotkey + )); + SubtensorModule::stake_into_subnet( + &hotkey, + &coldkey2, + netuid, + 50_000_000_000u64.into(), + ::SwapInterface::max_price(), + false, + ) + .unwrap(); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey1, + netuid, + &hotkey, + 3000u64.into(), + )); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey2, + netuid, + &hotkey, + 2000u64.into(), + )); + + step_block(500); + + let total_conviction = SubtensorModule::hotkey_conviction(&hotkey, netuid); + let c1 = SubtensorModule::get_conviction(&coldkey1, netuid); + let c2 = SubtensorModule::get_conviction(&coldkey2, netuid); + + // Total conviction should be approximately sum of individual convictions + let diff = if total_conviction > (c1 + c2) { + total_conviction - (c1 + c2) + } else { + (c1 + c2) - total_conviction + }; + assert!(diff < U64F64::from_num(1)); + }); +} + +#[test] +fn test_mixed_perpetual_owner_and_decaying_non_owner_locks_roll_forward() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(1001); + let owner_hotkey = U256::from(1002); + let staker_coldkey = U256::from(1); + let staker_hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(staker_coldkey, staker_hotkey, 100_000_000_000); + + add_balance_to_coldkey_account(&owner_coldkey, 100_000_000_000u64.into()); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &owner_coldkey, + &owner_hotkey + )); + SubtensorModule::stake_into_subnet( + &owner_hotkey, + &owner_coldkey, + netuid, + 100_000_000_000u64.into(), + ::SwapInterface::max_price(), + false, + ) + .unwrap(); + + let owner_lock_amount = AlphaBalance::from(10_000u64); + let staker_lock_amount = AlphaBalance::from(20_000u64); + assert_ok!(SubtensorModule::do_lock_stake( + &owner_coldkey, + netuid, + &owner_hotkey, + owner_lock_amount, + )); + assert_ok!(SubtensorModule::do_lock_stake( + &staker_coldkey, + netuid, + &staker_hotkey, + staker_lock_amount, + )); + assert_ok!(SubtensorModule::do_set_perpetual_lock( + &owner_coldkey, + netuid, + true, + )); + + System::set_block_number(System::block_number() + UnlockRate::::get()); + + let owner_lock = roll_forward_lock( + OwnerLock::::get(netuid).unwrap(), + SubtensorModule::get_current_block_as_u64(), + true, + true, + ); + let staker_lock = roll_forward_lock( + HotkeyLock::::get(netuid, staker_hotkey).unwrap(), + SubtensorModule::get_current_block_as_u64(), + false, + false, + ); + + assert_eq!(owner_lock.locked_mass, owner_lock_amount); + assert_eq!( + owner_lock.conviction, + U64F64::from_num(u64::from(owner_lock_amount)) + ); + assert!(staker_lock.locked_mass < staker_lock_amount); + assert!(staker_lock.conviction > U64F64::from_num(0)); + }); +} + +#[test] +fn test_total_conviction_equals_sum_of_participating_aggregate_convictions() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(1001); + let owner_hotkey = U256::from(1002); + let staker_coldkey = U256::from(1); + let staker_hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(staker_coldkey, staker_hotkey, 100_000_000_000); + + add_balance_to_coldkey_account(&owner_coldkey, 100_000_000_000u64.into()); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &owner_coldkey, + &owner_hotkey + )); + SubtensorModule::stake_into_subnet( + &owner_hotkey, + &owner_coldkey, + netuid, + 100_000_000_000u64.into(), + ::SwapInterface::max_price(), + false, + ) + .unwrap(); + + assert_ok!(SubtensorModule::do_lock_stake( + &owner_coldkey, + netuid, + &owner_hotkey, + 10_000u64.into(), + )); + assert_ok!(SubtensorModule::do_lock_stake( + &staker_coldkey, + netuid, + &staker_hotkey, + 20_000u64.into(), + )); + assert_ok!(SubtensorModule::do_set_perpetual_lock( + &owner_coldkey, + netuid, + true, + )); + + step_block(1_000); + + let owner_conviction = SubtensorModule::hotkey_conviction(&owner_hotkey, netuid); + let staker_conviction = SubtensorModule::hotkey_conviction(&staker_hotkey, netuid); + let expected = owner_conviction.saturating_add(staker_conviction); + let total = SubtensorModule::get_total_conviction(netuid); + let diff = if total > expected { + total - expected + } else { + expected - total + }; + + assert!(diff < U64F64::from_num(1)); + }); +} + +#[test] +fn test_total_conviction_equals_sum_of_individual_lock_convictions_for_many_lockers() { + new_test_ext(1).execute_with(|| { + let first_coldkey = U256::from(1); + let first_hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(first_coldkey, first_hotkey, 100_000_000_000); + + let mut lockers = vec![(first_coldkey, first_hotkey)]; + for i in 1..10u64 { + let coldkey = U256::from(10 + i); + let hotkey = U256::from(100 + (i % 3)); + add_balance_to_coldkey_account(&coldkey, 100_000_000_000u64.into()); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &coldkey, &hotkey + )); + SubtensorModule::stake_into_subnet( + &hotkey, + &coldkey, + netuid, + 50_000_000_000u64.into(), + ::SwapInterface::max_price(), + false, + ) + .unwrap(); + lockers.push((coldkey, hotkey)); + } + + for (index, (coldkey, hotkey)) in lockers.iter().enumerate() { + assert_ok!(SubtensorModule::do_lock_stake( + coldkey, + netuid, + hotkey, + AlphaBalance::from(1_000u64 + index as u64), + )); + } + + step_block(1_000); + + let now = SubtensorModule::get_current_block_as_u64(); + let individual_sum = Lock::::iter() + .filter(|((_coldkey, lock_netuid, _hotkey), _lock)| *lock_netuid == netuid) + .map(|((coldkey, _netuid, hotkey), lock)| { + roll_forward_individual_lock(&coldkey, netuid, &hotkey, lock, now).conviction + }) + .fold(U64F64::from_num(0), |acc, conviction| { + acc.saturating_add(conviction) + }); + let total = SubtensorModule::get_total_conviction(netuid); + let diff = if total > individual_sum { + total - individual_sum + } else { + individual_sum - total + }; + + assert!(diff < U64F64::from_num(1)); + }); +} + +#[test] +fn test_subnet_king_single_hotkey() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + 5000u64.into(), + )); + + step_block(100); + + let king = SubtensorModule::subnet_king(netuid); + assert_eq!(king, Some(hotkey)); + }); +} + +#[test] +fn test_subnet_king_highest_conviction_wins() { + new_test_ext(1).execute_with(|| { + let coldkey1 = U256::from(1); + let coldkey2 = U256::from(5); + let hotkey_a = U256::from(2); + let hotkey_b = U256::from(3); + + let netuid = setup_subnet_with_stake(coldkey1, hotkey_a, 100_000_000_000); + + add_balance_to_coldkey_account(&coldkey2, 100_000_000_000u64.into()); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &coldkey2, &hotkey_b + )); + SubtensorModule::stake_into_subnet( + &hotkey_b, + &coldkey2, + netuid, + 50_000_000_000u64.into(), + ::SwapInterface::max_price(), + false, + ) + .unwrap(); + + // coldkey1 locks more to hotkey_a + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey1, + netuid, + &hotkey_a, + 8000u64.into(), + )); + // coldkey2 locks less to hotkey_b + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey2, + netuid, + &hotkey_b, + 2000u64.into(), + )); + + step_block(500); + + let king = SubtensorModule::subnet_king(netuid); + assert_eq!(king, Some(hotkey_a)); + }); +} + +#[test] +fn test_subnet_king_no_locks() { + new_test_ext(1).execute_with(|| { + let netuid = subtensor_runtime_common::NetUid::from(99); + let king = SubtensorModule::subnet_king(netuid); + assert_eq!(king, None); + }); +} + +#[test] +fn test_change_subnet_owner_if_needed_reassigns_to_subnet_king() { + new_test_ext(1).execute_with(|| { + // Start with the subnet's existing owner, then create a different hotkey owner + // that can become subnet king. + let old_owner_coldkey = U256::from(1); + let old_owner_hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(old_owner_coldkey, old_owner_hotkey, 100_000_000_000); + SubnetOwner::::insert(netuid, old_owner_coldkey); + SubnetOwnerHotkey::::insert(netuid, old_owner_hotkey); + + let new_owner_coldkey = U256::from(5); + let king_hotkey = U256::from(6); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &new_owner_coldkey, + &king_hotkey + )); + + // Make the subnet old enough and set alpha out so 1_000 conviction is exactly + // the 10% minimum required to trigger reassignment. + let now = crate::staking::lock::ONE_YEAR + 1; + System::set_block_number(now); + NetworkRegisteredAt::::insert(netuid, 1); + SubnetAlphaOut::::insert(netuid, AlphaBalance::from(10_000u64)); + + // Seed matching individual and aggregate lock rows for the future king. + let locked_mass = AlphaBalance::from(1_000u64); + Lock::::insert( + (new_owner_coldkey, netuid, king_hotkey), + LockState { + locked_mass, + conviction: U64F64::from_num(1_000), + last_update: now, + }, + ); + HotkeyLock::::insert( + netuid, + king_hotkey, + LockState { + locked_mass, + conviction: U64F64::from_num(1_000), + last_update: now, + }, + ); + + // Reassignment should select the king hotkey and its owning coldkey. + SubtensorModule::change_subnet_owner_if_needed(netuid); + + assert_eq!(SubnetOwner::::get(netuid), new_owner_coldkey); + assert_eq!(SubnetOwnerHotkey::::get(netuid), king_hotkey); + + // The new owner's aggregate conviction is progressed to locked mass. + let owner_lock = Lock::::get((new_owner_coldkey, netuid, king_hotkey)).unwrap(); + assert_eq!(owner_lock.conviction, U64F64::from_num(1_000)); + + let king_lock = OwnerLock::::get(netuid).unwrap(); + assert_eq!(king_lock.conviction, U64F64::from_num(1_000)); + }); +} + +#[test] +fn test_run_coinbase_reassigns_subnet_owner_by_conviction_on_epoch() { + new_test_ext(1).execute_with(|| { + let old_owner_coldkey = U256::from(1); + let old_owner_hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(old_owner_coldkey, old_owner_hotkey, 100_000_000_000); + SubnetOwner::::insert(netuid, old_owner_coldkey); + SubnetOwnerHotkey::::insert(netuid, old_owner_hotkey); + + let new_owner_coldkey = U256::from(5); + let king_hotkey = U256::from(6); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &new_owner_coldkey, + &king_hotkey + )); + + let now = crate::staking::lock::ONE_YEAR + 1; + System::set_block_number(now); + NetworkRegisteredAt::::insert(netuid, 1); + SubnetAlphaOut::::insert(netuid, AlphaBalance::from(10_000u64)); + SubtensorModule::set_tempo_unchecked(netuid, 1); + LastEpochBlock::::insert(netuid, now.saturating_sub(1)); + PendingEpochAt::::insert(netuid, 0); + + let locked_mass = AlphaBalance::from(1_000u64); + Lock::::insert( + (new_owner_coldkey, netuid, king_hotkey), + LockState { + locked_mass, + conviction: U64F64::from_num(1_000), + last_update: now, + }, + ); + HotkeyLock::::insert( + netuid, + king_hotkey, + LockState { + locked_mass, + conviction: U64F64::from_num(1_000), + last_update: now, + }, + ); + + assert_eq!(SubnetOwner::::get(netuid), old_owner_coldkey); + assert_eq!(SubnetOwnerHotkey::::get(netuid), old_owner_hotkey); + + SubtensorModule::run_coinbase(SubtensorModule::mint_tao(0.into())); + + assert_eq!(SubnetOwner::::get(netuid), new_owner_coldkey); + assert_eq!(SubnetOwnerHotkey::::get(netuid), king_hotkey); + assert_eq!(LastEpochBlock::::get(netuid), now); + }); +} + +#[test] +fn test_change_subnet_owner_rebuilds_old_owner_hotkey_by_lock_mode() { + new_test_ext(1).execute_with(|| { + let old_owner_coldkey = U256::from(1); + let old_owner_hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(old_owner_coldkey, old_owner_hotkey, 100_000_000_000); + SubnetOwner::::insert(netuid, old_owner_coldkey); + SubnetOwnerHotkey::::insert(netuid, old_owner_hotkey); + + let perpetual_coldkey = U256::from(3); + let decaying_coldkey = U256::from(4); + let king_coldkey = U256::from(5); + let king_hotkey = U256::from(6); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &king_coldkey, + &king_hotkey + )); + register_ok_neuron(netuid, king_hotkey, king_coldkey, 0); + + let now = crate::staking::lock::ONE_YEAR + 1; + System::set_block_number(now); + NetworkRegisteredAt::::insert(netuid, 1); + SubnetAlphaOut::::insert(netuid, AlphaBalance::from(10_000u64)); + DecayingLock::::insert(perpetual_coldkey, netuid, false); + + Lock::::insert( + (perpetual_coldkey, netuid, old_owner_hotkey), + LockState { + locked_mass: 400u64.into(), + conviction: U64F64::from_num(400), + last_update: now, + }, + ); + Lock::::insert( + (decaying_coldkey, netuid, old_owner_hotkey), + LockState { + locked_mass: 300u64.into(), + conviction: U64F64::from_num(300), + last_update: now, + }, + ); + OwnerLock::::insert( + netuid, + LockState { + locked_mass: 400u64.into(), + conviction: U64F64::from_num(400), + last_update: now, + }, + ); + DecayingOwnerLock::::insert( + netuid, + LockState { + locked_mass: 300u64.into(), + conviction: U64F64::from_num(300), + last_update: now, + }, + ); + Lock::::insert( + (king_coldkey, netuid, king_hotkey), + LockState { + locked_mass: 1_000u64.into(), + conviction: U64F64::from_num(1_000), + last_update: now, + }, + ); + HotkeyLock::::insert( + netuid, + king_hotkey, + LockState { + locked_mass: 1_000u64.into(), + conviction: U64F64::from_num(1_000), + last_update: now, + }, + ); + + SubtensorModule::change_subnet_owner_if_needed(netuid); + + assert_eq!(SubnetOwnerHotkey::::get(netuid), king_hotkey); + assert_eq!( + HotkeyLock::::get(netuid, old_owner_hotkey) + .unwrap() + .locked_mass, + 400u64.into() + ); + assert_eq!( + DecayingHotkeyLock::::get(netuid, old_owner_hotkey) + .unwrap() + .locked_mass, + 300u64.into() + ); + assert_eq!( + OwnerLock::::get(netuid).unwrap().locked_mass, + 1_000u64.into() + ); + }); +} + +#[test] +fn test_swap_hotkey_locks_moves_owner_hotkey_aggregate_to_owner_lock() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(1); + let old_owner_hotkey = U256::from(2); + let new_owner_hotkey = U256::from(3); + let locking_coldkey = U256::from(4); + let netuid = setup_subnet_with_stake(owner_coldkey, old_owner_hotkey, 100_000_000_000); + SubnetOwner::::insert(netuid, owner_coldkey); + SubnetOwnerHotkey::::insert(netuid, old_owner_hotkey); + + assert_ok!(SubtensorModule::create_account_if_non_existent( + &owner_coldkey, + &new_owner_hotkey + )); + + let now = SubtensorModule::get_current_block_as_u64(); + Lock::::insert( + (locking_coldkey, netuid, old_owner_hotkey), + LockState { + locked_mass: 500u64.into(), + conviction: U64F64::from_num(500), + last_update: now, + }, + ); + SubtensorModule::add_locking_coldkey(&old_owner_hotkey, netuid, &locking_coldkey); + OwnerLock::::insert( + netuid, + LockState { + locked_mass: 500u64.into(), + conviction: U64F64::from_num(500), + last_update: now, + }, + ); + + SubtensorModule::swap_hotkey_locks(&old_owner_hotkey, &new_owner_hotkey); + + assert!(Lock::::get((locking_coldkey, netuid, old_owner_hotkey)).is_none()); + assert!(Lock::::get((locking_coldkey, netuid, new_owner_hotkey)).is_some()); + assert!(HotkeyLock::::get(netuid, new_owner_hotkey).is_none()); + assert!(DecayingHotkeyLock::::get(netuid, new_owner_hotkey).is_none()); + assert_eq!( + OwnerLock::::get(netuid).unwrap().locked_mass, + 500u64.into() + ); + assert!(!LockingColdkeys::::contains_key(( + netuid, + old_owner_hotkey, + locking_coldkey + ))); + assert!(LockingColdkeys::::contains_key(( + netuid, + new_owner_hotkey, + locking_coldkey + ))); + }); +} + +#[test] +fn test_change_subnet_owner_if_needed_does_not_reassign_when_required_condition_is_missing() { + let assert_owner_unchanged = + |alpha_out: u64, registered_at: u64, owner_conviction: u64, king_conviction: u64| { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(1001); + let owner_hotkey = U256::from(1002); + let staker_coldkey = U256::from(1); + let staker_hotkey = U256::from(2); + let netuid = + setup_subnet_with_stake(staker_coldkey, staker_hotkey, 100_000_000_000); + + let king_coldkey = U256::from(5); + let king_hotkey = U256::from(6); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &king_coldkey, + &king_hotkey + )); + + let now = crate::staking::lock::ONE_YEAR + 10; + System::set_block_number(now); + NetworkRegisteredAt::::insert(netuid, registered_at); + SubnetAlphaOut::::insert(netuid, AlphaBalance::from(alpha_out)); + + let locked_mass = AlphaBalance::from(1_000u64); + HotkeyLock::::insert( + netuid, + owner_hotkey, + LockState { + locked_mass, + conviction: U64F64::from_num(owner_conviction), + last_update: now, + }, + ); + HotkeyLock::::insert( + netuid, + king_hotkey, + LockState { + locked_mass, + conviction: U64F64::from_num(king_conviction), + last_update: now, + }, + ); + + SubtensorModule::change_subnet_owner_if_needed(netuid); + + assert_eq!(SubnetOwner::::get(netuid), owner_coldkey); + assert_eq!(SubnetOwnerHotkey::::get(netuid), owner_hotkey); + }); + }; + + // Missing condition 1: total conviction is below 10% of SubnetAlphaOut. + assert_owner_unchanged(30_000, 1, 500, 1_000); + + // Missing condition 2: subnet is younger than one year. + assert_owner_unchanged(20_000, crate::staking::lock::ONE_YEAR, 500, 1_000); + + // Missing condition 3: challenger is not the subnet king because owner's conviction is higher. + assert_owner_unchanged(20_000, 1, 2_000, 1_000); +} diff --git a/pallets/subtensor/src/tests/locks/hotkey_swap_lock.rs b/pallets/subtensor/src/tests/locks/hotkey_swap_lock.rs new file mode 100644 index 0000000000..e5060245b7 --- /dev/null +++ b/pallets/subtensor/src/tests/locks/hotkey_swap_lock.rs @@ -0,0 +1,88 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Hotkey swap interaction. + +use super::helpers::*; +use super::prelude::*; + +// ========================================================================= +// GROUP 12: Hotkey swap interaction +// ========================================================================= + +#[test] +fn test_hotkey_swap_swaps_locks_and_convictions() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let old_hotkey = U256::from(2); + let new_hotkey = U256::from(20); + let netuid = setup_subnet_with_stake(coldkey, old_hotkey, 100_000_000_000); + Owner::::insert(old_hotkey, coldkey); + Owner::::insert(new_hotkey, coldkey); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &old_hotkey, + 5000u64.into(), + )); + assert!(LockingColdkeys::::contains_key(( + netuid, old_hotkey, coldkey + ))); + assert_eq!( + LockingColdkeys::::iter_prefix((netuid, old_hotkey)).count(), + 1 + ); + + // Mock a non-zero conviction + let mut lock = Lock::::get((coldkey, netuid, old_hotkey)).unwrap(); + lock.conviction = U64F64::from_num(1234); + Lock::::insert((coldkey, netuid, old_hotkey), lock); + let mut hotkey_lock = HotkeyLock::::get(netuid, old_hotkey).unwrap(); + hotkey_lock.conviction = U64F64::from_num(1234); + HotkeyLock::::insert(netuid, old_hotkey, hotkey_lock); + + // Perform hotkey swap + let mut weight = Weight::zero(); + assert_ok!(SubtensorModule::perform_hotkey_swap_on_all_subnets( + &old_hotkey, + &new_hotkey, + &coldkey, + &mut weight, + false + )); + + // Lock references new_hotkey, conviction is not reset + let lock = Lock::::get((coldkey, netuid, new_hotkey)).unwrap(); + assert_eq!(lock.locked_mass, 5000u64.into()); + assert!(lock.conviction > U64F64::from_num(0)); + assert!(!LockingColdkeys::::contains_key(( + netuid, old_hotkey, coldkey + ))); + assert!(LockingColdkeys::::contains_key(( + netuid, new_hotkey, coldkey + ))); + + // Hotkey lock data also updated, conviction is not reset + let hotkey_lock = HotkeyLock::::get(netuid, new_hotkey).unwrap(); + assert_eq!(hotkey_lock.locked_mass, 5000u64.into()); + assert!(hotkey_lock.conviction > U64F64::from_num(0)); + + // Trying to top up to new_hotkey works + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &new_hotkey, + 100u64.into() + )); + + // Trying to top up to old_hotkey fails (old_hotkey is no longer associated with coldkey) + assert_noop!( + SubtensorModule::do_lock_stake(&coldkey, netuid, &old_hotkey, 100u64.into()), + Error::::HotKeyAccountNotExists + ); + }); +} diff --git a/pallets/subtensor/src/tests/locks/lock_queries.rs b/pallets/subtensor/src/tests/locks/lock_queries.rs new file mode 100644 index 0000000000..1059658bd7 --- /dev/null +++ b/pallets/subtensor/src/tests/locks/lock_queries.rs @@ -0,0 +1,435 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Green-path — lock queries. + +use super::helpers::*; +use super::prelude::*; + +// ========================================================================= +// GROUP 2: Green-path — lock queries +// ========================================================================= + +#[test] +fn test_get_current_locked_no_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let netuid = subtensor_runtime_common::NetUid::from(1); + assert_eq!( + SubtensorModule::get_current_locked(&coldkey, netuid), + AlphaBalance::ZERO + ); + }); +} + +#[test] +fn test_get_conviction_no_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let netuid = subtensor_runtime_common::NetUid::from(1); + assert_eq!( + SubtensorModule::get_conviction(&coldkey, netuid), + U64F64::from_num(0) + ); + }); +} + +#[test] +fn test_get_coldkey_lock_rolls_forward() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + 5000u64.into(), + )); + + let initial_lock = + SubtensorModule::get_coldkey_lock(&coldkey, netuid).expect("coldkey lock should exist"); + assert_eq!(initial_lock.conviction, U64F64::from_num(0)); + + step_block(1000); + + let rolled_lock = + SubtensorModule::get_coldkey_lock(&coldkey, netuid).expect("coldkey lock should exist"); + assert_eq!(rolled_lock.locked_mass, initial_lock.locked_mass); + assert!(rolled_lock.conviction > initial_lock.conviction); + assert_eq!( + rolled_lock.last_update, + SubtensorModule::get_current_block_as_u64() + ); + }); +} + +#[test] +fn test_get_coldkey_lock_no_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let netuid = subtensor_runtime_common::NetUid::from(1); + + assert!(SubtensorModule::get_coldkey_lock(&coldkey, netuid).is_none()); + }); +} + +#[test] +fn test_available_to_unstake_no_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + let available = SubtensorModule::available_to_unstake(&coldkey, netuid); + assert_eq!(available, total); + }); +} + +#[test] +fn test_available_to_unstake_with_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + let lock_amount = total / 2.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount, + )); + + let available = SubtensorModule::available_to_unstake(&coldkey, netuid); + assert_eq!(available, total - lock_amount); + }); +} + +#[test] +fn test_available_to_unstake_fully_locked() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid, &hotkey, total, + )); + + let available = SubtensorModule::available_to_unstake(&coldkey, netuid); + assert_eq!(available, AlphaBalance::ZERO); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_empty_coldkeys() { + new_test_ext(1).execute_with(|| { + let result = SubtensorModule::get_stake_availability_for_coldkeys(Vec::new(), None); + assert!(result.is_empty()); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_empty_netuids() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let result = + SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(Vec::new())); + assert_eq!(result.len(), 1); + assert!(result.contains_key(&coldkey)); + assert!(result.get(&coldkey).unwrap().is_empty()); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_filters_empty_rows() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + let result = + SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(vec![netuid])); + + assert_eq!(result.len(), 1); + assert!(result.contains_key(&coldkey)); + assert!(result.get(&coldkey).unwrap().is_empty()); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_stake_without_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + + let result = + SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(vec![netuid])); + + assert_eq!(result.len(), 1); + let availability = result.get(&coldkey).unwrap().get(&netuid).unwrap(); + assert_eq!(availability.total(), total); + assert_eq!(availability.locked(), AlphaBalance::ZERO); + assert_eq!(availability.available(), total); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_partial_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + let lock_amount = total / 2.into(); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount, + )); + + let result = + SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(vec![netuid])); + let availability = result.get(&coldkey).unwrap().get(&netuid).unwrap(); + + assert_eq!(availability.total(), total); + assert_eq!( + availability.locked(), + SubtensorModule::get_current_locked(&coldkey, netuid) + ); + assert_eq!(availability.available(), total - availability.locked()); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_fully_locked() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid, &hotkey, total, + )); + + let result = + SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(vec![netuid])); + let availability = result.get(&coldkey).unwrap().get(&netuid).unwrap(); + + assert_eq!(availability.total(), total); + assert_eq!(availability.locked(), total); + assert_eq!(availability.available(), AlphaBalance::ZERO); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_preserves_coldkey_grouping() { + new_test_ext(1).execute_with(|| { + let coldkey_a = U256::from(1); + let hotkey_a = U256::from(2); + let coldkey_b = U256::from(3); + let hotkey_b = U256::from(4); + let netuid_a = setup_subnet_with_stake(coldkey_a, hotkey_a, 100_000_000_000); + let netuid_b = setup_subnet_with_stake(coldkey_b, hotkey_b, 100_000_000_000); + + let result = SubtensorModule::get_stake_availability_for_coldkeys( + vec![coldkey_a, coldkey_b], + Some(vec![netuid_a, netuid_b]), + ); + + assert_eq!(result.len(), 2); + assert_eq!(result.get(&coldkey_a).unwrap().len(), 1); + assert!(result.get(&coldkey_a).unwrap().contains_key(&netuid_a)); + assert_eq!(result.get(&coldkey_b).unwrap().len(), 1); + assert!(result.get(&coldkey_b).unwrap().contains_key(&netuid_b)); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_none_netuids_uses_all_subnets() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let result = SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], None); + + assert_eq!(result.len(), 1); + assert!(result.get(&coldkey).unwrap().contains_key(&netuid)); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_one_coldkey_two_subnets() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey_a = U256::from(2); + let hotkey_b = U256::from(3); + let netuid_a = setup_subnet_with_stake(coldkey, hotkey_a, 100_000_000_000); + let netuid_b = setup_subnet_with_stake(coldkey, hotkey_b, 100_000_000_000); + let total_a = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid_a); + let total_b = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid_b); + + let result = SubtensorModule::get_stake_availability_for_coldkeys( + vec![coldkey], + Some(vec![netuid_a, netuid_b]), + ); + + assert_eq!(result.len(), 1); + let subnets = result.get(&coldkey).unwrap(); + assert_eq!(subnets.len(), 2); + assert!(subnets.contains_key(&netuid_a)); + assert!(subnets.contains_key(&netuid_b)); + + let row_a = subnets.get(&netuid_a).unwrap(); + assert_eq!(row_a.total(), total_a); + assert_eq!(row_a.locked(), AlphaBalance::ZERO); + assert_eq!(row_a.available(), total_a); + + let row_b = subnets.get(&netuid_b).unwrap(); + assert_eq!(row_b.total(), total_b); + assert_eq!(row_b.locked(), AlphaBalance::ZERO); + assert_eq!(row_b.available(), total_b); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_filters_to_requested_netuid() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey_a = U256::from(2); + let hotkey_b = U256::from(3); + let netuid_a = setup_subnet_with_stake(coldkey, hotkey_a, 100_000_000_000); + let netuid_b = setup_subnet_with_stake(coldkey, hotkey_b, 100_000_000_000); + + let result = SubtensorModule::get_stake_availability_for_coldkeys( + vec![coldkey], + Some(vec![netuid_b]), + ); + + assert_eq!(result.len(), 1); + let subnets = result.get(&coldkey).unwrap(); + assert_eq!(subnets.len(), 1); + assert!(subnets.contains_key(&netuid_b)); + assert!(!subnets.contains_key(&netuid_a)); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_dedups_netuids() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let result = SubtensorModule::get_stake_availability_for_coldkeys( + vec![coldkey], + Some(vec![netuid, netuid]), + ); + + assert_eq!(result.len(), 1); + assert_eq!(result.get(&coldkey).unwrap().len(), 1); + assert!(result.get(&coldkey).unwrap().contains_key(&netuid)); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_skips_nonexistent_netuid() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + let nonexistent = subtensor_runtime_common::NetUid::from(99); + + let result = SubtensorModule::get_stake_availability_for_coldkeys( + vec![coldkey], + Some(vec![nonexistent]), + ); + assert_eq!(result.len(), 1); + assert!(result.get(&coldkey).unwrap().is_empty()); + + // Mix real + fake requires at least two subnets on chain so len(requested) <= subnet_count. + let subnet_owner_coldkey = U256::from(2001); + let subnet_owner_hotkey = U256::from(2002); + let _other_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + let result = SubtensorModule::get_stake_availability_for_coldkeys( + vec![coldkey], + Some(vec![netuid, nonexistent]), + ); + assert_eq!(result.len(), 1); + let subnets = result.get(&coldkey).unwrap(); + assert_eq!(subnets.len(), 1); + assert!(subnets.contains_key(&netuid)); + assert!(!subnets.contains_key(&nonexistent)); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_rejects_oversized_netuid_list() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + let subnet_count = SubtensorModule::get_all_subnet_netuids().len(); + let requested: Vec = (0..=subnet_count as u16) + .map(subtensor_runtime_common::NetUid::from) + .collect(); + + let result = + SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(requested)); + assert_eq!(result.len(), 1); + assert!(result.contains_key(&coldkey)); + assert!(result.get(&coldkey).unwrap().is_empty()); + + let result = + SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(vec![netuid])); + assert_eq!(result.get(&coldkey).unwrap().len(), 1); + assert!(result.get(&coldkey).unwrap().contains_key(&netuid)); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_uses_rolled_forward_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + let lock_amount = total / 2.into(); + + DecayingLock::::remove(coldkey, netuid); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount, + )); + let raw_lock = Lock::::get((coldkey, netuid, hotkey)).unwrap(); + + step_block(1000); + + let result = + SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(vec![netuid])); + let availability = result.get(&coldkey).unwrap().get(&netuid).unwrap(); + let rolled_locked = SubtensorModule::get_current_locked(&coldkey, netuid); + + assert!(rolled_locked < raw_lock.locked_mass); + assert_eq!(availability.locked(), rolled_locked); + assert_eq!(availability.available(), total - rolled_locked); + }); +} diff --git a/pallets/subtensor/src/tests/locks/lock_rejection.rs b/pallets/subtensor/src/tests/locks/lock_rejection.rs new file mode 100644 index 0000000000..0d152f6670 --- /dev/null +++ b/pallets/subtensor/src/tests/locks/lock_rejection.rs @@ -0,0 +1,93 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Lock rejection cases. + +use super::helpers::*; +use super::prelude::*; + +// ========================================================================= +// GROUP 4: Lock rejection cases +// ========================================================================= + +#[test] +fn test_lock_stake_zero_amount() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + assert_noop!( + SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, AlphaBalance::ZERO,), + Error::::AmountTooLow + ); + }); +} + +#[test] +fn test_lock_stake_exceeds_total_alpha() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + let too_much = total + 1.into(); + + assert_noop!( + SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, too_much), + Error::::InsufficientStakeForLock + ); + }); +} + +#[test] +fn test_lock_stake_wrong_hotkey() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey_a = U256::from(2); + let hotkey_b = U256::from(3); + let netuid = setup_subnet_with_stake(coldkey, hotkey_a, 100_000_000_000); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &coldkey, &hotkey_b + )); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey_a, + 1000u64.into(), + )); + + assert_noop!( + SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey_b, 500u64.into(),), + Error::::LockHotkeyMismatch + ); + }); +} + +#[test] +fn test_lock_stake_topup_exceeds_total() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + // Lock 80% initially + let initial = total * 8.into() / 10.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid, &hotkey, initial + )); + + // Try to top up the remaining 30% (exceeds total by 10%) + let topup = total * 3.into() / 10.into(); + assert_noop!( + SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, topup), + Error::::InsufficientStakeForLock + ); + }); +} diff --git a/pallets/subtensor/src/tests/locks/lock_stake_creation.rs b/pallets/subtensor/src/tests/locks/lock_stake_creation.rs new file mode 100644 index 0000000000..49c99a2ba0 --- /dev/null +++ b/pallets/subtensor/src/tests/locks/lock_stake_creation.rs @@ -0,0 +1,647 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Green-path — basic lock creation. + +use super::helpers::*; +use super::prelude::*; + +// ========================================================================= +// GROUP 1: Green-path — basic lock creation +// ========================================================================= + +#[test] +fn test_lock_stake_creates_new_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let alpha = get_alpha(&hotkey, &coldkey, netuid); + let lock_amount = alpha.to_u64() / 2; + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount.into(), + )); + + let lock = Lock::::get((coldkey, netuid, hotkey)).expect("Lock should exist"); + assert_eq!(lock.locked_mass, lock_amount.into()); + assert_eq!(lock.conviction, U64F64::from_num(0)); + assert_eq!( + lock.last_update, + SubtensorModule::get_current_block_as_u64() + ); + + // Hotkey lock should also be created + let hotkey_lock = HotkeyLock::::get(netuid, hotkey); + assert!(hotkey_lock.is_some()); + assert_eq!(hotkey_lock.unwrap().locked_mass, lock_amount.into()); + }); +} + +#[test] +fn test_lock_stake_defaults_to_decaying_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + DecayingLock::::remove(coldkey, netuid); + + let lock_amount: AlphaBalance = 5000u64.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount, + )); + + assert!(DecayingLock::::get(coldkey, netuid).is_none()); + assert!(HotkeyLock::::get(netuid, hotkey).is_none()); + + let decaying_hotkey_lock = DecayingHotkeyLock::::get(netuid, hotkey) + .expect("default lock should use decaying aggregate"); + assert_eq!(decaying_hotkey_lock.locked_mass, lock_amount); + }); +} + +#[test] +fn test_lock_stake_by_subnet_owner_coldkey_gets_immediate_conviction() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(1); + let owner_hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(owner_coldkey, owner_hotkey, 300_000_000_000); + SubnetOwner::::insert(netuid, owner_coldkey); + SubnetOwnerHotkey::::insert(netuid, owner_hotkey); + + let lock_amount: AlphaBalance = 5000u64.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &owner_coldkey, + netuid, + &owner_hotkey, + lock_amount, + )); + + let lock = Lock::::get((owner_coldkey, netuid, owner_hotkey)) + .expect("lock to owner hotkey should exist"); + assert_eq!(lock.locked_mass, lock_amount); + assert_eq!(lock.conviction, U64F64::saturating_from_num(5000)); + let owner_lock = OwnerLock::::get(netuid).expect("owner lock should exist"); + assert_eq!(owner_lock.locked_mass, lock_amount); + assert_eq!(owner_lock.conviction, U64F64::saturating_from_num(5000)); + }); +} + +#[test] +fn test_lock_to_subnet_owner_hotkey_gets_immediate_conviction_for_non_owner_coldkey() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let staker_hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, staker_hotkey, 300_000_000_000); + let owner_hotkey = SubnetOwnerHotkey::::get(netuid); + + let lock_amount: AlphaBalance = 5000u64.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &owner_hotkey, + lock_amount, + )); + + let lock = Lock::::get((coldkey, netuid, owner_hotkey)) + .expect("lock to owner hotkey should exist"); + assert_eq!(lock.locked_mass, lock_amount); + assert_eq!(lock.conviction, U64F64::saturating_from_num(5000)); + + let owner_lock = OwnerLock::::get(netuid).expect("owner lock should exist"); + assert_eq!(owner_lock.locked_mass, lock_amount); + assert_eq!(owner_lock.conviction, U64F64::saturating_from_num(5000)); + assert!( + HotkeyLock::::get(netuid, owner_hotkey).is_none(), + "lock to owner hotkey should use OwnerLock, not HotkeyLock" + ); + }); +} + +#[test] +fn test_decaying_lock_to_subnet_owner_hotkey_keeps_decaying_mass() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let staker_hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, staker_hotkey, 300_000_000_000); + let owner_hotkey = SubnetOwnerHotkey::::get(netuid); + + assert_ok!(SubtensorModule::do_set_perpetual_lock( + &coldkey, netuid, false, + )); + + let lock_amount: AlphaBalance = 5000u64.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &owner_hotkey, + lock_amount, + )); + + step_block(1_000); + let now = SubtensorModule::get_current_block_as_u64(); + let rolled = roll_forward_individual_lock( + &coldkey, + netuid, + &owner_hotkey, + Lock::::get((coldkey, netuid, owner_hotkey)).unwrap(), + now, + ); + + assert!(rolled.locked_mass < lock_amount); + assert_eq!( + rolled.conviction, + U64F64::saturating_from_num(u64::from(rolled.locked_mass)) + ); + assert_eq!( + SubtensorModule::hotkey_conviction(&owner_hotkey, netuid), + rolled.conviction + ); + assert!( + OwnerLock::::get(netuid).is_none(), + "decaying lock to owner hotkey should not use perpetual OwnerLock" + ); + assert!( + DecayingOwnerLock::::get(netuid).is_some(), + "decaying lock to owner hotkey should use DecayingOwnerLock" + ); + }); +} + +#[test] +fn test_lock_by_subnet_owner_coldkey_to_non_owner_hotkey_matures_normally() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(1); + let non_owner_hotkey = U256::from(2); + let owner_hotkey = U256::from(3); + let netuid = setup_subnet_with_stake(owner_coldkey, non_owner_hotkey, 300_000_000_000); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &owner_coldkey, + &owner_hotkey + )); + SubnetOwner::::insert(netuid, owner_coldkey); + SubnetOwnerHotkey::::insert(netuid, owner_hotkey); + + let lock_amount: AlphaBalance = 5000u64.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &owner_coldkey, + netuid, + &non_owner_hotkey, + lock_amount, + )); + + let lock = Lock::::get((owner_coldkey, netuid, non_owner_hotkey)) + .expect("lock to non-owner hotkey should exist"); + assert_eq!(lock.locked_mass, lock_amount); + assert_eq!(lock.conviction, U64F64::saturating_from_num(0)); + assert!( + OwnerLock::::get(netuid).is_none(), + "owner coldkey lock to a non-owner hotkey should not use OwnerLock" + ); + + let hotkey_lock = + HotkeyLock::::get(netuid, non_owner_hotkey).expect("hotkey lock should exist"); + assert_eq!(hotkey_lock.locked_mass, lock_amount); + assert_eq!(hotkey_lock.conviction, U64F64::saturating_from_num(0)); + }); +} + +#[test] +fn test_lock_stake_topup_by_subnet_owner_coldkey_gets_immediate_conviction() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(1); + let owner_hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(owner_coldkey, owner_hotkey, 100_000_000_000); + SubnetOwner::::insert(netuid, owner_coldkey); + SubnetOwnerHotkey::::insert(netuid, owner_hotkey); + + let first_lock: AlphaBalance = 5000u64.into(); + let second_lock: AlphaBalance = 7000u64.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &owner_coldkey, + netuid, + &owner_hotkey, + first_lock, + )); + assert_ok!(SubtensorModule::do_lock_stake( + &owner_coldkey, + netuid, + &owner_hotkey, + second_lock, + )); + + let expected_locked = first_lock + second_lock; + let lock = Lock::::get((owner_coldkey, netuid, owner_hotkey)) + .expect("lock to owner hotkey should exist"); + assert_eq!(lock.locked_mass, expected_locked); + assert_eq!( + lock.conviction, + U64F64::saturating_from_num(u64::from(expected_locked)) + ); + + let owner_lock = OwnerLock::::get(netuid).expect("owner lock should exist"); + assert_eq!(owner_lock.locked_mass, expected_locked); + assert_eq!( + owner_lock.conviction, + U64F64::saturating_from_num(u64::from(expected_locked)) + ); + }); +} + +#[test] +fn test_set_perpetual_lock_toggles_owner_lock_decay() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(1); + let owner_hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(owner_coldkey, owner_hotkey, 100_000_000_000); + SubnetOwner::::insert(netuid, owner_coldkey); + SubnetOwnerHotkey::::insert(netuid, owner_hotkey); + + let lock_amount: AlphaBalance = 5000u64.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &owner_coldkey, + netuid, + &owner_hotkey, + lock_amount, + )); + + assert_ok!(SubtensorModule::set_perpetual_lock( + RuntimeOrigin::signed(owner_coldkey), + netuid, + true, + )); + step_block(100); + assert_eq!( + SubtensorModule::get_current_locked(&owner_coldkey, netuid), + lock_amount + ); + + assert_ok!(SubtensorModule::set_perpetual_lock( + RuntimeOrigin::signed(owner_coldkey), + netuid, + false, + )); + step_block(100); + assert!(SubtensorModule::get_current_locked(&owner_coldkey, netuid) < lock_amount); + }); +} + +#[test] +fn test_set_perpetual_lock_is_per_coldkey_and_rolls_lock_at_boundary() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 300_000_000_000); + + let lock_amount: AlphaBalance = 5000u64.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount, + )); + + assert_ok!(SubtensorModule::set_perpetual_lock( + RuntimeOrigin::signed(coldkey), + netuid, + false, + )); + System::set_block_number(System::block_number() + UnlockRate::::get() / 10); + assert_ok!(SubtensorModule::set_perpetual_lock( + RuntimeOrigin::signed(coldkey), + netuid, + true, + )); + + let locked_at_boundary = SubtensorModule::get_current_locked(&coldkey, netuid); + assert!(locked_at_boundary < lock_amount); + + System::set_block_number(System::block_number() + UnlockRate::::get() / 10); + assert_eq!( + SubtensorModule::get_current_locked(&coldkey, netuid), + locked_at_boundary + ); + + assert_ok!(SubtensorModule::set_perpetual_lock( + RuntimeOrigin::signed(coldkey), + netuid, + false, + )); + System::set_block_number(System::block_number() + UnlockRate::::get() / 10); + assert!(SubtensorModule::get_current_locked(&coldkey, netuid) < locked_at_boundary); + }); +} + +#[test] +fn test_mixed_perpetual_and_decaying_non_owner_locks_same_hotkey_update_aggregates() { + new_test_ext(1).execute_with(|| { + let perpetual_coldkey = U256::from(1); + let decaying_coldkey = U256::from(3); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(perpetual_coldkey, hotkey, 100_000_000_000); + + assert_ok!(SubtensorModule::create_account_if_non_existent( + &decaying_coldkey, + &hotkey + )); + add_balance_to_coldkey_account(&decaying_coldkey, 100_000_000_000u64.into()); + SubtensorModule::stake_into_subnet( + &hotkey, + &decaying_coldkey, + netuid, + 100_000_000_000u64.into(), + ::SwapInterface::max_price(), + false, + ) + .unwrap(); + + let lock_amount: AlphaBalance = 10_000u64.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &perpetual_coldkey, + netuid, + &hotkey, + lock_amount, + )); + assert_ok!(SubtensorModule::do_lock_stake( + &decaying_coldkey, + netuid, + &hotkey, + lock_amount, + )); + assert_ok!(SubtensorModule::do_set_perpetual_lock( + &decaying_coldkey, + netuid, + false, + )); + + step_block(1_000); + let now = SubtensorModule::get_current_block_as_u64(); + + let perpetual_lock = roll_forward_individual_lock( + &perpetual_coldkey, + netuid, + &hotkey, + Lock::::get((perpetual_coldkey, netuid, hotkey)).unwrap(), + now, + ); + let decaying_lock = roll_forward_individual_lock( + &decaying_coldkey, + netuid, + &hotkey, + Lock::::get((decaying_coldkey, netuid, hotkey)).unwrap(), + now, + ); + let perpetual_hotkey_lock = + roll_forward_hotkey_lock(HotkeyLock::::get(netuid, hotkey).unwrap(), now); + let decaying_hotkey_lock = roll_forward_decaying_hotkey_lock( + DecayingHotkeyLock::::get(netuid, hotkey).unwrap(), + now, + ); + + assert_eq!(perpetual_lock.locked_mass, lock_amount); + assert_eq!(perpetual_hotkey_lock.locked_mass, lock_amount); + assert!(decaying_lock.locked_mass < lock_amount); + assert_eq!(decaying_hotkey_lock.locked_mass, decaying_lock.locked_mass); + assert_eq!( + SubtensorModule::hotkey_conviction(&hotkey, netuid), + perpetual_hotkey_lock + .conviction + .saturating_add(decaying_hotkey_lock.conviction) + ); + }); +} + +#[test] +#[ignore] +fn plot_perpetual_decay_perpetual_lock_curve() { + new_test_ext(1).execute_with(|| { + const ALPHA: u64 = 1_000_000_000; + const ALPHA_F64: f64 = ALPHA as f64; + + let owner_coldkey = U256::from(1); + let owner_hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(owner_coldkey, owner_hotkey, 300_000_000_000); + SubnetOwner::::insert(netuid, owner_coldkey); + SubnetOwnerHotkey::::insert(netuid, owner_hotkey); + MaturityRate::::put(300u64); + UnlockRate::::put(200u64); + + let lock_amount: AlphaBalance = (1_000u64 * ALPHA).into(); + assert_ok!(SubtensorModule::do_lock_stake( + &owner_coldkey, + netuid, + &owner_hotkey, + lock_amount, + )); + assert_ok!(SubtensorModule::do_set_perpetual_lock( + &owner_coldkey, + netuid, + true, + )); + + println!("block,locked_mass,conviction"); + for block in 0..=2_000u64 { + System::set_block_number(block); + + if block == 1_000 { + assert_ok!(SubtensorModule::do_set_perpetual_lock( + &owner_coldkey, + netuid, + false, + )); + } else if block == 1_200 { + assert_ok!(SubtensorModule::do_set_perpetual_lock( + &owner_coldkey, + netuid, + true, + )); + } + + let lock = Lock::::get((owner_coldkey, netuid, owner_hotkey)).unwrap(); + let rolled = + roll_forward_individual_lock(&owner_coldkey, netuid, &owner_hotkey, lock, block); + SubtensorModule::insert_lock_state( + &owner_coldkey, + netuid, + &owner_hotkey, + rolled.clone(), + ); + SubtensorModule::insert_owner_lock_state(netuid, rolled.clone()); + println!( + "{},{},{}", + block, + u64::from(rolled.locked_mass) as f64 / ALPHA_F64, + rolled.conviction.to_num::() / ALPHA_F64 + ); + } + }); +} + +#[test] +#[ignore] +fn plot_decaying_non_owner_lock_curve() { + new_test_ext(1).execute_with(|| { + const ALPHA: u64 = 1_000_000_000; + const ALPHA_F64: f64 = ALPHA as f64; + + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 300_000_000_000); + MaturityRate::::put(300u64); + UnlockRate::::put(200u64); + System::set_block_number(0); + + let lock_amount: AlphaBalance = (1_000u64 * ALPHA).into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount, + )); + assert_ok!(SubtensorModule::do_set_perpetual_lock( + &coldkey, netuid, false, + )); + + println!("block,locked_mass,conviction"); + for block in 0..=2_000u64 { + System::set_block_number(block); + + let lock = Lock::::get((coldkey, netuid, hotkey)).unwrap(); + let rolled = roll_forward_individual_lock(&coldkey, netuid, &hotkey, lock, block); + SubtensorModule::insert_lock_state(&coldkey, netuid, &hotkey, rolled.clone()); + SubtensorModule::insert_hotkey_lock_state(netuid, &hotkey, rolled.clone()); + println!( + "{},{},{}", + block, + u64::from(rolled.locked_mass) as f64 / ALPHA_F64, + rolled.conviction.to_num::() / ALPHA_F64 + ); + } + }); +} + +#[test] +#[ignore] +fn plot_perpetual_decay_perpetual_non_owner_lock_curve() { + new_test_ext(1).execute_with(|| { + const ALPHA: u64 = 1_000_000_000; + const ALPHA_F64: f64 = ALPHA as f64; + + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 1_000_000_000_000); + MaturityRate::::put(300u64); + UnlockRate::::put(200u64); + System::set_block_number(0); + + let lock_amount: AlphaBalance = (1_000u64 * ALPHA).into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount, + )); + assert_ok!(SubtensorModule::do_set_perpetual_lock( + &coldkey, netuid, true, + )); + + println!("block,locked_mass,conviction"); + for block in 0..=2_000u64 { + System::set_block_number(block); + + if block == 1_000 { + assert_ok!(SubtensorModule::do_set_perpetual_lock( + &coldkey, netuid, false, + )); + } else if block == 1_200 { + assert_ok!(SubtensorModule::do_set_perpetual_lock( + &coldkey, netuid, true, + )); + } + + let lock = Lock::::get((coldkey, netuid, hotkey)).unwrap(); + let rolled = roll_forward_individual_lock(&coldkey, netuid, &hotkey, lock, block); + SubtensorModule::insert_lock_state(&coldkey, netuid, &hotkey, rolled.clone()); + if DecayingLock::::get(coldkey, netuid) == Some(false) { + SubtensorModule::insert_hotkey_lock_state(netuid, &hotkey, rolled.clone()); + } else { + SubtensorModule::insert_decaying_hotkey_lock_state(netuid, &hotkey, rolled.clone()); + } + println!( + "{},{},{}", + block, + u64::from(rolled.locked_mass) as f64 / ALPHA_F64, + rolled.conviction.to_num::() / ALPHA_F64 + ); + + // Add more lock (emulate owner auto-lock) + let auto_lock_amount: AlphaBalance = 200_000_000_u64.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + auto_lock_amount, + )); + } + }); +} + +#[test] +fn test_lock_stake_emits_event() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let lock_amount: u64 = 1000; + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount.into(), + )); + + System::assert_last_event( + Event::StakeLocked { + coldkey, + hotkey, + netuid, + amount: lock_amount.into(), + } + .into(), + ); + }); +} + +#[test] +fn test_lock_stake_full_amount() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total_alpha = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + assert!(!total_alpha.is_zero()); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + total_alpha, + )); + + let lock = Lock::::get((coldkey, netuid, hotkey)).unwrap(); + assert_eq!(lock.locked_mass, total_alpha); + }); +} diff --git a/pallets/subtensor/src/tests/locks/lock_stake_extrinsic.rs b/pallets/subtensor/src/tests/locks/lock_stake_extrinsic.rs new file mode 100644 index 0000000000..780b83998f --- /dev/null +++ b/pallets/subtensor/src/tests/locks/lock_stake_extrinsic.rs @@ -0,0 +1,41 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Lock extrinsic via dispatch. + +use super::helpers::*; +use super::prelude::*; + +// ========================================================================= +// GROUP 13: Lock extrinsic via dispatch +// ========================================================================= + +#[test] +fn test_lock_stake_extrinsic() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let lock_amount: u64 = 5000; + assert_ok!(SubtensorModule::lock_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + lock_amount.into(), + )); + + let lock = Lock::::get((coldkey, netuid, hotkey)).expect("Lock should exist"); + assert_eq!(lock.locked_mass, lock_amount.into()); + assert_eq!(lock.conviction, U64F64::from_num(0)); + + // Hotkey lock should also be updated + let hotkey_lock = + HotkeyLock::::get(netuid, hotkey).expect("Hotkey lock should exist"); + assert_eq!(hotkey_lock.locked_mass, lock_amount.into()); + assert_eq!(hotkey_lock.conviction, U64F64::from_num(0)); + }); +} diff --git a/pallets/subtensor/src/tests/locks/lock_topup.rs b/pallets/subtensor/src/tests/locks/lock_topup.rs new file mode 100644 index 0000000000..17112dd235 --- /dev/null +++ b/pallets/subtensor/src/tests/locks/lock_topup.rs @@ -0,0 +1,201 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Incremental locks (top-up). + +use super::helpers::*; +use super::prelude::*; + +// ========================================================================= +// GROUP 3: Incremental locks (top-up) +// ========================================================================= + +#[test] +fn test_lock_stake_topup() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let first_lock = 1000u64; + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + first_lock.into() + )); + + step_block(100); + + let second_lock = 500u64; + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + second_lock.into() + )); + + let lock = Lock::::get((coldkey, netuid, hotkey)).unwrap(); + // locked_mass should be decayed(first_lock) + second_lock + // Since tau is large (216000), decay over 100 blocks is small; locked_mass ~ 1000 + 500 + assert!(lock.locked_mass > 1490.into()); + assert!(lock.locked_mass < 1501.into()); + // conviction should have grown from the time the first lock was active + assert!(lock.conviction > U64F64::from_num(0)); + assert_eq!( + lock.last_update, + SubtensorModule::get_current_block_as_u64() + ); + + // Hotkey lock should also be created + let hotkey_lock = HotkeyLock::::get(netuid, hotkey).unwrap(); + assert!(hotkey_lock.locked_mass > 1490.into()); + assert_eq!(hotkey_lock.locked_mass, lock.locked_mass); + assert!(hotkey_lock.conviction > U64F64::from_num(0)); + }); +} + +#[test] +fn test_lock_stake_topup_multiple_times() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let chunk = 500u64.into(); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid, &hotkey, chunk + )); + step_block(50); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid, &hotkey, chunk + )); + step_block(50); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid, &hotkey, chunk + )); + + let lock = Lock::::get((coldkey, netuid, hotkey)).unwrap(); + // After three top-ups with small decay, should be close to 1500 + assert!(lock.locked_mass > 1490.into()); + assert!(lock.locked_mass <= 1500.into()); + assert!(lock.conviction > U64F64::from_num(0)); + + // Hotkey lock should also be updated + let hotkey_lock = HotkeyLock::::get(netuid, hotkey).unwrap(); + assert!(hotkey_lock.locked_mass > 1490.into()); + assert_eq!(hotkey_lock.locked_mass, lock.locked_mass); + assert!(hotkey_lock.conviction > U64F64::from_num(0)); + }); +} + +#[test] +fn test_lock_stake_topup_same_block() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let first = 1000u64.into(); + let second = 500u64.into(); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid, &hotkey, first + )); + // No block advancement — same block top-up + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid, &hotkey, second + )); + + let lock = Lock::::get((coldkey, netuid, hotkey)).unwrap(); + // dt=0 means no decay, simple addition + assert_eq!(lock.locked_mass, first + second); + assert_eq!(lock.conviction, U64F64::from_num(0)); + + // Hotkey lock should also be updated + let hotkey_lock = HotkeyLock::::get(netuid, hotkey).unwrap(); + assert_eq!(hotkey_lock.locked_mass, first + second); + assert_eq!(hotkey_lock.conviction, U64F64::from_num(0)); + }); +} + +#[test] +fn test_locking_coldkeys_added_once_by_lock_stake() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + 100u64.into(), + )); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + 50u64.into(), + )); + + assert!(LockingColdkeys::::contains_key(( + netuid, hotkey, coldkey + ))); + assert_eq!( + LockingColdkeys::::iter_prefix((netuid, hotkey)).count(), + 1 + ); + }); +} + +#[test] +fn test_locking_coldkeys_removed_when_lock_is_fully_reduced() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + let amount = 100u64.into(); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid, &hotkey, amount + )); + assert!(LockingColdkeys::::contains_key(( + netuid, hotkey, coldkey + ))); + + SubtensorModule::force_reduce_lock(&coldkey, netuid, amount); + + assert!(Lock::::get((coldkey, netuid, hotkey)).is_none()); + assert!(!LockingColdkeys::::contains_key(( + netuid, hotkey, coldkey + ))); + }); +} + +#[test] +fn test_lock_state_is_zero_uses_dust_threshold() { + let below_threshold = LockState { + locked_mass: AlphaBalance::from(99u64), + conviction: U64F64::from_num(99), + last_update: 0, + }; + let locked_mass_at_threshold = LockState { + locked_mass: AlphaBalance::from(100u64), + conviction: U64F64::from_num(99), + last_update: 0, + }; + let conviction_at_threshold = LockState { + locked_mass: AlphaBalance::from(99u64), + conviction: U64F64::from_num(100), + last_update: 0, + }; + + assert!(below_threshold.is_zero()); + assert!(!locked_mass_at_threshold.is_zero()); + assert!(!conviction_at_threshold.is_zero()); +} diff --git a/pallets/subtensor/src/tests/locks/mod.rs b/pallets/subtensor/src/tests/locks/mod.rs new file mode 100644 index 0000000000..fd19daf7ee --- /dev/null +++ b/pallets/subtensor/src/tests/locks/mod.rs @@ -0,0 +1,58 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Unit tests for stake locks, conviction, and lock invariants. +//! +//! Split from the former monolithic `tests/locks.rs` into concept modules. +//! +//! ## Search anchors +//! +//! | Module | Owns | +//! |--------|------| +//! | [`helpers`] | setup/roll-forward fixtures | +//! | [`account_flags_reject_locked_alpha`] | AccountFlags reject-locked-alpha | +//! | [`lock_stake_creation`] | Green-path — basic lock creation | +//! | [`lock_queries`] | Green-path — lock queries | +//! | [`lock_topup`] | Incremental locks (top-up) | +//! | [`lock_rejection`] | Lock rejection cases | +//! | [`conviction_roll_forward`] | ConvictionModel roll-forward math | +//! | [`unstake_lock_invariant`] | Unstake invariant enforcement | +//! | [`move_transfer_lock`] | Move/transfer invariant enforcement | +//! | [`multi_subnet_locks`] | Multi-subnet locks | +//! | [`hotkey_conviction_subnet_king`] | Hotkey conviction and subnet king | +//! | [`force_reduce_lock`] | Lock force-reduction | +//! | [`coldkey_swap_lock`] | Coldkey swap interaction | +//! | [`hotkey_swap_lock`] | Hotkey swap interaction | +//! | [`lock_stake_extrinsic`] | Lock extrinsic via dispatch | +//! | [`recycle_burn_lock`] | Recycle/burn alpha checks against lock | +//! | [`subnet_dissolution_lock`] | Subnet dissolution | +//! | [`clear_small_nomination_lock`] | Clear small nomination checks lock | +//! | [`emission_lock`] | Emission interaction | +//! | [`neuron_replacement_lock`] | Neuron replacement | +//! | [`moving_lock`] | Moving lock | + +mod account_flags_reject_locked_alpha; +mod clear_small_nomination_lock; +mod coldkey_swap_lock; +mod conviction_roll_forward; +mod emission_lock; +mod force_reduce_lock; +mod helpers; +mod hotkey_conviction_subnet_king; +mod hotkey_swap_lock; +mod lock_queries; +mod lock_rejection; +mod lock_stake_creation; +mod lock_stake_extrinsic; +mod lock_topup; +mod move_transfer_lock; +mod moving_lock; +mod multi_subnet_locks; +mod neuron_replacement_lock; +mod prelude; +mod recycle_burn_lock; +mod subnet_dissolution_lock; +mod unstake_lock_invariant; diff --git a/pallets/subtensor/src/tests/locks/move_transfer_lock.rs b/pallets/subtensor/src/tests/locks/move_transfer_lock.rs new file mode 100644 index 0000000000..b3c917c1ad --- /dev/null +++ b/pallets/subtensor/src/tests/locks/move_transfer_lock.rs @@ -0,0 +1,538 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Move/transfer invariant enforcement. + +use super::helpers::*; +use super::prelude::*; + +// ========================================================================= +// GROUP 7: Move/transfer invariant enforcement +// ========================================================================= + +#[test] +fn test_move_stake_same_coldkey_same_subnet_allowed() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey_a = U256::from(2); + let hotkey_b = U256::from(3); + let netuid = setup_subnet_with_stake(coldkey, hotkey_a, 100_000_000_000); + + assert_ok!(SubtensorModule::create_account_if_non_existent( + &coldkey, &hotkey_b + )); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + // Lock the full amount to hotkey_a + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid, &hotkey_a, total + )); + + // Move from hotkey_a to hotkey_b on same subnet — total coldkey alpha unchanged + let alpha = get_alpha(&hotkey_a, &coldkey, netuid); + let move_amount = alpha / 2.into(); + assert_ok!(SubtensorModule::do_move_stake( + RuntimeOrigin::signed(coldkey), + hotkey_a, + hotkey_b, + netuid, + netuid, + move_amount, + )); + }); +} + +#[test] +fn test_do_transfer_stake_same_subnet_transfers_lock_to_destination_coldkey() { + new_test_ext(1).execute_with(|| { + let coldkey_sender = U256::from(1); + let coldkey_receiver = U256::from(5); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey_sender, hotkey, 100_000_000_000); + DecayingLock::::insert(coldkey_receiver, netuid, false); + assert_ok!(SubtensorModule::set_reject_locked_alpha( + RuntimeOrigin::signed(coldkey_receiver), + false, + )); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_sender, netuid); + let lock_half = total / 2.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey_sender, + netuid, + &hotkey, + lock_half, + )); + + let sender_lock_before = + Lock::::get((coldkey_sender, netuid, hotkey)).expect("sender lock should exist"); + let hotkey_lock_before = + HotkeyLock::::get(netuid, hotkey).expect("hotkey lock should exist"); + + step_block(1); + + let transfer_amount = total; + assert_ok!(SubtensorModule::do_transfer_stake( + RuntimeOrigin::signed(coldkey_sender), + coldkey_receiver, + hotkey, + netuid, + netuid, + transfer_amount, + )); + + let expected_sender_lock = roll_forward_lock( + sender_lock_before, + SubtensorModule::get_current_block_as_u64(), + false, + true, + ); + + assert!(Lock::::get((coldkey_sender, netuid, hotkey)).is_none()); + + let receiver_lock = Lock::::get((coldkey_receiver, netuid, hotkey)) + .expect("receiver lock should exist after transfer"); + assert_eq!(receiver_lock.locked_mass, expected_sender_lock.locked_mass); + assert!(receiver_lock.conviction > U64F64::from_num(0)); + assert!(receiver_lock.conviction <= expected_sender_lock.conviction); + + let hotkey_lock_after = + HotkeyLock::::get(netuid, hotkey).expect("hotkey lock should remain"); + let expected_hotkey_lock = roll_forward_lock( + hotkey_lock_before, + SubtensorModule::get_current_block_as_u64(), + false, + true, + ); + assert_eq!( + hotkey_lock_after.locked_mass, + expected_hotkey_lock.locked_mass + ); + }); +} + +// Regression test: a same-subnet transfer that changes the hotkey must move the +// individual lock and the aggregate lock to the destination hotkey. Before the +// fix the recipient's lock (and aggregate conviction) stayed on the origin +// hotkey while the stake landed on the destination hotkey. +#[test] +fn test_do_transfer_stake_and_hotkey_same_subnet_moves_lock_to_destination_hotkey() { + new_test_ext(1).execute_with(|| { + let coldkey_sender = U256::from(1); + let coldkey_receiver = U256::from(5); + let origin_hotkey = U256::from(2); + let destination_hotkey = U256::from(6); + let netuid = setup_subnet_with_stake(coldkey_sender, origin_hotkey, 100_000_000_000); + + // The destination hotkey is owned by the receiving coldkey, so origin and + // destination hotkeys have different owners. + assert_ok!(SubtensorModule::create_account_if_non_existent( + &coldkey_receiver, + &destination_hotkey + )); + DecayingLock::::insert(coldkey_receiver, netuid, false); + assert_ok!(SubtensorModule::set_reject_locked_alpha( + RuntimeOrigin::signed(coldkey_receiver), + false, + )); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_sender, netuid); + let lock_half = total / 2.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey_sender, + netuid, + &origin_hotkey, + lock_half, + )); + + let sender_lock_before = Lock::::get((coldkey_sender, netuid, origin_hotkey)) + .expect("sender lock should exist"); + + step_block(1); + + // Transfer the whole position (unlocked and locked halves) to the + // destination coldkey and hotkey. + assert_ok!(SubtensorModule::do_transfer_stake_and_hotkey( + RuntimeOrigin::signed(coldkey_sender), + coldkey_receiver, + origin_hotkey, + destination_hotkey, + netuid, + netuid, + total, + )); + + let expected_sender_lock = roll_forward_lock( + sender_lock_before, + SubtensorModule::get_current_block_as_u64(), + false, + true, + ); + + // The sender's lock is fully transferred away. + assert!(Lock::::get((coldkey_sender, netuid, origin_hotkey)).is_none()); + + // The receiver's lock follows the stake to the destination hotkey and + // does not stay stranded on the origin hotkey. + assert!(Lock::::get((coldkey_receiver, netuid, origin_hotkey)).is_none()); + let receiver_lock = Lock::::get((coldkey_receiver, netuid, destination_hotkey)) + .expect("receiver lock should exist on the destination hotkey"); + assert_eq!(receiver_lock.locked_mass, expected_sender_lock.locked_mass); + + // The hotkeys are owned by different coldkeys, so the transferred + // conviction is forfeited, mirroring do_move_lock. + assert_eq!(receiver_lock.conviction, U64F64::from_num(0)); + + // The aggregate lock moves off the origin hotkey and onto the destination hotkey. + assert!( + HotkeyLock::::get(netuid, origin_hotkey) + .map(|lock| lock.locked_mass) + .unwrap_or(AlphaBalance::ZERO) + .is_zero() + ); + let destination_hotkey_lock = HotkeyLock::::get(netuid, destination_hotkey) + .expect("destination hotkey aggregate lock should exist"); + assert_eq!( + destination_hotkey_lock.locked_mass, + expected_sender_lock.locked_mass + ); + }); +} + +// When origin and destination hotkeys share an owning coldkey, the transferred +// conviction follows the lock to the destination hotkey instead of being forfeited. +#[test] +fn test_do_transfer_stake_and_hotkey_same_owner_preserves_conviction() { + new_test_ext(1).execute_with(|| { + let coldkey_sender = U256::from(1); + let coldkey_receiver = U256::from(5); + let origin_hotkey = U256::from(2); + let destination_hotkey = U256::from(6); + let netuid = setup_subnet_with_stake(coldkey_sender, origin_hotkey, 100_000_000_000); + + // Both hotkeys are owned by the sending coldkey. + assert_ok!(SubtensorModule::create_account_if_non_existent( + &coldkey_sender, + &destination_hotkey + )); + DecayingLock::::insert(coldkey_receiver, netuid, false); + assert_ok!(SubtensorModule::set_reject_locked_alpha( + RuntimeOrigin::signed(coldkey_receiver), + false, + )); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_sender, netuid); + let lock_half = total / 2.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey_sender, + netuid, + &origin_hotkey, + lock_half, + )); + + let sender_lock_before = Lock::::get((coldkey_sender, netuid, origin_hotkey)) + .expect("sender lock should exist"); + + step_block(1); + + assert_ok!(SubtensorModule::do_transfer_stake_and_hotkey( + RuntimeOrigin::signed(coldkey_sender), + coldkey_receiver, + origin_hotkey, + destination_hotkey, + netuid, + netuid, + total, + )); + + let expected_sender_lock = roll_forward_lock( + sender_lock_before, + SubtensorModule::get_current_block_as_u64(), + false, + true, + ); + + let receiver_lock = Lock::::get((coldkey_receiver, netuid, destination_hotkey)) + .expect("receiver lock should exist on the destination hotkey"); + assert_eq!(receiver_lock.locked_mass, expected_sender_lock.locked_mass); + + // Same-owner hotkey change: the conviction moved with the lock. + assert!(receiver_lock.conviction > U64F64::from_num(0)); + assert!(receiver_lock.conviction <= expected_sender_lock.conviction); + }); +} + +// The LockHotkeyMismatch guard is checked against the hotkey the stake lands on: +// a recipient with an existing lock can only receive locked alpha onto that same +// hotkey, and transfers targeting any other hotkey are rejected. +#[test] +fn test_do_transfer_stake_and_hotkey_locked_requires_destination_match_receiver_lock() { + new_test_ext(1).execute_with(|| { + let coldkey_sender = U256::from(1); + let coldkey_receiver = U256::from(5); + let origin_hotkey = U256::from(2); + let receiver_lock_hotkey = U256::from(6); + let other_hotkey = U256::from(7); + let netuid = setup_subnet_with_stake(coldkey_sender, origin_hotkey, 100_000_000_000); + + assert_ok!(SubtensorModule::create_account_if_non_existent( + &coldkey_receiver, + &receiver_lock_hotkey + )); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &coldkey_receiver, + &other_hotkey + )); + DecayingLock::::insert(coldkey_receiver, netuid, false); + assert_ok!(SubtensorModule::set_reject_locked_alpha( + RuntimeOrigin::signed(coldkey_receiver), + false, + )); + + // The receiver already has an active lock on receiver_lock_hotkey. + let receiver_locked = AlphaBalance::from(1_000_000u64); + SubtensorModule::insert_lock_state( + &coldkey_receiver, + netuid, + &receiver_lock_hotkey, + LockState { + locked_mass: receiver_locked, + conviction: U64F64::from_num(0), + last_update: SubtensorModule::get_current_block_as_u64(), + }, + ); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_sender, netuid); + let lock_half = total / 2.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey_sender, + netuid, + &origin_hotkey, + lock_half, + )); + let sender_lock_before = Lock::::get((coldkey_sender, netuid, origin_hotkey)) + .expect("sender lock should exist"); + + step_block(1); + + // Locked alpha targeting a hotkey other than the receiver's lock hotkey fails. + assert_noop!( + SubtensorModule::do_transfer_stake_and_hotkey( + RuntimeOrigin::signed(coldkey_sender), + coldkey_receiver, + origin_hotkey, + other_hotkey, + netuid, + netuid, + total, + ), + Error::::LockHotkeyMismatch + ); + + // Targeting the receiver's lock hotkey succeeds even though it differs + // from the origin hotkey (the pre-fix check compared against the origin + // hotkey and would have rejected this). + assert_ok!(SubtensorModule::do_transfer_stake_and_hotkey( + RuntimeOrigin::signed(coldkey_sender), + coldkey_receiver, + origin_hotkey, + receiver_lock_hotkey, + netuid, + netuid, + total, + )); + + let expected_sender_lock = roll_forward_lock( + sender_lock_before, + SubtensorModule::get_current_block_as_u64(), + false, + true, + ); + let receiver_lock = Lock::::get((coldkey_receiver, netuid, receiver_lock_hotkey)) + .expect("receiver lock should exist on its lock hotkey"); + assert_eq!( + receiver_lock.locked_mass, + receiver_locked.saturating_add(expected_sender_lock.locked_mass) + ); + }); +} + +#[test] +fn test_move_stake_cross_subnet_blocked_by_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid_a = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let subnet_owner2_ck = U256::from(2001); + let subnet_owner2_hk = U256::from(2002); + let netuid_b = add_dynamic_network(&subnet_owner2_hk, &subnet_owner2_ck); + setup_reserves( + netuid_b, + (100_000_000_000u64 * 1_000_000).into(), + (100_000_000_000u64 * 10_000_000).into(), + ); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid_a); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid_a, &hotkey, total + )); + + step_block(1); + + let alpha = get_alpha(&hotkey, &coldkey, netuid_a); + assert_noop!( + SubtensorModule::do_move_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + hotkey, + netuid_a, + netuid_b, + alpha, + ), + Error::::StakeUnavailable + ); + }); +} + +#[test] +fn test_do_transfer_stake_rejects_locked_alpha_to_flagged_destination() { + new_test_ext(1).execute_with(|| { + let coldkey_sender = U256::from(1); + let coldkey_receiver = U256::from(5); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey_sender, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_sender, netuid); + let lock_half = total / 2.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey_sender, + netuid, + &hotkey, + lock_half, + )); + assert_ok!(SubtensorModule::set_reject_locked_alpha( + RuntimeOrigin::signed(coldkey_receiver), + true, + )); + + let sender_lock_before = + Lock::::get((coldkey_sender, netuid, hotkey)).expect("sender lock should exist"); + let sender_alpha_before = + SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_sender, netuid); + let receiver_alpha_before = + SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_receiver, netuid); + + assert_noop!( + SubtensorModule::do_transfer_stake( + RuntimeOrigin::signed(coldkey_sender), + coldkey_receiver, + hotkey, + netuid, + netuid, + total, + ), + Error::::AccountRejectsLockedAlpha + ); + + assert_eq!( + Lock::::get((coldkey_sender, netuid, hotkey)), + Some(sender_lock_before) + ); + assert!(Lock::::get((coldkey_receiver, netuid, hotkey)).is_none()); + assert_eq!( + SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_sender, netuid), + sender_alpha_before + ); + assert_eq!( + SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_receiver, netuid), + receiver_alpha_before + ); + }); +} + +#[test] +fn test_do_transfer_stake_allows_unlocked_alpha_to_flagged_destination() { + new_test_ext(1).execute_with(|| { + let coldkey_sender = U256::from(1); + let coldkey_receiver = U256::from(5); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey_sender, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_sender, netuid); + let lock_half = total / 2.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey_sender, + netuid, + &hotkey, + lock_half, + )); + assert_ok!(SubtensorModule::set_reject_locked_alpha( + RuntimeOrigin::signed(coldkey_receiver), + true, + )); + + let unlocked_transfer = lock_half / 2.into(); + assert_ok!(SubtensorModule::do_transfer_stake( + RuntimeOrigin::signed(coldkey_sender), + coldkey_receiver, + hotkey, + netuid, + netuid, + unlocked_transfer, + )); + + assert!(Lock::::get((coldkey_receiver, netuid, hotkey)).is_none()); + assert_eq!( + SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_receiver, netuid), + unlocked_transfer + ); + }); +} + +#[test] +fn test_transfer_stake_cross_coldkey_allowed_partial() { + new_test_ext(1).execute_with(|| { + let coldkey_sender = U256::from(1); + let coldkey_receiver = U256::from(5); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey_sender, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey_sender, netuid); + let lock_half = total / 2.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey_sender, + netuid, + &hotkey, + lock_half, + )); + + let sender_lock_before = + Lock::::get((coldkey_sender, netuid, hotkey)).expect("sender lock should exist"); + + step_block(1); + + // Transfer the unlocked portion + let alpha = get_alpha(&hotkey, &coldkey_sender, netuid); + let transfer_amount = alpha / 4.into(); // well within the unlocked half + assert_ok!(SubtensorModule::do_transfer_stake( + RuntimeOrigin::signed(coldkey_sender), + coldkey_receiver, + hotkey, + netuid, + netuid, + transfer_amount, + )); + + let sender_lock_after = + Lock::::get((coldkey_sender, netuid, hotkey)).expect("sender lock should remain"); + assert_eq!( + sender_lock_after.locked_mass, + roll_forward_lock(sender_lock_before, 2, false, true).locked_mass + ); + assert!(Lock::::get((coldkey_receiver, netuid, hotkey)).is_none()); + }); +} diff --git a/pallets/subtensor/src/tests/locks/moving_lock.rs b/pallets/subtensor/src/tests/locks/moving_lock.rs new file mode 100644 index 0000000000..cfb27f3618 --- /dev/null +++ b/pallets/subtensor/src/tests/locks/moving_lock.rs @@ -0,0 +1,439 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Moving lock. + +use super::helpers::*; +use super::prelude::*; + +// ========================================================================= +// GROUP 19: Moving lock +// ========================================================================= + +#[test] +fn test_moving_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey_origin = U256::from(2); + let hotkey_destination = U256::from(3); + let netuid = setup_subnet_with_stake(coldkey, hotkey_origin, 100_000_000_000); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &coldkey, + &hotkey_destination + )); + + let lock_amount = 5000u64.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey_origin, + lock_amount + )); + + // Mock a non-zero conviction + let mut lock = Lock::::get((coldkey, netuid, hotkey_origin)).unwrap(); + lock.conviction = U64F64::from_num(1234); + Lock::::insert((coldkey, netuid, hotkey_origin), lock); + let mut hotkey_lock = HotkeyLock::::get(netuid, hotkey_origin).unwrap(); + hotkey_lock.conviction = U64F64::from_num(1234); + HotkeyLock::::insert(netuid, hotkey_origin, hotkey_lock); + + assert_ok!(SubtensorModule::move_lock( + RuntimeOrigin::signed(coldkey), + hotkey_destination, + netuid, + )); + let lock = Lock::::get((coldkey, netuid, hotkey_destination)).unwrap(); + assert_eq!(lock.locked_mass, lock_amount); + assert_eq!(lock.conviction, U64F64::from_num(1234)); + + // Hotkey lock is removed on origin and added on destination + assert!(HotkeyLock::::get(netuid, hotkey_origin).is_none()); + let hotkey_lock_destination_after = + HotkeyLock::::get(netuid, hotkey_destination).unwrap(); + assert_eq!(hotkey_lock_destination_after.locked_mass, lock_amount); + + // Conviction is not reset because owner is the same for origin and destination + // hotkeys + assert_eq!( + hotkey_lock_destination_after.conviction, + U64F64::from_num(1234) + ); + }); +} + +#[test] +fn test_moving_lock_to_subnet_owner_hotkey_gets_owner_conviction_for_non_owner_coldkey() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey_origin = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey_origin, 100_000_000_000); + let owner_hotkey = SubnetOwnerHotkey::::get(netuid); + + let lock_amount = 5000u64.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey_origin, + lock_amount + )); + + assert_ok!(SubtensorModule::move_lock( + RuntimeOrigin::signed(coldkey), + owner_hotkey, + netuid, + )); + + let lock = Lock::::get((coldkey, netuid, owner_hotkey)).unwrap(); + assert_eq!(lock.locked_mass, lock_amount); + assert_eq!(lock.conviction, U64F64::from_num(5000)); + + assert!( + HotkeyLock::::get(netuid, owner_hotkey).is_none(), + "lock moved to owner hotkey should use OwnerLock" + ); + let owner_lock = OwnerLock::::get(netuid).unwrap(); + assert_eq!(owner_lock.locked_mass, lock_amount); + assert_eq!(owner_lock.conviction, U64F64::from_num(5000)); + }); +} + +#[test] +fn test_moving_partial_lock() { + new_test_ext(1).execute_with(|| { + let coldkey1 = U256::from(1); + let coldkey2 = U256::from(2); + let hotkey_origin = U256::from(3); + let hotkey_destination = U256::from(4); + let netuid = setup_subnet_with_stake(coldkey1, hotkey_origin, 100_000_000_000); + + // Make hotkey_origin and hotkey_destination owned by different coldkeys + assert_ok!(SubtensorModule::create_account_if_non_existent( + &coldkey1, + &hotkey_origin + )); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &coldkey2, + &hotkey_destination + )); + + // Add coldkey2 stake + add_balance_to_coldkey_account(&coldkey2, 100_000_000_000u64.into()); + SubtensorModule::stake_into_subnet( + &hotkey_origin, + &coldkey2, + netuid, + 50_000_000_000u64.into(), + ::SwapInterface::max_price(), + false, + ) + .unwrap(); + DecayingLock::::insert(coldkey2, netuid, false); + + let lock_amount = 5000u64.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey1, + netuid, + &hotkey_origin, + lock_amount + )); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey2, + netuid, + &hotkey_origin, + lock_amount + )); + + // Mock a non-zero conviction + let mut lock1 = Lock::::get((coldkey1, netuid, hotkey_origin)).unwrap(); + lock1.conviction = U64F64::from_num(1000); + Lock::::insert((coldkey1, netuid, hotkey_origin), lock1); + let mut lock2 = Lock::::get((coldkey2, netuid, hotkey_origin)).unwrap(); + lock2.conviction = U64F64::from_num(1000); + Lock::::insert((coldkey2, netuid, hotkey_origin), lock2); + let mut hotkey_lock = HotkeyLock::::get(netuid, hotkey_origin).unwrap(); + hotkey_lock.conviction = U64F64::from_num(2000); + HotkeyLock::::insert(netuid, hotkey_origin, hotkey_lock); + + // Move lock for coldkey1 to hotkey_destination, coldkey2's lock should be unaffected + assert_ok!(SubtensorModule::move_lock( + RuntimeOrigin::signed(coldkey1), + hotkey_destination, + netuid, + )); + let lock1_after = Lock::::get((coldkey1, netuid, hotkey_destination)).unwrap(); + let lock2_after = Lock::::get((coldkey2, netuid, hotkey_origin)).unwrap(); + assert_eq!(lock1_after.locked_mass, lock_amount); + assert_eq!(lock1_after.conviction, U64F64::from_num(0)); + assert_eq!(lock2_after.locked_mass, lock_amount); + assert_eq!(lock2_after.conviction, U64F64::from_num(1000)); + + // Hotkey lock is removed on origin and added on destination + let hotkey_lock_origin_after = HotkeyLock::::get(netuid, hotkey_origin).unwrap(); + let hotkey_lock_destination_after = + HotkeyLock::::get(netuid, hotkey_destination).unwrap(); + assert_eq!(hotkey_lock_origin_after.locked_mass, lock_amount); + assert_eq!(hotkey_lock_origin_after.conviction, U64F64::from_num(1000)); + assert_eq!(hotkey_lock_destination_after.locked_mass, lock_amount); + assert_eq!( + hotkey_lock_destination_after.conviction, + U64F64::from_num(0) + ); + }); +} + +#[test] +fn test_moving_partial_lock_same_owners() { + new_test_ext(1).execute_with(|| { + let coldkey1 = U256::from(1); + let coldkey2 = U256::from(2); + let hotkey_origin = U256::from(3); + let hotkey_destination = U256::from(4); + let netuid = setup_subnet_with_stake(coldkey1, hotkey_origin, 100_000_000_000); + + // Add coldkey2 stake + add_balance_to_coldkey_account(&coldkey2, 100_000_000_000u64.into()); + + // Make hotkey_origin and hotkey_destination both owned by coldkey1 + assert_ok!(SubtensorModule::create_account_if_non_existent( + &coldkey1, + &hotkey_origin + )); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &coldkey1, + &hotkey_destination + )); + SubtensorModule::stake_into_subnet( + &hotkey_origin, + &coldkey2, + netuid, + 50_000_000_000u64.into(), + ::SwapInterface::max_price(), + false, + ) + .unwrap(); + DecayingLock::::insert(coldkey2, netuid, false); + + let lock_amount = 5000u64.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey1, + netuid, + &hotkey_origin, + lock_amount + )); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey2, + netuid, + &hotkey_origin, + lock_amount + )); + + // Mock a non-zero conviction + let mut lock1 = Lock::::get((coldkey1, netuid, hotkey_origin)).unwrap(); + lock1.conviction = U64F64::from_num(1000); + Lock::::insert((coldkey1, netuid, hotkey_origin), lock1); + let mut lock2 = Lock::::get((coldkey2, netuid, hotkey_origin)).unwrap(); + lock2.conviction = U64F64::from_num(1000); + Lock::::insert((coldkey2, netuid, hotkey_origin), lock2); + let mut hotkey_lock = HotkeyLock::::get(netuid, hotkey_origin).unwrap(); + hotkey_lock.conviction = U64F64::from_num(2000); + HotkeyLock::::insert(netuid, hotkey_origin, hotkey_lock); + + // Move lock for coldkey1 to hotkey_destination, coldkey2's lock should be unaffected + assert_ok!(SubtensorModule::move_lock( + RuntimeOrigin::signed(coldkey1), + hotkey_destination, + netuid, + )); + let lock1_after = Lock::::get((coldkey1, netuid, hotkey_destination)).unwrap(); + let lock2_after = Lock::::get((coldkey2, netuid, hotkey_origin)).unwrap(); + assert_eq!(lock1_after.locked_mass, lock_amount); + assert_eq!(lock1_after.conviction, U64F64::from_num(1000)); + assert_eq!(lock2_after.locked_mass, lock_amount); + assert_eq!(lock2_after.conviction, U64F64::from_num(1000)); + + // Hotkey lock is moved to destination with conviction + let hotkey_lock_origin_after = HotkeyLock::::get(netuid, hotkey_origin).unwrap(); + let hotkey_lock_destination_after = + HotkeyLock::::get(netuid, hotkey_destination).unwrap(); + assert_eq!(hotkey_lock_origin_after.locked_mass, lock_amount); + assert_eq!(hotkey_lock_origin_after.conviction, U64F64::from_num(1000)); + assert_eq!(hotkey_lock_destination_after.locked_mass, lock_amount); + assert_eq!( + hotkey_lock_destination_after.conviction, + U64F64::from_num(1000) + ); + }); +} + +#[test] +fn test_hotkey_swap_moves_lock_and_conviction_to_new_hotkey() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let old_hotkey = U256::from(2); + let new_hotkey = U256::from(3); + let netuid = setup_subnet_with_stake(coldkey, old_hotkey, 100_000_000_000); + let lock_amount: AlphaBalance = 5000u64.into(); + let conviction = U64F64::from_num(1000); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &old_hotkey, + lock_amount, + )); + + let mut lock = Lock::::get((coldkey, netuid, old_hotkey)).unwrap(); + lock.conviction = conviction; + Lock::::insert((coldkey, netuid, old_hotkey), lock); + + let mut hotkey_lock = HotkeyLock::::get(netuid, old_hotkey).unwrap(); + hotkey_lock.conviction = conviction; + HotkeyLock::::insert(netuid, old_hotkey, hotkey_lock); + + add_balance_to_coldkey_account( + &coldkey, + (SubtensorModule::get_key_swap_cost() + 1000.into()).into(), + ); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + None, + false, + )); + + assert!(Lock::::get((coldkey, netuid, old_hotkey)).is_none()); + assert!(HotkeyLock::::get(netuid, old_hotkey).is_none()); + + let moved_lock = Lock::::get((coldkey, netuid, new_hotkey)).unwrap(); + assert_eq!(moved_lock.locked_mass, lock_amount); + assert_eq!(moved_lock.conviction, conviction); + + let moved_hotkey_lock = HotkeyLock::::get(netuid, new_hotkey).unwrap(); + assert_eq!(moved_hotkey_lock.locked_mass, lock_amount); + assert_eq!(moved_hotkey_lock.conviction, conviction); + assert_eq!( + SubtensorModule::hotkey_conviction(&new_hotkey, netuid), + conviction + ); + }); +} + +#[test] +fn test_swap_hotkey_v2_on_subnet_moves_lock_and_conviction_to_new_hotkey() { + new_test_ext(100).execute_with(|| { + let coldkey = U256::from(1); + let old_hotkey = U256::from(2); + let new_hotkey = U256::from(3); + let netuid = setup_subnet_with_stake(coldkey, old_hotkey, 100_000_000_000); + let lock_amount: AlphaBalance = 5000u64.into(); + let conviction = U64F64::from_num(1000); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &old_hotkey, + lock_amount, + )); + + let mut lock = Lock::::get((coldkey, netuid, old_hotkey)).unwrap(); + lock.conviction = conviction; + Lock::::insert((coldkey, netuid, old_hotkey), lock); + + let mut hotkey_lock = HotkeyLock::::get(netuid, old_hotkey).unwrap(); + hotkey_lock.conviction = conviction; + HotkeyLock::::insert(netuid, old_hotkey, hotkey_lock); + + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000u64.into()); + assert_ok!(SubtensorModule::swap_hotkey_v2( + RuntimeOrigin::signed(coldkey), + old_hotkey, + new_hotkey, + Some(netuid), + false, + )); + + assert!(Lock::::get((coldkey, netuid, old_hotkey)).is_none()); + assert!(HotkeyLock::::get(netuid, old_hotkey).is_none()); + + let moved_lock = Lock::::get((coldkey, netuid, new_hotkey)).unwrap(); + assert_eq!(moved_lock.locked_mass, lock_amount); + assert_eq!(moved_lock.conviction, conviction); + + let moved_hotkey_lock = HotkeyLock::::get(netuid, new_hotkey).unwrap(); + assert_eq!(moved_hotkey_lock.locked_mass, lock_amount); + assert_eq!(moved_hotkey_lock.conviction, conviction); + assert_eq!( + SubtensorModule::hotkey_conviction(&new_hotkey, netuid), + conviction + ); + }); +} + +#[test] +fn test_swap_hotkey_v2_on_subnet_does_not_move_locks_on_other_subnets() { + new_test_ext(100).execute_with(|| { + let coldkey = U256::from(1); + let old_hotkey = U256::from(2); + let new_hotkey = U256::from(3); + let swapped_netuid = setup_subnet_with_stake(coldkey, old_hotkey, 100_000_000_000); + let untouched_netuid = setup_subnet_with_stake(coldkey, old_hotkey, 100_000_000_000); + let lock_amount: AlphaBalance = 5000u64.into(); + let conviction = U64F64::from_num(1000); + + for netuid in [swapped_netuid, untouched_netuid] { + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &old_hotkey, + lock_amount, + )); + + let mut lock = Lock::::get((coldkey, netuid, old_hotkey)).unwrap(); + lock.conviction = conviction; + Lock::::insert((coldkey, netuid, old_hotkey), lock); + + let mut hotkey_lock = HotkeyLock::::get(netuid, old_hotkey).unwrap(); + hotkey_lock.conviction = conviction; + HotkeyLock::::insert(netuid, old_hotkey, hotkey_lock); + } + + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000u64.into()); + assert_ok!(SubtensorModule::swap_hotkey_v2( + RuntimeOrigin::signed(coldkey), + old_hotkey, + new_hotkey, + Some(swapped_netuid), + false, + )); + + assert!(Lock::::get((coldkey, swapped_netuid, old_hotkey)).is_none()); + assert!(HotkeyLock::::get(swapped_netuid, old_hotkey).is_none()); + assert_eq!( + Lock::::get((coldkey, swapped_netuid, new_hotkey)) + .unwrap() + .conviction, + conviction + ); + assert_eq!( + HotkeyLock::::get(swapped_netuid, new_hotkey) + .unwrap() + .conviction, + conviction + ); + + let untouched_lock = Lock::::get((coldkey, untouched_netuid, old_hotkey)).unwrap(); + assert_eq!(untouched_lock.locked_mass, lock_amount); + assert_eq!(untouched_lock.conviction, conviction); + assert!(Lock::::get((coldkey, untouched_netuid, new_hotkey)).is_none()); + + let untouched_hotkey_lock = HotkeyLock::::get(untouched_netuid, old_hotkey).unwrap(); + assert_eq!(untouched_hotkey_lock.locked_mass, lock_amount); + assert_eq!(untouched_hotkey_lock.conviction, conviction); + assert!(HotkeyLock::::get(untouched_netuid, new_hotkey).is_none()); + }); +} diff --git a/pallets/subtensor/src/tests/locks/multi_subnet_locks.rs b/pallets/subtensor/src/tests/locks/multi_subnet_locks.rs new file mode 100644 index 0000000000..42e65aafbd --- /dev/null +++ b/pallets/subtensor/src/tests/locks/multi_subnet_locks.rs @@ -0,0 +1,134 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Multi-subnet locks. + +use super::helpers::*; +use super::prelude::*; + +// ========================================================================= +// GROUP 8: Multi-subnet locks +// ========================================================================= + +#[test] +fn test_lock_on_multiple_subnets() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey_a = U256::from(2); + let hotkey_b = U256::from(3); + + let netuid_a = setup_subnet_with_stake(coldkey, hotkey_a, 100_000_000_000); + + let subnet_owner2_ck = U256::from(2001); + let subnet_owner2_hk = U256::from(2002); + let netuid_b = add_dynamic_network(&subnet_owner2_hk, &subnet_owner2_ck); + setup_reserves( + netuid_b, + (100_000_000_000u64 * 1_000_000).into(), + (100_000_000_000u64 * 10_000_000).into(), + ); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &coldkey, &hotkey_b + )); + add_balance_to_coldkey_account(&coldkey, 100_000_000_000u64.into()); + SubtensorModule::stake_into_subnet( + &hotkey_b, + &coldkey, + netuid_b, + 100_000_000_000u64.into(), + ::SwapInterface::max_price(), + false, + ) + .unwrap(); + DecayingLock::::insert(coldkey, netuid_b, false); + + // Lock on subnet A to hotkey_a + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid_a, + &hotkey_a, + 1000u64.into(), + )); + + // Lock on subnet B to hotkey_b (different hotkey is fine — different subnet) + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid_b, + &hotkey_b, + 2000u64.into(), + )); + + let lock_a = Lock::::get((coldkey, netuid_a, hotkey_a)).unwrap(); + let lock_b = Lock::::get((coldkey, netuid_b, hotkey_b)).unwrap(); + assert_eq!(lock_a.locked_mass, 1000u64.into()); + assert_eq!(lock_b.locked_mass, 2000u64.into()); + + // Hotkey locks should also be separate + let hotkey_lock_a = HotkeyLock::::get(netuid_a, hotkey_a).unwrap(); + let hotkey_lock_b = HotkeyLock::::get(netuid_b, hotkey_b).unwrap(); + assert_eq!(hotkey_lock_a.locked_mass, 1000u64.into()); + assert_eq!(hotkey_lock_b.locked_mass, 2000u64.into()); + }); +} + +#[test] +fn test_unstake_one_subnet_does_not_affect_other() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid_a = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + // Lock on subnet A + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid_a, + &hotkey, + 5000u64.into(), + )); + + // Subnet B — no lock, just stake + let subnet_owner2_ck = U256::from(2001); + let subnet_owner2_hk = U256::from(2002); + let netuid_b = add_dynamic_network(&subnet_owner2_hk, &subnet_owner2_ck); + setup_reserves( + netuid_b, + (100_000_000_000u64 * 1_000_000).into(), + (100_000_000_000u64 * 10_000_000).into(), + ); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &coldkey, &hotkey + )); + add_balance_to_coldkey_account(&coldkey, 100_000_000_000u64.into()); + SubtensorModule::stake_into_subnet( + &hotkey, + &coldkey, + netuid_b, + 100_000_000_000u64.into(), + ::SwapInterface::max_price(), + false, + ) + .unwrap(); + + step_block(1); + + // Unstake from subnet B — should succeed (no lock there) + let alpha_b = get_alpha(&hotkey, &coldkey, netuid_b); + assert_ok!(SubtensorModule::do_remove_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid_b, + alpha_b, + )); + + // Lock on subnet A unaffected + let lock_a = Lock::::get((coldkey, netuid_a, hotkey)).unwrap(); + assert_eq!(lock_a.locked_mass, 5000u64.into()); + + // Hotkey lock on subnet A also unaffected + let hotkey_lock_a = HotkeyLock::::get(netuid_a, hotkey).unwrap(); + assert_eq!(hotkey_lock_a.locked_mass, 5000u64.into()); + }); +} diff --git a/pallets/subtensor/src/tests/locks/neuron_replacement_lock.rs b/pallets/subtensor/src/tests/locks/neuron_replacement_lock.rs new file mode 100644 index 0000000000..e8d68e8c5f --- /dev/null +++ b/pallets/subtensor/src/tests/locks/neuron_replacement_lock.rs @@ -0,0 +1,63 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Neuron replacement. + +use super::helpers::*; +use super::prelude::*; + +// ========================================================================= +// GROUP 18: Neuron replacement +// ========================================================================= + +#[test] +fn test_neuron_replacement_does_not_affect_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + // Register the hotkey as a neuron + register_ok_neuron(netuid, hotkey, coldkey, 0); + + let lock_amount = 5000u64.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount + )); + assert_ok!(SubtensorModule::do_set_perpetual_lock( + &coldkey, netuid, false, + )); + + let total_before = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + let locked_before = SubtensorModule::get_current_locked(&coldkey, netuid); + + // Replace the neuron with a different hotkey + let new_hotkey = U256::from(99); + let uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey).unwrap(); + SubtensorModule::replace_neuron( + netuid, + uid, + &new_hotkey, + SubtensorModule::get_current_block_as_u64(), + ); + + // Alpha and lock should be unaffected by neuron replacement + let total_after = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + let locked_after = SubtensorModule::get_current_locked(&coldkey, netuid); + + assert_eq!(total_after, total_before); + assert_eq!(locked_after, locked_before); + + // Lock still references original hotkey + assert!(Lock::::get((coldkey, netuid, hotkey)).is_some()); + + // Aggregate lock still references original hotkey + assert!(DecayingHotkeyLock::::get(netuid, hotkey).is_some()); + }); +} diff --git a/pallets/subtensor/src/tests/locks/prelude.rs b/pallets/subtensor/src/tests/locks/prelude.rs new file mode 100644 index 0000000000..40b80b033e --- /dev/null +++ b/pallets/subtensor/src/tests/locks/prelude.rs @@ -0,0 +1,21 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Shared imports for lock unit tests. + +pub use approx::assert_abs_diff_eq; +pub use frame_support::dispatch::{GetDispatchInfo, Pays}; +pub use frame_support::weights::Weight; +pub use frame_support::{assert_noop, assert_ok}; +pub use safe_math::FixedExt; +pub use sp_core::U256; +pub use substrate_fixed::types::U64F64; +pub use subtensor_runtime_common::{AlphaBalance, NetUidStorageIndex, TaoBalance}; +pub use subtensor_swap_interface::SwapHandler; + +pub use super::super::mock::*; +pub use crate::staking::lock::{ConvictionModel, LockState}; +pub use crate::*; diff --git a/pallets/subtensor/src/tests/locks/recycle_burn_lock.rs b/pallets/subtensor/src/tests/locks/recycle_burn_lock.rs new file mode 100644 index 0000000000..a390d7c62e --- /dev/null +++ b/pallets/subtensor/src/tests/locks/recycle_burn_lock.rs @@ -0,0 +1,91 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Recycle/burn alpha checks against lock. + +use super::helpers::*; +use super::prelude::*; + +// ========================================================================= +// GROUP 14: Recycle/burn alpha checks against lock +// ========================================================================= + +#[test] +fn test_recycle_alpha_checks_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, total)); + + step_block(1); + + // Unstake should be blocked + let alpha = get_alpha(&hotkey, &coldkey, netuid); + assert_noop!( + SubtensorModule::do_remove_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + alpha, + ), + Error::::StakeUnavailable + ); + + // recycle_alpha checks lock and should fail if it would reduce alpha below locked amount + let recycle_amount = alpha / 2.into(); + assert_noop!( + SubtensorModule::do_recycle_alpha( + RuntimeOrigin::signed(coldkey), + hotkey, + recycle_amount, + netuid, + ), + Error::::StakeUnavailable + ); + + // Alpha is not below locked_mass + let total_after = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + let locked = SubtensorModule::get_current_locked(&coldkey, netuid); + assert!(total_after >= locked); + }); +} + +#[test] +fn test_burn_alpha_checks_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid, &hotkey, total + )); + + step_block(1); + + // burn_alpha checks lock and should fail if it would reduce alpha below locked amount + let alpha = get_alpha(&hotkey, &coldkey, netuid); + let burn_amount = alpha / 2.into(); + assert_noop!( + SubtensorModule::do_burn_alpha( + RuntimeOrigin::signed(coldkey), + hotkey, + burn_amount, + netuid, + ), + Error::::StakeUnavailable + ); + + // Alpha is not below locked_mass + let total_after = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + let locked = SubtensorModule::get_current_locked(&coldkey, netuid); + assert!(total_after >= locked); + }); +} diff --git a/pallets/subtensor/src/tests/locks/subnet_dissolution_lock.rs b/pallets/subtensor/src/tests/locks/subnet_dissolution_lock.rs new file mode 100644 index 0000000000..e9269f6839 --- /dev/null +++ b/pallets/subtensor/src/tests/locks/subnet_dissolution_lock.rs @@ -0,0 +1,78 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Subnet dissolution. + +use super::helpers::*; +use super::prelude::*; + +// ========================================================================= +// GROUP 15: Subnet dissolution +// ========================================================================= + +#[test] +fn test_subnet_dissolution_orphans_locks() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + 5000u64.into(), + )); + assert!(Lock::::get((coldkey, netuid, hotkey)).is_some()); + + // Dissolve the subnet + assert_ok!(SubtensorModule::do_dissolve_network(netuid)); + run_block_idle(); + + // All Alpha entries are gone + assert_eq!( + SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid), + AlphaBalance::ZERO + ); + + // Lock entries are not orphaned + let lock = Lock::::get((coldkey, netuid, hotkey)); + assert!(lock.is_none()); + + // Hotkey lock is also removed + let hotkey_lock = HotkeyLock::::get(netuid, hotkey); + assert!(hotkey_lock.is_none()); + }); +} + +#[test] +fn test_subnet_dissolution_and_netuid_reuse() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey_old = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey_old, 100_000_000_000); + + // Lock on the old subnet + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey_old, + 5000u64.into(), + )); + + // Dissolve old subnet + assert_ok!(SubtensorModule::do_dissolve_network(netuid)); + run_block_idle(); + + // No stale lock from old subnet remains + let stale_lock = Lock::::get((coldkey, netuid, hotkey_old)); + assert!(stale_lock.is_none()); + + // No stale hotkey lock remains + let stale_hotkey_lock = HotkeyLock::::get(netuid, hotkey_old); + assert!(stale_hotkey_lock.is_none()); + }); +} diff --git a/pallets/subtensor/src/tests/locks/unstake_lock_invariant.rs b/pallets/subtensor/src/tests/locks/unstake_lock_invariant.rs new file mode 100644 index 0000000000..80ea8540e7 --- /dev/null +++ b/pallets/subtensor/src/tests/locks/unstake_lock_invariant.rs @@ -0,0 +1,241 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] +//! Unstake invariant enforcement. + +use super::helpers::*; +use super::prelude::*; + +// ========================================================================= +// GROUP 6: Unstake invariant enforcement +// ========================================================================= + +#[test] +fn test_unstake_allowed_when_no_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let alpha = get_alpha(&hotkey, &coldkey, netuid); + assert!(alpha > AlphaBalance::ZERO); + + assert_ok!(SubtensorModule::do_remove_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + alpha, + )); + }); +} + +#[test] +fn test_unstake_allowed_up_to_available() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + let lock_amount = total / 2.into(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount + )); + + // Unstake the unlocked half + let alpha = get_alpha(&hotkey, &coldkey, netuid); + let available_alpha: u64 = (alpha.to_u64()) / 2; + // Need to step a block to pass rate limiter + step_block(1); + assert_ok!(SubtensorModule::do_remove_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + available_alpha.into(), + )); + }); +} + +#[test] +fn test_unstake_rolls_forward_existing_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + let lock_amount = AlphaBalance::from(1_000_000_000u64); + + DecayingLock::::remove(coldkey, netuid); + let lock_block = SubtensorModule::get_current_block_as_u64(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount, + )); + + step_block(100); + let now = SubtensorModule::get_current_block_as_u64(); + let expected = roll_forward_decaying_hotkey_lock( + LockState { + locked_mass: lock_amount, + conviction: U64F64::from_num(0), + last_update: lock_block, + }, + now, + ); + + assert_ok!(SubtensorModule::do_remove_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + lock_amount, + )); + + assert_eq!( + Lock::::get((coldkey, netuid, hotkey)).expect("lock should remain"), + expected + ); + let aggregate = + DecayingHotkeyLock::::get(netuid, hotkey).expect("aggregate should remain"); + assert_eq!(aggregate.locked_mass, expected.locked_mass); + assert_eq!(aggregate.last_update, now); + }); +} + +#[test] +fn test_unstake_roll_forward_collects_decaying_lock_dust_from_hotkey_aggregate() { + new_test_ext(1).execute_with(|| { + const ONE_ALPHA: u64 = 1_000_000_000; + const DUST_ALPHA: u64 = 100; + const STAKE_TAO_RAO: u64 = 1_000 * 1_000_000_000; + + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let coldkey_1 = U256::from(2001); + let coldkey_2 = U256::from(2002); + let hotkey_1 = U256::from(3001); + let hotkey_2 = U256::from(3002); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + setup_reserves( + netuid, + (STAKE_TAO_RAO * 1_000).into(), + (STAKE_TAO_RAO * 10_000).into(), + ); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &coldkey_1, &hotkey_1 + )); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &coldkey_1, &hotkey_2 + )); + + for coldkey in [coldkey_1, coldkey_2] { + add_balance_to_coldkey_account(&coldkey, STAKE_TAO_RAO.into()); + SubtensorModule::stake_into_subnet( + &hotkey_1, + &coldkey, + netuid, + STAKE_TAO_RAO.into(), + ::SwapInterface::max_price(), + false, + ) + .unwrap(); + } + + let lock_block = SubtensorModule::get_current_block_as_u64(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey_1, + netuid, + &hotkey_2, + ONE_ALPHA.into(), + )); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey_2, + netuid, + &hotkey_2, + DUST_ALPHA.into(), + )); + + assert_eq!( + DecayingHotkeyLock::::get(netuid, hotkey_2) + .expect("decaying aggregate should exist") + .locked_mass, + AlphaBalance::from(ONE_ALPHA + DUST_ALPHA) + ); + + step_block(100); + let now = SubtensorModule::get_current_block_as_u64(); + let rolled_large_lock = roll_forward_decaying_hotkey_lock( + LockState { + locked_mass: ONE_ALPHA.into(), + conviction: U64F64::from_num(0), + last_update: lock_block, + }, + now, + ); + + assert_ok!(SubtensorModule::do_remove_stake( + RuntimeOrigin::signed(coldkey_1), + hotkey_1, + netuid, + ONE_ALPHA.into(), + )); + assert_eq!( + Lock::::get((coldkey_1, netuid, hotkey_2)).expect("coldkey1 lock should remain"), + rolled_large_lock + ); + assert_eq!( + DecayingHotkeyLock::::get(netuid, hotkey_2) + .expect("decaying aggregate should remain") + .locked_mass, + rolled_large_lock + .locked_mass + .saturating_add(AlphaBalance::from(DUST_ALPHA)) + ); + + assert_ok!(SubtensorModule::do_remove_stake( + RuntimeOrigin::signed(coldkey_2), + hotkey_1, + netuid, + ONE_ALPHA.into(), + )); + assert_eq!( + DecayingHotkeyLock::::get(netuid, hotkey_2) + .expect("decaying aggregate should remain") + .locked_mass, + rolled_large_lock.locked_mass + ); + }); +} + +#[test] +fn test_unstake_blocked_by_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + // Lock the entire amount + assert_ok!(SubtensorModule::do_lock_stake(&coldkey, netuid, &hotkey, total)); + + step_block(1); + + let alpha = get_alpha(&hotkey, &coldkey, netuid); + assert_noop!( + SubtensorModule::do_remove_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + alpha, + ), + Error::::StakeUnavailable + ); + }); +} diff --git a/pallets/subtensor/src/tests/math.rs b/pallets/subtensor/src/tests/math.rs deleted file mode 100644 index 6591d975b0..0000000000 --- a/pallets/subtensor/src/tests/math.rs +++ /dev/null @@ -1,2601 +0,0 @@ -#![allow( - clippy::arithmetic_side_effects, - clippy::unwrap_used, - clippy::indexing_slicing -)] -use substrate_fixed::types::{I32F32, I64F64}; - -use crate::epoch::math::*; -use rand::{RngExt, seq::SliceRandom}; -use substrate_fixed::{ - transcendental::exp, - types::{I96F32, I110F18}, -}; - -fn assert_float_compare(a: I32F32, b: I32F32, epsilon: I32F32) { - assert!(I32F32::abs(a - b) <= epsilon, "a({a:?}) != b({b:?})"); -} - -fn assert_float_compare_64(a: I64F64, b: I64F64, epsilon: I64F64) { - assert!(I64F64::abs(a - b) <= epsilon, "a({a:?}) != b({b:?})"); -} - -fn assert_vec_compare(va: &[I32F32], vb: &[I32F32], epsilon: I32F32) { - assert!(va.len() == vb.len()); - for i in 0..va.len() { - assert_float_compare(va[i], vb[i], epsilon); - } -} - -fn assert_vec_compare_64(va: &[I64F64], vb: &[I64F64], epsilon: I64F64) { - assert!(va.len() == vb.len()); - for i in 0..va.len() { - assert_float_compare_64(va[i], vb[i], epsilon); - } -} - -fn assert_vec_compare_u16(va: &[u16], vb: &[u16]) { - assert!(va.len() == vb.len()); - for i in 0..va.len() { - assert_eq!(va[i], vb[i]); - } -} - -pub fn assert_mat_compare(ma: &[Vec], mb: &[Vec], epsilon: I32F32) { - assert!(ma.len() == mb.len()); - for row in 0..ma.len() { - assert!(ma[row].len() == mb[row].len()); - for col in 0..ma[row].len() { - assert_float_compare(ma[row][col], mb[row][col], epsilon) - } - } -} - -fn assert_sparse_mat_compare( - ma: &[Vec<(u16, I32F32)>], - mb: &[Vec<(u16, I32F32)>], - epsilon: I32F32, -) { - assert!(ma.len() == mb.len()); - for row in 0..ma.len() { - assert!( - ma[row].len() == mb[row].len(), - "row: {}, ma: {:?}, mb: {:?}", - row, - ma[row], - mb[row] - ); - for j in 0..ma[row].len() { - assert!(ma[row][j].0 == mb[row][j].0); // u16 - assert_float_compare(ma[row][j].1, mb[row][j].1, epsilon) // I32F32 - } - } -} - -pub fn vec_to_fixed(vector: &[f32]) -> Vec { - vector.iter().map(|x| I32F32::from_num(*x)).collect() -} - -fn mat_to_fixed(matrix: &[Vec]) -> Vec> { - matrix.iter().map(|row| vec_to_fixed(row)).collect() -} - -fn assert_mat_approx_eq(left: &[Vec], right: &[Vec], epsilon: I32F32) { - assert_eq!(left.len(), right.len()); - for (left_row, right_row) in left.iter().zip(right.iter()) { - assert_eq!(left_row.len(), right_row.len()); - for (left_val, right_val) in left_row.iter().zip(right_row.iter()) { - assert!( - (left_val - right_val).abs() <= epsilon, - "left: {left_val:?}, right: {right_val:?}" - ); - } - } -} - -#[test] -fn test_vec_max_upscale_to_u16() { - let vector: Vec = vec_to_fixed(&[]); - let target: Vec = vec![]; - let result: Vec = vec_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &target); - let vector: Vec = vec_to_fixed(&[0.]); - let target: Vec = vec![0]; - let result: Vec = vec_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &target); - let vector: Vec = vec_to_fixed(&[0., 0.]); - let target: Vec = vec![0, 0]; - let result: Vec = vec_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &target); - let vector: Vec = vec_to_fixed(&[0., 1.]); - let target: Vec = vec![0, 65535]; - let result: Vec = vec_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &target); - let vector: Vec = vec_to_fixed(&[0., 0.000000001]); - let target: Vec = vec![0, 65535]; - let result: Vec = vec_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &target); - let vector: Vec = vec_to_fixed(&[0., 0.000016, 1.]); - let target: Vec = vec![0, 1, 65535]; - let result: Vec = vec_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &target); - let vector: Vec = vec_to_fixed(&[0.000000001, 0.000000001]); - let target: Vec = vec![65535, 65535]; - let result: Vec = vec_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &target); - let vector: Vec = vec_to_fixed(&[ - 0.000001, 0.000006, 0.000007, 0.0001, 0.001, 0.01, 0.1, 0.2, 0.3, 0.4, - ]); - let target: Vec = vec![0, 1, 1, 16, 164, 1638, 16384, 32768, 49151, 65535]; - let result: Vec = vec_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &target); - let vector: Vec = vec![I32F32::from_num(16384)]; - let target: Vec = vec![65535]; - let result: Vec = vec_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &target); - let vector: Vec = vec![I32F32::from_num(32768)]; - let target: Vec = vec![65535]; - let result: Vec = vec_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &target); - let vector: Vec = vec![I32F32::from_num(32769)]; - let target: Vec = vec![65535]; - let result: Vec = vec_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &target); - let vector: Vec = vec![I32F32::from_num(65535)]; - let target: Vec = vec![65535]; - let result: Vec = vec_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &target); - let vector: Vec = vec![I32F32::max_value()]; - let target: Vec = vec![65535]; - let result: Vec = vec_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &target); - let vector: Vec = vec_to_fixed(&[0., 1., 65535.]); - let target: Vec = vec![0, 1, 65535]; - let result: Vec = vec_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &target); - let vector: Vec = vec_to_fixed(&[0., 0.5, 1., 1.5, 2., 32768.]); - let target: Vec = vec![0, 1, 2, 3, 4, 65535]; - let result: Vec = vec_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &target); - let vector: Vec = vec_to_fixed(&[0., 0.5, 1., 1.5, 2., 32768., 32769.]); - let target: Vec = vec![0, 1, 2, 3, 4, 65533, 65535]; - let result: Vec = vec_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &target); - let vector: Vec = vec![ - I32F32::from_num(0), - I32F32::from_num(1), - I32F32::from_num(32768), - I32F32::from_num(32769), - I32F32::max_value(), - ]; - let target: Vec = vec![0, 0, 1, 1, 65535]; - let result: Vec = vec_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &target); -} - -#[test] -fn test_vec_u16_max_upscale_to_u16() { - let vector: Vec = vec![]; - let result: Vec = vec_u16_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &vector); - let vector: Vec = vec![0]; - let result: Vec = vec_u16_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &vector); - let vector: Vec = vec![0, 0]; - let result: Vec = vec_u16_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &vector); - let vector: Vec = vec![1]; - let target: Vec = vec![65535]; - let result: Vec = vec_u16_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &target); - let vector: Vec = vec![0, 1]; - let target: Vec = vec![0, 65535]; - let result: Vec = vec_u16_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &target); - let vector: Vec = vec![65534]; - let target: Vec = vec![65535]; - let result: Vec = vec_u16_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &target); - let vector: Vec = vec![65535]; - let target: Vec = vec![65535]; - let result: Vec = vec_u16_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &target); - let vector: Vec = vec![65535, 65535]; - let target: Vec = vec![65535, 65535]; - let result: Vec = vec_u16_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &target); - let vector: Vec = vec![0, 1, 65534]; - let target: Vec = vec![0, 1, 65535]; - let result: Vec = vec_u16_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &target); - let vector: Vec = vec![0, 1, 2, 3, 4, 65533, 65535]; - let result: Vec = vec_u16_max_upscale_to_u16(&vector); - assert_vec_compare_u16(&result, &vector); -} - -#[test] -fn test_check_vec_max_limited() { - let vector: Vec = vec![]; - let max_limit: u16 = 0; - assert!(check_vec_max_limited(&vector, max_limit)); - let vector: Vec = vec![]; - let max_limit: u16 = u16::MAX; - assert!(check_vec_max_limited(&vector, max_limit)); - let vector: Vec = vec![u16::MAX]; - let max_limit: u16 = u16::MAX; - assert!(check_vec_max_limited(&vector, max_limit)); - let vector: Vec = vec![u16::MAX]; - let max_limit: u16 = u16::MAX - 1; - assert!(!check_vec_max_limited(&vector, max_limit)); - let vector: Vec = vec![u16::MAX]; - let max_limit: u16 = 0; - assert!(!check_vec_max_limited(&vector, max_limit)); - let vector: Vec = vec![0]; - let max_limit: u16 = u16::MAX; - assert!(check_vec_max_limited(&vector, max_limit)); - let vector: Vec = vec![0, u16::MAX]; - let max_limit: u16 = u16::MAX; - assert!(check_vec_max_limited(&vector, max_limit)); - let vector: Vec = vec![0, u16::MAX, u16::MAX]; - let max_limit: u16 = u16::MAX / 2; - assert!(!check_vec_max_limited(&vector, max_limit)); - let vector: Vec = vec![0, u16::MAX, u16::MAX]; - let max_limit: u16 = u16::MAX / 2 + 1; - assert!(check_vec_max_limited(&vector, max_limit)); - let vector: Vec = vec![0, u16::MAX, u16::MAX, u16::MAX]; - let max_limit: u16 = u16::MAX / 3 - 1; - assert!(!check_vec_max_limited(&vector, max_limit)); - let vector: Vec = vec![0, u16::MAX, u16::MAX, u16::MAX]; - let max_limit: u16 = u16::MAX / 3; - assert!(check_vec_max_limited(&vector, max_limit)); -} - -#[test] -fn test_math_fixed_overflow() { - let max_32: I32F32 = I32F32::max_value(); - let max_u64: u64 = u64::MAX; - let _prod_96: I96F32 = I96F32::from_num(max_32) * I96F32::from_num(max_u64); - // let one: I96F32 = I96F32::from_num(1); - // let prod_96: I96F32 = (I96F32::from_num(max_32) + one) * I96F32::from_num(max_u64); // overflows - let _prod_110: I110F18 = I110F18::from_num(max_32) * I110F18::from_num(max_u64); - - let bonds_moving_average_val: u64 = 900_000_u64; - let bonds_moving_average: I64F64 = - I64F64::from_num(bonds_moving_average_val) / I64F64::from_num(1_000_000); - let alpha: I32F32 = I32F32::from_num(1) - I32F32::from_num(bonds_moving_average); - assert_eq!(I32F32::from_num(0.1), alpha); - - let bonds_moving_average: I64F64 = I64F64::from_num(max_32) / I64F64::from_num(max_32); - let alpha: I32F32 = I32F32::from_num(1) - I32F32::from_num(bonds_moving_average); - assert_eq!(I32F32::from_num(0), alpha); -} - -#[test] -fn test_math_u64_normalization() { - let min: u64 = 1; - let min32: u64 = 4_889_444; // 21_000_000_000_000_000 / 4_294_967_296 - let mid: u64 = 10_500_000_000_000_000; - let max: u64 = 21_000_000_000_000_000; - let min_64: I64F64 = I64F64::from_num(min); - let min32_64: I64F64 = I64F64::from_num(min32); - let mid_64: I64F64 = I64F64::from_num(mid); - let max_64: I64F64 = I64F64::from_num(max); - let max_sum: I64F64 = I64F64::from_num(max); - let min_frac: I64F64 = min_64 / max_sum; - assert_eq!(min_frac, I64F64::from_num(0.0000000000000000476)); - let min_frac_32: I32F32 = I32F32::from_num(min_frac); - assert_eq!(min_frac_32, I32F32::from_num(0)); - let min32_frac: I64F64 = min32_64 / max_sum; - assert_eq!(min32_frac, I64F64::from_num(0.00000000023283066664)); - let min32_frac_32: I32F32 = I32F32::from_num(min32_frac); - assert_eq!(min32_frac_32, I32F32::from_num(0.0000000002)); - let half: I64F64 = mid_64 / max_sum; - assert_eq!(half, I64F64::from_num(0.5)); - let half_32: I32F32 = I32F32::from_num(half); - assert_eq!(half_32, I32F32::from_num(0.5)); - let one: I64F64 = max_64 / max_sum; - assert_eq!(one, I64F64::from_num(1)); - let one_32: I32F32 = I32F32::from_num(one); - assert_eq!(one_32, I32F32::from_num(1)); -} - -#[test] -fn test_math_to_num() { - let val: I32F32 = I32F32::from_num(u16::MAX); - let res: u16 = val.to_num::(); - assert_eq!(res, u16::MAX); - let vector: Vec = vec![val; 1000]; - let target: Vec = vec![u16::MAX; 1000]; - let output: Vec = vector.iter().map(|e: &I32F32| e.to_num::()).collect(); - assert_eq!(output, target); - let output: Vec = vector - .iter() - .map(|e: &I32F32| (*e).to_num::()) - .collect(); - assert_eq!(output, target); - let val: I32F32 = I32F32::max_value(); - let res: u64 = val.to_num::(); - let vector: Vec = vec![val; 1000]; - let target: Vec = vec![res; 1000]; - let output: Vec = vector.iter().map(|e: &I32F32| e.to_num::()).collect(); - assert_eq!(output, target); - let output: Vec = vector - .iter() - .map(|e: &I32F32| (*e).to_num::()) - .collect(); - assert_eq!(output, target); - let val: I32F32 = I32F32::from_num(0); - let res: u64 = val.to_num::(); - let vector: Vec = vec![val; 1000]; - let target: Vec = vec![res; 1000]; - let output: Vec = vector.iter().map(|e: &I32F32| e.to_num::()).collect(); - assert_eq!(output, target); - let output: Vec = vector - .iter() - .map(|e: &I32F32| (*e).to_num::()) - .collect(); - assert_eq!(output, target); - let val: I96F32 = I96F32::from_num(u64::MAX); - let res: u64 = val.to_num::(); - assert_eq!(res, u64::MAX); - let vector: Vec = vec![val; 1000]; - let target: Vec = vec![u64::MAX; 1000]; - let output: Vec = vector.iter().map(|e: &I96F32| e.to_num::()).collect(); - assert_eq!(output, target); - let output: Vec = vector - .iter() - .map(|e: &I96F32| (*e).to_num::()) - .collect(); - assert_eq!(output, target); -} - -#[test] -fn test_math_vec_to_fixed() { - let vector: Vec = vec![0., 1., 2., 3.]; - let target: Vec = vec![ - I32F32::from_num(0.), - I32F32::from_num(1.), - I32F32::from_num(2.), - I32F32::from_num(3.), - ]; - let result = vec_to_fixed(&vector); - assert_vec_compare(&result, &target, I32F32::from_num(0)); -} - -// Reshape vector to matrix with specified number of rows, cast to I32F32. -pub fn vec_to_mat_fixed(vector: &[f32], rows: usize, transpose: bool) -> Vec> { - assert!( - vector.len() % rows == 0, - "Vector of len {:?} cannot reshape to {rows} rows.", - vector.len() - ); - let cols: usize = vector.len() / rows; - let mut mat: Vec> = vec![]; - if transpose { - for col in 0..cols { - let mut vals: Vec = vec![]; - for row in 0..rows { - vals.push(I32F32::from_num(vector[row * cols + col])); - } - mat.push(vals); - } - } else { - for row in 0..rows { - mat.push( - vector[row * cols..(row + 1) * cols] - .iter() - .map(|v| I32F32::from_num(*v)) - .collect(), - ); - } - } - mat -} - -#[test] -fn test_math_vec_to_mat_fixed() { - let vector: Vec = vec![0., 1., 2., 0., 10., 100.]; - let target: Vec> = vec![ - vec![ - I32F32::from_num(0.), - I32F32::from_num(1.), - I32F32::from_num(2.), - ], - vec![ - I32F32::from_num(0.), - I32F32::from_num(10.), - I32F32::from_num(100.), - ], - ]; - let mat = vec_to_mat_fixed(&vector, 2, false); - assert_mat_compare(&mat, &target, I32F32::from_num(0)); -} - -// Reshape vector to sparse matrix with specified number of input rows, cast f32 to I32F32. -fn vec_to_sparse_mat_fixed( - vector: &[f32], - rows: usize, - transpose: bool, -) -> Vec> { - assert!( - vector.len() % rows == 0, - "Vector of len {:?} cannot reshape to {rows} rows.", - vector.len() - ); - let cols: usize = vector.len() / rows; - let mut mat: Vec> = vec![]; - if transpose { - for col in 0..cols { - let mut row_vec: Vec<(u16, I32F32)> = vec![]; - for row in 0..rows { - if vector[row * cols + col] > 0. { - row_vec.push((row as u16, I32F32::from_num(vector[row * cols + col]))); - } - } - mat.push(row_vec); - } - } else { - for row in 0..rows { - let mut row_vec: Vec<(u16, I32F32)> = vec![]; - for col in 0..cols { - if vector[row * cols + col] > 0. { - row_vec.push((col as u16, I32F32::from_num(vector[row * cols + col]))); - } - } - mat.push(row_vec); - } - } - mat -} - -#[test] -fn test_math_vec_to_sparse_mat_fixed() { - let vector: Vec = vec![0., 1., 2., 0., 10., 100.]; - let target: Vec> = vec![ - vec![(1_u16, I32F32::from_num(1.)), (2_u16, I32F32::from_num(2.))], - vec![ - (1_u16, I32F32::from_num(10.)), - (2_u16, I32F32::from_num(100.)), - ], - ]; - let mat = vec_to_sparse_mat_fixed(&vector, 2, false); - assert_sparse_mat_compare(&mat, &target, I32F32::from_num(0)); - let vector: Vec = vec![0., 0.]; - let target: Vec> = vec![vec![], vec![]]; - let mat = vec_to_sparse_mat_fixed(&vector, 2, false); - assert_sparse_mat_compare(&mat, &target, I32F32::from_num(0)); - let vector: Vec = vec![0., 1., 2., 0., 10., 100.]; - let target: Vec> = vec![ - vec![], - vec![ - (0_u16, I32F32::from_num(1.)), - (1_u16, I32F32::from_num(10.)), - ], - vec![ - (0_u16, I32F32::from_num(2.)), - (1_u16, I32F32::from_num(100.)), - ], - ]; - let mat = vec_to_sparse_mat_fixed(&vector, 2, true); - assert_sparse_mat_compare(&mat, &target, I32F32::from_num(0)); - let vector: Vec = vec![0., 0.]; - let target: Vec> = vec![vec![]]; - let mat = vec_to_sparse_mat_fixed(&vector, 2, true); - assert_sparse_mat_compare(&mat, &target, I32F32::from_num(0)); -} - -#[test] -fn test_math_exp_safe() { - let zero: I32F32 = I32F32::from_num(0); - let one: I32F32 = I32F32::from_num(1); - let target: I32F32 = exp(zero).unwrap(); - assert_eq!(exp_safe(zero), target); - let target: I32F32 = exp(one).unwrap(); - assert_eq!(exp_safe(one), target); - let min_input: I32F32 = I32F32::from_num(-20); // <= 1/exp(-20) = 485 165 195,4097903 - let max_input: I32F32 = I32F32::from_num(20); // <= exp(20) = 485 165 195,4097903 - let target: I32F32 = exp(min_input).unwrap(); - assert_eq!(exp_safe(min_input), target); - assert_eq!(exp_safe(min_input - one), target); - assert_eq!(exp_safe(I32F32::min_value()), target); - let target: I32F32 = exp(max_input).unwrap(); - assert_eq!(exp_safe(max_input), target); - assert_eq!(exp_safe(max_input + one), target); - assert_eq!(exp_safe(I32F32::max_value()), target); -} - -#[test] -fn test_math_sigmoid_safe() { - let trust: Vec = vec![ - I32F32::min_value(), - I32F32::from_num(0), - I32F32::from_num(0.4), - I32F32::from_num(0.5), - I32F32::from_num(0.6), - I32F32::from_num(1), - I32F32::max_value(), - ]; - let consensus: Vec = trust - .iter() - .map(|t: &I32F32| sigmoid_safe(*t, I32F32::max_value(), I32F32::max_value())) - .collect(); - let target: Vec = vec_to_fixed(&[ - 0.0000000019, - 0.0000000019, - 0.0000000019, - 0.0000000019, - 0.0000000019, - 0.0000000019, - 0.5, - ]); - assert_eq!(&consensus, &target); - let consensus: Vec = trust - .iter() - .map(|t: &I32F32| sigmoid_safe(*t, I32F32::min_value(), I32F32::min_value())) - .collect(); - let target: Vec = vec_to_fixed(&[ - 0.5, - 0.0000000019, - 0.0000000019, - 0.0000000019, - 0.0000000019, - 0.0000000019, - 0.0000000019, - ]); - assert_eq!(&consensus, &target); - let consensus: Vec = trust - .iter() - .map(|t: &I32F32| sigmoid_safe(*t, I32F32::from_num(30), I32F32::from_num(0.5))) - .collect(); - let target: Vec = vec![ - 0.0000000019, - 0.0000003057, - 0.0474258729, - 0.5, - 0.952574127, - 0.9999996943, - 0.9999999981, - ]; - let target: Vec = target.iter().map(|c: &f64| I32F32::from_num(*c)).collect(); - assert_eq!(&consensus, &target); - let trust: Vec = vec_to_fixed(&[0., 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.]); - let consensus: Vec = trust - .iter() - .map(|t: &I32F32| sigmoid_safe(*t, I32F32::from_num(40), I32F32::from_num(0.5))) - .collect(); - let target: Vec = vec![ - 0.0000000019, - 0.0000001125, - 0.0000061442, - 0.0003353502, - 0.017986214, - 0.5, - 0.9820138067, - 0.9996646498, - 0.9999938558, - 0.9999998875, - 0.9999999981, - ]; - let target: Vec = target.iter().map(|c: &f64| I32F32::from_num(*c)).collect(); - assert_eq!(&consensus, &target); -} - -#[test] -fn test_math_is_topk() { - let vector: Vec = vec_to_fixed(&[]); - let result = is_topk(&vector, 5); - let target: Vec = vec![]; - assert_eq!(&result, &target); - let vector: Vec = vec_to_fixed(&[0., 1., 2., 3., 4., 5., 6., 7., 8., 9.]); - let result = is_topk(&vector, 0); - let target: Vec = vec![ - false, false, false, false, false, false, false, false, false, false, - ]; - assert_eq!(&result, &target); - let result = is_topk(&vector, 5); - let target: Vec = vec![ - false, false, false, false, false, true, true, true, true, true, - ]; - assert_eq!(&result, &target); - let result = is_topk(&vector, 10); - let target: Vec = vec![true, true, true, true, true, true, true, true, true, true]; - assert_eq!(&result, &target); - let result = is_topk(&vector, 100); - assert_eq!(&result, &target); - let vector: Vec = vec_to_fixed(&[9., 8., 7., 6., 5., 4., 3., 2., 1., 0.]); - let result = is_topk(&vector, 5); - let target: Vec = vec![ - true, true, true, true, true, false, false, false, false, false, - ]; - assert_eq!(&result, &target); - let vector: Vec = vec_to_fixed(&[9., 0., 8., 1., 7., 2., 6., 3., 5., 4.]); - let result = is_topk(&vector, 5); - let target: Vec = vec![ - true, false, true, false, true, false, true, false, true, false, - ]; - assert_eq!(&result, &target); - let vector: Vec = vec_to_fixed(&[0.9, 0., 0.8, 0.1, 0.7, 0.2, 0.6, 0.3, 0.5, 0.4]); - let result = is_topk(&vector, 5); - let target: Vec = vec![ - true, false, true, false, true, false, true, false, true, false, - ]; - assert_eq!(&result, &target); - let vector: Vec = vec_to_fixed(&[0., 1., 2., 3., 4., 5., 5., 5., 5., 6.]); - let result = is_topk(&vector, 5); - let target: Vec = vec![ - false, false, false, false, false, true, true, true, true, true, - ]; - assert_eq!(&result, &target); -} - -#[test] -fn test_math_sum() { - assert!(sum(&[]) == I32F32::from_num(0)); - assert!( - sum(&[ - I32F32::from_num(1.0), - I32F32::from_num(10.0), - I32F32::from_num(30.0) - ]) == I32F32::from_num(41) - ); - assert!( - sum(&[ - I32F32::from_num(-1.0), - I32F32::from_num(10.0), - I32F32::from_num(30.0) - ]) == I32F32::from_num(39) - ); -} - -#[test] -fn test_math_normalize() { - let epsilon: I32F32 = I32F32::from_num(0.0001); - let x: Vec = vec![]; - let y: Vec = normalize(&x); - assert_vec_compare(&x, &y, epsilon); - let x: Vec = vec![ - I32F32::from_num(1.0), - I32F32::from_num(10.0), - I32F32::from_num(30.0), - ]; - let y: Vec = normalize(&x); - assert_vec_compare( - &y, - &[ - I32F32::from_num(0.0243902437), - I32F32::from_num(0.243902439), - I32F32::from_num(0.7317073171), - ], - epsilon, - ); - assert_float_compare(sum(&y), I32F32::from_num(1.0), epsilon); - let x: Vec = vec![ - I32F32::from_num(-1.0), - I32F32::from_num(10.0), - I32F32::from_num(30.0), - ]; - let y: Vec = normalize(&x); - assert_vec_compare( - &y, - &[ - I32F32::from_num(-0.0256410255), - I32F32::from_num(0.2564102563), - I32F32::from_num(0.769230769), - ], - epsilon, - ); - assert_float_compare(sum(&y), I32F32::from_num(1.0), epsilon); -} - -#[test] -fn test_math_inplace_normalize() { - let epsilon: I32F32 = I32F32::from_num(0.0001); - let mut x1: Vec = vec![ - I32F32::from_num(1.0), - I32F32::from_num(10.0), - I32F32::from_num(30.0), - ]; - inplace_normalize(&mut x1); - assert_vec_compare( - &x1, - &[ - I32F32::from_num(0.0243902437), - I32F32::from_num(0.243902439), - I32F32::from_num(0.7317073171), - ], - epsilon, - ); - let mut x2: Vec = vec![ - I32F32::from_num(-1.0), - I32F32::from_num(10.0), - I32F32::from_num(30.0), - ]; - inplace_normalize(&mut x2); - assert_vec_compare( - &x2, - &[ - I32F32::from_num(-0.0256410255), - I32F32::from_num(0.2564102563), - I32F32::from_num(0.769230769), - ], - epsilon, - ); -} - -#[test] -fn test_math_inplace_normalize_64() { - let epsilon: I64F64 = I64F64::from_num(0.0001); - let mut x1: Vec = vec![ - I64F64::from_num(1.0), - I64F64::from_num(10.0), - I64F64::from_num(30.0), - ]; - inplace_normalize_64(&mut x1); - assert_vec_compare_64( - &x1, - &[ - I64F64::from_num(0.0243902437), - I64F64::from_num(0.243902439), - I64F64::from_num(0.7317073171), - ], - epsilon, - ); - let mut x2: Vec = vec![ - I64F64::from_num(-1.0), - I64F64::from_num(10.0), - I64F64::from_num(30.0), - ]; - inplace_normalize_64(&mut x2); - assert_vec_compare_64( - &x2, - &[ - I64F64::from_num(-0.0256410255), - I64F64::from_num(0.2564102563), - I64F64::from_num(0.769230769), - ], - epsilon, - ); -} - -#[test] -fn test_math_vecdiv() { - let x: Vec = vec_to_fixed(&[]); - let y: Vec = vec_to_fixed(&[]); - let result: Vec = vec_to_fixed(&[]); - assert_eq!(result, vecdiv(&x, &y)); - - let x: Vec = vec_to_fixed(&[0., 1., 0., 1.]); - let y: Vec = vec_to_fixed(&[0., 1., 1., 0.]); - let result: Vec = vec_to_fixed(&[0., 1., 0., 0.]); - assert_eq!(result, vecdiv(&x, &y)); - - let x: Vec = vec_to_fixed(&[1., 1., 10.]); - let y: Vec = vec_to_fixed(&[2., 3., 2.]); - let result: Vec = vec![fixed(1.) / fixed(2.), fixed(1.) / fixed(3.), fixed(5.)]; - assert_eq!(result, vecdiv(&x, &y)); -} - -#[test] -fn test_math_inplace_row_normalize() { - let epsilon: I32F32 = I32F32::from_num(0.0001); - let vector: Vec = vec![ - 0., 1., 2., 3., 4., 0., 10., 100., 1000., 10000., 0., 0., 0., 0., 0., 1., 1., 1., 1., 1., - ]; - let mut mat = vec_to_mat_fixed(&vector, 4, false); - inplace_row_normalize(&mut mat); - let target: Vec = vec![ - 0., 0.1, 0.2, 0.3, 0.4, 0., 0.0009, 0.009, 0.09, 0.9, 0., 0., 0., 0., 0., 0.2, 0.2, 0.2, - 0.2, 0.2, - ]; - assert_mat_compare(&mat, &vec_to_mat_fixed(&target, 4, false), epsilon); -} - -#[test] -fn test_math_inplace_row_normalize_sparse() { - let epsilon: I32F32 = I32F32::from_num(0.0001); - let vector: Vec = vec![ - 0., 1., 0., 2., 0., 3., 4., 0., 1., 0., 2., 0., 3., 0., 1., 0., 0., 2., 0., 3., 4., 0., - 10., 0., 100., 1000., 0., 10000., 0., 0., 0., 0., 0., 0., 0., 1., 1., 1., 1., 1., 1., 1., - ]; - let mut mat = vec_to_sparse_mat_fixed(&vector, 6, false); - inplace_row_normalize_sparse(&mut mat); - let target: Vec = vec![ - 0., 0.1, 0., 0.2, 0., 0.3, 0.4, 0., 0.166666, 0., 0.333333, 0., 0.5, 0., 0.1, 0., 0., 0.2, - 0., 0.3, 0.4, 0., 0.0009, 0., 0.009, 0.09, 0., 0.9, 0., 0., 0., 0., 0., 0., 0., 0.142857, - 0.142857, 0.142857, 0.142857, 0.142857, 0.142857, 0.142857, - ]; - assert_sparse_mat_compare(&mat, &vec_to_sparse_mat_fixed(&target, 6, false), epsilon); - let vector: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let mut mat = vec_to_sparse_mat_fixed(&vector, 3, false); - inplace_row_normalize_sparse(&mut mat); - assert_sparse_mat_compare( - &mat, - &vec_to_sparse_mat_fixed(&target, 3, false), - I32F32::from_num(0), - ); -} - -#[test] -fn test_math_inplace_col_normalize() { - let epsilon: I32F32 = I32F32::from_num(0.0001); - let vector: Vec = vec![ - 0., 1., 2., 3., 4., 0., 10., 100., 1000., 10000., 0., 0., 0., 0., 0., 1., 1., 1., 1., 1., - ]; - let mut mat = vec_to_mat_fixed(&vector, 4, true); - inplace_col_normalize(&mut mat); - let target: Vec = vec![ - 0., 0.1, 0.2, 0.3, 0.4, 0., 0.0009, 0.009, 0.09, 0.9, 0., 0., 0., 0., 0., 0.2, 0.2, 0.2, - 0.2, 0.2, - ]; - assert_mat_compare(&mat, &vec_to_mat_fixed(&target, 4, true), epsilon); -} - -#[test] -fn test_math_inplace_col_normalize_sparse() { - let epsilon: I32F32 = I32F32::from_num(0.0001); - let vector: Vec = vec![ - 0., 1., 0., 2., 0., 3., 4., 0., 1., 0., 2., 0., 3., 0., 1., 0., 0., 2., 0., 3., 4., 0., - 10., 0., 100., 1000., 0., 10000., 0., 0., 0., 0., 0., 0., 0., 1., 1., 1., 1., 1., 1., 1., - ]; - let mut mat = vec_to_sparse_mat_fixed(&vector, 6, true); - inplace_col_normalize_sparse(&mut mat, 6); - let target: Vec = vec![ - 0., 0.1, 0., 0.2, 0., 0.3, 0.4, 0., 0.166666, 0., 0.333333, 0., 0.5, 0., 0.1, 0., 0., 0.2, - 0., 0.3, 0.4, 0., 0.0009, 0., 0.009, 0.09, 0., 0.9, 0., 0., 0., 0., 0., 0., 0., 0.142857, - 0.142857, 0.142857, 0.142857, 0.142857, 0.142857, 0.142857, - ]; - assert_sparse_mat_compare(&mat, &vec_to_sparse_mat_fixed(&target, 6, true), epsilon); - let vector: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let mut mat = vec_to_sparse_mat_fixed(&vector, 3, false); - inplace_col_normalize_sparse(&mut mat, 6); - assert_sparse_mat_compare( - &mat, - &vec_to_sparse_mat_fixed(&target, 3, false), - I32F32::from_num(0), - ); - let mut mat: Vec> = vec![]; - let target: Vec> = vec![]; - inplace_col_normalize_sparse(&mut mat, 0); - assert_sparse_mat_compare(&mat, &target, epsilon); -} - -#[test] -fn test_math_inplace_col_max_upscale() { - let mut mat: Vec> = vec![vec![]]; - let target: Vec> = vec![vec![]]; - inplace_col_max_upscale(&mut mat); - assert_eq!(&mat, &target); - let mut mat: Vec> = vec![vec![I32F32::from_num(0)]]; - let target: Vec> = vec![vec![I32F32::from_num(0)]]; - inplace_col_max_upscale(&mut mat); - assert_eq!(&mat, &target); - let epsilon: I32F32 = I32F32::from_num(0.0001); - let vector: Vec = vec![ - 0., 1., 2., 3., 4., 0., 10., 100., 1000., 10000., 0., 0., 0., 0., 0., 1., 1., 1., 1., 1., - ]; - let mut mat: Vec> = vec_to_mat_fixed(&vector, 4, true); - inplace_col_max_upscale(&mut mat); - let target: Vec = vec![ - 0., 0.25, 0.5, 0.75, 1., 0., 0.001, 0.01, 0.1, 1., 0., 0., 0., 0., 0., 1., 1., 1., 1., 1., - ]; - assert_mat_compare(&mat, &vec_to_mat_fixed(&target, 4, true), epsilon); -} - -#[test] -fn test_math_inplace_col_max_upscale_sparse() { - let mut mat: Vec> = vec![vec![]]; - let target: Vec> = vec![vec![]]; - inplace_col_max_upscale_sparse(&mut mat, 0); - assert_eq!(&mat, &target); - let mut mat: Vec> = vec![vec![(0, I32F32::from_num(0))]]; - let target: Vec> = vec![vec![(0, I32F32::from_num(0))]]; - inplace_col_max_upscale_sparse(&mut mat, 1); - assert_eq!(&mat, &target); - let epsilon: I32F32 = I32F32::from_num(0.0001); - let vector: Vec = vec![ - 0., 1., 0., 2., 0., 3., 4., 0., 1., 0., 2., 0., 3., 0., 1., 0., 0., 2., 0., 3., 4., 0., - 10., 0., 100., 1000., 0., 10000., 0., 0., 0., 0., 0., 0., 0., 1., 1., 1., 1., 1., 1., 1., - ]; - let mut mat = vec_to_sparse_mat_fixed(&vector, 6, true); - inplace_col_max_upscale_sparse(&mut mat, 6); - let target: Vec = vec![ - 0., 0.25, 0., 0.5, 0., 0.75, 1., 0., 0.333333, 0., 0.666666, 0., 1., 0., 0.25, 0., 0., 0.5, - 0., 0.75, 1., 0., 0.001, 0., 0.01, 0.1, 0., 1., 0., 0., 0., 0., 0., 0., 0., 1., 1., 1., 1., - 1., 1., 1., - ]; - assert_sparse_mat_compare(&mat, &vec_to_sparse_mat_fixed(&target, 6, true), epsilon); - let vector: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let mut mat = vec_to_sparse_mat_fixed(&vector, 3, false); - inplace_col_max_upscale_sparse(&mut mat, 6); - assert_sparse_mat_compare( - &mat, - &vec_to_sparse_mat_fixed(&target, 3, false), - I32F32::from_num(0), - ); - let mut mat: Vec> = vec![]; - let target: Vec> = vec![]; - inplace_col_max_upscale_sparse(&mut mat, 0); - assert_sparse_mat_compare(&mat, &target, epsilon); -} - -#[test] -fn test_math_inplace_mask_vector() { - let mask: Vec = vec![false, false, false]; - let mut vector: Vec = vec_to_fixed(&[0., 1., 2.]); - let target: Vec = vec_to_fixed(&[0., 1., 2.]); - inplace_mask_vector(&mask, &mut vector); - assert_vec_compare(&vector, &target, I32F32::from_num(0)); - let mask: Vec = vec![false, true, false]; - let mut vector: Vec = vec_to_fixed(&[0., 1., 2.]); - let target: Vec = vec_to_fixed(&[0., 0., 2.]); - inplace_mask_vector(&mask, &mut vector); - assert_vec_compare(&vector, &target, I32F32::from_num(0)); - let mask: Vec = vec![true, true, true]; - let mut vector: Vec = vec_to_fixed(&[0., 1., 2.]); - let target: Vec = vec_to_fixed(&[0., 0., 0.]); - inplace_mask_vector(&mask, &mut vector); - assert_vec_compare(&vector, &target, I32F32::from_num(0)); -} - -#[test] -fn test_math_inplace_mask_matrix() { - let mask: Vec> = vec![ - vec![false, false, false], - vec![false, false, false], - vec![false, false, false], - ]; - let vector: Vec = vec![0., 1., 2., 3., 4., 5., 6., 7., 8.]; - let mut mat = vec_to_mat_fixed(&vector, 3, false); - inplace_mask_matrix(&mask, &mut mat); - assert_mat_compare( - &mat, - &vec_to_mat_fixed(&vector, 3, false), - I32F32::from_num(0), - ); - let mask: Vec> = vec![ - vec![true, false, false], - vec![false, true, false], - vec![false, false, true], - ]; - let target: Vec = vec![0., 1., 2., 3., 0., 5., 6., 7., 0.]; - let mut mat = vec_to_mat_fixed(&vector, 3, false); - inplace_mask_matrix(&mask, &mut mat); - assert_mat_compare( - &mat, - &vec_to_mat_fixed(&target, 3, false), - I32F32::from_num(0), - ); - let mask: Vec> = vec![ - vec![true, true, true], - vec![true, true, true], - vec![true, true, true], - ]; - let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let mut mat = vec_to_mat_fixed(&vector, 3, false); - inplace_mask_matrix(&mask, &mut mat); - assert_mat_compare( - &mat, - &vec_to_mat_fixed(&target, 3, false), - I32F32::from_num(0), - ); -} - -#[test] -fn test_math_inplace_mask_rows() { - let input: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9.]; - let mask: Vec = vec![false, false, false]; - let target: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9.]; - let mut mat = vec_to_mat_fixed(&input, 3, false); - inplace_mask_rows(&mask, &mut mat); - assert_mat_compare( - &mat, - &vec_to_mat_fixed(&target, 3, false), - I32F32::from_num(0), - ); - let mask: Vec = vec![true, true, true]; - let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let mut mat = vec_to_mat_fixed(&input, 3, false); - inplace_mask_rows(&mask, &mut mat); - assert_mat_compare( - &mat, - &vec_to_mat_fixed(&target, 3, false), - I32F32::from_num(0), - ); - let mask: Vec = vec![true, false, true]; - let target: Vec = vec![0., 0., 0., 4., 5., 6., 0., 0., 0.]; - let mut mat = vec_to_mat_fixed(&input, 3, false); - inplace_mask_rows(&mask, &mut mat); - assert_mat_compare( - &mat, - &vec_to_mat_fixed(&target, 3, false), - I32F32::from_num(0), - ); - let input: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let mut mat = vec_to_mat_fixed(&input, 3, false); - let mask: Vec = vec![false, false, false]; - let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; - inplace_mask_rows(&mask, &mut mat); - assert_mat_compare( - &mat, - &vec_to_mat_fixed(&target, 3, false), - I32F32::from_num(0), - ); -} - -#[test] -fn test_math_inplace_mask_diag() { - let vector: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9.]; - let target: Vec = vec![0., 2., 3., 4., 0., 6., 7., 8., 0.]; - let mut mat = vec_to_mat_fixed(&vector, 3, false); - inplace_mask_diag(&mut mat); - assert_mat_compare( - &mat, - &vec_to_mat_fixed(&target, 3, false), - I32F32::from_num(0), - ); -} - -#[test] -fn test_math_inplace_mask_diag_except_index() { - let vector: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9.]; - let rows = 3; - - for i in 0..rows { - let mut target: Vec = vec![0., 2., 3., 4., 0., 6., 7., 8., 0.]; - let row = i * rows; - let col = i; - target[row + col] = vector[row + col]; - - let mut mat = vec_to_mat_fixed(&vector, rows, false); - inplace_mask_diag_except_index(&mut mat, i as u16); - assert_mat_compare( - &mat, - &vec_to_mat_fixed(&target, rows, false), - I32F32::from_num(0), - ); - } -} - -#[test] -fn test_math_mask_rows_sparse() { - let input: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9.]; - let mat = vec_to_sparse_mat_fixed(&input, 3, false); - let mask: Vec = vec![false, false, false]; - let target: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9.]; - let result = mask_rows_sparse(&mask, &mat); - assert_sparse_mat_compare( - &result, - &vec_to_sparse_mat_fixed(&target, 3, false), - I32F32::from_num(0), - ); - let mask: Vec = vec![true, true, true]; - let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let result = mask_rows_sparse(&mask, &mat); - assert_sparse_mat_compare( - &result, - &vec_to_sparse_mat_fixed(&target, 3, false), - I32F32::from_num(0), - ); - let mask: Vec = vec![true, false, true]; - let target: Vec = vec![0., 0., 0., 4., 5., 6., 0., 0., 0.]; - let result = mask_rows_sparse(&mask, &mat); - assert_sparse_mat_compare( - &result, - &vec_to_sparse_mat_fixed(&target, 3, false), - I32F32::from_num(0), - ); - let input: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let mat = vec_to_sparse_mat_fixed(&input, 3, false); - let mask: Vec = vec![false, false, false]; - let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let result = mask_rows_sparse(&mask, &mat); - assert_sparse_mat_compare( - &result, - &vec_to_sparse_mat_fixed(&target, 3, false), - I32F32::from_num(0), - ); -} - -#[test] -fn test_math_mask_diag_sparse() { - let vector: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9.]; - let target: Vec = vec![0., 2., 3., 4., 0., 6., 7., 8., 0.]; - let mat = vec_to_sparse_mat_fixed(&vector, 3, false); - let result = mask_diag_sparse(&mat); - assert_sparse_mat_compare( - &result, - &vec_to_sparse_mat_fixed(&target, 3, false), - I32F32::from_num(0), - ); - let vector: Vec = vec![1., 0., 0., 0., 5., 0., 0., 0., 9.]; - let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let mat = vec_to_sparse_mat_fixed(&vector, 3, false); - let result = mask_diag_sparse(&mat); - assert_sparse_mat_compare( - &result, - &vec_to_sparse_mat_fixed(&target, 3, false), - I32F32::from_num(0), - ); - let vector: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let mat = vec_to_sparse_mat_fixed(&vector, 3, false); - let result = mask_diag_sparse(&mat); - assert_sparse_mat_compare( - &result, - &vec_to_sparse_mat_fixed(&target, 3, false), - I32F32::from_num(0), - ); -} - -#[test] -fn test_math_mask_diag_sparse_except_index() { - let rows = 3; - - let vector: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9.]; - let mat = vec_to_sparse_mat_fixed(&vector, rows, false); - - for i in 0..rows { - let mut target: Vec = vec![0., 2., 3., 4., 0., 6., 7., 8., 0.]; - let row = i * rows; - let col = i; - target[row + col] = vector[row + col]; - - let result = mask_diag_sparse_except_index(&mat, i as u16); - let target_as_mat = vec_to_sparse_mat_fixed(&target, rows, false); - - assert_sparse_mat_compare(&result, &target_as_mat, I32F32::from_num(0)); - } - - let vector: Vec = vec![1., 0., 0., 0., 5., 0., 0., 0., 9.]; - let mat = vec_to_sparse_mat_fixed(&vector, rows, false); - - for i in 0..rows { - let mut target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let row = i * rows; - let col = i; - target[row + col] = vector[row + col]; - - let result = mask_diag_sparse_except_index(&mat, i as u16); - let target_as_mat = vec_to_sparse_mat_fixed(&target, rows, false); - assert_eq!(result.len(), target_as_mat.len()); - - assert_sparse_mat_compare(&result, &target_as_mat, I32F32::from_num(0)); - } - - for i in 0..rows { - let vector: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let mat = vec_to_sparse_mat_fixed(&vector, rows, false); - - let mut target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let row = i * rows; - let col = i; - target[row + col] = vector[row + col]; - - let result = mask_diag_sparse_except_index(&mat, i as u16); - let target_as_mat = vec_to_sparse_mat_fixed(&target, rows, false); - assert_eq!(result.len(), target_as_mat.len()); - - assert_sparse_mat_compare(&result, &target_as_mat, I32F32::from_num(0)); - } -} - -#[test] -fn test_math_vec_mask_sparse_matrix() { - let vector: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9.]; - let target: Vec = vec![0., 2., 3., 4., 0., 6., 7., 8., 0.]; - let mat = vec_to_sparse_mat_fixed(&vector, 3, false); - let first_vector: Vec = vec![1, 2, 3]; - let second_vector: Vec = vec![1, 2, 3]; - let result = vec_mask_sparse_matrix(&mat, &first_vector, &second_vector, &|a, b| a == b); - assert_sparse_mat_compare( - &result, - &vec_to_sparse_mat_fixed(&target, 3, false), - I32F32::from_num(0), - ); - let target: Vec = vec![1., 0., 0., 4., 5., 0., 7., 8., 9.]; - let mat = vec_to_sparse_mat_fixed(&vector, 3, false); - let first_vector: Vec = vec![1, 2, 3]; - let second_vector: Vec = vec![1, 2, 3]; - let result = vec_mask_sparse_matrix(&mat, &first_vector, &second_vector, &|a, b| a < b); - assert_sparse_mat_compare( - &result, - &vec_to_sparse_mat_fixed(&target, 3, false), - I32F32::from_num(0), - ); - let vector: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let mat = vec_to_sparse_mat_fixed(&vector, 3, false); - let first_vector: Vec = vec![1, 2, 3]; - let second_vector: Vec = vec![1, 2, 3]; - let result = vec_mask_sparse_matrix(&mat, &first_vector, &second_vector, &|a, b| a == b); - assert_sparse_mat_compare( - &result, - &vec_to_sparse_mat_fixed(&target, 3, false), - I32F32::from_num(0), - ); -} - -#[test] -fn test_math_vec_mul() { - let vector: Vec = vec_to_fixed(&[1., 2., 3., 4.]); - let target: Vec = vec_to_fixed(&[1., 4., 9., 16.]); - let result = vec_mul(&vector, &vector); - assert_vec_compare(&result, &target, I32F32::from_num(0)); - let vector_empty: Vec = vec_to_fixed(&[]); - let result = vec_mul(&vector_empty, &vector); - let target: Vec = vec![]; - assert_vec_compare(&result, &target, I32F32::from_num(0)); - let vector_zero: Vec = vec_to_fixed(&[0., 0., 0., 0., 0., 0., 0., 0.]); - let result = vec_mul(&vector_zero, &vector); - let target: Vec = vec![I32F32::from_num(0); 4]; - assert_vec_compare(&result, &target, I32F32::from_num(0)); -} - -#[test] -fn test_math_mat_vec_mul() { - let matrix: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; - let matrix = vec_to_mat_fixed(&matrix, 4, false); - let vector: Vec = vec_to_fixed(&[1., 2., 3.]); - let target: Vec = vec![1., 4., 9., 4., 10., 18., 7., 16., 27., 10., 22., 36.]; - let target = vec_to_mat_fixed(&target, 4, false); - let result = mat_vec_mul(&matrix, &vector); - assert_mat_compare(&result, &target, I32F32::from_num(0)); - let vector_one: Vec = vec_to_fixed(&[1., 0., 0.]); - let target: Vec = vec![1., 0., 0., 4., 0., 0., 7., 0., 0., 10., 0., 0.]; - let target = vec_to_mat_fixed(&target, 4, false); - let result = mat_vec_mul(&matrix, &vector_one); - assert_mat_compare(&result, &target, I32F32::from_num(0)); - let vector_empty: Vec = vec_to_fixed(&[]); - let result = mat_vec_mul(&matrix, &vector_empty); - let target: Vec> = vec![vec![]; 4]; - assert_mat_compare(&result, &target, I32F32::from_num(0)); -} - -#[test] -fn test_math_mat_vec_mul_sparse() { - let matrix: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; - let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); - let vector: Vec = vec_to_fixed(&[1., 2., 3.]); - let target: Vec = vec![1., 4., 9., 4., 10., 18., 7., 16., 27., 10., 22., 36.]; - let target = vec_to_sparse_mat_fixed(&target, 4, false); - let result = mat_vec_mul_sparse(&matrix, &vector); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); - let vector_one: Vec = vec_to_fixed(&[1., 0., 0.]); - let target: Vec = vec![1., 0., 0., 4., 0., 0., 7., 0., 0., 10., 0., 0.]; - let target = vec_to_sparse_mat_fixed(&target, 4, false); - let result = mat_vec_mul_sparse(&matrix, &vector_one); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); - let vector_empty: Vec = vec_to_fixed(&[]); - let result = mat_vec_mul_sparse(&matrix, &vector_empty); - let target = vec![vec![]; 4]; - assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); -} - -#[test] -fn test_math_row_hadamard() { - let vector: Vec = vec_to_fixed(&[1., 2., 3., 4.]); - let matrix: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; - let matrix = vec_to_mat_fixed(&matrix, 4, false); - let result = row_hadamard(&matrix, &vector); - let target: Vec = vec![1., 2., 3., 8., 10., 12., 21., 24., 27., 40., 44., 48.]; - let target = vec_to_mat_fixed(&target, 4, false); - assert_mat_compare(&result, &target, I32F32::from_num(0)); -} - -#[test] -fn test_math_row_hadamard_sparse() { - let vector: Vec = vec_to_fixed(&[1., 2., 3., 4.]); - let matrix: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; - let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); - let result = row_hadamard_sparse(&matrix, &vector); - let target: Vec = vec![1., 2., 3., 8., 10., 12., 21., 24., 27., 40., 44., 48.]; - let target = vec_to_sparse_mat_fixed(&target, 4, false); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); - let matrix: Vec = vec![0., 2., 3., 4., 0., 6., 7., 8., 0., 10., 11., 12.]; - let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); - let result = row_hadamard_sparse(&matrix, &vector); - let target: Vec = vec![0., 2., 3., 8., 0., 12., 21., 24., 0., 40., 44., 48.]; - let target = vec_to_sparse_mat_fixed(&target, 4, false); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); - let matrix: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); - let result = row_hadamard_sparse(&matrix, &vector); - let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let target = vec_to_sparse_mat_fixed(&target, 4, false); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); -} - -#[test] -fn test_math_row_sum() { - let matrix: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; - let matrix = vec_to_mat_fixed(&matrix, 4, false); - let result = row_sum(&matrix); - let target: Vec = vec_to_fixed(&[6., 15., 24., 33.]); - assert_vec_compare(&result, &target, I32F32::from_num(0)); -} - -#[test] -fn test_math_row_sum_sparse() { - let matrix: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; - let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); - let result = row_sum_sparse(&matrix); - let target: Vec = vec_to_fixed(&[6., 15., 24., 33.]); - assert_vec_compare(&result, &target, I32F32::from_num(0)); - let matrix: Vec = vec![0., 2., 3., 4., 0., 6., 7., 8., 0., 10., 11., 12.]; - let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); - let result = row_sum_sparse(&matrix); - let target: Vec = vec_to_fixed(&[5., 10., 15., 33.]); - assert_vec_compare(&result, &target, I32F32::from_num(0)); - let matrix: Vec = vec![1., 2., 3., 0., 0., 0., 7., 8., 9., 10., 11., 12.]; - let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); - let result = row_sum_sparse(&matrix); - let target: Vec = vec_to_fixed(&[6., 0., 24., 33.]); - assert_vec_compare(&result, &target, I32F32::from_num(0)); - let matrix: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); - let result = row_sum_sparse(&matrix); - let target: Vec = vec_to_fixed(&[0., 0., 0., 0.]); - assert_vec_compare(&result, &target, I32F32::from_num(0)); -} - -#[test] -fn test_math_matmul() { - let vector: Vec = vec_to_fixed(&[1., 2., 3., 4.]); - let matrix: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; - let matrix = vec_to_mat_fixed(&matrix, 4, false); - let result = matmul(&matrix, &vector); - let target: Vec = vec_to_fixed(&[70., 80., 90.]); - assert_vec_compare(&result, &target, I32F32::from_num(0)); -} - -#[test] -fn test_math_matmul_transpose() { - let vector: Vec = vec_to_fixed(&[1., 2., 3.]); - let matrix: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; - let matrix = vec_to_mat_fixed(&matrix, 4, false); - let result = matmul_transpose(&matrix, &vector); - let target: Vec = vec_to_fixed(&[14., 32., 50., 68.]); - assert_vec_compare(&result, &target, I32F32::from_num(0)); -} - -#[test] -fn test_math_sparse_matmul() { - let vector: Vec = vec_to_fixed(&[1., 2., 3., 4.]); - let matrix: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; - let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); - let result = matmul_sparse(&matrix, &vector, 3); - let target: Vec = vec_to_fixed(&[70., 80., 90.]); - assert_vec_compare(&result, &target, I32F32::from_num(0)); - let matrix: Vec = vec![0., 2., 3., 4., 0., 6., 7., 8., 0., 10., 11., 12.]; - let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); - let result = matmul_sparse(&matrix, &vector, 3); - let target: Vec = vec_to_fixed(&[69., 70., 63.]); - assert_vec_compare(&result, &target, I32F32::from_num(0)); - let matrix: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); - let result = matmul_sparse(&matrix, &vector, 3); - let target: Vec = vec_to_fixed(&[0., 0., 0.]); - assert_vec_compare(&result, &target, I32F32::from_num(0)); -} - -#[test] -fn test_math_sparse_matmul_transpose() { - let vector: Vec = vec_to_fixed(&[1., 2., 3.]); - let matrix: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; - let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); - let result = matmul_transpose_sparse(&matrix, &vector); - let target: Vec = vec_to_fixed(&[14., 32., 50., 68.]); - assert_vec_compare(&result, &target, I32F32::from_num(0)); - let matrix: Vec = vec![0., 2., 3., 4., 0., 6., 7., 8., 0., 10., 11., 12.]; - let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); - let result = matmul_transpose_sparse(&matrix, &vector); - let target: Vec = vec_to_fixed(&[13., 22., 23., 68.]); - assert_vec_compare(&result, &target, I32F32::from_num(0)); - let matrix: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); - let result = matmul_transpose_sparse(&matrix, &vector); - let target: Vec = vec_to_fixed(&[0., 0., 0., 0.]); - assert_vec_compare(&result, &target, I32F32::from_num(0)); -} - -#[test] -fn test_math_inplace_col_clip() { - let vector: Vec = vec_to_fixed(&[0., 5., 12.]); - let matrix: Vec = vec![0., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; - let mut matrix = vec_to_mat_fixed(&matrix, 4, false); - let target: Vec = vec![0., 2., 3., 0., 5., 6., 0., 5., 9., 0., 5., 12.]; - let target = vec_to_mat_fixed(&target, 4, false); - inplace_col_clip(&mut matrix, &vector); - assert_mat_compare(&matrix, &target, I32F32::from_num(0)); -} - -#[test] -fn test_math_col_clip_sparse() { - let vector: Vec = vec_to_fixed(&[0., 5., 12.]); - let matrix: Vec = vec![0., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; - let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); - let target: Vec = vec![0., 2., 3., 0., 5., 6., 0., 5., 9., 0., 5., 12.]; - let target = vec_to_sparse_mat_fixed(&target, 4, false); - let result = col_clip_sparse(&matrix, &vector); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); - let matrix: Vec = vec![0., 2., 3., 4., 5., 6., 0., 0., 0., 10., 11., 12.]; - let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); - let target: Vec = vec![0., 2., 3., 0., 5., 6., 0., 0., 0., 0., 5., 12.]; - let target = vec_to_sparse_mat_fixed(&target, 4, false); - let result = col_clip_sparse(&matrix, &vector); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); - let matrix: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); - let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let target = vec_to_sparse_mat_fixed(&target, 4, false); - let result = col_clip_sparse(&matrix, &vector); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); -} - -#[test] -fn test_math_weighted_median() { - let mut rng = rand::rng(); - let zero: I32F32 = fixed(0.); - let one: I32F32 = fixed(1.); - for _ in 0..100 { - let stake: Vec = vec_to_fixed(&[]); - let score: Vec = vec_to_fixed(&[]); - let majority: I32F32 = fixed(0.51); - assert_eq!( - zero, - weighted_median( - &stake, - &score, - (0..stake.len()).collect::>().as_slice(), - one - majority, - zero, - stake.iter().sum() - ) - ); - - let stake: Vec = normalize(&vec_to_fixed(&[0.51])); - let score: Vec = vec_to_fixed(&[1.]); - let majority: I32F32 = fixed(0.51); - assert_eq!( - one, - weighted_median( - &stake, - &score, - (0..stake.len()).collect::>().as_slice(), - one - majority, - zero, - stake.iter().sum() - ) - ); - - let stake: Vec = vec_to_fixed(&[0.49, 0.51]); - let score: Vec = vec_to_fixed(&[0.5, 1.]); - let majority: I32F32 = fixed(0.51); - assert_eq!( - one, - weighted_median( - &stake, - &score, - (0..stake.len()).collect::>().as_slice(), - one - majority, - zero, - stake.iter().sum() - ) - ); - - let stake: Vec = vec_to_fixed(&[0.51, 0.49]); - let score: Vec = vec_to_fixed(&[0.5, 1.]); - let majority: I32F32 = fixed(0.51); - assert_eq!( - fixed(0.5), - weighted_median( - &stake, - &score, - (0..stake.len()).collect::>().as_slice(), - one - majority, - zero, - stake.iter().sum() - ) - ); - - let stake: Vec = vec_to_fixed(&[0.49, 0., 0.51]); - let score: Vec = vec_to_fixed(&[0.5, 0.7, 1.]); - let majority: I32F32 = fixed(0.51); - assert_eq!( - one, - weighted_median( - &stake, - &score, - (0..stake.len()).collect::>().as_slice(), - one - majority, - zero, - stake.iter().sum() - ) - ); - - let stake: Vec = vec_to_fixed(&[0.49, 0.01, 0.5]); - let score: Vec = vec_to_fixed(&[0.5, 0.7, 1.]); - let majority: I32F32 = fixed(0.51); - assert_eq!( - fixed(0.7), - weighted_median( - &stake, - &score, - (0..stake.len()).collect::>().as_slice(), - one - majority, - zero, - stake.iter().sum() - ) - ); - - let stake: Vec = vec_to_fixed(&[0.49, 0.51, 0.0]); - let score: Vec = vec_to_fixed(&[0.5, 0.7, 1.]); - let majority: I32F32 = fixed(0.51); - assert_eq!( - fixed(0.7), - weighted_median( - &stake, - &score, - (0..stake.len()).collect::>().as_slice(), - one - majority, - zero, - stake.iter().sum() - ) - ); - - let stake: Vec = vec_to_fixed(&[0.0, 0.49, 0.51]); - let score: Vec = vec_to_fixed(&[0.5, 0.7, 1.]); - let majority: I32F32 = fixed(0.51); - assert_eq!( - one, - weighted_median( - &stake, - &score, - (0..stake.len()).collect::>().as_slice(), - one - majority, - zero, - stake.iter().sum() - ) - ); - - let stake: Vec = vec_to_fixed(&[0.0, 0.49, 0.0, 0.51]); - let score: Vec = vec_to_fixed(&[0.5, 0.5, 1., 1.]); - let majority: I32F32 = fixed(0.51); - assert_eq!( - one, - weighted_median( - &stake, - &score, - (0..stake.len()).collect::>().as_slice(), - one - majority, - zero, - stake.iter().sum() - ) - ); - - let stake: Vec = vec_to_fixed(&[0.0, 0.49, 0.0, 0.51, 0.0]); - let score: Vec = vec_to_fixed(&[0.5, 0.5, 1., 1., 0.5]); - let majority: I32F32 = fixed(0.51); - assert_eq!( - one, - weighted_median( - &stake, - &score, - (0..stake.len()).collect::>().as_slice(), - one - majority, - zero, - stake.iter().sum() - ) - ); - - let stake: Vec = vec_to_fixed(&[0.2, 0.2, 0.2, 0.2, 0.2]); - let score: Vec = vec_to_fixed(&[0.8, 0.2, 1., 0.6, 0.4]); - let majority: I32F32 = fixed(0.51); - assert_eq!( - fixed(0.6), - weighted_median( - &stake, - &score, - (0..stake.len()).collect::>().as_slice(), - one - majority, - zero, - stake.iter().sum() - ) - ); - - let stake: Vec = vec_to_fixed(&[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]); - let score: Vec = vec_to_fixed(&[0.8, 0.8, 0.2, 0.2, 1.0, 1.0, 0.6, 0.6, 0.4, 0.4]); - let majority: I32F32 = fixed(0.51); - assert_eq!( - fixed(0.6), - weighted_median( - &stake, - &score, - (0..stake.len()).collect::>().as_slice(), - one - majority, - zero, - stake.iter().sum() - ) - ); - - let n: usize = 100; - for majority in vec_to_fixed(&[ - 0., 0.0000001, 0.25, 0.49, 0.49, 0.49, 0.5, 0.51, 0.51, 0.51, 0.9999999, 1., - ]) { - for allow_equal in [false, true] { - let mut stake: Vec = vec![]; - let mut score: Vec = vec![]; - let mut last_score: I32F32 = zero; - for i in 0..n { - if allow_equal { - match rng.random_range(0..2) { - 1 => stake.push(one), - _ => stake.push(zero), - } - if rng.random_range(0..2) == 1 { - last_score += one - } - score.push(last_score); - } else { - stake.push(one); - score.push(I32F32::from_num(i)); - } - } - inplace_normalize(&mut stake); - let total_stake: I32F32 = stake.iter().sum(); - let mut minority: I32F32 = total_stake - majority; - if minority < zero { - minority = zero; - } - let mut medians: Vec = vec![]; - let mut median_stake: I32F32 = zero; - let mut median_set = false; - let mut stake_sum: I32F32 = zero; - for i in 0..n { - stake_sum += stake[i]; - if !median_set && stake_sum >= minority { - median_stake = stake_sum; - median_set = true; - } - if median_set { - if median_stake < stake_sum { - if median_stake == minority && !medians.contains(&score[i]) { - medians.push(score[i]); - } - break; - } - if !medians.contains(&score[i]) { - medians.push(score[i]); - } - } - } - if medians.is_empty() { - medians.push(zero); - } - let stake_idx: Vec = (0..stake.len()).collect(); - let result: I32F32 = - weighted_median(&stake, &score, &stake_idx, minority, zero, total_stake); - assert!(medians.contains(&result)); - for _ in 0..10 { - let mut permuted_uids: Vec = (0..n).collect(); - permuted_uids.shuffle(&mut rng); - stake = permuted_uids.iter().map(|&i| stake[i]).collect(); - score = permuted_uids.iter().map(|&i| score[i]).collect(); - let result: I32F32 = - weighted_median(&stake, &score, &stake_idx, minority, zero, total_stake); - assert!(medians.contains(&result)); - } - } - } - } -} - -#[test] -fn test_math_weighted_median_col() { - let stake: Vec = vec_to_fixed(&[]); - let weights: Vec> = vec![vec![]]; - let median: Vec = vec_to_fixed(&[]); - assert_eq!(median, weighted_median_col(&stake, &weights, fixed(0.5))); - - let stake: Vec = vec_to_fixed(&[0., 0.]); - let weights: Vec = vec![0., 0., 0., 0.]; - let weights: Vec> = vec_to_mat_fixed(&weights, 2, false); - let median: Vec = vec_to_fixed(&[0., 0.]); - assert_eq!(median, weighted_median_col(&stake, &weights, fixed(0.5))); - - let stake: Vec = vec_to_fixed(&[0., 0.75, 0.25, 0.]); - let weights: Vec = vec![0., 0.1, 0., 0., 0.2, 0.4, 0., 0.3, 0.1, 0., 0.4, 0.5]; - let weights: Vec> = vec_to_mat_fixed(&weights, 4, false); - let median: Vec = vec_to_fixed(&[0., 0.3, 0.4]); - assert_eq!(median, weighted_median_col(&stake, &weights, fixed(0.24))); - let median: Vec = vec_to_fixed(&[0., 0.2, 0.4]); - assert_eq!(median, weighted_median_col(&stake, &weights, fixed(0.26))); - let median: Vec = vec_to_fixed(&[0., 0.2, 0.1]); - assert_eq!(median, weighted_median_col(&stake, &weights, fixed(0.76))); - - let stake: Vec = vec_to_fixed(&[0., 0.3, 0.2, 0.5]); - let weights: Vec = vec![0., 0.1, 0., 0., 0.2, 0.4, 0., 0.3, 0.1, 0., 0., 0.5]; - let weights: Vec> = vec_to_mat_fixed(&weights, 4, false); - let median: Vec = vec_to_fixed(&[0., 0., 0.4]); - assert_eq!(median, weighted_median_col(&stake, &weights, fixed(0.51))); -} - -#[test] -fn test_math_weighted_median_col_sparse() { - let stake: Vec = vec_to_fixed(&[]); - let weights: Vec> = vec![vec![]]; - let median: Vec = vec_to_fixed(&[]); - assert_eq!( - median, - weighted_median_col_sparse(&stake, &weights, 0, fixed(0.5)) - ); - - let stake: Vec = vec_to_fixed(&[0., 0.]); - let weights: Vec = vec![0., 0., 0., 0.]; - let weights: Vec> = vec_to_sparse_mat_fixed(&weights, 2, false); - let median: Vec = vec_to_fixed(&[0., 0.]); - assert_eq!( - median, - weighted_median_col_sparse(&stake, &weights, 2, fixed(0.5)) - ); - - let stake: Vec = vec_to_fixed(&[0., 0.75, 0.25, 0.]); - let weights: Vec = vec![0., 0.1, 0., 0., 0.2, 0.4, 0., 0.3, 0.1, 0., 0.4, 0.5]; - let weights: Vec> = vec_to_sparse_mat_fixed(&weights, 4, false); - let median: Vec = vec_to_fixed(&[0., 0.3, 0.4]); - assert_eq!( - median, - weighted_median_col_sparse(&stake, &weights, 3, fixed(0.24)) - ); - let median: Vec = vec_to_fixed(&[0., 0.2, 0.4]); - assert_eq!( - median, - weighted_median_col_sparse(&stake, &weights, 3, fixed(0.26)) - ); - let median: Vec = vec_to_fixed(&[0., 0.2, 0.1]); - assert_eq!( - median, - weighted_median_col_sparse(&stake, &weights, 3, fixed(0.76)) - ); - - let stake: Vec = vec_to_fixed(&[0., 0.3, 0.2, 0.5]); - let weights: Vec = vec![0., 0.1, 0., 0., 0.2, 0.4, 0., 0.3, 0.1, 0., 0., 0.5]; - let weights: Vec> = vec_to_sparse_mat_fixed(&weights, 4, false); - let median: Vec = vec_to_fixed(&[0., 0., 0.4]); - assert_eq!( - median, - weighted_median_col_sparse(&stake, &weights, 3, fixed(0.51)) - ); -} - -#[test] -fn test_math_interpolate() { - let mat1: Vec> = vec![vec![]]; - let mat2: Vec> = vec![vec![]]; - let target: Vec> = vec![vec![]]; - let ratio = I32F32::from_num(0); - let result = interpolate(&mat1, &mat2, ratio); - assert_mat_compare(&result, &target, I32F32::from_num(0)); - - let mat1: Vec> = vec![vec![I32F32::from_num(0)]]; - let mat2: Vec> = vec![vec![I32F32::from_num(1)]]; - let target: Vec> = vec![vec![I32F32::from_num(0)]]; - let ratio = I32F32::from_num(0); - let result = interpolate(&mat1, &mat2, ratio); - assert_mat_compare(&result, &target, I32F32::from_num(0)); - - let target: Vec> = vec![vec![I32F32::from_num(1)]]; - let ratio = I32F32::from_num(1); - let result = interpolate(&mat1, &mat2, ratio); - assert_mat_compare(&result, &target, I32F32::from_num(0)); - - let mat1: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let mat2: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let mat1 = vec_to_mat_fixed(&mat1, 4, false); - let mat2 = vec_to_mat_fixed(&mat2, 4, false); - let ratio = I32F32::from_num(0); - let target = vec_to_mat_fixed(&target, 4, false); - let result = interpolate(&mat1, &mat2, ratio); - assert_mat_compare(&result, &target, I32F32::from_num(0)); - - let ratio = I32F32::from_num(1); - let result = interpolate(&mat1, &mat2, ratio); - assert_mat_compare(&result, &target, I32F32::from_num(0)); - - let mat1: Vec = vec![1., 10., 100., 1000., 10000., 100000.]; - let mat2: Vec = vec![10., 100., 1000., 10000., 100000., 1000000.]; - let target: Vec = vec![1., 10., 100., 1000., 10000., 100000.]; - let mat1 = vec_to_mat_fixed(&mat1, 3, false); - let mat2 = vec_to_mat_fixed(&mat2, 3, false); - let ratio = I32F32::from_num(0); - let target = vec_to_mat_fixed(&target, 3, false); - let result = interpolate(&mat1, &mat2, ratio); - assert_mat_compare(&result, &target, I32F32::from_num(0)); - - let target: Vec = vec![9.1, 91., 910., 9100., 91000., 910000.]; - let ratio = I32F32::from_num(0.9); - let target = vec_to_mat_fixed(&target, 3, false); - let result = interpolate(&mat1, &mat2, ratio); - assert_mat_compare(&result, &target, I32F32::from_num(0.0001)); - - let mat1: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let mat2: Vec = vec![1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]; - let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let mat1 = vec_to_mat_fixed(&mat1, 4, false); - let mat2 = vec_to_mat_fixed(&mat2, 4, false); - let ratio = I32F32::from_num(0); - let target = vec_to_mat_fixed(&target, 4, false); - let result = interpolate(&mat1, &mat2, ratio); - assert_mat_compare(&result, &target, I32F32::from_num(0)); - - let target: Vec = vec![ - 0.000000001, - 0.000000001, - 0.000000001, - 0.000000001, - 0.000000001, - 0.000000001, - 0.000000001, - 0.000000001, - 0.000000001, - 0.000000001, - 0.000000001, - 0.000000001, - ]; - let ratio = I32F32::from_num(0.000000001); - let target = vec_to_mat_fixed(&target, 4, false); - let result = interpolate(&mat1, &mat2, ratio); - assert_mat_compare(&result, &target, I32F32::from_num(0)); - - let target: Vec = vec![0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]; - let ratio = I32F32::from_num(0.5); - let target = vec_to_mat_fixed(&target, 4, false); - let result = interpolate(&mat1, &mat2, ratio); - assert_mat_compare(&result, &target, I32F32::from_num(0)); - - let target: Vec = vec![ - 0.999_999_9, - 0.999_999_9, - 0.999_999_9, - 0.999_999_9, - 0.999_999_9, - 0.999_999_9, - 0.999_999_9, - 0.999_999_9, - 0.999_999_9, - 0.999_999_9, - 0.999_999_9, - 0.999_999_9, - ]; - let ratio = I32F32::from_num(0.9999998808); - let target = vec_to_mat_fixed(&target, 4, false); - let result = interpolate(&mat1, &mat2, ratio); - assert_mat_compare(&result, &target, I32F32::from_num(0)); - - let target: Vec = vec![1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]; - let ratio = I32F32::from_num(1); - let target = vec_to_mat_fixed(&target, 4, false); - let result = interpolate(&mat1, &mat2, ratio); - assert_mat_compare(&result, &target, I32F32::from_num(0)); -} - -#[test] -fn test_math_interpolate_sparse() { - let mat1: Vec> = vec![vec![]]; - let mat2: Vec> = vec![vec![]]; - let target: Vec> = vec![vec![]]; - let ratio = I32F32::from_num(0); - let result = interpolate_sparse(&mat1, &mat2, 0, ratio); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); - - let mat1: Vec = vec![0.]; - let mat2: Vec = vec![1.]; - let target: Vec = vec![0.]; - let mat1 = vec_to_sparse_mat_fixed(&mat1, 1, false); - let mat2 = vec_to_sparse_mat_fixed(&mat2, 1, false); - let ratio = I32F32::from_num(0); - let target = vec_to_sparse_mat_fixed(&target, 1, false); - let result = interpolate_sparse(&mat1, &mat2, 1, ratio); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); - - let target: Vec = vec![0.5]; - let ratio = I32F32::from_num(0.5); - let target = vec_to_sparse_mat_fixed(&target, 1, false); - let result = interpolate_sparse(&mat1, &mat2, 1, ratio); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); - - let target: Vec = vec![1.]; - let ratio = I32F32::from_num(1); - let target = vec_to_sparse_mat_fixed(&target, 1, false); - let result = interpolate_sparse(&mat1, &mat2, 1, ratio); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); - - let mat1: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let mat2: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let mat1 = vec_to_sparse_mat_fixed(&mat1, 4, false); - let mat2 = vec_to_sparse_mat_fixed(&mat2, 4, false); - let ratio = I32F32::from_num(0); - let target = vec_to_sparse_mat_fixed(&target, 4, false); - let result = interpolate_sparse(&mat1, &mat2, 3, ratio); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); - - let ratio = I32F32::from_num(1); - let result = interpolate_sparse(&mat1, &mat2, 3, ratio); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); - - let mat1: Vec = vec![1., 0., 100., 1000., 10000., 100000.]; - let mat2: Vec = vec![10., 100., 1000., 10000., 100000., 0.]; - let target: Vec = vec![1., 0., 100., 1000., 10000., 100000.]; - let mat1 = vec_to_sparse_mat_fixed(&mat1, 3, false); - let mat2 = vec_to_sparse_mat_fixed(&mat2, 3, false); - let ratio = I32F32::from_num(0); - let target = vec_to_sparse_mat_fixed(&target, 3, false); - let result = interpolate_sparse(&mat1, &mat2, 2, ratio); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); - - let target: Vec = vec![9.1, 90., 910., 9100., 91000., 10000.]; - let ratio = I32F32::from_num(0.9); - let target = vec_to_sparse_mat_fixed(&target, 3, false); - let result = interpolate_sparse(&mat1, &mat2, 2, ratio); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(0.0001)); - - let mat1: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let mat2: Vec = vec![1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]; - let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let mat1 = vec_to_sparse_mat_fixed(&mat1, 4, false); - let mat2 = vec_to_sparse_mat_fixed(&mat2, 4, false); - let ratio = I32F32::from_num(0); - let target = vec_to_sparse_mat_fixed(&target, 4, false); - let result = interpolate_sparse(&mat1, &mat2, 3, ratio); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); - - let target: Vec = vec![ - 0.000000001, - 0.000000001, - 0.000000001, - 0.000000001, - 0.000000001, - 0.000000001, - 0.000000001, - 0.000000001, - 0.000000001, - 0.000000001, - 0.000000001, - 0.000000001, - ]; - let ratio = I32F32::from_num(0.000000001); - let target = vec_to_sparse_mat_fixed(&target, 4, false); - let result = interpolate_sparse(&mat1, &mat2, 3, ratio); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); - - let target: Vec = vec![0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]; - let ratio = I32F32::from_num(0.5); - let target = vec_to_sparse_mat_fixed(&target, 4, false); - let result = interpolate_sparse(&mat1, &mat2, 3, ratio); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); - - let target: Vec = vec![ - 0.999_999_9, - 0.999_999_9, - 0.999_999_9, - 0.999_999_9, - 0.999_999_9, - 0.999_999_9, - 0.999_999_9, - 0.999_999_9, - 0.999_999_9, - 0.999_999_9, - 0.999_999_9, - 0.999_999_9, - ]; - let ratio = I32F32::from_num(0.9999998808); - let target = vec_to_sparse_mat_fixed(&target, 4, false); - let result = interpolate_sparse(&mat1, &mat2, 3, ratio); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); - - let target: Vec = vec![1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]; - let ratio = I32F32::from_num(1); - let target = vec_to_sparse_mat_fixed(&target, 4, false); - let result = interpolate_sparse(&mat1, &mat2, 3, ratio); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); -} - -#[test] -fn test_math_mat_ema_alpha() { - let old: Vec = vec![ - 0.1, 0.2, 3., 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.10, 0.11, 0.12, - ]; - let new: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; - let target: Vec = vec![ - 0.19, 0.38, 1., 0.436, 0.545, 0.6539, 0.763, 0.8719, 0.981, 1., 1., 1., - ]; - - let old = vec_to_mat_fixed(&old, 4, false); - let new = vec_to_mat_fixed(&new, 4, false); - let target = vec_to_mat_fixed(&target, 4, false); - let alphas = vec_to_mat_fixed(&[0.1; 12], 4, false); - let result = mat_ema_alpha(&new, &old, &alphas); - assert_mat_compare(&result, &target, I32F32::from_num(1e-4)); - let old: Vec = vec![ - 0.1, 0.2, 3., 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.10, 0.11, 0.12, - ]; - let new: Vec = vec![ - 10., 20., 30., 40., 50., 60., 70., 80., 90., 100., 110., 120., - ]; - let target: Vec = vec![ - 0.10, 0.2, 1., 0.0399, 0.05, 0.0599, 0.07, 0.07999, 0.09, 0.1, 0.10999, 0.11999, - ]; - let old = vec_to_mat_fixed(&old, 4, false); - let new = vec_to_mat_fixed(&new, 4, false); - let target = vec_to_mat_fixed(&target, 4, false); - let alphas = vec_to_mat_fixed(&[0.; 12], 4, false); - let result = mat_ema_alpha(&new, &old, &alphas); - assert_mat_compare(&result, &target, I32F32::from_num(1e-4)); - let old: Vec = vec![ - 0.001, 0.002, 0.003, 0.004, 0.05, 0.006, 0.007, 0.008, 0.009, 0.010, 0.011, 0.012, - ]; - let new: Vec = vec![ - 0.1, 0.2, 3., 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.10, 0.11, 0.12, - ]; - let target: Vec = vec![ - 0.10, 0.2, 1., 0.0399, 0.05, 0.0599, 0.07, 0.07999, 0.09, 0.1, 0.10999, 0.11999, - ]; - - let old = vec_to_mat_fixed(&old, 4, false); - let new = vec_to_mat_fixed(&new, 4, false); - let target = vec_to_mat_fixed(&target, 4, false); - let alphas = vec_to_mat_fixed(&[1.; 12], 4, false); - let result = mat_ema_alpha(&new, &old, &alphas); - assert_mat_compare(&result, &target, I32F32::from_num(1e-4)); -} - -#[test] -fn test_math_sparse_mat_ema_alpha() { - let old: Vec = vec![ - 0.1, 0.2, 3., 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.10, 0.11, 0.12, - ]; - let new: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; - let target: Vec = vec![ - 0.19, 0.38, 1., 0.43599, 0.545, 0.65399, 0.763, 0.87199, 0.981, 1., 1., 1., - ]; - let old = vec_to_sparse_mat_fixed(&old, 4, false); - let new = vec_to_sparse_mat_fixed(&new, 4, false); - let target = vec_to_sparse_mat_fixed(&target, 4, false); - let alphas = vec_to_mat_fixed(&[0.1; 12], 4, false); - let result = mat_ema_alpha_sparse(&new, &old, &alphas); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(1e-4)); - let old: Vec = vec![ - 0.001, 0.002, 0.003, 0.004, 0.05, 0.006, 0.007, 0.008, 0.009, 0.010, 0.011, 0.012, - ]; - let new: Vec = vec![ - 0.1, 0.2, 3., 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.10, 0.11, 0.12, - ]; - let target: Vec = vec![ - 0.0109, 0.0218, 0.30270, 0.007599, 0.05, 0.01139, 0.0133, 0.01519, 0.017, 0.01899, 0.02089, - 0.0227, - ]; - let old = vec_to_sparse_mat_fixed(&old, 4, false); - let new = vec_to_sparse_mat_fixed(&new, 4, false); - let target = vec_to_sparse_mat_fixed(&target, 4, false); - let alphas = vec_to_mat_fixed(&[0.1; 12], 4, false); - let result = mat_ema_alpha_sparse(&new, &old, &alphas); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(1e-4)); - let old: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let new: Vec = vec![ - 0.1, 0.2, 3., 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.10, 0.11, 0.12, - ]; - let target: Vec = vec![ - 0.01, 0.02, 0.3, 0.00399, 0.005, 0.00599, 0.007, 0.00799, 0.009, 0.01, 0.011, 0.01199, - ]; - let old = vec_to_sparse_mat_fixed(&old, 4, false); - let new = vec_to_sparse_mat_fixed(&new, 4, false); - let target = vec_to_sparse_mat_fixed(&target, 4, false); - let alphas = vec_to_mat_fixed(&[0.1; 12], 4, false); - let result = mat_ema_alpha_sparse(&new, &old, &alphas); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(1e-4)); - let old: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let new: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let old = vec_to_sparse_mat_fixed(&old, 4, false); - let new = vec_to_sparse_mat_fixed(&new, 4, false); - let target = vec_to_sparse_mat_fixed(&target, 4, false); - let alphas = vec_to_mat_fixed(&[0.1; 12], 4, false); - let result = mat_ema_alpha_sparse(&new, &old, &alphas); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(1e-4)); - let old: Vec = vec![1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - let new: Vec = vec![0., 0., 0., 0., 2., 0., 0., 0., 0., 0., 0., 0.]; - let target: Vec = vec![0.0, 0., 0., 0., 0.2, 0., 0., 0., 0., 0., 0., 0.]; - let old = vec_to_sparse_mat_fixed(&old, 4, false); - let new = vec_to_sparse_mat_fixed(&new, 4, false); - let target = vec_to_sparse_mat_fixed(&target, 4, false); - let alphas = vec_to_mat_fixed(&[0.1; 12], 4, false); - let result = mat_ema_alpha_sparse(&new, &old, &alphas); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(1e-1)); -} - -#[test] -fn test_math_matmul2() { - let epsilon: I32F32 = I32F32::from_num(0.0001); - let w: Vec> = vec![vec![I32F32::from_num(1.0); 3]; 3]; - assert_vec_compare( - &matmul(&w, &[I32F32::from_num(1.0); 3]), - &[ - I32F32::from_num(3), - I32F32::from_num(3), - I32F32::from_num(3), - ], - epsilon, - ); - assert_vec_compare( - &matmul(&w, &[I32F32::from_num(2.0); 3]), - &[ - I32F32::from_num(6), - I32F32::from_num(6), - I32F32::from_num(6), - ], - epsilon, - ); - assert_vec_compare( - &matmul(&w, &[I32F32::from_num(3.0); 3]), - &[ - I32F32::from_num(9), - I32F32::from_num(9), - I32F32::from_num(9), - ], - epsilon, - ); - assert_vec_compare( - &matmul(&w, &[I32F32::from_num(-1.0); 3]), - &[ - I32F32::from_num(-3), - I32F32::from_num(-3), - I32F32::from_num(-3), - ], - epsilon, - ); - let w: Vec> = vec![vec![I32F32::from_num(-1.0); 3]; 3]; - assert_vec_compare( - &matmul(&w, &[I32F32::from_num(1.0); 3]), - &[ - I32F32::from_num(-3), - I32F32::from_num(-3), - I32F32::from_num(-3), - ], - epsilon, - ); - assert_vec_compare( - &matmul(&w, &[I32F32::from_num(2.0); 3]), - &[ - I32F32::from_num(-6), - I32F32::from_num(-6), - I32F32::from_num(-6), - ], - epsilon, - ); - assert_vec_compare( - &matmul(&w, &[I32F32::from_num(3.0); 3]), - &[ - I32F32::from_num(-9), - I32F32::from_num(-9), - I32F32::from_num(-9), - ], - epsilon, - ); - assert_vec_compare( - &matmul(&w, &[I32F32::from_num(-1.0); 3]), - &[ - I32F32::from_num(3), - I32F32::from_num(3), - I32F32::from_num(3), - ], - epsilon, - ); - let w: Vec> = vec![ - vec![I32F32::from_num(1.0); 3], - vec![I32F32::from_num(2.0); 3], - vec![I32F32::from_num(3.0); 3], - ]; - assert_vec_compare( - &matmul(&w, &[I32F32::from_num(0.0); 3]), - &[ - I32F32::from_num(0.0), - I32F32::from_num(0.0), - I32F32::from_num(0.0), - ], - epsilon, - ); - assert_vec_compare( - &matmul(&w, &[I32F32::from_num(2.0); 3]), - &[ - I32F32::from_num(12), - I32F32::from_num(12), - I32F32::from_num(12), - ], - epsilon, - ); - let w: Vec> = vec![ - vec![ - I32F32::from_num(1), - I32F32::from_num(2), - I32F32::from_num(3) - ]; - 3 - ]; - assert_vec_compare( - &matmul(&w, &[I32F32::from_num(0.0); 3]), - &[ - I32F32::from_num(0.0), - I32F32::from_num(0.0), - I32F32::from_num(0.0), - ], - epsilon, - ); - assert_vec_compare( - &matmul(&w, &[I32F32::from_num(2.0); 3]), - &[ - I32F32::from_num(6), - I32F32::from_num(12), - I32F32::from_num(18), - ], - epsilon, - ); -} - -#[test] -fn test_math_fixed_to_u16() { - let expected = u16::MIN; - assert_eq!(fixed_to_u16(I32F32::from_num(expected)), expected); - - let expected = u16::MAX / 2; - assert_eq!(fixed_to_u16(I32F32::from_num(expected)), expected); - - let expected = u16::MAX; - assert_eq!(fixed_to_u16(I32F32::from_num(expected)), expected); -} - -#[test] -#[should_panic(expected = "overflow")] -fn test_math_fixed_to_u16_panics() { - let bad_input = I32F32::from_num(u32::MAX); - fixed_to_u16(bad_input); - - let bad_input = I32F32::from_num(-1); - fixed_to_u16(bad_input); -} - -// TODO: Investigate why `I32F32` and not `I64F64` -#[test] -fn test_math_fixed_to_u64() { - let expected = u64::MIN; - assert_eq!(fixed_to_u64(I32F32::from_num(expected)), expected); - - // let expected = u64::MAX / 2; - // assert_eq!(fixed_to_u64(I32F32::from_num(expected)), expected); - - // let expected = u64::MAX; - // assert_eq!(fixed_to_u64(I32F32::from_num(expected)), expected); -} - -#[test] -fn test_math_fixed_to_u64_saturates() { - let bad_input = I32F32::from_num(-1); - let expected = 0; - assert_eq!(fixed_to_u64(bad_input), expected); -} - -#[test] -fn test_math_fixed64_to_u64() { - let expected = u64::MIN; - let input = I64F64::from_num(expected); - assert_eq!(fixed64_to_u64(input), expected); - - let input = i64::MAX / 2; - let expected = u64::try_from(input).unwrap(); - assert_eq!(fixed64_to_u64(I64F64::from_num(input)), expected); - - let input = i64::MAX; - let expected = u64::try_from(input).unwrap(); - assert_eq!(fixed64_to_u64(I64F64::from_num(input)), expected); -} - -#[test] -fn test_math_fixed64_to_u64_saturates() { - let bad_input = I64F64::from_num(-1); - let expected = 0; - assert_eq!(fixed64_to_u64(bad_input), expected); -} - -/* @TODO: find the _true_ max, and half, input values */ -#[test] -fn test_math_fixed64_to_fixed32() { - let input = u64::MIN; - let expected = u32::try_from(input).unwrap(); - assert_eq!(fixed64_to_fixed32(I64F64::from_num(expected)), expected); - - let expected = u32::MAX / 2; - let input = u64::from(expected); - assert_eq!(fixed64_to_fixed32(I64F64::from_num(input)), expected); -} - -#[test] -fn test_math_fixed64_to_fixed32_saturates() { - let bad_input = I64F64::from_num(u32::MAX); - assert_eq!(fixed64_to_fixed32(bad_input), I32F32::max_value()); -} - -#[test] -fn test_math_u16_to_fixed() { - let input = u16::MIN; - let expected = I32F32::from_num(input); - assert_eq!(u16_to_fixed(input), expected); - - let input = u16::MAX / 2; - let expected = I32F32::from_num(input); - assert_eq!(u16_to_fixed(input), expected); - - let input = u16::MAX; - let expected = I32F32::from_num(input); - assert_eq!(u16_to_fixed(input), expected); -} - -#[test] -fn test_math_u16_proportion_to_fixed() { - let input = u16::MIN; - let expected = I32F32::from_num(input); - assert_eq!(u16_proportion_to_fixed(input), expected); -} - -#[test] -fn test_fixed_proportion_to_u16() { - let expected = u16::MIN; - let input = I32F32::from_num(expected); - assert_eq!(fixed_proportion_to_u16(input), expected); -} - -#[test] -fn test_fixed_proportion_to_u16_saturates() { - let expected = u16::MAX; - let input = I32F32::from_num(expected); - log::trace!("Testing with input: {input:?}"); // Debug output - let result = fixed_proportion_to_u16(input); - log::trace!("Testing with result: {result:?}"); // Debug output - assert_eq!(result, expected); -} - -#[test] -fn test_vec_fixed64_to_fixed32() { - let input = vec![I64F64::from_num(i32::MIN)]; - let expected = vec![I32F32::from_num(i32::MIN)]; - assert_eq!(vec_fixed64_to_fixed32(input), expected); - - let input = vec![I64F64::from_num(i32::MAX)]; - let expected = vec![I32F32::from_num(i32::MAX)]; - assert_eq!(vec_fixed64_to_fixed32(input), expected); -} - -#[test] -fn test_vec_fixed64_to_fixed32_saturates() { - let bad_input = vec![I64F64::from_num(i64::MAX)]; - assert_eq!(vec_fixed64_to_fixed32(bad_input), [I32F32::max_value()]); -} - -#[test] -#[allow(arithmetic_overflow)] -fn test_checked_sum() { - let overflowing_input = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, u64::MAX]; - // Expect None when overflow occurs - assert_eq!(checked_sum(&overflowing_input), None); - - let normal_input = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - // Expect Some when no overflow occurs - assert_eq!(checked_sum(&normal_input), Some(55)); - - let empty_input: Vec = vec![]; - // Expect Some(u16::default()) when input is empty - assert_eq!(checked_sum(&empty_input), Some(u16::default())); - - let single_input = vec![1]; - // Expect Some(...) when input is a single value - assert_eq!(checked_sum(&single_input), Some(1)); -} - -#[test] -fn test_mat_ema_alpha_sparse_empty() { - let new: Vec> = Vec::new(); - let old: Vec> = Vec::new(); - let alpha: Vec> = Vec::new(); - let result = mat_ema_alpha_sparse(&new, &old, &alpha); - assert_eq!(result, Vec::>::new()); -} - -#[test] -fn test_mat_ema_alpha_sparse_single_element() { - let new: Vec> = vec![vec![(0, I32F32::from_num(1.0))]]; - let old: Vec> = vec![vec![(0, I32F32::from_num(2.0))]]; - let alpha = vec![vec![I32F32::from_num(0.5)]]; - let result = mat_ema_alpha_sparse(&new, &old, &alpha); - assert_eq!(result, vec![vec![(0, I32F32::from_num(1.0))]]); -} - -#[test] -fn test_mat_ema_alpha_sparse_multiple_elements() { - let new: Vec> = vec![ - vec![(0, I32F32::from_num(1.0)), (1, I32F32::from_num(2.0))], - vec![(0, I32F32::from_num(3.0)), (1, I32F32::from_num(4.0))], - ]; - let old: Vec> = vec![ - vec![(0, I32F32::from_num(5.0)), (1, I32F32::from_num(6.0))], - vec![(0, I32F32::from_num(7.0)), (1, I32F32::from_num(8.0))], - ]; - let alpha = vec![vec![I32F32::from_num(0.1), I32F32::from_num(0.2)]; 2]; - let result = mat_ema_alpha_sparse(&new, &old, &alpha); - let expected = vec![ - vec![(0, I32F32::from_num(1.0)), (1, I32F32::from_num(1.0))], - vec![(0, I32F32::from_num(1.0)), (1, I32F32::from_num(1.0))], - ]; - assert_sparse_mat_compare(&result, &expected, I32F32::from_num(0.000001)); -} - -#[test] -fn test_mat_ema_alpha_sparse_zero_alpha() { - let new: Vec> = vec![vec![(0, I32F32::from_num(1.0))]]; - let old: Vec> = vec![vec![(0, I32F32::from_num(2.0))]]; - let alpha = vec![vec![I32F32::from_num(0.1), I32F32::from_num(0.0)]]; - let result = mat_ema_alpha_sparse(&new, &old, &alpha); - assert_eq!(result, vec![vec![(0, I32F32::from_num(1.0))]]); -} - -#[test] -fn test_mat_ema_alpha_sparse_one_alpha() { - let new: Vec> = vec![vec![(0, I32F32::from_num(1.0))]]; - let old: Vec> = vec![vec![(0, I32F32::from_num(2.0))]]; - let alpha = vec![vec![I32F32::from_num(1.0), I32F32::from_num(0.0)]]; - let result = mat_ema_alpha_sparse(&new, &old, &alpha); - assert_eq!(result, vec![vec![(0, I32F32::from_num(1.0))]]); -} - -#[test] -fn test_mat_ema_alpha_sparse_mixed_alpha() { - let new: Vec> = vec![ - vec![(0, I32F32::from_num(1.0)), (1, I32F32::from_num(2.0))], - vec![(0, I32F32::from_num(3.0)), (1, I32F32::from_num(4.0))], - ]; - let old: Vec> = vec![ - vec![(0, I32F32::from_num(5.0)), (1, I32F32::from_num(6.0))], - vec![(0, I32F32::from_num(7.0)), (1, I32F32::from_num(8.0))], - ]; - let alpha = vec![vec![I32F32::from_num(0.3), I32F32::from_num(0.7)]; 2]; - let result = mat_ema_alpha_sparse(&new, &old, &alpha); - assert_sparse_mat_compare( - &result, - &[ - vec![(0, I32F32::from_num(1.0)), (1, I32F32::from_num(1.0))], - vec![(0, I32F32::from_num(1.0)), (1, I32F32::from_num(1.0))], - ], - I32F32::from_num(0.000001), - ); -} - -#[test] -fn test_mat_ema_alpha_sparse_sparse_matrix() { - let new: Vec> = vec![ - vec![(0, I32F32::from_num(1.0))], - vec![(1, I32F32::from_num(4.0))], - ]; - let old: Vec> = vec![ - vec![(0, I32F32::from_num(5.0))], - vec![(1, I32F32::from_num(8.0))], - ]; - let alpha = vec![vec![I32F32::from_num(0.5), I32F32::from_num(0.5)]; 2]; - let result = mat_ema_alpha_sparse(&new, &old, &alpha); - assert_eq!( - result, - vec![ - vec![(0, I32F32::from_num(1.0))], - vec![(1, I32F32::from_num(1.0))] - ] - ); -} - -#[test] -fn test_mat_ema_alpha_basic() { - let new = mat_to_fixed(&[vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]]); - let old = mat_to_fixed(&[vec![0.5, 1.5, 2.5], vec![3.5, 4.5, 5.5]]); - let alpha = vec![ - vec![ - I32F32::from_num(0.5), - I32F32::from_num(0.5), - I32F32::from_num(0.5), - ]; - 2 - ]; - let expected = mat_to_fixed(&[vec![0.75, 1.0, 1.0], vec![1.0, 1.0, 1.0]]); - let result = mat_ema_alpha(&new, &old, &alpha); - assert_eq!(result, expected); -} - -#[test] -fn test_mat_ema_alpha_varying_alpha() { - let new = mat_to_fixed(&[vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]]); - let old = mat_to_fixed(&[vec![0.5, 1.5, 2.5], vec![3.5, 4.5, 5.5]]); - let alpha = vec![ - vec![ - I32F32::from_num(0.2), - I32F32::from_num(0.5), - I32F32::from_num(0.8), - ]; - 2 - ]; - let expected = mat_to_fixed(&[vec![0.6, 1.0, 1.0], vec![1.0, 1.0, 1.0]]); - let result = mat_ema_alpha(&new, &old, &alpha); - assert_mat_approx_eq(&result, &expected, I32F32::from_num(1e-6)); -} - -#[test] -fn test_mat_ema_alpha_sparse_varying_alpha() { - let weights = vec![ - vec![(0, I32F32::from_num(0.1)), (1, I32F32::from_num(0.2))], - vec![(0, I32F32::from_num(0.3)), (1, I32F32::from_num(0.4))], - ]; - let bonds = vec![ - vec![(0, I32F32::from_num(0.5)), (1, I32F32::from_num(0.6))], - vec![(0, I32F32::from_num(0.7)), (1, I32F32::from_num(0.8))], - ]; - let alpha = vec![ - vec![I32F32::from_num(0.9), I32F32::from_num(0.8)], - vec![I32F32::from_num(0.5), I32F32::from_num(0.7)], - ]; - - let expected = vec![ - vec![(0, I32F32::from_num(0.14)), (1, I32F32::from_num(0.28))], - vec![ - (0, I32F32::from_num(0.499999)), - (1, I32F32::from_num(0.519999)), - ], - ]; - - let result = mat_ema_alpha_sparse(&weights, &bonds, &alpha); - // Assert the results with an epsilon for approximate equality - assert_sparse_mat_compare(&result, &expected, I32F32::from_num(1e-6)); -} - -#[test] -fn test_mat_ema_alpha_empty_matrices() { - let new: Vec> = vec![]; - let old: Vec> = vec![]; - let alpha = vec![]; - let expected: Vec> = vec![vec![]; 1]; - let result = mat_ema_alpha(&new, &old, &alpha); - assert_eq!(result, expected); -} - -#[test] -fn test_mat_ema_alpha_single_element() { - let new = mat_to_fixed(&[vec![1.0]]); - let old = mat_to_fixed(&[vec![0.5]]); - let alpha = vec![vec![I32F32::from_num(0.5)]]; - let expected = mat_to_fixed(&[vec![0.75]]); - let result = mat_ema_alpha(&new, &old, &alpha); - assert_eq!(result, expected); -} - -#[test] -fn test_mat_ema_alpha_mismatched_dimensions() { - let new = mat_to_fixed(&[vec![1.0, 2.0], vec![3.0, 4.0]]); - let old = mat_to_fixed(&[vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]]); - let alpha = vec![ - vec![ - I32F32::from_num(0.5), - I32F32::from_num(0.5), - I32F32::from_num(0.5), - ]; - 2 - ]; - let result = mat_ema_alpha(&new, &old, &alpha); - assert_eq!(result[0][0], old[0][0]) -} diff --git a/pallets/subtensor/src/tests/math/ema_interpolate.rs b/pallets/subtensor/src/tests/math/ema_interpolate.rs new file mode 100644 index 0000000000..7084307f75 --- /dev/null +++ b/pallets/subtensor/src/tests/math/ema_interpolate.rs @@ -0,0 +1,610 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::unwrap_used, + clippy::indexing_slicing +)] +//! Tests for [`crate::epoch::math::ema_interpolate`]. + +use crate::epoch::math::*; +use substrate_fixed::types::I32F32; + +use super::helpers::*; + +#[test] +fn test_math_vec_mul() { + let vector: Vec = vec_to_fixed(&[1., 2., 3., 4.]); + let target: Vec = vec_to_fixed(&[1., 4., 9., 16.]); + let result = vec_mul(&vector, &vector); + assert_vec_compare(&result, &target, I32F32::from_num(0)); + let vector_empty: Vec = vec_to_fixed(&[]); + let result = vec_mul(&vector_empty, &vector); + let target: Vec = vec![]; + assert_vec_compare(&result, &target, I32F32::from_num(0)); + let vector_zero: Vec = vec_to_fixed(&[0., 0., 0., 0., 0., 0., 0., 0.]); + let result = vec_mul(&vector_zero, &vector); + let target: Vec = vec![I32F32::from_num(0); 4]; + assert_vec_compare(&result, &target, I32F32::from_num(0)); +} + +#[test] +fn test_math_mat_vec_mul() { + let matrix: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; + let matrix = vec_to_mat_fixed(&matrix, 4, false); + let vector: Vec = vec_to_fixed(&[1., 2., 3.]); + let target: Vec = vec![1., 4., 9., 4., 10., 18., 7., 16., 27., 10., 22., 36.]; + let target = vec_to_mat_fixed(&target, 4, false); + let result = mat_vec_mul(&matrix, &vector); + assert_mat_compare(&result, &target, I32F32::from_num(0)); + let vector_one: Vec = vec_to_fixed(&[1., 0., 0.]); + let target: Vec = vec![1., 0., 0., 4., 0., 0., 7., 0., 0., 10., 0., 0.]; + let target = vec_to_mat_fixed(&target, 4, false); + let result = mat_vec_mul(&matrix, &vector_one); + assert_mat_compare(&result, &target, I32F32::from_num(0)); + let vector_empty: Vec = vec_to_fixed(&[]); + let result = mat_vec_mul(&matrix, &vector_empty); + let target: Vec> = vec![vec![]; 4]; + assert_mat_compare(&result, &target, I32F32::from_num(0)); +} + +#[test] +fn test_math_mat_vec_mul_sparse() { + let matrix: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; + let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); + let vector: Vec = vec_to_fixed(&[1., 2., 3.]); + let target: Vec = vec![1., 4., 9., 4., 10., 18., 7., 16., 27., 10., 22., 36.]; + let target = vec_to_sparse_mat_fixed(&target, 4, false); + let result = mat_vec_mul_sparse(&matrix, &vector); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); + let vector_one: Vec = vec_to_fixed(&[1., 0., 0.]); + let target: Vec = vec![1., 0., 0., 4., 0., 0., 7., 0., 0., 10., 0., 0.]; + let target = vec_to_sparse_mat_fixed(&target, 4, false); + let result = mat_vec_mul_sparse(&matrix, &vector_one); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); + let vector_empty: Vec = vec_to_fixed(&[]); + let result = mat_vec_mul_sparse(&matrix, &vector_empty); + let target = vec![vec![]; 4]; + assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); +} + +#[test] +fn test_math_interpolate() { + let mat1: Vec> = vec![vec![]]; + let mat2: Vec> = vec![vec![]]; + let target: Vec> = vec![vec![]]; + let ratio = I32F32::from_num(0); + let result = interpolate(&mat1, &mat2, ratio); + assert_mat_compare(&result, &target, I32F32::from_num(0)); + + let mat1: Vec> = vec![vec![I32F32::from_num(0)]]; + let mat2: Vec> = vec![vec![I32F32::from_num(1)]]; + let target: Vec> = vec![vec![I32F32::from_num(0)]]; + let ratio = I32F32::from_num(0); + let result = interpolate(&mat1, &mat2, ratio); + assert_mat_compare(&result, &target, I32F32::from_num(0)); + + let target: Vec> = vec![vec![I32F32::from_num(1)]]; + let ratio = I32F32::from_num(1); + let result = interpolate(&mat1, &mat2, ratio); + assert_mat_compare(&result, &target, I32F32::from_num(0)); + + let mat1: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let mat2: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let mat1 = vec_to_mat_fixed(&mat1, 4, false); + let mat2 = vec_to_mat_fixed(&mat2, 4, false); + let ratio = I32F32::from_num(0); + let target = vec_to_mat_fixed(&target, 4, false); + let result = interpolate(&mat1, &mat2, ratio); + assert_mat_compare(&result, &target, I32F32::from_num(0)); + + let ratio = I32F32::from_num(1); + let result = interpolate(&mat1, &mat2, ratio); + assert_mat_compare(&result, &target, I32F32::from_num(0)); + + let mat1: Vec = vec![1., 10., 100., 1000., 10000., 100000.]; + let mat2: Vec = vec![10., 100., 1000., 10000., 100000., 1000000.]; + let target: Vec = vec![1., 10., 100., 1000., 10000., 100000.]; + let mat1 = vec_to_mat_fixed(&mat1, 3, false); + let mat2 = vec_to_mat_fixed(&mat2, 3, false); + let ratio = I32F32::from_num(0); + let target = vec_to_mat_fixed(&target, 3, false); + let result = interpolate(&mat1, &mat2, ratio); + assert_mat_compare(&result, &target, I32F32::from_num(0)); + + let target: Vec = vec![9.1, 91., 910., 9100., 91000., 910000.]; + let ratio = I32F32::from_num(0.9); + let target = vec_to_mat_fixed(&target, 3, false); + let result = interpolate(&mat1, &mat2, ratio); + assert_mat_compare(&result, &target, I32F32::from_num(0.0001)); + + let mat1: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let mat2: Vec = vec![1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]; + let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let mat1 = vec_to_mat_fixed(&mat1, 4, false); + let mat2 = vec_to_mat_fixed(&mat2, 4, false); + let ratio = I32F32::from_num(0); + let target = vec_to_mat_fixed(&target, 4, false); + let result = interpolate(&mat1, &mat2, ratio); + assert_mat_compare(&result, &target, I32F32::from_num(0)); + + let target: Vec = vec![ + 0.000000001, + 0.000000001, + 0.000000001, + 0.000000001, + 0.000000001, + 0.000000001, + 0.000000001, + 0.000000001, + 0.000000001, + 0.000000001, + 0.000000001, + 0.000000001, + ]; + let ratio = I32F32::from_num(0.000000001); + let target = vec_to_mat_fixed(&target, 4, false); + let result = interpolate(&mat1, &mat2, ratio); + assert_mat_compare(&result, &target, I32F32::from_num(0)); + + let target: Vec = vec![0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]; + let ratio = I32F32::from_num(0.5); + let target = vec_to_mat_fixed(&target, 4, false); + let result = interpolate(&mat1, &mat2, ratio); + assert_mat_compare(&result, &target, I32F32::from_num(0)); + + let target: Vec = vec![ + 0.999_999_9, + 0.999_999_9, + 0.999_999_9, + 0.999_999_9, + 0.999_999_9, + 0.999_999_9, + 0.999_999_9, + 0.999_999_9, + 0.999_999_9, + 0.999_999_9, + 0.999_999_9, + 0.999_999_9, + ]; + let ratio = I32F32::from_num(0.9999998808); + let target = vec_to_mat_fixed(&target, 4, false); + let result = interpolate(&mat1, &mat2, ratio); + assert_mat_compare(&result, &target, I32F32::from_num(0)); + + let target: Vec = vec![1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]; + let ratio = I32F32::from_num(1); + let target = vec_to_mat_fixed(&target, 4, false); + let result = interpolate(&mat1, &mat2, ratio); + assert_mat_compare(&result, &target, I32F32::from_num(0)); +} + +#[test] +fn test_math_interpolate_sparse() { + let mat1: Vec> = vec![vec![]]; + let mat2: Vec> = vec![vec![]]; + let target: Vec> = vec![vec![]]; + let ratio = I32F32::from_num(0); + let result = interpolate_sparse(&mat1, &mat2, 0, ratio); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); + + let mat1: Vec = vec![0.]; + let mat2: Vec = vec![1.]; + let target: Vec = vec![0.]; + let mat1 = vec_to_sparse_mat_fixed(&mat1, 1, false); + let mat2 = vec_to_sparse_mat_fixed(&mat2, 1, false); + let ratio = I32F32::from_num(0); + let target = vec_to_sparse_mat_fixed(&target, 1, false); + let result = interpolate_sparse(&mat1, &mat2, 1, ratio); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); + + let target: Vec = vec![0.5]; + let ratio = I32F32::from_num(0.5); + let target = vec_to_sparse_mat_fixed(&target, 1, false); + let result = interpolate_sparse(&mat1, &mat2, 1, ratio); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); + + let target: Vec = vec![1.]; + let ratio = I32F32::from_num(1); + let target = vec_to_sparse_mat_fixed(&target, 1, false); + let result = interpolate_sparse(&mat1, &mat2, 1, ratio); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); + + let mat1: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let mat2: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let mat1 = vec_to_sparse_mat_fixed(&mat1, 4, false); + let mat2 = vec_to_sparse_mat_fixed(&mat2, 4, false); + let ratio = I32F32::from_num(0); + let target = vec_to_sparse_mat_fixed(&target, 4, false); + let result = interpolate_sparse(&mat1, &mat2, 3, ratio); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); + + let ratio = I32F32::from_num(1); + let result = interpolate_sparse(&mat1, &mat2, 3, ratio); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); + + let mat1: Vec = vec![1., 0., 100., 1000., 10000., 100000.]; + let mat2: Vec = vec![10., 100., 1000., 10000., 100000., 0.]; + let target: Vec = vec![1., 0., 100., 1000., 10000., 100000.]; + let mat1 = vec_to_sparse_mat_fixed(&mat1, 3, false); + let mat2 = vec_to_sparse_mat_fixed(&mat2, 3, false); + let ratio = I32F32::from_num(0); + let target = vec_to_sparse_mat_fixed(&target, 3, false); + let result = interpolate_sparse(&mat1, &mat2, 2, ratio); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); + + let target: Vec = vec![9.1, 90., 910., 9100., 91000., 10000.]; + let ratio = I32F32::from_num(0.9); + let target = vec_to_sparse_mat_fixed(&target, 3, false); + let result = interpolate_sparse(&mat1, &mat2, 2, ratio); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(0.0001)); + + let mat1: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let mat2: Vec = vec![1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]; + let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let mat1 = vec_to_sparse_mat_fixed(&mat1, 4, false); + let mat2 = vec_to_sparse_mat_fixed(&mat2, 4, false); + let ratio = I32F32::from_num(0); + let target = vec_to_sparse_mat_fixed(&target, 4, false); + let result = interpolate_sparse(&mat1, &mat2, 3, ratio); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); + + let target: Vec = vec![ + 0.000000001, + 0.000000001, + 0.000000001, + 0.000000001, + 0.000000001, + 0.000000001, + 0.000000001, + 0.000000001, + 0.000000001, + 0.000000001, + 0.000000001, + 0.000000001, + ]; + let ratio = I32F32::from_num(0.000000001); + let target = vec_to_sparse_mat_fixed(&target, 4, false); + let result = interpolate_sparse(&mat1, &mat2, 3, ratio); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); + + let target: Vec = vec![0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]; + let ratio = I32F32::from_num(0.5); + let target = vec_to_sparse_mat_fixed(&target, 4, false); + let result = interpolate_sparse(&mat1, &mat2, 3, ratio); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); + + let target: Vec = vec![ + 0.999_999_9, + 0.999_999_9, + 0.999_999_9, + 0.999_999_9, + 0.999_999_9, + 0.999_999_9, + 0.999_999_9, + 0.999_999_9, + 0.999_999_9, + 0.999_999_9, + 0.999_999_9, + 0.999_999_9, + ]; + let ratio = I32F32::from_num(0.9999998808); + let target = vec_to_sparse_mat_fixed(&target, 4, false); + let result = interpolate_sparse(&mat1, &mat2, 3, ratio); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); + + let target: Vec = vec![1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]; + let ratio = I32F32::from_num(1); + let target = vec_to_sparse_mat_fixed(&target, 4, false); + let result = interpolate_sparse(&mat1, &mat2, 3, ratio); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); +} + +#[test] +fn test_math_mat_ema_alpha() { + let old: Vec = vec![ + 0.1, 0.2, 3., 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.10, 0.11, 0.12, + ]; + let new: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; + let target: Vec = vec![ + 0.19, 0.38, 1., 0.436, 0.545, 0.6539, 0.763, 0.8719, 0.981, 1., 1., 1., + ]; + + let old = vec_to_mat_fixed(&old, 4, false); + let new = vec_to_mat_fixed(&new, 4, false); + let target = vec_to_mat_fixed(&target, 4, false); + let alphas = vec_to_mat_fixed(&[0.1; 12], 4, false); + let result = mat_ema_alpha(&new, &old, &alphas); + assert_mat_compare(&result, &target, I32F32::from_num(1e-4)); + let old: Vec = vec![ + 0.1, 0.2, 3., 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.10, 0.11, 0.12, + ]; + let new: Vec = vec![ + 10., 20., 30., 40., 50., 60., 70., 80., 90., 100., 110., 120., + ]; + let target: Vec = vec![ + 0.10, 0.2, 1., 0.0399, 0.05, 0.0599, 0.07, 0.07999, 0.09, 0.1, 0.10999, 0.11999, + ]; + let old = vec_to_mat_fixed(&old, 4, false); + let new = vec_to_mat_fixed(&new, 4, false); + let target = vec_to_mat_fixed(&target, 4, false); + let alphas = vec_to_mat_fixed(&[0.; 12], 4, false); + let result = mat_ema_alpha(&new, &old, &alphas); + assert_mat_compare(&result, &target, I32F32::from_num(1e-4)); + let old: Vec = vec![ + 0.001, 0.002, 0.003, 0.004, 0.05, 0.006, 0.007, 0.008, 0.009, 0.010, 0.011, 0.012, + ]; + let new: Vec = vec![ + 0.1, 0.2, 3., 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.10, 0.11, 0.12, + ]; + let target: Vec = vec![ + 0.10, 0.2, 1., 0.0399, 0.05, 0.0599, 0.07, 0.07999, 0.09, 0.1, 0.10999, 0.11999, + ]; + + let old = vec_to_mat_fixed(&old, 4, false); + let new = vec_to_mat_fixed(&new, 4, false); + let target = vec_to_mat_fixed(&target, 4, false); + let alphas = vec_to_mat_fixed(&[1.; 12], 4, false); + let result = mat_ema_alpha(&new, &old, &alphas); + assert_mat_compare(&result, &target, I32F32::from_num(1e-4)); +} + +#[test] +fn test_math_sparse_mat_ema_alpha() { + let old: Vec = vec![ + 0.1, 0.2, 3., 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.10, 0.11, 0.12, + ]; + let new: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; + let target: Vec = vec![ + 0.19, 0.38, 1., 0.43599, 0.545, 0.65399, 0.763, 0.87199, 0.981, 1., 1., 1., + ]; + let old = vec_to_sparse_mat_fixed(&old, 4, false); + let new = vec_to_sparse_mat_fixed(&new, 4, false); + let target = vec_to_sparse_mat_fixed(&target, 4, false); + let alphas = vec_to_mat_fixed(&[0.1; 12], 4, false); + let result = mat_ema_alpha_sparse(&new, &old, &alphas); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(1e-4)); + let old: Vec = vec![ + 0.001, 0.002, 0.003, 0.004, 0.05, 0.006, 0.007, 0.008, 0.009, 0.010, 0.011, 0.012, + ]; + let new: Vec = vec![ + 0.1, 0.2, 3., 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.10, 0.11, 0.12, + ]; + let target: Vec = vec![ + 0.0109, 0.0218, 0.30270, 0.007599, 0.05, 0.01139, 0.0133, 0.01519, 0.017, 0.01899, 0.02089, + 0.0227, + ]; + let old = vec_to_sparse_mat_fixed(&old, 4, false); + let new = vec_to_sparse_mat_fixed(&new, 4, false); + let target = vec_to_sparse_mat_fixed(&target, 4, false); + let alphas = vec_to_mat_fixed(&[0.1; 12], 4, false); + let result = mat_ema_alpha_sparse(&new, &old, &alphas); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(1e-4)); + let old: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let new: Vec = vec![ + 0.1, 0.2, 3., 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.10, 0.11, 0.12, + ]; + let target: Vec = vec![ + 0.01, 0.02, 0.3, 0.00399, 0.005, 0.00599, 0.007, 0.00799, 0.009, 0.01, 0.011, 0.01199, + ]; + let old = vec_to_sparse_mat_fixed(&old, 4, false); + let new = vec_to_sparse_mat_fixed(&new, 4, false); + let target = vec_to_sparse_mat_fixed(&target, 4, false); + let alphas = vec_to_mat_fixed(&[0.1; 12], 4, false); + let result = mat_ema_alpha_sparse(&new, &old, &alphas); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(1e-4)); + let old: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let new: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let old = vec_to_sparse_mat_fixed(&old, 4, false); + let new = vec_to_sparse_mat_fixed(&new, 4, false); + let target = vec_to_sparse_mat_fixed(&target, 4, false); + let alphas = vec_to_mat_fixed(&[0.1; 12], 4, false); + let result = mat_ema_alpha_sparse(&new, &old, &alphas); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(1e-4)); + let old: Vec = vec![1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let new: Vec = vec![0., 0., 0., 0., 2., 0., 0., 0., 0., 0., 0., 0.]; + let target: Vec = vec![0.0, 0., 0., 0., 0.2, 0., 0., 0., 0., 0., 0., 0.]; + let old = vec_to_sparse_mat_fixed(&old, 4, false); + let new = vec_to_sparse_mat_fixed(&new, 4, false); + let target = vec_to_sparse_mat_fixed(&target, 4, false); + let alphas = vec_to_mat_fixed(&[0.1; 12], 4, false); + let result = mat_ema_alpha_sparse(&new, &old, &alphas); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(1e-1)); +} + +#[test] +fn test_mat_ema_alpha_sparse_empty() { + let new: Vec> = Vec::new(); + let old: Vec> = Vec::new(); + let alpha: Vec> = Vec::new(); + let result = mat_ema_alpha_sparse(&new, &old, &alpha); + assert_eq!(result, Vec::>::new()); +} + +#[test] +fn test_mat_ema_alpha_sparse_single_element() { + let new: Vec> = vec![vec![(0, I32F32::from_num(1.0))]]; + let old: Vec> = vec![vec![(0, I32F32::from_num(2.0))]]; + let alpha = vec![vec![I32F32::from_num(0.5)]]; + let result = mat_ema_alpha_sparse(&new, &old, &alpha); + assert_eq!(result, vec![vec![(0, I32F32::from_num(1.0))]]); +} + +#[test] +fn test_mat_ema_alpha_sparse_multiple_elements() { + let new: Vec> = vec![ + vec![(0, I32F32::from_num(1.0)), (1, I32F32::from_num(2.0))], + vec![(0, I32F32::from_num(3.0)), (1, I32F32::from_num(4.0))], + ]; + let old: Vec> = vec![ + vec![(0, I32F32::from_num(5.0)), (1, I32F32::from_num(6.0))], + vec![(0, I32F32::from_num(7.0)), (1, I32F32::from_num(8.0))], + ]; + let alpha = vec![vec![I32F32::from_num(0.1), I32F32::from_num(0.2)]; 2]; + let result = mat_ema_alpha_sparse(&new, &old, &alpha); + let expected = vec![ + vec![(0, I32F32::from_num(1.0)), (1, I32F32::from_num(1.0))], + vec![(0, I32F32::from_num(1.0)), (1, I32F32::from_num(1.0))], + ]; + assert_sparse_mat_compare(&result, &expected, I32F32::from_num(0.000001)); +} + +#[test] +fn test_mat_ema_alpha_sparse_zero_alpha() { + let new: Vec> = vec![vec![(0, I32F32::from_num(1.0))]]; + let old: Vec> = vec![vec![(0, I32F32::from_num(2.0))]]; + let alpha = vec![vec![I32F32::from_num(0.1), I32F32::from_num(0.0)]]; + let result = mat_ema_alpha_sparse(&new, &old, &alpha); + assert_eq!(result, vec![vec![(0, I32F32::from_num(1.0))]]); +} + +#[test] +fn test_mat_ema_alpha_sparse_one_alpha() { + let new: Vec> = vec![vec![(0, I32F32::from_num(1.0))]]; + let old: Vec> = vec![vec![(0, I32F32::from_num(2.0))]]; + let alpha = vec![vec![I32F32::from_num(1.0), I32F32::from_num(0.0)]]; + let result = mat_ema_alpha_sparse(&new, &old, &alpha); + assert_eq!(result, vec![vec![(0, I32F32::from_num(1.0))]]); +} + +#[test] +fn test_mat_ema_alpha_sparse_mixed_alpha() { + let new: Vec> = vec![ + vec![(0, I32F32::from_num(1.0)), (1, I32F32::from_num(2.0))], + vec![(0, I32F32::from_num(3.0)), (1, I32F32::from_num(4.0))], + ]; + let old: Vec> = vec![ + vec![(0, I32F32::from_num(5.0)), (1, I32F32::from_num(6.0))], + vec![(0, I32F32::from_num(7.0)), (1, I32F32::from_num(8.0))], + ]; + let alpha = vec![vec![I32F32::from_num(0.3), I32F32::from_num(0.7)]; 2]; + let result = mat_ema_alpha_sparse(&new, &old, &alpha); + assert_sparse_mat_compare( + &result, + &[ + vec![(0, I32F32::from_num(1.0)), (1, I32F32::from_num(1.0))], + vec![(0, I32F32::from_num(1.0)), (1, I32F32::from_num(1.0))], + ], + I32F32::from_num(0.000001), + ); +} + +#[test] +fn test_mat_ema_alpha_sparse_sparse_matrix() { + let new: Vec> = vec![ + vec![(0, I32F32::from_num(1.0))], + vec![(1, I32F32::from_num(4.0))], + ]; + let old: Vec> = vec![ + vec![(0, I32F32::from_num(5.0))], + vec![(1, I32F32::from_num(8.0))], + ]; + let alpha = vec![vec![I32F32::from_num(0.5), I32F32::from_num(0.5)]; 2]; + let result = mat_ema_alpha_sparse(&new, &old, &alpha); + assert_eq!( + result, + vec![ + vec![(0, I32F32::from_num(1.0))], + vec![(1, I32F32::from_num(1.0))] + ] + ); +} + +#[test] +fn test_mat_ema_alpha_basic() { + let new = mat_to_fixed(&[vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]]); + let old = mat_to_fixed(&[vec![0.5, 1.5, 2.5], vec![3.5, 4.5, 5.5]]); + let alpha = vec![ + vec![ + I32F32::from_num(0.5), + I32F32::from_num(0.5), + I32F32::from_num(0.5), + ]; + 2 + ]; + let expected = mat_to_fixed(&[vec![0.75, 1.0, 1.0], vec![1.0, 1.0, 1.0]]); + let result = mat_ema_alpha(&new, &old, &alpha); + assert_eq!(result, expected); +} + +#[test] +fn test_mat_ema_alpha_varying_alpha() { + let new = mat_to_fixed(&[vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]]); + let old = mat_to_fixed(&[vec![0.5, 1.5, 2.5], vec![3.5, 4.5, 5.5]]); + let alpha = vec![ + vec![ + I32F32::from_num(0.2), + I32F32::from_num(0.5), + I32F32::from_num(0.8), + ]; + 2 + ]; + let expected = mat_to_fixed(&[vec![0.6, 1.0, 1.0], vec![1.0, 1.0, 1.0]]); + let result = mat_ema_alpha(&new, &old, &alpha); + assert_mat_approx_eq(&result, &expected, I32F32::from_num(1e-6)); +} + +#[test] +fn test_mat_ema_alpha_sparse_varying_alpha() { + let weights = vec![ + vec![(0, I32F32::from_num(0.1)), (1, I32F32::from_num(0.2))], + vec![(0, I32F32::from_num(0.3)), (1, I32F32::from_num(0.4))], + ]; + let bonds = vec![ + vec![(0, I32F32::from_num(0.5)), (1, I32F32::from_num(0.6))], + vec![(0, I32F32::from_num(0.7)), (1, I32F32::from_num(0.8))], + ]; + let alpha = vec![ + vec![I32F32::from_num(0.9), I32F32::from_num(0.8)], + vec![I32F32::from_num(0.5), I32F32::from_num(0.7)], + ]; + + let expected = vec![ + vec![(0, I32F32::from_num(0.14)), (1, I32F32::from_num(0.28))], + vec![ + (0, I32F32::from_num(0.499999)), + (1, I32F32::from_num(0.519999)), + ], + ]; + + let result = mat_ema_alpha_sparse(&weights, &bonds, &alpha); + // Assert the results with an epsilon for approximate equality + assert_sparse_mat_compare(&result, &expected, I32F32::from_num(1e-6)); +} + +#[test] +fn test_mat_ema_alpha_empty_matrices() { + let new: Vec> = vec![]; + let old: Vec> = vec![]; + let alpha = vec![]; + let expected: Vec> = vec![vec![]; 1]; + let result = mat_ema_alpha(&new, &old, &alpha); + assert_eq!(result, expected); +} + +#[test] +fn test_mat_ema_alpha_single_element() { + let new = mat_to_fixed(&[vec![1.0]]); + let old = mat_to_fixed(&[vec![0.5]]); + let alpha = vec![vec![I32F32::from_num(0.5)]]; + let expected = mat_to_fixed(&[vec![0.75]]); + let result = mat_ema_alpha(&new, &old, &alpha); + assert_eq!(result, expected); +} + +#[test] +fn test_mat_ema_alpha_mismatched_dimensions() { + let new = mat_to_fixed(&[vec![1.0, 2.0], vec![3.0, 4.0]]); + let old = mat_to_fixed(&[vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]]); + let alpha = vec![ + vec![ + I32F32::from_num(0.5), + I32F32::from_num(0.5), + I32F32::from_num(0.5), + ]; + 2 + ]; + let result = mat_ema_alpha(&new, &old, &alpha); + assert_eq!(result[0][0], old[0][0]) +} diff --git a/pallets/subtensor/src/tests/math/fixed_conversions.rs b/pallets/subtensor/src/tests/math/fixed_conversions.rs new file mode 100644 index 0000000000..0abd484313 --- /dev/null +++ b/pallets/subtensor/src/tests/math/fixed_conversions.rs @@ -0,0 +1,441 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::unwrap_used, + clippy::indexing_slicing +)] +//! Tests for [`crate::epoch::math::fixed_conversions`]. + +use crate::epoch::math::*; +use substrate_fixed::types::{I32F32, I64F64}; + +use super::helpers::*; +use substrate_fixed::types::{I96F32, I110F18}; + +#[test] +fn test_vec_max_upscale_to_u16() { + let vector: Vec = vec_to_fixed(&[]); + let target: Vec = vec![]; + let result: Vec = vec_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &target); + let vector: Vec = vec_to_fixed(&[0.]); + let target: Vec = vec![0]; + let result: Vec = vec_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &target); + let vector: Vec = vec_to_fixed(&[0., 0.]); + let target: Vec = vec![0, 0]; + let result: Vec = vec_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &target); + let vector: Vec = vec_to_fixed(&[0., 1.]); + let target: Vec = vec![0, 65535]; + let result: Vec = vec_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &target); + let vector: Vec = vec_to_fixed(&[0., 0.000000001]); + let target: Vec = vec![0, 65535]; + let result: Vec = vec_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &target); + let vector: Vec = vec_to_fixed(&[0., 0.000016, 1.]); + let target: Vec = vec![0, 1, 65535]; + let result: Vec = vec_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &target); + let vector: Vec = vec_to_fixed(&[0.000000001, 0.000000001]); + let target: Vec = vec![65535, 65535]; + let result: Vec = vec_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &target); + let vector: Vec = vec_to_fixed(&[ + 0.000001, 0.000006, 0.000007, 0.0001, 0.001, 0.01, 0.1, 0.2, 0.3, 0.4, + ]); + let target: Vec = vec![0, 1, 1, 16, 164, 1638, 16384, 32768, 49151, 65535]; + let result: Vec = vec_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &target); + let vector: Vec = vec![I32F32::from_num(16384)]; + let target: Vec = vec![65535]; + let result: Vec = vec_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &target); + let vector: Vec = vec![I32F32::from_num(32768)]; + let target: Vec = vec![65535]; + let result: Vec = vec_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &target); + let vector: Vec = vec![I32F32::from_num(32769)]; + let target: Vec = vec![65535]; + let result: Vec = vec_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &target); + let vector: Vec = vec![I32F32::from_num(65535)]; + let target: Vec = vec![65535]; + let result: Vec = vec_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &target); + let vector: Vec = vec![I32F32::max_value()]; + let target: Vec = vec![65535]; + let result: Vec = vec_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &target); + let vector: Vec = vec_to_fixed(&[0., 1., 65535.]); + let target: Vec = vec![0, 1, 65535]; + let result: Vec = vec_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &target); + let vector: Vec = vec_to_fixed(&[0., 0.5, 1., 1.5, 2., 32768.]); + let target: Vec = vec![0, 1, 2, 3, 4, 65535]; + let result: Vec = vec_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &target); + let vector: Vec = vec_to_fixed(&[0., 0.5, 1., 1.5, 2., 32768., 32769.]); + let target: Vec = vec![0, 1, 2, 3, 4, 65533, 65535]; + let result: Vec = vec_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &target); + let vector: Vec = vec![ + I32F32::from_num(0), + I32F32::from_num(1), + I32F32::from_num(32768), + I32F32::from_num(32769), + I32F32::max_value(), + ]; + let target: Vec = vec![0, 0, 1, 1, 65535]; + let result: Vec = vec_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &target); +} + +#[test] +fn test_vec_u16_max_upscale_to_u16() { + let vector: Vec = vec![]; + let result: Vec = vec_u16_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &vector); + let vector: Vec = vec![0]; + let result: Vec = vec_u16_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &vector); + let vector: Vec = vec![0, 0]; + let result: Vec = vec_u16_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &vector); + let vector: Vec = vec![1]; + let target: Vec = vec![65535]; + let result: Vec = vec_u16_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &target); + let vector: Vec = vec![0, 1]; + let target: Vec = vec![0, 65535]; + let result: Vec = vec_u16_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &target); + let vector: Vec = vec![65534]; + let target: Vec = vec![65535]; + let result: Vec = vec_u16_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &target); + let vector: Vec = vec![65535]; + let target: Vec = vec![65535]; + let result: Vec = vec_u16_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &target); + let vector: Vec = vec![65535, 65535]; + let target: Vec = vec![65535, 65535]; + let result: Vec = vec_u16_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &target); + let vector: Vec = vec![0, 1, 65534]; + let target: Vec = vec![0, 1, 65535]; + let result: Vec = vec_u16_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &target); + let vector: Vec = vec![0, 1, 2, 3, 4, 65533, 65535]; + let result: Vec = vec_u16_max_upscale_to_u16(&vector); + assert_vec_compare_u16(&result, &vector); +} + +#[test] +fn test_math_fixed_overflow() { + let max_32: I32F32 = I32F32::max_value(); + let max_u64: u64 = u64::MAX; + let _prod_96: I96F32 = I96F32::from_num(max_32) * I96F32::from_num(max_u64); + // let one: I96F32 = I96F32::from_num(1); + // let prod_96: I96F32 = (I96F32::from_num(max_32) + one) * I96F32::from_num(max_u64); // overflows + let _prod_110: I110F18 = I110F18::from_num(max_32) * I110F18::from_num(max_u64); + + let bonds_moving_average_val: u64 = 900_000_u64; + let bonds_moving_average: I64F64 = + I64F64::from_num(bonds_moving_average_val) / I64F64::from_num(1_000_000); + let alpha: I32F32 = I32F32::from_num(1) - I32F32::from_num(bonds_moving_average); + assert_eq!(I32F32::from_num(0.1), alpha); + + let bonds_moving_average: I64F64 = I64F64::from_num(max_32) / I64F64::from_num(max_32); + let alpha: I32F32 = I32F32::from_num(1) - I32F32::from_num(bonds_moving_average); + assert_eq!(I32F32::from_num(0), alpha); +} + +#[test] +fn test_math_u64_normalization() { + let min: u64 = 1; + let min32: u64 = 4_889_444; // 21_000_000_000_000_000 / 4_294_967_296 + let mid: u64 = 10_500_000_000_000_000; + let max: u64 = 21_000_000_000_000_000; + let min_64: I64F64 = I64F64::from_num(min); + let min32_64: I64F64 = I64F64::from_num(min32); + let mid_64: I64F64 = I64F64::from_num(mid); + let max_64: I64F64 = I64F64::from_num(max); + let max_sum: I64F64 = I64F64::from_num(max); + let min_frac: I64F64 = min_64 / max_sum; + assert_eq!(min_frac, I64F64::from_num(0.0000000000000000476)); + let min_frac_32: I32F32 = I32F32::from_num(min_frac); + assert_eq!(min_frac_32, I32F32::from_num(0)); + let min32_frac: I64F64 = min32_64 / max_sum; + assert_eq!(min32_frac, I64F64::from_num(0.00000000023283066664)); + let min32_frac_32: I32F32 = I32F32::from_num(min32_frac); + assert_eq!(min32_frac_32, I32F32::from_num(0.0000000002)); + let half: I64F64 = mid_64 / max_sum; + assert_eq!(half, I64F64::from_num(0.5)); + let half_32: I32F32 = I32F32::from_num(half); + assert_eq!(half_32, I32F32::from_num(0.5)); + let one: I64F64 = max_64 / max_sum; + assert_eq!(one, I64F64::from_num(1)); + let one_32: I32F32 = I32F32::from_num(one); + assert_eq!(one_32, I32F32::from_num(1)); +} + +#[test] +fn test_math_to_num() { + let val: I32F32 = I32F32::from_num(u16::MAX); + let res: u16 = val.to_num::(); + assert_eq!(res, u16::MAX); + let vector: Vec = vec![val; 1000]; + let target: Vec = vec![u16::MAX; 1000]; + let output: Vec = vector.iter().map(|e: &I32F32| e.to_num::()).collect(); + assert_eq!(output, target); + let output: Vec = vector + .iter() + .map(|e: &I32F32| (*e).to_num::()) + .collect(); + assert_eq!(output, target); + let val: I32F32 = I32F32::max_value(); + let res: u64 = val.to_num::(); + let vector: Vec = vec![val; 1000]; + let target: Vec = vec![res; 1000]; + let output: Vec = vector.iter().map(|e: &I32F32| e.to_num::()).collect(); + assert_eq!(output, target); + let output: Vec = vector + .iter() + .map(|e: &I32F32| (*e).to_num::()) + .collect(); + assert_eq!(output, target); + let val: I32F32 = I32F32::from_num(0); + let res: u64 = val.to_num::(); + let vector: Vec = vec![val; 1000]; + let target: Vec = vec![res; 1000]; + let output: Vec = vector.iter().map(|e: &I32F32| e.to_num::()).collect(); + assert_eq!(output, target); + let output: Vec = vector + .iter() + .map(|e: &I32F32| (*e).to_num::()) + .collect(); + assert_eq!(output, target); + let val: I96F32 = I96F32::from_num(u64::MAX); + let res: u64 = val.to_num::(); + assert_eq!(res, u64::MAX); + let vector: Vec = vec![val; 1000]; + let target: Vec = vec![u64::MAX; 1000]; + let output: Vec = vector.iter().map(|e: &I96F32| e.to_num::()).collect(); + assert_eq!(output, target); + let output: Vec = vector + .iter() + .map(|e: &I96F32| (*e).to_num::()) + .collect(); + assert_eq!(output, target); +} + +#[test] +fn test_math_vec_to_fixed() { + let vector: Vec = vec![0., 1., 2., 3.]; + let target: Vec = vec![ + I32F32::from_num(0.), + I32F32::from_num(1.), + I32F32::from_num(2.), + I32F32::from_num(3.), + ]; + let result = vec_to_fixed(&vector); + assert_vec_compare(&result, &target, I32F32::from_num(0)); +} + +// Reshape vector to matrix with specified number of rows, cast to I32F32. + +#[test] +fn test_math_vec_to_mat_fixed() { + let vector: Vec = vec![0., 1., 2., 0., 10., 100.]; + let target: Vec> = vec![ + vec![ + I32F32::from_num(0.), + I32F32::from_num(1.), + I32F32::from_num(2.), + ], + vec![ + I32F32::from_num(0.), + I32F32::from_num(10.), + I32F32::from_num(100.), + ], + ]; + let mat = vec_to_mat_fixed(&vector, 2, false); + assert_mat_compare(&mat, &target, I32F32::from_num(0)); +} + +// Reshape vector to sparse matrix with specified number of input rows, cast f32 to I32F32. + +#[test] +fn test_math_vec_to_sparse_mat_fixed() { + let vector: Vec = vec![0., 1., 2., 0., 10., 100.]; + let target: Vec> = vec![ + vec![(1_u16, I32F32::from_num(1.)), (2_u16, I32F32::from_num(2.))], + vec![ + (1_u16, I32F32::from_num(10.)), + (2_u16, I32F32::from_num(100.)), + ], + ]; + let mat = vec_to_sparse_mat_fixed(&vector, 2, false); + assert_sparse_mat_compare(&mat, &target, I32F32::from_num(0)); + let vector: Vec = vec![0., 0.]; + let target: Vec> = vec![vec![], vec![]]; + let mat = vec_to_sparse_mat_fixed(&vector, 2, false); + assert_sparse_mat_compare(&mat, &target, I32F32::from_num(0)); + let vector: Vec = vec![0., 1., 2., 0., 10., 100.]; + let target: Vec> = vec![ + vec![], + vec![ + (0_u16, I32F32::from_num(1.)), + (1_u16, I32F32::from_num(10.)), + ], + vec![ + (0_u16, I32F32::from_num(2.)), + (1_u16, I32F32::from_num(100.)), + ], + ]; + let mat = vec_to_sparse_mat_fixed(&vector, 2, true); + assert_sparse_mat_compare(&mat, &target, I32F32::from_num(0)); + let vector: Vec = vec![0., 0.]; + let target: Vec> = vec![vec![]]; + let mat = vec_to_sparse_mat_fixed(&vector, 2, true); + assert_sparse_mat_compare(&mat, &target, I32F32::from_num(0)); +} + +#[test] +fn test_math_fixed_to_u16() { + let expected = u16::MIN; + assert_eq!(fixed_to_u16(I32F32::from_num(expected)), expected); + + let expected = u16::MAX / 2; + assert_eq!(fixed_to_u16(I32F32::from_num(expected)), expected); + + let expected = u16::MAX; + assert_eq!(fixed_to_u16(I32F32::from_num(expected)), expected); +} + +#[test] +#[should_panic(expected = "overflow")] +fn test_math_fixed_to_u16_panics() { + let bad_input = I32F32::from_num(u32::MAX); + fixed_to_u16(bad_input); + + let bad_input = I32F32::from_num(-1); + fixed_to_u16(bad_input); +} + +// TODO: Investigate why `I32F32` and not `I64F64` +#[test] +fn test_math_fixed_to_u64() { + let expected = u64::MIN; + assert_eq!(fixed_to_u64(I32F32::from_num(expected)), expected); + + // let expected = u64::MAX / 2; + // assert_eq!(fixed_to_u64(I32F32::from_num(expected)), expected); + + // let expected = u64::MAX; + // assert_eq!(fixed_to_u64(I32F32::from_num(expected)), expected); +} + +#[test] +fn test_math_fixed_to_u64_saturates() { + let bad_input = I32F32::from_num(-1); + let expected = 0; + assert_eq!(fixed_to_u64(bad_input), expected); +} + +#[test] +fn test_math_fixed64_to_u64() { + let expected = u64::MIN; + let input = I64F64::from_num(expected); + assert_eq!(fixed64_to_u64(input), expected); + + let input = i64::MAX / 2; + let expected = u64::try_from(input).unwrap(); + assert_eq!(fixed64_to_u64(I64F64::from_num(input)), expected); + + let input = i64::MAX; + let expected = u64::try_from(input).unwrap(); + assert_eq!(fixed64_to_u64(I64F64::from_num(input)), expected); +} + +#[test] +fn test_math_fixed64_to_u64_saturates() { + let bad_input = I64F64::from_num(-1); + let expected = 0; + assert_eq!(fixed64_to_u64(bad_input), expected); +} + +/* @TODO: find the _true_ max, and half, input values */ +#[test] +fn test_math_fixed64_to_fixed32() { + let input = u64::MIN; + let expected = u32::try_from(input).unwrap(); + assert_eq!(fixed64_to_fixed32(I64F64::from_num(expected)), expected); + + let expected = u32::MAX / 2; + let input = u64::from(expected); + assert_eq!(fixed64_to_fixed32(I64F64::from_num(input)), expected); +} + +#[test] +fn test_math_fixed64_to_fixed32_saturates() { + let bad_input = I64F64::from_num(u32::MAX); + assert_eq!(fixed64_to_fixed32(bad_input), I32F32::max_value()); +} + +#[test] +fn test_math_u16_to_fixed() { + let input = u16::MIN; + let expected = I32F32::from_num(input); + assert_eq!(u16_to_fixed(input), expected); + + let input = u16::MAX / 2; + let expected = I32F32::from_num(input); + assert_eq!(u16_to_fixed(input), expected); + + let input = u16::MAX; + let expected = I32F32::from_num(input); + assert_eq!(u16_to_fixed(input), expected); +} + +#[test] +fn test_math_u16_proportion_to_fixed() { + let input = u16::MIN; + let expected = I32F32::from_num(input); + assert_eq!(u16_proportion_to_fixed(input), expected); +} + +#[test] +fn test_fixed_proportion_to_u16() { + let expected = u16::MIN; + let input = I32F32::from_num(expected); + assert_eq!(fixed_proportion_to_u16(input), expected); +} + +#[test] +fn test_fixed_proportion_to_u16_saturates() { + let expected = u16::MAX; + let input = I32F32::from_num(expected); + log::trace!("Testing with input: {input:?}"); // Debug output + let result = fixed_proportion_to_u16(input); + log::trace!("Testing with result: {result:?}"); // Debug output + assert_eq!(result, expected); +} + +#[test] +fn test_vec_fixed64_to_fixed32() { + let input = vec![I64F64::from_num(i32::MIN)]; + let expected = vec![I32F32::from_num(i32::MIN)]; + assert_eq!(vec_fixed64_to_fixed32(input), expected); + + let input = vec![I64F64::from_num(i32::MAX)]; + let expected = vec![I32F32::from_num(i32::MAX)]; + assert_eq!(vec_fixed64_to_fixed32(input), expected); +} + +#[test] +fn test_vec_fixed64_to_fixed32_saturates() { + let bad_input = vec![I64F64::from_num(i64::MAX)]; + assert_eq!(vec_fixed64_to_fixed32(bad_input), [I32F32::max_value()]); +} diff --git a/pallets/subtensor/src/tests/math/helpers.rs b/pallets/subtensor/src/tests/math/helpers.rs new file mode 100644 index 0000000000..4a3186bebb --- /dev/null +++ b/pallets/subtensor/src/tests/math/helpers.rs @@ -0,0 +1,158 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::unwrap_used, + clippy::indexing_slicing +)] +//! Shared fixtures and assertions for epoch math unit tests. +//! +//! Re-exported from [`super`] for callers such as `tests/epoch.rs` +//! (`assert_mat_compare`, `vec_to_fixed`, `vec_to_mat_fixed`). + +use substrate_fixed::types::{I32F32, I64F64}; + +pub(super) fn assert_float_compare(a: I32F32, b: I32F32, epsilon: I32F32) { + assert!(I32F32::abs(a - b) <= epsilon, "a({a:?}) != b({b:?})"); +} + +pub(super) fn assert_float_compare_64(a: I64F64, b: I64F64, epsilon: I64F64) { + assert!(I64F64::abs(a - b) <= epsilon, "a({a:?}) != b({b:?})"); +} + +pub(super) fn assert_vec_compare(va: &[I32F32], vb: &[I32F32], epsilon: I32F32) { + assert!(va.len() == vb.len()); + for i in 0..va.len() { + assert_float_compare(va[i], vb[i], epsilon); + } +} + +pub(super) fn assert_vec_compare_64(va: &[I64F64], vb: &[I64F64], epsilon: I64F64) { + assert!(va.len() == vb.len()); + for i in 0..va.len() { + assert_float_compare_64(va[i], vb[i], epsilon); + } +} + +pub(super) fn assert_vec_compare_u16(va: &[u16], vb: &[u16]) { + assert!(va.len() == vb.len()); + for i in 0..va.len() { + assert_eq!(va[i], vb[i]); + } +} + +pub fn assert_mat_compare(ma: &[Vec], mb: &[Vec], epsilon: I32F32) { + assert!(ma.len() == mb.len()); + for row in 0..ma.len() { + assert!(ma[row].len() == mb[row].len()); + for col in 0..ma[row].len() { + assert_float_compare(ma[row][col], mb[row][col], epsilon) + } + } +} + +pub(super) fn assert_sparse_mat_compare( + ma: &[Vec<(u16, I32F32)>], + mb: &[Vec<(u16, I32F32)>], + epsilon: I32F32, +) { + assert!(ma.len() == mb.len()); + for row in 0..ma.len() { + assert!( + ma[row].len() == mb[row].len(), + "row: {}, ma: {:?}, mb: {:?}", + row, + ma[row], + mb[row] + ); + for j in 0..ma[row].len() { + assert!(ma[row][j].0 == mb[row][j].0); // u16 + assert_float_compare(ma[row][j].1, mb[row][j].1, epsilon) // I32F32 + } + } +} + +pub fn vec_to_fixed(vector: &[f32]) -> Vec { + vector.iter().map(|x| I32F32::from_num(*x)).collect() +} + +pub(super) fn mat_to_fixed(matrix: &[Vec]) -> Vec> { + matrix.iter().map(|row| vec_to_fixed(row)).collect() +} + +pub(super) fn assert_mat_approx_eq(left: &[Vec], right: &[Vec], epsilon: I32F32) { + assert_eq!(left.len(), right.len()); + for (left_row, right_row) in left.iter().zip(right.iter()) { + assert_eq!(left_row.len(), right_row.len()); + for (left_val, right_val) in left_row.iter().zip(right_row.iter()) { + assert!( + (left_val - right_val).abs() <= epsilon, + "left: {left_val:?}, right: {right_val:?}" + ); + } + } +} + +pub fn vec_to_mat_fixed(vector: &[f32], rows: usize, transpose: bool) -> Vec> { + assert!( + vector.len() % rows == 0, + "Vector of len {:?} cannot reshape to {rows} rows.", + vector.len() + ); + let cols: usize = vector.len() / rows; + let mut mat: Vec> = vec![]; + if transpose { + for col in 0..cols { + let mut vals: Vec = vec![]; + for row in 0..rows { + vals.push(I32F32::from_num(vector[row * cols + col])); + } + mat.push(vals); + } + } else { + for row in 0..rows { + mat.push( + vector[row * cols..(row + 1) * cols] + .iter() + .map(|v| I32F32::from_num(*v)) + .collect(), + ); + } + } + mat +} + +// Reshape vector to sparse matrix with specified number of input rows, cast f32 to I32F32. +pub(super) fn vec_to_sparse_mat_fixed( + vector: &[f32], + rows: usize, + transpose: bool, +) -> Vec> { + assert!( + vector.len() % rows == 0, + "Vector of len {:?} cannot reshape to {rows} rows.", + vector.len() + ); + let cols: usize = vector.len() / rows; + let mut mat: Vec> = vec![]; + if transpose { + for col in 0..cols { + let mut row_vec: Vec<(u16, I32F32)> = vec![]; + for row in 0..rows { + if vector[row * cols + col] > 0. { + row_vec.push((row as u16, I32F32::from_num(vector[row * cols + col]))); + } + } + mat.push(row_vec); + } + } else { + for row in 0..rows { + let mut row_vec: Vec<(u16, I32F32)> = vec![]; + for col in 0..cols { + if vector[row * cols + col] > 0. { + row_vec.push((col as u16, I32F32::from_num(vector[row * cols + col]))); + } + } + mat.push(row_vec); + } + } + mat +} diff --git a/pallets/subtensor/src/tests/math/matmul_clip.rs b/pallets/subtensor/src/tests/math/matmul_clip.rs new file mode 100644 index 0000000000..93b98653ae --- /dev/null +++ b/pallets/subtensor/src/tests/math/matmul_clip.rs @@ -0,0 +1,267 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::unwrap_used, + clippy::indexing_slicing +)] +//! Tests for [`crate::epoch::math::matmul_clip`]. + +use crate::epoch::math::*; +use substrate_fixed::types::I32F32; + +use super::helpers::*; + +#[test] +fn test_math_row_hadamard() { + let vector: Vec = vec_to_fixed(&[1., 2., 3., 4.]); + let matrix: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; + let matrix = vec_to_mat_fixed(&matrix, 4, false); + let result = row_hadamard(&matrix, &vector); + let target: Vec = vec![1., 2., 3., 8., 10., 12., 21., 24., 27., 40., 44., 48.]; + let target = vec_to_mat_fixed(&target, 4, false); + assert_mat_compare(&result, &target, I32F32::from_num(0)); +} + +#[test] +fn test_math_row_hadamard_sparse() { + let vector: Vec = vec_to_fixed(&[1., 2., 3., 4.]); + let matrix: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; + let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); + let result = row_hadamard_sparse(&matrix, &vector); + let target: Vec = vec![1., 2., 3., 8., 10., 12., 21., 24., 27., 40., 44., 48.]; + let target = vec_to_sparse_mat_fixed(&target, 4, false); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); + let matrix: Vec = vec![0., 2., 3., 4., 0., 6., 7., 8., 0., 10., 11., 12.]; + let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); + let result = row_hadamard_sparse(&matrix, &vector); + let target: Vec = vec![0., 2., 3., 8., 0., 12., 21., 24., 0., 40., 44., 48.]; + let target = vec_to_sparse_mat_fixed(&target, 4, false); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); + let matrix: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); + let result = row_hadamard_sparse(&matrix, &vector); + let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let target = vec_to_sparse_mat_fixed(&target, 4, false); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); +} + +#[test] +fn test_math_matmul() { + let vector: Vec = vec_to_fixed(&[1., 2., 3., 4.]); + let matrix: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; + let matrix = vec_to_mat_fixed(&matrix, 4, false); + let result = matmul(&matrix, &vector); + let target: Vec = vec_to_fixed(&[70., 80., 90.]); + assert_vec_compare(&result, &target, I32F32::from_num(0)); +} + +#[test] +fn test_math_matmul_transpose() { + let vector: Vec = vec_to_fixed(&[1., 2., 3.]); + let matrix: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; + let matrix = vec_to_mat_fixed(&matrix, 4, false); + let result = matmul_transpose(&matrix, &vector); + let target: Vec = vec_to_fixed(&[14., 32., 50., 68.]); + assert_vec_compare(&result, &target, I32F32::from_num(0)); +} + +#[test] +fn test_math_sparse_matmul() { + let vector: Vec = vec_to_fixed(&[1., 2., 3., 4.]); + let matrix: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; + let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); + let result = matmul_sparse(&matrix, &vector, 3); + let target: Vec = vec_to_fixed(&[70., 80., 90.]); + assert_vec_compare(&result, &target, I32F32::from_num(0)); + let matrix: Vec = vec![0., 2., 3., 4., 0., 6., 7., 8., 0., 10., 11., 12.]; + let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); + let result = matmul_sparse(&matrix, &vector, 3); + let target: Vec = vec_to_fixed(&[69., 70., 63.]); + assert_vec_compare(&result, &target, I32F32::from_num(0)); + let matrix: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); + let result = matmul_sparse(&matrix, &vector, 3); + let target: Vec = vec_to_fixed(&[0., 0., 0.]); + assert_vec_compare(&result, &target, I32F32::from_num(0)); +} + +#[test] +fn test_math_sparse_matmul_transpose() { + let vector: Vec = vec_to_fixed(&[1., 2., 3.]); + let matrix: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; + let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); + let result = matmul_transpose_sparse(&matrix, &vector); + let target: Vec = vec_to_fixed(&[14., 32., 50., 68.]); + assert_vec_compare(&result, &target, I32F32::from_num(0)); + let matrix: Vec = vec![0., 2., 3., 4., 0., 6., 7., 8., 0., 10., 11., 12.]; + let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); + let result = matmul_transpose_sparse(&matrix, &vector); + let target: Vec = vec_to_fixed(&[13., 22., 23., 68.]); + assert_vec_compare(&result, &target, I32F32::from_num(0)); + let matrix: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); + let result = matmul_transpose_sparse(&matrix, &vector); + let target: Vec = vec_to_fixed(&[0., 0., 0., 0.]); + assert_vec_compare(&result, &target, I32F32::from_num(0)); +} + +#[test] +fn test_math_inplace_col_clip() { + let vector: Vec = vec_to_fixed(&[0., 5., 12.]); + let matrix: Vec = vec![0., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; + let mut matrix = vec_to_mat_fixed(&matrix, 4, false); + let target: Vec = vec![0., 2., 3., 0., 5., 6., 0., 5., 9., 0., 5., 12.]; + let target = vec_to_mat_fixed(&target, 4, false); + inplace_col_clip(&mut matrix, &vector); + assert_mat_compare(&matrix, &target, I32F32::from_num(0)); +} + +#[test] +fn test_math_col_clip_sparse() { + let vector: Vec = vec_to_fixed(&[0., 5., 12.]); + let matrix: Vec = vec![0., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; + let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); + let target: Vec = vec![0., 2., 3., 0., 5., 6., 0., 5., 9., 0., 5., 12.]; + let target = vec_to_sparse_mat_fixed(&target, 4, false); + let result = col_clip_sparse(&matrix, &vector); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); + let matrix: Vec = vec![0., 2., 3., 4., 5., 6., 0., 0., 0., 10., 11., 12.]; + let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); + let target: Vec = vec![0., 2., 3., 0., 5., 6., 0., 0., 0., 0., 5., 12.]; + let target = vec_to_sparse_mat_fixed(&target, 4, false); + let result = col_clip_sparse(&matrix, &vector); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); + let matrix: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); + let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let target = vec_to_sparse_mat_fixed(&target, 4, false); + let result = col_clip_sparse(&matrix, &vector); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(0)); +} + +#[test] +fn test_math_matmul2() { + let epsilon: I32F32 = I32F32::from_num(0.0001); + let w: Vec> = vec![vec![I32F32::from_num(1.0); 3]; 3]; + assert_vec_compare( + &matmul(&w, &[I32F32::from_num(1.0); 3]), + &[ + I32F32::from_num(3), + I32F32::from_num(3), + I32F32::from_num(3), + ], + epsilon, + ); + assert_vec_compare( + &matmul(&w, &[I32F32::from_num(2.0); 3]), + &[ + I32F32::from_num(6), + I32F32::from_num(6), + I32F32::from_num(6), + ], + epsilon, + ); + assert_vec_compare( + &matmul(&w, &[I32F32::from_num(3.0); 3]), + &[ + I32F32::from_num(9), + I32F32::from_num(9), + I32F32::from_num(9), + ], + epsilon, + ); + assert_vec_compare( + &matmul(&w, &[I32F32::from_num(-1.0); 3]), + &[ + I32F32::from_num(-3), + I32F32::from_num(-3), + I32F32::from_num(-3), + ], + epsilon, + ); + let w: Vec> = vec![vec![I32F32::from_num(-1.0); 3]; 3]; + assert_vec_compare( + &matmul(&w, &[I32F32::from_num(1.0); 3]), + &[ + I32F32::from_num(-3), + I32F32::from_num(-3), + I32F32::from_num(-3), + ], + epsilon, + ); + assert_vec_compare( + &matmul(&w, &[I32F32::from_num(2.0); 3]), + &[ + I32F32::from_num(-6), + I32F32::from_num(-6), + I32F32::from_num(-6), + ], + epsilon, + ); + assert_vec_compare( + &matmul(&w, &[I32F32::from_num(3.0); 3]), + &[ + I32F32::from_num(-9), + I32F32::from_num(-9), + I32F32::from_num(-9), + ], + epsilon, + ); + assert_vec_compare( + &matmul(&w, &[I32F32::from_num(-1.0); 3]), + &[ + I32F32::from_num(3), + I32F32::from_num(3), + I32F32::from_num(3), + ], + epsilon, + ); + let w: Vec> = vec![ + vec![I32F32::from_num(1.0); 3], + vec![I32F32::from_num(2.0); 3], + vec![I32F32::from_num(3.0); 3], + ]; + assert_vec_compare( + &matmul(&w, &[I32F32::from_num(0.0); 3]), + &[ + I32F32::from_num(0.0), + I32F32::from_num(0.0), + I32F32::from_num(0.0), + ], + epsilon, + ); + assert_vec_compare( + &matmul(&w, &[I32F32::from_num(2.0); 3]), + &[ + I32F32::from_num(12), + I32F32::from_num(12), + I32F32::from_num(12), + ], + epsilon, + ); + let w: Vec> = vec![ + vec![ + I32F32::from_num(1), + I32F32::from_num(2), + I32F32::from_num(3) + ]; + 3 + ]; + assert_vec_compare( + &matmul(&w, &[I32F32::from_num(0.0); 3]), + &[ + I32F32::from_num(0.0), + I32F32::from_num(0.0), + I32F32::from_num(0.0), + ], + epsilon, + ); + assert_vec_compare( + &matmul(&w, &[I32F32::from_num(2.0); 3]), + &[ + I32F32::from_num(6), + I32F32::from_num(12), + I32F32::from_num(18), + ], + epsilon, + ); +} diff --git a/pallets/subtensor/src/tests/math/matrix_normalize_mask.rs b/pallets/subtensor/src/tests/math/matrix_normalize_mask.rs new file mode 100644 index 0000000000..3074b0f114 --- /dev/null +++ b/pallets/subtensor/src/tests/math/matrix_normalize_mask.rs @@ -0,0 +1,487 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::unwrap_used, + clippy::indexing_slicing +)] +//! Tests for [`crate::epoch::math::matrix_normalize_mask`]. + +use crate::epoch::math::*; +use substrate_fixed::types::I32F32; + +use super::helpers::*; + +#[test] +fn test_math_inplace_row_normalize() { + let epsilon: I32F32 = I32F32::from_num(0.0001); + let vector: Vec = vec![ + 0., 1., 2., 3., 4., 0., 10., 100., 1000., 10000., 0., 0., 0., 0., 0., 1., 1., 1., 1., 1., + ]; + let mut mat = vec_to_mat_fixed(&vector, 4, false); + inplace_row_normalize(&mut mat); + let target: Vec = vec![ + 0., 0.1, 0.2, 0.3, 0.4, 0., 0.0009, 0.009, 0.09, 0.9, 0., 0., 0., 0., 0., 0.2, 0.2, 0.2, + 0.2, 0.2, + ]; + assert_mat_compare(&mat, &vec_to_mat_fixed(&target, 4, false), epsilon); +} + +#[test] +fn test_math_inplace_row_normalize_sparse() { + let epsilon: I32F32 = I32F32::from_num(0.0001); + let vector: Vec = vec![ + 0., 1., 0., 2., 0., 3., 4., 0., 1., 0., 2., 0., 3., 0., 1., 0., 0., 2., 0., 3., 4., 0., + 10., 0., 100., 1000., 0., 10000., 0., 0., 0., 0., 0., 0., 0., 1., 1., 1., 1., 1., 1., 1., + ]; + let mut mat = vec_to_sparse_mat_fixed(&vector, 6, false); + inplace_row_normalize_sparse(&mut mat); + let target: Vec = vec![ + 0., 0.1, 0., 0.2, 0., 0.3, 0.4, 0., 0.166666, 0., 0.333333, 0., 0.5, 0., 0.1, 0., 0., 0.2, + 0., 0.3, 0.4, 0., 0.0009, 0., 0.009, 0.09, 0., 0.9, 0., 0., 0., 0., 0., 0., 0., 0.142857, + 0.142857, 0.142857, 0.142857, 0.142857, 0.142857, 0.142857, + ]; + assert_sparse_mat_compare(&mat, &vec_to_sparse_mat_fixed(&target, 6, false), epsilon); + let vector: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let mut mat = vec_to_sparse_mat_fixed(&vector, 3, false); + inplace_row_normalize_sparse(&mut mat); + assert_sparse_mat_compare( + &mat, + &vec_to_sparse_mat_fixed(&target, 3, false), + I32F32::from_num(0), + ); +} + +#[test] +fn test_math_inplace_col_normalize() { + let epsilon: I32F32 = I32F32::from_num(0.0001); + let vector: Vec = vec![ + 0., 1., 2., 3., 4., 0., 10., 100., 1000., 10000., 0., 0., 0., 0., 0., 1., 1., 1., 1., 1., + ]; + let mut mat = vec_to_mat_fixed(&vector, 4, true); + inplace_col_normalize(&mut mat); + let target: Vec = vec![ + 0., 0.1, 0.2, 0.3, 0.4, 0., 0.0009, 0.009, 0.09, 0.9, 0., 0., 0., 0., 0., 0.2, 0.2, 0.2, + 0.2, 0.2, + ]; + assert_mat_compare(&mat, &vec_to_mat_fixed(&target, 4, true), epsilon); +} + +#[test] +fn test_math_inplace_col_normalize_sparse() { + let epsilon: I32F32 = I32F32::from_num(0.0001); + let vector: Vec = vec![ + 0., 1., 0., 2., 0., 3., 4., 0., 1., 0., 2., 0., 3., 0., 1., 0., 0., 2., 0., 3., 4., 0., + 10., 0., 100., 1000., 0., 10000., 0., 0., 0., 0., 0., 0., 0., 1., 1., 1., 1., 1., 1., 1., + ]; + let mut mat = vec_to_sparse_mat_fixed(&vector, 6, true); + inplace_col_normalize_sparse(&mut mat, 6); + let target: Vec = vec![ + 0., 0.1, 0., 0.2, 0., 0.3, 0.4, 0., 0.166666, 0., 0.333333, 0., 0.5, 0., 0.1, 0., 0., 0.2, + 0., 0.3, 0.4, 0., 0.0009, 0., 0.009, 0.09, 0., 0.9, 0., 0., 0., 0., 0., 0., 0., 0.142857, + 0.142857, 0.142857, 0.142857, 0.142857, 0.142857, 0.142857, + ]; + assert_sparse_mat_compare(&mat, &vec_to_sparse_mat_fixed(&target, 6, true), epsilon); + let vector: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let mut mat = vec_to_sparse_mat_fixed(&vector, 3, false); + inplace_col_normalize_sparse(&mut mat, 6); + assert_sparse_mat_compare( + &mat, + &vec_to_sparse_mat_fixed(&target, 3, false), + I32F32::from_num(0), + ); + let mut mat: Vec> = vec![]; + let target: Vec> = vec![]; + inplace_col_normalize_sparse(&mut mat, 0); + assert_sparse_mat_compare(&mat, &target, epsilon); +} + +#[test] +fn test_math_inplace_col_max_upscale() { + let mut mat: Vec> = vec![vec![]]; + let target: Vec> = vec![vec![]]; + inplace_col_max_upscale(&mut mat); + assert_eq!(&mat, &target); + let mut mat: Vec> = vec![vec![I32F32::from_num(0)]]; + let target: Vec> = vec![vec![I32F32::from_num(0)]]; + inplace_col_max_upscale(&mut mat); + assert_eq!(&mat, &target); + let epsilon: I32F32 = I32F32::from_num(0.0001); + let vector: Vec = vec![ + 0., 1., 2., 3., 4., 0., 10., 100., 1000., 10000., 0., 0., 0., 0., 0., 1., 1., 1., 1., 1., + ]; + let mut mat: Vec> = vec_to_mat_fixed(&vector, 4, true); + inplace_col_max_upscale(&mut mat); + let target: Vec = vec![ + 0., 0.25, 0.5, 0.75, 1., 0., 0.001, 0.01, 0.1, 1., 0., 0., 0., 0., 0., 1., 1., 1., 1., 1., + ]; + assert_mat_compare(&mat, &vec_to_mat_fixed(&target, 4, true), epsilon); +} + +#[test] +fn test_math_inplace_col_max_upscale_sparse() { + let mut mat: Vec> = vec![vec![]]; + let target: Vec> = vec![vec![]]; + inplace_col_max_upscale_sparse(&mut mat, 0); + assert_eq!(&mat, &target); + let mut mat: Vec> = vec![vec![(0, I32F32::from_num(0))]]; + let target: Vec> = vec![vec![(0, I32F32::from_num(0))]]; + inplace_col_max_upscale_sparse(&mut mat, 1); + assert_eq!(&mat, &target); + let epsilon: I32F32 = I32F32::from_num(0.0001); + let vector: Vec = vec![ + 0., 1., 0., 2., 0., 3., 4., 0., 1., 0., 2., 0., 3., 0., 1., 0., 0., 2., 0., 3., 4., 0., + 10., 0., 100., 1000., 0., 10000., 0., 0., 0., 0., 0., 0., 0., 1., 1., 1., 1., 1., 1., 1., + ]; + let mut mat = vec_to_sparse_mat_fixed(&vector, 6, true); + inplace_col_max_upscale_sparse(&mut mat, 6); + let target: Vec = vec![ + 0., 0.25, 0., 0.5, 0., 0.75, 1., 0., 0.333333, 0., 0.666666, 0., 1., 0., 0.25, 0., 0., 0.5, + 0., 0.75, 1., 0., 0.001, 0., 0.01, 0.1, 0., 1., 0., 0., 0., 0., 0., 0., 0., 1., 1., 1., 1., + 1., 1., 1., + ]; + assert_sparse_mat_compare(&mat, &vec_to_sparse_mat_fixed(&target, 6, true), epsilon); + let vector: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let mut mat = vec_to_sparse_mat_fixed(&vector, 3, false); + inplace_col_max_upscale_sparse(&mut mat, 6); + assert_sparse_mat_compare( + &mat, + &vec_to_sparse_mat_fixed(&target, 3, false), + I32F32::from_num(0), + ); + let mut mat: Vec> = vec![]; + let target: Vec> = vec![]; + inplace_col_max_upscale_sparse(&mut mat, 0); + assert_sparse_mat_compare(&mat, &target, epsilon); +} + +#[test] +fn test_math_inplace_mask_vector() { + let mask: Vec = vec![false, false, false]; + let mut vector: Vec = vec_to_fixed(&[0., 1., 2.]); + let target: Vec = vec_to_fixed(&[0., 1., 2.]); + inplace_mask_vector(&mask, &mut vector); + assert_vec_compare(&vector, &target, I32F32::from_num(0)); + let mask: Vec = vec![false, true, false]; + let mut vector: Vec = vec_to_fixed(&[0., 1., 2.]); + let target: Vec = vec_to_fixed(&[0., 0., 2.]); + inplace_mask_vector(&mask, &mut vector); + assert_vec_compare(&vector, &target, I32F32::from_num(0)); + let mask: Vec = vec![true, true, true]; + let mut vector: Vec = vec_to_fixed(&[0., 1., 2.]); + let target: Vec = vec_to_fixed(&[0., 0., 0.]); + inplace_mask_vector(&mask, &mut vector); + assert_vec_compare(&vector, &target, I32F32::from_num(0)); +} + +#[test] +fn test_math_inplace_mask_matrix() { + let mask: Vec> = vec![ + vec![false, false, false], + vec![false, false, false], + vec![false, false, false], + ]; + let vector: Vec = vec![0., 1., 2., 3., 4., 5., 6., 7., 8.]; + let mut mat = vec_to_mat_fixed(&vector, 3, false); + inplace_mask_matrix(&mask, &mut mat); + assert_mat_compare( + &mat, + &vec_to_mat_fixed(&vector, 3, false), + I32F32::from_num(0), + ); + let mask: Vec> = vec![ + vec![true, false, false], + vec![false, true, false], + vec![false, false, true], + ]; + let target: Vec = vec![0., 1., 2., 3., 0., 5., 6., 7., 0.]; + let mut mat = vec_to_mat_fixed(&vector, 3, false); + inplace_mask_matrix(&mask, &mut mat); + assert_mat_compare( + &mat, + &vec_to_mat_fixed(&target, 3, false), + I32F32::from_num(0), + ); + let mask: Vec> = vec![ + vec![true, true, true], + vec![true, true, true], + vec![true, true, true], + ]; + let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let mut mat = vec_to_mat_fixed(&vector, 3, false); + inplace_mask_matrix(&mask, &mut mat); + assert_mat_compare( + &mat, + &vec_to_mat_fixed(&target, 3, false), + I32F32::from_num(0), + ); +} + +#[test] +fn test_math_inplace_mask_rows() { + let input: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9.]; + let mask: Vec = vec![false, false, false]; + let target: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9.]; + let mut mat = vec_to_mat_fixed(&input, 3, false); + inplace_mask_rows(&mask, &mut mat); + assert_mat_compare( + &mat, + &vec_to_mat_fixed(&target, 3, false), + I32F32::from_num(0), + ); + let mask: Vec = vec![true, true, true]; + let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let mut mat = vec_to_mat_fixed(&input, 3, false); + inplace_mask_rows(&mask, &mut mat); + assert_mat_compare( + &mat, + &vec_to_mat_fixed(&target, 3, false), + I32F32::from_num(0), + ); + let mask: Vec = vec![true, false, true]; + let target: Vec = vec![0., 0., 0., 4., 5., 6., 0., 0., 0.]; + let mut mat = vec_to_mat_fixed(&input, 3, false); + inplace_mask_rows(&mask, &mut mat); + assert_mat_compare( + &mat, + &vec_to_mat_fixed(&target, 3, false), + I32F32::from_num(0), + ); + let input: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let mut mat = vec_to_mat_fixed(&input, 3, false); + let mask: Vec = vec![false, false, false]; + let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; + inplace_mask_rows(&mask, &mut mat); + assert_mat_compare( + &mat, + &vec_to_mat_fixed(&target, 3, false), + I32F32::from_num(0), + ); +} + +#[test] +fn test_math_inplace_mask_diag() { + let vector: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9.]; + let target: Vec = vec![0., 2., 3., 4., 0., 6., 7., 8., 0.]; + let mut mat = vec_to_mat_fixed(&vector, 3, false); + inplace_mask_diag(&mut mat); + assert_mat_compare( + &mat, + &vec_to_mat_fixed(&target, 3, false), + I32F32::from_num(0), + ); +} + +#[test] +fn test_math_inplace_mask_diag_except_index() { + let vector: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9.]; + let rows = 3; + + for i in 0..rows { + let mut target: Vec = vec![0., 2., 3., 4., 0., 6., 7., 8., 0.]; + let row = i * rows; + let col = i; + target[row + col] = vector[row + col]; + + let mut mat = vec_to_mat_fixed(&vector, rows, false); + inplace_mask_diag_except_index(&mut mat, i as u16); + assert_mat_compare( + &mat, + &vec_to_mat_fixed(&target, rows, false), + I32F32::from_num(0), + ); + } +} + +#[test] +fn test_math_mask_rows_sparse() { + let input: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9.]; + let mat = vec_to_sparse_mat_fixed(&input, 3, false); + let mask: Vec = vec![false, false, false]; + let target: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9.]; + let result = mask_rows_sparse(&mask, &mat); + assert_sparse_mat_compare( + &result, + &vec_to_sparse_mat_fixed(&target, 3, false), + I32F32::from_num(0), + ); + let mask: Vec = vec![true, true, true]; + let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let result = mask_rows_sparse(&mask, &mat); + assert_sparse_mat_compare( + &result, + &vec_to_sparse_mat_fixed(&target, 3, false), + I32F32::from_num(0), + ); + let mask: Vec = vec![true, false, true]; + let target: Vec = vec![0., 0., 0., 4., 5., 6., 0., 0., 0.]; + let result = mask_rows_sparse(&mask, &mat); + assert_sparse_mat_compare( + &result, + &vec_to_sparse_mat_fixed(&target, 3, false), + I32F32::from_num(0), + ); + let input: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let mat = vec_to_sparse_mat_fixed(&input, 3, false); + let mask: Vec = vec![false, false, false]; + let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let result = mask_rows_sparse(&mask, &mat); + assert_sparse_mat_compare( + &result, + &vec_to_sparse_mat_fixed(&target, 3, false), + I32F32::from_num(0), + ); +} + +#[test] +fn test_math_mask_diag_sparse() { + let vector: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9.]; + let target: Vec = vec![0., 2., 3., 4., 0., 6., 7., 8., 0.]; + let mat = vec_to_sparse_mat_fixed(&vector, 3, false); + let result = mask_diag_sparse(&mat); + assert_sparse_mat_compare( + &result, + &vec_to_sparse_mat_fixed(&target, 3, false), + I32F32::from_num(0), + ); + let vector: Vec = vec![1., 0., 0., 0., 5., 0., 0., 0., 9.]; + let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let mat = vec_to_sparse_mat_fixed(&vector, 3, false); + let result = mask_diag_sparse(&mat); + assert_sparse_mat_compare( + &result, + &vec_to_sparse_mat_fixed(&target, 3, false), + I32F32::from_num(0), + ); + let vector: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let mat = vec_to_sparse_mat_fixed(&vector, 3, false); + let result = mask_diag_sparse(&mat); + assert_sparse_mat_compare( + &result, + &vec_to_sparse_mat_fixed(&target, 3, false), + I32F32::from_num(0), + ); +} + +#[test] +fn test_math_mask_diag_sparse_except_index() { + let rows = 3; + + let vector: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9.]; + let mat = vec_to_sparse_mat_fixed(&vector, rows, false); + + for i in 0..rows { + let mut target: Vec = vec![0., 2., 3., 4., 0., 6., 7., 8., 0.]; + let row = i * rows; + let col = i; + target[row + col] = vector[row + col]; + + let result = mask_diag_sparse_except_index(&mat, i as u16); + let target_as_mat = vec_to_sparse_mat_fixed(&target, rows, false); + + assert_sparse_mat_compare(&result, &target_as_mat, I32F32::from_num(0)); + } + + let vector: Vec = vec![1., 0., 0., 0., 5., 0., 0., 0., 9.]; + let mat = vec_to_sparse_mat_fixed(&vector, rows, false); + + for i in 0..rows { + let mut target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let row = i * rows; + let col = i; + target[row + col] = vector[row + col]; + + let result = mask_diag_sparse_except_index(&mat, i as u16); + let target_as_mat = vec_to_sparse_mat_fixed(&target, rows, false); + assert_eq!(result.len(), target_as_mat.len()); + + assert_sparse_mat_compare(&result, &target_as_mat, I32F32::from_num(0)); + } + + for i in 0..rows { + let vector: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let mat = vec_to_sparse_mat_fixed(&vector, rows, false); + + let mut target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let row = i * rows; + let col = i; + target[row + col] = vector[row + col]; + + let result = mask_diag_sparse_except_index(&mat, i as u16); + let target_as_mat = vec_to_sparse_mat_fixed(&target, rows, false); + assert_eq!(result.len(), target_as_mat.len()); + + assert_sparse_mat_compare(&result, &target_as_mat, I32F32::from_num(0)); + } +} + +#[test] +fn test_math_vec_mask_sparse_matrix() { + let vector: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9.]; + let target: Vec = vec![0., 2., 3., 4., 0., 6., 7., 8., 0.]; + let mat = vec_to_sparse_mat_fixed(&vector, 3, false); + let first_vector: Vec = vec![1, 2, 3]; + let second_vector: Vec = vec![1, 2, 3]; + let result = vec_mask_sparse_matrix(&mat, &first_vector, &second_vector, &|a, b| a == b); + assert_sparse_mat_compare( + &result, + &vec_to_sparse_mat_fixed(&target, 3, false), + I32F32::from_num(0), + ); + let target: Vec = vec![1., 0., 0., 4., 5., 0., 7., 8., 9.]; + let mat = vec_to_sparse_mat_fixed(&vector, 3, false); + let first_vector: Vec = vec![1, 2, 3]; + let second_vector: Vec = vec![1, 2, 3]; + let result = vec_mask_sparse_matrix(&mat, &first_vector, &second_vector, &|a, b| a < b); + assert_sparse_mat_compare( + &result, + &vec_to_sparse_mat_fixed(&target, 3, false), + I32F32::from_num(0), + ); + let vector: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let target: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let mat = vec_to_sparse_mat_fixed(&vector, 3, false); + let first_vector: Vec = vec![1, 2, 3]; + let second_vector: Vec = vec![1, 2, 3]; + let result = vec_mask_sparse_matrix(&mat, &first_vector, &second_vector, &|a, b| a == b); + assert_sparse_mat_compare( + &result, + &vec_to_sparse_mat_fixed(&target, 3, false), + I32F32::from_num(0), + ); +} + +#[test] +fn test_math_row_sum() { + let matrix: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; + let matrix = vec_to_mat_fixed(&matrix, 4, false); + let result = row_sum(&matrix); + let target: Vec = vec_to_fixed(&[6., 15., 24., 33.]); + assert_vec_compare(&result, &target, I32F32::from_num(0)); +} + +#[test] +fn test_math_row_sum_sparse() { + let matrix: Vec = vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]; + let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); + let result = row_sum_sparse(&matrix); + let target: Vec = vec_to_fixed(&[6., 15., 24., 33.]); + assert_vec_compare(&result, &target, I32F32::from_num(0)); + let matrix: Vec = vec![0., 2., 3., 4., 0., 6., 7., 8., 0., 10., 11., 12.]; + let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); + let result = row_sum_sparse(&matrix); + let target: Vec = vec_to_fixed(&[5., 10., 15., 33.]); + assert_vec_compare(&result, &target, I32F32::from_num(0)); + let matrix: Vec = vec![1., 2., 3., 0., 0., 0., 7., 8., 9., 10., 11., 12.]; + let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); + let result = row_sum_sparse(&matrix); + let target: Vec = vec_to_fixed(&[6., 0., 24., 33.]); + assert_vec_compare(&result, &target, I32F32::from_num(0)); + let matrix: Vec = vec![0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + let matrix = vec_to_sparse_mat_fixed(&matrix, 4, false); + let result = row_sum_sparse(&matrix); + let target: Vec = vec_to_fixed(&[0., 0., 0., 0.]); + assert_vec_compare(&result, &target, I32F32::from_num(0)); +} diff --git a/pallets/subtensor/src/tests/math/mod.rs b/pallets/subtensor/src/tests/math/mod.rs new file mode 100644 index 0000000000..c0bd3b84d5 --- /dev/null +++ b/pallets/subtensor/src/tests/math/mod.rs @@ -0,0 +1,30 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::unwrap_used, + clippy::indexing_slicing +)] +//! Unit tests for [`crate::epoch::math`] fixed-point helpers. +//! +//! Layout mirrors `epoch/math/` so each concept module has a matching test file. +//! +//! ## Search anchors +//! +//! | Module | Owns | +//! |--------|------| +//! | [`helpers`] | assert/compare fixtures; `vec_to_fixed` / `vec_to_mat_fixed` | +//! | [`fixed_conversions`] | u16 ↔ fixed proportions, max-upscale, overflow | +//! | [`vector_ops`] | normalize, top-k, exp/sigmoid, elementwise div | +//! | [`matrix_normalize_mask`] | row/col normalize & boolean masks (dense/sparse) | +//! | [`matmul_clip`] | matmul, Hadamard, column clip | +//! | [`weighted_median`] | stake-weighted median consensus | +//! | [`ema_interpolate`] | bonds EMA, interpolate, vec/mat-vector mul | + +mod ema_interpolate; +mod fixed_conversions; +mod helpers; +mod matmul_clip; +mod matrix_normalize_mask; +mod vector_ops; +mod weighted_median; + +pub use helpers::{assert_mat_compare, vec_to_fixed, vec_to_mat_fixed}; diff --git a/pallets/subtensor/src/tests/math/vector_ops.rs b/pallets/subtensor/src/tests/math/vector_ops.rs new file mode 100644 index 0000000000..e8701c10c3 --- /dev/null +++ b/pallets/subtensor/src/tests/math/vector_ops.rs @@ -0,0 +1,362 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::unwrap_used, + clippy::indexing_slicing +)] +//! Tests for [`crate::epoch::math::vector_ops`]. + +use crate::epoch::math::*; +use substrate_fixed::{ + transcendental::exp, + types::{I32F32, I64F64}, +}; + +use super::helpers::*; + +#[test] +fn test_check_vec_max_limited() { + let vector: Vec = vec![]; + let max_limit: u16 = 0; + assert!(check_vec_max_limited(&vector, max_limit)); + let vector: Vec = vec![]; + let max_limit: u16 = u16::MAX; + assert!(check_vec_max_limited(&vector, max_limit)); + let vector: Vec = vec![u16::MAX]; + let max_limit: u16 = u16::MAX; + assert!(check_vec_max_limited(&vector, max_limit)); + let vector: Vec = vec![u16::MAX]; + let max_limit: u16 = u16::MAX - 1; + assert!(!check_vec_max_limited(&vector, max_limit)); + let vector: Vec = vec![u16::MAX]; + let max_limit: u16 = 0; + assert!(!check_vec_max_limited(&vector, max_limit)); + let vector: Vec = vec![0]; + let max_limit: u16 = u16::MAX; + assert!(check_vec_max_limited(&vector, max_limit)); + let vector: Vec = vec![0, u16::MAX]; + let max_limit: u16 = u16::MAX; + assert!(check_vec_max_limited(&vector, max_limit)); + let vector: Vec = vec![0, u16::MAX, u16::MAX]; + let max_limit: u16 = u16::MAX / 2; + assert!(!check_vec_max_limited(&vector, max_limit)); + let vector: Vec = vec![0, u16::MAX, u16::MAX]; + let max_limit: u16 = u16::MAX / 2 + 1; + assert!(check_vec_max_limited(&vector, max_limit)); + let vector: Vec = vec![0, u16::MAX, u16::MAX, u16::MAX]; + let max_limit: u16 = u16::MAX / 3 - 1; + assert!(!check_vec_max_limited(&vector, max_limit)); + let vector: Vec = vec![0, u16::MAX, u16::MAX, u16::MAX]; + let max_limit: u16 = u16::MAX / 3; + assert!(check_vec_max_limited(&vector, max_limit)); +} + +#[test] +fn test_math_exp_safe() { + let zero: I32F32 = I32F32::from_num(0); + let one: I32F32 = I32F32::from_num(1); + let target: I32F32 = exp(zero).unwrap(); + assert_eq!(exp_safe(zero), target); + let target: I32F32 = exp(one).unwrap(); + assert_eq!(exp_safe(one), target); + let min_input: I32F32 = I32F32::from_num(-20); // <= 1/exp(-20) = 485 165 195,4097903 + let max_input: I32F32 = I32F32::from_num(20); // <= exp(20) = 485 165 195,4097903 + let target: I32F32 = exp(min_input).unwrap(); + assert_eq!(exp_safe(min_input), target); + assert_eq!(exp_safe(min_input - one), target); + assert_eq!(exp_safe(I32F32::min_value()), target); + let target: I32F32 = exp(max_input).unwrap(); + assert_eq!(exp_safe(max_input), target); + assert_eq!(exp_safe(max_input + one), target); + assert_eq!(exp_safe(I32F32::max_value()), target); +} + +#[test] +fn test_math_sigmoid_safe() { + let trust: Vec = vec![ + I32F32::min_value(), + I32F32::from_num(0), + I32F32::from_num(0.4), + I32F32::from_num(0.5), + I32F32::from_num(0.6), + I32F32::from_num(1), + I32F32::max_value(), + ]; + let consensus: Vec = trust + .iter() + .map(|t: &I32F32| sigmoid_safe(*t, I32F32::max_value(), I32F32::max_value())) + .collect(); + let target: Vec = vec_to_fixed(&[ + 0.0000000019, + 0.0000000019, + 0.0000000019, + 0.0000000019, + 0.0000000019, + 0.0000000019, + 0.5, + ]); + assert_eq!(&consensus, &target); + let consensus: Vec = trust + .iter() + .map(|t: &I32F32| sigmoid_safe(*t, I32F32::min_value(), I32F32::min_value())) + .collect(); + let target: Vec = vec_to_fixed(&[ + 0.5, + 0.0000000019, + 0.0000000019, + 0.0000000019, + 0.0000000019, + 0.0000000019, + 0.0000000019, + ]); + assert_eq!(&consensus, &target); + let consensus: Vec = trust + .iter() + .map(|t: &I32F32| sigmoid_safe(*t, I32F32::from_num(30), I32F32::from_num(0.5))) + .collect(); + let target: Vec = vec![ + 0.0000000019, + 0.0000003057, + 0.0474258729, + 0.5, + 0.952574127, + 0.9999996943, + 0.9999999981, + ]; + let target: Vec = target.iter().map(|c: &f64| I32F32::from_num(*c)).collect(); + assert_eq!(&consensus, &target); + let trust: Vec = vec_to_fixed(&[0., 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.]); + let consensus: Vec = trust + .iter() + .map(|t: &I32F32| sigmoid_safe(*t, I32F32::from_num(40), I32F32::from_num(0.5))) + .collect(); + let target: Vec = vec![ + 0.0000000019, + 0.0000001125, + 0.0000061442, + 0.0003353502, + 0.017986214, + 0.5, + 0.9820138067, + 0.9996646498, + 0.9999938558, + 0.9999998875, + 0.9999999981, + ]; + let target: Vec = target.iter().map(|c: &f64| I32F32::from_num(*c)).collect(); + assert_eq!(&consensus, &target); +} + +#[test] +fn test_math_is_topk() { + let vector: Vec = vec_to_fixed(&[]); + let result = is_topk(&vector, 5); + let target: Vec = vec![]; + assert_eq!(&result, &target); + let vector: Vec = vec_to_fixed(&[0., 1., 2., 3., 4., 5., 6., 7., 8., 9.]); + let result = is_topk(&vector, 0); + let target: Vec = vec![ + false, false, false, false, false, false, false, false, false, false, + ]; + assert_eq!(&result, &target); + let result = is_topk(&vector, 5); + let target: Vec = vec![ + false, false, false, false, false, true, true, true, true, true, + ]; + assert_eq!(&result, &target); + let result = is_topk(&vector, 10); + let target: Vec = vec![true, true, true, true, true, true, true, true, true, true]; + assert_eq!(&result, &target); + let result = is_topk(&vector, 100); + assert_eq!(&result, &target); + let vector: Vec = vec_to_fixed(&[9., 8., 7., 6., 5., 4., 3., 2., 1., 0.]); + let result = is_topk(&vector, 5); + let target: Vec = vec![ + true, true, true, true, true, false, false, false, false, false, + ]; + assert_eq!(&result, &target); + let vector: Vec = vec_to_fixed(&[9., 0., 8., 1., 7., 2., 6., 3., 5., 4.]); + let result = is_topk(&vector, 5); + let target: Vec = vec![ + true, false, true, false, true, false, true, false, true, false, + ]; + assert_eq!(&result, &target); + let vector: Vec = vec_to_fixed(&[0.9, 0., 0.8, 0.1, 0.7, 0.2, 0.6, 0.3, 0.5, 0.4]); + let result = is_topk(&vector, 5); + let target: Vec = vec![ + true, false, true, false, true, false, true, false, true, false, + ]; + assert_eq!(&result, &target); + let vector: Vec = vec_to_fixed(&[0., 1., 2., 3., 4., 5., 5., 5., 5., 6.]); + let result = is_topk(&vector, 5); + let target: Vec = vec![ + false, false, false, false, false, true, true, true, true, true, + ]; + assert_eq!(&result, &target); +} + +#[test] +fn test_math_sum() { + assert!(sum(&[]) == I32F32::from_num(0)); + assert!( + sum(&[ + I32F32::from_num(1.0), + I32F32::from_num(10.0), + I32F32::from_num(30.0) + ]) == I32F32::from_num(41) + ); + assert!( + sum(&[ + I32F32::from_num(-1.0), + I32F32::from_num(10.0), + I32F32::from_num(30.0) + ]) == I32F32::from_num(39) + ); +} + +#[test] +fn test_math_normalize() { + let epsilon: I32F32 = I32F32::from_num(0.0001); + let x: Vec = vec![]; + let y: Vec = normalize(&x); + assert_vec_compare(&x, &y, epsilon); + let x: Vec = vec![ + I32F32::from_num(1.0), + I32F32::from_num(10.0), + I32F32::from_num(30.0), + ]; + let y: Vec = normalize(&x); + assert_vec_compare( + &y, + &[ + I32F32::from_num(0.0243902437), + I32F32::from_num(0.243902439), + I32F32::from_num(0.7317073171), + ], + epsilon, + ); + assert_float_compare(sum(&y), I32F32::from_num(1.0), epsilon); + let x: Vec = vec![ + I32F32::from_num(-1.0), + I32F32::from_num(10.0), + I32F32::from_num(30.0), + ]; + let y: Vec = normalize(&x); + assert_vec_compare( + &y, + &[ + I32F32::from_num(-0.0256410255), + I32F32::from_num(0.2564102563), + I32F32::from_num(0.769230769), + ], + epsilon, + ); + assert_float_compare(sum(&y), I32F32::from_num(1.0), epsilon); +} + +#[test] +fn test_math_inplace_normalize() { + let epsilon: I32F32 = I32F32::from_num(0.0001); + let mut x1: Vec = vec![ + I32F32::from_num(1.0), + I32F32::from_num(10.0), + I32F32::from_num(30.0), + ]; + inplace_normalize(&mut x1); + assert_vec_compare( + &x1, + &[ + I32F32::from_num(0.0243902437), + I32F32::from_num(0.243902439), + I32F32::from_num(0.7317073171), + ], + epsilon, + ); + let mut x2: Vec = vec![ + I32F32::from_num(-1.0), + I32F32::from_num(10.0), + I32F32::from_num(30.0), + ]; + inplace_normalize(&mut x2); + assert_vec_compare( + &x2, + &[ + I32F32::from_num(-0.0256410255), + I32F32::from_num(0.2564102563), + I32F32::from_num(0.769230769), + ], + epsilon, + ); +} + +#[test] +fn test_math_inplace_normalize_64() { + let epsilon: I64F64 = I64F64::from_num(0.0001); + let mut x1: Vec = vec![ + I64F64::from_num(1.0), + I64F64::from_num(10.0), + I64F64::from_num(30.0), + ]; + inplace_normalize_64(&mut x1); + assert_vec_compare_64( + &x1, + &[ + I64F64::from_num(0.0243902437), + I64F64::from_num(0.243902439), + I64F64::from_num(0.7317073171), + ], + epsilon, + ); + let mut x2: Vec = vec![ + I64F64::from_num(-1.0), + I64F64::from_num(10.0), + I64F64::from_num(30.0), + ]; + inplace_normalize_64(&mut x2); + assert_vec_compare_64( + &x2, + &[ + I64F64::from_num(-0.0256410255), + I64F64::from_num(0.2564102563), + I64F64::from_num(0.769230769), + ], + epsilon, + ); +} + +#[test] +fn test_math_vecdiv() { + let x: Vec = vec_to_fixed(&[]); + let y: Vec = vec_to_fixed(&[]); + let result: Vec = vec_to_fixed(&[]); + assert_eq!(result, elementwise_safe_div(&x, &y)); + + let x: Vec = vec_to_fixed(&[0., 1., 0., 1.]); + let y: Vec = vec_to_fixed(&[0., 1., 1., 0.]); + let result: Vec = vec_to_fixed(&[0., 1., 0., 0.]); + assert_eq!(result, elementwise_safe_div(&x, &y)); + + let x: Vec = vec_to_fixed(&[1., 1., 10.]); + let y: Vec = vec_to_fixed(&[2., 3., 2.]); + let result: Vec = vec![fixed(1.) / fixed(2.), fixed(1.) / fixed(3.), fixed(5.)]; + assert_eq!(result, elementwise_safe_div(&x, &y)); +} + +#[test] +#[allow(arithmetic_overflow)] +fn test_checked_sum() { + let overflowing_input = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, u64::MAX]; + // Expect None when overflow occurs + assert_eq!(checked_sum(&overflowing_input), None); + + let normal_input = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + // Expect Some when no overflow occurs + assert_eq!(checked_sum(&normal_input), Some(55)); + + let empty_input: Vec = vec![]; + // Expect Some(u16::default()) when input is empty + assert_eq!(checked_sum(&empty_input), Some(u16::default())); + + let single_input = vec![1]; + // Expect Some(...) when input is a single value + assert_eq!(checked_sum(&single_input), Some(1)); +} diff --git a/pallets/subtensor/src/tests/math/weighted_median.rs b/pallets/subtensor/src/tests/math/weighted_median.rs new file mode 100644 index 0000000000..e9ef881a24 --- /dev/null +++ b/pallets/subtensor/src/tests/math/weighted_median.rs @@ -0,0 +1,348 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::unwrap_used, + clippy::indexing_slicing +)] +//! Tests for [`crate::epoch::math::weighted_median`]. + +use crate::epoch::math::*; +use substrate_fixed::types::I32F32; + +use super::helpers::*; +use rand::{RngExt, seq::SliceRandom}; + +#[test] +fn test_math_weighted_median() { + let mut rng = rand::rng(); + let zero: I32F32 = fixed(0.); + let one: I32F32 = fixed(1.); + for _ in 0..100 { + let stake: Vec = vec_to_fixed(&[]); + let score: Vec = vec_to_fixed(&[]); + let majority: I32F32 = fixed(0.51); + assert_eq!( + zero, + weighted_median( + &stake, + &score, + (0..stake.len()).collect::>().as_slice(), + one - majority, + zero, + stake.iter().sum() + ) + ); + + let stake: Vec = normalize(&vec_to_fixed(&[0.51])); + let score: Vec = vec_to_fixed(&[1.]); + let majority: I32F32 = fixed(0.51); + assert_eq!( + one, + weighted_median( + &stake, + &score, + (0..stake.len()).collect::>().as_slice(), + one - majority, + zero, + stake.iter().sum() + ) + ); + + let stake: Vec = vec_to_fixed(&[0.49, 0.51]); + let score: Vec = vec_to_fixed(&[0.5, 1.]); + let majority: I32F32 = fixed(0.51); + assert_eq!( + one, + weighted_median( + &stake, + &score, + (0..stake.len()).collect::>().as_slice(), + one - majority, + zero, + stake.iter().sum() + ) + ); + + let stake: Vec = vec_to_fixed(&[0.51, 0.49]); + let score: Vec = vec_to_fixed(&[0.5, 1.]); + let majority: I32F32 = fixed(0.51); + assert_eq!( + fixed(0.5), + weighted_median( + &stake, + &score, + (0..stake.len()).collect::>().as_slice(), + one - majority, + zero, + stake.iter().sum() + ) + ); + + let stake: Vec = vec_to_fixed(&[0.49, 0., 0.51]); + let score: Vec = vec_to_fixed(&[0.5, 0.7, 1.]); + let majority: I32F32 = fixed(0.51); + assert_eq!( + one, + weighted_median( + &stake, + &score, + (0..stake.len()).collect::>().as_slice(), + one - majority, + zero, + stake.iter().sum() + ) + ); + + let stake: Vec = vec_to_fixed(&[0.49, 0.01, 0.5]); + let score: Vec = vec_to_fixed(&[0.5, 0.7, 1.]); + let majority: I32F32 = fixed(0.51); + assert_eq!( + fixed(0.7), + weighted_median( + &stake, + &score, + (0..stake.len()).collect::>().as_slice(), + one - majority, + zero, + stake.iter().sum() + ) + ); + + let stake: Vec = vec_to_fixed(&[0.49, 0.51, 0.0]); + let score: Vec = vec_to_fixed(&[0.5, 0.7, 1.]); + let majority: I32F32 = fixed(0.51); + assert_eq!( + fixed(0.7), + weighted_median( + &stake, + &score, + (0..stake.len()).collect::>().as_slice(), + one - majority, + zero, + stake.iter().sum() + ) + ); + + let stake: Vec = vec_to_fixed(&[0.0, 0.49, 0.51]); + let score: Vec = vec_to_fixed(&[0.5, 0.7, 1.]); + let majority: I32F32 = fixed(0.51); + assert_eq!( + one, + weighted_median( + &stake, + &score, + (0..stake.len()).collect::>().as_slice(), + one - majority, + zero, + stake.iter().sum() + ) + ); + + let stake: Vec = vec_to_fixed(&[0.0, 0.49, 0.0, 0.51]); + let score: Vec = vec_to_fixed(&[0.5, 0.5, 1., 1.]); + let majority: I32F32 = fixed(0.51); + assert_eq!( + one, + weighted_median( + &stake, + &score, + (0..stake.len()).collect::>().as_slice(), + one - majority, + zero, + stake.iter().sum() + ) + ); + + let stake: Vec = vec_to_fixed(&[0.0, 0.49, 0.0, 0.51, 0.0]); + let score: Vec = vec_to_fixed(&[0.5, 0.5, 1., 1., 0.5]); + let majority: I32F32 = fixed(0.51); + assert_eq!( + one, + weighted_median( + &stake, + &score, + (0..stake.len()).collect::>().as_slice(), + one - majority, + zero, + stake.iter().sum() + ) + ); + + let stake: Vec = vec_to_fixed(&[0.2, 0.2, 0.2, 0.2, 0.2]); + let score: Vec = vec_to_fixed(&[0.8, 0.2, 1., 0.6, 0.4]); + let majority: I32F32 = fixed(0.51); + assert_eq!( + fixed(0.6), + weighted_median( + &stake, + &score, + (0..stake.len()).collect::>().as_slice(), + one - majority, + zero, + stake.iter().sum() + ) + ); + + let stake: Vec = vec_to_fixed(&[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]); + let score: Vec = vec_to_fixed(&[0.8, 0.8, 0.2, 0.2, 1.0, 1.0, 0.6, 0.6, 0.4, 0.4]); + let majority: I32F32 = fixed(0.51); + assert_eq!( + fixed(0.6), + weighted_median( + &stake, + &score, + (0..stake.len()).collect::>().as_slice(), + one - majority, + zero, + stake.iter().sum() + ) + ); + + let n: usize = 100; + for majority in vec_to_fixed(&[ + 0., 0.0000001, 0.25, 0.49, 0.49, 0.49, 0.5, 0.51, 0.51, 0.51, 0.9999999, 1., + ]) { + for allow_equal in [false, true] { + let mut stake: Vec = vec![]; + let mut score: Vec = vec![]; + let mut last_score: I32F32 = zero; + for i in 0..n { + if allow_equal { + match rng.random_range(0..2) { + 1 => stake.push(one), + _ => stake.push(zero), + } + if rng.random_range(0..2) == 1 { + last_score += one + } + score.push(last_score); + } else { + stake.push(one); + score.push(I32F32::from_num(i)); + } + } + inplace_normalize(&mut stake); + let total_stake: I32F32 = stake.iter().sum(); + let mut minority: I32F32 = total_stake - majority; + if minority < zero { + minority = zero; + } + let mut medians: Vec = vec![]; + let mut median_stake: I32F32 = zero; + let mut median_set = false; + let mut stake_sum: I32F32 = zero; + for i in 0..n { + stake_sum += stake[i]; + if !median_set && stake_sum >= minority { + median_stake = stake_sum; + median_set = true; + } + if median_set { + if median_stake < stake_sum { + if median_stake == minority && !medians.contains(&score[i]) { + medians.push(score[i]); + } + break; + } + if !medians.contains(&score[i]) { + medians.push(score[i]); + } + } + } + if medians.is_empty() { + medians.push(zero); + } + let stake_idx: Vec = (0..stake.len()).collect(); + let result: I32F32 = + weighted_median(&stake, &score, &stake_idx, minority, zero, total_stake); + assert!(medians.contains(&result)); + for _ in 0..10 { + let mut permuted_uids: Vec = (0..n).collect(); + permuted_uids.shuffle(&mut rng); + stake = permuted_uids.iter().map(|&i| stake[i]).collect(); + score = permuted_uids.iter().map(|&i| score[i]).collect(); + let result: I32F32 = + weighted_median(&stake, &score, &stake_idx, minority, zero, total_stake); + assert!(medians.contains(&result)); + } + } + } + } +} + +#[test] +fn test_math_weighted_median_col() { + let stake: Vec = vec_to_fixed(&[]); + let weights: Vec> = vec![vec![]]; + let median: Vec = vec_to_fixed(&[]); + assert_eq!(median, weighted_median_col(&stake, &weights, fixed(0.5))); + + let stake: Vec = vec_to_fixed(&[0., 0.]); + let weights: Vec = vec![0., 0., 0., 0.]; + let weights: Vec> = vec_to_mat_fixed(&weights, 2, false); + let median: Vec = vec_to_fixed(&[0., 0.]); + assert_eq!(median, weighted_median_col(&stake, &weights, fixed(0.5))); + + let stake: Vec = vec_to_fixed(&[0., 0.75, 0.25, 0.]); + let weights: Vec = vec![0., 0.1, 0., 0., 0.2, 0.4, 0., 0.3, 0.1, 0., 0.4, 0.5]; + let weights: Vec> = vec_to_mat_fixed(&weights, 4, false); + let median: Vec = vec_to_fixed(&[0., 0.3, 0.4]); + assert_eq!(median, weighted_median_col(&stake, &weights, fixed(0.24))); + let median: Vec = vec_to_fixed(&[0., 0.2, 0.4]); + assert_eq!(median, weighted_median_col(&stake, &weights, fixed(0.26))); + let median: Vec = vec_to_fixed(&[0., 0.2, 0.1]); + assert_eq!(median, weighted_median_col(&stake, &weights, fixed(0.76))); + + let stake: Vec = vec_to_fixed(&[0., 0.3, 0.2, 0.5]); + let weights: Vec = vec![0., 0.1, 0., 0., 0.2, 0.4, 0., 0.3, 0.1, 0., 0., 0.5]; + let weights: Vec> = vec_to_mat_fixed(&weights, 4, false); + let median: Vec = vec_to_fixed(&[0., 0., 0.4]); + assert_eq!(median, weighted_median_col(&stake, &weights, fixed(0.51))); +} + +#[test] +fn test_math_weighted_median_col_sparse() { + let stake: Vec = vec_to_fixed(&[]); + let weights: Vec> = vec![vec![]]; + let median: Vec = vec_to_fixed(&[]); + assert_eq!( + median, + weighted_median_col_sparse(&stake, &weights, 0, fixed(0.5)) + ); + + let stake: Vec = vec_to_fixed(&[0., 0.]); + let weights: Vec = vec![0., 0., 0., 0.]; + let weights: Vec> = vec_to_sparse_mat_fixed(&weights, 2, false); + let median: Vec = vec_to_fixed(&[0., 0.]); + assert_eq!( + median, + weighted_median_col_sparse(&stake, &weights, 2, fixed(0.5)) + ); + + let stake: Vec = vec_to_fixed(&[0., 0.75, 0.25, 0.]); + let weights: Vec = vec![0., 0.1, 0., 0., 0.2, 0.4, 0., 0.3, 0.1, 0., 0.4, 0.5]; + let weights: Vec> = vec_to_sparse_mat_fixed(&weights, 4, false); + let median: Vec = vec_to_fixed(&[0., 0.3, 0.4]); + assert_eq!( + median, + weighted_median_col_sparse(&stake, &weights, 3, fixed(0.24)) + ); + let median: Vec = vec_to_fixed(&[0., 0.2, 0.4]); + assert_eq!( + median, + weighted_median_col_sparse(&stake, &weights, 3, fixed(0.26)) + ); + let median: Vec = vec_to_fixed(&[0., 0.2, 0.1]); + assert_eq!( + median, + weighted_median_col_sparse(&stake, &weights, 3, fixed(0.76)) + ); + + let stake: Vec = vec_to_fixed(&[0., 0.3, 0.2, 0.5]); + let weights: Vec = vec![0., 0.1, 0., 0., 0.2, 0.4, 0., 0.3, 0.1, 0., 0., 0.5]; + let weights: Vec> = vec_to_sparse_mat_fixed(&weights, 4, false); + let median: Vec = vec_to_fixed(&[0., 0., 0.4]); + assert_eq!( + median, + weighted_median_col_sparse(&stake, &weights, 3, fixed(0.51)) + ); +} diff --git a/pallets/subtensor/src/tests/mechanism.rs b/pallets/subtensor/src/tests/mechanism.rs index 764e67617e..3b7872bf1f 100644 --- a/pallets/subtensor/src/tests/mechanism.rs +++ b/pallets/subtensor/src/tests/mechanism.rs @@ -1,3 +1,7 @@ +//! Tests for multi-mechanism subnet state ([`crate::subnets::mechanism`]). +//! +//! Covers mechanism count, per-mech weights/bonds, and emission routing. + #![allow( clippy::arithmetic_side_effects, clippy::expect_used, @@ -1396,7 +1400,7 @@ fn test_reveal_crv3_commits_sub_success() { // Verify weights applied under the selected mecid index let idx = SubtensorModule::get_mechanism_storage_index(netuid, mecid); - let weights_sparse = SubtensorModule::get_weights_sparse(idx); + let weights_sparse = SubtensorModule::unnormalized_weights_sparse(idx); let row = weights_sparse.get(uid1 as usize).cloned().unwrap_or_default(); assert!(!row.is_empty(), "expected weights set for validator uid1 under mecid"); diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs deleted file mode 100644 index c9d8b51b9b..0000000000 --- a/pallets/subtensor/src/tests/migration.rs +++ /dev/null @@ -1,5167 +0,0 @@ -#![allow( - unused, - clippy::expect_used, - clippy::indexing_slicing, - clippy::panic, - clippy::unwrap_used -)] - -use super::mock::*; -use crate::staking::lock::LockState; -use crate::*; -use alloc::collections::BTreeMap; -use approx::{assert_abs_diff_eq, assert_relative_eq}; -use codec::{Decode, Encode}; -use frame_support::{ - StorageHasher, Twox64Concat, assert_ok, - storage::unhashed::{get, get_raw, put, put_raw}, - storage_alias, - traits::{Currency, StorageInstance, StoredMap, fungible::Inspect}, - weights::Weight, -}; -use safe_math::SafeDiv; - -use crate::migrations::migrate_coldkey_swap_scheduled_to_announcements::deprecated as coldkey_swap_deprecated; -use crate::migrations::migrate_storage; -use frame_support::traits::Bounded; -use frame_system::Config; -use pallet_drand::types::RoundNumber; -use pallet_scheduler::ScheduledOf; -use scale_info::prelude::collections::VecDeque; -use sp_core::{H160, H256, U256, crypto::Ss58Codec}; -use sp_io::hashing::twox_128; -use sp_runtime::{ - AccountId32, PerU16, - traits::{Hash, Zero}, -}; -use sp_std::marker::PhantomData; -use substrate_fixed::types::{I96F32, U64F64}; -use substrate_fixed::{traits::ToFixed, types::extra::U2}; -use subtensor_runtime_common::{AlphaBalance, NetUid, NetUidStorageIndex, TaoBalance}; - -#[allow(clippy::arithmetic_side_effects)] -fn close(value: u64, target: u64, eps: u64) { - assert!( - (value as i64 - target as i64).abs() < eps as i64, - "Assertion failed: value = {value}, target = {target}, eps = {eps}" - ) -} - -#[test] -fn test_migrate_associated_evm_address_index() { - new_test_ext(1).execute_with(|| { - let migration_name = b"migrate_associated_evm_address_index".to_vec(); - let netuid = NetUid::from(1); - let other_netuid = NetUid::from(2); - let evm_key = H160::repeat_byte(1); - let other_evm_key = H160::repeat_byte(2); - - HasMigrationRun::::remove(&migration_name); - AssociatedUidsByEvmAddress::::remove(netuid, evm_key); - AssociatedUidsByEvmAddress::::remove(other_netuid, other_evm_key); - - AssociatedEvmAddress::::insert(netuid, 0, (evm_key, 10)); - AssociatedEvmAddress::::insert(netuid, 1, (evm_key, 11)); - AssociatedEvmAddress::::insert(other_netuid, 0, (other_evm_key, 12)); - - crate::migrations::migrate_associated_evm_address_index::migrate_associated_evm_address_index::(); - - assert_eq!( - AssociatedUidsByEvmAddress::::get(netuid, evm_key).into_inner(), - vec![(0, 10), (1, 11)] - ); - assert_eq!( - AssociatedUidsByEvmAddress::::get(other_netuid, other_evm_key).into_inner(), - vec![(0, 12)] - ); - assert!(HasMigrationRun::::get(&migration_name)); - }); -} - -#[test] -fn test_migrate_clear_orphan_subnet_identities_v3() { - new_test_ext(1).execute_with(|| { - let migration_name = b"migrate_clear_orphan_subnet_identities_v3".to_vec(); - HasMigrationRun::::remove(&migration_name); - - let orphan_netuid = NetUid::from(1); - let live_netuid = NetUid::from(2); - - // live_netuid is a registered network; orphan_netuid is not. - NetworksAdded::::insert(live_netuid, true); - - let orphan_identity = SubnetIdentityV3 { - subnet_name: b"orphan".to_vec(), - ..Default::default() - }; - let live_identity = SubnetIdentityV3 { - subnet_name: b"live".to_vec(), - ..Default::default() - }; - - SubnetIdentitiesV3::::insert(orphan_netuid, orphan_identity); - SubnetIdentitiesV3::::insert(live_netuid, live_identity.clone()); - - crate::migrations::migrate_clear_orphan_subnet_identities_v3::migrate_clear_orphan_subnet_identities_v3::(); - - // The orphan identity is removed; the live subnet identity is preserved. - assert!(!SubnetIdentitiesV3::::contains_key(orphan_netuid)); - assert_eq!( - SubnetIdentitiesV3::::get(live_netuid), - Some(live_identity.clone()) - ); - - // Migration is marked as run. - assert!(HasMigrationRun::::get(&migration_name)); - - // Idempotent: re-running is a no-op (live identity still present). - crate::migrations::migrate_clear_orphan_subnet_identities_v3::migrate_clear_orphan_subnet_identities_v3::(); - assert_eq!( - SubnetIdentitiesV3::::get(live_netuid), - Some(live_identity) - ); - }); -} - -#[test] -fn test_migrate_associated_evm_address_index_reconciles_over_cap_buckets() { - new_test_ext(1).execute_with(|| { - let migration_name = b"migrate_associated_evm_address_index".to_vec(); - let netuid = NetUid::from(1); - let evm_key = H160::repeat_byte(1); - - HasMigrationRun::::remove(&migration_name); - AssociatedUidsByEvmAddress::::remove(netuid, evm_key); - - // Seed more forward-map associations for a single address than the reverse index can hold. - let cap = MAX_ASSOCIATED_UIDS_PER_EVM_ADDRESS; - let total = cap + 8; - for uid in 0..total { - AssociatedEvmAddress::::insert(netuid, uid as u16, (evm_key, 100 + uid as u64)); - } - - crate::migrations::migrate_associated_evm_address_index::migrate_associated_evm_address_index::(); - - // The reverse index is bounded by the cap. - let bucket = AssociatedUidsByEvmAddress::::get(netuid, evm_key); - assert_eq!(bucket.len() as u32, cap); - - // The forward map was pruned to match, so the two maps agree on the cap: every remaining - // forward entry is present in the reverse index, and there are no extras on either side. - let forward: Vec = AssociatedEvmAddress::::iter_prefix(netuid) - .map(|(uid, _)| uid) - .collect(); - assert_eq!(forward.len() as u32, cap); - for uid in &forward { - assert!( - bucket.iter().any(|(stored_uid, _)| stored_uid == uid), - "forward uid {uid} missing from reverse index" - ); - } - for (uid, _) in bucket.iter() { - assert!( - forward.contains(uid), - "reverse uid {uid} missing from forward map" - ); - } - - assert!(HasMigrationRun::::get(&migration_name)); - }); -} - -#[test] -fn test_migrate_tao_in_refund_deployment_block() { - new_test_ext(1).execute_with(|| { - let deployment_block: u64 = 42; - let migration_name = b"migrate_tao_in_refund_deployment_block".to_vec(); - - TaoInRefundDeploymentBlock::::put(0); - HasMigrationRun::::remove(&migration_name); - - run_to_block(deployment_block); - crate::migrations::migrate_tao_in_refund_deployment_block::migrate_tao_in_refund_deployment_block::(); - - assert_eq!(TaoInRefundDeploymentBlock::::get(), deployment_block); - assert!(HasMigrationRun::::get(&migration_name)); - - run_to_block(deployment_block.saturating_add(1)); - crate::migrations::migrate_tao_in_refund_deployment_block::migrate_tao_in_refund_deployment_block::(); - - assert_eq!(TaoInRefundDeploymentBlock::::get(), deployment_block); - }); -} - -#[test] -fn test_migrate_fix_subnet_hotkey_lock_swaps_moves_or_discards_conflicts() { - new_test_ext(1).execute_with(|| { - let migration_name = b"migrate_fix_subnet_hotkey_lock_swaps".to_vec(); - let old_hotkey = - decode_account_id32::("5Ca8L8PkbqXUtzohKtSM3i1naGQxANGLx51kJsEPNB14Admz") - .expect("old hotkey should decode"); - let new_hotkey = - decode_account_id32::("5Evgh9QTXJLxYLusVy3tcY5S6Z3GgRSNDb9AzXUchX5dco3P") - .expect("new hotkey should decode"); - let netuid = NetUid::from(28); - let coldkey_to_move = U256::from(1); - let coldkey_with_conflict = U256::from(2); - let chained_coldkey = - decode_account_id32::("5EWUPMenvyvHdEGUHfUhSTeTDJDLzLkKZq74LFLRWtzcqZiS") - .expect("chained coldkey should decode"); - let chained_first_hotkey = - decode_account_id32::("5H3Kuy7L7DBSy7BS2c9EBayJYGkHV1pzWtnJm3iXvThT4VUJ") - .expect("chained first hotkey should decode"); - let chained_middle_hotkey = - decode_account_id32::("5CSiRF3sMKt1c3MT4KsRLBWENGkymVE7wA2zUDPsYy6JtpGE") - .expect("chained middle hotkey should decode"); - let chained_final_hotkey = - decode_account_id32::("5EsnHJK89FgF55EYwXtqhUwLu3c14xakyQ8PWoomcFwpxk5e") - .expect("chained final hotkey should decode"); - let chained_netuid = NetUid::from(97); - - HasMigrationRun::::remove(&migration_name); - - let moved_lock = LockState { - locked_mass: AlphaBalance::from(10_u64), - conviction: U64F64::from_num(3), - last_update: 11, - }; - let discarded_lock = LockState { - locked_mass: AlphaBalance::from(20_u64), - conviction: U64F64::from_num(5), - last_update: 12, - }; - let existing_destination_lock = LockState { - locked_mass: AlphaBalance::from(77_u64), - conviction: U64F64::from_num(7), - last_update: 10, - }; - let chained_lock = LockState { - locked_mass: AlphaBalance::from(33_u64), - conviction: U64F64::from_num(4), - last_update: 13, - }; - - Lock::::insert( - (coldkey_to_move, netuid, old_hotkey), - moved_lock.clone(), - ); - LockingColdkeys::::insert((netuid, old_hotkey, coldkey_to_move), ()); - Lock::::insert( - (coldkey_with_conflict, netuid, old_hotkey), - discarded_lock.clone(), - ); - LockingColdkeys::::insert((netuid, old_hotkey, coldkey_with_conflict), ()); - Lock::::insert( - (coldkey_with_conflict, netuid, new_hotkey), - existing_destination_lock.clone(), - ); - LockingColdkeys::::insert((netuid, new_hotkey, coldkey_with_conflict), ()); - DecayingLock::::insert(coldkey_to_move, netuid, false); - DecayingLock::::insert(coldkey_with_conflict, netuid, false); - DecayingLock::::insert(chained_coldkey, chained_netuid, false); - HotkeyLock::::insert( - netuid, - old_hotkey, - LockState { - locked_mass: AlphaBalance::from(30_u64), - conviction: U64F64::from_num(8), - last_update: 12, - }, - ); - HotkeyLock::::insert(netuid, new_hotkey, existing_destination_lock.clone()); - Lock::::insert( - (chained_coldkey, chained_netuid, chained_first_hotkey), - chained_lock.clone(), - ); - LockingColdkeys::::insert( - (chained_netuid, chained_first_hotkey, chained_coldkey), - (), - ); - HotkeyLock::::insert(chained_netuid, chained_first_hotkey, chained_lock.clone()); - - let weight = - crate::migrations::migrate_fix_subnet_hotkey_lock_swaps::migrate_fix_subnet_hotkey_lock_swaps::(); - - assert!(!weight.is_zero(), "migration weight should be non-zero"); - assert!(HasMigrationRun::::get(&migration_name)); - assert!(Lock::::get((coldkey_to_move, netuid, old_hotkey)).is_none()); - assert!(Lock::::get((coldkey_with_conflict, netuid, old_hotkey)).is_none()); - assert!(!LockingColdkeys::::contains_key(( - netuid, - old_hotkey, - coldkey_to_move - ))); - assert!(!LockingColdkeys::::contains_key(( - netuid, - old_hotkey, - coldkey_with_conflict - ))); - assert_eq!( - Lock::::get((coldkey_to_move, netuid, new_hotkey)), - Some(moved_lock.clone()) - ); - assert!(LockingColdkeys::::contains_key(( - netuid, - new_hotkey, - coldkey_to_move - ))); - assert_eq!( - Lock::::get((coldkey_with_conflict, netuid, new_hotkey)), - Some(existing_destination_lock.clone()) - ); - assert!(LockingColdkeys::::contains_key(( - netuid, - new_hotkey, - coldkey_with_conflict - ))); - assert!(HotkeyLock::::get(netuid, old_hotkey).is_none()); - - let new_aggregate = HotkeyLock::::get(netuid, new_hotkey) - .expect("new aggregate should exist"); - assert_eq!( - new_aggregate.locked_mass, - existing_destination_lock - .locked_mass - .saturating_add(moved_lock.locked_mass) - ); - assert_eq!( - new_aggregate.conviction, - existing_destination_lock - .conviction - .saturating_add(moved_lock.conviction) - ); - assert!(Lock::::get(( - chained_coldkey, - chained_netuid, - chained_first_hotkey - )) - .is_none()); - assert!(Lock::::get(( - chained_coldkey, - chained_netuid, - chained_middle_hotkey - )) - .is_none()); - assert!(!LockingColdkeys::::contains_key(( - chained_netuid, - chained_first_hotkey, - chained_coldkey - ))); - assert!(!LockingColdkeys::::contains_key(( - chained_netuid, - chained_middle_hotkey, - chained_coldkey - ))); - assert_eq!( - Lock::::get((chained_coldkey, chained_netuid, chained_final_hotkey)), - Some(chained_lock.clone()) - ); - assert!(LockingColdkeys::::contains_key(( - chained_netuid, - chained_final_hotkey, - chained_coldkey - ))); - assert!(HotkeyLock::::get(chained_netuid, chained_first_hotkey).is_none()); - assert!(HotkeyLock::::get(chained_netuid, chained_middle_hotkey).is_none()); - assert_eq!( - HotkeyLock::::get(chained_netuid, chained_final_hotkey), - Some(chained_lock) - ); - }); -} -#[test] -fn test_migration_transfer_nets_to_foundation() { - new_test_ext(1).execute_with(|| { - // Create subnet 1 - add_network(1.into(), 1, 0); - // Create subnet 11 - add_network(11.into(), 1, 0); - - log::info!("{:?}", SubtensorModule::get_subnet_owner(1.into())); - //assert_eq!(SubtensorModule::::get_subnet_owner(1), ); - - // Run the migration to transfer ownership - let hex = - hex_literal::hex!["feabaafee293d3b76dae304e2f9d885f77d2b17adab9e17e921b321eccd61c77"]; - crate::migrations::migrate_transfer_ownership_to_foundation::migrate_transfer_ownership_to_foundation::(hex); - - log::info!("new owner: {:?}", SubtensorModule::get_subnet_owner(1.into())); - }) -} - -#[test] -fn test_migration_delete_subnet_3() { - new_test_ext(1).execute_with(|| { - // Create subnet 3 - add_network(3.into(), 1, 0); - assert!(SubtensorModule::if_subnet_exist(3.into())); - - // Run the migration to transfer ownership - crate::migrations::migrate_delete_subnet_3::migrate_delete_subnet_3::(); - - assert!(!SubtensorModule::if_subnet_exist(3.into())); - }) -} - -#[test] -fn test_migration_delete_subnet_21() { - new_test_ext(1).execute_with(|| { - // Create subnet 21 - add_network(21.into(), 1, 0); - assert!(SubtensorModule::if_subnet_exist(21.into())); - - // Run the migration to transfer ownership - crate::migrations::migrate_delete_subnet_21::migrate_delete_subnet_21::(); - - assert!(!SubtensorModule::if_subnet_exist(21.into())); - }) -} - -#[test] -fn test_migrate_commit_reveal_2() { - new_test_ext(1).execute_with(|| { - // ------------------------------ - // Step 1: Simulate Old Storage Entries - // ------------------------------ - const MIGRATION_NAME: &str = "migrate_commit_reveal_2_v2"; - - let pallet_prefix = twox_128("SubtensorModule".as_bytes()); - let storage_prefix_interval = twox_128("WeightCommitRevealInterval".as_bytes()); - let storage_prefix_commits = twox_128("WeightCommits".as_bytes()); - - let netuid = NetUid::from(1); - let interval_value: u64 = 50u64; - - // Construct the full key for WeightCommitRevealInterval - let mut interval_key = Vec::new(); - interval_key.extend_from_slice(&pallet_prefix); - interval_key.extend_from_slice(&storage_prefix_interval); - interval_key.extend_from_slice(&netuid.encode()); - - put_raw(&interval_key, &interval_value.encode()); - - let test_account: U256 = U256::from(1); - - // Construct the full key for WeightCommits (DoubleMap) - let mut commit_key = Vec::new(); - commit_key.extend_from_slice(&pallet_prefix); - commit_key.extend_from_slice(&storage_prefix_commits); - - // First key (netuid) hashed with Twox64Concat - let netuid_hashed = Twox64Concat::hash(&netuid.encode()); - commit_key.extend_from_slice(&netuid_hashed); - - // Second key (account) hashed with Twox64Concat - let account_hashed = Twox64Concat::hash(&test_account.encode()); - commit_key.extend_from_slice(&account_hashed); - - let commit_value: (H256, u64) = (H256::from_low_u64_be(42), 100); - put_raw(&commit_key, &commit_value.encode()); - - let stored_interval = get_raw(&interval_key).expect("Expected to get a value"); - assert_eq!( - u64::decode(&mut &stored_interval[..]).expect("Failed to decode interval value"), - interval_value - ); - - let stored_commit = get_raw(&commit_key).expect("Expected to get a value"); - assert_eq!( - <(H256, u64)>::decode(&mut &stored_commit[..]).expect("Failed to decode commit value"), - commit_value - ); - - assert!( - !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should not have run yet" - ); - - // ------------------------------ - // Step 2: Run the Migration - // ------------------------------ - let weight = crate::migrations::migrate_commit_reveal_v2::migrate_commit_reveal_2::(); - - assert!( - HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should be marked as run" - ); - - // ------------------------------ - // Step 3: Verify Migration Effects - // ------------------------------ - let stored_interval_after = get_raw(&interval_key); - assert!( - stored_interval_after.is_none(), - "WeightCommitRevealInterval should be cleared" - ); - - let stored_commit_after = get_raw(&commit_key); - assert!( - stored_commit_after.is_none(), - "WeightCommits entry should be cleared" - ); - - assert!(!weight.is_zero(), "Migration weight should be non-zero"); - }); -} - -// Leaving in for reference. Will remove later. -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::migration::test_migrate_rao --exact --show-output --nocapture -// #[test] -// fn test_migrate_rao() { -// new_test_ext(1).execute_with(|| { -// // Setup initial state -// let netuid_0: u16 = 0; -// let netuid_1: u16 = 1; -// let netuid_2: u16 = 2; -// let netuid_3: u16 = 3; -// let hotkey1 = U256::from(1); -// let hotkey2 = U256::from(2); -// let coldkey1 = U256::from(3); -// let coldkey2 = U256::from(4); -// let coldkey3 = U256::from(5); -// let stake_amount: u64 = 1_000_000_000; -// let lock_amount: u64 = 500; -// NetworkMinLockCost::::set(500); - -// // Add networks root and alpha -// add_network(netuid_0, 1, 0); -// add_network(netuid_1, 1, 0); -// add_network(netuid_2, 1, 0); -// add_network(netuid_3, 1, 0); - -// // Set subnet lock -// SubnetLocked::::insert(netuid_1, lock_amount); - -// // Add some initial stake -// EmissionValues::::insert(netuid_1, 1_000_000_000); -// EmissionValues::::insert(netuid_2, 2_000_000_000); -// EmissionValues::::insert(netuid_3, 3_000_000_000); - -// Owner::::insert(hotkey1, coldkey1); -// Owner::::insert(hotkey2, coldkey2); -// Stake::::insert(hotkey1, coldkey1, stake_amount); -// Stake::::insert(hotkey1, coldkey2, stake_amount); -// Stake::::insert(hotkey2, coldkey2, stake_amount); -// Stake::::insert(hotkey2, coldkey3, stake_amount); - -// // Verify initial conditions -// assert_eq!(SubnetTAO::::get(netuid_0), 0); -// assert_eq!(SubnetTAO::::get(netuid_1), 0); -// assert_eq!(SubnetAlphaOut::::get(netuid_0), 0); -// assert_eq!(SubnetAlphaOut::::get(netuid_1), 0); -// assert_eq!(SubnetAlphaIn::::get(netuid_0), 0); -// assert_eq!(SubnetAlphaIn::::get(netuid_1), 0); -// assert_eq!(TotalHotkeyShares::::get(hotkey1, netuid_0), 0); -// assert_eq!(TotalHotkeyShares::::get(hotkey1, netuid_1), 0); -// assert_eq!(TotalHotkeyAlpha::::get(hotkey1, netuid_0), 0); -// assert_eq!(TotalHotkeyAlpha::::get(hotkey2, netuid_1), 0); - -// // Run migration -// crate::migrations::migrate_rao::migrate_rao::(); - -// // Verify root subnet (netuid 0) state after migration -// assert_eq!(SubnetTAO::::get(netuid_0), 4 * stake_amount); // Root has everything -// assert_eq!(SubnetTAO::::get(netuid_1), 1_000_000_000); // Always 1000000000 -// assert_eq!(SubnetAlphaIn::::get(netuid_0), 1_000_000_000); // Always 1_000_000_000 -// assert_eq!(SubnetAlphaIn::::get(netuid_1), 1_000_000_000); // Always 1_000_000_000 -// assert_eq!(SubnetAlphaOut::::get(netuid_0), 4 * stake_amount); // Root has everything. -// assert_eq!(SubnetAlphaOut::::get(netuid_1), 0); // No stake outstanding. - -// // Assert share information for hotkey1 on netuid_0 -// assert_eq!( -// TotalHotkeyShares::::get(hotkey1, netuid_0), -// 2 * stake_amount -// ); // Shares -// // Assert no shares for hotkey1 on netuid_1 -// assert_eq!(TotalHotkeyShares::::get(hotkey1, netuid_1), 0); // No shares -// // Assert alpha for hotkey1 on netuid_0 -// assert_eq!( -// TotalHotkeyAlpha::::get(hotkey1, netuid_0), -// 2 * stake_amount -// ); // Alpha -// // Assert no alpha for hotkey1 on netuid_1 -// assert_eq!(TotalHotkeyAlpha::::get(hotkey1, netuid_1), 0); // No alpha. -// // Assert share information for hotkey2 on netuid_0 -// assert_eq!( -// TotalHotkeyShares::::get(hotkey2, netuid_0), -// 2 * stake_amount -// ); // Shares -// // Assert no shares for hotkey2 on netuid_1 -// assert_eq!(TotalHotkeyShares::::get(hotkey2, netuid_1), 0); // No shares -// // Assert alpha for hotkey2 on netuid_0 -// assert_eq!( -// TotalHotkeyAlpha::::get(hotkey2, netuid_0), -// 2 * stake_amount -// ); // Alpha -// // Assert no alpha for hotkey2 on netuid_1 -// assert_eq!(TotalHotkeyAlpha::::get(hotkey2, netuid_1), 0); // No alpha. - -// // Assert stake balances for hotkey1 and coldkey1 on netuid_0 -// assert_eq!( -// SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( -// &hotkey1, &coldkey1, netuid_0 -// ), -// stake_amount -// ); -// // Assert stake balances for hotkey1 and coldkey2 on netuid_0 -// assert_eq!( -// SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( -// &hotkey1, &coldkey2, netuid_0 -// ), -// stake_amount -// ); -// // Assert stake balances for hotkey2 and coldkey2 on netuid_0 -// assert_eq!( -// SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( -// &hotkey2, &coldkey2, netuid_0 -// ), -// stake_amount -// ); -// // Assert stake balances for hotkey2 and coldkey3 on netuid_0 -// assert_eq!( -// SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( -// &hotkey2, &coldkey3, netuid_0 -// ), -// stake_amount -// ); -// // Assert total stake for hotkey1 on netuid_0 -// assert_eq!( -// SubtensorModule::get_stake_for_hotkey_on_subnet(&hotkey1, netuid_0), -// 2 * stake_amount -// ); -// // Assert total stake for hotkey2 on netuid_0 -// assert_eq!( -// SubtensorModule::get_stake_for_hotkey_on_subnet(&hotkey2, netuid_0), -// 2 * stake_amount -// ); -// // Increase stake for hotkey1 and coldkey1 on netuid_0 -// mock_increase_stake_for_hotkey_and_coldkey_on_subnet( -// &hotkey1, -// &coldkey1, -// netuid_0, -// stake_amount, -// ); -// // Assert updated stake for hotkey1 and coldkey1 on netuid_0 -// assert_eq!( -// SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( -// &hotkey1, &coldkey1, netuid_0 -// ), -// 2 * stake_amount -// ); -// // Assert updated total stake for hotkey1 on netuid_0 -// assert_eq!( -// SubtensorModule::get_stake_for_hotkey_on_subnet(&hotkey1, netuid_0), -// 3 * stake_amount -// ); -// // Increase stake for hotkey1 and coldkey1 on netuid_1 -// mock_increase_stake_for_hotkey_and_coldkey_on_subnet( -// &hotkey1, -// &coldkey1, -// netuid_1, -// stake_amount, -// ); -// // Assert updated stake for hotkey1 and coldkey1 on netuid_1 -// assert_eq!( -// SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( -// &hotkey1, &coldkey1, netuid_1 -// ), -// stake_amount -// ); -// // Assert updated total stake for hotkey1 on netuid_1 -// assert_eq!( -// SubtensorModule::get_stake_for_hotkey_on_subnet(&hotkey1, netuid_1), -// stake_amount -// ); - -// // Run the coinbase -// let emission: u64 = 1_000_000_000; -// SubtensorModule::run_coinbase(I96F32::from_num(emission)); -// close( -// SubnetTaoInEmission::::get(netuid_1), -// emission / 6, -// 100, -// ); -// close( -// SubnetTaoInEmission::::get(netuid_2), -// 2 * (emission / 6), -// 100, -// ); -// close( -// SubnetTaoInEmission::::get(netuid_3), -// 3 * (emission / 6), -// 100, -// ); -// }); -// } - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::migration::test_migrate_subnet_volume --exact --show-output -#[test] -fn test_migrate_subnet_volume() { - new_test_ext(1).execute_with(|| { - // Setup initial state - let netuid_1 = NetUid::from(1); - add_network(netuid_1, 1, 0); - - // SubnetValue for netuid 1 key - let old_key: [u8; 34] = hex_literal::hex!( - "658faa385070e074c85bf6b568cf05553c3226e141696000b4b239c65bc2b2b40100" - ); - - // Old value in u64 format - let old_value: u64 = 123_456_789_000_u64; - put::(&old_key, &old_value); // Store as u64 - - // Ensure it is stored as `u64` - assert_eq!(get::(&old_key), Some(old_value)); - - // Run migration - crate::migrations::migrate_subnet_volume::migrate_subnet_volume::(); - - // Verify the value is now stored as `u128` - let new_value: Option = get(&old_key); - let new_value_as_subnet_volume = SubnetVolume::::get(netuid_1); - assert_eq!(new_value, Some(old_value as u128)); - assert_eq!(new_value_as_subnet_volume, old_value as u128); - - // Ensure migration does not break when running twice - let weight_second_run = - crate::migrations::migrate_subnet_volume::migrate_subnet_volume::(); - - // Verify the value is still stored as `u128` - let new_value: Option = get(&old_key); - assert_eq!(new_value, Some(old_value as u128)); - }); -} - -#[test] -fn test_migrate_set_first_emission_block_number() { - new_test_ext(1).execute_with(|| { - let netuids: [NetUid; 3] = [1.into(), 2.into(), 3.into()]; - let block_number = 100; - for netuid in netuids.iter() { - add_network(*netuid, 1, 0); - } - run_to_block(block_number); - let weight = crate::migrations::migrate_set_first_emission_block_number::migrate_set_first_emission_block_number::(); - - let expected_weight: Weight = ::DbWeight::get().reads(3) + ::DbWeight::get().writes(netuids.len() as u64); - assert_eq!(weight, expected_weight); - - assert_eq!(FirstEmissionBlockNumber::::get(NetUid::ROOT), None); - for netuid in netuids.iter() { - assert_eq!(FirstEmissionBlockNumber::::get(netuid), Some(block_number)); - } -}); -} - -#[test] -fn test_migrate_set_subtoken_enable() { - new_test_ext(1).execute_with(|| { - let netuids: [NetUid; 3] = [1.into(), 2.into(), 3.into()]; - let block_number = 100; - for netuid in netuids.iter() { - add_network(*netuid, 1, 0); - } - - let new_netuid = NetUid::from(4); - add_network_without_emission_block(new_netuid, 1, 0); - - let weight = - crate::migrations::migrate_set_subtoken_enabled::migrate_set_subtoken_enabled::(); - - let expected_weight: Weight = ::DbWeight::get().reads(1) - + ::DbWeight::get().writes(netuids.len() as u64 + 2); - assert_eq!(weight, expected_weight); - - for netuid in netuids.iter() { - assert!(SubtokenEnabled::::get(netuid)); - } - assert!(!SubtokenEnabled::::get(new_netuid)); - }); -} - -#[test] -fn test_migrate_remove_zero_total_hotkey_alpha() { - new_test_ext(1).execute_with(|| { - const MIGRATION_NAME: &str = "migrate_remove_zero_total_hotkey_alpha"; - let netuid = NetUid::from(1u16); - - let hotkey_zero = U256::from(100u64); - let hotkey_nonzero = U256::from(101u64); - - // Insert one zero-alpha entry and one non-zero entry - TotalHotkeyAlpha::::insert(hotkey_zero, netuid, AlphaBalance::ZERO); - TotalHotkeyAlpha::::insert(hotkey_nonzero, netuid, AlphaBalance::from(123)); - - assert_eq!(TotalHotkeyAlpha::::get(hotkey_zero, netuid), AlphaBalance::ZERO); - assert_eq!(TotalHotkeyAlpha::::get(hotkey_nonzero, netuid), AlphaBalance::from(123)); - - assert!( - !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should not have run yet." - ); - - let weight = crate::migrations::migrate_remove_zero_total_hotkey_alpha::migrate_remove_zero_total_hotkey_alpha::(); - - assert!( - HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should be marked as run." - ); - - assert!( - !TotalHotkeyAlpha::::contains_key(hotkey_zero, netuid), - "Zero-alpha entry should have been removed." - ); - - assert_eq!(TotalHotkeyAlpha::::get(hotkey_nonzero, netuid), AlphaBalance::from(123)); - - assert!( - !weight.is_zero(), - "Migration weight should be non-zero." - ); - }); -} - -#[test] -fn test_migrate_revealed_commitments() { - new_test_ext(1).execute_with(|| { - // -------------------------------- - // Step 1: Simulate Old Storage Entries - // -------------------------------- - const MIGRATION_NAME: &str = "migrate_revealed_commitments_v2"; - - // Pallet prefix == twox_128("Commitments") - let pallet_prefix = twox_128("Commitments".as_bytes()); - // Storage item prefix == twox_128("RevealedCommitments") - let storage_prefix = twox_128("RevealedCommitments".as_bytes()); - - // Example keys for the DoubleMap: - // Key1 (netuid) uses Identity (no hash) - // Key2 (account) uses Twox64Concat - let netuid = NetUid::from(123); - let account_id: u64 = 999; // Or however your test `AccountId` is represented - - // Construct the full storage key for `RevealedCommitments(netuid, account_id)` - let mut storage_key = Vec::new(); - storage_key.extend_from_slice(&pallet_prefix); - storage_key.extend_from_slice(&storage_prefix); - - // Identity for netuid => no hashing, just raw encode - storage_key.extend_from_slice(&netuid.encode()); - - // Twox64Concat for account - let account_hashed = Twox64Concat::hash(&account_id.encode()); - storage_key.extend_from_slice(&account_hashed); - - // Simulate an old value we might have stored: - // For example, the old type was `RevealedData` - // We'll just store a random encoded value for demonstration - let old_value = (vec![1, 2, 3, 4], 42u64); - put_raw(&storage_key, &old_value.encode()); - - // Confirm the storage value is set - let stored_value = get_raw(&storage_key).expect("Expected to get a value"); - let decoded_value = <(Vec, u64)>::decode(&mut &stored_value[..]) - .expect("Failed to decode the old revealed commitments"); - assert_eq!(decoded_value, old_value); - - // Also confirm that the migration has NOT run yet - assert!( - !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should not have run yet" - ); - - // -------------------------------- - // Step 2: Run the Migration - // -------------------------------- - let weight = crate::migrations::migrate_upgrade_revealed_commitments::migrate_upgrade_revealed_commitments::(); - - // Migration should be marked as run - assert!( - HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should now be marked as run" - ); - - // -------------------------------- - // Step 3: Verify Migration Effects - // -------------------------------- - // The old key/value should be removed - let stored_value_after = get_raw(&storage_key); - assert!( - stored_value_after.is_none(), - "Old storage entry should be cleared" - ); - - // Weight returned should be > 0 (some cost was incurred clearing storage) - assert!(!weight.is_zero(), "Migration weight should be non-zero"); - }); -} - -#[test] -fn test_migrate_remove_total_hotkey_coldkey_stakes_this_interval() { - new_test_ext(1).execute_with(|| { - const MIGRATION_NAME: &str = "migrate_remove_total_hotkey_coldkey_stakes_this_interval"; - - let pallet_name = twox_128(b"SubtensorModule"); - let storage_name = twox_128(b"TotalHotkeyColdkeyStakesThisInterval"); - let prefix = [pallet_name, storage_name].concat(); - - // Set up 200 000 entries to be deleted. - for i in 0..200_000{ - let hotkey = U256::from(i as u64); - let coldkey = U256::from(i as u64); - let key = [prefix.clone(), hotkey.encode(), coldkey.encode()].concat(); - let value = (100 + i, 200 + i); - put_raw(&key, &value.encode()); - } - - assert!(frame_support::storage::unhashed::contains_prefixed_key(&prefix), "Entries should exist before migration."); - assert!( - !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should not have run yet." - ); - - // Run migration - let weight = crate::migrations::migrate_remove_total_hotkey_coldkey_stakes_this_interval::migrate_remove_total_hotkey_coldkey_stakes_this_interval::(); - - assert!(!frame_support::storage::unhashed::contains_prefixed_key(&prefix), "All entries should have been removed."); - assert!( - HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should be marked as run." - ); - assert!(!weight.is_zero(),"Migration weight should be non-zero."); - }); -} -fn test_migrate_remove_last_hotkey_coldkey_emission_on_netuid() { - const MIGRATION_NAME: &str = "migrate_remove_last_hotkey_coldkey_emission_on_netuid"; - let pallet_name = "SubtensorModule"; - let storage_name = "LastHotkeyColdkeyEmissionOnNetuid"; - let migration = crate::migrations::migrate_orphaned_storage_items::remove_last_hotkey_coldkey_emission_on_netuid::; - - test_remove_storage_item( - MIGRATION_NAME, - pallet_name, - storage_name, - migration, - 200_000, - ); -} -#[test] -fn test_migrate_remove_subnet_alpha_emission_sell() { - const MIGRATION_NAME: &str = "migrate_remove_subnet_alpha_emission_sell"; - let pallet_name = "SubtensorModule"; - let storage_name = "SubnetAlphaEmissionSell"; - let migration = - crate::migrations::migrate_orphaned_storage_items::remove_subnet_alpha_emission_sell::; - - test_remove_storage_item( - MIGRATION_NAME, - pallet_name, - storage_name, - migration, - 200_000, - ); -} - -#[test] -fn test_migrate_remove_neurons_to_prune_at_next_epoch() { - const MIGRATION_NAME: &str = "migrate_remove_neurons_to_prune_at_next_epoch"; - let pallet_name = "SubtensorModule"; - let storage_name = "NeuronsToPruneAtNextEpoch"; - let migration = - crate::migrations::migrate_orphaned_storage_items::remove_neurons_to_prune_at_next_epoch::< - Test, - >; - - test_remove_storage_item( - MIGRATION_NAME, - pallet_name, - storage_name, - migration, - 200_000, - ); -} - -#[test] -fn test_migrate_remove_total_stake_at_dynamic() { - const MIGRATION_NAME: &str = "migrate_remove_total_stake_at_dynamic"; - let pallet_name = "SubtensorModule"; - let storage_name = "TotalStakeAtDynamic"; - let migration = - crate::migrations::migrate_orphaned_storage_items::remove_total_stake_at_dynamic::; - - test_remove_storage_item( - MIGRATION_NAME, - pallet_name, - storage_name, - migration, - 200_000, - ); -} - -#[test] -fn test_migrate_remove_subnet_name() { - const MIGRATION_NAME: &str = "migrate_remove_subnet_name"; - let pallet_name = "SubtensorModule"; - let storage_name = "SubnetName"; - let migration = crate::migrations::migrate_orphaned_storage_items::remove_subnet_name::; - - test_remove_storage_item( - MIGRATION_NAME, - pallet_name, - storage_name, - migration, - 200_000, - ); -} - -#[test] -fn test_migrate_remove_network_min_allowed_uids() { - const MIGRATION_NAME: &str = "migrate_remove_network_min_allowed_uids"; - let pallet_name = "SubtensorModule"; - let storage_name = "NetworkMinAllowedUids"; - let migration = - crate::migrations::migrate_orphaned_storage_items::remove_network_min_allowed_uids::; - - test_remove_storage_item(MIGRATION_NAME, pallet_name, storage_name, migration, 1); -} - -#[test] -fn test_migrate_remove_dynamic_block() { - const MIGRATION_NAME: &str = "migrate_remove_dynamic_block"; - let pallet_name = "SubtensorModule"; - let storage_name = "DynamicBlock"; - let migration = crate::migrations::migrate_orphaned_storage_items::remove_dynamic_block::; - - test_remove_storage_item(MIGRATION_NAME, pallet_name, storage_name, migration, 1); -} - -#[allow(clippy::arithmetic_side_effects)] -fn test_remove_storage_item Weight>( - migration_name: &'static str, - pallet_name: &'static str, - storage_name: &'static str, - migration: F, - test_entries_number: i32, -) { - new_test_ext(1).execute_with(|| { - let pallet_name = twox_128(pallet_name.as_bytes()); - let storage_name = twox_128(storage_name.as_bytes()); - let prefix = [pallet_name, storage_name].concat(); - - // Set up entries to be deleted. - for i in 0..test_entries_number { - let hotkey = U256::from(i as u64); - let coldkey = U256::from(i as u64); - let key = [prefix.clone(), hotkey.encode(), coldkey.encode()].concat(); - let value = (100 + i, 200 + i); - put_raw(&key, &value.encode()); - } - - assert!( - frame_support::storage::unhashed::contains_prefixed_key(&prefix), - "Entries should exist before migration." - ); - assert!( - !HasMigrationRun::::get(migration_name.as_bytes().to_vec()), - "Migration should not have run yet." - ); - - // Run migration - let weight = migration(); - - assert!( - !frame_support::storage::unhashed::contains_prefixed_key(&prefix), - "All entries should have been removed." - ); - assert!( - HasMigrationRun::::get(migration_name.as_bytes().to_vec()), - "Migration should be marked as run." - ); - assert!(!weight.is_zero(), "Migration weight should be non-zero."); - }); -} - -#[test] -fn test_migrate_remove_commitments_rate_limit() { - new_test_ext(1).execute_with(|| { - // ------------------------------ - // Step 1: Simulate Old Storage Entry - // ------------------------------ - const MIGRATION_NAME: &str = "migrate_remove_commitments_rate_limit"; - - // Build the raw storage key: twox128("Commitments") ++ twox128("RateLimit") - let pallet_prefix = twox_128("Commitments".as_bytes()); - let storage_prefix = twox_128("RateLimit".as_bytes()); - - let mut key = Vec::new(); - key.extend_from_slice(&pallet_prefix); - key.extend_from_slice(&storage_prefix); - - let original_value: u64 = 123; - put_raw(&key, &original_value.encode()); - - let stored_before = get_raw(&key).expect("Expected RateLimit to exist"); - assert_eq!( - u64::decode(&mut &stored_before[..]).expect("Failed to decode RateLimit"), - original_value - ); - - assert!( - !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should not have run yet" - ); - - // ------------------------------ - // Step 2: Run the Migration - // ------------------------------ - let weight = crate::migrations::migrate_remove_commitments_rate_limit:: - migrate_remove_commitments_rate_limit::(); - - assert!( - HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should be marked as completed" - ); - - // ------------------------------ - // Step 3: Verify Migration Effects - // ------------------------------ - assert!( - get_raw(&key).is_none(), - "RateLimit storage should have been cleared" - ); - - assert!(!weight.is_zero(), "Migration weight should be non-zero"); - }); -} - -#[test] -fn test_migrate_network_last_registered() { - new_test_ext(1).execute_with(|| { - // ------------------------------ - // Step 1: Simulate Old Storage Entry - // ------------------------------ - const MIGRATION_NAME: &str = "migrate_network_last_registered"; - - let pallet_name = "SubtensorModule"; - let storage_name = "NetworkLastRegistered"; - let pallet_name_hash = twox_128(pallet_name.as_bytes()); - let storage_name_hash = twox_128(storage_name.as_bytes()); - let prefix = [pallet_name_hash, storage_name_hash].concat(); - - let mut full_key = prefix.clone(); - - let original_value: u64 = 123; - put_raw(&full_key, &original_value.encode()); - - let stored_before = get_raw(&full_key).expect("Expected RateLimit to exist"); - assert_eq!( - u64::decode(&mut &stored_before[..]).expect("Failed to decode RateLimit"), - original_value - ); - - assert!( - !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should not have run yet" - ); - - // ------------------------------ - // Step 2: Run the Migration - // ------------------------------ - let weight = crate::migrations::migrate_rate_limiting_last_blocks:: - migrate_obsolete_rate_limiting_last_blocks_storage::(); - - assert!( - HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should be marked as completed" - ); - - // ------------------------------ - // Step 3: Verify Migration Effects - // ------------------------------ - - assert_eq!( - SubtensorModule::get_network_last_lock_block(), - original_value - ); - assert_eq!( - get_raw(&full_key), - None, - "RateLimit storage should have been cleared" - ); - - assert!(!weight.is_zero(), "Migration weight should be non-zero"); - }); -} - -#[allow(deprecated)] -#[test] -fn test_migrate_last_block_tx() { - new_test_ext(1).execute_with(|| { - // ------------------------------ - // Step 1: Simulate Old Storage Entry - // ------------------------------ - const MIGRATION_NAME: &str = "migrate_last_tx_block"; - - let test_account: U256 = U256::from(1); - let original_value: u64 = 123; - - LastTxBlock::::insert(test_account, original_value); - - assert!( - !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should not have run yet" - ); - - // ------------------------------ - // Step 2: Run the Migration - // ------------------------------ - let weight = crate::migrations::migrate_rate_limiting_last_blocks:: - migrate_obsolete_rate_limiting_last_blocks_storage::(); - - assert!( - HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should be marked as completed" - ); - - // ------------------------------ - // Step 3: Verify Migration Effects - // ------------------------------ - - assert_eq!( - SubtensorModule::get_last_tx_block(&test_account), - original_value - ); - assert!( - !LastTxBlock::::contains_key(test_account), - "RateLimit storage should have been cleared" - ); - - assert!(!weight.is_zero(), "Migration weight should be non-zero"); - }); -} - -#[allow(deprecated)] -#[test] -fn test_migrate_last_tx_block_childkey_take() { - new_test_ext(1).execute_with(|| { - // ------------------------------ - // Step 1: Simulate Old Storage Entry - // ------------------------------ - const MIGRATION_NAME: &str = "migrate_last_tx_block_childkey_take"; - - let test_account: U256 = U256::from(1); - let original_value: u64 = 123; - - LastTxBlockChildKeyTake::::insert(test_account, original_value); - - assert!( - !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should not have run yet" - ); - - // ------------------------------ - // Step 2: Run the Migration - // ------------------------------ - let weight = crate::migrations::migrate_rate_limiting_last_blocks:: - migrate_obsolete_rate_limiting_last_blocks_storage::(); - - assert!( - HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should be marked as completed" - ); - - // ------------------------------ - // Step 3: Verify Migration Effects - // ------------------------------ - - assert_eq!( - SubtensorModule::get_last_tx_block_childkey_take(&test_account), - original_value - ); - assert!( - !LastTxBlockChildKeyTake::::contains_key(test_account), - "RateLimit storage should have been cleared" - ); - - assert!(!weight.is_zero(), "Migration weight should be non-zero"); - }); -} - -#[allow(deprecated)] -// PerU16 must SCALE-encode byte-identically to u16, so the take/epoch storages -// retyped from u16 to PerU16 require no storage migration. -#[test] -fn test_per_u16_encodes_identically_to_u16() { - assert_eq!(PerU16::from_parts(5).encode(), 5u16.encode()); - assert_eq!(PerU16::from_parts(u16::MAX).encode(), u16::MAX.encode()); - assert_eq!(PerU16::zero().encode(), 0u16.encode()); -} - -#[allow(deprecated)] -#[test] -fn test_migrate_last_tx_block_delegate_take() { - new_test_ext(1).execute_with(|| { - // ------------------------------ - // Step 1: Simulate Old Storage Entry - // ------------------------------ - const MIGRATION_NAME: &str = "migrate_last_tx_block_delegate_take"; - - let test_account: U256 = U256::from(1); - let original_value: u64 = 123; - - LastTxBlockDelegateTake::::insert(test_account, original_value); - - assert!( - !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should not have run yet" - ); - - // ------------------------------ - // Step 2: Run the Migration - // ------------------------------ - let weight = crate::migrations::migrate_rate_limiting_last_blocks:: - migrate_last_tx_block_delegate_take::(); - - assert!( - HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should be marked as completed" - ); - - // ------------------------------ - // Step 3: Verify Migration Effects - // ------------------------------ - - assert_eq!( - SubtensorModule::get_last_tx_block_delegate_take(&test_account), - original_value - ); - assert!( - !LastTxBlockDelegateTake::::contains_key(test_account), - "RateLimit storage should have been cleared" - ); - - assert!(!weight.is_zero(), "Migration weight should be non-zero"); - }); -} - -#[test] -fn test_migrate_rate_limit_keys() { - new_test_ext(1).execute_with(|| { - const MIGRATION_NAME: &[u8] = b"migrate_rate_limit_keys"; - let prefix = { - let pallet_prefix = twox_128("SubtensorModule".as_bytes()); - let storage_prefix = twox_128("LastRateLimitedBlock".as_bytes()); - [pallet_prefix, storage_prefix].concat() - }; - - // Seed new-format entries that must survive the migration untouched. - let new_last_account = U256::from(10); - SubtensorModule::set_last_tx_block(&new_last_account, 555); - let new_child_account = U256::from(11); - SubtensorModule::set_last_tx_block_childkey(&new_child_account, 777); - let new_delegate_account = U256::from(12); - SubtensorModule::set_last_tx_block_delegate_take(&new_delegate_account, 888); - - // Legacy NetworkLastRegistered entry (index 1) - let mut legacy_network_key = prefix.clone(); - legacy_network_key.push(1u8); - sp_io::storage::set(&legacy_network_key, &111u64.encode()); - - // Legacy LastTxBlock entry (index 2) for an account that already has a new-format value. - let mut legacy_last_key = prefix.clone(); - legacy_last_key.push(2u8); - legacy_last_key.extend_from_slice(&new_last_account.encode()); - sp_io::storage::set(&legacy_last_key, &666u64.encode()); - - // Legacy LastTxBlockChildKeyTake entry (index 3) - let legacy_child_account = U256::from(3); - ChildKeys::::insert( - legacy_child_account, - NetUid::from(0), - vec![(0u64, U256::from(99))], - ); - let mut legacy_child_key = prefix.clone(); - legacy_child_key.push(3u8); - legacy_child_key.extend_from_slice(&legacy_child_account.encode()); - sp_io::storage::set(&legacy_child_key, &333u64.encode()); - - // Legacy LastTxBlockDelegateTake entry (index 4) - let legacy_delegate_account = U256::from(4); - Delegates::::insert(legacy_delegate_account, PerU16::from_parts(500)); - let mut legacy_delegate_key = prefix.clone(); - legacy_delegate_key.push(4u8); - legacy_delegate_key.extend_from_slice(&legacy_delegate_account.encode()); - sp_io::storage::set(&legacy_delegate_key, &444u64.encode()); - - let weight = crate::migrations::migrate_rate_limit_keys::migrate_rate_limit_keys::(); - assert!( - HasMigrationRun::::get(MIGRATION_NAME.to_vec()), - "Migration should be marked as executed" - ); - assert!(!weight.is_zero(), "Migration weight should be non-zero"); - - // Legacy entries were migrated and cleared. - assert_eq!( - SubtensorModule::get_network_last_lock_block(), - 111u64, - "Network last lock block should match migrated value" - ); - assert!( - sp_io::storage::get(&legacy_network_key).is_none(), - "Legacy network entry should be cleared" - ); - - assert_eq!( - SubtensorModule::get_last_tx_block(&new_last_account), - 666u64, - "LastTxBlock should reflect the merged legacy value" - ); - assert!( - sp_io::storage::get(&legacy_last_key).is_none(), - "Legacy LastTxBlock entry should be cleared" - ); - - assert_eq!( - SubtensorModule::get_last_tx_block_childkey_take(&legacy_child_account), - 333u64, - "Child key take block should be migrated" - ); - assert!( - sp_io::storage::get(&legacy_child_key).is_none(), - "Legacy child take entry should be cleared" - ); - - assert_eq!( - SubtensorModule::get_last_tx_block_delegate_take(&legacy_delegate_account), - 444u64, - "Delegate take block should be migrated" - ); - assert!( - sp_io::storage::get(&legacy_delegate_key).is_none(), - "Legacy delegate take entry should be cleared" - ); - - // New-format entries remain untouched. - assert_eq!( - SubtensorModule::get_last_tx_block_childkey_take(&new_child_account), - 777u64, - "Existing child take entry should be preserved" - ); - assert_eq!( - SubtensorModule::get_last_tx_block_delegate_take(&new_delegate_account), - 888u64, - "Existing delegate take entry should be preserved" - ); - }); -} - -#[test] -fn test_migrate_remove_add_stake_burn_rate_limit() { - new_test_ext(1).execute_with(|| { - const MIGRATION_NAME: &[u8] = b"migrate_remove_add_stake_burn_rate_limit"; - let netuid = NetUid::from(1); - let other_netuid = NetUid::from(2); - let preserved_netuid = NetUid::from(3); - let add_stake_burn_key = RateLimitKey::AddStakeBurn(netuid); - let other_add_stake_burn_key = RateLimitKey::AddStakeBurn(other_netuid); - let preserved_key = RateLimitKey::SetSNOwnerHotkey(preserved_netuid); - - SubtensorModule::set_rate_limited_last_block(&add_stake_burn_key, 100); - SubtensorModule::set_rate_limited_last_block(&other_add_stake_burn_key, 200); - SubtensorModule::set_rate_limited_last_block(&preserved_key, 300); - - let weight = - crate::migrations::migrate_remove_add_stake_burn_rate_limit::migrate_remove_add_stake_burn_rate_limit::(); - - assert!( - HasMigrationRun::::get(MIGRATION_NAME.to_vec()), - "Migration should be marked as executed" - ); - assert!(!weight.is_zero(), "Migration weight should be non-zero"); - - assert_eq!( - SubtensorModule::get_rate_limited_last_block(&add_stake_burn_key), - 0 - ); - assert_eq!( - SubtensorModule::get_rate_limited_last_block(&other_add_stake_burn_key), - 0 - ); - assert_eq!( - SubtensorModule::get_rate_limited_last_block(&preserved_key), - 300 - ); - }); -} - -#[test] -fn test_migrate_populate_locking_coldkeys() { - new_test_ext(1).execute_with(|| { - const MIGRATION_NAME: &[u8] = b"migrate_populate_locking_coldkeys"; - - let netuid = NetUid::from(1); - let coldkey_1 = U256::from(1001); - let coldkey_2 = U256::from(1002); - let hotkey = U256::from(2001); - let expired_hotkey = U256::from(2002); - - Lock::::insert( - (coldkey_1, netuid, hotkey), - LockState { - locked_mass: AlphaBalance::from(1_000_u64), - conviction: U64F64::from_num(0), - last_update: 1, - }, - ); - Lock::::insert( - (coldkey_2, netuid, hotkey), - LockState { - locked_mass: AlphaBalance::from(2_000_u64), - conviction: U64F64::from_num(0), - last_update: 1, - }, - ); - Lock::::insert( - (coldkey_1, netuid, expired_hotkey), - LockState { - locked_mass: AlphaBalance::ZERO, - conviction: U64F64::from_num(1), - last_update: 1, - }, - ); - - assert_eq!( - LockingColdkeys::::iter_prefix((netuid, hotkey)).count(), - 0 - ); - assert_eq!( - LockingColdkeys::::iter_prefix((netuid, expired_hotkey)).count(), - 0 - ); - assert!(!HasMigrationRun::::get(MIGRATION_NAME.to_vec())); - - let weight = - crate::migrations::migrate_populate_locking_coldkeys::migrate_populate_locking_coldkeys::(); - - assert!(!weight.is_zero(), "migration weight should be non-zero"); - assert!(LockingColdkeys::::contains_key(( - netuid, hotkey, coldkey_1 - ))); - assert!(LockingColdkeys::::contains_key(( - netuid, hotkey, coldkey_2 - ))); - assert_eq!( - LockingColdkeys::::iter_prefix((netuid, hotkey)).count(), - 2 - ); - assert_eq!( - LockingColdkeys::::iter_prefix((netuid, expired_hotkey)).count(), - 0 - ); - assert!(Lock::::get((coldkey_1, netuid, expired_hotkey)).is_none()); - assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); - - let _ = LockingColdkeys::::clear_prefix((netuid, hotkey), u32::MAX, None); - let second_weight = - crate::migrations::migrate_populate_locking_coldkeys::migrate_populate_locking_coldkeys::(); - - assert_eq!( - second_weight, - ::DbWeight::get().reads(1), - "second run should only read the migration flag" - ); - assert_eq!( - LockingColdkeys::::iter_prefix((netuid, hotkey)).count(), - 0 - ); - }); -} - -#[test] -fn test_migrate_populate_locking_coldkeys_removes_dust_from_aggregate() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let coldkey_1 = U256::from(1101); - let coldkey_2 = U256::from(1102); - let hotkey = U256::from(2101); - let dust_lock = LockState { - locked_mass: AlphaBalance::from(60_u64), - conviction: U64F64::from_num(0), - last_update: 1, - }; - - DecayingLock::::insert(coldkey_1, netuid, false); - DecayingLock::::insert(coldkey_2, netuid, false); - Lock::::insert((coldkey_1, netuid, hotkey), dust_lock.clone()); - Lock::::insert((coldkey_2, netuid, hotkey), dust_lock); - HotkeyLock::::insert( - netuid, - hotkey, - LockState { - locked_mass: AlphaBalance::from(120_u64), - conviction: U64F64::from_num(0), - last_update: 1, - }, - ); - - crate::migrations::migrate_populate_locking_coldkeys::migrate_populate_locking_coldkeys::< - Test, - >(); - - assert!(Lock::::get((coldkey_1, netuid, hotkey)).is_none()); - assert!(Lock::::get((coldkey_2, netuid, hotkey)).is_none()); - assert!(HotkeyLock::::get(netuid, hotkey).is_none()); - assert_eq!( - LockingColdkeys::::iter_prefix((netuid, hotkey)).count(), - 0 - ); - }); -} - -#[test] -fn test_migrate_fix_staking_hot_keys() { - new_test_ext(1).execute_with(|| { - const MIGRATION_NAME: &[u8] = b"migrate_fix_staking_hot_keys"; - - assert!( - !HasMigrationRun::::get(MIGRATION_NAME.to_vec()), - "Migration should not have run yet" - ); - - // Add some data - Alpha::::insert( - (U256::from(1), U256::from(2), NetUid::ROOT), - U64F64::from(1_u64), - ); - // Run migration - let weight = - migrations::migrate_fix_staking_hot_keys::migrate_fix_staking_hot_keys::(); - - assert!( - HasMigrationRun::::get(MIGRATION_NAME.to_vec()), - "Migration should be marked as completed" - ); - - // Check migration has been marked as run - assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); - - // Verify results - assert_eq!( - StakingHotkeys::::get(U256::from(2)), - vec![U256::from(1)] - ); - }); -} - -#[test] -fn test_migrate_fix_root_subnet_tao() { - new_test_ext(1).execute_with(|| { - const MIGRATION_NAME: &str = "migrate_fix_root_subnet_tao"; - - let mut expected_total_stake = 0_u64; - // Seed some hotkeys with some fake stake. - for i in 0..100_000 { - Owner::::insert(U256::from(U256::from(i)), U256::from(i + 1_000_000)); - let stake = i + 1_000_000; - TotalHotkeyAlpha::::insert( - U256::from(U256::from(i)), - NetUid::ROOT, - AlphaBalance::from(stake), - ); - expected_total_stake += stake; - } - - assert_eq!(SubnetTAO::::get(NetUid::ROOT), TaoBalance::ZERO); - assert!( - !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should not have run yet" - ); - - // Run the migration - let weight = - crate::migrations::migrate_fix_root_subnet_tao::migrate_fix_root_subnet_tao::(); - - // Verify the migration ran correctly - assert!( - HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should be marked as run" - ); - assert!(!weight.is_zero(), "Migration weight should be non-zero"); - assert_eq!( - SubnetTAO::::get(NetUid::ROOT), - expected_total_stake.into() - ); - }); -} - -// cargo test --package pallet-subtensor --lib -- tests::migration::test_migrate_fix_root_tao_and_alpha_in --exact --show-output -#[test] -fn test_migrate_fix_root_tao_and_alpha_in() { - new_test_ext(1).execute_with(|| { - const MIGRATION_NAME: &str = "migrate_fix_root_tao_and_alpha_in"; - - // Set counters initially - let initial_value = 1_000_000_000_000_u64; - SubnetTAO::::insert(NetUid::ROOT, TaoBalance::from(initial_value)); - SubnetAlphaIn::::insert(NetUid::ROOT, AlphaBalance::from(initial_value)); - SubnetAlphaOut::::insert(NetUid::ROOT, AlphaBalance::from(initial_value)); - SubnetVolume::::insert(NetUid::ROOT, initial_value as u128); - TotalStake::::set(TaoBalance::from(initial_value)); - - assert!( - !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should not have run yet" - ); - - // Run the migration - let weight = - crate::migrations::migrate_fix_root_tao_and_alpha_in::migrate_fix_root_tao_and_alpha_in::(); - - // Verify the migration ran correctly - assert!( - HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should be marked as run" - ); - assert!(!weight.is_zero(), "Migration weight should be non-zero"); - - // Verify counters have changed - assert!(SubnetTAO::::get(NetUid::ROOT) != initial_value.into()); - assert!(SubnetAlphaIn::::get(NetUid::ROOT) != initial_value.into()); - assert!(SubnetAlphaOut::::get(NetUid::ROOT) != initial_value.into()); - assert!(SubnetVolume::::get(NetUid::ROOT) != initial_value as u128); - assert!(TotalStake::::get() != initial_value.into()); - }); -} - -#[test] -fn test_migrate_subnet_symbols() { - new_test_ext(1).execute_with(|| { - const MIGRATION_NAME: &str = "migrate_subnet_symbols"; - - // Create 100 subnets - for i in 0..100 { - add_network(i.into(), 1, 0); - } - - // Shift some symbols - TokenSymbol::::insert( - NetUid::from(21), - SubtensorModule::get_symbol_for_subnet(NetUid::from(142)), - ); - TokenSymbol::::insert( - NetUid::from(42), - SubtensorModule::get_symbol_for_subnet(NetUid::from(184)), - ); - TokenSymbol::::insert( - NetUid::from(83), - SubtensorModule::get_symbol_for_subnet(NetUid::from(242)), - ); - TokenSymbol::::insert( - NetUid::from(99), - SubtensorModule::get_symbol_for_subnet(NetUid::from(284)), - ); - - // Run the migration - let weight = crate::migrations::migrate_subnet_symbols::migrate_subnet_symbols::(); - - // Check that the symbols have been corrected - assert_eq!( - TokenSymbol::::get(NetUid::from(21)), - SubtensorModule::get_symbol_for_subnet(NetUid::from(21)) - ); - assert_eq!( - TokenSymbol::::get(NetUid::from(42)), - SubtensorModule::get_symbol_for_subnet(NetUid::from(42)) - ); - assert_eq!( - TokenSymbol::::get(NetUid::from(83)), - SubtensorModule::get_symbol_for_subnet(NetUid::from(83)) - ); - assert_eq!( - TokenSymbol::::get(NetUid::from(99)), - SubtensorModule::get_symbol_for_subnet(NetUid::from(99)) - ); - - assert!(!weight.is_zero(), "Migration weight should be non-zero"); - }); -} - -#[test] -fn test_migrate_set_registration_enable() { - new_test_ext(1).execute_with(|| { - const MIGRATION_NAME: &str = "migrate_set_registration_enable"; - - // Create 3 subnets - let netuids: [NetUid; 3] = [1.into(), 2.into(), 3.into()]; - for netuid in netuids.iter() { - add_network(*netuid, 1, 0); - // Set registration to false to simulate the need for migration - SubtensorModule::set_network_registration_allowed(*netuid, false); - } - - // Sanity check: registration is disabled before migration - for netuid in netuids.iter() { - assert!(!SubtensorModule::get_network_registration_allowed(*netuid)); - } - - // Run the migration - let weight = - crate::migrations::migrate_set_registration_enable::migrate_set_registration_enable::< - Test, - >(); - - // After migration, regular registration should be enabled for all subnets except root - for netuid in netuids.iter() { - assert!(SubtensorModule::get_network_registration_allowed(*netuid)); - } - - // Migration should be marked as run - assert!(HasMigrationRun::::get( - MIGRATION_NAME.as_bytes().to_vec() - )); - - // Weight should be non-zero - assert!(!weight.is_zero(), "Migration weight should be non-zero"); - }); -} - -#[test] -fn test_migrate_set_nominator_min_stake() { - new_test_ext(1).execute_with(|| { - const MIGRATION_NAME: &str = "migrate_set_nominator_min_stake"; - - let min_nomination_initial = 100_000_000; - let min_nomination_migrated = 10_000_000; - NominatorMinRequiredStake::::set(min_nomination_initial); - - assert_eq!( - NominatorMinRequiredStake::::get(), - min_nomination_initial - ); - assert!( - !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should not have run yet" - ); - - // Run the migration - let weight = - crate::migrations::migrate_set_nominator_min_stake::migrate_set_nominator_min_stake::< - Test, - >(); - - // Verify the migration ran correctly - assert!( - HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should be marked as run" - ); - assert!(!weight.is_zero(), "Migration weight should be non-zero"); - assert_eq!( - NominatorMinRequiredStake::::get(), - min_nomination_migrated - ); - }); -} - -#[test] -fn test_migrate_crv3_commits_add_block() { - new_test_ext(1).execute_with(|| { - // ------------------------------ - // 0. Constants / helpers - // ------------------------------ - const MIG_NAME: &[u8] = b"crv3_commits_add_block_v1"; - let netuid = NetUid::from(99); - let epoch: u64 = 7; - let tempo: u16 = 360; - - // ------------------------------ - // 1. Create a network so helper can compute first‑block - // ------------------------------ - add_network(netuid, tempo, 0); - - // ------------------------------ - // 2. Simulate OLD storage (3‑tuple) - // ------------------------------ - let who: U256 = U256::from(0xdeadbeef_u64); - let ciphertext: BoundedVec> = - vec![1u8, 2, 3].try_into().unwrap(); - let round: RoundNumber = 42; - - let old_queue: VecDeque<_> = VecDeque::from(vec![(who, ciphertext.clone(), round)]); - - CRV3WeightCommits::::insert( - NetUidStorageIndex::from(netuid), - epoch, - old_queue.clone(), - ); - - // Sanity: entry decodes under old alias - assert_eq!( - CRV3WeightCommits::::get(NetUidStorageIndex::from(netuid), epoch), - old_queue - ); - - assert!( - !HasMigrationRun::::get(MIG_NAME.to_vec()), - "migration flag should be false before run" - ); - - // ------------------------------ - // 3. Run migration - // ------------------------------ - let w = crate::migrations::migrate_crv3_commits_add_block::migrate_crv3_commits_add_block::< - Test, - >(); - assert!(!w.is_zero(), "weight must be non-zero"); - - // ------------------------------ - // 4. Verify results - // ------------------------------ - assert!( - HasMigrationRun::::get(MIG_NAME.to_vec()), - "migration flag not set" - ); - - // Old storage must be empty (drained) - assert!( - CRV3WeightCommits::::get(NetUidStorageIndex::from(netuid), epoch).is_empty(), - "old queue should have been drained" - ); - - let new_q = CRV3WeightCommitsV2::::get(NetUidStorageIndex::from(netuid), epoch); - assert_eq!(new_q.len(), 1, "exactly one migrated element expected"); - - let (who2, commit_block, cipher2, round2) = new_q.front().cloned().unwrap(); - assert_eq!(who2, who); - assert_eq!(cipher2, ciphertext); - assert_eq!(round2, round); - - let expected_block = Pallet::::get_first_block_of_epoch(netuid, epoch); - assert_eq!( - commit_block, expected_block, - "commit_block should equal first block of epoch key" - ); - }); -} - -#[test] -fn test_migrate_disable_commit_reveal() { - const MIG_NAME: &[u8] = b"disable_commit_reveal_v1"; - let netuids = [NetUid::from(1), NetUid::from(2), NetUid::from(42)]; - - // --------------------------------------------------------------------- - // 1. build initial state ─ all nets enabled - // --------------------------------------------------------------------- - new_test_ext(1).execute_with(|| { - for (i, netuid) in netuids.iter().enumerate() { - add_network(*netuid, 5u16 + i as u16, 0); - CommitRevealWeightsEnabled::::insert(*netuid, true); - } - assert!( - !HasMigrationRun::::get(MIG_NAME), - "migration flag should be unset before run" - ); - - // ----------------------------------------------------------------- - // 2. run migration - // ----------------------------------------------------------------- - let w = crate::migrations::migrate_disable_commit_reveal::migrate_disable_commit_reveal::< - Test, - >(); - - assert!( - HasMigrationRun::::get(MIG_NAME), - "migration flag not set" - ); - - // ----------------------------------------------------------------- - // 3. verify every netuid is now disabled and only one value exists - // ----------------------------------------------------------------- - for netuid in netuids { - assert!( - !CommitRevealWeightsEnabled::::get(netuid), - "commit-reveal should be disabled for netuid {netuid}" - ); - } - - // There should be no stray keys - let collected: Vec<_> = CommitRevealWeightsEnabled::::iter().collect(); - assert_eq!(collected.len(), netuids.len(), "unexpected key count"); - for (k, v) in collected { - assert!(!v, "found an enabled flag after migration for netuid {k}"); - } - - // ----------------------------------------------------------------- - // 4. running again should be a no-op - // ----------------------------------------------------------------- - let w2 = crate::migrations::migrate_disable_commit_reveal::migrate_disable_commit_reveal::< - Test, - >(); - assert_eq!( - w2, - ::DbWeight::get().reads(1), - "second run should read the flag and do nothing else" - ); - }); -} - -#[test] -fn test_migrate_commit_reveal_settings() { - new_test_ext(1).execute_with(|| { - const MIGRATION_NAME: &str = "migrate_commit_reveal_settings"; - - // Set up some networks first - let netuid1: u16 = 1; - let netuid2: u16 = 2; - // Add networks to simulate existing networks - add_network(netuid1.into(), 1, 0); - add_network(netuid2.into(), 1, 0); - - // Ensure the storage items use default values initially (but aren't explicitly set) - // Since these are ValueQuery storage items, they return defaults even when not set - assert_eq!(RevealPeriodEpochs::::get(NetUid::from(netuid1)), 1u64); - assert_eq!(RevealPeriodEpochs::::get(NetUid::from(netuid2)), 1u64); - assert!(CommitRevealWeightsEnabled::::get(NetUid::from(netuid1))); - assert!(CommitRevealWeightsEnabled::::get(NetUid::from(netuid2))); - - // Check migration hasn't run - assert!(!HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec())); - - // Run migration - let weight = crate::migrations::migrate_commit_reveal_settings::migrate_commit_reveal_settings::(); - - // Check migration has been marked as run - assert!(HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec())); - - // Verify RevealPeriodEpochs was set correctly - assert_eq!(RevealPeriodEpochs::::get(NetUid::from(netuid1)), 1u64); - assert_eq!(RevealPeriodEpochs::::get(NetUid::from(netuid2)), 1u64); - - // Verify CommitRevealWeightsEnabled was set correctly - assert!(CommitRevealWeightsEnabled::::get(NetUid::from(netuid1))); - assert!(CommitRevealWeightsEnabled::::get(NetUid::from(netuid2))); - }); -} - -#[test] -fn test_migrate_commit_reveal_settings_already_run() { - new_test_ext(1).execute_with(|| { - const MIGRATION_NAME: &str = "migrate_commit_reveal_settings"; - // Mark migration as already run - HasMigrationRun::::insert(MIGRATION_NAME.as_bytes().to_vec(), true); - - // Run migration - let weight = crate::migrations::migrate_commit_reveal_settings::migrate_commit_reveal_settings::(); - - // Should only have read weight for checking migration status - let expected_weight = ::DbWeight::get().reads(1); - assert_eq!(weight, expected_weight); - }); -} - -#[test] -fn test_migrate_commit_reveal_settings_no_networks() { - new_test_ext(1).execute_with(|| { - const MIGRATION_NAME: &str = "migrate_commit_reveal_settings"; - - // Check migration hasn't run - assert!(!HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec())); - - // Run migration - let weight = crate::migrations::migrate_commit_reveal_settings::migrate_commit_reveal_settings::(); - - // Check migration has been marked as run - assert!(HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec())); - - // Check that weight calculation is correct (no networks, so no additional reads/writes) - // 1 read for migration check + 0 reads for networks + 0 writes for storage + 1 write for migration flag - let expected_weight = ::DbWeight::get().reads(1) + ::DbWeight::get().writes(1); - assert_eq!(weight, expected_weight); - }); -} - -#[test] -fn test_migrate_commit_reveal_settings_multiple_networks() { - new_test_ext(1).execute_with(|| { - const MIGRATION_NAME: &str = "migrate_commit_reveal_settings"; - - // Set up multiple networks - let netuids = vec![1u16, 2u16, 3u16, 10u16, 42u16]; - for netuid in &netuids { - add_network((*netuid).into(), 1, 0); - } - - // Run migration - let weight = crate::migrations::migrate_commit_reveal_settings::migrate_commit_reveal_settings::(); - - // Verify all networks have correct settings - for netuid in &netuids { - assert_eq!(RevealPeriodEpochs::::get(NetUid::from(*netuid)), 1u64); - assert!(CommitRevealWeightsEnabled::::get(NetUid::from(*netuid))); - } - - // Check migration has been marked as run - assert!(HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec())); - }); -} - -#[test] -fn test_migrate_commit_reveal_settings_values_access() { - new_test_ext(1).execute_with(|| { - let netuid: u16 = 1; - add_network(netuid.into(), 1, 0); - - // Run migration - crate::migrations::migrate_commit_reveal_settings::migrate_commit_reveal_settings::(); - - // Test that we can access the values using the pallet functions - assert_eq!( - SubtensorModule::get_reveal_period(NetUid::from(netuid)), - 1u64 - ); - - // Test direct storage access - assert_eq!(RevealPeriodEpochs::::get(NetUid::from(netuid)), 1u64); - assert!(CommitRevealWeightsEnabled::::get(NetUid::from( - netuid - ))); - }); -} - -#[test] -fn test_migrate_auto_stake_destination() { - new_test_ext(1).execute_with(|| { - // ------------------------------ - // Step 1: Simulate Old Storage Entries - // ------------------------------ - const MIGRATION_NAME: &[u8] = b"migrate_auto_stake_destination"; - let netuids = [NetUid::ROOT, NetUid::from(1), NetUid::from(2), NetUid::from(42)]; - for netuid in &netuids { - NetworksAdded::::insert(*netuid, true); - } - - let pallet_prefix = twox_128("SubtensorModule".as_bytes()); - let storage_prefix = twox_128("AutoStakeDestination".as_bytes()); - - // Create test accounts - let coldkey1: U256 = U256::from(1); - let coldkey2: U256 = U256::from(2); - let hotkey1: U256 = U256::from(100); - let hotkey2: U256 = U256::from(200); - - // Construct storage keys for old format (StorageMap) - let mut key1 = Vec::new(); - key1.extend_from_slice(&pallet_prefix); - key1.extend_from_slice(&storage_prefix); - key1.extend_from_slice(&Blake2_128Concat::hash(&coldkey1.encode())); - - let mut key2 = Vec::new(); - key2.extend_from_slice(&pallet_prefix); - key2.extend_from_slice(&storage_prefix); - key2.extend_from_slice(&Blake2_128Concat::hash(&coldkey2.encode())); - - // Store old format entries - put_raw(&key1, &hotkey1.encode()); - put_raw(&key2, &hotkey2.encode()); - - // Verify old entries are stored - assert_eq!(get_raw(&key1), Some(hotkey1.encode())); - assert_eq!(get_raw(&key2), Some(hotkey2.encode())); - - assert!( - !HasMigrationRun::::get(MIGRATION_NAME.to_vec()), - "Migration should not have run yet" - ); - - // ------------------------------ - // Step 2: Run the Migration - // ------------------------------ - let weight = crate::migrations::migrate_auto_stake_destination::migrate_auto_stake_destination::(); - - assert!( - HasMigrationRun::::get(MIGRATION_NAME.to_vec()), - "Migration should be marked as run" - ); - - // ------------------------------ - // Step 3: Verify Migration Effects - // ------------------------------ - - // Verify new format entries exist - for netuid in &netuids { - if *netuid == NetUid::ROOT { - assert_eq!( - AutoStakeDestination::::get(coldkey1, NetUid::ROOT), - None - ); - assert_eq!( - AutoStakeDestination::::get(coldkey2, NetUid::ROOT), - None - ); - } else { - assert_eq!( - AutoStakeDestination::::get(coldkey1, *netuid), - Some(hotkey1) - ); - assert_eq!( - AutoStakeDestination::::get(coldkey2, *netuid), - Some(hotkey2) - ); - - // Verify entry for AutoStakeDestinationColdkeys - assert_eq!( - AutoStakeDestinationColdkeys::::get(hotkey1, *netuid), - vec![coldkey1] - ); - assert_eq!( - AutoStakeDestinationColdkeys::::get(hotkey2, *netuid), - vec![coldkey2] - ); - } - } - - // Verify old format entries are cleared - assert_eq!(get_raw(&key1), None, "Old storage entry 1 should be cleared"); - assert_eq!(get_raw(&key2), None, "Old storage entry 2 should be cleared"); - - // Verify weight calculation - assert!(!weight.is_zero(), "Migration weight should be non-zero"); - - // ------------------------------ - // Step 4: Test Migration Idempotency - // ------------------------------ - let weight_second_run = crate::migrations::migrate_auto_stake_destination::migrate_auto_stake_destination::(); - - // Second run should only read the migration flag - assert_eq!( - weight_second_run, - ::DbWeight::get().reads(1), - "Second run should only read the migration flag" - ); - }); -} - -#[test] -fn test_migrate_crv3_v2_to_timelocked() { - new_test_ext(1).execute_with(|| { - // ------------------------------ - // 0. Constants / helpers - // ------------------------------ - const MIG_NAME: &[u8] = b"crv3_v2_to_timelocked_v1"; - let netuid = NetUid::from(99); - let epoch: u64 = 7; - - // ------------------------------ - // 1. Simulate OLD storage (4‑tuple; V2 layout) - // ------------------------------ - let who: U256 = U256::from(0xdeadbeef_u64); - let commit_block: u64 = 12345; - let ciphertext: BoundedVec> = - vec![1u8, 2, 3].try_into().unwrap(); - let round: RoundNumber = 9; - - let old_queue: VecDeque<_> = - VecDeque::from(vec![(who, commit_block, ciphertext.clone(), round)]); - - // Insert under the deprecated alias - CRV3WeightCommitsV2::::insert( - NetUidStorageIndex::from(netuid), - epoch, - old_queue.clone(), - ); - - // Sanity: entry decodes under old alias - assert_eq!( - CRV3WeightCommitsV2::::get(NetUidStorageIndex::from(netuid), epoch), - old_queue, - "pre-migration: old queue should be present" - ); - - // Destination should be empty pre-migration - assert!( - TimelockedWeightCommits::::get(NetUidStorageIndex::from(netuid), epoch) - .is_empty(), - "pre-migration: destination should be empty" - ); - - assert!( - !HasMigrationRun::::get(MIG_NAME.to_vec()), - "migration flag should be false before run" - ); - - // ------------------------------ - // 2. Run migration - // ------------------------------ - let w = crate::migrations::migrate_crv3_v2_to_timelocked::migrate_crv3_v2_to_timelocked::< - Test, - >(); - assert!(!w.is_zero(), "weight must be non-zero"); - - // ------------------------------ - // 3. Verify results - // ------------------------------ - assert!( - HasMigrationRun::::get(MIG_NAME.to_vec()), - "migration flag not set" - ); - - // Old storage must be empty (drained) - assert!( - CRV3WeightCommitsV2::::get(NetUidStorageIndex::from(netuid), epoch).is_empty(), - "old queue should have been drained" - ); - - // New storage must match exactly - let new_q = TimelockedWeightCommits::::get(NetUidStorageIndex::from(netuid), epoch); - assert_eq!( - new_q, old_queue, - "migrated queue must exactly match the old queue" - ); - - // Verify the front element matches what we inserted - let (who2, commit_block2, cipher2, round2) = new_q.front().cloned().unwrap(); - assert_eq!(who2, who); - assert_eq!(commit_block2, commit_block); - assert_eq!(cipher2, ciphertext); - assert_eq!(round2, round); - }); -} - -#[test] -fn test_migrate_remove_network_modality() { - new_test_ext(1).execute_with(|| { - // ------------------------------ - // 0. Constants / helpers - // ------------------------------ - const MIGRATION_NAME: &str = "migrate_remove_network_modality"; - - // Create multiple networks to test - let netuids: [NetUid; 3] = [1.into(), 2.into(), 3.into()]; - for netuid in netuids.iter() { - add_network(*netuid, 1, 0); - } - - // Set initial storage version to 7 (below target) - StorageVersion::new(7).put::>(); - assert_eq!( - Pallet::::on_chain_storage_version(), - StorageVersion::new(7) - ); - - // ------------------------------ - // 1. Simulate NetworkModality entries using deprecated storage alias - // ------------------------------ - // We need to manually create storage entries that would exist for NetworkModality - // Since NetworkModality was a StorageMap<_, Identity, NetUid, u16>, we simulate this - let pallet_prefix = twox_128("SubtensorModule".as_bytes()); - let storage_prefix = twox_128("NetworkModality".as_bytes()); - - // Create NetworkModality entries for each network - for (i, netuid) in netuids.iter().enumerate() { - let mut key = Vec::new(); - key.extend_from_slice(&pallet_prefix); - key.extend_from_slice(&storage_prefix); - // Identity encoding for netuid - key.extend_from_slice(&netuid.encode()); - - let modality_value: u16 = (i as u16) + 1; // Different values for testing - put_raw(&key, &modality_value.encode()); - - // Verify the entry was created - let stored_value = get_raw(&key).expect("NetworkModality entry should exist"); - assert_eq!( - u16::decode(&mut &stored_value[..]).expect("Failed to decode modality"), - modality_value - ); - } - - assert!( - !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should not have run yet" - ); - - // ------------------------------ - // 2. Run migration - // ------------------------------ - let weight = - crate::migrations::migrate_remove_network_modality::migrate_remove_network_modality::< - Test, - >(); - - // ------------------------------ - // 3. Verify migration effects - // ------------------------------ - assert!( - HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), - "Migration should be marked as run" - ); - - // Verify weight is non-zero - assert!(!weight.is_zero(), "Migration weight should be non-zero"); - - // Verify weight calculation: 1 read (version check) + 1 read (total networks) + N writes (removal) + 1 write (version update) - let expected_weight = ::DbWeight::get().reads(2) - + ::DbWeight::get().writes(netuids.len() as u64 + 1); - assert_eq!( - weight, expected_weight, - "Weight calculation should be correct" - ); - }); -} - -#[test] -fn test_migrate_remove_network_modality_already_run() { - new_test_ext(1).execute_with(|| { - const MIGRATION_NAME: &str = "migrate_remove_network_modality"; - - // Mark migration as already run - HasMigrationRun::::insert(MIGRATION_NAME.as_bytes().to_vec(), true); - - // Set storage version to 8 (target version) - StorageVersion::new(8).put::>(); - assert_eq!( - Pallet::::on_chain_storage_version(), - StorageVersion::new(8) - ); - - // Run migration - let weight = - crate::migrations::migrate_remove_network_modality::migrate_remove_network_modality::< - Test, - >(); - - // Should only have read weight for checking migration status - let expected_weight = ::DbWeight::get().reads(1); - assert_eq!( - weight, expected_weight, - "Second run should only read the migration flag" - ); - - // Verify migration is still marked as run - assert!(HasMigrationRun::::get( - MIGRATION_NAME.as_bytes().to_vec() - )); - }); -} - -#[test] -fn test_migrate_subnet_limit_to_default() { - new_test_ext(1).execute_with(|| { - // ------------------------------ - // 0. Constants / helpers - // ------------------------------ - const MIG_NAME: &[u8] = b"subnet_limit_to_default"; - - // Compute a non-default value safely - let default: u16 = DefaultSubnetLimit::::get(); - let not_default: u16 = default.wrapping_add(1); - - // ------------------------------ - // 1. Pre-state: ensure a non-default value is stored - // ------------------------------ - SubnetLimit::::put(not_default); - assert_eq!( - SubnetLimit::::get(), - not_default, - "precondition failed: SubnetLimit should be non-default before migration" - ); - - assert!( - !HasMigrationRun::::get(MIG_NAME.to_vec()), - "migration flag should be false before run" - ); - - // ------------------------------ - // 2. Run migration - // ------------------------------ - let w = crate::migrations::migrate_subnet_limit_to_default::migrate_subnet_limit_to_default::(); - assert!(!w.is_zero(), "weight must be non-zero"); - - // ------------------------------ - // 3. Verify results - // ------------------------------ - assert!( - HasMigrationRun::::get(MIG_NAME.to_vec()), - "migration flag not set" - ); - - assert_eq!( - SubnetLimit::::get(), - default, - "SubnetLimit should be reset to the configured default" - ); - }); -} - -#[test] -fn test_migrate_network_lock_reduction_interval_and_decay() { - new_test_ext(0).execute_with(|| { - const FOUR_DAYS: u64 = 28_800; - const EIGHT_DAYS: u64 = 57_600; - const ONE_WEEK_BLOCKS: u64 = 50_400; - - // ── pre ────────────────────────────────────────────────────────────── - assert!( - !HasMigrationRun::::get(b"migrate_network_lock_reduction_interval".to_vec()), - "HasMigrationRun should be false before migration" - ); - - // ensure current_block > 0 - step_block(1); - let current_block_before = Pallet::::get_current_block_as_u64(); - - // ── run migration ──────────────────────────────────────────────────── - let weight = crate::migrations::migrate_network_lock_reduction_interval::migrate_network_lock_reduction_interval::(); - assert!(!weight.is_zero(), "migration weight should be > 0"); - - // ── params & flags ─────────────────────────────────────────────────── - assert_eq!(NetworkLockReductionInterval::::get(), EIGHT_DAYS); - assert_eq!(NetworkRateLimit::::get(), FOUR_DAYS); - assert_eq!( - Pallet::::get_network_last_lock(), - 1_000_000_000_000u64.into(), // 1000 TAO in rao - "last_lock should be 1_000_000_000_000 rao" - ); - - // last_lock_block should be set one week in the future - let last_lock_block = Pallet::::get_network_last_lock_block(); - let expected_block = current_block_before + ONE_WEEK_BLOCKS; - assert_eq!( - last_lock_block, - expected_block, - "last_lock_block should be current + ONE_WEEK_BLOCKS" - ); - - // registration start block should match the same future block - assert_eq!( - NetworkRegistrationStartBlock::::get(), - expected_block, - "NetworkRegistrationStartBlock should equal last_lock_block" - ); - - // lock cost should be 2000 TAO immediately after migration - let lock_cost_now = Pallet::::get_network_lock_cost(); - assert_eq!( - lock_cost_now, - 2_000_000_000_000u64.into(), - "lock cost should be 2000 TAO right after migration" - ); - - assert!( - HasMigrationRun::::get(b"migrate_network_lock_reduction_interval".to_vec()), - "HasMigrationRun should be true after migration" - ); - }); -} - -#[test] -fn test_migrate_restore_subnet_locked_65_128() { - use sp_runtime::traits::SaturatedConversion; - new_test_ext(0).execute_with(|| { - let name = b"migrate_restore_subnet_locked".to_vec(); - assert!( - !HasMigrationRun::::get(name.clone()), - "HasMigrationRun should be false before migration" - ); - - // Expected snapshot for netuids 65..128. - const EXPECTED: &[(u16, u64)] = &[ - (65, 37_274_536_408), - (66, 65_230_444_016), - (67, 114_153_284_032), - (68, 199_768_252_064), - (69, 349_594_445_728), - (70, 349_412_366_216), - (71, 213_408_488_702), - (72, 191_341_473_067), - (73, 246_711_333_592), - (74, 291_874_466_228), - (75, 247_485_227_056), - (76, 291_241_991_316), - (77, 303_154_601_714), - (78, 287_407_417_932), - (79, 254_935_051_664), - (80, 255_413_055_349), - (81, 249_790_431_509), - (82, 261_343_249_180), - (83, 261_361_408_796), - (84, 201_938_003_214), - (85, 264_805_234_604), - (86, 223_171_973_880), - (87, 180_397_358_280), - (88, 270_596_039_760), - (89, 286_399_608_951), - (90, 267_684_201_301), - (91, 284_637_542_762), - (92, 288_373_410_868), - (93, 290_836_604_849), - (94, 270_861_792_144), - (95, 210_595_055_304), - (96, 315_263_727_200), - (97, 158_244_884_792), - (98, 168_102_223_900), - (99, 252_153_339_800), - (100, 378_230_014_000), - (101, 205_977_765_866), - (102, 149_434_017_849), - (103, 135_476_471_008), - (104, 147_970_415_680), - (105, 122_003_668_139), - (106, 133_585_556_570), - (107, 200_137_144_216), - (108, 106_767_623_816), - (109, 124_280_483_748), - (110, 186_420_726_696), - (111, 249_855_564_892), - (112, 196_761_272_984), - (113, 147_120_048_727), - (114, 84_021_895_534), - (115, 98_002_215_656), - (116, 89_944_262_256), - (117, 107_183_582_952), - (118, 110_644_724_664), - (119, 99_380_483_902), - (120, 138_829_019_156), - (121, 111_988_743_976), - (122, 130_264_686_152), - (123, 118_034_291_488), - (124, 79_312_501_676), - (125, 43_214_310_704), - (126, 64_755_449_962), - (127, 97_101_698_382), - (128, 145_645_807_991), - ]; - - // Run migration - let weight = - crate::migrations::migrate_subnet_locked::migrate_restore_subnet_locked::(); - assert!(!weight.is_zero(), "migration weight should be > 0"); - - // Read back storage as (u16 -> u64) - let actual: BTreeMap = SubnetLocked::::iter() - .map(|(k, v)| (k.saturated_into::(), u64::from(v))) - .collect(); - - let expected: BTreeMap = EXPECTED.iter().copied().collect(); - - // 1) exact content - assert_eq!( - actual, expected, - "SubnetLocked map mismatch for 65..128 snapshot" - ); - - // 2) count and total - let expected_len = expected.len(); - let expected_sum: u128 = expected.values().map(|v| *v as u128).sum(); - - let count_after = actual.len(); - let sum_after: u128 = actual.values().map(|v| *v as u128).sum(); - - assert_eq!(count_after, expected_len, "entry count mismatch"); - assert_eq!(sum_after, expected_sum, "total RAO sum mismatch"); - - // 3) migration flag set - assert!( - HasMigrationRun::::get(name.clone()), - "HasMigrationRun should be true after migration" - ); - - // 4) idempotence - let before = actual.clone(); - let _again = - crate::migrations::migrate_subnet_locked::migrate_restore_subnet_locked::(); - let after: BTreeMap = SubnetLocked::::iter() - .map(|(k, v)| (k.saturated_into::(), u64::from(v))) - .collect(); - assert_eq!( - before, after, - "re-running the migration should not change storage" - ); - }); -} - -#[test] -fn test_migrate_network_lock_cost_2500_sets_price_and_decay() { - new_test_ext(0).execute_with(|| { - // ── constants ─────────────────────────────────────────────────────── - const RAO_PER_TAO: u64 = 1_000_000_000; - const TARGET_COST_TAO: u64 = 2_500; - const TARGET_COST_RAO: u64 = TARGET_COST_TAO * RAO_PER_TAO; - const NEW_LAST_LOCK_RAO: u64 = (TARGET_COST_TAO / 2) * RAO_PER_TAO; - - let migration_key = b"migrate_network_lock_cost_2500".to_vec(); - - // ── pre ────────────────────────────────────────────────────────────── - assert!( - !HasMigrationRun::::get(migration_key.clone()), - "HasMigrationRun should be false before migration" - ); - - // Ensure current_block > 0 so mult == 2 in get_network_lock_cost() - step_block(1); - let current_block_before = Pallet::::get_current_block_as_u64(); - - // Snapshot interval to ensure migration doesn't change it - let interval_before = NetworkLockReductionInterval::::get(); - - // ── run migration ──────────────────────────────────────────────────── - let weight = crate::migrations::migrate_network_lock_cost_2500::migrate_network_lock_cost_2500::(); - assert!(!weight.is_zero(), "migration weight should be > 0"); - - // ── asserts: params & flags ───────────────────────────────────────── - assert_eq!( - Pallet::::get_network_last_lock(), - NEW_LAST_LOCK_RAO.into(), - "last_lock should be set to 1,250 TAO (in rao)" - ); - assert_eq!( - Pallet::::get_network_last_lock_block(), - current_block_before, - "last_lock_block should be set to the current block" - ); - - // Lock cost should be exactly 2,500 TAO immediately after migration - let lock_cost_now = Pallet::::get_network_lock_cost(); - assert_eq!( - lock_cost_now, - TARGET_COST_RAO.into(), - "lock cost should be 2,500 TAO right after migration" - ); - - // Interval should be unchanged by this migration - assert_eq!( - NetworkLockReductionInterval::::get(), - interval_before, - "lock reduction interval should not be modified by this migration" - ); - - assert!( - HasMigrationRun::::get(migration_key.clone()), - "HasMigrationRun should be true after migration" - ); - - // ── decay check (1 block later) ───────────────────────────────────── - // Expected: cost = max(min_lock, 2*L - floor(L / eff_interval) * delta_blocks) - let eff_interval = Pallet::::get_lock_reduction_interval(); - let per_block_decrement: u64 = if eff_interval == 0 { - 0 - } else { - NEW_LAST_LOCK_RAO / eff_interval - }; - - let min_lock_rao: u64 = Pallet::::get_network_min_lock().to_u64(); - - step_block(1); - let expected_after_1: u64 = - core::cmp::max(min_lock_rao, TARGET_COST_RAO - per_block_decrement); - let lock_cost_after_1 = Pallet::::get_network_lock_cost(); - assert_eq!( - lock_cost_after_1, - expected_after_1.into(), - "lock cost should decay by one per-block step after 1 block" - ); - - // ── idempotency: running the migration again should do nothing ────── - let last_lock_before_rerun = Pallet::::get_network_last_lock(); - let last_lock_block_before_rerun = Pallet::::get_network_last_lock_block(); - let cost_before_rerun = Pallet::::get_network_lock_cost(); - - let _weight2 = crate::migrations::migrate_network_lock_cost_2500::migrate_network_lock_cost_2500::(); - - assert!( - HasMigrationRun::::get(migration_key.clone()), - "HasMigrationRun remains true on second run" - ); - assert_eq!( - Pallet::::get_network_last_lock(), - last_lock_before_rerun, - "second run should not modify last_lock" - ); - assert_eq!( - Pallet::::get_network_last_lock_block(), - last_lock_block_before_rerun, - "second run should not modify last_lock_block" - ); - assert_eq!( - Pallet::::get_network_lock_cost(), - cost_before_rerun, - "second run should not change current lock cost" - ); - }); -} - -#[test] -fn test_migrate_kappa_map_to_default() { - new_test_ext(1).execute_with(|| { - // ------------------------------ - // 0. Constants / helpers - // ------------------------------ - const MIG_NAME: &[u8] = b"kappa_map_to_default"; - let default: u16 = DefaultKappa::::get(); - - let not_default: u16 = if default == u16::MAX { - default - 1 - } else { - default + 1 - }; - - // ------------------------------ - // 1. Pre-state: seed using the correct key type (NetUid) - // ------------------------------ - let n0: NetUid = 0u16.into(); - let n1: NetUid = 1u16.into(); - let n2: NetUid = 42u16.into(); - - Kappa::::insert(n0, not_default); - Kappa::::insert(n1, default); - Kappa::::insert(n2, not_default); - - assert_eq!( - Kappa::::get(n0), - not_default, - "precondition failed: Kappa[n0] should be non-default before migration" - ); - assert_eq!( - Kappa::::get(n1), - default, - "precondition failed: Kappa[n1] should be default before migration" - ); - assert_eq!( - Kappa::::get(n2), - not_default, - "precondition failed: Kappa[n2] should be non-default before migration" - ); - - assert!( - !HasMigrationRun::::get(MIG_NAME.to_vec()), - "migration flag should be false before run" - ); - - // ------------------------------ - // 2. Run migration - // ------------------------------ - let w = - crate::migrations::migrate_kappa_map_to_default::migrate_kappa_map_to_default::(); - assert!(!w.is_zero(), "weight must be non-zero"); - - // ------------------------------ - // 3. Verify results - // ------------------------------ - assert!( - HasMigrationRun::::get(MIG_NAME.to_vec()), - "migration flag not set" - ); - - assert_eq!( - Kappa::::get(n0), - default, - "Kappa[n0] should be reset to the configured default" - ); - assert_eq!( - Kappa::::get(n1), - default, - "Kappa[n1] should remain at the configured default" - ); - assert_eq!( - Kappa::::get(n2), - default, - "Kappa[n2] should be reset to the configured default" - ); - }); -} - -#[test] -fn test_migrate_remove_tao_dividends() { - const MIGRATION_NAME: &str = "migrate_remove_tao_dividends"; - let pallet_name = "SubtensorModule"; - let storage_name = "TaoDividendsPerSubnet"; - let migration = - crate::migrations::migrate_remove_tao_dividends::migrate_remove_tao_dividends::; - - test_remove_storage_item( - MIGRATION_NAME, - pallet_name, - storage_name, - migration, - 200_000, - ); - - let storage_name = "PendingAlphaSwapped"; - test_remove_storage_item( - MIGRATION_NAME, - pallet_name, - storage_name, - migration, - 200_000, - ); - - let storage_name = "PendingRootDivs"; - test_remove_storage_item( - MIGRATION_NAME, - pallet_name, - storage_name, - migration, - 200_000, - ); -} - -fn do_setup_unactive_sn() -> (Vec, Vec) { - // Register some subnets - let netuid0 = add_dynamic_network_without_emission_block(&U256::from(0), &U256::from(0)); - let netuid1 = add_dynamic_network_without_emission_block(&U256::from(1), &U256::from(1)); - let netuid2 = add_dynamic_network_without_emission_block(&U256::from(2), &U256::from(2)); - let inactive_netuids = vec![netuid0, netuid1, netuid2]; - // Add active subnets - let netuid3 = add_dynamic_network_without_emission_block(&U256::from(3), &U256::from(3)); - let netuid4 = add_dynamic_network_without_emission_block(&U256::from(4), &U256::from(4)); - let netuid5 = add_dynamic_network_without_emission_block(&U256::from(5), &U256::from(5)); - let active_netuids = vec![netuid3, netuid4, netuid5]; - let netuids: Vec = inactive_netuids - .iter() - .chain(active_netuids.iter()) - .copied() - .collect(); - - let initial_tao = Pallet::::get_network_min_lock(); - let initial_alpha: AlphaBalance = initial_tao.to_u64().into(); - - const EXTRA_POOL_TAO: u64 = 123_123_u64; - const EXTRA_POOL_ALPHA: u64 = 123_123_u64; - - // Add stake to the subnet pools - for netuid in &netuids { - let extra_for_pool = TaoBalance::from(EXTRA_POOL_TAO); - let stake_in_pool = TaoBalance::from( - u64::from(initial_tao) - .checked_add(EXTRA_POOL_TAO) - .expect("initial_tao + extra_for_pool overflow"), - ); - SubnetTAO::::insert(netuid, stake_in_pool); - TotalStake::::mutate(|total_stake| { - let updated_total = u64::from(*total_stake) - .checked_add(EXTRA_POOL_TAO) - .expect("total stake overflow"); - *total_stake = updated_total.into(); - }); - TotalIssuance::::mutate(|total_issuance| { - let updated_total = u64::from(*total_issuance) - .checked_add(EXTRA_POOL_TAO) - .expect("total issuance overflow"); - *total_issuance = updated_total.into(); - }); - - let subnet_alpha_in = AlphaBalance::from( - u64::from(initial_alpha) - .checked_add(EXTRA_POOL_ALPHA) - .expect("initial alpha + extra alpha overflow"), - ); - SubnetAlphaIn::::insert(netuid, subnet_alpha_in); - SubnetAlphaOut::::insert(netuid, AlphaBalance::from(EXTRA_POOL_ALPHA)); - SubnetVolume::::insert(netuid, 123123_u128); - - // Try registering on the subnet to simulate a real network - // give balance to the coldkey - let coldkey_account_id = U256::from(1111); - let hotkey_account_id = U256::from(1111); - let burn_cost = SubtensorModule::get_burn(*netuid); - // Registration requires keep-alive coverage above the burn (Preservation::Preserve). - let fund = TaoBalance::from( - u64::from(burn_cost) - .checked_add(u64::from(ExistentialDeposit::get())) - .and_then(|value| value.checked_add(10)) - .expect("burn funding overflow"), - ); - add_balance_to_coldkey_account(&coldkey_account_id, fund); - TotalIssuance::::mutate(|total_issuance| { - let updated_total = u64::from(*total_issuance) - .checked_add(u64::from(fund)) - .expect("total issuance overflow (burn)"); - *total_issuance = updated_total.into(); - }); - - // register the neuron - assert_ok!(SubtensorModule::burned_register( - <::RuntimeOrigin>::signed(coldkey_account_id), - *netuid, - hotkey_account_id - )); - } - - for netuid in &active_netuids { - // Set the FirstEmissionBlockNumber for the active subnet - FirstEmissionBlockNumber::::insert(netuid, 100); - // Also set SubtokenEnabled to true - SubtokenEnabled::::insert(netuid, true); - } - - let alpha_amt = AlphaBalance::from(123123_u64); - // Create some Stake entries - for netuid in &netuids { - for hotkey in 0..10 { - let hk = U256::from(hotkey); - TotalHotkeyAlpha::::insert(hk, netuid, alpha_amt); - TotalHotkeyShares::::insert(hk, netuid, U64F64::from(123123_u64)); - TotalHotkeyAlphaLastEpoch::::insert(hk, netuid, alpha_amt); - - RootClaimable::::mutate(hk, |claimable| { - claimable.insert(*netuid, I96F32::from(alpha_amt.to_u64())); - }); - for coldkey in 0..10 { - let ck = U256::from(coldkey); - Alpha::::insert((hk, ck, netuid), U64F64::from(123_u64)); - RootClaimed::::insert((netuid, hk, ck), 222_u128); - } - } - } - // Add some pending emissions - let alpha_em_amt = AlphaBalance::from(355555_u64); - for netuid in &netuids { - PendingServerEmission::::insert(netuid, alpha_em_amt); - PendingValidatorEmission::::insert(netuid, alpha_em_amt); - PendingRootAlphaDivs::::insert(netuid, alpha_em_amt); - PendingOwnerCut::::insert(netuid, alpha_em_amt); - - SubnetTaoInEmission::::insert(netuid, TaoBalance::from(12345678_u64)); - SubnetAlphaInEmission::::insert(netuid, AlphaBalance::from(12345678_u64)); - SubnetAlphaOutEmission::::insert(netuid, AlphaBalance::from(12345678_u64)); - } - - (active_netuids, inactive_netuids) -} - -#[test] -fn test_migrate_reset_unactive_sn_get_unactive_netuids() { - new_test_ext(1).execute_with(|| { - let (active_netuids, inactive_netuids) = do_setup_unactive_sn(); - - let initial_tao = Pallet::::get_network_min_lock(); - let initial_alpha: AlphaBalance = initial_tao.to_u64().into(); - - let (unactive_netuids, w) = - crate::migrations::migrate_reset_unactive_sn::get_unactive_sn_netuids::( - initial_alpha, - ); - // Make sure ALL the inactive subnets are in the unactive netuids - assert!( - inactive_netuids - .iter() - .all(|netuid| unactive_netuids.contains(netuid)) - ); - // Make sure the active subnets are not in the unactive netuids - assert!( - active_netuids - .iter() - .all(|netuid| !unactive_netuids.contains(netuid)) - ); - }); -} - -#[test] -fn test_migrate_reset_unactive_sn() { - new_test_ext(1).execute_with(|| { - use sp_std::collections::btree_map::BTreeMap; - - let (active_netuids, inactive_netuids) = do_setup_unactive_sn(); - - let initial_tao = Pallet::::get_network_min_lock(); - let initial_alpha: AlphaBalance = initial_tao.to_u64().into(); - - let mut locked_before: BTreeMap = BTreeMap::new(); - let mut rao_recycled_before: BTreeMap = BTreeMap::new(); - - for netuid in active_netuids.iter().chain(inactive_netuids.iter()) { - locked_before.insert(*netuid, SubnetLocked::::get(*netuid)); - rao_recycled_before.insert(*netuid, RAORecycledForRegistration::::get(netuid)); - } - - // Run the migration - let w = crate::migrations::migrate_reset_unactive_sn::migrate_reset_unactive_sn::(); - assert!(!w.is_zero(), "weight must be non-zero"); - - // Verify the results - for netuid in &inactive_netuids { - let netuid = *netuid; - - assert_eq!( - SubnetLocked::::get(netuid), - *locked_before.get(&netuid).unwrap(), - "SubnetLocked unexpectedly changed for inactive subnet {netuid:?}" - ); - assert_eq!( - RAORecycledForRegistration::::get(netuid), - *rao_recycled_before.get(&netuid).unwrap(), - "RAORecycledForRegistration unexpectedly changed for inactive subnet {netuid:?}" - ); - - assert_eq!( - PendingServerEmission::::get(netuid), - AlphaBalance::ZERO - ); - assert_eq!( - PendingValidatorEmission::::get(netuid), - AlphaBalance::ZERO - ); - assert_eq!( - PendingRootAlphaDivs::::get(netuid), - AlphaBalance::ZERO - ); - assert_eq!( - // not modified - RAORecycledForRegistration::::get(netuid), - *rao_recycled_before.get(&netuid).unwrap() - ); - assert_eq!(PendingOwnerCut::::get(netuid), AlphaBalance::ZERO); - assert_ne!(SubnetTAO::::get(netuid), initial_tao); - assert_ne!(SubnetAlphaIn::::get(netuid), initial_alpha); - assert_ne!(SubnetAlphaOut::::get(netuid), AlphaBalance::ZERO); - assert_eq!(SubnetTaoInEmission::::get(netuid), TaoBalance::ZERO); - assert_eq!( - SubnetAlphaInEmission::::get(netuid), - AlphaBalance::ZERO - ); - assert_eq!( - SubnetAlphaOutEmission::::get(netuid), - AlphaBalance::ZERO - ); - assert_ne!(SubnetVolume::::get(netuid), 0u128); - for hotkey in 0..10 { - let hk = U256::from(hotkey); - assert_ne!( - TotalHotkeyAlpha::::get(hk, netuid), - AlphaBalance::ZERO - ); - assert_ne!( - TotalHotkeyShares::::get(hk, netuid), - U64F64::from_num(0.0) - ); - assert_ne!( - TotalHotkeyAlphaLastEpoch::::get(hk, netuid), - AlphaBalance::ZERO - ); - assert_ne!(RootClaimable::::get(hk).get(&netuid), None); - for coldkey in 0..10 { - let ck = U256::from(coldkey); - assert_ne!(Alpha::::get((hk, ck, netuid)), U64F64::from_num(0.0)); - assert_ne!(RootClaimed::::get((netuid, hk, ck)), 0u128); - } - } - - // Don't touch SubnetLocked - assert_ne!(SubnetLocked::::get(netuid), TaoBalance::ZERO); - } - - // !!! Make sure the active subnets were not reset - for netuid in &active_netuids { - let netuid = *netuid; - - assert_eq!( - SubnetLocked::::get(netuid), - *locked_before.get(&netuid).unwrap(), - "SubnetLocked unexpectedly changed for active subnet {netuid:?}" - ); - assert_eq!( - RAORecycledForRegistration::::get(netuid), - *rao_recycled_before.get(&netuid).unwrap(), - "RAORecycledForRegistration unexpectedly changed for active subnet {netuid:?}" - ); - - assert_ne!( - PendingServerEmission::::get(netuid), - AlphaBalance::ZERO - ); - assert_ne!( - PendingValidatorEmission::::get(netuid), - AlphaBalance::ZERO - ); - assert_ne!( - PendingRootAlphaDivs::::get(netuid), - AlphaBalance::ZERO - ); - assert_eq!( - // unchanged (already asserted above via snapshot) - RAORecycledForRegistration::::get(netuid), - *rao_recycled_before.get(&netuid).unwrap() - ); - assert_ne!(SubnetTaoInEmission::::get(netuid), TaoBalance::ZERO); - assert_ne!( - SubnetAlphaInEmission::::get(netuid), - AlphaBalance::ZERO - ); - assert_ne!( - SubnetAlphaOutEmission::::get(netuid), - AlphaBalance::ZERO - ); - assert_ne!(PendingOwnerCut::::get(netuid), AlphaBalance::ZERO); - assert_ne!(SubnetTAO::::get(netuid), initial_tao); - assert_ne!(SubnetAlphaIn::::get(netuid), initial_alpha); - assert_ne!(SubnetAlphaOut::::get(netuid), AlphaBalance::ZERO); - assert_ne!(SubnetVolume::::get(netuid), 0u128); - for hotkey in 0..10 { - let hk = U256::from(hotkey); - assert_ne!( - TotalHotkeyAlpha::::get(hk, netuid), - AlphaBalance::ZERO - ); - assert_ne!( - TotalHotkeyShares::::get(hk, netuid), - U64F64::from_num(0.0) - ); - assert_ne!( - TotalHotkeyAlphaLastEpoch::::get(hk, netuid), - AlphaBalance::ZERO - ); - assert!(RootClaimable::::get(hk).contains_key(&netuid)); - for coldkey in 0..10 { - let ck = U256::from(coldkey); - assert_ne!(Alpha::::get((hk, ck, netuid)), U64F64::from_num(0.0)); - assert_ne!(RootClaimed::::get((netuid, hk, ck)), 0u128); - } - } - // Don't touch SubnetLocked - assert_ne!(SubnetLocked::::get(netuid), TaoBalance::ZERO); - } - }); -} - -#[test] -fn test_migrate_reset_unactive_sn_idempotence() { - new_test_ext(1).execute_with(|| { - let (active_netuids, inactive_netuids) = do_setup_unactive_sn(); - let netuids = inactive_netuids - .iter() - .chain(active_netuids.iter()) - .copied() - .collect::>(); - - // Run total issuance migration *before* running the migration. - crate::migrations::migrate_init_total_issuance::migrate_init_total_issuance::(); - - // Run the migration - let w = crate::migrations::migrate_reset_unactive_sn::migrate_reset_unactive_sn::(); - assert!(!w.is_zero(), "weight must be non-zero"); - - // Store the values after running the migration - let mut subnet_tao_before = BTreeMap::new(); - for netuid in &netuids { - subnet_tao_before.insert(netuid, SubnetTAO::::get(netuid)); - } - let total_stake_before = TotalStake::::get(); - let total_issuance_before = TotalIssuance::::get(); - - // Run total issuance migration again, to make sure no changes happen from it. - crate::migrations::migrate_init_total_issuance::migrate_init_total_issuance::(); - - // Verify that none of the values are different - for netuid in &netuids { - assert_eq!( - SubnetTAO::::get(netuid), - *subnet_tao_before.get(netuid).unwrap_or(&TaoBalance::ZERO) - ); - } - assert_eq!(TotalStake::::get(), total_stake_before); - assert_eq!(TotalIssuance::::get(), total_issuance_before); - }); -} - -fn test_migrate_remove_old_identity_maps() { - let migration = - crate::migrations::migrate_remove_old_identity_maps::migrate_remove_old_identity_maps::; - - const MIGRATION_NAME: &str = "migrate_remove_old_identity_maps"; - - let pallet_name = "SubtensorModule"; - - test_remove_storage_item(MIGRATION_NAME, pallet_name, "Identities", migration, 100); - - test_remove_storage_item( - MIGRATION_NAME, - pallet_name, - "SubnetIdentities", - migration, - 100, - ); - - test_remove_storage_item( - MIGRATION_NAME, - pallet_name, - "SubnetIdentitiesV2", - migration, - 100, - ); -} - -#[test] -fn test_migrate_remove_unknown_neuron_axon_cert_prom() { - use crate::migrations::migrate_remove_unknown_neuron_axon_cert_prom::*; - const MIGRATION_NAME: &[u8] = b"migrate_remove_neuron_axon_cert_prom"; - - new_test_ext(1).execute_with(|| { - setup_for(NetUid::from(2), 64, 1231); - setup_for(NetUid::from(42), 256, 15151); - setup_for(NetUid::from(99), 1024, 32323); - assert!(!HasMigrationRun::::get(MIGRATION_NAME)); - - let w = migrate_remove_unknown_neuron_axon_cert_prom::(); - assert!(!w.is_zero(), "Weight must be non-zero"); - - assert!(HasMigrationRun::::get(MIGRATION_NAME)); - assert_for(NetUid::from(2), 64, 1231); - assert_for(NetUid::from(42), 256, 15151); - assert_for(NetUid::from(99), 1024, 32323); - }); - - fn setup_for(netuid: NetUid, uids: u32, items: u32) { - NetworksAdded::::insert(netuid, true); - - for i in 1u32..=uids { - let hk = U256::from(netuid.inner() as u32 * 1000 + i); - Uids::::insert(netuid, hk, i as u16); - } - - for i in 1u32..=items { - let hk = U256::from(netuid.inner() as u32 * 1000 + i); - Axons::::insert(netuid, hk, AxonInfo::default()); - NeuronCertificates::::insert(netuid, hk, NeuronCertificate::default()); - Prometheus::::insert(netuid, hk, PrometheusInfo::default()); - } - } - - fn assert_for(netuid: NetUid, uids: u32, items: u32) { - assert_eq!( - Axons::::iter_key_prefix(netuid).count(), - uids as usize - ); - assert_eq!( - NeuronCertificates::::iter_key_prefix(netuid).count(), - uids as usize - ); - assert_eq!( - Prometheus::::iter_key_prefix(netuid).count(), - uids as usize - ); - - for i in 1u32..=uids { - let hk = U256::from(netuid.inner() as u32 * 1000 + i); - assert!(Axons::::contains_key(netuid, hk)); - assert!(NeuronCertificates::::contains_key(netuid, hk)); - assert!(Prometheus::::contains_key(netuid, hk)); - } - - for i in uids + 1u32..=items { - let hk = U256::from(netuid.inner() as u32 * 1000 + i); - assert!(!Axons::::contains_key(netuid, hk)); - assert!(!NeuronCertificates::::contains_key(netuid, hk)); - assert!(!Prometheus::::contains_key(netuid, hk)); - } - } -} - -// cargo test --package pallet-subtensor --lib -- tests::migration::test_migrate_cleanup_swap_v3 --exact --nocapture -#[test] -fn test_migrate_cleanup_swap_v3() { - use crate::migrations::migrate_cleanup_swap_v3::deprecated_swap_maps; - use substrate_fixed::types::U64F64; - - new_test_ext(1).execute_with(|| { - let migration = crate::migrations::migrate_cleanup_swap_v3::migrate_cleanup_swap_v3::; - - const MIGRATION_NAME: &str = "migrate_cleanup_swap_v3"; - - let provided: u64 = 9876; - let reserves: u64 = 1_000_000; - - SubnetTAO::::insert(NetUid::from(1), TaoBalance::from(reserves)); - SubnetAlphaIn::::insert(NetUid::from(1), AlphaBalance::from(reserves)); - - // Insert deprecated maps values - deprecated_swap_maps::SubnetTaoProvided::::insert( - NetUid::from(1), - TaoBalance::from(provided), - ); - deprecated_swap_maps::SubnetAlphaInProvided::::insert( - NetUid::from(1), - AlphaBalance::from(provided), - ); - - // Run migration - let weight = migration(); - - // Test that values are removed from state - assert!(!deprecated_swap_maps::SubnetTaoProvided::::contains_key(NetUid::from(1)),); - assert!( - !deprecated_swap_maps::SubnetAlphaInProvided::::contains_key(NetUid::from(1)), - ); - - // Provided got added to reserves - assert_eq!( - u64::from(SubnetTAO::::get(NetUid::from(1))), - reserves + provided - ); - assert_eq!( - u64::from(SubnetAlphaIn::::get(NetUid::from(1))), - reserves + provided - ); - }); -} - -// Regression test for issue #2793: migrate_cleanup_swap_v3 must be wired into the pallet -// on_runtime_upgrade hook. Seeds a *Provided residual, runs the full upgrade hook, and asserts -// the residual is folded into the main reserves. Without the wiring line in hooks.rs this fails. -#[test] -fn test_migrate_cleanup_swap_v3_runs_on_runtime_upgrade() { - use crate::migrations::migrate_cleanup_swap_v3::deprecated_swap_maps; - use frame_support::traits::Hooks; - - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let provided: u64 = 9876; - - deprecated_swap_maps::SubnetTaoProvided::::insert(netuid, TaoBalance::from(provided)); - deprecated_swap_maps::SubnetAlphaInProvided::::insert( - netuid, - AlphaBalance::from(provided), - ); - - let tao_before = u64::from(SubnetTAO::::get(netuid)); - let alpha_before = u64::from(SubnetAlphaIn::::get(netuid)); - - let _ = as Hooks>::on_runtime_upgrade(); - - assert!(!deprecated_swap_maps::SubnetTaoProvided::::contains_key(netuid)); - assert!(!deprecated_swap_maps::SubnetAlphaInProvided::::contains_key(netuid)); - assert_eq!( - u64::from(SubnetTAO::::get(netuid)), - tao_before + provided - ); - assert_eq!( - u64::from(SubnetAlphaIn::::get(netuid)), - alpha_before + provided - ); - }); -} - -#[test] -fn test_migrate_coldkey_swap_scheduled_to_announcements() { - new_test_ext(1000).execute_with(|| { - use crate::migrations::migrate_coldkey_swap_scheduled_to_announcements::*; - use coldkey_swap_deprecated as deprecated; - - const MIGRATION_NAME: &[u8] = b"migrate_coldkey_swap_scheduled_to_announcements"; - let now = frame_system::Pallet::::block_number(); - - // Set the schedule duration and reschedule duration - deprecated::ColdkeySwapScheduleDuration::::set(Some(now + 100)); - deprecated::ColdkeySwapRescheduleDuration::::set(Some(now + 200)); - - let make_swap_task = |who: U256, new_coldkey: U256| -> ScheduledOf { - let call_bytes = deprecated::RuntimeCall::::SubtensorCall( - deprecated::SubtensorCall::SwapColdkey { - old_coldkey: who, - new_coldkey, - swap_cost: 1000.into(), - }, - ) - .encode(); - pallet_scheduler::Scheduled { - maybe_id: None, - priority: 63, - call: Bounded::Inline(BoundedVec::truncate_from(call_bytes)), - maybe_periodic: None, - origin: OriginCaller::system(frame_system::RawOrigin::Root), - _phantom: PhantomData, - } - }; - - let make_other_task = || -> ScheduledOf { - let call_bytes = RuntimeCall::SubtensorModule(crate::Call::burned_register { - netuid: 1u16.into(), - hotkey: U256::from(999), - }) - .encode(); - pallet_scheduler::Scheduled { - maybe_id: None, - priority: 63, - call: Bounded::Inline(BoundedVec::truncate_from(call_bytes)), - maybe_periodic: None, - origin: OriginCaller::system(frame_system::RawOrigin::Root), - _phantom: PhantomData, - } - }; - - deprecated::ColdkeySwapScheduled::::insert( - U256::from(1), - (now + 100, U256::from(10)), - ); - pallet_scheduler::Agenda::::insert( - now + 100, - BoundedVec::truncate_from(vec![ - Some(make_swap_task(U256::from(1), U256::from(10))), - Some(make_other_task()), - ]), - ); - - deprecated::ColdkeySwapScheduled::::insert( - U256::from(2), - (now - 200, U256::from(20)), - ); - - deprecated::ColdkeySwapScheduled::::insert( - U256::from(3), - (now + 200, U256::from(30)), - ); - pallet_scheduler::Agenda::::insert( - now + 200, - BoundedVec::truncate_from(vec![Some(make_swap_task(U256::from(3), U256::from(30)))]), - ); - - deprecated::ColdkeySwapScheduled::::insert( - U256::from(4), - (now - 400, U256::from(40)), - ); - - deprecated::ColdkeySwapScheduled::::insert( - U256::from(5), - (now + 300, U256::from(50)), - ); - pallet_scheduler::Agenda::::insert( - now + 300, - BoundedVec::truncate_from(vec![ - Some(make_other_task()), - Some(make_swap_task(U256::from(5), U256::from(50))), - ]), - ); - - let w = migrate_coldkey_swap_scheduled_to_announcements::(); - - assert!(!w.is_zero(), "weight must be non-zero"); - assert!(HasMigrationRun::::get(MIGRATION_NAME)); - - // Ensure the deprecated storage is cleared - assert!(!deprecated::ColdkeySwapScheduleDuration::::exists()); - assert!(!deprecated::ColdkeySwapRescheduleDuration::::exists()); - assert_eq!(deprecated::ColdkeySwapScheduled::::iter().count(), 0); - - assert_eq!( - pallet_scheduler::Agenda::::get(now + 100), - vec![None, Some(make_other_task())], - "swap task for who=1 should be cancelled" - ); - - assert_eq!( - pallet_scheduler::Agenda::::get(now + 200), - vec![None], - "swap task for who=3 should be cancelled" - ); - - assert_eq!( - pallet_scheduler::Agenda::::get(now + 300), - vec![Some(make_other_task()), None], - "swap task for who=5 should be cancelled" - ); - - let delay = ColdkeySwapAnnouncementDelay::::get(); - assert_eq!(ColdkeySwapAnnouncements::::iter().count(), 3); - assert!(!ColdkeySwapAnnouncements::::contains_key(U256::from( - 2 - ))); - assert!(!ColdkeySwapAnnouncements::::contains_key(U256::from( - 4 - ))); - assert_eq!( - ColdkeySwapAnnouncements::::get(U256::from(1)), - Some(( - now + 100 - delay, - ::Hashing::hash_of(&U256::from(10)) - )) - ); - assert_eq!( - ColdkeySwapAnnouncements::::get(U256::from(3)), - Some(( - now + 200 - delay, - ::Hashing::hash_of(&U256::from(30)) - )) - ); - assert_eq!( - ColdkeySwapAnnouncements::::get(U256::from(5)), - Some(( - now + 300 - delay, - ::Hashing::hash_of(&U256::from(50)) - )) - ); - }); -} - -#[test] -fn test_migrate_clear_deprecated_registration_maps() { - new_test_ext(1).execute_with(|| { - const MIG_NAME: &[u8] = b"migrate_clear_deprecated_registration_maps_v1"; - - let netuid0: NetUid = 0u16.into(); - let netuid1: NetUid = 1u16.into(); - - // -------------------------------------------------------------------- - // 0) Pre-state - // -------------------------------------------------------------------- - assert!( - !HasMigrationRun::::get(MIG_NAME.to_vec()), - "migration flag should be false before run" - ); - - // New-model storage must remain untouched by this migration. - crate::BurnHalfLife::::insert(netuid0, 777u16); - crate::BurnIncreaseMult::::insert(netuid0, U64F64::from_num(9)); - - crate::BurnHalfLife::::insert(netuid1, 888u16); - crate::BurnIncreaseMult::::insert(netuid1, U64F64::from_num(11)); - - assert_eq!(crate::BurnHalfLife::::get(netuid0), 777u16); - assert_eq!(crate::BurnIncreaseMult::::get(netuid0), 9u64); - - assert_eq!(crate::BurnHalfLife::::get(netuid1), 888u16); - assert_eq!(crate::BurnIncreaseMult::::get(netuid1), 11u64); - - // Seed deprecated storage items that the migration is expected to clear. - crate::NetworkPowRegistrationAllowed::::insert(netuid0, true); - - crate::POWRegistrationsThisInterval::::insert(netuid0, 7u16); - crate::BurnRegistrationsThisInterval::::insert(netuid0, 8u16); - - crate::NetworkPowRegistrationAllowed::::insert(netuid1, false); - - crate::POWRegistrationsThisInterval::::insert(netuid1, 17u16); - crate::BurnRegistrationsThisInterval::::insert(netuid1, 18u16); - - assert!(crate::NetworkPowRegistrationAllowed::::contains_key(netuid0)); - assert!(crate::POWRegistrationsThisInterval::::contains_key(netuid0)); - assert!(crate::BurnRegistrationsThisInterval::::contains_key(netuid0)); - - assert!(crate::NetworkPowRegistrationAllowed::::contains_key(netuid1)); - assert!(crate::POWRegistrationsThisInterval::::contains_key(netuid1)); - assert!(crate::BurnRegistrationsThisInterval::::contains_key(netuid1)); - - // -------------------------------------------------------------------- - // 1) Run migration - // -------------------------------------------------------------------- - let w = crate::migrations::migrate_clear_deprecated_registration_maps::migrate_clear_deprecated_registration_maps::(); - assert!(!w.is_zero(), "weight must be non-zero"); - - // -------------------------------------------------------------------- - // 2) Post-state: deprecated storage cleared - // -------------------------------------------------------------------- - assert!( - HasMigrationRun::::get(MIG_NAME.to_vec()), - "migration flag should be true after run" - ); - - assert!(!crate::NetworkPowRegistrationAllowed::::contains_key(netuid0)); - assert!(!crate::POWRegistrationsThisInterval::::contains_key(netuid0)); - assert!(!crate::BurnRegistrationsThisInterval::::contains_key(netuid0)); - - assert!(!crate::NetworkPowRegistrationAllowed::::contains_key(netuid1)); - assert!(!crate::POWRegistrationsThisInterval::::contains_key(netuid1)); - assert!(!crate::BurnRegistrationsThisInterval::::contains_key(netuid1)); - - // -------------------------------------------------------------------- - // 3) Post-state: new-model storage unchanged - // -------------------------------------------------------------------- - assert_eq!(crate::BurnHalfLife::::get(netuid0), 777u16); - assert_eq!(crate::BurnIncreaseMult::::get(netuid0), 9u64); - - assert_eq!(crate::BurnHalfLife::::get(netuid1), 888u16); - assert_eq!(crate::BurnIncreaseMult::::get(netuid1), 11u64); - - // -------------------------------------------------------------------- - // 4) Idempotency - // -------------------------------------------------------------------- - let w2 = crate::migrations::migrate_clear_deprecated_registration_maps::migrate_clear_deprecated_registration_maps::(); - assert!(!w2.is_zero(), "second call should still return non-zero read weight"); - - assert!( - HasMigrationRun::::get(MIG_NAME.to_vec()), - "migration flag should remain true after second run" - ); - - assert_eq!(crate::BurnHalfLife::::get(netuid0), 777u16); - assert_eq!(crate::BurnIncreaseMult::::get(netuid0), 9u64); - - assert_eq!(crate::BurnHalfLife::::get(netuid1), 888u16); - assert_eq!(crate::BurnIncreaseMult::::get(netuid1), 11u64); - }); -} - -#[test] -fn test_migrate_fix_bad_hk_swap_only_genesis() { - new_test_ext(1).execute_with(|| { - use crate::migrations::migrate_fix_bad_hk_swap::*; - const MIGRATION_NAME: &[u8] = b"migrate_fix_bad_hk_swap"; - - let coldkey = "5H1WgA7ET3FmEarJK6qc1vaTWbNd6g41mgvyLRkysrH4MDdo"; - let account_id32: AccountId32 = - AccountId32::from_ss58check(coldkey).expect("Invalid coldkey"); - let mut account_id32_slice: &[u8] = account_id32.as_ref(); - let coldkey_account_id: ::AccountId = - ::AccountId::decode(&mut account_id32_slice).expect("Invalid coldkey"); - let netuid = NetUid::from(59); - // Setup - // Add subnet 59 - add_network(netuid, 10, 0); - SubtokenEnabled::::insert(netuid, true); - SubnetMechanism::::insert(netuid, 1); - - // Add stake to hotkey matching - let hotkey = "5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN"; - let account_id32: AccountId32 = - AccountId32::from_ss58check(hotkey).expect("Invalid hotkey"); - let mut account_id32_slice: &[u8] = account_id32.as_ref(); - let hotkey_account_id: ::AccountId = - ::AccountId::decode(&mut account_id32_slice).expect("Invalid hotkey"); - - // Give balance to coldkey - add_balance_to_coldkey_account(&coldkey_account_id, 100_000222.into()); - // Give stake to hotkey - let stake_added = 222222.into(); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - stake_added, - ); - - // Check genesis hash - let genesis_hash = frame_system::Pallet::::block_hash(0); - let genesis_bytes = genesis_hash.as_ref(); - let mainnet_genesis = - hex_literal::hex!("2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03"); - assert_ne!(genesis_bytes, mainnet_genesis); - - // Run migration - let w = migrate_fix_bad_hk_swap::(); - assert!(!w.is_zero(), "weight must be non-zero"); - - // Check stake did not change - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid - ), - stake_added - ); - }); -} - -#[test] -fn test_migrate_fix_bad_hk_swap_runs_on_mainnet_genesis() { - new_test_ext(1).execute_with(|| { - use crate::migrations::migrate_fix_bad_hk_swap::*; - const MIGRATION_NAME: &[u8] = b"migrate_fix_bad_hk_swap"; - - let coldkey = "5H1WgA7ET3FmEarJK6qc1vaTWbNd6g41mgvyLRkysrH4MDdo"; - let account_id32: AccountId32 = - AccountId32::from_ss58check(coldkey).expect("Invalid coldkey"); - let mut account_id32_slice: &[u8] = account_id32.as_ref(); - let coldkey_account_id: ::AccountId = - ::AccountId::decode(&mut account_id32_slice).expect("Invalid coldkey"); - let netuid = NetUid::from(59); - // Setup - // Add subnet 59 - add_network(netuid, 10, 0); - SubtokenEnabled::::insert(netuid, true); - SubnetMechanism::::insert(netuid, 1); - - // Add stake to hotkey matching - let hotkey = "5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN"; - let account_id32: AccountId32 = - AccountId32::from_ss58check(hotkey).expect("Invalid hotkey"); - let mut account_id32_slice: &[u8] = account_id32.as_ref(); - let hotkey_account_id: ::AccountId = - ::AccountId::decode(&mut account_id32_slice).expect("Invalid hotkey"); - - // Give balance to coldkey - add_balance_to_coldkey_account(&coldkey_account_id, 100_000222.into()); - // Give stake to hotkey - let stake_added = 222222.into(); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - stake_added, - ); - - // Set genesis hash to mainnet genesis - let mainnet_genesis = - hex_literal::hex!("2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03"); - frame_system::BlockHash::::insert(0, H256::from_slice(&mainnet_genesis)); - // Check genesis hash - let genesis_hash = frame_system::Pallet::::block_hash(0); - let genesis_bytes = genesis_hash.as_ref(); - assert_eq!(genesis_bytes, mainnet_genesis); - - // Run migration - let w = migrate_fix_bad_hk_swap::(); - assert!(!w.is_zero(), "weight must be non-zero"); - - // Check stake DID change - assert_ne!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid - ), - stake_added - ); - }); -} - -fn decode_account_id32(ss58_string: &str) -> Option { - let account_id32: AccountId32 = AccountId32::from_ss58check(ss58_string).ok()?; - let mut account_id32_slice: &[u8] = account_id32.as_ref(); - T::AccountId::decode(&mut account_id32_slice).ok() -} - -#[test] -fn test_migrate_fix_bad_hk_swap_mainnet() { - new_test_ext(1).execute_with(|| { - use crate::migrations::migrate_fix_bad_hk_swap::*; - - let netuid = NetUid::from(59); - // Add subnet 59 - add_network(netuid, 10, 0); - SubtokenEnabled::::insert(netuid, true); - SubnetMechanism::::insert(netuid, 1); - - let hotkey = "5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN"; - let hotkey_account_id = decode_account_id32::(hotkey).expect("Invalid hotkey"); - - #[rustfmt::skip] - let diffs: [(&str, i64); 112] = [ - ("5Fn9SqQhx5bhDua7AGgkKxxk3gfZ75WWBGCMPeKH1WBgPaMQ", -2375685930981_i64), - ("5Fnhtm7cpxEbZaChnRZ8yWoF8MXVxmobkmLRehh5bkYtyZA9", -4090996138227), - ("5C7j3w2zz1SVejRuFrb2zFWHXT7UfG7eWA87KXL1WyV5KLVR", -607494031), - ("5DthZ1rvnXBb9oXVNtrMaMsDAnRxBPZCjD6fdRdeqC3fg1ca", -17022477949), - ("5F7BkPL3EVjKTYMbBkEmPAtTZQSGeyNzFPaf1DtebPFmJsJ7", -4016510), - ("5EefisctzgWdVGFQaL4LjFFacTE7dM4YJVNy3ogGBQoapTU1", -13106893093), - ("5CwkvpBxHCaRK9xBC2n6WdhpF5zg9t5WLkGorASaoErdynFQ", 439139249152), - ("5FU7ErUtmi22xuqeeCYVpNZp6WVSSL98hqDi5iyeZbkXtkbe", -35958768555), - ("5D7HL8T95qkHQTPFjgSFCjRoeM7oE3vQBYjiR1kAPbPxcMKu", -201914811997), - ("5HL3pPdDFY94Qdf8VnbfT4W6LXFkpd68Y5GSGzNJfntMdGZX", -235660917467), - ("5EcYAz8SBKWsogA6meJmVXcwVp4tjCvw3ZnJE6UXTyWNUdF2", -500070769668), - ("5EoE3c7XMf8TN3yudAaFjv4yvjtWYRviHcXi73EXkLHmWTCB", -86442928436), - ("5CMDjL7t2biHGREBwrmd8renD74FLEhjCVqfJG2MXckWBwDu", 1039317), - ("5CVGKimL4cLgyTqvYKQbPKYFZfiztsdczU7HrwNdSFKbbn5D", 4224201), - ("5HmZnEcW4eHbXmUEFWJbc4GHnBBYEK8ZPsFa25PuEmP5iuwM", -13156128), - ("5FNa4J4fTKh555CEyXHgR29RicSm8nTEHx36utTa4MJepJyX", -9519954), - ("5Gun93uQgffYpxqMKSmfG18AHiQW7Z2GR2dfPPR8W188vJYc", -1127662), - ("5HW1C4js4RyjQqNwALSUZC8NJ2WinD5Si2X2XkstXrMW2uYo", -34457336758), - ("5EqMhjdLY9h64ui2mizRZyBp1mEPJ7s4TsfAxQSQkAFmMfzE", -9346443829744), - ("5H3XwzydgE2XUGoJCR4dSj7tkd7uxZDJqik69hux2DBcruom", -1215347774), - ("5DjkmYpCUX6dBTvGoyN9j4QZhtMPhdcywDE8cJ8Qq1vg4X6e", -3603984447), - ("5E7Z7Btjz74XpZLH5fRzfZqiHCo4j9PXKfqi88kQ5MFrds34", -823907380854), - ("5DSBWN4hN9413C6o6A2hR9tYUbHjWsQqPRV74GrnCrMkCGJx", -309708781), - ("5FMLRmKPqsTsMbakpVUwoYro1P64QXVNTWyzNDugaNwSKRzF", -137525398263), - ("5EFZf5pnTqLegv6gxCrb6TKBQBGz9xLJNK8x9eR273cSons6", -1521760918), - ("5CGAGEuMLaidBDk8bDZKJb23dxRSP1wLenLALGLw8BTG1E3W", -544739696), - ("5HSzRtcQjD5KP6Nh2GVSS16aLDe6q9R33Wpu6s2eEeeo3AYS", -2309184790), - ("5DALvFDcfANQJcWz6AXMfDqabnoZhdDMoH6FxqYUibug1ja7", -369405632507), - ("5Fy8iWkpcbsskmEN1nYZDdS9zKh167Em9RRYisoss7jaYXxi", 15257429), - ("5CQCVTRyqJgZKBDmtHzpoF8su6BScLcNGbX8t3WMm5qYbbJH", -10721968), - ("5DXZByh2NS4MU61a1aaLrcLYpyzpJgHe95TEBdcEN2cF1SA5", -655946136), - ("5EX5yAYiABFzKDQJDe1kRVwFm3XRRY4HyLMe4Vu9A5U2VEVT", -325581360246), - ("5FvabwjtyW887gtc7vUnUc47KVhy17UeaNLRjzTg5nkVACMP", -77588524213), - ("5HTbYi5cmgWJxvyTy9JeYdtnjoDzjXnEXTGFsPEVx9iRPmVF", -53542953784), - ("5CWzmvA17MAMQ9mnAecLxFXS2N8846rz6T7m4QNHyVtJVq4j", 2672295922502), - ("5DSYntgHZY4krYUtkkQZyyoffVtu5e8rYWhXuhs832zY6YKy", -2680205688), - ("5EYyTFyLDqXscaa5VtXTvUc3x2ow2TeT8G12ZDMZwE6uFWPQ", -39165843935), - ("5CohfM1qdyNwdeJEex1Zyht3S2WS48rV993DmVbyKs2mEEd6", -4004685632), - ("5Gx6Y7UQD39Latgxigr6mHbnh1herpwNPau2PjvzwLWEjXL3", -559504), - ("5Hh4Efq5WDwe8URjjUqUNX8KxtMwLHLViwoRvXfEpXQCZakh", -32541090531), - ("5GWRHC7Nd8njqTPsdJkp6ngniCCBu9UjGhLfxp2jF1fPrfZ4", -5394093031), - ("5GNAB64UN32krzr3Xxu5LW6naeu2P3XULcdBCR9VZ5Libyit", -24884230), - ("5EEz25th1nYNM5xR1UsyFFAUaXMjdHqLxZ3wUjyHokYbXHku", -12525171), - ("5HKJq4JCS9xoKdYhcRnsRp1bodovba7ncd5KTYVwfReKaxHT", -408133990236), - ("5DXs7x664RL5NdSW77DTseLiu84unstuHGuqvmY61UtJwzRN", -3095078614148), - ("5CDfdDaA2p9sK1ia5yMVYfzgtFs2e1TrSAxuQqXoS28Lcrxf", -1032856892), - ("5Ecg4vD2zKXHDFhQqogWq1dZdijPsDty8rGsZu3raeoJSiXb", -995678), - ("5C5Yg63TNLb68Tu819qXd3Bt4giG8mAPzLmAFSqa2HC1R5Rm", -40818739830910), - ("5HHH25Wuf9rmVuk9cMKU1hCCPJ1qbHBd1SyHj91R3fMT36yb", -391416057906), - ("5GKGGE5YLHoDciYJ6Ec2YnUP3SykSQPA47hqmwBP63EtVrd9", -413944553000), - ("5CSi9ZLyiXfLeYtEFaZSBuTofNMRnXEJEJE9CS4gGaT6CkWt", -17811605275), - ("5CSoA7QVdFHHBZz53bbRV2mC5vhL64ehhWa8ibtLppmt2n3J", -65701320107), - ("5GpA5BtfMMX52rXztrha79YqfwR4YaSfTuAcb48Yt73U4h71", -2194562), - ("5Euz5wpb4xiDWfV1A6AKK6i6ca3WoZQD5hCVyf1fws8GXh4z", -6143407839874), - ("5DZzmhCG7SMK3LwrkmHZ8ZBwaAByMjfBpEid14nNQdxHipCE", -386645), - ("5Fc9Vo3hkbr6bPxJpjQo5sQ43L5Hc2G8R5BdqRYF8psvB5pw", 55668553), - ("5GuSHC3iowySHLDW4pEyEZE6PKxKP62YpJYJyBy5tijzAnYz", -159317636526), - ("5HVVZrUBPvjYHiwaSvtvaN9GZogoznM49m2AEmVW6RXnYCka", -1995572213), - ("5EcGpeV2wjkCVsBjsBifSWbdcqH98b6oEY8beDY59c4fXkhw", -177096614584), - ("5GnCjvWJEESwVNFZzy85zbBzw26etuEt87WiqsE3ee2Ws1wm", -1961445), - ("5GWuPUpTuChAqKxvU22TRLvRkBFiyWWZnq9cLpJN6SSvkho1", -94157569391), - ("5FXHf7q5rvBXnzQgmsa31Db9rjcRy6ZHKMiyDSb8Vs5p2msN", -688433531658), - ("5GbxkzytnvbRuNQ7qxPpfPuWMoeitS8V4KDY9jSshE5fDegD", -19085313), - ("5Gus1B7c9uWkky7Yawh2tKR1V6AMh5DbqUBPq881JHqeqVqY", -16101671818), - ("5DLhRdbvWkYYScDmwx4QgJfieSN4apBWbZ2yno3MfgbR8hBP", -21062025), - ("5Cg5kVyNEs7MWWRHU8X5MHwX5cN3aegvC4RBt2JK19w2GiR8", -2593737050), - ("5Dkushsxtc8AdCf287MtTYHQv9DoZeBRpttUpBtmyFhGy3uR", -48672832345630), - ("5EqNqVsHj9bQVyEujcm62zjMYUFhTLY7rTP854txSrJzyoco", -3828526), - ("5Dea6d6nKErEbRQ4MBGuCALn8NZ2xo4kaa51hB5KMriPBkEM", -1560192853875), - ("5DNt2XDWdeMd4H92FLnfUvkqyXzmavezHvzLboP3VgT1xLZV", -831964576998), - ("5FKtFoTeK8aaG6HZTrDgvoYHVQ5NY4S9VyV7W5K74cWcwLYA", -60823501166), - ("5GEBanZKUU7Hrf8K2VNi33HxyJRstgQ3WD3odHgvMj2nPbhi", -98946626902), - ("5CUtw7LYB2n2bzgXt6YnmKDHt6PsB3kKAyD9azYJNcRG8TNg", -9779588557490), - ("5EynbF72b12fbgMvEeL1vJSY342rCryNbuwxFivU1Xevtmv3", -17314385200455), - ("5CapiZRuULed8ConS1gbjMVgnwcT5JnQah7tx6sZnK7sJJuJ", -5810972), - ("5DnaxLaNduf41WM6WWZ4fkzcGzWNWx6eLJyQpSaMueUGCsaU", -12668760), - ("5Cqz9SChYPxTFZ2623rE2aQQ5ttQoLwZ8yfwYgiZyQDANqZn", -683549), - ("5C8ZcLzF23GrXKdH4Pg3ZXC3vKQsF5PM8VvhzzxzTQksgj8e", -44720570590), - ("5GuNsmoswrP6hTKZkKcpTpZftTMKrmnCHvTL2V3NHJy2fpen", -5042891812715), - ("5F1TYDkLnP36HHY5btigxyKUPzBraxdrU1aX1bqFfPfcfnzU", -1189104279832), - ("5Dc384z9HuTGF6oratZs1fLciCHtPZaLhrHfCVw82a5AikWZ", -616163196988), - ("5DhcaEUsRKhZQ31qRffJqjtLmFkbVaCebn8nVjYhvB4KJtX5", -17746006723), - ("5HYE7z3xTcrN1rqz54NyZRAkehFfRMcaEcdoMq5g5ATET5wQ", 212509751245), - ("5F97DdEVTy9gPCtN6jkJJENDJuQiRGiwbMVSL74qRq8FCq5W", 2225287736222), - ("5FUVN133rSvuKXgsXKMR2ZEaysxZjkRUFUWS1UMyNGre9xFV", -73216740161), - ("5CZeimtfpRqQgPxVwr1MzfG2Sok8E1AMERHo6vUmEdRS5JiU", -3937802), - ("5Eqq2JwGh7qbtnjPiFEPmmnHxs3S4J4Ahg8fr4sybZV1tPdY", -173406860562), - ("5ERfDw6K3GmQqwqsEG6foFtu7VsYGifPi556UJKQsBnfbHKN", 96022588728), - ("5Ek8RkU6KMv5Fx7yivRVoQkuJYAKhULWiLWDpbGG4hvR9HFD", 968139369093), - ("5HpCpGALzqgnDTP1HXFiuhzD5MFaDTRHjXBCvaMY9LNNRkT9", 104943979521), - ("5FYqS77gxW9gHG8id1YYPS7Cd4TQmNUMhF8h3S77Fq2VvvRQ", 729199757977), - ("5FtBqMg13pNf1N6TwfG6BmwyaDM77mkeQ16UTHsGasrVDedX", 131457064336), - ("5GbnWR2XhWrRMt123SdrLbR9G2a4N5dtzA3TSu3Czkzoeu7x", 2295599153), - ("5GgiowcCG4kLpwkCTGxxQJQv8WwKFyBQ6McPRmqKtWPy8EaK", 113838605389), - ("5FL5YtYozpUAGaiVWonpbwEYdEMij3obJHSH3ACY4vgWmDgy", 8689039), - ("5Egq58bxRv7boM2s3rnDxx1udnkzxPQ23HuoqohVxjh9RenC", 216373234348), - ("5FRGeeEgRNR8U33FDKvN7yUgts8zR3qRJH4yKKWoR9GswBRb", 2196574958718), - ("5FnhSy79BPYyrmmFsbckinQw1fLiLqqPkQL2vgZwPxbRfu3k", 42319631507), - ("5Hj8jMhqAv7cfyRh5STfbZefMhv17QxZ1RxWq9jNcLAEsRRo", 132216702183491), - ("5EXYTGMqumAH6RLQgHwkMEMnSvHcpHc89R6U8krfNJTYWm9J", 504320264499), - ("5EFh8ctzmytXURqrCTUBWHTs87f7TMWB6XKUzdqxKXVUtvS2", 2209599669432), - ("5CqVqEcRBkw7Gm2reJ33cj7puR9W2Tq7qsLxSruV1BgnMqKN", 1033387458788), - ("5D79enmLSGimsruoraGagofhaSeYJZvGUqFCCrr83ZfZs1HS", 7591184215233), - ("5HbpyjsvyXLWtf1QT1CyNUdyut6scM5dM7ytm8hoxFvRtU1i", 129833188275), - ("5CnxCi7CdEriWSdw4LcXdbtjodxA6uTat4gBm4wuT9QToMdo", 3132978), - ("5G48fiQjhAd8hc4rYc6GituCuAPKznL28jyyyq1auMyZiG4t", 514913328178), - ("5FFGjW2hJ7tQ41qghSsLP4cVmA8j9pZVSrr2CrLG7fQAsLHJ", 346794972723), - ("5FWjnxeRMtMFxRc9kvZKCG5iJAyyz2kmXV8u3kqyiXizZtiz", 225939835005), - ("5CUw3sB4oxd3dVSHUr3kxsB591VEjaPzr444KkfjwVFnLRfJ", 208250614494), - ("5EaBhxNUwMRyKsaeA2BEjDCrvwE5J8FDSpfCHK9gGmnmbhCa", 278083207003), - ("5GHJ5HxFxYQyVoNFUxR3JCqqCKRumaFCY7N5zMxwF4CpRUWr", 1381466224829), - ("5H1WgA7ET3FmEarJK6qc1vaTWbNd6g41mgvyLRkysrH4MDdo", 774889), - ]; - - for (coldkey, diff) in diffs { - let coldkey_account_id = decode_account_id32::(coldkey).expect("Invalid coldkey"); - if diff > 0 { - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - diff.unsigned_abs().into(), - ); - } - } - - let w = try_restore_shares::(); - assert!(!w.is_zero(), "weight must be non-zero"); - - // Check stake is near 0 for all positive entires and near diff for all negative - for (coldkey, diff) in diffs { - let coldkey_account_id = decode_account_id32::(coldkey).expect("Invalid coldkey"); - - let stake_float: f64 = num_traits::ToPrimitive::to_f64( - &SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - ) - .to_u64(), - ) - .expect("float conv fail"); - if diff > 0 { - log::debug!("diff: {} for ck: {}", diff, coldkey); - assert_relative_eq!(stake_float, 0_f64, max_relative = 0.001_f64); - } else { - let diff_float: f64 = - num_traits::ToPrimitive::to_f64(&diff.unsigned_abs()).expect("float conv fail"); - assert_relative_eq!(stake_float, diff_float, max_relative = 0.001_f64); - } - } - }); -} - -#[test] -fn test_migrate_fix_bad_hk_swap_mainnet_some_exits() { - // test with some of the gainers have exited fully or partially before the migration - // i.e. balance is less than they owe - new_test_ext(1).execute_with(|| { - use crate::migrations::migrate_fix_bad_hk_swap::*; - - let netuid = NetUid::from(59); - // Add subnet 59 - add_network(netuid, 10, 0); - SubtokenEnabled::::insert(netuid, true); - SubnetMechanism::::insert(netuid, 1); - - let hotkey = "5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN"; - let hotkey_account_id = decode_account_id32::(hotkey).expect("Invalid hotkey"); - - #[rustfmt::skip] - let diffs: [(&str, i64); 112] = [ - ("5Fn9SqQhx5bhDua7AGgkKxxk3gfZ75WWBGCMPeKH1WBgPaMQ", -2375685930981_i64), - ("5Fnhtm7cpxEbZaChnRZ8yWoF8MXVxmobkmLRehh5bkYtyZA9", -4090996138227), - ("5C7j3w2zz1SVejRuFrb2zFWHXT7UfG7eWA87KXL1WyV5KLVR", -607494031), - ("5DthZ1rvnXBb9oXVNtrMaMsDAnRxBPZCjD6fdRdeqC3fg1ca", -17022477949), - ("5F7BkPL3EVjKTYMbBkEmPAtTZQSGeyNzFPaf1DtebPFmJsJ7", -4016510), - ("5EefisctzgWdVGFQaL4LjFFacTE7dM4YJVNy3ogGBQoapTU1", -13106893093), - ("5CwkvpBxHCaRK9xBC2n6WdhpF5zg9t5WLkGorASaoErdynFQ", 439139249152), - ("5FU7ErUtmi22xuqeeCYVpNZp6WVSSL98hqDi5iyeZbkXtkbe", -35958768555), - ("5D7HL8T95qkHQTPFjgSFCjRoeM7oE3vQBYjiR1kAPbPxcMKu", -201914811997), - ("5HL3pPdDFY94Qdf8VnbfT4W6LXFkpd68Y5GSGzNJfntMdGZX", -235660917467), - ("5EcYAz8SBKWsogA6meJmVXcwVp4tjCvw3ZnJE6UXTyWNUdF2", -500070769668), - ("5EoE3c7XMf8TN3yudAaFjv4yvjtWYRviHcXi73EXkLHmWTCB", -86442928436), - ("5CMDjL7t2biHGREBwrmd8renD74FLEhjCVqfJG2MXckWBwDu", 1039317), - ("5CVGKimL4cLgyTqvYKQbPKYFZfiztsdczU7HrwNdSFKbbn5D", 4224201), - ("5HmZnEcW4eHbXmUEFWJbc4GHnBBYEK8ZPsFa25PuEmP5iuwM", -13156128), - ("5FNa4J4fTKh555CEyXHgR29RicSm8nTEHx36utTa4MJepJyX", -9519954), - ("5Gun93uQgffYpxqMKSmfG18AHiQW7Z2GR2dfPPR8W188vJYc", -1127662), - ("5HW1C4js4RyjQqNwALSUZC8NJ2WinD5Si2X2XkstXrMW2uYo", -34457336758), - ("5EqMhjdLY9h64ui2mizRZyBp1mEPJ7s4TsfAxQSQkAFmMfzE", -9346443829744), - ("5H3XwzydgE2XUGoJCR4dSj7tkd7uxZDJqik69hux2DBcruom", -1215347774), - ("5DjkmYpCUX6dBTvGoyN9j4QZhtMPhdcywDE8cJ8Qq1vg4X6e", -3603984447), - ("5E7Z7Btjz74XpZLH5fRzfZqiHCo4j9PXKfqi88kQ5MFrds34", -823907380854), - ("5DSBWN4hN9413C6o6A2hR9tYUbHjWsQqPRV74GrnCrMkCGJx", -309708781), - ("5FMLRmKPqsTsMbakpVUwoYro1P64QXVNTWyzNDugaNwSKRzF", -137525398263), - ("5EFZf5pnTqLegv6gxCrb6TKBQBGz9xLJNK8x9eR273cSons6", -1521760918), - ("5CGAGEuMLaidBDk8bDZKJb23dxRSP1wLenLALGLw8BTG1E3W", -544739696), - ("5HSzRtcQjD5KP6Nh2GVSS16aLDe6q9R33Wpu6s2eEeeo3AYS", -2309184790), - ("5DALvFDcfANQJcWz6AXMfDqabnoZhdDMoH6FxqYUibug1ja7", -369405632507), - ("5Fy8iWkpcbsskmEN1nYZDdS9zKh167Em9RRYisoss7jaYXxi", 15257429), - ("5CQCVTRyqJgZKBDmtHzpoF8su6BScLcNGbX8t3WMm5qYbbJH", -10721968), - ("5DXZByh2NS4MU61a1aaLrcLYpyzpJgHe95TEBdcEN2cF1SA5", -655946136), - ("5EX5yAYiABFzKDQJDe1kRVwFm3XRRY4HyLMe4Vu9A5U2VEVT", -325581360246), - ("5FvabwjtyW887gtc7vUnUc47KVhy17UeaNLRjzTg5nkVACMP", -77588524213), - ("5HTbYi5cmgWJxvyTy9JeYdtnjoDzjXnEXTGFsPEVx9iRPmVF", -53542953784), - ("5CWzmvA17MAMQ9mnAecLxFXS2N8846rz6T7m4QNHyVtJVq4j", 2672295922502), - ("5DSYntgHZY4krYUtkkQZyyoffVtu5e8rYWhXuhs832zY6YKy", -2680205688), - ("5EYyTFyLDqXscaa5VtXTvUc3x2ow2TeT8G12ZDMZwE6uFWPQ", -39165843935), - ("5CohfM1qdyNwdeJEex1Zyht3S2WS48rV993DmVbyKs2mEEd6", -4004685632), - ("5Gx6Y7UQD39Latgxigr6mHbnh1herpwNPau2PjvzwLWEjXL3", -559504), - ("5Hh4Efq5WDwe8URjjUqUNX8KxtMwLHLViwoRvXfEpXQCZakh", -32541090531), - ("5GWRHC7Nd8njqTPsdJkp6ngniCCBu9UjGhLfxp2jF1fPrfZ4", -5394093031), - ("5GNAB64UN32krzr3Xxu5LW6naeu2P3XULcdBCR9VZ5Libyit", -24884230), - ("5EEz25th1nYNM5xR1UsyFFAUaXMjdHqLxZ3wUjyHokYbXHku", -12525171), - ("5HKJq4JCS9xoKdYhcRnsRp1bodovba7ncd5KTYVwfReKaxHT", -408133990236), - ("5DXs7x664RL5NdSW77DTseLiu84unstuHGuqvmY61UtJwzRN", -3095078614148), - ("5CDfdDaA2p9sK1ia5yMVYfzgtFs2e1TrSAxuQqXoS28Lcrxf", -1032856892), - ("5Ecg4vD2zKXHDFhQqogWq1dZdijPsDty8rGsZu3raeoJSiXb", -995678), - ("5C5Yg63TNLb68Tu819qXd3Bt4giG8mAPzLmAFSqa2HC1R5Rm", -40818739830910), - ("5HHH25Wuf9rmVuk9cMKU1hCCPJ1qbHBd1SyHj91R3fMT36yb", -391416057906), - ("5GKGGE5YLHoDciYJ6Ec2YnUP3SykSQPA47hqmwBP63EtVrd9", -413944553000), - ("5CSi9ZLyiXfLeYtEFaZSBuTofNMRnXEJEJE9CS4gGaT6CkWt", -17811605275), - ("5CSoA7QVdFHHBZz53bbRV2mC5vhL64ehhWa8ibtLppmt2n3J", -65701320107), - ("5GpA5BtfMMX52rXztrha79YqfwR4YaSfTuAcb48Yt73U4h71", -2194562), - ("5Euz5wpb4xiDWfV1A6AKK6i6ca3WoZQD5hCVyf1fws8GXh4z", -6143407839874), - ("5DZzmhCG7SMK3LwrkmHZ8ZBwaAByMjfBpEid14nNQdxHipCE", -386645), - ("5Fc9Vo3hkbr6bPxJpjQo5sQ43L5Hc2G8R5BdqRYF8psvB5pw", 55668553), - ("5GuSHC3iowySHLDW4pEyEZE6PKxKP62YpJYJyBy5tijzAnYz", -159317636526), - ("5HVVZrUBPvjYHiwaSvtvaN9GZogoznM49m2AEmVW6RXnYCka", -1995572213), - ("5EcGpeV2wjkCVsBjsBifSWbdcqH98b6oEY8beDY59c4fXkhw", -177096614584), - ("5GnCjvWJEESwVNFZzy85zbBzw26etuEt87WiqsE3ee2Ws1wm", -1961445), - ("5GWuPUpTuChAqKxvU22TRLvRkBFiyWWZnq9cLpJN6SSvkho1", -94157569391), - ("5FXHf7q5rvBXnzQgmsa31Db9rjcRy6ZHKMiyDSb8Vs5p2msN", -688433531658), - ("5GbxkzytnvbRuNQ7qxPpfPuWMoeitS8V4KDY9jSshE5fDegD", -19085313), - ("5Gus1B7c9uWkky7Yawh2tKR1V6AMh5DbqUBPq881JHqeqVqY", -16101671818), - ("5DLhRdbvWkYYScDmwx4QgJfieSN4apBWbZ2yno3MfgbR8hBP", -21062025), - ("5Cg5kVyNEs7MWWRHU8X5MHwX5cN3aegvC4RBt2JK19w2GiR8", -2593737050), - ("5Dkushsxtc8AdCf287MtTYHQv9DoZeBRpttUpBtmyFhGy3uR", -48672832345630), - ("5EqNqVsHj9bQVyEujcm62zjMYUFhTLY7rTP854txSrJzyoco", -3828526), - ("5Dea6d6nKErEbRQ4MBGuCALn8NZ2xo4kaa51hB5KMriPBkEM", -1560192853875), - ("5DNt2XDWdeMd4H92FLnfUvkqyXzmavezHvzLboP3VgT1xLZV", -831964576998), - ("5FKtFoTeK8aaG6HZTrDgvoYHVQ5NY4S9VyV7W5K74cWcwLYA", -60823501166), - ("5GEBanZKUU7Hrf8K2VNi33HxyJRstgQ3WD3odHgvMj2nPbhi", -98946626902), - ("5CUtw7LYB2n2bzgXt6YnmKDHt6PsB3kKAyD9azYJNcRG8TNg", -9779588557490), - ("5EynbF72b12fbgMvEeL1vJSY342rCryNbuwxFivU1Xevtmv3", -17314385200455), - ("5CapiZRuULed8ConS1gbjMVgnwcT5JnQah7tx6sZnK7sJJuJ", -5810972), - ("5DnaxLaNduf41WM6WWZ4fkzcGzWNWx6eLJyQpSaMueUGCsaU", -12668760), - ("5Cqz9SChYPxTFZ2623rE2aQQ5ttQoLwZ8yfwYgiZyQDANqZn", -683549), - ("5C8ZcLzF23GrXKdH4Pg3ZXC3vKQsF5PM8VvhzzxzTQksgj8e", -44720570590), - ("5GuNsmoswrP6hTKZkKcpTpZftTMKrmnCHvTL2V3NHJy2fpen", -5042891812715), - ("5F1TYDkLnP36HHY5btigxyKUPzBraxdrU1aX1bqFfPfcfnzU", -1189104279832), - ("5Dc384z9HuTGF6oratZs1fLciCHtPZaLhrHfCVw82a5AikWZ", -616163196988), - ("5DhcaEUsRKhZQ31qRffJqjtLmFkbVaCebn8nVjYhvB4KJtX5", -17746006723), - ("5HYE7z3xTcrN1rqz54NyZRAkehFfRMcaEcdoMq5g5ATET5wQ", 212509751245), - ("5F97DdEVTy9gPCtN6jkJJENDJuQiRGiwbMVSL74qRq8FCq5W", 2225287736222), - ("5FUVN133rSvuKXgsXKMR2ZEaysxZjkRUFUWS1UMyNGre9xFV", -73216740161), - ("5CZeimtfpRqQgPxVwr1MzfG2Sok8E1AMERHo6vUmEdRS5JiU", -3937802), - ("5Eqq2JwGh7qbtnjPiFEPmmnHxs3S4J4Ahg8fr4sybZV1tPdY", -173406860562), - ("5ERfDw6K3GmQqwqsEG6foFtu7VsYGifPi556UJKQsBnfbHKN", 96022588728), - ("5Ek8RkU6KMv5Fx7yivRVoQkuJYAKhULWiLWDpbGG4hvR9HFD", 968139369093), - ("5HpCpGALzqgnDTP1HXFiuhzD5MFaDTRHjXBCvaMY9LNNRkT9", 104943979521), - ("5FYqS77gxW9gHG8id1YYPS7Cd4TQmNUMhF8h3S77Fq2VvvRQ", 729199757977), - ("5FtBqMg13pNf1N6TwfG6BmwyaDM77mkeQ16UTHsGasrVDedX", 131457064336), - ("5GbnWR2XhWrRMt123SdrLbR9G2a4N5dtzA3TSu3Czkzoeu7x", 2295599153), - ("5GgiowcCG4kLpwkCTGxxQJQv8WwKFyBQ6McPRmqKtWPy8EaK", 113838605389), - ("5FL5YtYozpUAGaiVWonpbwEYdEMij3obJHSH3ACY4vgWmDgy", 8689039), - ("5Egq58bxRv7boM2s3rnDxx1udnkzxPQ23HuoqohVxjh9RenC", 216373234348), - ("5FRGeeEgRNR8U33FDKvN7yUgts8zR3qRJH4yKKWoR9GswBRb", 2196574958718), - ("5FnhSy79BPYyrmmFsbckinQw1fLiLqqPkQL2vgZwPxbRfu3k", 42319631507), - ("5Hj8jMhqAv7cfyRh5STfbZefMhv17QxZ1RxWq9jNcLAEsRRo", 132216702183491), - ("5EXYTGMqumAH6RLQgHwkMEMnSvHcpHc89R6U8krfNJTYWm9J", 504320264499), - ("5EFh8ctzmytXURqrCTUBWHTs87f7TMWB6XKUzdqxKXVUtvS2", 2209599669432), - ("5CqVqEcRBkw7Gm2reJ33cj7puR9W2Tq7qsLxSruV1BgnMqKN", 1033387458788), - ("5D79enmLSGimsruoraGagofhaSeYJZvGUqFCCrr83ZfZs1HS", 7591184215233), - ("5HbpyjsvyXLWtf1QT1CyNUdyut6scM5dM7ytm8hoxFvRtU1i", 129833188275), - ("5CnxCi7CdEriWSdw4LcXdbtjodxA6uTat4gBm4wuT9QToMdo", 3132978), - ("5G48fiQjhAd8hc4rYc6GituCuAPKznL28jyyyq1auMyZiG4t", 514913328178), - ("5FFGjW2hJ7tQ41qghSsLP4cVmA8j9pZVSrr2CrLG7fQAsLHJ", 346794972723), - ("5FWjnxeRMtMFxRc9kvZKCG5iJAyyz2kmXV8u3kqyiXizZtiz", 225939835005), - ("5CUw3sB4oxd3dVSHUr3kxsB591VEjaPzr444KkfjwVFnLRfJ", 208250614494), - ("5EaBhxNUwMRyKsaeA2BEjDCrvwE5J8FDSpfCHK9gGmnmbhCa", 278083207003), - ("5GHJ5HxFxYQyVoNFUxR3JCqqCKRumaFCY7N5zMxwF4CpRUWr", 1381466224829), - ("5H1WgA7ET3FmEarJK6qc1vaTWbNd6g41mgvyLRkysrH4MDdo", 774889), - ]; - - for (coldkey, diff) in diffs { - let coldkey_account_id = decode_account_id32::(coldkey).expect("Invalid coldkey"); - if diff > 0 { - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - diff.unsigned_abs().into(), - ); - } - } - - // For one of the gainers, remove some of the stake - let idx = 6; - let gained_ck = diffs[idx].0; - let coldkey_account_id = decode_account_id32::(gained_ck).expect("Invalid coldkey"); - SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - num_traits::ToPrimitive::to_u64( - &(num_traits::ToPrimitive::to_f64(&diffs[idx].1).expect("float conv fail") - * 0.9_f64) - .abs(), - ) - .expect("u64 conv fail") - .into(), - ); - - let w = try_restore_shares::(); - assert!(!w.is_zero(), "weight must be non-zero"); - - // Check stake is near 0 for all positive entires except the one we removed - // Check the stake for all negative entries is proportional to the amount they lost - let total_lost: f64 = diffs - .iter() - .map(|(_, diff)| { - if diff.is_negative() { - num_traits::ToPrimitive::to_f64(&diff.saturating_abs()) - .expect("float conv fail") - } else { - 0_f64 - } - }) - .sum::(); - let mut total_returned = 0_f64; - for (coldkey, diff) in diffs { - let coldkey_account_id = decode_account_id32::(coldkey).expect("Invalid coldkey"); - - let stake_float: f64 = num_traits::ToPrimitive::to_f64( - &SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - ) - .to_u64(), - ) - .expect("float conv fail"); - if diff > 0 { - log::debug!("diff: {} for ck: {}", diff, coldkey); - assert_relative_eq!(stake_float, 0_f64, max_relative = 0.001_f64); - } else { - total_returned += stake_float; - } - } - - for (coldkey, diff) in diffs { - let coldkey_account_id = decode_account_id32::(coldkey).expect("Invalid coldkey"); - let stake_float: f64 = num_traits::ToPrimitive::to_f64( - &SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - ) - .to_u64(), - ) - .expect("float conv fail"); - if diff < 0 { - // Should get a return proportional to the amount they lost - // versus the amount that was able to be recovered - let prop_returned: f64 = num_traits::ToPrimitive::to_f64(&diff.abs()) - .expect("float conv fail") - / total_lost - * total_returned; - assert_relative_eq!(stake_float, prop_returned, max_relative = 0.001_f64); - } - } - }); -} - -#[test] -fn test_migrate_fix_bad_hk_swap_mainnet_has_more() { - // test with some of the gainers have a balance higher than the gain before the migration - new_test_ext(1).execute_with(|| { - use crate::migrations::migrate_fix_bad_hk_swap::*; - - let netuid = NetUid::from(59); - // Add subnet 59 - add_network(netuid, 10, 0); - SubtokenEnabled::::insert(netuid, true); - SubnetMechanism::::insert(netuid, 1); - - let hotkey = "5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN"; - let hotkey_account_id = decode_account_id32::(hotkey).expect("Invalid hotkey"); - - #[rustfmt::skip] - let diffs: [(&str, i64); 112] = [ - ("5Fn9SqQhx5bhDua7AGgkKxxk3gfZ75WWBGCMPeKH1WBgPaMQ", -2375685930981_i64), - ("5Fnhtm7cpxEbZaChnRZ8yWoF8MXVxmobkmLRehh5bkYtyZA9", -4090996138227), - ("5C7j3w2zz1SVejRuFrb2zFWHXT7UfG7eWA87KXL1WyV5KLVR", -607494031), - ("5DthZ1rvnXBb9oXVNtrMaMsDAnRxBPZCjD6fdRdeqC3fg1ca", -17022477949), - ("5F7BkPL3EVjKTYMbBkEmPAtTZQSGeyNzFPaf1DtebPFmJsJ7", -4016510), - ("5EefisctzgWdVGFQaL4LjFFacTE7dM4YJVNy3ogGBQoapTU1", -13106893093), - ("5CwkvpBxHCaRK9xBC2n6WdhpF5zg9t5WLkGorASaoErdynFQ", 439139249152), - ("5FU7ErUtmi22xuqeeCYVpNZp6WVSSL98hqDi5iyeZbkXtkbe", -35958768555), - ("5D7HL8T95qkHQTPFjgSFCjRoeM7oE3vQBYjiR1kAPbPxcMKu", -201914811997), - ("5HL3pPdDFY94Qdf8VnbfT4W6LXFkpd68Y5GSGzNJfntMdGZX", -235660917467), - ("5EcYAz8SBKWsogA6meJmVXcwVp4tjCvw3ZnJE6UXTyWNUdF2", -500070769668), - ("5EoE3c7XMf8TN3yudAaFjv4yvjtWYRviHcXi73EXkLHmWTCB", -86442928436), - ("5CMDjL7t2biHGREBwrmd8renD74FLEhjCVqfJG2MXckWBwDu", 1039317), - ("5CVGKimL4cLgyTqvYKQbPKYFZfiztsdczU7HrwNdSFKbbn5D", 4224201), - ("5HmZnEcW4eHbXmUEFWJbc4GHnBBYEK8ZPsFa25PuEmP5iuwM", -13156128), - ("5FNa4J4fTKh555CEyXHgR29RicSm8nTEHx36utTa4MJepJyX", -9519954), - ("5Gun93uQgffYpxqMKSmfG18AHiQW7Z2GR2dfPPR8W188vJYc", -1127662), - ("5HW1C4js4RyjQqNwALSUZC8NJ2WinD5Si2X2XkstXrMW2uYo", -34457336758), - ("5EqMhjdLY9h64ui2mizRZyBp1mEPJ7s4TsfAxQSQkAFmMfzE", -9346443829744), - ("5H3XwzydgE2XUGoJCR4dSj7tkd7uxZDJqik69hux2DBcruom", -1215347774), - ("5DjkmYpCUX6dBTvGoyN9j4QZhtMPhdcywDE8cJ8Qq1vg4X6e", -3603984447), - ("5E7Z7Btjz74XpZLH5fRzfZqiHCo4j9PXKfqi88kQ5MFrds34", -823907380854), - ("5DSBWN4hN9413C6o6A2hR9tYUbHjWsQqPRV74GrnCrMkCGJx", -309708781), - ("5FMLRmKPqsTsMbakpVUwoYro1P64QXVNTWyzNDugaNwSKRzF", -137525398263), - ("5EFZf5pnTqLegv6gxCrb6TKBQBGz9xLJNK8x9eR273cSons6", -1521760918), - ("5CGAGEuMLaidBDk8bDZKJb23dxRSP1wLenLALGLw8BTG1E3W", -544739696), - ("5HSzRtcQjD5KP6Nh2GVSS16aLDe6q9R33Wpu6s2eEeeo3AYS", -2309184790), - ("5DALvFDcfANQJcWz6AXMfDqabnoZhdDMoH6FxqYUibug1ja7", -369405632507), - ("5Fy8iWkpcbsskmEN1nYZDdS9zKh167Em9RRYisoss7jaYXxi", 15257429), - ("5CQCVTRyqJgZKBDmtHzpoF8su6BScLcNGbX8t3WMm5qYbbJH", -10721968), - ("5DXZByh2NS4MU61a1aaLrcLYpyzpJgHe95TEBdcEN2cF1SA5", -655946136), - ("5EX5yAYiABFzKDQJDe1kRVwFm3XRRY4HyLMe4Vu9A5U2VEVT", -325581360246), - ("5FvabwjtyW887gtc7vUnUc47KVhy17UeaNLRjzTg5nkVACMP", -77588524213), - ("5HTbYi5cmgWJxvyTy9JeYdtnjoDzjXnEXTGFsPEVx9iRPmVF", -53542953784), - ("5CWzmvA17MAMQ9mnAecLxFXS2N8846rz6T7m4QNHyVtJVq4j", 2672295922502), - ("5DSYntgHZY4krYUtkkQZyyoffVtu5e8rYWhXuhs832zY6YKy", -2680205688), - ("5EYyTFyLDqXscaa5VtXTvUc3x2ow2TeT8G12ZDMZwE6uFWPQ", -39165843935), - ("5CohfM1qdyNwdeJEex1Zyht3S2WS48rV993DmVbyKs2mEEd6", -4004685632), - ("5Gx6Y7UQD39Latgxigr6mHbnh1herpwNPau2PjvzwLWEjXL3", -559504), - ("5Hh4Efq5WDwe8URjjUqUNX8KxtMwLHLViwoRvXfEpXQCZakh", -32541090531), - ("5GWRHC7Nd8njqTPsdJkp6ngniCCBu9UjGhLfxp2jF1fPrfZ4", -5394093031), - ("5GNAB64UN32krzr3Xxu5LW6naeu2P3XULcdBCR9VZ5Libyit", -24884230), - ("5EEz25th1nYNM5xR1UsyFFAUaXMjdHqLxZ3wUjyHokYbXHku", -12525171), - ("5HKJq4JCS9xoKdYhcRnsRp1bodovba7ncd5KTYVwfReKaxHT", -408133990236), - ("5DXs7x664RL5NdSW77DTseLiu84unstuHGuqvmY61UtJwzRN", -3095078614148), - ("5CDfdDaA2p9sK1ia5yMVYfzgtFs2e1TrSAxuQqXoS28Lcrxf", -1032856892), - ("5Ecg4vD2zKXHDFhQqogWq1dZdijPsDty8rGsZu3raeoJSiXb", -995678), - ("5C5Yg63TNLb68Tu819qXd3Bt4giG8mAPzLmAFSqa2HC1R5Rm", -40818739830910), - ("5HHH25Wuf9rmVuk9cMKU1hCCPJ1qbHBd1SyHj91R3fMT36yb", -391416057906), - ("5GKGGE5YLHoDciYJ6Ec2YnUP3SykSQPA47hqmwBP63EtVrd9", -413944553000), - ("5CSi9ZLyiXfLeYtEFaZSBuTofNMRnXEJEJE9CS4gGaT6CkWt", -17811605275), - ("5CSoA7QVdFHHBZz53bbRV2mC5vhL64ehhWa8ibtLppmt2n3J", -65701320107), - ("5GpA5BtfMMX52rXztrha79YqfwR4YaSfTuAcb48Yt73U4h71", -2194562), - ("5Euz5wpb4xiDWfV1A6AKK6i6ca3WoZQD5hCVyf1fws8GXh4z", -6143407839874), - ("5DZzmhCG7SMK3LwrkmHZ8ZBwaAByMjfBpEid14nNQdxHipCE", -386645), - ("5Fc9Vo3hkbr6bPxJpjQo5sQ43L5Hc2G8R5BdqRYF8psvB5pw", 55668553), - ("5GuSHC3iowySHLDW4pEyEZE6PKxKP62YpJYJyBy5tijzAnYz", -159317636526), - ("5HVVZrUBPvjYHiwaSvtvaN9GZogoznM49m2AEmVW6RXnYCka", -1995572213), - ("5EcGpeV2wjkCVsBjsBifSWbdcqH98b6oEY8beDY59c4fXkhw", -177096614584), - ("5GnCjvWJEESwVNFZzy85zbBzw26etuEt87WiqsE3ee2Ws1wm", -1961445), - ("5GWuPUpTuChAqKxvU22TRLvRkBFiyWWZnq9cLpJN6SSvkho1", -94157569391), - ("5FXHf7q5rvBXnzQgmsa31Db9rjcRy6ZHKMiyDSb8Vs5p2msN", -688433531658), - ("5GbxkzytnvbRuNQ7qxPpfPuWMoeitS8V4KDY9jSshE5fDegD", -19085313), - ("5Gus1B7c9uWkky7Yawh2tKR1V6AMh5DbqUBPq881JHqeqVqY", -16101671818), - ("5DLhRdbvWkYYScDmwx4QgJfieSN4apBWbZ2yno3MfgbR8hBP", -21062025), - ("5Cg5kVyNEs7MWWRHU8X5MHwX5cN3aegvC4RBt2JK19w2GiR8", -2593737050), - ("5Dkushsxtc8AdCf287MtTYHQv9DoZeBRpttUpBtmyFhGy3uR", -48672832345630), - ("5EqNqVsHj9bQVyEujcm62zjMYUFhTLY7rTP854txSrJzyoco", -3828526), - ("5Dea6d6nKErEbRQ4MBGuCALn8NZ2xo4kaa51hB5KMriPBkEM", -1560192853875), - ("5DNt2XDWdeMd4H92FLnfUvkqyXzmavezHvzLboP3VgT1xLZV", -831964576998), - ("5FKtFoTeK8aaG6HZTrDgvoYHVQ5NY4S9VyV7W5K74cWcwLYA", -60823501166), - ("5GEBanZKUU7Hrf8K2VNi33HxyJRstgQ3WD3odHgvMj2nPbhi", -98946626902), - ("5CUtw7LYB2n2bzgXt6YnmKDHt6PsB3kKAyD9azYJNcRG8TNg", -9779588557490), - ("5EynbF72b12fbgMvEeL1vJSY342rCryNbuwxFivU1Xevtmv3", -17314385200455), - ("5CapiZRuULed8ConS1gbjMVgnwcT5JnQah7tx6sZnK7sJJuJ", -5810972), - ("5DnaxLaNduf41WM6WWZ4fkzcGzWNWx6eLJyQpSaMueUGCsaU", -12668760), - ("5Cqz9SChYPxTFZ2623rE2aQQ5ttQoLwZ8yfwYgiZyQDANqZn", -683549), - ("5C8ZcLzF23GrXKdH4Pg3ZXC3vKQsF5PM8VvhzzxzTQksgj8e", -44720570590), - ("5GuNsmoswrP6hTKZkKcpTpZftTMKrmnCHvTL2V3NHJy2fpen", -5042891812715), - ("5F1TYDkLnP36HHY5btigxyKUPzBraxdrU1aX1bqFfPfcfnzU", -1189104279832), - ("5Dc384z9HuTGF6oratZs1fLciCHtPZaLhrHfCVw82a5AikWZ", -616163196988), - ("5DhcaEUsRKhZQ31qRffJqjtLmFkbVaCebn8nVjYhvB4KJtX5", -17746006723), - ("5HYE7z3xTcrN1rqz54NyZRAkehFfRMcaEcdoMq5g5ATET5wQ", 212509751245), - ("5F97DdEVTy9gPCtN6jkJJENDJuQiRGiwbMVSL74qRq8FCq5W", 2225287736222), - ("5FUVN133rSvuKXgsXKMR2ZEaysxZjkRUFUWS1UMyNGre9xFV", -73216740161), - ("5CZeimtfpRqQgPxVwr1MzfG2Sok8E1AMERHo6vUmEdRS5JiU", -3937802), - ("5Eqq2JwGh7qbtnjPiFEPmmnHxs3S4J4Ahg8fr4sybZV1tPdY", -173406860562), - ("5ERfDw6K3GmQqwqsEG6foFtu7VsYGifPi556UJKQsBnfbHKN", 96022588728), - ("5Ek8RkU6KMv5Fx7yivRVoQkuJYAKhULWiLWDpbGG4hvR9HFD", 968139369093), - ("5HpCpGALzqgnDTP1HXFiuhzD5MFaDTRHjXBCvaMY9LNNRkT9", 104943979521), - ("5FYqS77gxW9gHG8id1YYPS7Cd4TQmNUMhF8h3S77Fq2VvvRQ", 729199757977), - ("5FtBqMg13pNf1N6TwfG6BmwyaDM77mkeQ16UTHsGasrVDedX", 131457064336), - ("5GbnWR2XhWrRMt123SdrLbR9G2a4N5dtzA3TSu3Czkzoeu7x", 2295599153), - ("5GgiowcCG4kLpwkCTGxxQJQv8WwKFyBQ6McPRmqKtWPy8EaK", 113838605389), - ("5FL5YtYozpUAGaiVWonpbwEYdEMij3obJHSH3ACY4vgWmDgy", 8689039), - ("5Egq58bxRv7boM2s3rnDxx1udnkzxPQ23HuoqohVxjh9RenC", 216373234348), - ("5FRGeeEgRNR8U33FDKvN7yUgts8zR3qRJH4yKKWoR9GswBRb", 2196574958718), - ("5FnhSy79BPYyrmmFsbckinQw1fLiLqqPkQL2vgZwPxbRfu3k", 42319631507), - ("5Hj8jMhqAv7cfyRh5STfbZefMhv17QxZ1RxWq9jNcLAEsRRo", 132216702183491), - ("5EXYTGMqumAH6RLQgHwkMEMnSvHcpHc89R6U8krfNJTYWm9J", 504320264499), - ("5EFh8ctzmytXURqrCTUBWHTs87f7TMWB6XKUzdqxKXVUtvS2", 2209599669432), - ("5CqVqEcRBkw7Gm2reJ33cj7puR9W2Tq7qsLxSruV1BgnMqKN", 1033387458788), - ("5D79enmLSGimsruoraGagofhaSeYJZvGUqFCCrr83ZfZs1HS", 7591184215233), - ("5HbpyjsvyXLWtf1QT1CyNUdyut6scM5dM7ytm8hoxFvRtU1i", 129833188275), - ("5CnxCi7CdEriWSdw4LcXdbtjodxA6uTat4gBm4wuT9QToMdo", 3132978), - ("5G48fiQjhAd8hc4rYc6GituCuAPKznL28jyyyq1auMyZiG4t", 514913328178), - ("5FFGjW2hJ7tQ41qghSsLP4cVmA8j9pZVSrr2CrLG7fQAsLHJ", 346794972723), - ("5FWjnxeRMtMFxRc9kvZKCG5iJAyyz2kmXV8u3kqyiXizZtiz", 225939835005), - ("5CUw3sB4oxd3dVSHUr3kxsB591VEjaPzr444KkfjwVFnLRfJ", 208250614494), - ("5EaBhxNUwMRyKsaeA2BEjDCrvwE5J8FDSpfCHK9gGmnmbhCa", 278083207003), - ("5GHJ5HxFxYQyVoNFUxR3JCqqCKRumaFCY7N5zMxwF4CpRUWr", 1381466224829), - ("5H1WgA7ET3FmEarJK6qc1vaTWbNd6g41mgvyLRkysrH4MDdo", 774889), - ]; - - for (coldkey, diff) in diffs { - let coldkey_account_id = decode_account_id32::(coldkey).expect("Invalid coldkey"); - if diff > 0 { - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - diff.unsigned_abs().into(), - ); - } - } - - // For one of the gainers, add some extra stake - let idx = 6; - let gained_ck = diffs[idx].0; - let coldkey_account_id = decode_account_id32::(gained_ck).expect("Invalid coldkey"); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - num_traits::ToPrimitive::to_u64( - &((num_traits::ToPrimitive::to_f64(&diffs[idx].1).expect("float conv fail") - * 0.9_f64) - .abs()), - ) - .expect("u64 conv fail") - .into(), - ); - let extra_balance = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - ) - .saturating_sub( - num_traits::ToPrimitive::to_u64(&diffs[idx].1.abs()) - .expect("float conv fail") - .into(), - ); - assert!( - extra_balance.to_u64() > 0_u64, - "extra balance must be positive" - ); - - let w = try_restore_shares::(); - assert!(!w.is_zero(), "weight must be non-zero"); - - // Check stake is near 0 for all positive entires except the one we removed - // Check the stake for all negative entries is proportional to the amount they lost - let total_lost: f64 = diffs - .iter() - .map(|(_, diff)| { - if diff.is_negative() { - num_traits::ToPrimitive::to_f64(&diff.saturating_abs()) - .expect("float conv fail") - } else { - 0_f64 - } - }) - .sum::(); - let mut total_returned = 0_f64; - for (coldkey, diff) in diffs { - let coldkey_account_id = decode_account_id32::(coldkey).expect("Invalid coldkey"); - - let stake_float: f64 = num_traits::ToPrimitive::to_f64( - &SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - ) - .to_u64(), - ) - .expect("float conv fail"); - if diff > 0 { - if coldkey == gained_ck { - // this CK should retain the extra balance - assert_relative_eq!( - stake_float, - num_traits::ToPrimitive::to_f64(&extra_balance.to_u64()) - .expect("float conv fail"), - max_relative = 0.001_f64 - ); - } else { - assert_relative_eq!(stake_float, 0_f64, max_relative = 0.001_f64); - } - } else { - total_returned += stake_float; - } - } - - for (coldkey, diff) in diffs { - let coldkey_account_id = decode_account_id32::(coldkey).expect("Invalid coldkey"); - let stake_float: f64 = num_traits::ToPrimitive::to_f64( - &SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - ) - .to_u64(), - ) - .expect("float conv fail"); - if diff < 0 { - // Should get a return proportional to the amount they lost - // versus the amount that was able to be recovered - let prop_returned: f64 = num_traits::ToPrimitive::to_f64(&diff.abs()) - .expect("float conv fail") - / total_lost - * total_returned; - assert_relative_eq!(stake_float, prop_returned, max_relative = 0.001_f64); - } - } - }); -} - -#[test] -fn test_migrate_fix_bad_hk_swap_mainnet_some_entries() { - // test with some of the losers have an existing balance before the migration - new_test_ext(1).execute_with(|| { - use crate::migrations::migrate_fix_bad_hk_swap::*; - - let netuid = NetUid::from(59); - // Add subnet 59 - add_network(netuid, 10, 0); - SubtokenEnabled::::insert(netuid, true); - SubnetMechanism::::insert(netuid, 1); - - let hotkey = "5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN"; - let hotkey_account_id = decode_account_id32::(hotkey).expect("Invalid hotkey"); - - #[rustfmt::skip] - let diffs: [(&str, i64); 112] = [ - ("5Fn9SqQhx5bhDua7AGgkKxxk3gfZ75WWBGCMPeKH1WBgPaMQ", -2375685930981_i64), - ("5Fnhtm7cpxEbZaChnRZ8yWoF8MXVxmobkmLRehh5bkYtyZA9", -4090996138227), - ("5C7j3w2zz1SVejRuFrb2zFWHXT7UfG7eWA87KXL1WyV5KLVR", -607494031), - ("5DthZ1rvnXBb9oXVNtrMaMsDAnRxBPZCjD6fdRdeqC3fg1ca", -17022477949), - ("5F7BkPL3EVjKTYMbBkEmPAtTZQSGeyNzFPaf1DtebPFmJsJ7", -4016510), - ("5EefisctzgWdVGFQaL4LjFFacTE7dM4YJVNy3ogGBQoapTU1", -13106893093), - ("5CwkvpBxHCaRK9xBC2n6WdhpF5zg9t5WLkGorASaoErdynFQ", 439139249152), - ("5FU7ErUtmi22xuqeeCYVpNZp6WVSSL98hqDi5iyeZbkXtkbe", -35958768555), - ("5D7HL8T95qkHQTPFjgSFCjRoeM7oE3vQBYjiR1kAPbPxcMKu", -201914811997), - ("5HL3pPdDFY94Qdf8VnbfT4W6LXFkpd68Y5GSGzNJfntMdGZX", -235660917467), - ("5EcYAz8SBKWsogA6meJmVXcwVp4tjCvw3ZnJE6UXTyWNUdF2", -500070769668), - ("5EoE3c7XMf8TN3yudAaFjv4yvjtWYRviHcXi73EXkLHmWTCB", -86442928436), - ("5CMDjL7t2biHGREBwrmd8renD74FLEhjCVqfJG2MXckWBwDu", 1039317), - ("5CVGKimL4cLgyTqvYKQbPKYFZfiztsdczU7HrwNdSFKbbn5D", 4224201), - ("5HmZnEcW4eHbXmUEFWJbc4GHnBBYEK8ZPsFa25PuEmP5iuwM", -13156128), - ("5FNa4J4fTKh555CEyXHgR29RicSm8nTEHx36utTa4MJepJyX", -9519954), - ("5Gun93uQgffYpxqMKSmfG18AHiQW7Z2GR2dfPPR8W188vJYc", -1127662), - ("5HW1C4js4RyjQqNwALSUZC8NJ2WinD5Si2X2XkstXrMW2uYo", -34457336758), - ("5EqMhjdLY9h64ui2mizRZyBp1mEPJ7s4TsfAxQSQkAFmMfzE", -9346443829744), - ("5H3XwzydgE2XUGoJCR4dSj7tkd7uxZDJqik69hux2DBcruom", -1215347774), - ("5DjkmYpCUX6dBTvGoyN9j4QZhtMPhdcywDE8cJ8Qq1vg4X6e", -3603984447), - ("5E7Z7Btjz74XpZLH5fRzfZqiHCo4j9PXKfqi88kQ5MFrds34", -823907380854), - ("5DSBWN4hN9413C6o6A2hR9tYUbHjWsQqPRV74GrnCrMkCGJx", -309708781), - ("5FMLRmKPqsTsMbakpVUwoYro1P64QXVNTWyzNDugaNwSKRzF", -137525398263), - ("5EFZf5pnTqLegv6gxCrb6TKBQBGz9xLJNK8x9eR273cSons6", -1521760918), - ("5CGAGEuMLaidBDk8bDZKJb23dxRSP1wLenLALGLw8BTG1E3W", -544739696), - ("5HSzRtcQjD5KP6Nh2GVSS16aLDe6q9R33Wpu6s2eEeeo3AYS", -2309184790), - ("5DALvFDcfANQJcWz6AXMfDqabnoZhdDMoH6FxqYUibug1ja7", -369405632507), - ("5Fy8iWkpcbsskmEN1nYZDdS9zKh167Em9RRYisoss7jaYXxi", 15257429), - ("5CQCVTRyqJgZKBDmtHzpoF8su6BScLcNGbX8t3WMm5qYbbJH", -10721968), - ("5DXZByh2NS4MU61a1aaLrcLYpyzpJgHe95TEBdcEN2cF1SA5", -655946136), - ("5EX5yAYiABFzKDQJDe1kRVwFm3XRRY4HyLMe4Vu9A5U2VEVT", -325581360246), - ("5FvabwjtyW887gtc7vUnUc47KVhy17UeaNLRjzTg5nkVACMP", -77588524213), - ("5HTbYi5cmgWJxvyTy9JeYdtnjoDzjXnEXTGFsPEVx9iRPmVF", -53542953784), - ("5CWzmvA17MAMQ9mnAecLxFXS2N8846rz6T7m4QNHyVtJVq4j", 2672295922502), - ("5DSYntgHZY4krYUtkkQZyyoffVtu5e8rYWhXuhs832zY6YKy", -2680205688), - ("5EYyTFyLDqXscaa5VtXTvUc3x2ow2TeT8G12ZDMZwE6uFWPQ", -39165843935), - ("5CohfM1qdyNwdeJEex1Zyht3S2WS48rV993DmVbyKs2mEEd6", -4004685632), - ("5Gx6Y7UQD39Latgxigr6mHbnh1herpwNPau2PjvzwLWEjXL3", -559504), - ("5Hh4Efq5WDwe8URjjUqUNX8KxtMwLHLViwoRvXfEpXQCZakh", -32541090531), - ("5GWRHC7Nd8njqTPsdJkp6ngniCCBu9UjGhLfxp2jF1fPrfZ4", -5394093031), - ("5GNAB64UN32krzr3Xxu5LW6naeu2P3XULcdBCR9VZ5Libyit", -24884230), - ("5EEz25th1nYNM5xR1UsyFFAUaXMjdHqLxZ3wUjyHokYbXHku", -12525171), - ("5HKJq4JCS9xoKdYhcRnsRp1bodovba7ncd5KTYVwfReKaxHT", -408133990236), - ("5DXs7x664RL5NdSW77DTseLiu84unstuHGuqvmY61UtJwzRN", -3095078614148), - ("5CDfdDaA2p9sK1ia5yMVYfzgtFs2e1TrSAxuQqXoS28Lcrxf", -1032856892), - ("5Ecg4vD2zKXHDFhQqogWq1dZdijPsDty8rGsZu3raeoJSiXb", -995678), - ("5C5Yg63TNLb68Tu819qXd3Bt4giG8mAPzLmAFSqa2HC1R5Rm", -40818739830910), - ("5HHH25Wuf9rmVuk9cMKU1hCCPJ1qbHBd1SyHj91R3fMT36yb", -391416057906), - ("5GKGGE5YLHoDciYJ6Ec2YnUP3SykSQPA47hqmwBP63EtVrd9", -413944553000), - ("5CSi9ZLyiXfLeYtEFaZSBuTofNMRnXEJEJE9CS4gGaT6CkWt", -17811605275), - ("5CSoA7QVdFHHBZz53bbRV2mC5vhL64ehhWa8ibtLppmt2n3J", -65701320107), - ("5GpA5BtfMMX52rXztrha79YqfwR4YaSfTuAcb48Yt73U4h71", -2194562), - ("5Euz5wpb4xiDWfV1A6AKK6i6ca3WoZQD5hCVyf1fws8GXh4z", -6143407839874), - ("5DZzmhCG7SMK3LwrkmHZ8ZBwaAByMjfBpEid14nNQdxHipCE", -386645), - ("5Fc9Vo3hkbr6bPxJpjQo5sQ43L5Hc2G8R5BdqRYF8psvB5pw", 55668553), - ("5GuSHC3iowySHLDW4pEyEZE6PKxKP62YpJYJyBy5tijzAnYz", -159317636526), - ("5HVVZrUBPvjYHiwaSvtvaN9GZogoznM49m2AEmVW6RXnYCka", -1995572213), - ("5EcGpeV2wjkCVsBjsBifSWbdcqH98b6oEY8beDY59c4fXkhw", -177096614584), - ("5GnCjvWJEESwVNFZzy85zbBzw26etuEt87WiqsE3ee2Ws1wm", -1961445), - ("5GWuPUpTuChAqKxvU22TRLvRkBFiyWWZnq9cLpJN6SSvkho1", -94157569391), - ("5FXHf7q5rvBXnzQgmsa31Db9rjcRy6ZHKMiyDSb8Vs5p2msN", -688433531658), - ("5GbxkzytnvbRuNQ7qxPpfPuWMoeitS8V4KDY9jSshE5fDegD", -19085313), - ("5Gus1B7c9uWkky7Yawh2tKR1V6AMh5DbqUBPq881JHqeqVqY", -16101671818), - ("5DLhRdbvWkYYScDmwx4QgJfieSN4apBWbZ2yno3MfgbR8hBP", -21062025), - ("5Cg5kVyNEs7MWWRHU8X5MHwX5cN3aegvC4RBt2JK19w2GiR8", -2593737050), - ("5Dkushsxtc8AdCf287MtTYHQv9DoZeBRpttUpBtmyFhGy3uR", -48672832345630), - ("5EqNqVsHj9bQVyEujcm62zjMYUFhTLY7rTP854txSrJzyoco", -3828526), - ("5Dea6d6nKErEbRQ4MBGuCALn8NZ2xo4kaa51hB5KMriPBkEM", -1560192853875), - ("5DNt2XDWdeMd4H92FLnfUvkqyXzmavezHvzLboP3VgT1xLZV", -831964576998), - ("5FKtFoTeK8aaG6HZTrDgvoYHVQ5NY4S9VyV7W5K74cWcwLYA", -60823501166), - ("5GEBanZKUU7Hrf8K2VNi33HxyJRstgQ3WD3odHgvMj2nPbhi", -98946626902), - ("5CUtw7LYB2n2bzgXt6YnmKDHt6PsB3kKAyD9azYJNcRG8TNg", -9779588557490), - ("5EynbF72b12fbgMvEeL1vJSY342rCryNbuwxFivU1Xevtmv3", -17314385200455), - ("5CapiZRuULed8ConS1gbjMVgnwcT5JnQah7tx6sZnK7sJJuJ", -5810972), - ("5DnaxLaNduf41WM6WWZ4fkzcGzWNWx6eLJyQpSaMueUGCsaU", -12668760), - ("5Cqz9SChYPxTFZ2623rE2aQQ5ttQoLwZ8yfwYgiZyQDANqZn", -683549), - ("5C8ZcLzF23GrXKdH4Pg3ZXC3vKQsF5PM8VvhzzxzTQksgj8e", -44720570590), - ("5GuNsmoswrP6hTKZkKcpTpZftTMKrmnCHvTL2V3NHJy2fpen", -5042891812715), - ("5F1TYDkLnP36HHY5btigxyKUPzBraxdrU1aX1bqFfPfcfnzU", -1189104279832), - ("5Dc384z9HuTGF6oratZs1fLciCHtPZaLhrHfCVw82a5AikWZ", -616163196988), - ("5DhcaEUsRKhZQ31qRffJqjtLmFkbVaCebn8nVjYhvB4KJtX5", -17746006723), - ("5HYE7z3xTcrN1rqz54NyZRAkehFfRMcaEcdoMq5g5ATET5wQ", 212509751245), - ("5F97DdEVTy9gPCtN6jkJJENDJuQiRGiwbMVSL74qRq8FCq5W", 2225287736222), - ("5FUVN133rSvuKXgsXKMR2ZEaysxZjkRUFUWS1UMyNGre9xFV", -73216740161), - ("5CZeimtfpRqQgPxVwr1MzfG2Sok8E1AMERHo6vUmEdRS5JiU", -3937802), - ("5Eqq2JwGh7qbtnjPiFEPmmnHxs3S4J4Ahg8fr4sybZV1tPdY", -173406860562), - ("5ERfDw6K3GmQqwqsEG6foFtu7VsYGifPi556UJKQsBnfbHKN", 96022588728), - ("5Ek8RkU6KMv5Fx7yivRVoQkuJYAKhULWiLWDpbGG4hvR9HFD", 968139369093), - ("5HpCpGALzqgnDTP1HXFiuhzD5MFaDTRHjXBCvaMY9LNNRkT9", 104943979521), - ("5FYqS77gxW9gHG8id1YYPS7Cd4TQmNUMhF8h3S77Fq2VvvRQ", 729199757977), - ("5FtBqMg13pNf1N6TwfG6BmwyaDM77mkeQ16UTHsGasrVDedX", 131457064336), - ("5GbnWR2XhWrRMt123SdrLbR9G2a4N5dtzA3TSu3Czkzoeu7x", 2295599153), - ("5GgiowcCG4kLpwkCTGxxQJQv8WwKFyBQ6McPRmqKtWPy8EaK", 113838605389), - ("5FL5YtYozpUAGaiVWonpbwEYdEMij3obJHSH3ACY4vgWmDgy", 8689039), - ("5Egq58bxRv7boM2s3rnDxx1udnkzxPQ23HuoqohVxjh9RenC", 216373234348), - ("5FRGeeEgRNR8U33FDKvN7yUgts8zR3qRJH4yKKWoR9GswBRb", 2196574958718), - ("5FnhSy79BPYyrmmFsbckinQw1fLiLqqPkQL2vgZwPxbRfu3k", 42319631507), - ("5Hj8jMhqAv7cfyRh5STfbZefMhv17QxZ1RxWq9jNcLAEsRRo", 132216702183491), - ("5EXYTGMqumAH6RLQgHwkMEMnSvHcpHc89R6U8krfNJTYWm9J", 504320264499), - ("5EFh8ctzmytXURqrCTUBWHTs87f7TMWB6XKUzdqxKXVUtvS2", 2209599669432), - ("5CqVqEcRBkw7Gm2reJ33cj7puR9W2Tq7qsLxSruV1BgnMqKN", 1033387458788), - ("5D79enmLSGimsruoraGagofhaSeYJZvGUqFCCrr83ZfZs1HS", 7591184215233), - ("5HbpyjsvyXLWtf1QT1CyNUdyut6scM5dM7ytm8hoxFvRtU1i", 129833188275), - ("5CnxCi7CdEriWSdw4LcXdbtjodxA6uTat4gBm4wuT9QToMdo", 3132978), - ("5G48fiQjhAd8hc4rYc6GituCuAPKznL28jyyyq1auMyZiG4t", 514913328178), - ("5FFGjW2hJ7tQ41qghSsLP4cVmA8j9pZVSrr2CrLG7fQAsLHJ", 346794972723), - ("5FWjnxeRMtMFxRc9kvZKCG5iJAyyz2kmXV8u3kqyiXizZtiz", 225939835005), - ("5CUw3sB4oxd3dVSHUr3kxsB591VEjaPzr444KkfjwVFnLRfJ", 208250614494), - ("5EaBhxNUwMRyKsaeA2BEjDCrvwE5J8FDSpfCHK9gGmnmbhCa", 278083207003), - ("5GHJ5HxFxYQyVoNFUxR3JCqqCKRumaFCY7N5zMxwF4CpRUWr", 1381466224829), - ("5H1WgA7ET3FmEarJK6qc1vaTWbNd6g41mgvyLRkysrH4MDdo", 774889), - ]; - - for (coldkey, diff) in diffs { - let coldkey_account_id = decode_account_id32::(coldkey).expect("Invalid coldkey"); - if diff > 0 { - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - diff.unsigned_abs().into(), - ); - } - } - - // For one of the losers, add some extra stake - let idx = 0; - let lost_ck = diffs[idx].0; - let coldkey_account_id = decode_account_id32::(lost_ck).expect("Invalid coldkey"); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - num_traits::ToPrimitive::to_u64( - &((num_traits::ToPrimitive::to_f64(&diffs[idx].1).expect("float conv fail") - * 0.9_f64) - .abs()), - ) - .expect("u64 conv fail") - .into(), - ); - let extra_balance = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - ); - assert!(!extra_balance.is_zero(), "extra balance must be non-zero"); - - let w = try_restore_shares::(); - assert!(!w.is_zero(), "weight must be non-zero"); - - // Check stake is near 0 for all positive entires except the one we removed - // Check the stake for all negative entries is proportional to the amount they lost - let total_lost: f64 = diffs - .iter() - .map(|(_, diff)| { - if diff.is_negative() { - num_traits::ToPrimitive::to_f64(&diff.saturating_abs()) - .expect("float conv fail") - } else { - 0_f64 - } - }) - .sum::(); - let mut total_returned = 0_f64; - for (coldkey, diff) in diffs { - let coldkey_account_id = decode_account_id32::(coldkey).expect("Invalid coldkey"); - - let stake_float: f64 = num_traits::ToPrimitive::to_f64( - &SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - ) - .to_u64(), - ) - .expect("float conv fail"); - if diff > 0 { - assert_relative_eq!(stake_float, 0_f64, max_relative = 0.001_f64); - } else if coldkey == lost_ck { - total_returned += stake_float - - num_traits::ToPrimitive::to_f64(&extra_balance.to_u64()) - .expect("float conv fail"); - } else { - total_returned += stake_float; - } - } - - for (coldkey, diff) in diffs { - let coldkey_account_id = decode_account_id32::(coldkey).expect("Invalid coldkey"); - let stake_float: f64 = num_traits::ToPrimitive::to_f64( - &SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - ) - .to_u64(), - ) - .expect("float conv fail"); - if diff < 0 { - // Should get a return proportional to the amount they lost - // versus the amount that was able to be recovered - let prop_returned: f64 = num_traits::ToPrimitive::to_f64(&diff.abs()) - .expect("float conv fail") - / total_lost - * total_returned; - - let mut expected_stake: f64 = prop_returned; - if coldkey == lost_ck { - // this CK should retain the extra balance - expected_stake = prop_returned - + num_traits::ToPrimitive::to_f64(&extra_balance.to_u64()) - .expect("float conv fail"); - } - - assert_relative_eq!(stake_float, expected_stake, max_relative = 0.001_f64); - } - } - }); -} - -fn decode_account_id32_test(ss58_string: &str) -> U256 { - let account_id32: AccountId32 = AccountId32::from_ss58check(ss58_string).unwrap(); - let mut account_id32_slice: &[u8] = account_id32.as_ref(); - U256::decode(&mut account_id32_slice).unwrap() -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::migration::test_migrate_fix_root_claimed_overclaim --exact --nocapture -#[test] -fn test_migrate_fix_root_claimed_overclaim() { - use crate::migrations::migrate_fix_root_claimed_overclaim::*; - - let new_hotkey = decode_account_id32_test("5H6BqkzjYvViiqp7rQLXjpnaEmW7U9CoKxXhQ4efMqtX1mQw"); - let untouched_hotkey = U256::from(7777_u64); - let coldkey_a = U256::from(42_u64); - let coldkey_b = U256::from(43_u64); - - let root_netuid = NetUid::from(0_u16); - let netuid_a = NetUid::from(27_u16); - let netuid_b = NetUid::from(1_u16); - - let mainnet_genesis = - hex_literal::hex!("2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03"); - const MIGRATION_NAME: &[u8] = b"migrate_fix_root_claimed_overclaim"; - - // CASE 1: new hotkey has no root stake → RootClaimable is cleared - new_test_ext(1).execute_with(|| { - frame_system::BlockHash::::insert(0u64, H256::from_slice(&mainnet_genesis)); - - RootClaimable::::mutate(new_hotkey, |map| { - map.insert(netuid_a, I96F32::from_num(500_000_u64)); - map.insert(netuid_b, I96F32::from_num(300_000_u64)); - }); - RootClaimed::::insert((netuid_a, new_hotkey, coldkey_a), 999u128); - RootClaimed::::insert((netuid_b, new_hotkey, coldkey_b), 111u128); - - // Unrelated hotkey's claimed entry must stay intact - RootClaimable::::mutate(untouched_hotkey, |map| { - map.insert(netuid_a, I96F32::from_num(42_u64)); - }); - RootClaimed::::insert((netuid_a, untouched_hotkey, coldkey_a), 555u128); - - assert!(!HasMigrationRun::::get(MIGRATION_NAME.to_vec())); - - let w = migrate_fix_root_claimed_overclaim::(); - assert!(!w.is_zero()); - assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); - - assert!( - RootClaimable::::get(new_hotkey).is_empty(), - "new hotkey RootClaimable must be cleared" - ); - assert_eq!( - RootClaimed::::get((netuid_a, new_hotkey, coldkey_a)), - 999u128, - "RootClaimed entries must be left intact" - ); - assert_eq!( - RootClaimed::::get((netuid_b, new_hotkey, coldkey_b)), - 111u128, - "RootClaimed entries must be left intact" - ); - - assert_eq!( - RootClaimable::::get(untouched_hotkey) - .get(&netuid_a) - .copied(), - Some(I96F32::from_num(42_u64)) - ); - assert_eq!( - RootClaimed::::get((netuid_a, untouched_hotkey, coldkey_a)), - 555u128 - ); - }); - - // CASE 2: new hotkey has root stake → state is preserved - new_test_ext(1).execute_with(|| { - frame_system::BlockHash::::insert(0u64, H256::from_slice(&mainnet_genesis)); - - RootClaimable::::mutate(new_hotkey, |map| { - map.insert(netuid_a, I96F32::from_num(500_000_u64)); - }); - RootClaimed::::insert((netuid_a, new_hotkey, coldkey_a), 999u128); - - TotalHotkeyAlpha::::insert(new_hotkey, root_netuid, AlphaBalance::from(1_000u64)); - - let w = migrate_fix_root_claimed_overclaim::(); - assert!(!w.is_zero()); - assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); - - assert_eq!( - RootClaimable::::get(new_hotkey) - .get(&netuid_a) - .copied(), - Some(I96F32::from_num(500_000_u64)), - "must not clear when new hotkey still holds root stake" - ); - assert_eq!( - RootClaimed::::get((netuid_a, new_hotkey, coldkey_a)), - 999u128 - ); - }); - - // CASE 3: idempotency — second run is a no-op - new_test_ext(1).execute_with(|| { - frame_system::BlockHash::::insert(0u64, H256::from_slice(&mainnet_genesis)); - HasMigrationRun::::insert(MIGRATION_NAME.to_vec(), true); - - RootClaimable::::mutate(new_hotkey, |map| { - map.insert(netuid_a, I96F32::from_num(777_u64)); - }); - - let w = migrate_fix_root_claimed_overclaim::(); - assert_eq!( - w, - ::DbWeight::get().reads(1), - "second run should only read the migration flag" - ); - assert_eq!( - RootClaimable::::get(new_hotkey) - .get(&netuid_a) - .copied(), - Some(I96F32::from_num(777_u64)) - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::migration::test_migrate_fix_root_claimed_incorrect_genesis --exact --nocapture -#[test] -fn test_migrate_fix_root_claimed_incorrect_genesis() { - use crate::migrations::migrate_fix_root_claimed_overclaim::*; - - let old_hotkey = decode_account_id32_test("5GmvyePN9aYErXBBhBnxZKGoGk4LKZApE4NkaSzW62CYCYNA"); - let new_hotkey = decode_account_id32_test("5H6BqkzjYvViiqp7rQLXjpnaEmW7U9CoKxXhQ4efMqtX1mQw"); - let coldkey = U256::from(42_u64); - - let netuid_target = NetUid::from(27_u16); - let netuid_other = NetUid::from(1_u16); - - let mainnet_genesis = - hex_literal::hex!("2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03"); - const MIGRATION_NAME: &[u8] = b"migrate_fix_root_claimed_overclaim"; - - // CASE 2: non-mainnet genesis — full no-op - new_test_ext(1).execute_with(|| { - frame_system::BlockHash::::insert(0u64, H256::from_low_u64_be(0xdeadbeef)); - - RootClaimable::::mutate(new_hotkey, |map| { - map.insert(netuid_target, I96F32::from_num(123_u64)); - }); - Alpha::::insert( - (new_hotkey, coldkey, netuid_target), - U64F64::from_num(1_000_u64), - ); - - let w = migrate_fix_root_claimed_overclaim::(); - assert!( - !w.is_zero(), - "weight must be non-zero (writes migration flag)" - ); - assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); - - assert!( - RootClaimable::::get(old_hotkey).is_empty(), - "migration must not touch storage on non-mainnet" - ); - assert!( - RootClaimable::::get(new_hotkey).contains_key(&netuid_target), - "new_hotkey data must remain untouched on non-mainnet" - ); - }); -} - -#[test] -fn test_migrate_subnet_balances() { - new_test_ext(1).execute_with(|| { - let netuid1 = NetUid::from(1); - let netuid2 = NetUid::from(2); - add_network(netuid1, 1, 0); - add_network(netuid2, 1, 0); - - // Add network locks - let lock1 = TaoBalance::from(123_000_000_000_u64); - let lock2 = TaoBalance::from(321_000_000_000_u64); - SubnetLocked::::insert(netuid1, lock1); - SubnetLocked::::insert(netuid2, lock2); - - // Add SubnetTAO - let reserve1 = TaoBalance::from(456_000_000_000_u64); - let reserve2 = TaoBalance::from(654_000_000_000_u64); - SubnetTAO::::insert(netuid1, reserve1); - SubnetTAO::::insert(netuid2, reserve2); - - // Run migration - crate::migrations::migrate_subnet_balances::migrate_subnet_balances::(); - - // Test that subnet balances got updated - let subnet_account_1 = SubtensorModule::get_subnet_account_id(netuid1).unwrap(); - let subnet_account_2 = SubtensorModule::get_subnet_account_id(netuid2).unwrap(); - let balance1 = SubtensorModule::get_coldkey_balance(&subnet_account_1); - let balance2 = SubtensorModule::get_coldkey_balance(&subnet_account_2); - let initial_pool_tao = NetworkMinLockCost::::get(); - assert_eq!(balance1, lock1 + reserve1 - initial_pool_tao); - assert_eq!(balance2, lock2 + reserve2 - initial_pool_tao); - - // Check migration has been marked as run - const MIGRATION_NAME: &[u8] = b"migrate_subnet_balances"; - assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); - }); -} - -#[test] -fn test_migrate_fix_total_issuance_evm_fees() { - new_test_ext(1).execute_with(|| { - const MIGRATION_NAME: &[u8] = b"migrate_fix_total_issuance_evm_fees"; - const DUST_MIGRATION_NAME: &[u8] = b"migrate_fix_total_issuance_after_dust_collection"; - - let account = U256::from(42); - let balances_total_issuance = TaoBalance::from(123_456_789_u64); - Balances::make_free_balance_be(&account, balances_total_issuance); - - let broken_subtensor_total_issuance = TaoBalance::from(987_654_321_u64); - TotalIssuance::::put(broken_subtensor_total_issuance); - - assert_eq!(Balances::total_issuance(), balances_total_issuance); - assert_eq!( - TotalIssuance::::get(), - broken_subtensor_total_issuance - ); - assert!(!HasMigrationRun::::get(MIGRATION_NAME.to_vec())); - - let weight = crate::migrations::migrate_fix_total_issuance_evm_fees::migrate_fix_total_issuance_evm_fees::(); - - assert!(!weight.is_zero(), "weight must be non-zero"); - assert_eq!(TotalIssuance::::get(), balances_total_issuance); - assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); - assert!(!HasMigrationRun::::get( - DUST_MIGRATION_NAME.to_vec() - )); - - let second_wrong_value = TaoBalance::from(555_u64); - TotalIssuance::::put(second_wrong_value); - - crate::migrations::migrate_fix_total_issuance_evm_fees::migrate_fix_total_issuance_evm_fees::(); - - assert_eq!(TotalIssuance::::get(), balances_total_issuance); - assert!(HasMigrationRun::::get( - DUST_MIGRATION_NAME.to_vec() - )); - - let third_wrong_value = TaoBalance::from(777_u64); - TotalIssuance::::put(third_wrong_value); - - crate::migrations::migrate_fix_total_issuance_evm_fees::migrate_fix_total_issuance_evm_fees::(); - - assert_eq!( - TotalIssuance::::get(), - third_wrong_value, - "migration must not run after all known migration keys have run" - ); - }); -} - -#[test] -fn test_migrate_reset_tnet_conviction_locks() { - new_test_ext(1).execute_with(|| { - const MIGRATION_NAME: &[u8] = b"migrate_reset_tnet_conviction_locks"; - - let netuid = NetUid::from(1); - let other_netuid = NetUid::from(2); - let coldkey_1 = U256::from(1001); - let coldkey_2 = U256::from(1002); - let hotkey_1 = U256::from(2001); - let hotkey_2 = U256::from(2002); - - let lock_1 = LockState { - locked_mass: AlphaBalance::from(10_u64), - conviction: U64F64::from_num(1.5), - last_update: 11, - }; - let lock_2 = LockState { - locked_mass: AlphaBalance::from(20_u64), - conviction: U64F64::from_num(2.5), - last_update: 22, - }; - - Lock::::insert((coldkey_1, netuid, hotkey_1), lock_1.clone()); - Lock::::insert((coldkey_2, other_netuid, hotkey_2), lock_2.clone()); - HotkeyLock::::insert(netuid, hotkey_1, lock_1.clone()); - DecayingHotkeyLock::::insert(other_netuid, hotkey_2, lock_2.clone()); - OwnerLock::::insert(netuid, lock_1.clone()); - DecayingOwnerLock::::insert(other_netuid, lock_2.clone()); - DecayingLock::::insert(coldkey_1, netuid, false); - DecayingLock::::insert(coldkey_2, other_netuid, false); - - assert!(!HasMigrationRun::::get(MIGRATION_NAME.to_vec())); - assert_eq!(Lock::::iter().count(), 2); - assert_eq!(HotkeyLock::::iter().count(), 1); - assert_eq!(DecayingHotkeyLock::::iter().count(), 1); - assert_eq!(OwnerLock::::iter().count(), 1); - assert_eq!(DecayingOwnerLock::::iter().count(), 1); - assert_eq!(DecayingLock::::iter().count(), 2); - - let raw_owner_lock_key = { - let mut key = Vec::new(); - key.extend_from_slice(&twox_128("SubtensorModule".as_bytes())); - key.extend_from_slice(&twox_128("OwnerLock".as_bytes())); - key.extend_from_slice(&NetUid::from(99).encode()); - key - }; - let raw_decaying_hotkey_lock_key = { - let mut key = Vec::new(); - key.extend_from_slice(&twox_128("SubtensorModule".as_bytes())); - key.extend_from_slice(&twox_128("DecayingHotkeyLock".as_bytes())); - key.extend_from_slice(&NetUid::from(100).encode()); - key.extend_from_slice(&Blake2_128Concat::hash(&U256::from(3003).encode())); - key - }; - - // Simulate deprecated aggregate entries with bytes that the current - // `LockState` type should never need to decode during this reset. - put_raw(&raw_owner_lock_key, &123_u32.encode()); - put_raw(&raw_decaying_hotkey_lock_key, &(456_u32, 789_u32).encode()); - assert!(get_raw(&raw_owner_lock_key).is_some()); - assert!(get_raw(&raw_decaying_hotkey_lock_key).is_some()); - - let weight = - crate::migrations::migrate_reset_tnet_conviction_locks::migrate_reset_tnet_conviction_locks::(); - - assert!(!weight.is_zero(), "migration weight should be non-zero"); - assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); - assert!(get_raw(&raw_owner_lock_key).is_none()); - assert!(get_raw(&raw_decaying_hotkey_lock_key).is_none()); - assert_eq!(Lock::::iter().count(), 0); - assert_eq!(HotkeyLock::::iter().count(), 0); - assert_eq!(DecayingHotkeyLock::::iter().count(), 0); - assert_eq!(OwnerLock::::iter().count(), 0); - assert_eq!(DecayingOwnerLock::::iter().count(), 0); - assert_eq!(DecayingLock::::iter().count(), 0); - - Lock::::insert((coldkey_1, netuid, hotkey_1), lock_1); - let second_weight = - crate::migrations::migrate_reset_tnet_conviction_locks::migrate_reset_tnet_conviction_locks::(); - - assert_eq!( - second_weight, - ::DbWeight::get().reads(1), - "second run should only read the migration flag" - ); - assert_eq!( - Lock::::iter().count(), - 1, - "migration must not run more than once" - ); - }); -} - -#[test] -fn test_migrate_dynamic_tempo_aligns_first_post_upgrade_fire() { - new_test_ext(1).execute_with(|| { - const MIGRATION_NAME: &str = "dynamic_tempo_v1"; - let netuid = NetUid::from(7u16); - let tempo: u16 = 360; - - add_network(netuid, tempo, 0); - let current_block = 1234u64; - run_to_block(current_block); - - // Compute next-fire block - let netuid_plus_one = (u16::from(netuid) as u64) + 1; - let tempo_plus_one = (tempo as u64) + 1; - let adjusted = current_block + netuid_plus_one; - let remainder = adjusted % tempo_plus_one; - let legacy_blocks_until_next = (tempo as u64) - remainder; - let expected_next_fire = current_block + legacy_blocks_until_next; - - crate::migrations::migrate_dynamic_tempo::migrate_dynamic_tempo::(); - - // New formula: next fire = LastEpochBlock + tempo. - let last_epoch = LastEpochBlock::::get(netuid); - assert_eq!( - last_epoch + tempo as u64, - expected_next_fire, - "back-fill should make new scheduler fire at the same block as legacy modulo" - ); - assert!(HasMigrationRun::::get( - MIGRATION_NAME.as_bytes().to_vec() - )); - }); -} - -#[test] -fn test_migrate_dynamic_tempo_preserves_non_standard_tempo() { - new_test_ext(1).execute_with(|| { - // Three subnets — one standard, two with non-standard tempo - // (simulates the 2 mainnet subnets root configured outside MIN/MAX bounds). - let standard = NetUid::from(1u16); - let small = NetUid::from(2u16); - let large = NetUid::from(3u16); - - add_network(standard, 360, 0); - add_network(small, 10, 0); // < MIN_TEMPO (360) - add_network(large, 60_000, 0); // > MAX_TEMPO (50_400) - - crate::migrations::migrate_dynamic_tempo::migrate_dynamic_tempo::(); - - // Tempo values preserved as-is — no clamp. - assert_eq!(Tempo::::get(standard), 360); - assert_eq!(Tempo::::get(small), 10); - assert_eq!(Tempo::::get(large), 60_000); - - // All non-zero tempos got LastEpochBlock seeded. - assert!(LastEpochBlock::::contains_key(standard)); - assert!(LastEpochBlock::::contains_key(small)); - assert!(LastEpochBlock::::contains_key(large)); - }); -} - -#[test] -fn test_migrate_dynamic_tempo_activity_cutoff_round_trips_production_values() { - new_test_ext(1).execute_with(|| { - // (cutoff_blocks, tempo) combinations from production data. - let cases: [(u16, u16); 6] = [ - (5000, 360), - (6000, 360), - (7200, 360), - (12000, 360), - (1000, 360), - (360, 360), - ]; - - for (i, &(cutoff, tempo)) in cases.iter().enumerate() { - let netuid = NetUid::from((i + 1) as u16); - add_network(netuid, tempo, 0); - ActivityCutoff::::insert(netuid, cutoff); - } - - crate::migrations::migrate_dynamic_tempo::migrate_dynamic_tempo::(); - - for (i, &(cutoff, _)) in cases.iter().enumerate() { - let netuid = NetUid::from((i + 1) as u16); - // get_activity_cutoff_blocks = factor * tempo / 1000 must equal original cutoff exactly. - assert_eq!( - crate::Pallet::::get_activity_cutoff_blocks(netuid), - cutoff as u64, - "ceiling division must round-trip cutoff exactly for netuid {}", - u16::from(netuid) - ); - } - }); -} - -#[test] -fn test_migrate_dynamic_tempo_idempotent() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1u16); - add_network(netuid, 360, 0); - - crate::migrations::migrate_dynamic_tempo::migrate_dynamic_tempo::(); - let last_epoch_first = LastEpochBlock::::get(netuid); - - // Mutate state to verify second run is a no-op. - run_to_block(crate::Pallet::::get_current_block_as_u64() + 100); - crate::migrations::migrate_dynamic_tempo::migrate_dynamic_tempo::(); - - assert_eq!( - LastEpochBlock::::get(netuid), - last_epoch_first, - "second migration call must be a no-op" - ); - }); -} diff --git a/pallets/subtensor/src/tests/migration/associated_evm_address_index.rs b/pallets/subtensor/src/tests/migration/associated_evm_address_index.rs new file mode 100644 index 0000000000..c46a96cdbd --- /dev/null +++ b/pallets/subtensor/src/tests/migration/associated_evm_address_index.rs @@ -0,0 +1,133 @@ +#![allow( + unused, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used +)] +//! AssociatedEvmAddress index + orphan subnet identity cleanup. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_migrate_associated_evm_address_index() { + new_test_ext(1).execute_with(|| { + let migration_name = b"migrate_associated_evm_address_index".to_vec(); + let netuid = NetUid::from(1); + let other_netuid = NetUid::from(2); + let evm_key = H160::repeat_byte(1); + let other_evm_key = H160::repeat_byte(2); + + HasMigrationRun::::remove(&migration_name); + AssociatedUidsByEvmAddress::::remove(netuid, evm_key); + AssociatedUidsByEvmAddress::::remove(other_netuid, other_evm_key); + + AssociatedEvmAddress::::insert(netuid, 0, (evm_key, 10)); + AssociatedEvmAddress::::insert(netuid, 1, (evm_key, 11)); + AssociatedEvmAddress::::insert(other_netuid, 0, (other_evm_key, 12)); + + crate::migrations::migrate_associated_evm_address_index::migrate_associated_evm_address_index::(); + + assert_eq!( + AssociatedUidsByEvmAddress::::get(netuid, evm_key).into_inner(), + vec![(0, 10), (1, 11)] + ); + assert_eq!( + AssociatedUidsByEvmAddress::::get(other_netuid, other_evm_key).into_inner(), + vec![(0, 12)] + ); + assert!(HasMigrationRun::::get(&migration_name)); + }); +} + +#[test] +fn test_migrate_clear_orphan_subnet_identities_v3() { + new_test_ext(1).execute_with(|| { + let migration_name = b"migrate_clear_orphan_subnet_identities_v3".to_vec(); + HasMigrationRun::::remove(&migration_name); + + let orphan_netuid = NetUid::from(1); + let live_netuid = NetUid::from(2); + + // live_netuid is a registered network; orphan_netuid is not. + NetworksAdded::::insert(live_netuid, true); + + let orphan_identity = SubnetIdentityV3 { + subnet_name: b"orphan".to_vec(), + ..Default::default() + }; + let live_identity = SubnetIdentityV3 { + subnet_name: b"live".to_vec(), + ..Default::default() + }; + + SubnetIdentitiesV3::::insert(orphan_netuid, orphan_identity); + SubnetIdentitiesV3::::insert(live_netuid, live_identity.clone()); + + crate::migrations::migrate_clear_orphan_subnet_identities_v3::migrate_clear_orphan_subnet_identities_v3::(); + + // The orphan identity is removed; the live subnet identity is preserved. + assert!(!SubnetIdentitiesV3::::contains_key(orphan_netuid)); + assert_eq!( + SubnetIdentitiesV3::::get(live_netuid), + Some(live_identity.clone()) + ); + + // Migration is marked as run. + assert!(HasMigrationRun::::get(&migration_name)); + + // Idempotent: re-running is a no-op (live identity still present). + crate::migrations::migrate_clear_orphan_subnet_identities_v3::migrate_clear_orphan_subnet_identities_v3::(); + assert_eq!( + SubnetIdentitiesV3::::get(live_netuid), + Some(live_identity) + ); + }); +} + +#[test] +fn test_migrate_associated_evm_address_index_reconciles_over_cap_buckets() { + new_test_ext(1).execute_with(|| { + let migration_name = b"migrate_associated_evm_address_index".to_vec(); + let netuid = NetUid::from(1); + let evm_key = H160::repeat_byte(1); + + HasMigrationRun::::remove(&migration_name); + AssociatedUidsByEvmAddress::::remove(netuid, evm_key); + + // Seed more forward-map associations for a single address than the reverse index can hold. + let cap = MAX_ASSOCIATED_UIDS_PER_EVM_ADDRESS; + let total = cap + 8; + for uid in 0..total { + AssociatedEvmAddress::::insert(netuid, uid as u16, (evm_key, 100 + uid as u64)); + } + + crate::migrations::migrate_associated_evm_address_index::migrate_associated_evm_address_index::(); + + // The reverse index is bounded by the cap. + let bucket = AssociatedUidsByEvmAddress::::get(netuid, evm_key); + assert_eq!(bucket.len() as u32, cap); + + // The forward map was pruned to match, so the two maps agree on the cap: every remaining + // forward entry is present in the reverse index, and there are no extras on either side. + let forward: Vec = AssociatedEvmAddress::::iter_prefix(netuid) + .map(|(uid, _)| uid) + .collect(); + assert_eq!(forward.len() as u32, cap); + for uid in &forward { + assert!( + bucket.iter().any(|(stored_uid, _)| stored_uid == uid), + "forward uid {uid} missing from reverse index" + ); + } + for (uid, _) in bucket.iter() { + assert!( + forward.contains(uid), + "reverse uid {uid} missing from forward map" + ); + } + + assert!(HasMigrationRun::::get(&migration_name)); + }); +} diff --git a/pallets/subtensor/src/tests/migration/auto_stake_destination.rs b/pallets/subtensor/src/tests/migration/auto_stake_destination.rs new file mode 100644 index 0000000000..518e9f5754 --- /dev/null +++ b/pallets/subtensor/src/tests/migration/auto_stake_destination.rs @@ -0,0 +1,124 @@ +#![allow( + unused, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used +)] +//! auto-stake destination migration. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_migrate_auto_stake_destination() { + new_test_ext(1).execute_with(|| { + // ------------------------------ + // Step 1: Simulate Old Storage Entries + // ------------------------------ + const MIGRATION_NAME: &[u8] = b"migrate_auto_stake_destination"; + let netuids = [NetUid::ROOT, NetUid::from(1), NetUid::from(2), NetUid::from(42)]; + for netuid in &netuids { + NetworksAdded::::insert(*netuid, true); + } + + let pallet_prefix = twox_128("SubtensorModule".as_bytes()); + let storage_prefix = twox_128("AutoStakeDestination".as_bytes()); + + // Create test accounts + let coldkey1: U256 = U256::from(1); + let coldkey2: U256 = U256::from(2); + let hotkey1: U256 = U256::from(100); + let hotkey2: U256 = U256::from(200); + + // Construct storage keys for old format (StorageMap) + let mut key1 = Vec::new(); + key1.extend_from_slice(&pallet_prefix); + key1.extend_from_slice(&storage_prefix); + key1.extend_from_slice(&Blake2_128Concat::hash(&coldkey1.encode())); + + let mut key2 = Vec::new(); + key2.extend_from_slice(&pallet_prefix); + key2.extend_from_slice(&storage_prefix); + key2.extend_from_slice(&Blake2_128Concat::hash(&coldkey2.encode())); + + // Store old format entries + put_raw(&key1, &hotkey1.encode()); + put_raw(&key2, &hotkey2.encode()); + + // Verify old entries are stored + assert_eq!(get_raw(&key1), Some(hotkey1.encode())); + assert_eq!(get_raw(&key2), Some(hotkey2.encode())); + + assert!( + !HasMigrationRun::::get(MIGRATION_NAME.to_vec()), + "Migration should not have run yet" + ); + + // ------------------------------ + // Step 2: Run the Migration + // ------------------------------ + let weight = crate::migrations::migrate_auto_stake_destination::migrate_auto_stake_destination::(); + + assert!( + HasMigrationRun::::get(MIGRATION_NAME.to_vec()), + "Migration should be marked as run" + ); + + // ------------------------------ + // Step 3: Verify Migration Effects + // ------------------------------ + + // Verify new format entries exist + for netuid in &netuids { + if *netuid == NetUid::ROOT { + assert_eq!( + AutoStakeDestination::::get(coldkey1, NetUid::ROOT), + None + ); + assert_eq!( + AutoStakeDestination::::get(coldkey2, NetUid::ROOT), + None + ); + } else { + assert_eq!( + AutoStakeDestination::::get(coldkey1, *netuid), + Some(hotkey1) + ); + assert_eq!( + AutoStakeDestination::::get(coldkey2, *netuid), + Some(hotkey2) + ); + + // Verify entry for AutoStakeDestinationColdkeys + assert_eq!( + AutoStakeDestinationColdkeys::::get(hotkey1, *netuid), + vec![coldkey1] + ); + assert_eq!( + AutoStakeDestinationColdkeys::::get(hotkey2, *netuid), + vec![coldkey2] + ); + } + } + + // Verify old format entries are cleared + assert_eq!(get_raw(&key1), None, "Old storage entry 1 should be cleared"); + assert_eq!(get_raw(&key2), None, "Old storage entry 2 should be cleared"); + + // Verify weight calculation + assert!(!weight.is_zero(), "Migration weight should be non-zero"); + + // ------------------------------ + // Step 4: Test Migration Idempotency + // ------------------------------ + let weight_second_run = crate::migrations::migrate_auto_stake_destination::migrate_auto_stake_destination::(); + + // Second run should only read the migration flag + assert_eq!( + weight_second_run, + ::DbWeight::get().reads(1), + "Second run should only read the migration flag" + ); + }); +} diff --git a/pallets/subtensor/src/tests/migration/commit_reveal.rs b/pallets/subtensor/src/tests/migration/commit_reveal.rs new file mode 100644 index 0000000000..d4d160cc96 --- /dev/null +++ b/pallets/subtensor/src/tests/migration/commit_reveal.rs @@ -0,0 +1,716 @@ +#![allow( + unused, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used +)] +//! commit-reveal v2/v3, settings, disable, timelocked CR. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_migrate_commit_reveal_2() { + new_test_ext(1).execute_with(|| { + // ------------------------------ + // Step 1: Simulate Old Storage Entries + // ------------------------------ + const MIGRATION_NAME: &str = "migrate_commit_reveal_2_v2"; + + let pallet_prefix = twox_128("SubtensorModule".as_bytes()); + let storage_prefix_interval = twox_128("WeightCommitRevealInterval".as_bytes()); + let storage_prefix_commits = twox_128("WeightCommits".as_bytes()); + + let netuid = NetUid::from(1); + let interval_value: u64 = 50u64; + + // Construct the full key for WeightCommitRevealInterval + let mut interval_key = Vec::new(); + interval_key.extend_from_slice(&pallet_prefix); + interval_key.extend_from_slice(&storage_prefix_interval); + interval_key.extend_from_slice(&netuid.encode()); + + put_raw(&interval_key, &interval_value.encode()); + + let test_account: U256 = U256::from(1); + + // Construct the full key for WeightCommits (DoubleMap) + let mut commit_key = Vec::new(); + commit_key.extend_from_slice(&pallet_prefix); + commit_key.extend_from_slice(&storage_prefix_commits); + + // First key (netuid) hashed with Twox64Concat + let netuid_hashed = Twox64Concat::hash(&netuid.encode()); + commit_key.extend_from_slice(&netuid_hashed); + + // Second key (account) hashed with Twox64Concat + let account_hashed = Twox64Concat::hash(&test_account.encode()); + commit_key.extend_from_slice(&account_hashed); + + let commit_value: (H256, u64) = (H256::from_low_u64_be(42), 100); + put_raw(&commit_key, &commit_value.encode()); + + let stored_interval = get_raw(&interval_key).expect("Expected to get a value"); + assert_eq!( + u64::decode(&mut &stored_interval[..]).expect("Failed to decode interval value"), + interval_value + ); + + let stored_commit = get_raw(&commit_key).expect("Expected to get a value"); + assert_eq!( + <(H256, u64)>::decode(&mut &stored_commit[..]).expect("Failed to decode commit value"), + commit_value + ); + + assert!( + !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should not have run yet" + ); + + // ------------------------------ + // Step 2: Run the Migration + // ------------------------------ + let weight = crate::migrations::migrate_commit_reveal_v2::migrate_commit_reveal_2::(); + + assert!( + HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should be marked as run" + ); + + // ------------------------------ + // Step 3: Verify Migration Effects + // ------------------------------ + let stored_interval_after = get_raw(&interval_key); + assert!( + stored_interval_after.is_none(), + "WeightCommitRevealInterval should be cleared" + ); + + let stored_commit_after = get_raw(&commit_key); + assert!( + stored_commit_after.is_none(), + "WeightCommits entry should be cleared" + ); + + assert!(!weight.is_zero(), "Migration weight should be non-zero"); + }); +} + +// Leaving in for reference. Will remove later. +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::migration::test_migrate_rao --exact --show-output --nocapture +// #[test] +// fn test_migrate_rao() { +// new_test_ext(1).execute_with(|| { +// // Setup initial state +// let netuid_0: u16 = 0; +// let netuid_1: u16 = 1; +// let netuid_2: u16 = 2; +// let netuid_3: u16 = 3; +// let hotkey1 = U256::from(1); +// let hotkey2 = U256::from(2); +// let coldkey1 = U256::from(3); +// let coldkey2 = U256::from(4); +// let coldkey3 = U256::from(5); +// let stake_amount: u64 = 1_000_000_000; +// let lock_amount: u64 = 500; +// NetworkMinLockCost::::set(500); + +// // Add networks root and alpha +// add_network(netuid_0, 1, 0); +// add_network(netuid_1, 1, 0); +// add_network(netuid_2, 1, 0); +// add_network(netuid_3, 1, 0); + +// // Set subnet lock +// SubnetLocked::::insert(netuid_1, lock_amount); + +// // Add some initial stake +// EmissionValues::::insert(netuid_1, 1_000_000_000); +// EmissionValues::::insert(netuid_2, 2_000_000_000); +// EmissionValues::::insert(netuid_3, 3_000_000_000); + +// Owner::::insert(hotkey1, coldkey1); +// Owner::::insert(hotkey2, coldkey2); +// Stake::::insert(hotkey1, coldkey1, stake_amount); +// Stake::::insert(hotkey1, coldkey2, stake_amount); +// Stake::::insert(hotkey2, coldkey2, stake_amount); +// Stake::::insert(hotkey2, coldkey3, stake_amount); + +// // Verify initial conditions +// assert_eq!(SubnetTAO::::get(netuid_0), 0); +// assert_eq!(SubnetTAO::::get(netuid_1), 0); +// assert_eq!(SubnetAlphaOut::::get(netuid_0), 0); +// assert_eq!(SubnetAlphaOut::::get(netuid_1), 0); +// assert_eq!(SubnetAlphaIn::::get(netuid_0), 0); +// assert_eq!(SubnetAlphaIn::::get(netuid_1), 0); +// assert_eq!(TotalHotkeyShares::::get(hotkey1, netuid_0), 0); +// assert_eq!(TotalHotkeyShares::::get(hotkey1, netuid_1), 0); +// assert_eq!(TotalHotkeyAlpha::::get(hotkey1, netuid_0), 0); +// assert_eq!(TotalHotkeyAlpha::::get(hotkey2, netuid_1), 0); + +// // Run migration +// crate::migrations::migrate_rao::migrate_rao::(); + +// // Verify root subnet (netuid 0) state after migration +// assert_eq!(SubnetTAO::::get(netuid_0), 4 * stake_amount); // Root has everything +// assert_eq!(SubnetTAO::::get(netuid_1), 1_000_000_000); // Always 1000000000 +// assert_eq!(SubnetAlphaIn::::get(netuid_0), 1_000_000_000); // Always 1_000_000_000 +// assert_eq!(SubnetAlphaIn::::get(netuid_1), 1_000_000_000); // Always 1_000_000_000 +// assert_eq!(SubnetAlphaOut::::get(netuid_0), 4 * stake_amount); // Root has everything. +// assert_eq!(SubnetAlphaOut::::get(netuid_1), 0); // No stake outstanding. + +// // Assert share information for hotkey1 on netuid_0 +// assert_eq!( +// TotalHotkeyShares::::get(hotkey1, netuid_0), +// 2 * stake_amount +// ); // Shares +// // Assert no shares for hotkey1 on netuid_1 +// assert_eq!(TotalHotkeyShares::::get(hotkey1, netuid_1), 0); // No shares +// // Assert alpha for hotkey1 on netuid_0 +// assert_eq!( +// TotalHotkeyAlpha::::get(hotkey1, netuid_0), +// 2 * stake_amount +// ); // Alpha +// // Assert no alpha for hotkey1 on netuid_1 +// assert_eq!(TotalHotkeyAlpha::::get(hotkey1, netuid_1), 0); // No alpha. +// // Assert share information for hotkey2 on netuid_0 +// assert_eq!( +// TotalHotkeyShares::::get(hotkey2, netuid_0), +// 2 * stake_amount +// ); // Shares +// // Assert no shares for hotkey2 on netuid_1 +// assert_eq!(TotalHotkeyShares::::get(hotkey2, netuid_1), 0); // No shares +// // Assert alpha for hotkey2 on netuid_0 +// assert_eq!( +// TotalHotkeyAlpha::::get(hotkey2, netuid_0), +// 2 * stake_amount +// ); // Alpha +// // Assert no alpha for hotkey2 on netuid_1 +// assert_eq!(TotalHotkeyAlpha::::get(hotkey2, netuid_1), 0); // No alpha. + +// // Assert stake balances for hotkey1 and coldkey1 on netuid_0 +// assert_eq!( +// SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( +// &hotkey1, &coldkey1, netuid_0 +// ), +// stake_amount +// ); +// // Assert stake balances for hotkey1 and coldkey2 on netuid_0 +// assert_eq!( +// SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( +// &hotkey1, &coldkey2, netuid_0 +// ), +// stake_amount +// ); +// // Assert stake balances for hotkey2 and coldkey2 on netuid_0 +// assert_eq!( +// SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( +// &hotkey2, &coldkey2, netuid_0 +// ), +// stake_amount +// ); +// // Assert stake balances for hotkey2 and coldkey3 on netuid_0 +// assert_eq!( +// SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( +// &hotkey2, &coldkey3, netuid_0 +// ), +// stake_amount +// ); +// // Assert total stake for hotkey1 on netuid_0 +// assert_eq!( +// SubtensorModule::get_stake_for_hotkey_on_subnet(&hotkey1, netuid_0), +// 2 * stake_amount +// ); +// // Assert total stake for hotkey2 on netuid_0 +// assert_eq!( +// SubtensorModule::get_stake_for_hotkey_on_subnet(&hotkey2, netuid_0), +// 2 * stake_amount +// ); +// // Increase stake for hotkey1 and coldkey1 on netuid_0 +// mock_increase_stake_for_hotkey_and_coldkey_on_subnet( +// &hotkey1, +// &coldkey1, +// netuid_0, +// stake_amount, +// ); +// // Assert updated stake for hotkey1 and coldkey1 on netuid_0 +// assert_eq!( +// SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( +// &hotkey1, &coldkey1, netuid_0 +// ), +// 2 * stake_amount +// ); +// // Assert updated total stake for hotkey1 on netuid_0 +// assert_eq!( +// SubtensorModule::get_stake_for_hotkey_on_subnet(&hotkey1, netuid_0), +// 3 * stake_amount +// ); +// // Increase stake for hotkey1 and coldkey1 on netuid_1 +// mock_increase_stake_for_hotkey_and_coldkey_on_subnet( +// &hotkey1, +// &coldkey1, +// netuid_1, +// stake_amount, +// ); +// // Assert updated stake for hotkey1 and coldkey1 on netuid_1 +// assert_eq!( +// SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( +// &hotkey1, &coldkey1, netuid_1 +// ), +// stake_amount +// ); +// // Assert updated total stake for hotkey1 on netuid_1 +// assert_eq!( +// SubtensorModule::get_stake_for_hotkey_on_subnet(&hotkey1, netuid_1), +// stake_amount +// ); + +// // Run the coinbase +// let emission: u64 = 1_000_000_000; +// SubtensorModule::run_coinbase(I96F32::from_num(emission)); +// close( +// SubnetTaoInEmission::::get(netuid_1), +// emission / 6, +// 100, +// ); +// close( +// SubnetTaoInEmission::::get(netuid_2), +// 2 * (emission / 6), +// 100, +// ); +// close( +// SubnetTaoInEmission::::get(netuid_3), +// 3 * (emission / 6), +// 100, +// ); +// }); +// } + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::migration::test_migrate_subnet_volume --exact --show-output + +#[test] +fn test_migrate_revealed_commitments() { + new_test_ext(1).execute_with(|| { + // -------------------------------- + // Step 1: Simulate Old Storage Entries + // -------------------------------- + const MIGRATION_NAME: &str = "migrate_revealed_commitments_v2"; + + // Pallet prefix == twox_128("Commitments") + let pallet_prefix = twox_128("Commitments".as_bytes()); + // Storage item prefix == twox_128("RevealedCommitments") + let storage_prefix = twox_128("RevealedCommitments".as_bytes()); + + // Example keys for the DoubleMap: + // Key1 (netuid) uses Identity (no hash) + // Key2 (account) uses Twox64Concat + let netuid = NetUid::from(123); + let account_id: u64 = 999; // Or however your test `AccountId` is represented + + // Construct the full storage key for `RevealedCommitments(netuid, account_id)` + let mut storage_key = Vec::new(); + storage_key.extend_from_slice(&pallet_prefix); + storage_key.extend_from_slice(&storage_prefix); + + // Identity for netuid => no hashing, just raw encode + storage_key.extend_from_slice(&netuid.encode()); + + // Twox64Concat for account + let account_hashed = Twox64Concat::hash(&account_id.encode()); + storage_key.extend_from_slice(&account_hashed); + + // Simulate an old value we might have stored: + // For example, the old type was `RevealedData` + // We'll just store a random encoded value for demonstration + let old_value = (vec![1, 2, 3, 4], 42u64); + put_raw(&storage_key, &old_value.encode()); + + // Confirm the storage value is set + let stored_value = get_raw(&storage_key).expect("Expected to get a value"); + let decoded_value = <(Vec, u64)>::decode(&mut &stored_value[..]) + .expect("Failed to decode the old revealed commitments"); + assert_eq!(decoded_value, old_value); + + // Also confirm that the migration has NOT run yet + assert!( + !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should not have run yet" + ); + + // -------------------------------- + // Step 2: Run the Migration + // -------------------------------- + let weight = crate::migrations::migrate_upgrade_revealed_commitments::migrate_upgrade_revealed_commitments::(); + + // Migration should be marked as run + assert!( + HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should now be marked as run" + ); + + // -------------------------------- + // Step 3: Verify Migration Effects + // -------------------------------- + // The old key/value should be removed + let stored_value_after = get_raw(&storage_key); + assert!( + stored_value_after.is_none(), + "Old storage entry should be cleared" + ); + + // Weight returned should be > 0 (some cost was incurred clearing storage) + assert!(!weight.is_zero(), "Migration weight should be non-zero"); + }); +} + +#[test] +fn test_migrate_crv3_commits_add_block() { + new_test_ext(1).execute_with(|| { + // ------------------------------ + // 0. Constants / helpers + // ------------------------------ + const MIG_NAME: &[u8] = b"crv3_commits_add_block_v1"; + let netuid = NetUid::from(99); + let epoch: u64 = 7; + let tempo: u16 = 360; + + // ------------------------------ + // 1. Create a network so helper can compute first‑block + // ------------------------------ + add_network(netuid, tempo, 0); + + // ------------------------------ + // 2. Simulate OLD storage (3‑tuple) + // ------------------------------ + let who: U256 = U256::from(0xdeadbeef_u64); + let ciphertext: BoundedVec> = + vec![1u8, 2, 3].try_into().unwrap(); + let round: RoundNumber = 42; + + let old_queue: VecDeque<_> = VecDeque::from(vec![(who, ciphertext.clone(), round)]); + + CRV3WeightCommits::::insert( + NetUidStorageIndex::from(netuid), + epoch, + old_queue.clone(), + ); + + // Sanity: entry decodes under old alias + assert_eq!( + CRV3WeightCommits::::get(NetUidStorageIndex::from(netuid), epoch), + old_queue + ); + + assert!( + !HasMigrationRun::::get(MIG_NAME.to_vec()), + "migration flag should be false before run" + ); + + // ------------------------------ + // 3. Run migration + // ------------------------------ + let w = crate::migrations::migrate_crv3_commits_add_block::migrate_crv3_commits_add_block::< + Test, + >(); + assert!(!w.is_zero(), "weight must be non-zero"); + + // ------------------------------ + // 4. Verify results + // ------------------------------ + assert!( + HasMigrationRun::::get(MIG_NAME.to_vec()), + "migration flag not set" + ); + + // Old storage must be empty (drained) + assert!( + CRV3WeightCommits::::get(NetUidStorageIndex::from(netuid), epoch).is_empty(), + "old queue should have been drained" + ); + + let new_q = CRV3WeightCommitsV2::::get(NetUidStorageIndex::from(netuid), epoch); + assert_eq!(new_q.len(), 1, "exactly one migrated element expected"); + + let (who2, commit_block, cipher2, round2) = new_q.front().cloned().unwrap(); + assert_eq!(who2, who); + assert_eq!(cipher2, ciphertext); + assert_eq!(round2, round); + + let expected_block = Pallet::::get_first_block_of_epoch(netuid, epoch); + assert_eq!( + commit_block, expected_block, + "commit_block should equal first block of epoch key" + ); + }); +} + +#[test] +fn test_migrate_disable_commit_reveal() { + const MIG_NAME: &[u8] = b"disable_commit_reveal_v1"; + let netuids = [NetUid::from(1), NetUid::from(2), NetUid::from(42)]; + + // --------------------------------------------------------------------- + // 1. build initial state ─ all nets enabled + // --------------------------------------------------------------------- + new_test_ext(1).execute_with(|| { + for (i, netuid) in netuids.iter().enumerate() { + add_network(*netuid, 5u16 + i as u16, 0); + CommitRevealWeightsEnabled::::insert(*netuid, true); + } + assert!( + !HasMigrationRun::::get(MIG_NAME), + "migration flag should be unset before run" + ); + + // ----------------------------------------------------------------- + // 2. run migration + // ----------------------------------------------------------------- + let w = crate::migrations::migrate_disable_commit_reveal::migrate_disable_commit_reveal::< + Test, + >(); + + assert!( + HasMigrationRun::::get(MIG_NAME), + "migration flag not set" + ); + + // ----------------------------------------------------------------- + // 3. verify every netuid is now disabled and only one value exists + // ----------------------------------------------------------------- + for netuid in netuids { + assert!( + !CommitRevealWeightsEnabled::::get(netuid), + "commit-reveal should be disabled for netuid {netuid}" + ); + } + + // There should be no stray keys + let collected: Vec<_> = CommitRevealWeightsEnabled::::iter().collect(); + assert_eq!(collected.len(), netuids.len(), "unexpected key count"); + for (k, v) in collected { + assert!(!v, "found an enabled flag after migration for netuid {k}"); + } + + // ----------------------------------------------------------------- + // 4. running again should be a no-op + // ----------------------------------------------------------------- + let w2 = crate::migrations::migrate_disable_commit_reveal::migrate_disable_commit_reveal::< + Test, + >(); + assert_eq!( + w2, + ::DbWeight::get().reads(1), + "second run should read the flag and do nothing else" + ); + }); +} + +#[test] +fn test_migrate_commit_reveal_settings() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &str = "migrate_commit_reveal_settings"; + + // Set up some networks first + let netuid1: u16 = 1; + let netuid2: u16 = 2; + // Add networks to simulate existing networks + add_network(netuid1.into(), 1, 0); + add_network(netuid2.into(), 1, 0); + + // Ensure the storage items use default values initially (but aren't explicitly set) + // Since these are ValueQuery storage items, they return defaults even when not set + assert_eq!(RevealPeriodEpochs::::get(NetUid::from(netuid1)), 1u64); + assert_eq!(RevealPeriodEpochs::::get(NetUid::from(netuid2)), 1u64); + assert!(CommitRevealWeightsEnabled::::get(NetUid::from(netuid1))); + assert!(CommitRevealWeightsEnabled::::get(NetUid::from(netuid2))); + + // Check migration hasn't run + assert!(!HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec())); + + // Run migration + let weight = crate::migrations::migrate_commit_reveal_settings::migrate_commit_reveal_settings::(); + + // Check migration has been marked as run + assert!(HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec())); + + // Verify RevealPeriodEpochs was set correctly + assert_eq!(RevealPeriodEpochs::::get(NetUid::from(netuid1)), 1u64); + assert_eq!(RevealPeriodEpochs::::get(NetUid::from(netuid2)), 1u64); + + // Verify CommitRevealWeightsEnabled was set correctly + assert!(CommitRevealWeightsEnabled::::get(NetUid::from(netuid1))); + assert!(CommitRevealWeightsEnabled::::get(NetUid::from(netuid2))); + }); +} + +#[test] +fn test_migrate_commit_reveal_settings_already_run() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &str = "migrate_commit_reveal_settings"; + // Mark migration as already run + HasMigrationRun::::insert(MIGRATION_NAME.as_bytes().to_vec(), true); + + // Run migration + let weight = crate::migrations::migrate_commit_reveal_settings::migrate_commit_reveal_settings::(); + + // Should only have read weight for checking migration status + let expected_weight = ::DbWeight::get().reads(1); + assert_eq!(weight, expected_weight); + }); +} + +#[test] +fn test_migrate_commit_reveal_settings_no_networks() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &str = "migrate_commit_reveal_settings"; + + // Check migration hasn't run + assert!(!HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec())); + + // Run migration + let weight = crate::migrations::migrate_commit_reveal_settings::migrate_commit_reveal_settings::(); + + // Check migration has been marked as run + assert!(HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec())); + + // Check that weight calculation is correct (no networks, so no additional reads/writes) + // 1 read for migration check + 0 reads for networks + 0 writes for storage + 1 write for migration flag + let expected_weight = ::DbWeight::get().reads(1) + ::DbWeight::get().writes(1); + assert_eq!(weight, expected_weight); + }); +} + +#[test] +fn test_migrate_commit_reveal_settings_multiple_networks() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &str = "migrate_commit_reveal_settings"; + + // Set up multiple networks + let netuids = vec![1u16, 2u16, 3u16, 10u16, 42u16]; + for netuid in &netuids { + add_network((*netuid).into(), 1, 0); + } + + // Run migration + let weight = crate::migrations::migrate_commit_reveal_settings::migrate_commit_reveal_settings::(); + + // Verify all networks have correct settings + for netuid in &netuids { + assert_eq!(RevealPeriodEpochs::::get(NetUid::from(*netuid)), 1u64); + assert!(CommitRevealWeightsEnabled::::get(NetUid::from(*netuid))); + } + + // Check migration has been marked as run + assert!(HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec())); + }); +} + +#[test] +fn test_migrate_commit_reveal_settings_values_access() { + new_test_ext(1).execute_with(|| { + let netuid: u16 = 1; + add_network(netuid.into(), 1, 0); + + // Run migration + crate::migrations::migrate_commit_reveal_settings::migrate_commit_reveal_settings::(); + + // Test that we can access the values using the pallet functions + assert_eq!( + SubtensorModule::get_reveal_period(NetUid::from(netuid)), + 1u64 + ); + + // Test direct storage access + assert_eq!(RevealPeriodEpochs::::get(NetUid::from(netuid)), 1u64); + assert!(CommitRevealWeightsEnabled::::get(NetUid::from( + netuid + ))); + }); +} + +#[test] +fn test_migrate_crv3_v2_to_timelocked() { + new_test_ext(1).execute_with(|| { + // ------------------------------ + // 0. Constants / helpers + // ------------------------------ + const MIG_NAME: &[u8] = b"crv3_v2_to_timelocked_v1"; + let netuid = NetUid::from(99); + let epoch: u64 = 7; + + // ------------------------------ + // 1. Simulate OLD storage (4‑tuple; V2 layout) + // ------------------------------ + let who: U256 = U256::from(0xdeadbeef_u64); + let commit_block: u64 = 12345; + let ciphertext: BoundedVec> = + vec![1u8, 2, 3].try_into().unwrap(); + let round: RoundNumber = 9; + + let old_queue: VecDeque<_> = + VecDeque::from(vec![(who, commit_block, ciphertext.clone(), round)]); + + // Insert under the deprecated alias + CRV3WeightCommitsV2::::insert( + NetUidStorageIndex::from(netuid), + epoch, + old_queue.clone(), + ); + + // Sanity: entry decodes under old alias + assert_eq!( + CRV3WeightCommitsV2::::get(NetUidStorageIndex::from(netuid), epoch), + old_queue, + "pre-migration: old queue should be present" + ); + + // Destination should be empty pre-migration + assert!( + TimelockedWeightCommits::::get(NetUidStorageIndex::from(netuid), epoch) + .is_empty(), + "pre-migration: destination should be empty" + ); + + assert!( + !HasMigrationRun::::get(MIG_NAME.to_vec()), + "migration flag should be false before run" + ); + + // ------------------------------ + // 2. Run migration + // ------------------------------ + let w = crate::migrations::migrate_crv3_v2_to_timelocked::migrate_crv3_v2_to_timelocked::< + Test, + >(); + assert!(!w.is_zero(), "weight must be non-zero"); + + // ------------------------------ + // 3. Verify results + // ------------------------------ + assert!( + HasMigrationRun::::get(MIG_NAME.to_vec()), + "migration flag not set" + ); + + // Old storage must be empty (drained) + assert!( + CRV3WeightCommitsV2::::get(NetUidStorageIndex::from(netuid), epoch).is_empty(), + "old queue should have been drained" + ); + + // New storage must match exactly + let new_q = TimelockedWeightCommits::::get(NetUidStorageIndex::from(netuid), epoch); + assert_eq!( + new_q, old_queue, + "migrated queue must exactly match the old queue" + ); + + // Verify the front element matches what we inserted + let (who2, commit_block2, cipher2, round2) = new_q.front().cloned().unwrap(); + assert_eq!(who2, who); + assert_eq!(commit_block2, commit_block); + assert_eq!(cipher2, ciphertext); + assert_eq!(round2, round); + }); +} diff --git a/pallets/subtensor/src/tests/migration/conviction_and_tempo.rs b/pallets/subtensor/src/tests/migration/conviction_and_tempo.rs new file mode 100644 index 0000000000..b58a2fa0c7 --- /dev/null +++ b/pallets/subtensor/src/tests/migration/conviction_and_tempo.rs @@ -0,0 +1,221 @@ +#![allow( + unused, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used +)] +//! tnet conviction locks + dynamic tempo. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_migrate_reset_tnet_conviction_locks() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &[u8] = b"migrate_reset_tnet_conviction_locks"; + + let netuid = NetUid::from(1); + let other_netuid = NetUid::from(2); + let coldkey_1 = U256::from(1001); + let coldkey_2 = U256::from(1002); + let hotkey_1 = U256::from(2001); + let hotkey_2 = U256::from(2002); + + let lock_1 = LockState { + locked_mass: AlphaBalance::from(10_u64), + conviction: U64F64::from_num(1.5), + last_update: 11, + }; + let lock_2 = LockState { + locked_mass: AlphaBalance::from(20_u64), + conviction: U64F64::from_num(2.5), + last_update: 22, + }; + + Lock::::insert((coldkey_1, netuid, hotkey_1), lock_1.clone()); + Lock::::insert((coldkey_2, other_netuid, hotkey_2), lock_2.clone()); + HotkeyLock::::insert(netuid, hotkey_1, lock_1.clone()); + DecayingHotkeyLock::::insert(other_netuid, hotkey_2, lock_2.clone()); + OwnerLock::::insert(netuid, lock_1.clone()); + DecayingOwnerLock::::insert(other_netuid, lock_2.clone()); + DecayingLock::::insert(coldkey_1, netuid, false); + DecayingLock::::insert(coldkey_2, other_netuid, false); + + assert!(!HasMigrationRun::::get(MIGRATION_NAME.to_vec())); + assert_eq!(Lock::::iter().count(), 2); + assert_eq!(HotkeyLock::::iter().count(), 1); + assert_eq!(DecayingHotkeyLock::::iter().count(), 1); + assert_eq!(OwnerLock::::iter().count(), 1); + assert_eq!(DecayingOwnerLock::::iter().count(), 1); + assert_eq!(DecayingLock::::iter().count(), 2); + + let raw_owner_lock_key = { + let mut key = Vec::new(); + key.extend_from_slice(&twox_128("SubtensorModule".as_bytes())); + key.extend_from_slice(&twox_128("OwnerLock".as_bytes())); + key.extend_from_slice(&NetUid::from(99).encode()); + key + }; + let raw_decaying_hotkey_lock_key = { + let mut key = Vec::new(); + key.extend_from_slice(&twox_128("SubtensorModule".as_bytes())); + key.extend_from_slice(&twox_128("DecayingHotkeyLock".as_bytes())); + key.extend_from_slice(&NetUid::from(100).encode()); + key.extend_from_slice(&Blake2_128Concat::hash(&U256::from(3003).encode())); + key + }; + + // Simulate deprecated aggregate entries with bytes that the current + // `LockState` type should never need to decode during this reset. + put_raw(&raw_owner_lock_key, &123_u32.encode()); + put_raw(&raw_decaying_hotkey_lock_key, &(456_u32, 789_u32).encode()); + assert!(get_raw(&raw_owner_lock_key).is_some()); + assert!(get_raw(&raw_decaying_hotkey_lock_key).is_some()); + + let weight = + crate::migrations::migrate_reset_tnet_conviction_locks::migrate_reset_tnet_conviction_locks::(); + + assert!(!weight.is_zero(), "migration weight should be non-zero"); + assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); + assert!(get_raw(&raw_owner_lock_key).is_none()); + assert!(get_raw(&raw_decaying_hotkey_lock_key).is_none()); + assert_eq!(Lock::::iter().count(), 0); + assert_eq!(HotkeyLock::::iter().count(), 0); + assert_eq!(DecayingHotkeyLock::::iter().count(), 0); + assert_eq!(OwnerLock::::iter().count(), 0); + assert_eq!(DecayingOwnerLock::::iter().count(), 0); + assert_eq!(DecayingLock::::iter().count(), 0); + + Lock::::insert((coldkey_1, netuid, hotkey_1), lock_1); + let second_weight = + crate::migrations::migrate_reset_tnet_conviction_locks::migrate_reset_tnet_conviction_locks::(); + + assert_eq!( + second_weight, + ::DbWeight::get().reads(1), + "second run should only read the migration flag" + ); + assert_eq!( + Lock::::iter().count(), + 1, + "migration must not run more than once" + ); + }); +} + +#[test] +fn test_migrate_dynamic_tempo_aligns_first_post_upgrade_fire() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &str = "dynamic_tempo_v1"; + let netuid = NetUid::from(7u16); + let tempo: u16 = 360; + + add_network(netuid, tempo, 0); + let current_block = 1234u64; + run_to_block(current_block); + + // Compute next-fire block + let netuid_plus_one = (u16::from(netuid) as u64) + 1; + let tempo_plus_one = (tempo as u64) + 1; + let adjusted = current_block + netuid_plus_one; + let remainder = adjusted % tempo_plus_one; + let legacy_blocks_until_next = (tempo as u64) - remainder; + let expected_next_fire = current_block + legacy_blocks_until_next; + + crate::migrations::migrate_dynamic_tempo::migrate_dynamic_tempo::(); + + // New formula: next fire = LastEpochBlock + tempo. + let last_epoch = LastEpochBlock::::get(netuid); + assert_eq!( + last_epoch + tempo as u64, + expected_next_fire, + "back-fill should make new scheduler fire at the same block as legacy modulo" + ); + assert!(HasMigrationRun::::get( + MIGRATION_NAME.as_bytes().to_vec() + )); + }); +} + +#[test] +fn test_migrate_dynamic_tempo_preserves_non_standard_tempo() { + new_test_ext(1).execute_with(|| { + // Three subnets — one standard, two with non-standard tempo + // (simulates the 2 mainnet subnets root configured outside MIN/MAX bounds). + let standard = NetUid::from(1u16); + let small = NetUid::from(2u16); + let large = NetUid::from(3u16); + + add_network(standard, 360, 0); + add_network(small, 10, 0); // < MIN_TEMPO (360) + add_network(large, 60_000, 0); // > MAX_TEMPO (50_400) + + crate::migrations::migrate_dynamic_tempo::migrate_dynamic_tempo::(); + + // Tempo values preserved as-is — no clamp. + assert_eq!(Tempo::::get(standard), 360); + assert_eq!(Tempo::::get(small), 10); + assert_eq!(Tempo::::get(large), 60_000); + + // All non-zero tempos got LastEpochBlock seeded. + assert!(LastEpochBlock::::contains_key(standard)); + assert!(LastEpochBlock::::contains_key(small)); + assert!(LastEpochBlock::::contains_key(large)); + }); +} + +#[test] +fn test_migrate_dynamic_tempo_activity_cutoff_round_trips_production_values() { + new_test_ext(1).execute_with(|| { + // (cutoff_blocks, tempo) combinations from production data. + let cases: [(u16, u16); 6] = [ + (5000, 360), + (6000, 360), + (7200, 360), + (12000, 360), + (1000, 360), + (360, 360), + ]; + + for (i, &(cutoff, tempo)) in cases.iter().enumerate() { + let netuid = NetUid::from((i + 1) as u16); + add_network(netuid, tempo, 0); + ActivityCutoff::::insert(netuid, cutoff); + } + + crate::migrations::migrate_dynamic_tempo::migrate_dynamic_tempo::(); + + for (i, &(cutoff, _)) in cases.iter().enumerate() { + let netuid = NetUid::from((i + 1) as u16); + // get_activity_cutoff_blocks = factor * tempo / 1000 must equal original cutoff exactly. + assert_eq!( + crate::Pallet::::get_activity_cutoff_blocks(netuid), + cutoff as u64, + "ceiling division must round-trip cutoff exactly for netuid {}", + u16::from(netuid) + ); + } + }); +} + +#[test] +fn test_migrate_dynamic_tempo_idempotent() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + add_network(netuid, 360, 0); + + crate::migrations::migrate_dynamic_tempo::migrate_dynamic_tempo::(); + let last_epoch_first = LastEpochBlock::::get(netuid); + + // Mutate state to verify second run is a no-op. + run_to_block(crate::Pallet::::get_current_block_as_u64() + 100); + crate::migrations::migrate_dynamic_tempo::migrate_dynamic_tempo::(); + + assert_eq!( + LastEpochBlock::::get(netuid), + last_epoch_first, + "second migration call must be a no-op" + ); + }); +} diff --git a/pallets/subtensor/src/tests/migration/fix_bad_hk_swap_genesis.rs b/pallets/subtensor/src/tests/migration/fix_bad_hk_swap_genesis.rs new file mode 100644 index 0000000000..a8bda1e3c5 --- /dev/null +++ b/pallets/subtensor/src/tests/migration/fix_bad_hk_swap_genesis.rs @@ -0,0 +1,135 @@ +#![allow( + unused, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used +)] +//! Bad hotkey-swap repair — genesis-only cases. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_migrate_fix_bad_hk_swap_only_genesis() { + new_test_ext(1).execute_with(|| { + use crate::migrations::migrate_fix_bad_hk_swap::*; + const MIGRATION_NAME: &[u8] = b"migrate_fix_bad_hk_swap"; + + let coldkey = "5H1WgA7ET3FmEarJK6qc1vaTWbNd6g41mgvyLRkysrH4MDdo"; + let account_id32: AccountId32 = + AccountId32::from_ss58check(coldkey).expect("Invalid coldkey"); + let mut account_id32_slice: &[u8] = account_id32.as_ref(); + let coldkey_account_id: ::AccountId = + ::AccountId::decode(&mut account_id32_slice).expect("Invalid coldkey"); + let netuid = NetUid::from(59); + // Setup + // Add subnet 59 + add_network(netuid, 10, 0); + SubtokenEnabled::::insert(netuid, true); + SubnetMechanism::::insert(netuid, 1); + + // Add stake to hotkey matching + let hotkey = "5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN"; + let account_id32: AccountId32 = + AccountId32::from_ss58check(hotkey).expect("Invalid hotkey"); + let mut account_id32_slice: &[u8] = account_id32.as_ref(); + let hotkey_account_id: ::AccountId = + ::AccountId::decode(&mut account_id32_slice).expect("Invalid hotkey"); + + // Give balance to coldkey + add_balance_to_coldkey_account(&coldkey_account_id, 100_000222.into()); + // Give stake to hotkey + let stake_added = 222222.into(); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + stake_added, + ); + + // Check genesis hash + let genesis_hash = frame_system::Pallet::::block_hash(0); + let genesis_bytes = genesis_hash.as_ref(); + let mainnet_genesis = + hex_literal::hex!("2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03"); + assert_ne!(genesis_bytes, mainnet_genesis); + + // Run migration + let w = migrate_fix_bad_hk_swap::(); + assert!(!w.is_zero(), "weight must be non-zero"); + + // Check stake did not change + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid + ), + stake_added + ); + }); +} + +#[test] +fn test_migrate_fix_bad_hk_swap_runs_on_mainnet_genesis() { + new_test_ext(1).execute_with(|| { + use crate::migrations::migrate_fix_bad_hk_swap::*; + const MIGRATION_NAME: &[u8] = b"migrate_fix_bad_hk_swap"; + + let coldkey = "5H1WgA7ET3FmEarJK6qc1vaTWbNd6g41mgvyLRkysrH4MDdo"; + let account_id32: AccountId32 = + AccountId32::from_ss58check(coldkey).expect("Invalid coldkey"); + let mut account_id32_slice: &[u8] = account_id32.as_ref(); + let coldkey_account_id: ::AccountId = + ::AccountId::decode(&mut account_id32_slice).expect("Invalid coldkey"); + let netuid = NetUid::from(59); + // Setup + // Add subnet 59 + add_network(netuid, 10, 0); + SubtokenEnabled::::insert(netuid, true); + SubnetMechanism::::insert(netuid, 1); + + // Add stake to hotkey matching + let hotkey = "5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN"; + let account_id32: AccountId32 = + AccountId32::from_ss58check(hotkey).expect("Invalid hotkey"); + let mut account_id32_slice: &[u8] = account_id32.as_ref(); + let hotkey_account_id: ::AccountId = + ::AccountId::decode(&mut account_id32_slice).expect("Invalid hotkey"); + + // Give balance to coldkey + add_balance_to_coldkey_account(&coldkey_account_id, 100_000222.into()); + // Give stake to hotkey + let stake_added = 222222.into(); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + stake_added, + ); + + // Set genesis hash to mainnet genesis + let mainnet_genesis = + hex_literal::hex!("2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03"); + frame_system::BlockHash::::insert(0, H256::from_slice(&mainnet_genesis)); + // Check genesis hash + let genesis_hash = frame_system::Pallet::::block_hash(0); + let genesis_bytes = genesis_hash.as_ref(); + assert_eq!(genesis_bytes, mainnet_genesis); + + // Run migration + let w = migrate_fix_bad_hk_swap::(); + assert!(!w.is_zero(), "weight must be non-zero"); + + // Check stake DID change + assert_ne!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid + ), + stake_added + ); + }); +} diff --git a/pallets/subtensor/src/tests/migration/fix_bad_hk_swap_mainnet.rs b/pallets/subtensor/src/tests/migration/fix_bad_hk_swap_mainnet.rs new file mode 100644 index 0000000000..7b62d50296 --- /dev/null +++ b/pallets/subtensor/src/tests/migration/fix_bad_hk_swap_mainnet.rs @@ -0,0 +1,886 @@ +#![allow( + unused, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used +)] +//! Bad hotkey-swap repair — mainnet cases. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_migrate_fix_bad_hk_swap_mainnet() { + new_test_ext(1).execute_with(|| { + use crate::migrations::migrate_fix_bad_hk_swap::*; + + let netuid = NetUid::from(59); + // Add subnet 59 + add_network(netuid, 10, 0); + SubtokenEnabled::::insert(netuid, true); + SubnetMechanism::::insert(netuid, 1); + + let hotkey = "5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN"; + let hotkey_account_id = decode_account_id32::(hotkey).expect("Invalid hotkey"); + + #[rustfmt::skip] + let diffs: [(&str, i64); 112] = [ + ("5Fn9SqQhx5bhDua7AGgkKxxk3gfZ75WWBGCMPeKH1WBgPaMQ", -2375685930981_i64), + ("5Fnhtm7cpxEbZaChnRZ8yWoF8MXVxmobkmLRehh5bkYtyZA9", -4090996138227), + ("5C7j3w2zz1SVejRuFrb2zFWHXT7UfG7eWA87KXL1WyV5KLVR", -607494031), + ("5DthZ1rvnXBb9oXVNtrMaMsDAnRxBPZCjD6fdRdeqC3fg1ca", -17022477949), + ("5F7BkPL3EVjKTYMbBkEmPAtTZQSGeyNzFPaf1DtebPFmJsJ7", -4016510), + ("5EefisctzgWdVGFQaL4LjFFacTE7dM4YJVNy3ogGBQoapTU1", -13106893093), + ("5CwkvpBxHCaRK9xBC2n6WdhpF5zg9t5WLkGorASaoErdynFQ", 439139249152), + ("5FU7ErUtmi22xuqeeCYVpNZp6WVSSL98hqDi5iyeZbkXtkbe", -35958768555), + ("5D7HL8T95qkHQTPFjgSFCjRoeM7oE3vQBYjiR1kAPbPxcMKu", -201914811997), + ("5HL3pPdDFY94Qdf8VnbfT4W6LXFkpd68Y5GSGzNJfntMdGZX", -235660917467), + ("5EcYAz8SBKWsogA6meJmVXcwVp4tjCvw3ZnJE6UXTyWNUdF2", -500070769668), + ("5EoE3c7XMf8TN3yudAaFjv4yvjtWYRviHcXi73EXkLHmWTCB", -86442928436), + ("5CMDjL7t2biHGREBwrmd8renD74FLEhjCVqfJG2MXckWBwDu", 1039317), + ("5CVGKimL4cLgyTqvYKQbPKYFZfiztsdczU7HrwNdSFKbbn5D", 4224201), + ("5HmZnEcW4eHbXmUEFWJbc4GHnBBYEK8ZPsFa25PuEmP5iuwM", -13156128), + ("5FNa4J4fTKh555CEyXHgR29RicSm8nTEHx36utTa4MJepJyX", -9519954), + ("5Gun93uQgffYpxqMKSmfG18AHiQW7Z2GR2dfPPR8W188vJYc", -1127662), + ("5HW1C4js4RyjQqNwALSUZC8NJ2WinD5Si2X2XkstXrMW2uYo", -34457336758), + ("5EqMhjdLY9h64ui2mizRZyBp1mEPJ7s4TsfAxQSQkAFmMfzE", -9346443829744), + ("5H3XwzydgE2XUGoJCR4dSj7tkd7uxZDJqik69hux2DBcruom", -1215347774), + ("5DjkmYpCUX6dBTvGoyN9j4QZhtMPhdcywDE8cJ8Qq1vg4X6e", -3603984447), + ("5E7Z7Btjz74XpZLH5fRzfZqiHCo4j9PXKfqi88kQ5MFrds34", -823907380854), + ("5DSBWN4hN9413C6o6A2hR9tYUbHjWsQqPRV74GrnCrMkCGJx", -309708781), + ("5FMLRmKPqsTsMbakpVUwoYro1P64QXVNTWyzNDugaNwSKRzF", -137525398263), + ("5EFZf5pnTqLegv6gxCrb6TKBQBGz9xLJNK8x9eR273cSons6", -1521760918), + ("5CGAGEuMLaidBDk8bDZKJb23dxRSP1wLenLALGLw8BTG1E3W", -544739696), + ("5HSzRtcQjD5KP6Nh2GVSS16aLDe6q9R33Wpu6s2eEeeo3AYS", -2309184790), + ("5DALvFDcfANQJcWz6AXMfDqabnoZhdDMoH6FxqYUibug1ja7", -369405632507), + ("5Fy8iWkpcbsskmEN1nYZDdS9zKh167Em9RRYisoss7jaYXxi", 15257429), + ("5CQCVTRyqJgZKBDmtHzpoF8su6BScLcNGbX8t3WMm5qYbbJH", -10721968), + ("5DXZByh2NS4MU61a1aaLrcLYpyzpJgHe95TEBdcEN2cF1SA5", -655946136), + ("5EX5yAYiABFzKDQJDe1kRVwFm3XRRY4HyLMe4Vu9A5U2VEVT", -325581360246), + ("5FvabwjtyW887gtc7vUnUc47KVhy17UeaNLRjzTg5nkVACMP", -77588524213), + ("5HTbYi5cmgWJxvyTy9JeYdtnjoDzjXnEXTGFsPEVx9iRPmVF", -53542953784), + ("5CWzmvA17MAMQ9mnAecLxFXS2N8846rz6T7m4QNHyVtJVq4j", 2672295922502), + ("5DSYntgHZY4krYUtkkQZyyoffVtu5e8rYWhXuhs832zY6YKy", -2680205688), + ("5EYyTFyLDqXscaa5VtXTvUc3x2ow2TeT8G12ZDMZwE6uFWPQ", -39165843935), + ("5CohfM1qdyNwdeJEex1Zyht3S2WS48rV993DmVbyKs2mEEd6", -4004685632), + ("5Gx6Y7UQD39Latgxigr6mHbnh1herpwNPau2PjvzwLWEjXL3", -559504), + ("5Hh4Efq5WDwe8URjjUqUNX8KxtMwLHLViwoRvXfEpXQCZakh", -32541090531), + ("5GWRHC7Nd8njqTPsdJkp6ngniCCBu9UjGhLfxp2jF1fPrfZ4", -5394093031), + ("5GNAB64UN32krzr3Xxu5LW6naeu2P3XULcdBCR9VZ5Libyit", -24884230), + ("5EEz25th1nYNM5xR1UsyFFAUaXMjdHqLxZ3wUjyHokYbXHku", -12525171), + ("5HKJq4JCS9xoKdYhcRnsRp1bodovba7ncd5KTYVwfReKaxHT", -408133990236), + ("5DXs7x664RL5NdSW77DTseLiu84unstuHGuqvmY61UtJwzRN", -3095078614148), + ("5CDfdDaA2p9sK1ia5yMVYfzgtFs2e1TrSAxuQqXoS28Lcrxf", -1032856892), + ("5Ecg4vD2zKXHDFhQqogWq1dZdijPsDty8rGsZu3raeoJSiXb", -995678), + ("5C5Yg63TNLb68Tu819qXd3Bt4giG8mAPzLmAFSqa2HC1R5Rm", -40818739830910), + ("5HHH25Wuf9rmVuk9cMKU1hCCPJ1qbHBd1SyHj91R3fMT36yb", -391416057906), + ("5GKGGE5YLHoDciYJ6Ec2YnUP3SykSQPA47hqmwBP63EtVrd9", -413944553000), + ("5CSi9ZLyiXfLeYtEFaZSBuTofNMRnXEJEJE9CS4gGaT6CkWt", -17811605275), + ("5CSoA7QVdFHHBZz53bbRV2mC5vhL64ehhWa8ibtLppmt2n3J", -65701320107), + ("5GpA5BtfMMX52rXztrha79YqfwR4YaSfTuAcb48Yt73U4h71", -2194562), + ("5Euz5wpb4xiDWfV1A6AKK6i6ca3WoZQD5hCVyf1fws8GXh4z", -6143407839874), + ("5DZzmhCG7SMK3LwrkmHZ8ZBwaAByMjfBpEid14nNQdxHipCE", -386645), + ("5Fc9Vo3hkbr6bPxJpjQo5sQ43L5Hc2G8R5BdqRYF8psvB5pw", 55668553), + ("5GuSHC3iowySHLDW4pEyEZE6PKxKP62YpJYJyBy5tijzAnYz", -159317636526), + ("5HVVZrUBPvjYHiwaSvtvaN9GZogoznM49m2AEmVW6RXnYCka", -1995572213), + ("5EcGpeV2wjkCVsBjsBifSWbdcqH98b6oEY8beDY59c4fXkhw", -177096614584), + ("5GnCjvWJEESwVNFZzy85zbBzw26etuEt87WiqsE3ee2Ws1wm", -1961445), + ("5GWuPUpTuChAqKxvU22TRLvRkBFiyWWZnq9cLpJN6SSvkho1", -94157569391), + ("5FXHf7q5rvBXnzQgmsa31Db9rjcRy6ZHKMiyDSb8Vs5p2msN", -688433531658), + ("5GbxkzytnvbRuNQ7qxPpfPuWMoeitS8V4KDY9jSshE5fDegD", -19085313), + ("5Gus1B7c9uWkky7Yawh2tKR1V6AMh5DbqUBPq881JHqeqVqY", -16101671818), + ("5DLhRdbvWkYYScDmwx4QgJfieSN4apBWbZ2yno3MfgbR8hBP", -21062025), + ("5Cg5kVyNEs7MWWRHU8X5MHwX5cN3aegvC4RBt2JK19w2GiR8", -2593737050), + ("5Dkushsxtc8AdCf287MtTYHQv9DoZeBRpttUpBtmyFhGy3uR", -48672832345630), + ("5EqNqVsHj9bQVyEujcm62zjMYUFhTLY7rTP854txSrJzyoco", -3828526), + ("5Dea6d6nKErEbRQ4MBGuCALn8NZ2xo4kaa51hB5KMriPBkEM", -1560192853875), + ("5DNt2XDWdeMd4H92FLnfUvkqyXzmavezHvzLboP3VgT1xLZV", -831964576998), + ("5FKtFoTeK8aaG6HZTrDgvoYHVQ5NY4S9VyV7W5K74cWcwLYA", -60823501166), + ("5GEBanZKUU7Hrf8K2VNi33HxyJRstgQ3WD3odHgvMj2nPbhi", -98946626902), + ("5CUtw7LYB2n2bzgXt6YnmKDHt6PsB3kKAyD9azYJNcRG8TNg", -9779588557490), + ("5EynbF72b12fbgMvEeL1vJSY342rCryNbuwxFivU1Xevtmv3", -17314385200455), + ("5CapiZRuULed8ConS1gbjMVgnwcT5JnQah7tx6sZnK7sJJuJ", -5810972), + ("5DnaxLaNduf41WM6WWZ4fkzcGzWNWx6eLJyQpSaMueUGCsaU", -12668760), + ("5Cqz9SChYPxTFZ2623rE2aQQ5ttQoLwZ8yfwYgiZyQDANqZn", -683549), + ("5C8ZcLzF23GrXKdH4Pg3ZXC3vKQsF5PM8VvhzzxzTQksgj8e", -44720570590), + ("5GuNsmoswrP6hTKZkKcpTpZftTMKrmnCHvTL2V3NHJy2fpen", -5042891812715), + ("5F1TYDkLnP36HHY5btigxyKUPzBraxdrU1aX1bqFfPfcfnzU", -1189104279832), + ("5Dc384z9HuTGF6oratZs1fLciCHtPZaLhrHfCVw82a5AikWZ", -616163196988), + ("5DhcaEUsRKhZQ31qRffJqjtLmFkbVaCebn8nVjYhvB4KJtX5", -17746006723), + ("5HYE7z3xTcrN1rqz54NyZRAkehFfRMcaEcdoMq5g5ATET5wQ", 212509751245), + ("5F97DdEVTy9gPCtN6jkJJENDJuQiRGiwbMVSL74qRq8FCq5W", 2225287736222), + ("5FUVN133rSvuKXgsXKMR2ZEaysxZjkRUFUWS1UMyNGre9xFV", -73216740161), + ("5CZeimtfpRqQgPxVwr1MzfG2Sok8E1AMERHo6vUmEdRS5JiU", -3937802), + ("5Eqq2JwGh7qbtnjPiFEPmmnHxs3S4J4Ahg8fr4sybZV1tPdY", -173406860562), + ("5ERfDw6K3GmQqwqsEG6foFtu7VsYGifPi556UJKQsBnfbHKN", 96022588728), + ("5Ek8RkU6KMv5Fx7yivRVoQkuJYAKhULWiLWDpbGG4hvR9HFD", 968139369093), + ("5HpCpGALzqgnDTP1HXFiuhzD5MFaDTRHjXBCvaMY9LNNRkT9", 104943979521), + ("5FYqS77gxW9gHG8id1YYPS7Cd4TQmNUMhF8h3S77Fq2VvvRQ", 729199757977), + ("5FtBqMg13pNf1N6TwfG6BmwyaDM77mkeQ16UTHsGasrVDedX", 131457064336), + ("5GbnWR2XhWrRMt123SdrLbR9G2a4N5dtzA3TSu3Czkzoeu7x", 2295599153), + ("5GgiowcCG4kLpwkCTGxxQJQv8WwKFyBQ6McPRmqKtWPy8EaK", 113838605389), + ("5FL5YtYozpUAGaiVWonpbwEYdEMij3obJHSH3ACY4vgWmDgy", 8689039), + ("5Egq58bxRv7boM2s3rnDxx1udnkzxPQ23HuoqohVxjh9RenC", 216373234348), + ("5FRGeeEgRNR8U33FDKvN7yUgts8zR3qRJH4yKKWoR9GswBRb", 2196574958718), + ("5FnhSy79BPYyrmmFsbckinQw1fLiLqqPkQL2vgZwPxbRfu3k", 42319631507), + ("5Hj8jMhqAv7cfyRh5STfbZefMhv17QxZ1RxWq9jNcLAEsRRo", 132216702183491), + ("5EXYTGMqumAH6RLQgHwkMEMnSvHcpHc89R6U8krfNJTYWm9J", 504320264499), + ("5EFh8ctzmytXURqrCTUBWHTs87f7TMWB6XKUzdqxKXVUtvS2", 2209599669432), + ("5CqVqEcRBkw7Gm2reJ33cj7puR9W2Tq7qsLxSruV1BgnMqKN", 1033387458788), + ("5D79enmLSGimsruoraGagofhaSeYJZvGUqFCCrr83ZfZs1HS", 7591184215233), + ("5HbpyjsvyXLWtf1QT1CyNUdyut6scM5dM7ytm8hoxFvRtU1i", 129833188275), + ("5CnxCi7CdEriWSdw4LcXdbtjodxA6uTat4gBm4wuT9QToMdo", 3132978), + ("5G48fiQjhAd8hc4rYc6GituCuAPKznL28jyyyq1auMyZiG4t", 514913328178), + ("5FFGjW2hJ7tQ41qghSsLP4cVmA8j9pZVSrr2CrLG7fQAsLHJ", 346794972723), + ("5FWjnxeRMtMFxRc9kvZKCG5iJAyyz2kmXV8u3kqyiXizZtiz", 225939835005), + ("5CUw3sB4oxd3dVSHUr3kxsB591VEjaPzr444KkfjwVFnLRfJ", 208250614494), + ("5EaBhxNUwMRyKsaeA2BEjDCrvwE5J8FDSpfCHK9gGmnmbhCa", 278083207003), + ("5GHJ5HxFxYQyVoNFUxR3JCqqCKRumaFCY7N5zMxwF4CpRUWr", 1381466224829), + ("5H1WgA7ET3FmEarJK6qc1vaTWbNd6g41mgvyLRkysrH4MDdo", 774889), + ]; + + for (coldkey, diff) in diffs { + let coldkey_account_id = decode_account_id32::(coldkey).expect("Invalid coldkey"); + if diff > 0 { + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + diff.unsigned_abs().into(), + ); + } + } + + let w = try_restore_shares::(); + assert!(!w.is_zero(), "weight must be non-zero"); + + // Check stake is near 0 for all positive entires and near diff for all negative + for (coldkey, diff) in diffs { + let coldkey_account_id = decode_account_id32::(coldkey).expect("Invalid coldkey"); + + let stake_float: f64 = num_traits::ToPrimitive::to_f64( + &SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ) + .to_u64(), + ) + .expect("float conv fail"); + if diff > 0 { + log::debug!("diff: {} for ck: {}", diff, coldkey); + assert_relative_eq!(stake_float, 0_f64, max_relative = 0.001_f64); + } else { + let diff_float: f64 = + num_traits::ToPrimitive::to_f64(&diff.unsigned_abs()).expect("float conv fail"); + assert_relative_eq!(stake_float, diff_float, max_relative = 0.001_f64); + } + } + }); +} + +#[test] +fn test_migrate_fix_bad_hk_swap_mainnet_some_exits() { + // test with some of the gainers have exited fully or partially before the migration + // i.e. balance is less than they owe + new_test_ext(1).execute_with(|| { + use crate::migrations::migrate_fix_bad_hk_swap::*; + + let netuid = NetUid::from(59); + // Add subnet 59 + add_network(netuid, 10, 0); + SubtokenEnabled::::insert(netuid, true); + SubnetMechanism::::insert(netuid, 1); + + let hotkey = "5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN"; + let hotkey_account_id = decode_account_id32::(hotkey).expect("Invalid hotkey"); + + #[rustfmt::skip] + let diffs: [(&str, i64); 112] = [ + ("5Fn9SqQhx5bhDua7AGgkKxxk3gfZ75WWBGCMPeKH1WBgPaMQ", -2375685930981_i64), + ("5Fnhtm7cpxEbZaChnRZ8yWoF8MXVxmobkmLRehh5bkYtyZA9", -4090996138227), + ("5C7j3w2zz1SVejRuFrb2zFWHXT7UfG7eWA87KXL1WyV5KLVR", -607494031), + ("5DthZ1rvnXBb9oXVNtrMaMsDAnRxBPZCjD6fdRdeqC3fg1ca", -17022477949), + ("5F7BkPL3EVjKTYMbBkEmPAtTZQSGeyNzFPaf1DtebPFmJsJ7", -4016510), + ("5EefisctzgWdVGFQaL4LjFFacTE7dM4YJVNy3ogGBQoapTU1", -13106893093), + ("5CwkvpBxHCaRK9xBC2n6WdhpF5zg9t5WLkGorASaoErdynFQ", 439139249152), + ("5FU7ErUtmi22xuqeeCYVpNZp6WVSSL98hqDi5iyeZbkXtkbe", -35958768555), + ("5D7HL8T95qkHQTPFjgSFCjRoeM7oE3vQBYjiR1kAPbPxcMKu", -201914811997), + ("5HL3pPdDFY94Qdf8VnbfT4W6LXFkpd68Y5GSGzNJfntMdGZX", -235660917467), + ("5EcYAz8SBKWsogA6meJmVXcwVp4tjCvw3ZnJE6UXTyWNUdF2", -500070769668), + ("5EoE3c7XMf8TN3yudAaFjv4yvjtWYRviHcXi73EXkLHmWTCB", -86442928436), + ("5CMDjL7t2biHGREBwrmd8renD74FLEhjCVqfJG2MXckWBwDu", 1039317), + ("5CVGKimL4cLgyTqvYKQbPKYFZfiztsdczU7HrwNdSFKbbn5D", 4224201), + ("5HmZnEcW4eHbXmUEFWJbc4GHnBBYEK8ZPsFa25PuEmP5iuwM", -13156128), + ("5FNa4J4fTKh555CEyXHgR29RicSm8nTEHx36utTa4MJepJyX", -9519954), + ("5Gun93uQgffYpxqMKSmfG18AHiQW7Z2GR2dfPPR8W188vJYc", -1127662), + ("5HW1C4js4RyjQqNwALSUZC8NJ2WinD5Si2X2XkstXrMW2uYo", -34457336758), + ("5EqMhjdLY9h64ui2mizRZyBp1mEPJ7s4TsfAxQSQkAFmMfzE", -9346443829744), + ("5H3XwzydgE2XUGoJCR4dSj7tkd7uxZDJqik69hux2DBcruom", -1215347774), + ("5DjkmYpCUX6dBTvGoyN9j4QZhtMPhdcywDE8cJ8Qq1vg4X6e", -3603984447), + ("5E7Z7Btjz74XpZLH5fRzfZqiHCo4j9PXKfqi88kQ5MFrds34", -823907380854), + ("5DSBWN4hN9413C6o6A2hR9tYUbHjWsQqPRV74GrnCrMkCGJx", -309708781), + ("5FMLRmKPqsTsMbakpVUwoYro1P64QXVNTWyzNDugaNwSKRzF", -137525398263), + ("5EFZf5pnTqLegv6gxCrb6TKBQBGz9xLJNK8x9eR273cSons6", -1521760918), + ("5CGAGEuMLaidBDk8bDZKJb23dxRSP1wLenLALGLw8BTG1E3W", -544739696), + ("5HSzRtcQjD5KP6Nh2GVSS16aLDe6q9R33Wpu6s2eEeeo3AYS", -2309184790), + ("5DALvFDcfANQJcWz6AXMfDqabnoZhdDMoH6FxqYUibug1ja7", -369405632507), + ("5Fy8iWkpcbsskmEN1nYZDdS9zKh167Em9RRYisoss7jaYXxi", 15257429), + ("5CQCVTRyqJgZKBDmtHzpoF8su6BScLcNGbX8t3WMm5qYbbJH", -10721968), + ("5DXZByh2NS4MU61a1aaLrcLYpyzpJgHe95TEBdcEN2cF1SA5", -655946136), + ("5EX5yAYiABFzKDQJDe1kRVwFm3XRRY4HyLMe4Vu9A5U2VEVT", -325581360246), + ("5FvabwjtyW887gtc7vUnUc47KVhy17UeaNLRjzTg5nkVACMP", -77588524213), + ("5HTbYi5cmgWJxvyTy9JeYdtnjoDzjXnEXTGFsPEVx9iRPmVF", -53542953784), + ("5CWzmvA17MAMQ9mnAecLxFXS2N8846rz6T7m4QNHyVtJVq4j", 2672295922502), + ("5DSYntgHZY4krYUtkkQZyyoffVtu5e8rYWhXuhs832zY6YKy", -2680205688), + ("5EYyTFyLDqXscaa5VtXTvUc3x2ow2TeT8G12ZDMZwE6uFWPQ", -39165843935), + ("5CohfM1qdyNwdeJEex1Zyht3S2WS48rV993DmVbyKs2mEEd6", -4004685632), + ("5Gx6Y7UQD39Latgxigr6mHbnh1herpwNPau2PjvzwLWEjXL3", -559504), + ("5Hh4Efq5WDwe8URjjUqUNX8KxtMwLHLViwoRvXfEpXQCZakh", -32541090531), + ("5GWRHC7Nd8njqTPsdJkp6ngniCCBu9UjGhLfxp2jF1fPrfZ4", -5394093031), + ("5GNAB64UN32krzr3Xxu5LW6naeu2P3XULcdBCR9VZ5Libyit", -24884230), + ("5EEz25th1nYNM5xR1UsyFFAUaXMjdHqLxZ3wUjyHokYbXHku", -12525171), + ("5HKJq4JCS9xoKdYhcRnsRp1bodovba7ncd5KTYVwfReKaxHT", -408133990236), + ("5DXs7x664RL5NdSW77DTseLiu84unstuHGuqvmY61UtJwzRN", -3095078614148), + ("5CDfdDaA2p9sK1ia5yMVYfzgtFs2e1TrSAxuQqXoS28Lcrxf", -1032856892), + ("5Ecg4vD2zKXHDFhQqogWq1dZdijPsDty8rGsZu3raeoJSiXb", -995678), + ("5C5Yg63TNLb68Tu819qXd3Bt4giG8mAPzLmAFSqa2HC1R5Rm", -40818739830910), + ("5HHH25Wuf9rmVuk9cMKU1hCCPJ1qbHBd1SyHj91R3fMT36yb", -391416057906), + ("5GKGGE5YLHoDciYJ6Ec2YnUP3SykSQPA47hqmwBP63EtVrd9", -413944553000), + ("5CSi9ZLyiXfLeYtEFaZSBuTofNMRnXEJEJE9CS4gGaT6CkWt", -17811605275), + ("5CSoA7QVdFHHBZz53bbRV2mC5vhL64ehhWa8ibtLppmt2n3J", -65701320107), + ("5GpA5BtfMMX52rXztrha79YqfwR4YaSfTuAcb48Yt73U4h71", -2194562), + ("5Euz5wpb4xiDWfV1A6AKK6i6ca3WoZQD5hCVyf1fws8GXh4z", -6143407839874), + ("5DZzmhCG7SMK3LwrkmHZ8ZBwaAByMjfBpEid14nNQdxHipCE", -386645), + ("5Fc9Vo3hkbr6bPxJpjQo5sQ43L5Hc2G8R5BdqRYF8psvB5pw", 55668553), + ("5GuSHC3iowySHLDW4pEyEZE6PKxKP62YpJYJyBy5tijzAnYz", -159317636526), + ("5HVVZrUBPvjYHiwaSvtvaN9GZogoznM49m2AEmVW6RXnYCka", -1995572213), + ("5EcGpeV2wjkCVsBjsBifSWbdcqH98b6oEY8beDY59c4fXkhw", -177096614584), + ("5GnCjvWJEESwVNFZzy85zbBzw26etuEt87WiqsE3ee2Ws1wm", -1961445), + ("5GWuPUpTuChAqKxvU22TRLvRkBFiyWWZnq9cLpJN6SSvkho1", -94157569391), + ("5FXHf7q5rvBXnzQgmsa31Db9rjcRy6ZHKMiyDSb8Vs5p2msN", -688433531658), + ("5GbxkzytnvbRuNQ7qxPpfPuWMoeitS8V4KDY9jSshE5fDegD", -19085313), + ("5Gus1B7c9uWkky7Yawh2tKR1V6AMh5DbqUBPq881JHqeqVqY", -16101671818), + ("5DLhRdbvWkYYScDmwx4QgJfieSN4apBWbZ2yno3MfgbR8hBP", -21062025), + ("5Cg5kVyNEs7MWWRHU8X5MHwX5cN3aegvC4RBt2JK19w2GiR8", -2593737050), + ("5Dkushsxtc8AdCf287MtTYHQv9DoZeBRpttUpBtmyFhGy3uR", -48672832345630), + ("5EqNqVsHj9bQVyEujcm62zjMYUFhTLY7rTP854txSrJzyoco", -3828526), + ("5Dea6d6nKErEbRQ4MBGuCALn8NZ2xo4kaa51hB5KMriPBkEM", -1560192853875), + ("5DNt2XDWdeMd4H92FLnfUvkqyXzmavezHvzLboP3VgT1xLZV", -831964576998), + ("5FKtFoTeK8aaG6HZTrDgvoYHVQ5NY4S9VyV7W5K74cWcwLYA", -60823501166), + ("5GEBanZKUU7Hrf8K2VNi33HxyJRstgQ3WD3odHgvMj2nPbhi", -98946626902), + ("5CUtw7LYB2n2bzgXt6YnmKDHt6PsB3kKAyD9azYJNcRG8TNg", -9779588557490), + ("5EynbF72b12fbgMvEeL1vJSY342rCryNbuwxFivU1Xevtmv3", -17314385200455), + ("5CapiZRuULed8ConS1gbjMVgnwcT5JnQah7tx6sZnK7sJJuJ", -5810972), + ("5DnaxLaNduf41WM6WWZ4fkzcGzWNWx6eLJyQpSaMueUGCsaU", -12668760), + ("5Cqz9SChYPxTFZ2623rE2aQQ5ttQoLwZ8yfwYgiZyQDANqZn", -683549), + ("5C8ZcLzF23GrXKdH4Pg3ZXC3vKQsF5PM8VvhzzxzTQksgj8e", -44720570590), + ("5GuNsmoswrP6hTKZkKcpTpZftTMKrmnCHvTL2V3NHJy2fpen", -5042891812715), + ("5F1TYDkLnP36HHY5btigxyKUPzBraxdrU1aX1bqFfPfcfnzU", -1189104279832), + ("5Dc384z9HuTGF6oratZs1fLciCHtPZaLhrHfCVw82a5AikWZ", -616163196988), + ("5DhcaEUsRKhZQ31qRffJqjtLmFkbVaCebn8nVjYhvB4KJtX5", -17746006723), + ("5HYE7z3xTcrN1rqz54NyZRAkehFfRMcaEcdoMq5g5ATET5wQ", 212509751245), + ("5F97DdEVTy9gPCtN6jkJJENDJuQiRGiwbMVSL74qRq8FCq5W", 2225287736222), + ("5FUVN133rSvuKXgsXKMR2ZEaysxZjkRUFUWS1UMyNGre9xFV", -73216740161), + ("5CZeimtfpRqQgPxVwr1MzfG2Sok8E1AMERHo6vUmEdRS5JiU", -3937802), + ("5Eqq2JwGh7qbtnjPiFEPmmnHxs3S4J4Ahg8fr4sybZV1tPdY", -173406860562), + ("5ERfDw6K3GmQqwqsEG6foFtu7VsYGifPi556UJKQsBnfbHKN", 96022588728), + ("5Ek8RkU6KMv5Fx7yivRVoQkuJYAKhULWiLWDpbGG4hvR9HFD", 968139369093), + ("5HpCpGALzqgnDTP1HXFiuhzD5MFaDTRHjXBCvaMY9LNNRkT9", 104943979521), + ("5FYqS77gxW9gHG8id1YYPS7Cd4TQmNUMhF8h3S77Fq2VvvRQ", 729199757977), + ("5FtBqMg13pNf1N6TwfG6BmwyaDM77mkeQ16UTHsGasrVDedX", 131457064336), + ("5GbnWR2XhWrRMt123SdrLbR9G2a4N5dtzA3TSu3Czkzoeu7x", 2295599153), + ("5GgiowcCG4kLpwkCTGxxQJQv8WwKFyBQ6McPRmqKtWPy8EaK", 113838605389), + ("5FL5YtYozpUAGaiVWonpbwEYdEMij3obJHSH3ACY4vgWmDgy", 8689039), + ("5Egq58bxRv7boM2s3rnDxx1udnkzxPQ23HuoqohVxjh9RenC", 216373234348), + ("5FRGeeEgRNR8U33FDKvN7yUgts8zR3qRJH4yKKWoR9GswBRb", 2196574958718), + ("5FnhSy79BPYyrmmFsbckinQw1fLiLqqPkQL2vgZwPxbRfu3k", 42319631507), + ("5Hj8jMhqAv7cfyRh5STfbZefMhv17QxZ1RxWq9jNcLAEsRRo", 132216702183491), + ("5EXYTGMqumAH6RLQgHwkMEMnSvHcpHc89R6U8krfNJTYWm9J", 504320264499), + ("5EFh8ctzmytXURqrCTUBWHTs87f7TMWB6XKUzdqxKXVUtvS2", 2209599669432), + ("5CqVqEcRBkw7Gm2reJ33cj7puR9W2Tq7qsLxSruV1BgnMqKN", 1033387458788), + ("5D79enmLSGimsruoraGagofhaSeYJZvGUqFCCrr83ZfZs1HS", 7591184215233), + ("5HbpyjsvyXLWtf1QT1CyNUdyut6scM5dM7ytm8hoxFvRtU1i", 129833188275), + ("5CnxCi7CdEriWSdw4LcXdbtjodxA6uTat4gBm4wuT9QToMdo", 3132978), + ("5G48fiQjhAd8hc4rYc6GituCuAPKznL28jyyyq1auMyZiG4t", 514913328178), + ("5FFGjW2hJ7tQ41qghSsLP4cVmA8j9pZVSrr2CrLG7fQAsLHJ", 346794972723), + ("5FWjnxeRMtMFxRc9kvZKCG5iJAyyz2kmXV8u3kqyiXizZtiz", 225939835005), + ("5CUw3sB4oxd3dVSHUr3kxsB591VEjaPzr444KkfjwVFnLRfJ", 208250614494), + ("5EaBhxNUwMRyKsaeA2BEjDCrvwE5J8FDSpfCHK9gGmnmbhCa", 278083207003), + ("5GHJ5HxFxYQyVoNFUxR3JCqqCKRumaFCY7N5zMxwF4CpRUWr", 1381466224829), + ("5H1WgA7ET3FmEarJK6qc1vaTWbNd6g41mgvyLRkysrH4MDdo", 774889), + ]; + + for (coldkey, diff) in diffs { + let coldkey_account_id = decode_account_id32::(coldkey).expect("Invalid coldkey"); + if diff > 0 { + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + diff.unsigned_abs().into(), + ); + } + } + + // For one of the gainers, remove some of the stake + let idx = 6; + let gained_ck = diffs[idx].0; + let coldkey_account_id = decode_account_id32::(gained_ck).expect("Invalid coldkey"); + SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + num_traits::ToPrimitive::to_u64( + &(num_traits::ToPrimitive::to_f64(&diffs[idx].1).expect("float conv fail") + * 0.9_f64) + .abs(), + ) + .expect("u64 conv fail") + .into(), + ); + + let w = try_restore_shares::(); + assert!(!w.is_zero(), "weight must be non-zero"); + + // Check stake is near 0 for all positive entires except the one we removed + // Check the stake for all negative entries is proportional to the amount they lost + let total_lost: f64 = diffs + .iter() + .map(|(_, diff)| { + if diff.is_negative() { + num_traits::ToPrimitive::to_f64(&diff.saturating_abs()) + .expect("float conv fail") + } else { + 0_f64 + } + }) + .sum::(); + let mut total_returned = 0_f64; + for (coldkey, diff) in diffs { + let coldkey_account_id = decode_account_id32::(coldkey).expect("Invalid coldkey"); + + let stake_float: f64 = num_traits::ToPrimitive::to_f64( + &SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ) + .to_u64(), + ) + .expect("float conv fail"); + if diff > 0 { + log::debug!("diff: {} for ck: {}", diff, coldkey); + assert_relative_eq!(stake_float, 0_f64, max_relative = 0.001_f64); + } else { + total_returned += stake_float; + } + } + + for (coldkey, diff) in diffs { + let coldkey_account_id = decode_account_id32::(coldkey).expect("Invalid coldkey"); + let stake_float: f64 = num_traits::ToPrimitive::to_f64( + &SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ) + .to_u64(), + ) + .expect("float conv fail"); + if diff < 0 { + // Should get a return proportional to the amount they lost + // versus the amount that was able to be recovered + let prop_returned: f64 = num_traits::ToPrimitive::to_f64(&diff.abs()) + .expect("float conv fail") + / total_lost + * total_returned; + assert_relative_eq!(stake_float, prop_returned, max_relative = 0.001_f64); + } + } + }); +} + +#[test] +fn test_migrate_fix_bad_hk_swap_mainnet_has_more() { + // test with some of the gainers have a balance higher than the gain before the migration + new_test_ext(1).execute_with(|| { + use crate::migrations::migrate_fix_bad_hk_swap::*; + + let netuid = NetUid::from(59); + // Add subnet 59 + add_network(netuid, 10, 0); + SubtokenEnabled::::insert(netuid, true); + SubnetMechanism::::insert(netuid, 1); + + let hotkey = "5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN"; + let hotkey_account_id = decode_account_id32::(hotkey).expect("Invalid hotkey"); + + #[rustfmt::skip] + let diffs: [(&str, i64); 112] = [ + ("5Fn9SqQhx5bhDua7AGgkKxxk3gfZ75WWBGCMPeKH1WBgPaMQ", -2375685930981_i64), + ("5Fnhtm7cpxEbZaChnRZ8yWoF8MXVxmobkmLRehh5bkYtyZA9", -4090996138227), + ("5C7j3w2zz1SVejRuFrb2zFWHXT7UfG7eWA87KXL1WyV5KLVR", -607494031), + ("5DthZ1rvnXBb9oXVNtrMaMsDAnRxBPZCjD6fdRdeqC3fg1ca", -17022477949), + ("5F7BkPL3EVjKTYMbBkEmPAtTZQSGeyNzFPaf1DtebPFmJsJ7", -4016510), + ("5EefisctzgWdVGFQaL4LjFFacTE7dM4YJVNy3ogGBQoapTU1", -13106893093), + ("5CwkvpBxHCaRK9xBC2n6WdhpF5zg9t5WLkGorASaoErdynFQ", 439139249152), + ("5FU7ErUtmi22xuqeeCYVpNZp6WVSSL98hqDi5iyeZbkXtkbe", -35958768555), + ("5D7HL8T95qkHQTPFjgSFCjRoeM7oE3vQBYjiR1kAPbPxcMKu", -201914811997), + ("5HL3pPdDFY94Qdf8VnbfT4W6LXFkpd68Y5GSGzNJfntMdGZX", -235660917467), + ("5EcYAz8SBKWsogA6meJmVXcwVp4tjCvw3ZnJE6UXTyWNUdF2", -500070769668), + ("5EoE3c7XMf8TN3yudAaFjv4yvjtWYRviHcXi73EXkLHmWTCB", -86442928436), + ("5CMDjL7t2biHGREBwrmd8renD74FLEhjCVqfJG2MXckWBwDu", 1039317), + ("5CVGKimL4cLgyTqvYKQbPKYFZfiztsdczU7HrwNdSFKbbn5D", 4224201), + ("5HmZnEcW4eHbXmUEFWJbc4GHnBBYEK8ZPsFa25PuEmP5iuwM", -13156128), + ("5FNa4J4fTKh555CEyXHgR29RicSm8nTEHx36utTa4MJepJyX", -9519954), + ("5Gun93uQgffYpxqMKSmfG18AHiQW7Z2GR2dfPPR8W188vJYc", -1127662), + ("5HW1C4js4RyjQqNwALSUZC8NJ2WinD5Si2X2XkstXrMW2uYo", -34457336758), + ("5EqMhjdLY9h64ui2mizRZyBp1mEPJ7s4TsfAxQSQkAFmMfzE", -9346443829744), + ("5H3XwzydgE2XUGoJCR4dSj7tkd7uxZDJqik69hux2DBcruom", -1215347774), + ("5DjkmYpCUX6dBTvGoyN9j4QZhtMPhdcywDE8cJ8Qq1vg4X6e", -3603984447), + ("5E7Z7Btjz74XpZLH5fRzfZqiHCo4j9PXKfqi88kQ5MFrds34", -823907380854), + ("5DSBWN4hN9413C6o6A2hR9tYUbHjWsQqPRV74GrnCrMkCGJx", -309708781), + ("5FMLRmKPqsTsMbakpVUwoYro1P64QXVNTWyzNDugaNwSKRzF", -137525398263), + ("5EFZf5pnTqLegv6gxCrb6TKBQBGz9xLJNK8x9eR273cSons6", -1521760918), + ("5CGAGEuMLaidBDk8bDZKJb23dxRSP1wLenLALGLw8BTG1E3W", -544739696), + ("5HSzRtcQjD5KP6Nh2GVSS16aLDe6q9R33Wpu6s2eEeeo3AYS", -2309184790), + ("5DALvFDcfANQJcWz6AXMfDqabnoZhdDMoH6FxqYUibug1ja7", -369405632507), + ("5Fy8iWkpcbsskmEN1nYZDdS9zKh167Em9RRYisoss7jaYXxi", 15257429), + ("5CQCVTRyqJgZKBDmtHzpoF8su6BScLcNGbX8t3WMm5qYbbJH", -10721968), + ("5DXZByh2NS4MU61a1aaLrcLYpyzpJgHe95TEBdcEN2cF1SA5", -655946136), + ("5EX5yAYiABFzKDQJDe1kRVwFm3XRRY4HyLMe4Vu9A5U2VEVT", -325581360246), + ("5FvabwjtyW887gtc7vUnUc47KVhy17UeaNLRjzTg5nkVACMP", -77588524213), + ("5HTbYi5cmgWJxvyTy9JeYdtnjoDzjXnEXTGFsPEVx9iRPmVF", -53542953784), + ("5CWzmvA17MAMQ9mnAecLxFXS2N8846rz6T7m4QNHyVtJVq4j", 2672295922502), + ("5DSYntgHZY4krYUtkkQZyyoffVtu5e8rYWhXuhs832zY6YKy", -2680205688), + ("5EYyTFyLDqXscaa5VtXTvUc3x2ow2TeT8G12ZDMZwE6uFWPQ", -39165843935), + ("5CohfM1qdyNwdeJEex1Zyht3S2WS48rV993DmVbyKs2mEEd6", -4004685632), + ("5Gx6Y7UQD39Latgxigr6mHbnh1herpwNPau2PjvzwLWEjXL3", -559504), + ("5Hh4Efq5WDwe8URjjUqUNX8KxtMwLHLViwoRvXfEpXQCZakh", -32541090531), + ("5GWRHC7Nd8njqTPsdJkp6ngniCCBu9UjGhLfxp2jF1fPrfZ4", -5394093031), + ("5GNAB64UN32krzr3Xxu5LW6naeu2P3XULcdBCR9VZ5Libyit", -24884230), + ("5EEz25th1nYNM5xR1UsyFFAUaXMjdHqLxZ3wUjyHokYbXHku", -12525171), + ("5HKJq4JCS9xoKdYhcRnsRp1bodovba7ncd5KTYVwfReKaxHT", -408133990236), + ("5DXs7x664RL5NdSW77DTseLiu84unstuHGuqvmY61UtJwzRN", -3095078614148), + ("5CDfdDaA2p9sK1ia5yMVYfzgtFs2e1TrSAxuQqXoS28Lcrxf", -1032856892), + ("5Ecg4vD2zKXHDFhQqogWq1dZdijPsDty8rGsZu3raeoJSiXb", -995678), + ("5C5Yg63TNLb68Tu819qXd3Bt4giG8mAPzLmAFSqa2HC1R5Rm", -40818739830910), + ("5HHH25Wuf9rmVuk9cMKU1hCCPJ1qbHBd1SyHj91R3fMT36yb", -391416057906), + ("5GKGGE5YLHoDciYJ6Ec2YnUP3SykSQPA47hqmwBP63EtVrd9", -413944553000), + ("5CSi9ZLyiXfLeYtEFaZSBuTofNMRnXEJEJE9CS4gGaT6CkWt", -17811605275), + ("5CSoA7QVdFHHBZz53bbRV2mC5vhL64ehhWa8ibtLppmt2n3J", -65701320107), + ("5GpA5BtfMMX52rXztrha79YqfwR4YaSfTuAcb48Yt73U4h71", -2194562), + ("5Euz5wpb4xiDWfV1A6AKK6i6ca3WoZQD5hCVyf1fws8GXh4z", -6143407839874), + ("5DZzmhCG7SMK3LwrkmHZ8ZBwaAByMjfBpEid14nNQdxHipCE", -386645), + ("5Fc9Vo3hkbr6bPxJpjQo5sQ43L5Hc2G8R5BdqRYF8psvB5pw", 55668553), + ("5GuSHC3iowySHLDW4pEyEZE6PKxKP62YpJYJyBy5tijzAnYz", -159317636526), + ("5HVVZrUBPvjYHiwaSvtvaN9GZogoznM49m2AEmVW6RXnYCka", -1995572213), + ("5EcGpeV2wjkCVsBjsBifSWbdcqH98b6oEY8beDY59c4fXkhw", -177096614584), + ("5GnCjvWJEESwVNFZzy85zbBzw26etuEt87WiqsE3ee2Ws1wm", -1961445), + ("5GWuPUpTuChAqKxvU22TRLvRkBFiyWWZnq9cLpJN6SSvkho1", -94157569391), + ("5FXHf7q5rvBXnzQgmsa31Db9rjcRy6ZHKMiyDSb8Vs5p2msN", -688433531658), + ("5GbxkzytnvbRuNQ7qxPpfPuWMoeitS8V4KDY9jSshE5fDegD", -19085313), + ("5Gus1B7c9uWkky7Yawh2tKR1V6AMh5DbqUBPq881JHqeqVqY", -16101671818), + ("5DLhRdbvWkYYScDmwx4QgJfieSN4apBWbZ2yno3MfgbR8hBP", -21062025), + ("5Cg5kVyNEs7MWWRHU8X5MHwX5cN3aegvC4RBt2JK19w2GiR8", -2593737050), + ("5Dkushsxtc8AdCf287MtTYHQv9DoZeBRpttUpBtmyFhGy3uR", -48672832345630), + ("5EqNqVsHj9bQVyEujcm62zjMYUFhTLY7rTP854txSrJzyoco", -3828526), + ("5Dea6d6nKErEbRQ4MBGuCALn8NZ2xo4kaa51hB5KMriPBkEM", -1560192853875), + ("5DNt2XDWdeMd4H92FLnfUvkqyXzmavezHvzLboP3VgT1xLZV", -831964576998), + ("5FKtFoTeK8aaG6HZTrDgvoYHVQ5NY4S9VyV7W5K74cWcwLYA", -60823501166), + ("5GEBanZKUU7Hrf8K2VNi33HxyJRstgQ3WD3odHgvMj2nPbhi", -98946626902), + ("5CUtw7LYB2n2bzgXt6YnmKDHt6PsB3kKAyD9azYJNcRG8TNg", -9779588557490), + ("5EynbF72b12fbgMvEeL1vJSY342rCryNbuwxFivU1Xevtmv3", -17314385200455), + ("5CapiZRuULed8ConS1gbjMVgnwcT5JnQah7tx6sZnK7sJJuJ", -5810972), + ("5DnaxLaNduf41WM6WWZ4fkzcGzWNWx6eLJyQpSaMueUGCsaU", -12668760), + ("5Cqz9SChYPxTFZ2623rE2aQQ5ttQoLwZ8yfwYgiZyQDANqZn", -683549), + ("5C8ZcLzF23GrXKdH4Pg3ZXC3vKQsF5PM8VvhzzxzTQksgj8e", -44720570590), + ("5GuNsmoswrP6hTKZkKcpTpZftTMKrmnCHvTL2V3NHJy2fpen", -5042891812715), + ("5F1TYDkLnP36HHY5btigxyKUPzBraxdrU1aX1bqFfPfcfnzU", -1189104279832), + ("5Dc384z9HuTGF6oratZs1fLciCHtPZaLhrHfCVw82a5AikWZ", -616163196988), + ("5DhcaEUsRKhZQ31qRffJqjtLmFkbVaCebn8nVjYhvB4KJtX5", -17746006723), + ("5HYE7z3xTcrN1rqz54NyZRAkehFfRMcaEcdoMq5g5ATET5wQ", 212509751245), + ("5F97DdEVTy9gPCtN6jkJJENDJuQiRGiwbMVSL74qRq8FCq5W", 2225287736222), + ("5FUVN133rSvuKXgsXKMR2ZEaysxZjkRUFUWS1UMyNGre9xFV", -73216740161), + ("5CZeimtfpRqQgPxVwr1MzfG2Sok8E1AMERHo6vUmEdRS5JiU", -3937802), + ("5Eqq2JwGh7qbtnjPiFEPmmnHxs3S4J4Ahg8fr4sybZV1tPdY", -173406860562), + ("5ERfDw6K3GmQqwqsEG6foFtu7VsYGifPi556UJKQsBnfbHKN", 96022588728), + ("5Ek8RkU6KMv5Fx7yivRVoQkuJYAKhULWiLWDpbGG4hvR9HFD", 968139369093), + ("5HpCpGALzqgnDTP1HXFiuhzD5MFaDTRHjXBCvaMY9LNNRkT9", 104943979521), + ("5FYqS77gxW9gHG8id1YYPS7Cd4TQmNUMhF8h3S77Fq2VvvRQ", 729199757977), + ("5FtBqMg13pNf1N6TwfG6BmwyaDM77mkeQ16UTHsGasrVDedX", 131457064336), + ("5GbnWR2XhWrRMt123SdrLbR9G2a4N5dtzA3TSu3Czkzoeu7x", 2295599153), + ("5GgiowcCG4kLpwkCTGxxQJQv8WwKFyBQ6McPRmqKtWPy8EaK", 113838605389), + ("5FL5YtYozpUAGaiVWonpbwEYdEMij3obJHSH3ACY4vgWmDgy", 8689039), + ("5Egq58bxRv7boM2s3rnDxx1udnkzxPQ23HuoqohVxjh9RenC", 216373234348), + ("5FRGeeEgRNR8U33FDKvN7yUgts8zR3qRJH4yKKWoR9GswBRb", 2196574958718), + ("5FnhSy79BPYyrmmFsbckinQw1fLiLqqPkQL2vgZwPxbRfu3k", 42319631507), + ("5Hj8jMhqAv7cfyRh5STfbZefMhv17QxZ1RxWq9jNcLAEsRRo", 132216702183491), + ("5EXYTGMqumAH6RLQgHwkMEMnSvHcpHc89R6U8krfNJTYWm9J", 504320264499), + ("5EFh8ctzmytXURqrCTUBWHTs87f7TMWB6XKUzdqxKXVUtvS2", 2209599669432), + ("5CqVqEcRBkw7Gm2reJ33cj7puR9W2Tq7qsLxSruV1BgnMqKN", 1033387458788), + ("5D79enmLSGimsruoraGagofhaSeYJZvGUqFCCrr83ZfZs1HS", 7591184215233), + ("5HbpyjsvyXLWtf1QT1CyNUdyut6scM5dM7ytm8hoxFvRtU1i", 129833188275), + ("5CnxCi7CdEriWSdw4LcXdbtjodxA6uTat4gBm4wuT9QToMdo", 3132978), + ("5G48fiQjhAd8hc4rYc6GituCuAPKznL28jyyyq1auMyZiG4t", 514913328178), + ("5FFGjW2hJ7tQ41qghSsLP4cVmA8j9pZVSrr2CrLG7fQAsLHJ", 346794972723), + ("5FWjnxeRMtMFxRc9kvZKCG5iJAyyz2kmXV8u3kqyiXizZtiz", 225939835005), + ("5CUw3sB4oxd3dVSHUr3kxsB591VEjaPzr444KkfjwVFnLRfJ", 208250614494), + ("5EaBhxNUwMRyKsaeA2BEjDCrvwE5J8FDSpfCHK9gGmnmbhCa", 278083207003), + ("5GHJ5HxFxYQyVoNFUxR3JCqqCKRumaFCY7N5zMxwF4CpRUWr", 1381466224829), + ("5H1WgA7ET3FmEarJK6qc1vaTWbNd6g41mgvyLRkysrH4MDdo", 774889), + ]; + + for (coldkey, diff) in diffs { + let coldkey_account_id = decode_account_id32::(coldkey).expect("Invalid coldkey"); + if diff > 0 { + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + diff.unsigned_abs().into(), + ); + } + } + + // For one of the gainers, add some extra stake + let idx = 6; + let gained_ck = diffs[idx].0; + let coldkey_account_id = decode_account_id32::(gained_ck).expect("Invalid coldkey"); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + num_traits::ToPrimitive::to_u64( + &((num_traits::ToPrimitive::to_f64(&diffs[idx].1).expect("float conv fail") + * 0.9_f64) + .abs()), + ) + .expect("u64 conv fail") + .into(), + ); + let extra_balance = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ) + .saturating_sub( + num_traits::ToPrimitive::to_u64(&diffs[idx].1.abs()) + .expect("float conv fail") + .into(), + ); + assert!( + extra_balance.to_u64() > 0_u64, + "extra balance must be positive" + ); + + let w = try_restore_shares::(); + assert!(!w.is_zero(), "weight must be non-zero"); + + // Check stake is near 0 for all positive entires except the one we removed + // Check the stake for all negative entries is proportional to the amount they lost + let total_lost: f64 = diffs + .iter() + .map(|(_, diff)| { + if diff.is_negative() { + num_traits::ToPrimitive::to_f64(&diff.saturating_abs()) + .expect("float conv fail") + } else { + 0_f64 + } + }) + .sum::(); + let mut total_returned = 0_f64; + for (coldkey, diff) in diffs { + let coldkey_account_id = decode_account_id32::(coldkey).expect("Invalid coldkey"); + + let stake_float: f64 = num_traits::ToPrimitive::to_f64( + &SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ) + .to_u64(), + ) + .expect("float conv fail"); + if diff > 0 { + if coldkey == gained_ck { + // this CK should retain the extra balance + assert_relative_eq!( + stake_float, + num_traits::ToPrimitive::to_f64(&extra_balance.to_u64()) + .expect("float conv fail"), + max_relative = 0.001_f64 + ); + } else { + assert_relative_eq!(stake_float, 0_f64, max_relative = 0.001_f64); + } + } else { + total_returned += stake_float; + } + } + + for (coldkey, diff) in diffs { + let coldkey_account_id = decode_account_id32::(coldkey).expect("Invalid coldkey"); + let stake_float: f64 = num_traits::ToPrimitive::to_f64( + &SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ) + .to_u64(), + ) + .expect("float conv fail"); + if diff < 0 { + // Should get a return proportional to the amount they lost + // versus the amount that was able to be recovered + let prop_returned: f64 = num_traits::ToPrimitive::to_f64(&diff.abs()) + .expect("float conv fail") + / total_lost + * total_returned; + assert_relative_eq!(stake_float, prop_returned, max_relative = 0.001_f64); + } + } + }); +} + +#[test] +fn test_migrate_fix_bad_hk_swap_mainnet_some_entries() { + // test with some of the losers have an existing balance before the migration + new_test_ext(1).execute_with(|| { + use crate::migrations::migrate_fix_bad_hk_swap::*; + + let netuid = NetUid::from(59); + // Add subnet 59 + add_network(netuid, 10, 0); + SubtokenEnabled::::insert(netuid, true); + SubnetMechanism::::insert(netuid, 1); + + let hotkey = "5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN"; + let hotkey_account_id = decode_account_id32::(hotkey).expect("Invalid hotkey"); + + #[rustfmt::skip] + let diffs: [(&str, i64); 112] = [ + ("5Fn9SqQhx5bhDua7AGgkKxxk3gfZ75WWBGCMPeKH1WBgPaMQ", -2375685930981_i64), + ("5Fnhtm7cpxEbZaChnRZ8yWoF8MXVxmobkmLRehh5bkYtyZA9", -4090996138227), + ("5C7j3w2zz1SVejRuFrb2zFWHXT7UfG7eWA87KXL1WyV5KLVR", -607494031), + ("5DthZ1rvnXBb9oXVNtrMaMsDAnRxBPZCjD6fdRdeqC3fg1ca", -17022477949), + ("5F7BkPL3EVjKTYMbBkEmPAtTZQSGeyNzFPaf1DtebPFmJsJ7", -4016510), + ("5EefisctzgWdVGFQaL4LjFFacTE7dM4YJVNy3ogGBQoapTU1", -13106893093), + ("5CwkvpBxHCaRK9xBC2n6WdhpF5zg9t5WLkGorASaoErdynFQ", 439139249152), + ("5FU7ErUtmi22xuqeeCYVpNZp6WVSSL98hqDi5iyeZbkXtkbe", -35958768555), + ("5D7HL8T95qkHQTPFjgSFCjRoeM7oE3vQBYjiR1kAPbPxcMKu", -201914811997), + ("5HL3pPdDFY94Qdf8VnbfT4W6LXFkpd68Y5GSGzNJfntMdGZX", -235660917467), + ("5EcYAz8SBKWsogA6meJmVXcwVp4tjCvw3ZnJE6UXTyWNUdF2", -500070769668), + ("5EoE3c7XMf8TN3yudAaFjv4yvjtWYRviHcXi73EXkLHmWTCB", -86442928436), + ("5CMDjL7t2biHGREBwrmd8renD74FLEhjCVqfJG2MXckWBwDu", 1039317), + ("5CVGKimL4cLgyTqvYKQbPKYFZfiztsdczU7HrwNdSFKbbn5D", 4224201), + ("5HmZnEcW4eHbXmUEFWJbc4GHnBBYEK8ZPsFa25PuEmP5iuwM", -13156128), + ("5FNa4J4fTKh555CEyXHgR29RicSm8nTEHx36utTa4MJepJyX", -9519954), + ("5Gun93uQgffYpxqMKSmfG18AHiQW7Z2GR2dfPPR8W188vJYc", -1127662), + ("5HW1C4js4RyjQqNwALSUZC8NJ2WinD5Si2X2XkstXrMW2uYo", -34457336758), + ("5EqMhjdLY9h64ui2mizRZyBp1mEPJ7s4TsfAxQSQkAFmMfzE", -9346443829744), + ("5H3XwzydgE2XUGoJCR4dSj7tkd7uxZDJqik69hux2DBcruom", -1215347774), + ("5DjkmYpCUX6dBTvGoyN9j4QZhtMPhdcywDE8cJ8Qq1vg4X6e", -3603984447), + ("5E7Z7Btjz74XpZLH5fRzfZqiHCo4j9PXKfqi88kQ5MFrds34", -823907380854), + ("5DSBWN4hN9413C6o6A2hR9tYUbHjWsQqPRV74GrnCrMkCGJx", -309708781), + ("5FMLRmKPqsTsMbakpVUwoYro1P64QXVNTWyzNDugaNwSKRzF", -137525398263), + ("5EFZf5pnTqLegv6gxCrb6TKBQBGz9xLJNK8x9eR273cSons6", -1521760918), + ("5CGAGEuMLaidBDk8bDZKJb23dxRSP1wLenLALGLw8BTG1E3W", -544739696), + ("5HSzRtcQjD5KP6Nh2GVSS16aLDe6q9R33Wpu6s2eEeeo3AYS", -2309184790), + ("5DALvFDcfANQJcWz6AXMfDqabnoZhdDMoH6FxqYUibug1ja7", -369405632507), + ("5Fy8iWkpcbsskmEN1nYZDdS9zKh167Em9RRYisoss7jaYXxi", 15257429), + ("5CQCVTRyqJgZKBDmtHzpoF8su6BScLcNGbX8t3WMm5qYbbJH", -10721968), + ("5DXZByh2NS4MU61a1aaLrcLYpyzpJgHe95TEBdcEN2cF1SA5", -655946136), + ("5EX5yAYiABFzKDQJDe1kRVwFm3XRRY4HyLMe4Vu9A5U2VEVT", -325581360246), + ("5FvabwjtyW887gtc7vUnUc47KVhy17UeaNLRjzTg5nkVACMP", -77588524213), + ("5HTbYi5cmgWJxvyTy9JeYdtnjoDzjXnEXTGFsPEVx9iRPmVF", -53542953784), + ("5CWzmvA17MAMQ9mnAecLxFXS2N8846rz6T7m4QNHyVtJVq4j", 2672295922502), + ("5DSYntgHZY4krYUtkkQZyyoffVtu5e8rYWhXuhs832zY6YKy", -2680205688), + ("5EYyTFyLDqXscaa5VtXTvUc3x2ow2TeT8G12ZDMZwE6uFWPQ", -39165843935), + ("5CohfM1qdyNwdeJEex1Zyht3S2WS48rV993DmVbyKs2mEEd6", -4004685632), + ("5Gx6Y7UQD39Latgxigr6mHbnh1herpwNPau2PjvzwLWEjXL3", -559504), + ("5Hh4Efq5WDwe8URjjUqUNX8KxtMwLHLViwoRvXfEpXQCZakh", -32541090531), + ("5GWRHC7Nd8njqTPsdJkp6ngniCCBu9UjGhLfxp2jF1fPrfZ4", -5394093031), + ("5GNAB64UN32krzr3Xxu5LW6naeu2P3XULcdBCR9VZ5Libyit", -24884230), + ("5EEz25th1nYNM5xR1UsyFFAUaXMjdHqLxZ3wUjyHokYbXHku", -12525171), + ("5HKJq4JCS9xoKdYhcRnsRp1bodovba7ncd5KTYVwfReKaxHT", -408133990236), + ("5DXs7x664RL5NdSW77DTseLiu84unstuHGuqvmY61UtJwzRN", -3095078614148), + ("5CDfdDaA2p9sK1ia5yMVYfzgtFs2e1TrSAxuQqXoS28Lcrxf", -1032856892), + ("5Ecg4vD2zKXHDFhQqogWq1dZdijPsDty8rGsZu3raeoJSiXb", -995678), + ("5C5Yg63TNLb68Tu819qXd3Bt4giG8mAPzLmAFSqa2HC1R5Rm", -40818739830910), + ("5HHH25Wuf9rmVuk9cMKU1hCCPJ1qbHBd1SyHj91R3fMT36yb", -391416057906), + ("5GKGGE5YLHoDciYJ6Ec2YnUP3SykSQPA47hqmwBP63EtVrd9", -413944553000), + ("5CSi9ZLyiXfLeYtEFaZSBuTofNMRnXEJEJE9CS4gGaT6CkWt", -17811605275), + ("5CSoA7QVdFHHBZz53bbRV2mC5vhL64ehhWa8ibtLppmt2n3J", -65701320107), + ("5GpA5BtfMMX52rXztrha79YqfwR4YaSfTuAcb48Yt73U4h71", -2194562), + ("5Euz5wpb4xiDWfV1A6AKK6i6ca3WoZQD5hCVyf1fws8GXh4z", -6143407839874), + ("5DZzmhCG7SMK3LwrkmHZ8ZBwaAByMjfBpEid14nNQdxHipCE", -386645), + ("5Fc9Vo3hkbr6bPxJpjQo5sQ43L5Hc2G8R5BdqRYF8psvB5pw", 55668553), + ("5GuSHC3iowySHLDW4pEyEZE6PKxKP62YpJYJyBy5tijzAnYz", -159317636526), + ("5HVVZrUBPvjYHiwaSvtvaN9GZogoznM49m2AEmVW6RXnYCka", -1995572213), + ("5EcGpeV2wjkCVsBjsBifSWbdcqH98b6oEY8beDY59c4fXkhw", -177096614584), + ("5GnCjvWJEESwVNFZzy85zbBzw26etuEt87WiqsE3ee2Ws1wm", -1961445), + ("5GWuPUpTuChAqKxvU22TRLvRkBFiyWWZnq9cLpJN6SSvkho1", -94157569391), + ("5FXHf7q5rvBXnzQgmsa31Db9rjcRy6ZHKMiyDSb8Vs5p2msN", -688433531658), + ("5GbxkzytnvbRuNQ7qxPpfPuWMoeitS8V4KDY9jSshE5fDegD", -19085313), + ("5Gus1B7c9uWkky7Yawh2tKR1V6AMh5DbqUBPq881JHqeqVqY", -16101671818), + ("5DLhRdbvWkYYScDmwx4QgJfieSN4apBWbZ2yno3MfgbR8hBP", -21062025), + ("5Cg5kVyNEs7MWWRHU8X5MHwX5cN3aegvC4RBt2JK19w2GiR8", -2593737050), + ("5Dkushsxtc8AdCf287MtTYHQv9DoZeBRpttUpBtmyFhGy3uR", -48672832345630), + ("5EqNqVsHj9bQVyEujcm62zjMYUFhTLY7rTP854txSrJzyoco", -3828526), + ("5Dea6d6nKErEbRQ4MBGuCALn8NZ2xo4kaa51hB5KMriPBkEM", -1560192853875), + ("5DNt2XDWdeMd4H92FLnfUvkqyXzmavezHvzLboP3VgT1xLZV", -831964576998), + ("5FKtFoTeK8aaG6HZTrDgvoYHVQ5NY4S9VyV7W5K74cWcwLYA", -60823501166), + ("5GEBanZKUU7Hrf8K2VNi33HxyJRstgQ3WD3odHgvMj2nPbhi", -98946626902), + ("5CUtw7LYB2n2bzgXt6YnmKDHt6PsB3kKAyD9azYJNcRG8TNg", -9779588557490), + ("5EynbF72b12fbgMvEeL1vJSY342rCryNbuwxFivU1Xevtmv3", -17314385200455), + ("5CapiZRuULed8ConS1gbjMVgnwcT5JnQah7tx6sZnK7sJJuJ", -5810972), + ("5DnaxLaNduf41WM6WWZ4fkzcGzWNWx6eLJyQpSaMueUGCsaU", -12668760), + ("5Cqz9SChYPxTFZ2623rE2aQQ5ttQoLwZ8yfwYgiZyQDANqZn", -683549), + ("5C8ZcLzF23GrXKdH4Pg3ZXC3vKQsF5PM8VvhzzxzTQksgj8e", -44720570590), + ("5GuNsmoswrP6hTKZkKcpTpZftTMKrmnCHvTL2V3NHJy2fpen", -5042891812715), + ("5F1TYDkLnP36HHY5btigxyKUPzBraxdrU1aX1bqFfPfcfnzU", -1189104279832), + ("5Dc384z9HuTGF6oratZs1fLciCHtPZaLhrHfCVw82a5AikWZ", -616163196988), + ("5DhcaEUsRKhZQ31qRffJqjtLmFkbVaCebn8nVjYhvB4KJtX5", -17746006723), + ("5HYE7z3xTcrN1rqz54NyZRAkehFfRMcaEcdoMq5g5ATET5wQ", 212509751245), + ("5F97DdEVTy9gPCtN6jkJJENDJuQiRGiwbMVSL74qRq8FCq5W", 2225287736222), + ("5FUVN133rSvuKXgsXKMR2ZEaysxZjkRUFUWS1UMyNGre9xFV", -73216740161), + ("5CZeimtfpRqQgPxVwr1MzfG2Sok8E1AMERHo6vUmEdRS5JiU", -3937802), + ("5Eqq2JwGh7qbtnjPiFEPmmnHxs3S4J4Ahg8fr4sybZV1tPdY", -173406860562), + ("5ERfDw6K3GmQqwqsEG6foFtu7VsYGifPi556UJKQsBnfbHKN", 96022588728), + ("5Ek8RkU6KMv5Fx7yivRVoQkuJYAKhULWiLWDpbGG4hvR9HFD", 968139369093), + ("5HpCpGALzqgnDTP1HXFiuhzD5MFaDTRHjXBCvaMY9LNNRkT9", 104943979521), + ("5FYqS77gxW9gHG8id1YYPS7Cd4TQmNUMhF8h3S77Fq2VvvRQ", 729199757977), + ("5FtBqMg13pNf1N6TwfG6BmwyaDM77mkeQ16UTHsGasrVDedX", 131457064336), + ("5GbnWR2XhWrRMt123SdrLbR9G2a4N5dtzA3TSu3Czkzoeu7x", 2295599153), + ("5GgiowcCG4kLpwkCTGxxQJQv8WwKFyBQ6McPRmqKtWPy8EaK", 113838605389), + ("5FL5YtYozpUAGaiVWonpbwEYdEMij3obJHSH3ACY4vgWmDgy", 8689039), + ("5Egq58bxRv7boM2s3rnDxx1udnkzxPQ23HuoqohVxjh9RenC", 216373234348), + ("5FRGeeEgRNR8U33FDKvN7yUgts8zR3qRJH4yKKWoR9GswBRb", 2196574958718), + ("5FnhSy79BPYyrmmFsbckinQw1fLiLqqPkQL2vgZwPxbRfu3k", 42319631507), + ("5Hj8jMhqAv7cfyRh5STfbZefMhv17QxZ1RxWq9jNcLAEsRRo", 132216702183491), + ("5EXYTGMqumAH6RLQgHwkMEMnSvHcpHc89R6U8krfNJTYWm9J", 504320264499), + ("5EFh8ctzmytXURqrCTUBWHTs87f7TMWB6XKUzdqxKXVUtvS2", 2209599669432), + ("5CqVqEcRBkw7Gm2reJ33cj7puR9W2Tq7qsLxSruV1BgnMqKN", 1033387458788), + ("5D79enmLSGimsruoraGagofhaSeYJZvGUqFCCrr83ZfZs1HS", 7591184215233), + ("5HbpyjsvyXLWtf1QT1CyNUdyut6scM5dM7ytm8hoxFvRtU1i", 129833188275), + ("5CnxCi7CdEriWSdw4LcXdbtjodxA6uTat4gBm4wuT9QToMdo", 3132978), + ("5G48fiQjhAd8hc4rYc6GituCuAPKznL28jyyyq1auMyZiG4t", 514913328178), + ("5FFGjW2hJ7tQ41qghSsLP4cVmA8j9pZVSrr2CrLG7fQAsLHJ", 346794972723), + ("5FWjnxeRMtMFxRc9kvZKCG5iJAyyz2kmXV8u3kqyiXizZtiz", 225939835005), + ("5CUw3sB4oxd3dVSHUr3kxsB591VEjaPzr444KkfjwVFnLRfJ", 208250614494), + ("5EaBhxNUwMRyKsaeA2BEjDCrvwE5J8FDSpfCHK9gGmnmbhCa", 278083207003), + ("5GHJ5HxFxYQyVoNFUxR3JCqqCKRumaFCY7N5zMxwF4CpRUWr", 1381466224829), + ("5H1WgA7ET3FmEarJK6qc1vaTWbNd6g41mgvyLRkysrH4MDdo", 774889), + ]; + + for (coldkey, diff) in diffs { + let coldkey_account_id = decode_account_id32::(coldkey).expect("Invalid coldkey"); + if diff > 0 { + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + diff.unsigned_abs().into(), + ); + } + } + + // For one of the losers, add some extra stake + let idx = 0; + let lost_ck = diffs[idx].0; + let coldkey_account_id = decode_account_id32::(lost_ck).expect("Invalid coldkey"); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + num_traits::ToPrimitive::to_u64( + &((num_traits::ToPrimitive::to_f64(&diffs[idx].1).expect("float conv fail") + * 0.9_f64) + .abs()), + ) + .expect("u64 conv fail") + .into(), + ); + let extra_balance = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ); + assert!(!extra_balance.is_zero(), "extra balance must be non-zero"); + + let w = try_restore_shares::(); + assert!(!w.is_zero(), "weight must be non-zero"); + + // Check stake is near 0 for all positive entires except the one we removed + // Check the stake for all negative entries is proportional to the amount they lost + let total_lost: f64 = diffs + .iter() + .map(|(_, diff)| { + if diff.is_negative() { + num_traits::ToPrimitive::to_f64(&diff.saturating_abs()) + .expect("float conv fail") + } else { + 0_f64 + } + }) + .sum::(); + let mut total_returned = 0_f64; + for (coldkey, diff) in diffs { + let coldkey_account_id = decode_account_id32::(coldkey).expect("Invalid coldkey"); + + let stake_float: f64 = num_traits::ToPrimitive::to_f64( + &SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ) + .to_u64(), + ) + .expect("float conv fail"); + if diff > 0 { + assert_relative_eq!(stake_float, 0_f64, max_relative = 0.001_f64); + } else if coldkey == lost_ck { + total_returned += stake_float + - num_traits::ToPrimitive::to_f64(&extra_balance.to_u64()) + .expect("float conv fail"); + } else { + total_returned += stake_float; + } + } + + for (coldkey, diff) in diffs { + let coldkey_account_id = decode_account_id32::(coldkey).expect("Invalid coldkey"); + let stake_float: f64 = num_traits::ToPrimitive::to_f64( + &SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ) + .to_u64(), + ) + .expect("float conv fail"); + if diff < 0 { + // Should get a return proportional to the amount they lost + // versus the amount that was able to be recovered + let prop_returned: f64 = num_traits::ToPrimitive::to_f64(&diff.abs()) + .expect("float conv fail") + / total_lost + * total_returned; + + let mut expected_stake: f64 = prop_returned; + if coldkey == lost_ck { + // this CK should retain the extra balance + expected_stake = prop_returned + + num_traits::ToPrimitive::to_f64(&extra_balance.to_u64()) + .expect("float conv fail"); + } + + assert_relative_eq!(stake_float, expected_stake, max_relative = 0.001_f64); + } + } + }); +} diff --git a/pallets/subtensor/src/tests/migration/fix_root_claimed.rs b/pallets/subtensor/src/tests/migration/fix_root_claimed.rs new file mode 100644 index 0000000000..26ee4e8dd4 --- /dev/null +++ b/pallets/subtensor/src/tests/migration/fix_root_claimed.rs @@ -0,0 +1,177 @@ +#![allow( + unused, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used +)] +//! root claimed overclaim repair. + +use super::helpers::*; +use super::prelude::*; + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::migration::test_migrate_fix_root_claimed_overclaim --exact --nocapture +#[test] +fn test_migrate_fix_root_claimed_overclaim() { + use crate::migrations::migrate_fix_root_claimed_overclaim::*; + + let new_hotkey = decode_account_id32_test("5H6BqkzjYvViiqp7rQLXjpnaEmW7U9CoKxXhQ4efMqtX1mQw"); + let untouched_hotkey = U256::from(7777_u64); + let coldkey_a = U256::from(42_u64); + let coldkey_b = U256::from(43_u64); + + let root_netuid = NetUid::from(0_u16); + let netuid_a = NetUid::from(27_u16); + let netuid_b = NetUid::from(1_u16); + + let mainnet_genesis = + hex_literal::hex!("2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03"); + const MIGRATION_NAME: &[u8] = b"migrate_fix_root_claimed_overclaim"; + + // CASE 1: new hotkey has no root stake → RootClaimable is cleared + new_test_ext(1).execute_with(|| { + frame_system::BlockHash::::insert(0u64, H256::from_slice(&mainnet_genesis)); + + RootClaimable::::mutate(new_hotkey, |map| { + map.insert(netuid_a, I96F32::from_num(500_000_u64)); + map.insert(netuid_b, I96F32::from_num(300_000_u64)); + }); + RootClaimed::::insert((netuid_a, new_hotkey, coldkey_a), 999u128); + RootClaimed::::insert((netuid_b, new_hotkey, coldkey_b), 111u128); + + // Unrelated hotkey's claimed entry must stay intact + RootClaimable::::mutate(untouched_hotkey, |map| { + map.insert(netuid_a, I96F32::from_num(42_u64)); + }); + RootClaimed::::insert((netuid_a, untouched_hotkey, coldkey_a), 555u128); + + assert!(!HasMigrationRun::::get(MIGRATION_NAME.to_vec())); + + let w = migrate_fix_root_claimed_overclaim::(); + assert!(!w.is_zero()); + assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); + + assert!( + RootClaimable::::get(new_hotkey).is_empty(), + "new hotkey RootClaimable must be cleared" + ); + assert_eq!( + RootClaimed::::get((netuid_a, new_hotkey, coldkey_a)), + 999u128, + "RootClaimed entries must be left intact" + ); + assert_eq!( + RootClaimed::::get((netuid_b, new_hotkey, coldkey_b)), + 111u128, + "RootClaimed entries must be left intact" + ); + + assert_eq!( + RootClaimable::::get(untouched_hotkey) + .get(&netuid_a) + .copied(), + Some(I96F32::from_num(42_u64)) + ); + assert_eq!( + RootClaimed::::get((netuid_a, untouched_hotkey, coldkey_a)), + 555u128 + ); + }); + + // CASE 2: new hotkey has root stake → state is preserved + new_test_ext(1).execute_with(|| { + frame_system::BlockHash::::insert(0u64, H256::from_slice(&mainnet_genesis)); + + RootClaimable::::mutate(new_hotkey, |map| { + map.insert(netuid_a, I96F32::from_num(500_000_u64)); + }); + RootClaimed::::insert((netuid_a, new_hotkey, coldkey_a), 999u128); + + TotalHotkeyAlpha::::insert(new_hotkey, root_netuid, AlphaBalance::from(1_000u64)); + + let w = migrate_fix_root_claimed_overclaim::(); + assert!(!w.is_zero()); + assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); + + assert_eq!( + RootClaimable::::get(new_hotkey) + .get(&netuid_a) + .copied(), + Some(I96F32::from_num(500_000_u64)), + "must not clear when new hotkey still holds root stake" + ); + assert_eq!( + RootClaimed::::get((netuid_a, new_hotkey, coldkey_a)), + 999u128 + ); + }); + + // CASE 3: idempotency — second run is a no-op + new_test_ext(1).execute_with(|| { + frame_system::BlockHash::::insert(0u64, H256::from_slice(&mainnet_genesis)); + HasMigrationRun::::insert(MIGRATION_NAME.to_vec(), true); + + RootClaimable::::mutate(new_hotkey, |map| { + map.insert(netuid_a, I96F32::from_num(777_u64)); + }); + + let w = migrate_fix_root_claimed_overclaim::(); + assert_eq!( + w, + ::DbWeight::get().reads(1), + "second run should only read the migration flag" + ); + assert_eq!( + RootClaimable::::get(new_hotkey) + .get(&netuid_a) + .copied(), + Some(I96F32::from_num(777_u64)) + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::migration::test_migrate_fix_root_claimed_incorrect_genesis --exact --nocapture +#[test] +fn test_migrate_fix_root_claimed_incorrect_genesis() { + use crate::migrations::migrate_fix_root_claimed_overclaim::*; + + let old_hotkey = decode_account_id32_test("5GmvyePN9aYErXBBhBnxZKGoGk4LKZApE4NkaSzW62CYCYNA"); + let new_hotkey = decode_account_id32_test("5H6BqkzjYvViiqp7rQLXjpnaEmW7U9CoKxXhQ4efMqtX1mQw"); + let coldkey = U256::from(42_u64); + + let netuid_target = NetUid::from(27_u16); + let netuid_other = NetUid::from(1_u16); + + let mainnet_genesis = + hex_literal::hex!("2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03"); + const MIGRATION_NAME: &[u8] = b"migrate_fix_root_claimed_overclaim"; + + // CASE 2: non-mainnet genesis — full no-op + new_test_ext(1).execute_with(|| { + frame_system::BlockHash::::insert(0u64, H256::from_low_u64_be(0xdeadbeef)); + + RootClaimable::::mutate(new_hotkey, |map| { + map.insert(netuid_target, I96F32::from_num(123_u64)); + }); + Alpha::::insert( + (new_hotkey, coldkey, netuid_target), + U64F64::from_num(1_000_u64), + ); + + let w = migrate_fix_root_claimed_overclaim::(); + assert!( + !w.is_zero(), + "weight must be non-zero (writes migration flag)" + ); + assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); + + assert!( + RootClaimable::::get(old_hotkey).is_empty(), + "migration must not touch storage on non-mainnet" + ); + assert!( + RootClaimable::::get(new_hotkey).contains_key(&netuid_target), + "new_hotkey data must remain untouched on non-mainnet" + ); + }); +} diff --git a/pallets/subtensor/src/tests/migration/fix_staking_and_root_tao.rs b/pallets/subtensor/src/tests/migration/fix_staking_and_root_tao.rs new file mode 100644 index 0000000000..11b7c7013f --- /dev/null +++ b/pallets/subtensor/src/tests/migration/fix_staking_and_root_tao.rs @@ -0,0 +1,255 @@ +#![allow( + unused, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used +)] +//! staking hotkeys, root TAO/alpha, symbols, registration/nominator settings. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_migrate_fix_staking_hot_keys() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &[u8] = b"migrate_fix_staking_hot_keys"; + + assert!( + !HasMigrationRun::::get(MIGRATION_NAME.to_vec()), + "Migration should not have run yet" + ); + + // Add some data + Alpha::::insert( + (U256::from(1), U256::from(2), NetUid::ROOT), + U64F64::from(1_u64), + ); + // Run migration + let weight = + migrations::migrate_fix_staking_hot_keys::migrate_fix_staking_hot_keys::(); + + assert!( + HasMigrationRun::::get(MIGRATION_NAME.to_vec()), + "Migration should be marked as completed" + ); + + // Check migration has been marked as run + assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); + + // Verify results + assert_eq!( + StakingHotkeys::::get(U256::from(2)), + vec![U256::from(1)] + ); + }); +} + +#[test] +fn test_migrate_fix_root_subnet_tao() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &str = "migrate_fix_root_subnet_tao"; + + let mut expected_total_stake = 0_u64; + // Seed some hotkeys with some fake stake. + for i in 0..100_000 { + Owner::::insert(U256::from(U256::from(i)), U256::from(i + 1_000_000)); + let stake = i + 1_000_000; + TotalHotkeyAlpha::::insert( + U256::from(U256::from(i)), + NetUid::ROOT, + AlphaBalance::from(stake), + ); + expected_total_stake += stake; + } + + assert_eq!(SubnetTAO::::get(NetUid::ROOT), TaoBalance::ZERO); + assert!( + !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should not have run yet" + ); + + // Run the migration + let weight = + crate::migrations::migrate_fix_root_subnet_tao::migrate_fix_root_subnet_tao::(); + + // Verify the migration ran correctly + assert!( + HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should be marked as run" + ); + assert!(!weight.is_zero(), "Migration weight should be non-zero"); + assert_eq!( + SubnetTAO::::get(NetUid::ROOT), + expected_total_stake.into() + ); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::migration::test_migrate_fix_root_tao_and_alpha_in --exact --show-output +#[test] +fn test_migrate_fix_root_tao_and_alpha_in() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &str = "migrate_fix_root_tao_and_alpha_in"; + + // Set counters initially + let initial_value = 1_000_000_000_000_u64; + SubnetTAO::::insert(NetUid::ROOT, TaoBalance::from(initial_value)); + SubnetAlphaIn::::insert(NetUid::ROOT, AlphaBalance::from(initial_value)); + SubnetAlphaOut::::insert(NetUid::ROOT, AlphaBalance::from(initial_value)); + SubnetVolume::::insert(NetUid::ROOT, initial_value as u128); + TotalStake::::set(TaoBalance::from(initial_value)); + + assert!( + !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should not have run yet" + ); + + // Run the migration + let weight = + crate::migrations::migrate_fix_root_tao_and_alpha_in::migrate_fix_root_tao_and_alpha_in::(); + + // Verify the migration ran correctly + assert!( + HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should be marked as run" + ); + assert!(!weight.is_zero(), "Migration weight should be non-zero"); + + // Verify counters have changed + assert!(SubnetTAO::::get(NetUid::ROOT) != initial_value.into()); + assert!(SubnetAlphaIn::::get(NetUid::ROOT) != initial_value.into()); + assert!(SubnetAlphaOut::::get(NetUid::ROOT) != initial_value.into()); + assert!(SubnetVolume::::get(NetUid::ROOT) != initial_value as u128); + assert!(TotalStake::::get() != initial_value.into()); + }); +} + +#[test] +fn test_migrate_subnet_symbols() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &str = "migrate_subnet_symbols"; + + // Create 100 subnets + for i in 0..100 { + add_network(i.into(), 1, 0); + } + + // Shift some symbols + TokenSymbol::::insert( + NetUid::from(21), + SubtensorModule::get_symbol_for_subnet(NetUid::from(142)), + ); + TokenSymbol::::insert( + NetUid::from(42), + SubtensorModule::get_symbol_for_subnet(NetUid::from(184)), + ); + TokenSymbol::::insert( + NetUid::from(83), + SubtensorModule::get_symbol_for_subnet(NetUid::from(242)), + ); + TokenSymbol::::insert( + NetUid::from(99), + SubtensorModule::get_symbol_for_subnet(NetUid::from(284)), + ); + + // Run the migration + let weight = crate::migrations::migrate_subnet_symbols::migrate_subnet_symbols::(); + + // Check that the symbols have been corrected + assert_eq!( + TokenSymbol::::get(NetUid::from(21)), + SubtensorModule::get_symbol_for_subnet(NetUid::from(21)) + ); + assert_eq!( + TokenSymbol::::get(NetUid::from(42)), + SubtensorModule::get_symbol_for_subnet(NetUid::from(42)) + ); + assert_eq!( + TokenSymbol::::get(NetUid::from(83)), + SubtensorModule::get_symbol_for_subnet(NetUid::from(83)) + ); + assert_eq!( + TokenSymbol::::get(NetUid::from(99)), + SubtensorModule::get_symbol_for_subnet(NetUid::from(99)) + ); + + assert!(!weight.is_zero(), "Migration weight should be non-zero"); + }); +} + +#[test] +fn test_migrate_set_registration_enable() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &str = "migrate_set_registration_enable"; + + // Create 3 subnets + let netuids: [NetUid; 3] = [1.into(), 2.into(), 3.into()]; + for netuid in netuids.iter() { + add_network(*netuid, 1, 0); + // Set registration to false to simulate the need for migration + SubtensorModule::set_network_registration_allowed(*netuid, false); + } + + // Sanity check: registration is disabled before migration + for netuid in netuids.iter() { + assert!(!SubtensorModule::get_network_registration_allowed(*netuid)); + } + + // Run the migration + let weight = + crate::migrations::migrate_set_registration_enable::migrate_set_registration_enable::< + Test, + >(); + + // After migration, regular registration should be enabled for all subnets except root + for netuid in netuids.iter() { + assert!(SubtensorModule::get_network_registration_allowed(*netuid)); + } + + // Migration should be marked as run + assert!(HasMigrationRun::::get( + MIGRATION_NAME.as_bytes().to_vec() + )); + + // Weight should be non-zero + assert!(!weight.is_zero(), "Migration weight should be non-zero"); + }); +} + +#[test] +fn test_migrate_set_nominator_min_stake() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &str = "migrate_set_nominator_min_stake"; + + let min_nomination_initial = 100_000_000; + let min_nomination_migrated = 10_000_000; + NominatorMinRequiredStake::::set(min_nomination_initial); + + assert_eq!( + NominatorMinRequiredStake::::get(), + min_nomination_initial + ); + assert!( + !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should not have run yet" + ); + + // Run the migration + let weight = + crate::migrations::migrate_set_nominator_min_stake::migrate_set_nominator_min_stake::< + Test, + >(); + + // Verify the migration ran correctly + assert!( + HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should be marked as run" + ); + assert!(!weight.is_zero(), "Migration weight should be non-zero"); + assert_eq!( + NominatorMinRequiredStake::::get(), + min_nomination_migrated + ); + }); +} diff --git a/pallets/subtensor/src/tests/migration/fix_subnet_hotkey_lock_swaps.rs b/pallets/subtensor/src/tests/migration/fix_subnet_hotkey_lock_swaps.rs new file mode 100644 index 0000000000..344a1245cc --- /dev/null +++ b/pallets/subtensor/src/tests/migration/fix_subnet_hotkey_lock_swaps.rs @@ -0,0 +1,212 @@ +#![allow( + unused, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used +)] +//! tao-in refund deployment block + subnet hotkey lock-swap repair. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_migrate_tao_in_refund_deployment_block() { + new_test_ext(1).execute_with(|| { + let deployment_block: u64 = 42; + let migration_name = b"migrate_tao_in_refund_deployment_block".to_vec(); + + TaoInRefundDeploymentBlock::::put(0); + HasMigrationRun::::remove(&migration_name); + + run_to_block(deployment_block); + crate::migrations::migrate_tao_in_refund_deployment_block::migrate_tao_in_refund_deployment_block::(); + + assert_eq!(TaoInRefundDeploymentBlock::::get(), deployment_block); + assert!(HasMigrationRun::::get(&migration_name)); + + run_to_block(deployment_block.saturating_add(1)); + crate::migrations::migrate_tao_in_refund_deployment_block::migrate_tao_in_refund_deployment_block::(); + + assert_eq!(TaoInRefundDeploymentBlock::::get(), deployment_block); + }); +} + +#[test] +fn test_migrate_fix_subnet_hotkey_lock_swaps_moves_or_discards_conflicts() { + new_test_ext(1).execute_with(|| { + let migration_name = b"migrate_fix_subnet_hotkey_lock_swaps".to_vec(); + let old_hotkey = + decode_account_id32::("5Ca8L8PkbqXUtzohKtSM3i1naGQxANGLx51kJsEPNB14Admz") + .expect("old hotkey should decode"); + let new_hotkey = + decode_account_id32::("5Evgh9QTXJLxYLusVy3tcY5S6Z3GgRSNDb9AzXUchX5dco3P") + .expect("new hotkey should decode"); + let netuid = NetUid::from(28); + let coldkey_to_move = U256::from(1); + let coldkey_with_conflict = U256::from(2); + let chained_coldkey = + decode_account_id32::("5EWUPMenvyvHdEGUHfUhSTeTDJDLzLkKZq74LFLRWtzcqZiS") + .expect("chained coldkey should decode"); + let chained_first_hotkey = + decode_account_id32::("5H3Kuy7L7DBSy7BS2c9EBayJYGkHV1pzWtnJm3iXvThT4VUJ") + .expect("chained first hotkey should decode"); + let chained_middle_hotkey = + decode_account_id32::("5CSiRF3sMKt1c3MT4KsRLBWENGkymVE7wA2zUDPsYy6JtpGE") + .expect("chained middle hotkey should decode"); + let chained_final_hotkey = + decode_account_id32::("5EsnHJK89FgF55EYwXtqhUwLu3c14xakyQ8PWoomcFwpxk5e") + .expect("chained final hotkey should decode"); + let chained_netuid = NetUid::from(97); + + HasMigrationRun::::remove(&migration_name); + + let moved_lock = LockState { + locked_mass: AlphaBalance::from(10_u64), + conviction: U64F64::from_num(3), + last_update: 11, + }; + let discarded_lock = LockState { + locked_mass: AlphaBalance::from(20_u64), + conviction: U64F64::from_num(5), + last_update: 12, + }; + let existing_destination_lock = LockState { + locked_mass: AlphaBalance::from(77_u64), + conviction: U64F64::from_num(7), + last_update: 10, + }; + let chained_lock = LockState { + locked_mass: AlphaBalance::from(33_u64), + conviction: U64F64::from_num(4), + last_update: 13, + }; + + Lock::::insert( + (coldkey_to_move, netuid, old_hotkey), + moved_lock.clone(), + ); + LockingColdkeys::::insert((netuid, old_hotkey, coldkey_to_move), ()); + Lock::::insert( + (coldkey_with_conflict, netuid, old_hotkey), + discarded_lock.clone(), + ); + LockingColdkeys::::insert((netuid, old_hotkey, coldkey_with_conflict), ()); + Lock::::insert( + (coldkey_with_conflict, netuid, new_hotkey), + existing_destination_lock.clone(), + ); + LockingColdkeys::::insert((netuid, new_hotkey, coldkey_with_conflict), ()); + DecayingLock::::insert(coldkey_to_move, netuid, false); + DecayingLock::::insert(coldkey_with_conflict, netuid, false); + DecayingLock::::insert(chained_coldkey, chained_netuid, false); + HotkeyLock::::insert( + netuid, + old_hotkey, + LockState { + locked_mass: AlphaBalance::from(30_u64), + conviction: U64F64::from_num(8), + last_update: 12, + }, + ); + HotkeyLock::::insert(netuid, new_hotkey, existing_destination_lock.clone()); + Lock::::insert( + (chained_coldkey, chained_netuid, chained_first_hotkey), + chained_lock.clone(), + ); + LockingColdkeys::::insert( + (chained_netuid, chained_first_hotkey, chained_coldkey), + (), + ); + HotkeyLock::::insert(chained_netuid, chained_first_hotkey, chained_lock.clone()); + + let weight = + crate::migrations::migrate_fix_subnet_hotkey_lock_swaps::migrate_fix_subnet_hotkey_lock_swaps::(); + + assert!(!weight.is_zero(), "migration weight should be non-zero"); + assert!(HasMigrationRun::::get(&migration_name)); + assert!(Lock::::get((coldkey_to_move, netuid, old_hotkey)).is_none()); + assert!(Lock::::get((coldkey_with_conflict, netuid, old_hotkey)).is_none()); + assert!(!LockingColdkeys::::contains_key(( + netuid, + old_hotkey, + coldkey_to_move + ))); + assert!(!LockingColdkeys::::contains_key(( + netuid, + old_hotkey, + coldkey_with_conflict + ))); + assert_eq!( + Lock::::get((coldkey_to_move, netuid, new_hotkey)), + Some(moved_lock.clone()) + ); + assert!(LockingColdkeys::::contains_key(( + netuid, + new_hotkey, + coldkey_to_move + ))); + assert_eq!( + Lock::::get((coldkey_with_conflict, netuid, new_hotkey)), + Some(existing_destination_lock.clone()) + ); + assert!(LockingColdkeys::::contains_key(( + netuid, + new_hotkey, + coldkey_with_conflict + ))); + assert!(HotkeyLock::::get(netuid, old_hotkey).is_none()); + + let new_aggregate = HotkeyLock::::get(netuid, new_hotkey) + .expect("new aggregate should exist"); + assert_eq!( + new_aggregate.locked_mass, + existing_destination_lock + .locked_mass + .saturating_add(moved_lock.locked_mass) + ); + assert_eq!( + new_aggregate.conviction, + existing_destination_lock + .conviction + .saturating_add(moved_lock.conviction) + ); + assert!(Lock::::get(( + chained_coldkey, + chained_netuid, + chained_first_hotkey + )) + .is_none()); + assert!(Lock::::get(( + chained_coldkey, + chained_netuid, + chained_middle_hotkey + )) + .is_none()); + assert!(!LockingColdkeys::::contains_key(( + chained_netuid, + chained_first_hotkey, + chained_coldkey + ))); + assert!(!LockingColdkeys::::contains_key(( + chained_netuid, + chained_middle_hotkey, + chained_coldkey + ))); + assert_eq!( + Lock::::get((chained_coldkey, chained_netuid, chained_final_hotkey)), + Some(chained_lock.clone()) + ); + assert!(LockingColdkeys::::contains_key(( + chained_netuid, + chained_final_hotkey, + chained_coldkey + ))); + assert!(HotkeyLock::::get(chained_netuid, chained_first_hotkey).is_none()); + assert!(HotkeyLock::::get(chained_netuid, chained_middle_hotkey).is_none()); + assert_eq!( + HotkeyLock::::get(chained_netuid, chained_final_hotkey), + Some(chained_lock) + ); + }); +} diff --git a/pallets/subtensor/src/tests/migration/helpers.rs b/pallets/subtensor/src/tests/migration/helpers.rs new file mode 100644 index 0000000000..0545fe76cc --- /dev/null +++ b/pallets/subtensor/src/tests/migration/helpers.rs @@ -0,0 +1,76 @@ +#![allow( + unused, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used +)] +//! Shared fixtures for migration unit tests. + +use super::prelude::*; + +#[allow(clippy::arithmetic_side_effects)] +pub(super) fn close(value: u64, target: u64, eps: u64) { + assert!( + (value as i64 - target as i64).abs() < eps as i64, + "Assertion failed: value = {value}, target = {target}, eps = {eps}" + ) +} + +#[allow(clippy::arithmetic_side_effects)] +pub(super) fn test_remove_storage_item Weight>( + migration_name: &'static str, + pallet_name: &'static str, + storage_name: &'static str, + migration: F, + test_entries_number: i32, +) { + new_test_ext(1).execute_with(|| { + let pallet_name = twox_128(pallet_name.as_bytes()); + let storage_name = twox_128(storage_name.as_bytes()); + let prefix = [pallet_name, storage_name].concat(); + + // Set up entries to be deleted. + for i in 0..test_entries_number { + let hotkey = U256::from(i as u64); + let coldkey = U256::from(i as u64); + let key = [prefix.clone(), hotkey.encode(), coldkey.encode()].concat(); + let value = (100 + i, 200 + i); + put_raw(&key, &value.encode()); + } + + assert!( + frame_support::storage::unhashed::contains_prefixed_key(&prefix), + "Entries should exist before migration." + ); + assert!( + !HasMigrationRun::::get(migration_name.as_bytes().to_vec()), + "Migration should not have run yet." + ); + + // Run migration + let weight = migration(); + + assert!( + !frame_support::storage::unhashed::contains_prefixed_key(&prefix), + "All entries should have been removed." + ); + assert!( + HasMigrationRun::::get(migration_name.as_bytes().to_vec()), + "Migration should be marked as run." + ); + assert!(!weight.is_zero(), "Migration weight should be non-zero."); + }); +} + +pub(super) fn decode_account_id32(ss58_string: &str) -> Option { + let account_id32: AccountId32 = AccountId32::from_ss58check(ss58_string).ok()?; + let mut account_id32_slice: &[u8] = account_id32.as_ref(); + T::AccountId::decode(&mut account_id32_slice).ok() +} + +pub(super) fn decode_account_id32_test(ss58_string: &str) -> U256 { + let account_id32: AccountId32 = AccountId32::from_ss58check(ss58_string).unwrap(); + let mut account_id32_slice: &[u8] = account_id32.as_ref(); + U256::decode(&mut account_id32_slice).unwrap() +} diff --git a/pallets/subtensor/src/tests/migration/mod.rs b/pallets/subtensor/src/tests/migration/mod.rs new file mode 100644 index 0000000000..ce841b99f3 --- /dev/null +++ b/pallets/subtensor/src/tests/migration/mod.rs @@ -0,0 +1,56 @@ +#![allow( + unused, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used +)] +//! Unit tests for storage / runtime migrations. +//! +//! Split from the former monolithic `tests/migration.rs` into concept modules +//! named after the migration families they cover. +//! +//! ## Search anchors +//! +//! | Module | Owns | +//! |--------|------| +//! | [`helpers`] | shared fixtures (`close`, `test_remove_storage_item`, SS58 decode helpers) | +//! | [`associated_evm_address_index`] | AssociatedEvmAddress index + orphan subnet identity cleanup | +//! | [`fix_subnet_hotkey_lock_swaps`] | tao-in refund deployment block + subnet hotkey lock-swap repair | +//! | [`transfer_and_delete_subnets`] | foundation ownership transfer + delete subnet 3/21 | +//! | [`commit_reveal`] | commit-reveal v2/v3, settings, disable, timelocked CR | +//! | [`subnet_volume_emission_flags`] | subnet volume, first emission block, subtoken, zero hotkey alpha | +//! | [`remove_unused_storage`] | orphan / deprecated storage item removals | +//! | [`rate_limit_keys`] | rate-limit key migrations and last-tx block maps | +//! | [`populate_locking_coldkeys`] | populate LockingColdkeys aggregate | +//! | [`fix_staking_and_root_tao`] | staking hotkeys, root TAO/alpha, symbols, registration/nominator settings | +//! | [`auto_stake_destination`] | auto-stake destination migration | +//! | [`network_modality_and_locks`] | network modality removal, subnet limit, lock cost/decay, kappa | +//! | [`reset_unactive_sn`] | reset inactive subnet state | +//! | [`swap_cleanup`] | swap v3 cleanup, coldkey-swap announcements, registration map clear, axon/cert purge | +//! | [`fix_bad_hk_swap_genesis`] | bad hotkey-swap repair — genesis-only cases | +//! | [`fix_bad_hk_swap_mainnet`] | bad hotkey-swap repair — mainnet cases | +//! | [`fix_root_claimed`] | root claimed overclaim repair | +//! | [`subnet_balances_and_issuance`] | subnet balances + total issuance EVM fees | +//! | [`conviction_and_tempo`] | tnet conviction locks + dynamic tempo | + +mod associated_evm_address_index; +mod auto_stake_destination; +mod commit_reveal; +mod conviction_and_tempo; +mod fix_bad_hk_swap_genesis; +mod fix_bad_hk_swap_mainnet; +mod fix_root_claimed; +mod fix_staking_and_root_tao; +mod fix_subnet_hotkey_lock_swaps; +mod helpers; +mod network_modality_and_locks; +mod populate_locking_coldkeys; +mod prelude; +mod rate_limit_keys; +mod remove_unused_storage; +mod reset_unactive_sn; +mod subnet_balances_and_issuance; +mod subnet_volume_emission_flags; +mod swap_cleanup; +mod transfer_and_delete_subnets; diff --git a/pallets/subtensor/src/tests/migration/network_modality_and_locks.rs b/pallets/subtensor/src/tests/migration/network_modality_and_locks.rs new file mode 100644 index 0000000000..187815bbd5 --- /dev/null +++ b/pallets/subtensor/src/tests/migration/network_modality_and_locks.rs @@ -0,0 +1,554 @@ +#![allow( + unused, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used +)] +//! network modality removal, subnet limit, lock cost/decay, kappa. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_migrate_remove_network_modality() { + new_test_ext(1).execute_with(|| { + // ------------------------------ + // 0. Constants / helpers + // ------------------------------ + const MIGRATION_NAME: &str = "migrate_remove_network_modality"; + + // Create multiple networks to test + let netuids: [NetUid; 3] = [1.into(), 2.into(), 3.into()]; + for netuid in netuids.iter() { + add_network(*netuid, 1, 0); + } + + // Set initial storage version to 7 (below target) + StorageVersion::new(7).put::>(); + assert_eq!( + Pallet::::on_chain_storage_version(), + StorageVersion::new(7) + ); + + // ------------------------------ + // 1. Simulate NetworkModality entries using deprecated storage alias + // ------------------------------ + // We need to manually create storage entries that would exist for NetworkModality + // Since NetworkModality was a StorageMap<_, Identity, NetUid, u16>, we simulate this + let pallet_prefix = twox_128("SubtensorModule".as_bytes()); + let storage_prefix = twox_128("NetworkModality".as_bytes()); + + // Create NetworkModality entries for each network + for (i, netuid) in netuids.iter().enumerate() { + let mut key = Vec::new(); + key.extend_from_slice(&pallet_prefix); + key.extend_from_slice(&storage_prefix); + // Identity encoding for netuid + key.extend_from_slice(&netuid.encode()); + + let modality_value: u16 = (i as u16) + 1; // Different values for testing + put_raw(&key, &modality_value.encode()); + + // Verify the entry was created + let stored_value = get_raw(&key).expect("NetworkModality entry should exist"); + assert_eq!( + u16::decode(&mut &stored_value[..]).expect("Failed to decode modality"), + modality_value + ); + } + + assert!( + !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should not have run yet" + ); + + // ------------------------------ + // 2. Run migration + // ------------------------------ + let weight = + crate::migrations::migrate_remove_network_modality::migrate_remove_network_modality::< + Test, + >(); + + // ------------------------------ + // 3. Verify migration effects + // ------------------------------ + assert!( + HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should be marked as run" + ); + + // Verify weight is non-zero + assert!(!weight.is_zero(), "Migration weight should be non-zero"); + + // Verify weight calculation: 1 read (version check) + 1 read (total networks) + N writes (removal) + 1 write (version update) + let expected_weight = ::DbWeight::get().reads(2) + + ::DbWeight::get().writes(netuids.len() as u64 + 1); + assert_eq!( + weight, expected_weight, + "Weight calculation should be correct" + ); + }); +} + +#[test] +fn test_migrate_remove_network_modality_already_run() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &str = "migrate_remove_network_modality"; + + // Mark migration as already run + HasMigrationRun::::insert(MIGRATION_NAME.as_bytes().to_vec(), true); + + // Set storage version to 8 (target version) + StorageVersion::new(8).put::>(); + assert_eq!( + Pallet::::on_chain_storage_version(), + StorageVersion::new(8) + ); + + // Run migration + let weight = + crate::migrations::migrate_remove_network_modality::migrate_remove_network_modality::< + Test, + >(); + + // Should only have read weight for checking migration status + let expected_weight = ::DbWeight::get().reads(1); + assert_eq!( + weight, expected_weight, + "Second run should only read the migration flag" + ); + + // Verify migration is still marked as run + assert!(HasMigrationRun::::get( + MIGRATION_NAME.as_bytes().to_vec() + )); + }); +} + +#[test] +fn test_migrate_subnet_limit_to_default() { + new_test_ext(1).execute_with(|| { + // ------------------------------ + // 0. Constants / helpers + // ------------------------------ + const MIG_NAME: &[u8] = b"subnet_limit_to_default"; + + // Compute a non-default value safely + let default: u16 = DefaultSubnetLimit::::get(); + let not_default: u16 = default.wrapping_add(1); + + // ------------------------------ + // 1. Pre-state: ensure a non-default value is stored + // ------------------------------ + SubnetLimit::::put(not_default); + assert_eq!( + SubnetLimit::::get(), + not_default, + "precondition failed: SubnetLimit should be non-default before migration" + ); + + assert!( + !HasMigrationRun::::get(MIG_NAME.to_vec()), + "migration flag should be false before run" + ); + + // ------------------------------ + // 2. Run migration + // ------------------------------ + let w = crate::migrations::migrate_subnet_limit_to_default::migrate_subnet_limit_to_default::(); + assert!(!w.is_zero(), "weight must be non-zero"); + + // ------------------------------ + // 3. Verify results + // ------------------------------ + assert!( + HasMigrationRun::::get(MIG_NAME.to_vec()), + "migration flag not set" + ); + + assert_eq!( + SubnetLimit::::get(), + default, + "SubnetLimit should be reset to the configured default" + ); + }); +} + +#[test] +fn test_migrate_network_lock_reduction_interval_and_decay() { + new_test_ext(0).execute_with(|| { + const FOUR_DAYS: u64 = 28_800; + const EIGHT_DAYS: u64 = 57_600; + const ONE_WEEK_BLOCKS: u64 = 50_400; + + // ── pre ────────────────────────────────────────────────────────────── + assert!( + !HasMigrationRun::::get(b"migrate_network_lock_reduction_interval".to_vec()), + "HasMigrationRun should be false before migration" + ); + + // ensure current_block > 0 + step_block(1); + let current_block_before = Pallet::::get_current_block_as_u64(); + + // ── run migration ──────────────────────────────────────────────────── + let weight = crate::migrations::migrate_network_lock_reduction_interval::migrate_network_lock_reduction_interval::(); + assert!(!weight.is_zero(), "migration weight should be > 0"); + + // ── params & flags ─────────────────────────────────────────────────── + assert_eq!(NetworkLockReductionInterval::::get(), EIGHT_DAYS); + assert_eq!(NetworkRateLimit::::get(), FOUR_DAYS); + assert_eq!( + Pallet::::get_network_last_lock(), + 1_000_000_000_000u64.into(), // 1000 TAO in rao + "last_lock should be 1_000_000_000_000 rao" + ); + + // last_lock_block should be set one week in the future + let last_lock_block = Pallet::::get_network_last_lock_block(); + let expected_block = current_block_before + ONE_WEEK_BLOCKS; + assert_eq!( + last_lock_block, + expected_block, + "last_lock_block should be current + ONE_WEEK_BLOCKS" + ); + + // registration start block should match the same future block + assert_eq!( + NetworkRegistrationStartBlock::::get(), + expected_block, + "NetworkRegistrationStartBlock should equal last_lock_block" + ); + + // lock cost should be 2000 TAO immediately after migration + let lock_cost_now = Pallet::::get_network_lock_cost(); + assert_eq!( + lock_cost_now, + 2_000_000_000_000u64.into(), + "lock cost should be 2000 TAO right after migration" + ); + + assert!( + HasMigrationRun::::get(b"migrate_network_lock_reduction_interval".to_vec()), + "HasMigrationRun should be true after migration" + ); + }); +} + +#[test] +fn test_migrate_restore_subnet_locked_65_128() { + use sp_runtime::traits::SaturatedConversion; + new_test_ext(0).execute_with(|| { + let name = b"migrate_restore_subnet_locked".to_vec(); + assert!( + !HasMigrationRun::::get(name.clone()), + "HasMigrationRun should be false before migration" + ); + + // Expected snapshot for netuids 65..128. + const EXPECTED: &[(u16, u64)] = &[ + (65, 37_274_536_408), + (66, 65_230_444_016), + (67, 114_153_284_032), + (68, 199_768_252_064), + (69, 349_594_445_728), + (70, 349_412_366_216), + (71, 213_408_488_702), + (72, 191_341_473_067), + (73, 246_711_333_592), + (74, 291_874_466_228), + (75, 247_485_227_056), + (76, 291_241_991_316), + (77, 303_154_601_714), + (78, 287_407_417_932), + (79, 254_935_051_664), + (80, 255_413_055_349), + (81, 249_790_431_509), + (82, 261_343_249_180), + (83, 261_361_408_796), + (84, 201_938_003_214), + (85, 264_805_234_604), + (86, 223_171_973_880), + (87, 180_397_358_280), + (88, 270_596_039_760), + (89, 286_399_608_951), + (90, 267_684_201_301), + (91, 284_637_542_762), + (92, 288_373_410_868), + (93, 290_836_604_849), + (94, 270_861_792_144), + (95, 210_595_055_304), + (96, 315_263_727_200), + (97, 158_244_884_792), + (98, 168_102_223_900), + (99, 252_153_339_800), + (100, 378_230_014_000), + (101, 205_977_765_866), + (102, 149_434_017_849), + (103, 135_476_471_008), + (104, 147_970_415_680), + (105, 122_003_668_139), + (106, 133_585_556_570), + (107, 200_137_144_216), + (108, 106_767_623_816), + (109, 124_280_483_748), + (110, 186_420_726_696), + (111, 249_855_564_892), + (112, 196_761_272_984), + (113, 147_120_048_727), + (114, 84_021_895_534), + (115, 98_002_215_656), + (116, 89_944_262_256), + (117, 107_183_582_952), + (118, 110_644_724_664), + (119, 99_380_483_902), + (120, 138_829_019_156), + (121, 111_988_743_976), + (122, 130_264_686_152), + (123, 118_034_291_488), + (124, 79_312_501_676), + (125, 43_214_310_704), + (126, 64_755_449_962), + (127, 97_101_698_382), + (128, 145_645_807_991), + ]; + + // Run migration + let weight = + crate::migrations::migrate_subnet_locked::migrate_restore_subnet_locked::(); + assert!(!weight.is_zero(), "migration weight should be > 0"); + + // Read back storage as (u16 -> u64) + let actual: BTreeMap = SubnetLocked::::iter() + .map(|(k, v)| (k.saturated_into::(), u64::from(v))) + .collect(); + + let expected: BTreeMap = EXPECTED.iter().copied().collect(); + + // 1) exact content + assert_eq!( + actual, expected, + "SubnetLocked map mismatch for 65..128 snapshot" + ); + + // 2) count and total + let expected_len = expected.len(); + let expected_sum: u128 = expected.values().map(|v| *v as u128).sum(); + + let count_after = actual.len(); + let sum_after: u128 = actual.values().map(|v| *v as u128).sum(); + + assert_eq!(count_after, expected_len, "entry count mismatch"); + assert_eq!(sum_after, expected_sum, "total RAO sum mismatch"); + + // 3) migration flag set + assert!( + HasMigrationRun::::get(name.clone()), + "HasMigrationRun should be true after migration" + ); + + // 4) idempotence + let before = actual.clone(); + let _again = + crate::migrations::migrate_subnet_locked::migrate_restore_subnet_locked::(); + let after: BTreeMap = SubnetLocked::::iter() + .map(|(k, v)| (k.saturated_into::(), u64::from(v))) + .collect(); + assert_eq!( + before, after, + "re-running the migration should not change storage" + ); + }); +} + +#[test] +fn test_migrate_network_lock_cost_2500_sets_price_and_decay() { + new_test_ext(0).execute_with(|| { + // ── constants ─────────────────────────────────────────────────────── + const RAO_PER_TAO: u64 = 1_000_000_000; + const TARGET_COST_TAO: u64 = 2_500; + const TARGET_COST_RAO: u64 = TARGET_COST_TAO * RAO_PER_TAO; + const NEW_LAST_LOCK_RAO: u64 = (TARGET_COST_TAO / 2) * RAO_PER_TAO; + + let migration_key = b"migrate_network_lock_cost_2500".to_vec(); + + // ── pre ────────────────────────────────────────────────────────────── + assert!( + !HasMigrationRun::::get(migration_key.clone()), + "HasMigrationRun should be false before migration" + ); + + // Ensure current_block > 0 so mult == 2 in get_network_lock_cost() + step_block(1); + let current_block_before = Pallet::::get_current_block_as_u64(); + + // Snapshot interval to ensure migration doesn't change it + let interval_before = NetworkLockReductionInterval::::get(); + + // ── run migration ──────────────────────────────────────────────────── + let weight = crate::migrations::migrate_network_lock_cost_2500::migrate_network_lock_cost_2500::(); + assert!(!weight.is_zero(), "migration weight should be > 0"); + + // ── asserts: params & flags ───────────────────────────────────────── + assert_eq!( + Pallet::::get_network_last_lock(), + NEW_LAST_LOCK_RAO.into(), + "last_lock should be set to 1,250 TAO (in rao)" + ); + assert_eq!( + Pallet::::get_network_last_lock_block(), + current_block_before, + "last_lock_block should be set to the current block" + ); + + // Lock cost should be exactly 2,500 TAO immediately after migration + let lock_cost_now = Pallet::::get_network_lock_cost(); + assert_eq!( + lock_cost_now, + TARGET_COST_RAO.into(), + "lock cost should be 2,500 TAO right after migration" + ); + + // Interval should be unchanged by this migration + assert_eq!( + NetworkLockReductionInterval::::get(), + interval_before, + "lock reduction interval should not be modified by this migration" + ); + + assert!( + HasMigrationRun::::get(migration_key.clone()), + "HasMigrationRun should be true after migration" + ); + + // ── decay check (1 block later) ───────────────────────────────────── + // Expected: cost = max(min_lock, 2*L - floor(L / eff_interval) * delta_blocks) + let eff_interval = Pallet::::get_lock_reduction_interval(); + let per_block_decrement: u64 = if eff_interval == 0 { + 0 + } else { + NEW_LAST_LOCK_RAO / eff_interval + }; + + let min_lock_rao: u64 = Pallet::::get_network_min_lock().to_u64(); + + step_block(1); + let expected_after_1: u64 = + core::cmp::max(min_lock_rao, TARGET_COST_RAO - per_block_decrement); + let lock_cost_after_1 = Pallet::::get_network_lock_cost(); + assert_eq!( + lock_cost_after_1, + expected_after_1.into(), + "lock cost should decay by one per-block step after 1 block" + ); + + // ── idempotency: running the migration again should do nothing ────── + let last_lock_before_rerun = Pallet::::get_network_last_lock(); + let last_lock_block_before_rerun = Pallet::::get_network_last_lock_block(); + let cost_before_rerun = Pallet::::get_network_lock_cost(); + + let _weight2 = crate::migrations::migrate_network_lock_cost_2500::migrate_network_lock_cost_2500::(); + + assert!( + HasMigrationRun::::get(migration_key.clone()), + "HasMigrationRun remains true on second run" + ); + assert_eq!( + Pallet::::get_network_last_lock(), + last_lock_before_rerun, + "second run should not modify last_lock" + ); + assert_eq!( + Pallet::::get_network_last_lock_block(), + last_lock_block_before_rerun, + "second run should not modify last_lock_block" + ); + assert_eq!( + Pallet::::get_network_lock_cost(), + cost_before_rerun, + "second run should not change current lock cost" + ); + }); +} + +#[test] +fn test_migrate_kappa_map_to_default() { + new_test_ext(1).execute_with(|| { + // ------------------------------ + // 0. Constants / helpers + // ------------------------------ + const MIG_NAME: &[u8] = b"kappa_map_to_default"; + let default: u16 = DefaultKappa::::get(); + + let not_default: u16 = if default == u16::MAX { + default - 1 + } else { + default + 1 + }; + + // ------------------------------ + // 1. Pre-state: seed using the correct key type (NetUid) + // ------------------------------ + let n0: NetUid = 0u16.into(); + let n1: NetUid = 1u16.into(); + let n2: NetUid = 42u16.into(); + + Kappa::::insert(n0, not_default); + Kappa::::insert(n1, default); + Kappa::::insert(n2, not_default); + + assert_eq!( + Kappa::::get(n0), + not_default, + "precondition failed: Kappa[n0] should be non-default before migration" + ); + assert_eq!( + Kappa::::get(n1), + default, + "precondition failed: Kappa[n1] should be default before migration" + ); + assert_eq!( + Kappa::::get(n2), + not_default, + "precondition failed: Kappa[n2] should be non-default before migration" + ); + + assert!( + !HasMigrationRun::::get(MIG_NAME.to_vec()), + "migration flag should be false before run" + ); + + // ------------------------------ + // 2. Run migration + // ------------------------------ + let w = + crate::migrations::migrate_kappa_map_to_default::migrate_kappa_map_to_default::(); + assert!(!w.is_zero(), "weight must be non-zero"); + + // ------------------------------ + // 3. Verify results + // ------------------------------ + assert!( + HasMigrationRun::::get(MIG_NAME.to_vec()), + "migration flag not set" + ); + + assert_eq!( + Kappa::::get(n0), + default, + "Kappa[n0] should be reset to the configured default" + ); + assert_eq!( + Kappa::::get(n1), + default, + "Kappa[n1] should remain at the configured default" + ); + assert_eq!( + Kappa::::get(n2), + default, + "Kappa[n2] should be reset to the configured default" + ); + }); +} diff --git a/pallets/subtensor/src/tests/migration/populate_locking_coldkeys.rs b/pallets/subtensor/src/tests/migration/populate_locking_coldkeys.rs new file mode 100644 index 0000000000..9ad8f1846c --- /dev/null +++ b/pallets/subtensor/src/tests/migration/populate_locking_coldkeys.rs @@ -0,0 +1,135 @@ +#![allow( + unused, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used +)] +//! populate LockingColdkeys aggregate. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_migrate_populate_locking_coldkeys() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &[u8] = b"migrate_populate_locking_coldkeys"; + + let netuid = NetUid::from(1); + let coldkey_1 = U256::from(1001); + let coldkey_2 = U256::from(1002); + let hotkey = U256::from(2001); + let expired_hotkey = U256::from(2002); + + Lock::::insert( + (coldkey_1, netuid, hotkey), + LockState { + locked_mass: AlphaBalance::from(1_000_u64), + conviction: U64F64::from_num(0), + last_update: 1, + }, + ); + Lock::::insert( + (coldkey_2, netuid, hotkey), + LockState { + locked_mass: AlphaBalance::from(2_000_u64), + conviction: U64F64::from_num(0), + last_update: 1, + }, + ); + Lock::::insert( + (coldkey_1, netuid, expired_hotkey), + LockState { + locked_mass: AlphaBalance::ZERO, + conviction: U64F64::from_num(1), + last_update: 1, + }, + ); + + assert_eq!( + LockingColdkeys::::iter_prefix((netuid, hotkey)).count(), + 0 + ); + assert_eq!( + LockingColdkeys::::iter_prefix((netuid, expired_hotkey)).count(), + 0 + ); + assert!(!HasMigrationRun::::get(MIGRATION_NAME.to_vec())); + + let weight = + crate::migrations::migrate_populate_locking_coldkeys::migrate_populate_locking_coldkeys::(); + + assert!(!weight.is_zero(), "migration weight should be non-zero"); + assert!(LockingColdkeys::::contains_key(( + netuid, hotkey, coldkey_1 + ))); + assert!(LockingColdkeys::::contains_key(( + netuid, hotkey, coldkey_2 + ))); + assert_eq!( + LockingColdkeys::::iter_prefix((netuid, hotkey)).count(), + 2 + ); + assert_eq!( + LockingColdkeys::::iter_prefix((netuid, expired_hotkey)).count(), + 0 + ); + assert!(Lock::::get((coldkey_1, netuid, expired_hotkey)).is_none()); + assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); + + let _ = LockingColdkeys::::clear_prefix((netuid, hotkey), u32::MAX, None); + let second_weight = + crate::migrations::migrate_populate_locking_coldkeys::migrate_populate_locking_coldkeys::(); + + assert_eq!( + second_weight, + ::DbWeight::get().reads(1), + "second run should only read the migration flag" + ); + assert_eq!( + LockingColdkeys::::iter_prefix((netuid, hotkey)).count(), + 0 + ); + }); +} + +#[test] +fn test_migrate_populate_locking_coldkeys_removes_dust_from_aggregate() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let coldkey_1 = U256::from(1101); + let coldkey_2 = U256::from(1102); + let hotkey = U256::from(2101); + let dust_lock = LockState { + locked_mass: AlphaBalance::from(60_u64), + conviction: U64F64::from_num(0), + last_update: 1, + }; + + DecayingLock::::insert(coldkey_1, netuid, false); + DecayingLock::::insert(coldkey_2, netuid, false); + Lock::::insert((coldkey_1, netuid, hotkey), dust_lock.clone()); + Lock::::insert((coldkey_2, netuid, hotkey), dust_lock); + HotkeyLock::::insert( + netuid, + hotkey, + LockState { + locked_mass: AlphaBalance::from(120_u64), + conviction: U64F64::from_num(0), + last_update: 1, + }, + ); + + crate::migrations::migrate_populate_locking_coldkeys::migrate_populate_locking_coldkeys::< + Test, + >(); + + assert!(Lock::::get((coldkey_1, netuid, hotkey)).is_none()); + assert!(Lock::::get((coldkey_2, netuid, hotkey)).is_none()); + assert!(HotkeyLock::::get(netuid, hotkey).is_none()); + assert_eq!( + LockingColdkeys::::iter_prefix((netuid, hotkey)).count(), + 0 + ); + }); +} diff --git a/pallets/subtensor/src/tests/migration/prelude.rs b/pallets/subtensor/src/tests/migration/prelude.rs new file mode 100644 index 0000000000..f60814445a --- /dev/null +++ b/pallets/subtensor/src/tests/migration/prelude.rs @@ -0,0 +1,40 @@ +#![allow( + unused, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used +)] +//! Shared imports for migration unit tests. + +pub use super::super::mock::*; +pub use crate::staking::lock::LockState; +pub use crate::*; +pub use alloc::collections::BTreeMap; +pub use approx::{assert_abs_diff_eq, assert_relative_eq}; +pub use codec::{Decode, Encode}; +pub use frame_support::{ + StorageHasher, Twox64Concat, assert_ok, + storage::unhashed::{get, get_raw, put, put_raw}, + storage_alias, + traits::{Currency, StorageInstance, StoredMap, fungible::Inspect}, + weights::Weight, +}; +pub use safe_math::SafeDiv; + +pub use crate::migrations::migrate_coldkey_swap_scheduled_to_announcements::deprecated as coldkey_swap_deprecated; +pub use frame_support::traits::Bounded; +pub use frame_system::Config; +pub use pallet_drand::types::RoundNumber; +pub use pallet_scheduler::ScheduledOf; +pub use scale_info::prelude::collections::VecDeque; +pub use sp_core::{H160, H256, U256, crypto::Ss58Codec}; +pub use sp_io::hashing::twox_128; +pub use sp_runtime::{ + AccountId32, PerU16, + traits::{Hash, Zero}, +}; +pub use sp_std::marker::PhantomData; +pub use substrate_fixed::types::{I96F32, U64F64}; +pub use substrate_fixed::{traits::ToFixed, types::extra::U2}; +pub use subtensor_runtime_common::{AlphaBalance, NetUid, NetUidStorageIndex, TaoBalance}; diff --git a/pallets/subtensor/src/tests/migration/rate_limit_keys.rs b/pallets/subtensor/src/tests/migration/rate_limit_keys.rs new file mode 100644 index 0000000000..7746dc2836 --- /dev/null +++ b/pallets/subtensor/src/tests/migration/rate_limit_keys.rs @@ -0,0 +1,370 @@ +#![allow( + unused, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used +)] +//! rate-limit key migrations and last-tx block maps. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_migrate_network_last_registered() { + new_test_ext(1).execute_with(|| { + // ------------------------------ + // Step 1: Simulate Old Storage Entry + // ------------------------------ + const MIGRATION_NAME: &str = "migrate_network_last_registered"; + + let pallet_name = "SubtensorModule"; + let storage_name = "NetworkLastRegistered"; + let pallet_name_hash = twox_128(pallet_name.as_bytes()); + let storage_name_hash = twox_128(storage_name.as_bytes()); + let prefix = [pallet_name_hash, storage_name_hash].concat(); + + let mut full_key = prefix.clone(); + + let original_value: u64 = 123; + put_raw(&full_key, &original_value.encode()); + + let stored_before = get_raw(&full_key).expect("Expected RateLimit to exist"); + assert_eq!( + u64::decode(&mut &stored_before[..]).expect("Failed to decode RateLimit"), + original_value + ); + + assert!( + !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should not have run yet" + ); + + // ------------------------------ + // Step 2: Run the Migration + // ------------------------------ + let weight = crate::migrations::migrate_rate_limiting_last_blocks:: + migrate_obsolete_rate_limiting_last_blocks_storage::(); + + assert!( + HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should be marked as completed" + ); + + // ------------------------------ + // Step 3: Verify Migration Effects + // ------------------------------ + + assert_eq!( + SubtensorModule::get_network_last_lock_block(), + original_value + ); + assert_eq!( + get_raw(&full_key), + None, + "RateLimit storage should have been cleared" + ); + + assert!(!weight.is_zero(), "Migration weight should be non-zero"); + }); +} + +#[allow(deprecated)] +#[test] +fn test_migrate_last_block_tx() { + new_test_ext(1).execute_with(|| { + // ------------------------------ + // Step 1: Simulate Old Storage Entry + // ------------------------------ + const MIGRATION_NAME: &str = "migrate_last_tx_block"; + + let test_account: U256 = U256::from(1); + let original_value: u64 = 123; + + LastTxBlock::::insert(test_account, original_value); + + assert!( + !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should not have run yet" + ); + + // ------------------------------ + // Step 2: Run the Migration + // ------------------------------ + let weight = crate::migrations::migrate_rate_limiting_last_blocks:: + migrate_obsolete_rate_limiting_last_blocks_storage::(); + + assert!( + HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should be marked as completed" + ); + + // ------------------------------ + // Step 3: Verify Migration Effects + // ------------------------------ + + assert_eq!( + SubtensorModule::get_last_tx_block(&test_account), + original_value + ); + assert!( + !LastTxBlock::::contains_key(test_account), + "RateLimit storage should have been cleared" + ); + + assert!(!weight.is_zero(), "Migration weight should be non-zero"); + }); +} + +#[allow(deprecated)] +#[test] +fn test_migrate_last_tx_block_childkey_take() { + new_test_ext(1).execute_with(|| { + // ------------------------------ + // Step 1: Simulate Old Storage Entry + // ------------------------------ + const MIGRATION_NAME: &str = "migrate_last_tx_block_childkey_take"; + + let test_account: U256 = U256::from(1); + let original_value: u64 = 123; + + LastTxBlockChildKeyTake::::insert(test_account, original_value); + + assert!( + !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should not have run yet" + ); + + // ------------------------------ + // Step 2: Run the Migration + // ------------------------------ + let weight = crate::migrations::migrate_rate_limiting_last_blocks:: + migrate_obsolete_rate_limiting_last_blocks_storage::(); + + assert!( + HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should be marked as completed" + ); + + // ------------------------------ + // Step 3: Verify Migration Effects + // ------------------------------ + + assert_eq!( + SubtensorModule::get_last_tx_block_childkey_take(&test_account), + original_value + ); + assert!( + !LastTxBlockChildKeyTake::::contains_key(test_account), + "RateLimit storage should have been cleared" + ); + + assert!(!weight.is_zero(), "Migration weight should be non-zero"); + }); +} + +// PerU16 must SCALE-encode byte-identically to u16, so the take/epoch storages +// retyped from u16 to PerU16 require no storage migration. +#[test] +fn test_per_u16_encodes_identically_to_u16() { + assert_eq!(PerU16::from_parts(5).encode(), 5u16.encode()); + assert_eq!(PerU16::from_parts(u16::MAX).encode(), u16::MAX.encode()); + assert_eq!(PerU16::zero().encode(), 0u16.encode()); +} + +#[allow(deprecated)] +#[test] +fn test_migrate_last_tx_block_delegate_take() { + new_test_ext(1).execute_with(|| { + // ------------------------------ + // Step 1: Simulate Old Storage Entry + // ------------------------------ + const MIGRATION_NAME: &str = "migrate_last_tx_block_delegate_take"; + + let test_account: U256 = U256::from(1); + let original_value: u64 = 123; + + LastTxBlockDelegateTake::::insert(test_account, original_value); + + assert!( + !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should not have run yet" + ); + + // ------------------------------ + // Step 2: Run the Migration + // ------------------------------ + let weight = crate::migrations::migrate_rate_limiting_last_blocks:: + migrate_last_tx_block_delegate_take::(); + + assert!( + HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should be marked as completed" + ); + + // ------------------------------ + // Step 3: Verify Migration Effects + // ------------------------------ + + assert_eq!( + SubtensorModule::get_last_tx_block_delegate_take(&test_account), + original_value + ); + assert!( + !LastTxBlockDelegateTake::::contains_key(test_account), + "RateLimit storage should have been cleared" + ); + + assert!(!weight.is_zero(), "Migration weight should be non-zero"); + }); +} + +#[test] +fn test_migrate_rate_limit_keys() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &[u8] = b"migrate_rate_limit_keys"; + let prefix = { + let pallet_prefix = twox_128("SubtensorModule".as_bytes()); + let storage_prefix = twox_128("LastRateLimitedBlock".as_bytes()); + [pallet_prefix, storage_prefix].concat() + }; + + // Seed new-format entries that must survive the migration untouched. + let new_last_account = U256::from(10); + SubtensorModule::set_last_tx_block(&new_last_account, 555); + let new_child_account = U256::from(11); + SubtensorModule::set_last_tx_block_childkey(&new_child_account, 777); + let new_delegate_account = U256::from(12); + SubtensorModule::set_last_tx_block_delegate_take(&new_delegate_account, 888); + + // Legacy NetworkLastRegistered entry (index 1) + let mut legacy_network_key = prefix.clone(); + legacy_network_key.push(1u8); + sp_io::storage::set(&legacy_network_key, &111u64.encode()); + + // Legacy LastTxBlock entry (index 2) for an account that already has a new-format value. + let mut legacy_last_key = prefix.clone(); + legacy_last_key.push(2u8); + legacy_last_key.extend_from_slice(&new_last_account.encode()); + sp_io::storage::set(&legacy_last_key, &666u64.encode()); + + // Legacy LastTxBlockChildKeyTake entry (index 3) + let legacy_child_account = U256::from(3); + ChildKeys::::insert( + legacy_child_account, + NetUid::from(0), + vec![(0u64, U256::from(99))], + ); + let mut legacy_child_key = prefix.clone(); + legacy_child_key.push(3u8); + legacy_child_key.extend_from_slice(&legacy_child_account.encode()); + sp_io::storage::set(&legacy_child_key, &333u64.encode()); + + // Legacy LastTxBlockDelegateTake entry (index 4) + let legacy_delegate_account = U256::from(4); + Delegates::::insert(legacy_delegate_account, PerU16::from_parts(500)); + let mut legacy_delegate_key = prefix.clone(); + legacy_delegate_key.push(4u8); + legacy_delegate_key.extend_from_slice(&legacy_delegate_account.encode()); + sp_io::storage::set(&legacy_delegate_key, &444u64.encode()); + + let weight = crate::migrations::migrate_rate_limit_keys::migrate_rate_limit_keys::(); + assert!( + HasMigrationRun::::get(MIGRATION_NAME.to_vec()), + "Migration should be marked as executed" + ); + assert!(!weight.is_zero(), "Migration weight should be non-zero"); + + // Legacy entries were migrated and cleared. + assert_eq!( + SubtensorModule::get_network_last_lock_block(), + 111u64, + "Network last lock block should match migrated value" + ); + assert!( + sp_io::storage::get(&legacy_network_key).is_none(), + "Legacy network entry should be cleared" + ); + + assert_eq!( + SubtensorModule::get_last_tx_block(&new_last_account), + 666u64, + "LastTxBlock should reflect the merged legacy value" + ); + assert!( + sp_io::storage::get(&legacy_last_key).is_none(), + "Legacy LastTxBlock entry should be cleared" + ); + + assert_eq!( + SubtensorModule::get_last_tx_block_childkey_take(&legacy_child_account), + 333u64, + "Child key take block should be migrated" + ); + assert!( + sp_io::storage::get(&legacy_child_key).is_none(), + "Legacy child take entry should be cleared" + ); + + assert_eq!( + SubtensorModule::get_last_tx_block_delegate_take(&legacy_delegate_account), + 444u64, + "Delegate take block should be migrated" + ); + assert!( + sp_io::storage::get(&legacy_delegate_key).is_none(), + "Legacy delegate take entry should be cleared" + ); + + // New-format entries remain untouched. + assert_eq!( + SubtensorModule::get_last_tx_block_childkey_take(&new_child_account), + 777u64, + "Existing child take entry should be preserved" + ); + assert_eq!( + SubtensorModule::get_last_tx_block_delegate_take(&new_delegate_account), + 888u64, + "Existing delegate take entry should be preserved" + ); + }); +} + +#[test] +fn test_migrate_remove_add_stake_burn_rate_limit() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &[u8] = b"migrate_remove_add_stake_burn_rate_limit"; + let netuid = NetUid::from(1); + let other_netuid = NetUid::from(2); + let preserved_netuid = NetUid::from(3); + let add_stake_burn_key = RateLimitKey::AddStakeBurn(netuid); + let other_add_stake_burn_key = RateLimitKey::AddStakeBurn(other_netuid); + let preserved_key = RateLimitKey::SetSNOwnerHotkey(preserved_netuid); + + SubtensorModule::set_rate_limited_last_block(&add_stake_burn_key, 100); + SubtensorModule::set_rate_limited_last_block(&other_add_stake_burn_key, 200); + SubtensorModule::set_rate_limited_last_block(&preserved_key, 300); + + let weight = + crate::migrations::migrate_remove_add_stake_burn_rate_limit::migrate_remove_add_stake_burn_rate_limit::(); + + assert!( + HasMigrationRun::::get(MIGRATION_NAME.to_vec()), + "Migration should be marked as executed" + ); + assert!(!weight.is_zero(), "Migration weight should be non-zero"); + + assert_eq!( + SubtensorModule::get_rate_limited_last_block(&add_stake_burn_key), + 0 + ); + assert_eq!( + SubtensorModule::get_rate_limited_last_block(&other_add_stake_burn_key), + 0 + ); + assert_eq!( + SubtensorModule::get_rate_limited_last_block(&preserved_key), + 300 + ); + }); +} diff --git a/pallets/subtensor/src/tests/migration/remove_unused_storage.rs b/pallets/subtensor/src/tests/migration/remove_unused_storage.rs new file mode 100644 index 0000000000..5d1e1a6e0a --- /dev/null +++ b/pallets/subtensor/src/tests/migration/remove_unused_storage.rs @@ -0,0 +1,267 @@ +#![allow( + unused, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used +)] +//! orphan / deprecated storage item removals. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_migrate_remove_total_hotkey_coldkey_stakes_this_interval() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &str = "migrate_remove_total_hotkey_coldkey_stakes_this_interval"; + + let pallet_name = twox_128(b"SubtensorModule"); + let storage_name = twox_128(b"TotalHotkeyColdkeyStakesThisInterval"); + let prefix = [pallet_name, storage_name].concat(); + + // Set up 200 000 entries to be deleted. + for i in 0..200_000{ + let hotkey = U256::from(i as u64); + let coldkey = U256::from(i as u64); + let key = [prefix.clone(), hotkey.encode(), coldkey.encode()].concat(); + let value = (100 + i, 200 + i); + put_raw(&key, &value.encode()); + } + + assert!(frame_support::storage::unhashed::contains_prefixed_key(&prefix), "Entries should exist before migration."); + assert!( + !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should not have run yet." + ); + + // Run migration + let weight = crate::migrations::migrate_remove_total_hotkey_coldkey_stakes_this_interval::migrate_remove_total_hotkey_coldkey_stakes_this_interval::(); + + assert!(!frame_support::storage::unhashed::contains_prefixed_key(&prefix), "All entries should have been removed."); + assert!( + HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should be marked as run." + ); + assert!(!weight.is_zero(),"Migration weight should be non-zero."); + }); +} + +fn test_migrate_remove_last_hotkey_coldkey_emission_on_netuid() { + const MIGRATION_NAME: &str = "migrate_remove_last_hotkey_coldkey_emission_on_netuid"; + let pallet_name = "SubtensorModule"; + let storage_name = "LastHotkeyColdkeyEmissionOnNetuid"; + let migration = crate::migrations::migrate_orphaned_storage_items::remove_last_hotkey_coldkey_emission_on_netuid::; + + test_remove_storage_item( + MIGRATION_NAME, + pallet_name, + storage_name, + migration, + 200_000, + ); +} + +#[test] +fn test_migrate_remove_subnet_alpha_emission_sell() { + const MIGRATION_NAME: &str = "migrate_remove_subnet_alpha_emission_sell"; + let pallet_name = "SubtensorModule"; + let storage_name = "SubnetAlphaEmissionSell"; + let migration = + crate::migrations::migrate_orphaned_storage_items::remove_subnet_alpha_emission_sell::; + + test_remove_storage_item( + MIGRATION_NAME, + pallet_name, + storage_name, + migration, + 200_000, + ); +} + +#[test] +fn test_migrate_remove_neurons_to_prune_at_next_epoch() { + const MIGRATION_NAME: &str = "migrate_remove_neurons_to_prune_at_next_epoch"; + let pallet_name = "SubtensorModule"; + let storage_name = "NeuronsToPruneAtNextEpoch"; + let migration = + crate::migrations::migrate_orphaned_storage_items::remove_neurons_to_prune_at_next_epoch::< + Test, + >; + + test_remove_storage_item( + MIGRATION_NAME, + pallet_name, + storage_name, + migration, + 200_000, + ); +} + +#[test] +fn test_migrate_remove_total_stake_at_dynamic() { + const MIGRATION_NAME: &str = "migrate_remove_total_stake_at_dynamic"; + let pallet_name = "SubtensorModule"; + let storage_name = "TotalStakeAtDynamic"; + let migration = + crate::migrations::migrate_orphaned_storage_items::remove_total_stake_at_dynamic::; + + test_remove_storage_item( + MIGRATION_NAME, + pallet_name, + storage_name, + migration, + 200_000, + ); +} + +#[test] +fn test_migrate_remove_subnet_name() { + const MIGRATION_NAME: &str = "migrate_remove_subnet_name"; + let pallet_name = "SubtensorModule"; + let storage_name = "SubnetName"; + let migration = crate::migrations::migrate_orphaned_storage_items::remove_subnet_name::; + + test_remove_storage_item( + MIGRATION_NAME, + pallet_name, + storage_name, + migration, + 200_000, + ); +} + +#[test] +fn test_migrate_remove_network_min_allowed_uids() { + const MIGRATION_NAME: &str = "migrate_remove_network_min_allowed_uids"; + let pallet_name = "SubtensorModule"; + let storage_name = "NetworkMinAllowedUids"; + let migration = + crate::migrations::migrate_orphaned_storage_items::remove_network_min_allowed_uids::; + + test_remove_storage_item(MIGRATION_NAME, pallet_name, storage_name, migration, 1); +} + +#[test] +fn test_migrate_remove_dynamic_block() { + const MIGRATION_NAME: &str = "migrate_remove_dynamic_block"; + let pallet_name = "SubtensorModule"; + let storage_name = "DynamicBlock"; + let migration = crate::migrations::migrate_orphaned_storage_items::remove_dynamic_block::; + + test_remove_storage_item(MIGRATION_NAME, pallet_name, storage_name, migration, 1); +} + +#[test] +fn test_migrate_remove_commitments_rate_limit() { + new_test_ext(1).execute_with(|| { + // ------------------------------ + // Step 1: Simulate Old Storage Entry + // ------------------------------ + const MIGRATION_NAME: &str = "migrate_remove_commitments_rate_limit"; + + // Build the raw storage key: twox128("Commitments") ++ twox128("RateLimit") + let pallet_prefix = twox_128("Commitments".as_bytes()); + let storage_prefix = twox_128("RateLimit".as_bytes()); + + let mut key = Vec::new(); + key.extend_from_slice(&pallet_prefix); + key.extend_from_slice(&storage_prefix); + + let original_value: u64 = 123; + put_raw(&key, &original_value.encode()); + + let stored_before = get_raw(&key).expect("Expected RateLimit to exist"); + assert_eq!( + u64::decode(&mut &stored_before[..]).expect("Failed to decode RateLimit"), + original_value + ); + + assert!( + !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should not have run yet" + ); + + // ------------------------------ + // Step 2: Run the Migration + // ------------------------------ + let weight = crate::migrations::migrate_remove_commitments_rate_limit:: + migrate_remove_commitments_rate_limit::(); + + assert!( + HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should be marked as completed" + ); + + // ------------------------------ + // Step 3: Verify Migration Effects + // ------------------------------ + assert!( + get_raw(&key).is_none(), + "RateLimit storage should have been cleared" + ); + + assert!(!weight.is_zero(), "Migration weight should be non-zero"); + }); +} + +#[test] +fn test_migrate_remove_tao_dividends() { + const MIGRATION_NAME: &str = "migrate_remove_tao_dividends"; + let pallet_name = "SubtensorModule"; + let storage_name = "TaoDividendsPerSubnet"; + let migration = + crate::migrations::migrate_remove_tao_dividends::migrate_remove_tao_dividends::; + + test_remove_storage_item( + MIGRATION_NAME, + pallet_name, + storage_name, + migration, + 200_000, + ); + + let storage_name = "PendingAlphaSwapped"; + test_remove_storage_item( + MIGRATION_NAME, + pallet_name, + storage_name, + migration, + 200_000, + ); + + let storage_name = "PendingRootDivs"; + test_remove_storage_item( + MIGRATION_NAME, + pallet_name, + storage_name, + migration, + 200_000, + ); +} + +fn test_migrate_remove_old_identity_maps() { + let migration = + crate::migrations::migrate_remove_old_identity_maps::migrate_remove_old_identity_maps::; + + const MIGRATION_NAME: &str = "migrate_remove_old_identity_maps"; + + let pallet_name = "SubtensorModule"; + + test_remove_storage_item(MIGRATION_NAME, pallet_name, "Identities", migration, 100); + + test_remove_storage_item( + MIGRATION_NAME, + pallet_name, + "SubnetIdentities", + migration, + 100, + ); + + test_remove_storage_item( + MIGRATION_NAME, + pallet_name, + "SubnetIdentitiesV2", + migration, + 100, + ); +} diff --git a/pallets/subtensor/src/tests/migration/reset_unactive_sn.rs b/pallets/subtensor/src/tests/migration/reset_unactive_sn.rs new file mode 100644 index 0000000000..8e5b850d2a --- /dev/null +++ b/pallets/subtensor/src/tests/migration/reset_unactive_sn.rs @@ -0,0 +1,369 @@ +#![allow( + unused, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used +)] +//! reset inactive subnet state. + +use super::helpers::*; +use super::prelude::*; + +fn do_setup_unactive_sn() -> (Vec, Vec) { + // Register some subnets + let netuid0 = add_dynamic_network_without_emission_block(&U256::from(0), &U256::from(0)); + let netuid1 = add_dynamic_network_without_emission_block(&U256::from(1), &U256::from(1)); + let netuid2 = add_dynamic_network_without_emission_block(&U256::from(2), &U256::from(2)); + let inactive_netuids = vec![netuid0, netuid1, netuid2]; + // Add active subnets + let netuid3 = add_dynamic_network_without_emission_block(&U256::from(3), &U256::from(3)); + let netuid4 = add_dynamic_network_without_emission_block(&U256::from(4), &U256::from(4)); + let netuid5 = add_dynamic_network_without_emission_block(&U256::from(5), &U256::from(5)); + let active_netuids = vec![netuid3, netuid4, netuid5]; + let netuids: Vec = inactive_netuids + .iter() + .chain(active_netuids.iter()) + .copied() + .collect(); + + let initial_tao = Pallet::::get_network_min_lock(); + let initial_alpha: AlphaBalance = initial_tao.to_u64().into(); + + const EXTRA_POOL_TAO: u64 = 123_123_u64; + const EXTRA_POOL_ALPHA: u64 = 123_123_u64; + + // Add stake to the subnet pools + for netuid in &netuids { + let extra_for_pool = TaoBalance::from(EXTRA_POOL_TAO); + let stake_in_pool = TaoBalance::from( + u64::from(initial_tao) + .checked_add(EXTRA_POOL_TAO) + .expect("initial_tao + extra_for_pool overflow"), + ); + SubnetTAO::::insert(netuid, stake_in_pool); + TotalStake::::mutate(|total_stake| { + let updated_total = u64::from(*total_stake) + .checked_add(EXTRA_POOL_TAO) + .expect("total stake overflow"); + *total_stake = updated_total.into(); + }); + TotalIssuance::::mutate(|total_issuance| { + let updated_total = u64::from(*total_issuance) + .checked_add(EXTRA_POOL_TAO) + .expect("total issuance overflow"); + *total_issuance = updated_total.into(); + }); + + let subnet_alpha_in = AlphaBalance::from( + u64::from(initial_alpha) + .checked_add(EXTRA_POOL_ALPHA) + .expect("initial alpha + extra alpha overflow"), + ); + SubnetAlphaIn::::insert(netuid, subnet_alpha_in); + SubnetAlphaOut::::insert(netuid, AlphaBalance::from(EXTRA_POOL_ALPHA)); + SubnetVolume::::insert(netuid, 123123_u128); + + // Try registering on the subnet to simulate a real network + // give balance to the coldkey + let coldkey_account_id = U256::from(1111); + let hotkey_account_id = U256::from(1111); + let burn_cost = SubtensorModule::get_burn(*netuid); + // Registration requires keep-alive coverage above the burn (Preservation::Preserve). + let fund = TaoBalance::from( + u64::from(burn_cost) + .checked_add(u64::from(ExistentialDeposit::get())) + .and_then(|value| value.checked_add(10)) + .expect("burn funding overflow"), + ); + add_balance_to_coldkey_account(&coldkey_account_id, fund); + TotalIssuance::::mutate(|total_issuance| { + let updated_total = u64::from(*total_issuance) + .checked_add(u64::from(fund)) + .expect("total issuance overflow (burn)"); + *total_issuance = updated_total.into(); + }); + + // register the neuron + assert_ok!(SubtensorModule::burned_register( + <::RuntimeOrigin>::signed(coldkey_account_id), + *netuid, + hotkey_account_id + )); + } + + for netuid in &active_netuids { + // Set the FirstEmissionBlockNumber for the active subnet + FirstEmissionBlockNumber::::insert(netuid, 100); + // Also set SubtokenEnabled to true + SubtokenEnabled::::insert(netuid, true); + } + + let alpha_amt = AlphaBalance::from(123123_u64); + // Create some Stake entries + for netuid in &netuids { + for hotkey in 0..10 { + let hk = U256::from(hotkey); + TotalHotkeyAlpha::::insert(hk, netuid, alpha_amt); + TotalHotkeyShares::::insert(hk, netuid, U64F64::from(123123_u64)); + TotalHotkeyAlphaLastEpoch::::insert(hk, netuid, alpha_amt); + + RootClaimable::::mutate(hk, |claimable| { + claimable.insert(*netuid, I96F32::from(alpha_amt.to_u64())); + }); + for coldkey in 0..10 { + let ck = U256::from(coldkey); + Alpha::::insert((hk, ck, netuid), U64F64::from(123_u64)); + RootClaimed::::insert((netuid, hk, ck), 222_u128); + } + } + } + // Add some pending emissions + let alpha_em_amt = AlphaBalance::from(355555_u64); + for netuid in &netuids { + PendingServerEmission::::insert(netuid, alpha_em_amt); + PendingValidatorEmission::::insert(netuid, alpha_em_amt); + PendingRootAlphaDivs::::insert(netuid, alpha_em_amt); + PendingOwnerCut::::insert(netuid, alpha_em_amt); + + SubnetTaoInEmission::::insert(netuid, TaoBalance::from(12345678_u64)); + SubnetAlphaInEmission::::insert(netuid, AlphaBalance::from(12345678_u64)); + SubnetAlphaOutEmission::::insert(netuid, AlphaBalance::from(12345678_u64)); + } + + (active_netuids, inactive_netuids) +} + +#[test] +fn test_migrate_reset_unactive_sn_get_unactive_netuids() { + new_test_ext(1).execute_with(|| { + let (active_netuids, inactive_netuids) = do_setup_unactive_sn(); + + let initial_tao = Pallet::::get_network_min_lock(); + let initial_alpha: AlphaBalance = initial_tao.to_u64().into(); + + let (unactive_netuids, w) = + crate::migrations::migrate_reset_unactive_sn::get_unactive_sn_netuids::( + initial_alpha, + ); + // Make sure ALL the inactive subnets are in the unactive netuids + assert!( + inactive_netuids + .iter() + .all(|netuid| unactive_netuids.contains(netuid)) + ); + // Make sure the active subnets are not in the unactive netuids + assert!( + active_netuids + .iter() + .all(|netuid| !unactive_netuids.contains(netuid)) + ); + }); +} + +#[test] +fn test_migrate_reset_unactive_sn() { + new_test_ext(1).execute_with(|| { + use sp_std::collections::btree_map::BTreeMap; + + let (active_netuids, inactive_netuids) = do_setup_unactive_sn(); + + let initial_tao = Pallet::::get_network_min_lock(); + let initial_alpha: AlphaBalance = initial_tao.to_u64().into(); + + let mut locked_before: BTreeMap = BTreeMap::new(); + let mut rao_recycled_before: BTreeMap = BTreeMap::new(); + + for netuid in active_netuids.iter().chain(inactive_netuids.iter()) { + locked_before.insert(*netuid, SubnetLocked::::get(*netuid)); + rao_recycled_before.insert(*netuid, RAORecycledForRegistration::::get(netuid)); + } + + // Run the migration + let w = crate::migrations::migrate_reset_unactive_sn::migrate_reset_unactive_sn::(); + assert!(!w.is_zero(), "weight must be non-zero"); + + // Verify the results + for netuid in &inactive_netuids { + let netuid = *netuid; + + assert_eq!( + SubnetLocked::::get(netuid), + *locked_before.get(&netuid).unwrap(), + "SubnetLocked unexpectedly changed for inactive subnet {netuid:?}" + ); + assert_eq!( + RAORecycledForRegistration::::get(netuid), + *rao_recycled_before.get(&netuid).unwrap(), + "RAORecycledForRegistration unexpectedly changed for inactive subnet {netuid:?}" + ); + + assert_eq!( + PendingServerEmission::::get(netuid), + AlphaBalance::ZERO + ); + assert_eq!( + PendingValidatorEmission::::get(netuid), + AlphaBalance::ZERO + ); + assert_eq!( + PendingRootAlphaDivs::::get(netuid), + AlphaBalance::ZERO + ); + assert_eq!( + // not modified + RAORecycledForRegistration::::get(netuid), + *rao_recycled_before.get(&netuid).unwrap() + ); + assert_eq!(PendingOwnerCut::::get(netuid), AlphaBalance::ZERO); + assert_ne!(SubnetTAO::::get(netuid), initial_tao); + assert_ne!(SubnetAlphaIn::::get(netuid), initial_alpha); + assert_ne!(SubnetAlphaOut::::get(netuid), AlphaBalance::ZERO); + assert_eq!(SubnetTaoInEmission::::get(netuid), TaoBalance::ZERO); + assert_eq!( + SubnetAlphaInEmission::::get(netuid), + AlphaBalance::ZERO + ); + assert_eq!( + SubnetAlphaOutEmission::::get(netuid), + AlphaBalance::ZERO + ); + assert_ne!(SubnetVolume::::get(netuid), 0u128); + for hotkey in 0..10 { + let hk = U256::from(hotkey); + assert_ne!( + TotalHotkeyAlpha::::get(hk, netuid), + AlphaBalance::ZERO + ); + assert_ne!( + TotalHotkeyShares::::get(hk, netuid), + U64F64::from_num(0.0) + ); + assert_ne!( + TotalHotkeyAlphaLastEpoch::::get(hk, netuid), + AlphaBalance::ZERO + ); + assert_ne!(RootClaimable::::get(hk).get(&netuid), None); + for coldkey in 0..10 { + let ck = U256::from(coldkey); + assert_ne!(Alpha::::get((hk, ck, netuid)), U64F64::from_num(0.0)); + assert_ne!(RootClaimed::::get((netuid, hk, ck)), 0u128); + } + } + + // Don't touch SubnetLocked + assert_ne!(SubnetLocked::::get(netuid), TaoBalance::ZERO); + } + + // !!! Make sure the active subnets were not reset + for netuid in &active_netuids { + let netuid = *netuid; + + assert_eq!( + SubnetLocked::::get(netuid), + *locked_before.get(&netuid).unwrap(), + "SubnetLocked unexpectedly changed for active subnet {netuid:?}" + ); + assert_eq!( + RAORecycledForRegistration::::get(netuid), + *rao_recycled_before.get(&netuid).unwrap(), + "RAORecycledForRegistration unexpectedly changed for active subnet {netuid:?}" + ); + + assert_ne!( + PendingServerEmission::::get(netuid), + AlphaBalance::ZERO + ); + assert_ne!( + PendingValidatorEmission::::get(netuid), + AlphaBalance::ZERO + ); + assert_ne!( + PendingRootAlphaDivs::::get(netuid), + AlphaBalance::ZERO + ); + assert_eq!( + // unchanged (already asserted above via snapshot) + RAORecycledForRegistration::::get(netuid), + *rao_recycled_before.get(&netuid).unwrap() + ); + assert_ne!(SubnetTaoInEmission::::get(netuid), TaoBalance::ZERO); + assert_ne!( + SubnetAlphaInEmission::::get(netuid), + AlphaBalance::ZERO + ); + assert_ne!( + SubnetAlphaOutEmission::::get(netuid), + AlphaBalance::ZERO + ); + assert_ne!(PendingOwnerCut::::get(netuid), AlphaBalance::ZERO); + assert_ne!(SubnetTAO::::get(netuid), initial_tao); + assert_ne!(SubnetAlphaIn::::get(netuid), initial_alpha); + assert_ne!(SubnetAlphaOut::::get(netuid), AlphaBalance::ZERO); + assert_ne!(SubnetVolume::::get(netuid), 0u128); + for hotkey in 0..10 { + let hk = U256::from(hotkey); + assert_ne!( + TotalHotkeyAlpha::::get(hk, netuid), + AlphaBalance::ZERO + ); + assert_ne!( + TotalHotkeyShares::::get(hk, netuid), + U64F64::from_num(0.0) + ); + assert_ne!( + TotalHotkeyAlphaLastEpoch::::get(hk, netuid), + AlphaBalance::ZERO + ); + assert!(RootClaimable::::get(hk).contains_key(&netuid)); + for coldkey in 0..10 { + let ck = U256::from(coldkey); + assert_ne!(Alpha::::get((hk, ck, netuid)), U64F64::from_num(0.0)); + assert_ne!(RootClaimed::::get((netuid, hk, ck)), 0u128); + } + } + // Don't touch SubnetLocked + assert_ne!(SubnetLocked::::get(netuid), TaoBalance::ZERO); + } + }); +} + +#[test] +fn test_migrate_reset_unactive_sn_idempotence() { + new_test_ext(1).execute_with(|| { + let (active_netuids, inactive_netuids) = do_setup_unactive_sn(); + let netuids = inactive_netuids + .iter() + .chain(active_netuids.iter()) + .copied() + .collect::>(); + + // Run total issuance migration *before* running the migration. + crate::migrations::migrate_init_total_issuance::migrate_init_total_issuance::(); + + // Run the migration + let w = crate::migrations::migrate_reset_unactive_sn::migrate_reset_unactive_sn::(); + assert!(!w.is_zero(), "weight must be non-zero"); + + // Store the values after running the migration + let mut subnet_tao_before = BTreeMap::new(); + for netuid in &netuids { + subnet_tao_before.insert(netuid, SubnetTAO::::get(netuid)); + } + let total_stake_before = TotalStake::::get(); + let total_issuance_before = TotalIssuance::::get(); + + // Run total issuance migration again, to make sure no changes happen from it. + crate::migrations::migrate_init_total_issuance::migrate_init_total_issuance::(); + + // Verify that none of the values are different + for netuid in &netuids { + assert_eq!( + SubnetTAO::::get(netuid), + *subnet_tao_before.get(netuid).unwrap_or(&TaoBalance::ZERO) + ); + } + assert_eq!(TotalStake::::get(), total_stake_before); + assert_eq!(TotalIssuance::::get(), total_issuance_before); + }); +} diff --git a/pallets/subtensor/src/tests/migration/subnet_balances_and_issuance.rs b/pallets/subtensor/src/tests/migration/subnet_balances_and_issuance.rs new file mode 100644 index 0000000000..2f981af87a --- /dev/null +++ b/pallets/subtensor/src/tests/migration/subnet_balances_and_issuance.rs @@ -0,0 +1,101 @@ +#![allow( + unused, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used +)] +//! subnet balances + total issuance EVM fees. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_migrate_subnet_balances() { + new_test_ext(1).execute_with(|| { + let netuid1 = NetUid::from(1); + let netuid2 = NetUid::from(2); + add_network(netuid1, 1, 0); + add_network(netuid2, 1, 0); + + // Add network locks + let lock1 = TaoBalance::from(123_000_000_000_u64); + let lock2 = TaoBalance::from(321_000_000_000_u64); + SubnetLocked::::insert(netuid1, lock1); + SubnetLocked::::insert(netuid2, lock2); + + // Add SubnetTAO + let reserve1 = TaoBalance::from(456_000_000_000_u64); + let reserve2 = TaoBalance::from(654_000_000_000_u64); + SubnetTAO::::insert(netuid1, reserve1); + SubnetTAO::::insert(netuid2, reserve2); + + // Run migration + crate::migrations::migrate_subnet_balances::migrate_subnet_balances::(); + + // Test that subnet balances got updated + let subnet_account_1 = SubtensorModule::get_subnet_account_id(netuid1).unwrap(); + let subnet_account_2 = SubtensorModule::get_subnet_account_id(netuid2).unwrap(); + let balance1 = SubtensorModule::get_coldkey_balance(&subnet_account_1); + let balance2 = SubtensorModule::get_coldkey_balance(&subnet_account_2); + let initial_pool_tao = NetworkMinLockCost::::get(); + assert_eq!(balance1, lock1 + reserve1 - initial_pool_tao); + assert_eq!(balance2, lock2 + reserve2 - initial_pool_tao); + + // Check migration has been marked as run + const MIGRATION_NAME: &[u8] = b"migrate_subnet_balances"; + assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); + }); +} + +#[test] +fn test_migrate_fix_total_issuance_evm_fees() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &[u8] = b"migrate_fix_total_issuance_evm_fees"; + const DUST_MIGRATION_NAME: &[u8] = b"migrate_fix_total_issuance_after_dust_collection"; + + let account = U256::from(42); + let balances_total_issuance = TaoBalance::from(123_456_789_u64); + Balances::make_free_balance_be(&account, balances_total_issuance); + + let broken_subtensor_total_issuance = TaoBalance::from(987_654_321_u64); + TotalIssuance::::put(broken_subtensor_total_issuance); + + assert_eq!(Balances::total_issuance(), balances_total_issuance); + assert_eq!( + TotalIssuance::::get(), + broken_subtensor_total_issuance + ); + assert!(!HasMigrationRun::::get(MIGRATION_NAME.to_vec())); + + let weight = crate::migrations::migrate_fix_total_issuance_evm_fees::migrate_fix_total_issuance_evm_fees::(); + + assert!(!weight.is_zero(), "weight must be non-zero"); + assert_eq!(TotalIssuance::::get(), balances_total_issuance); + assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); + assert!(!HasMigrationRun::::get( + DUST_MIGRATION_NAME.to_vec() + )); + + let second_wrong_value = TaoBalance::from(555_u64); + TotalIssuance::::put(second_wrong_value); + + crate::migrations::migrate_fix_total_issuance_evm_fees::migrate_fix_total_issuance_evm_fees::(); + + assert_eq!(TotalIssuance::::get(), balances_total_issuance); + assert!(HasMigrationRun::::get( + DUST_MIGRATION_NAME.to_vec() + )); + + let third_wrong_value = TaoBalance::from(777_u64); + TotalIssuance::::put(third_wrong_value); + + crate::migrations::migrate_fix_total_issuance_evm_fees::migrate_fix_total_issuance_evm_fees::(); + + assert_eq!( + TotalIssuance::::get(), + third_wrong_value, + "migration must not run after all known migration keys have run" + ); + }); +} diff --git a/pallets/subtensor/src/tests/migration/subnet_volume_emission_flags.rs b/pallets/subtensor/src/tests/migration/subnet_volume_emission_flags.rs new file mode 100644 index 0000000000..82cd43987a --- /dev/null +++ b/pallets/subtensor/src/tests/migration/subnet_volume_emission_flags.rs @@ -0,0 +1,138 @@ +#![allow( + unused, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used +)] +//! subnet volume, first emission block, subtoken, zero hotkey alpha. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_migrate_subnet_volume() { + new_test_ext(1).execute_with(|| { + // Setup initial state + let netuid_1 = NetUid::from(1); + add_network(netuid_1, 1, 0); + + // SubnetValue for netuid 1 key + let old_key: [u8; 34] = hex_literal::hex!( + "658faa385070e074c85bf6b568cf05553c3226e141696000b4b239c65bc2b2b40100" + ); + + // Old value in u64 format + let old_value: u64 = 123_456_789_000_u64; + put::(&old_key, &old_value); // Store as u64 + + // Ensure it is stored as `u64` + assert_eq!(get::(&old_key), Some(old_value)); + + // Run migration + crate::migrations::migrate_subnet_volume::migrate_subnet_volume::(); + + // Verify the value is now stored as `u128` + let new_value: Option = get(&old_key); + let new_value_as_subnet_volume = SubnetVolume::::get(netuid_1); + assert_eq!(new_value, Some(old_value as u128)); + assert_eq!(new_value_as_subnet_volume, old_value as u128); + + // Ensure migration does not break when running twice + let weight_second_run = + crate::migrations::migrate_subnet_volume::migrate_subnet_volume::(); + + // Verify the value is still stored as `u128` + let new_value: Option = get(&old_key); + assert_eq!(new_value, Some(old_value as u128)); + }); +} + +#[test] +fn test_migrate_set_first_emission_block_number() { + new_test_ext(1).execute_with(|| { + let netuids: [NetUid; 3] = [1.into(), 2.into(), 3.into()]; + let block_number = 100; + for netuid in netuids.iter() { + add_network(*netuid, 1, 0); + } + run_to_block(block_number); + let weight = crate::migrations::migrate_set_first_emission_block_number::migrate_set_first_emission_block_number::(); + + let expected_weight: Weight = ::DbWeight::get().reads(3) + ::DbWeight::get().writes(netuids.len() as u64); + assert_eq!(weight, expected_weight); + + assert_eq!(FirstEmissionBlockNumber::::get(NetUid::ROOT), None); + for netuid in netuids.iter() { + assert_eq!(FirstEmissionBlockNumber::::get(netuid), Some(block_number)); + } +}); +} + +#[test] +fn test_migrate_set_subtoken_enable() { + new_test_ext(1).execute_with(|| { + let netuids: [NetUid; 3] = [1.into(), 2.into(), 3.into()]; + let block_number = 100; + for netuid in netuids.iter() { + add_network(*netuid, 1, 0); + } + + let new_netuid = NetUid::from(4); + add_network_without_emission_block(new_netuid, 1, 0); + + let weight = + crate::migrations::migrate_set_subtoken_enabled::migrate_set_subtoken_enabled::(); + + let expected_weight: Weight = ::DbWeight::get().reads(1) + + ::DbWeight::get().writes(netuids.len() as u64 + 2); + assert_eq!(weight, expected_weight); + + for netuid in netuids.iter() { + assert!(SubtokenEnabled::::get(netuid)); + } + assert!(!SubtokenEnabled::::get(new_netuid)); + }); +} + +#[test] +fn test_migrate_remove_zero_total_hotkey_alpha() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &str = "migrate_remove_zero_total_hotkey_alpha"; + let netuid = NetUid::from(1u16); + + let hotkey_zero = U256::from(100u64); + let hotkey_nonzero = U256::from(101u64); + + // Insert one zero-alpha entry and one non-zero entry + TotalHotkeyAlpha::::insert(hotkey_zero, netuid, AlphaBalance::ZERO); + TotalHotkeyAlpha::::insert(hotkey_nonzero, netuid, AlphaBalance::from(123)); + + assert_eq!(TotalHotkeyAlpha::::get(hotkey_zero, netuid), AlphaBalance::ZERO); + assert_eq!(TotalHotkeyAlpha::::get(hotkey_nonzero, netuid), AlphaBalance::from(123)); + + assert!( + !HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should not have run yet." + ); + + let weight = crate::migrations::migrate_remove_zero_total_hotkey_alpha::migrate_remove_zero_total_hotkey_alpha::(); + + assert!( + HasMigrationRun::::get(MIGRATION_NAME.as_bytes().to_vec()), + "Migration should be marked as run." + ); + + assert!( + !TotalHotkeyAlpha::::contains_key(hotkey_zero, netuid), + "Zero-alpha entry should have been removed." + ); + + assert_eq!(TotalHotkeyAlpha::::get(hotkey_nonzero, netuid), AlphaBalance::from(123)); + + assert!( + !weight.is_zero(), + "Migration weight should be non-zero." + ); + }); +} diff --git a/pallets/subtensor/src/tests/migration/swap_cleanup.rs b/pallets/subtensor/src/tests/migration/swap_cleanup.rs new file mode 100644 index 0000000000..ad92290c1e --- /dev/null +++ b/pallets/subtensor/src/tests/migration/swap_cleanup.rs @@ -0,0 +1,410 @@ +#![allow( + unused, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used +)] +//! swap v3 cleanup, coldkey-swap announcements, registration map clear, axon/cert purge. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_migrate_remove_unknown_neuron_axon_cert_prom() { + use crate::migrations::migrate_remove_unknown_neuron_axon_cert_prom::*; + const MIGRATION_NAME: &[u8] = b"migrate_remove_neuron_axon_cert_prom"; + + new_test_ext(1).execute_with(|| { + setup_for(NetUid::from(2), 64, 1231); + setup_for(NetUid::from(42), 256, 15151); + setup_for(NetUid::from(99), 1024, 32323); + assert!(!HasMigrationRun::::get(MIGRATION_NAME)); + + let w = migrate_remove_unknown_neuron_axon_cert_prom::(); + assert!(!w.is_zero(), "Weight must be non-zero"); + + assert!(HasMigrationRun::::get(MIGRATION_NAME)); + assert_for(NetUid::from(2), 64, 1231); + assert_for(NetUid::from(42), 256, 15151); + assert_for(NetUid::from(99), 1024, 32323); + }); + + fn setup_for(netuid: NetUid, uids: u32, items: u32) { + NetworksAdded::::insert(netuid, true); + + for i in 1u32..=uids { + let hk = U256::from(netuid.inner() as u32 * 1000 + i); + Uids::::insert(netuid, hk, i as u16); + } + + for i in 1u32..=items { + let hk = U256::from(netuid.inner() as u32 * 1000 + i); + Axons::::insert(netuid, hk, AxonInfo::default()); + NeuronCertificates::::insert(netuid, hk, NeuronCertificate::default()); + Prometheus::::insert(netuid, hk, PrometheusInfo::default()); + } + } + + fn assert_for(netuid: NetUid, uids: u32, items: u32) { + assert_eq!( + Axons::::iter_key_prefix(netuid).count(), + uids as usize + ); + assert_eq!( + NeuronCertificates::::iter_key_prefix(netuid).count(), + uids as usize + ); + assert_eq!( + Prometheus::::iter_key_prefix(netuid).count(), + uids as usize + ); + + for i in 1u32..=uids { + let hk = U256::from(netuid.inner() as u32 * 1000 + i); + assert!(Axons::::contains_key(netuid, hk)); + assert!(NeuronCertificates::::contains_key(netuid, hk)); + assert!(Prometheus::::contains_key(netuid, hk)); + } + + for i in uids + 1u32..=items { + let hk = U256::from(netuid.inner() as u32 * 1000 + i); + assert!(!Axons::::contains_key(netuid, hk)); + assert!(!NeuronCertificates::::contains_key(netuid, hk)); + assert!(!Prometheus::::contains_key(netuid, hk)); + } + } +} + +// cargo test --package pallet-subtensor --lib -- tests::migration::test_migrate_cleanup_swap_v3 --exact --nocapture +#[test] +fn test_migrate_cleanup_swap_v3() { + use crate::migrations::migrate_cleanup_swap_v3::deprecated_swap_maps; + use substrate_fixed::types::U64F64; + + new_test_ext(1).execute_with(|| { + let migration = crate::migrations::migrate_cleanup_swap_v3::migrate_cleanup_swap_v3::; + + const MIGRATION_NAME: &str = "migrate_cleanup_swap_v3"; + + let provided: u64 = 9876; + let reserves: u64 = 1_000_000; + + SubnetTAO::::insert(NetUid::from(1), TaoBalance::from(reserves)); + SubnetAlphaIn::::insert(NetUid::from(1), AlphaBalance::from(reserves)); + + // Insert deprecated maps values + deprecated_swap_maps::SubnetTaoProvided::::insert( + NetUid::from(1), + TaoBalance::from(provided), + ); + deprecated_swap_maps::SubnetAlphaInProvided::::insert( + NetUid::from(1), + AlphaBalance::from(provided), + ); + + // Run migration + let weight = migration(); + + // Test that values are removed from state + assert!(!deprecated_swap_maps::SubnetTaoProvided::::contains_key(NetUid::from(1)),); + assert!( + !deprecated_swap_maps::SubnetAlphaInProvided::::contains_key(NetUid::from(1)), + ); + + // Provided got added to reserves + assert_eq!( + u64::from(SubnetTAO::::get(NetUid::from(1))), + reserves + provided + ); + assert_eq!( + u64::from(SubnetAlphaIn::::get(NetUid::from(1))), + reserves + provided + ); + }); +} + +// Regression test for issue #2793: migrate_cleanup_swap_v3 must be wired into the pallet +// on_runtime_upgrade hook. Seeds a *Provided residual, runs the full upgrade hook, and asserts +// the residual is folded into the main reserves. Without the wiring line in hooks.rs this fails. +#[test] +fn test_migrate_cleanup_swap_v3_runs_on_runtime_upgrade() { + use crate::migrations::migrate_cleanup_swap_v3::deprecated_swap_maps; + use frame_support::traits::Hooks; + + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let provided: u64 = 9876; + + deprecated_swap_maps::SubnetTaoProvided::::insert(netuid, TaoBalance::from(provided)); + deprecated_swap_maps::SubnetAlphaInProvided::::insert( + netuid, + AlphaBalance::from(provided), + ); + + let tao_before = u64::from(SubnetTAO::::get(netuid)); + let alpha_before = u64::from(SubnetAlphaIn::::get(netuid)); + + let _ = as Hooks>::on_runtime_upgrade(); + + assert!(!deprecated_swap_maps::SubnetTaoProvided::::contains_key(netuid)); + assert!(!deprecated_swap_maps::SubnetAlphaInProvided::::contains_key(netuid)); + assert_eq!( + u64::from(SubnetTAO::::get(netuid)), + tao_before + provided + ); + assert_eq!( + u64::from(SubnetAlphaIn::::get(netuid)), + alpha_before + provided + ); + }); +} + +#[test] +fn test_migrate_coldkey_swap_scheduled_to_announcements() { + new_test_ext(1000).execute_with(|| { + use crate::migrations::migrate_coldkey_swap_scheduled_to_announcements::*; + use coldkey_swap_deprecated as deprecated; + + const MIGRATION_NAME: &[u8] = b"migrate_coldkey_swap_scheduled_to_announcements"; + let now = frame_system::Pallet::::block_number(); + + // Set the schedule duration and reschedule duration + deprecated::ColdkeySwapScheduleDuration::::set(Some(now + 100)); + deprecated::ColdkeySwapRescheduleDuration::::set(Some(now + 200)); + + let make_swap_task = |who: U256, new_coldkey: U256| -> ScheduledOf { + let call_bytes = deprecated::RuntimeCall::::SubtensorCall( + deprecated::SubtensorCall::SwapColdkey { + old_coldkey: who, + new_coldkey, + swap_cost: 1000.into(), + }, + ) + .encode(); + pallet_scheduler::Scheduled { + maybe_id: None, + priority: 63, + call: Bounded::Inline(BoundedVec::truncate_from(call_bytes)), + maybe_periodic: None, + origin: OriginCaller::system(frame_system::RawOrigin::Root), + _phantom: PhantomData, + } + }; + + let make_other_task = || -> ScheduledOf { + let call_bytes = RuntimeCall::SubtensorModule(crate::Call::burned_register { + netuid: 1u16.into(), + hotkey: U256::from(999), + }) + .encode(); + pallet_scheduler::Scheduled { + maybe_id: None, + priority: 63, + call: Bounded::Inline(BoundedVec::truncate_from(call_bytes)), + maybe_periodic: None, + origin: OriginCaller::system(frame_system::RawOrigin::Root), + _phantom: PhantomData, + } + }; + + deprecated::ColdkeySwapScheduled::::insert( + U256::from(1), + (now + 100, U256::from(10)), + ); + pallet_scheduler::Agenda::::insert( + now + 100, + BoundedVec::truncate_from(vec![ + Some(make_swap_task(U256::from(1), U256::from(10))), + Some(make_other_task()), + ]), + ); + + deprecated::ColdkeySwapScheduled::::insert( + U256::from(2), + (now - 200, U256::from(20)), + ); + + deprecated::ColdkeySwapScheduled::::insert( + U256::from(3), + (now + 200, U256::from(30)), + ); + pallet_scheduler::Agenda::::insert( + now + 200, + BoundedVec::truncate_from(vec![Some(make_swap_task(U256::from(3), U256::from(30)))]), + ); + + deprecated::ColdkeySwapScheduled::::insert( + U256::from(4), + (now - 400, U256::from(40)), + ); + + deprecated::ColdkeySwapScheduled::::insert( + U256::from(5), + (now + 300, U256::from(50)), + ); + pallet_scheduler::Agenda::::insert( + now + 300, + BoundedVec::truncate_from(vec![ + Some(make_other_task()), + Some(make_swap_task(U256::from(5), U256::from(50))), + ]), + ); + + let w = migrate_coldkey_swap_scheduled_to_announcements::(); + + assert!(!w.is_zero(), "weight must be non-zero"); + assert!(HasMigrationRun::::get(MIGRATION_NAME)); + + // Ensure the deprecated storage is cleared + assert!(!deprecated::ColdkeySwapScheduleDuration::::exists()); + assert!(!deprecated::ColdkeySwapRescheduleDuration::::exists()); + assert_eq!(deprecated::ColdkeySwapScheduled::::iter().count(), 0); + + assert_eq!( + pallet_scheduler::Agenda::::get(now + 100), + vec![None, Some(make_other_task())], + "swap task for who=1 should be cancelled" + ); + + assert_eq!( + pallet_scheduler::Agenda::::get(now + 200), + vec![None], + "swap task for who=3 should be cancelled" + ); + + assert_eq!( + pallet_scheduler::Agenda::::get(now + 300), + vec![Some(make_other_task()), None], + "swap task for who=5 should be cancelled" + ); + + let delay = ColdkeySwapAnnouncementDelay::::get(); + assert_eq!(ColdkeySwapAnnouncements::::iter().count(), 3); + assert!(!ColdkeySwapAnnouncements::::contains_key(U256::from( + 2 + ))); + assert!(!ColdkeySwapAnnouncements::::contains_key(U256::from( + 4 + ))); + assert_eq!( + ColdkeySwapAnnouncements::::get(U256::from(1)), + Some(( + now + 100 - delay, + ::Hashing::hash_of(&U256::from(10)) + )) + ); + assert_eq!( + ColdkeySwapAnnouncements::::get(U256::from(3)), + Some(( + now + 200 - delay, + ::Hashing::hash_of(&U256::from(30)) + )) + ); + assert_eq!( + ColdkeySwapAnnouncements::::get(U256::from(5)), + Some(( + now + 300 - delay, + ::Hashing::hash_of(&U256::from(50)) + )) + ); + }); +} + +#[test] +fn test_migrate_clear_deprecated_registration_maps() { + new_test_ext(1).execute_with(|| { + const MIG_NAME: &[u8] = b"migrate_clear_deprecated_registration_maps_v1"; + + let netuid0: NetUid = 0u16.into(); + let netuid1: NetUid = 1u16.into(); + + // -------------------------------------------------------------------- + // 0) Pre-state + // -------------------------------------------------------------------- + assert!( + !HasMigrationRun::::get(MIG_NAME.to_vec()), + "migration flag should be false before run" + ); + + // New-model storage must remain untouched by this migration. + crate::BurnHalfLife::::insert(netuid0, 777u16); + crate::BurnIncreaseMult::::insert(netuid0, U64F64::from_num(9)); + + crate::BurnHalfLife::::insert(netuid1, 888u16); + crate::BurnIncreaseMult::::insert(netuid1, U64F64::from_num(11)); + + assert_eq!(crate::BurnHalfLife::::get(netuid0), 777u16); + assert_eq!(crate::BurnIncreaseMult::::get(netuid0), 9u64); + + assert_eq!(crate::BurnHalfLife::::get(netuid1), 888u16); + assert_eq!(crate::BurnIncreaseMult::::get(netuid1), 11u64); + + // Seed deprecated storage items that the migration is expected to clear. + crate::NetworkPowRegistrationAllowed::::insert(netuid0, true); + + crate::POWRegistrationsThisInterval::::insert(netuid0, 7u16); + crate::BurnRegistrationsThisInterval::::insert(netuid0, 8u16); + + crate::NetworkPowRegistrationAllowed::::insert(netuid1, false); + + crate::POWRegistrationsThisInterval::::insert(netuid1, 17u16); + crate::BurnRegistrationsThisInterval::::insert(netuid1, 18u16); + + assert!(crate::NetworkPowRegistrationAllowed::::contains_key(netuid0)); + assert!(crate::POWRegistrationsThisInterval::::contains_key(netuid0)); + assert!(crate::BurnRegistrationsThisInterval::::contains_key(netuid0)); + + assert!(crate::NetworkPowRegistrationAllowed::::contains_key(netuid1)); + assert!(crate::POWRegistrationsThisInterval::::contains_key(netuid1)); + assert!(crate::BurnRegistrationsThisInterval::::contains_key(netuid1)); + + // -------------------------------------------------------------------- + // 1) Run migration + // -------------------------------------------------------------------- + let w = crate::migrations::migrate_clear_deprecated_registration_maps::migrate_clear_deprecated_registration_maps::(); + assert!(!w.is_zero(), "weight must be non-zero"); + + // -------------------------------------------------------------------- + // 2) Post-state: deprecated storage cleared + // -------------------------------------------------------------------- + assert!( + HasMigrationRun::::get(MIG_NAME.to_vec()), + "migration flag should be true after run" + ); + + assert!(!crate::NetworkPowRegistrationAllowed::::contains_key(netuid0)); + assert!(!crate::POWRegistrationsThisInterval::::contains_key(netuid0)); + assert!(!crate::BurnRegistrationsThisInterval::::contains_key(netuid0)); + + assert!(!crate::NetworkPowRegistrationAllowed::::contains_key(netuid1)); + assert!(!crate::POWRegistrationsThisInterval::::contains_key(netuid1)); + assert!(!crate::BurnRegistrationsThisInterval::::contains_key(netuid1)); + + // -------------------------------------------------------------------- + // 3) Post-state: new-model storage unchanged + // -------------------------------------------------------------------- + assert_eq!(crate::BurnHalfLife::::get(netuid0), 777u16); + assert_eq!(crate::BurnIncreaseMult::::get(netuid0), 9u64); + + assert_eq!(crate::BurnHalfLife::::get(netuid1), 888u16); + assert_eq!(crate::BurnIncreaseMult::::get(netuid1), 11u64); + + // -------------------------------------------------------------------- + // 4) Idempotency + // -------------------------------------------------------------------- + let w2 = crate::migrations::migrate_clear_deprecated_registration_maps::migrate_clear_deprecated_registration_maps::(); + assert!(!w2.is_zero(), "second call should still return non-zero read weight"); + + assert!( + HasMigrationRun::::get(MIG_NAME.to_vec()), + "migration flag should remain true after second run" + ); + + assert_eq!(crate::BurnHalfLife::::get(netuid0), 777u16); + assert_eq!(crate::BurnIncreaseMult::::get(netuid0), 9u64); + + assert_eq!(crate::BurnHalfLife::::get(netuid1), 888u16); + assert_eq!(crate::BurnIncreaseMult::::get(netuid1), 11u64); + }); +} diff --git a/pallets/subtensor/src/tests/migration/transfer_and_delete_subnets.rs b/pallets/subtensor/src/tests/migration/transfer_and_delete_subnets.rs new file mode 100644 index 0000000000..f5c737d9b0 --- /dev/null +++ b/pallets/subtensor/src/tests/migration/transfer_and_delete_subnets.rs @@ -0,0 +1,59 @@ +#![allow( + unused, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used +)] +//! foundation ownership transfer + delete subnet 3/21. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_migration_transfer_nets_to_foundation() { + new_test_ext(1).execute_with(|| { + // Create subnet 1 + add_network(1.into(), 1, 0); + // Create subnet 11 + add_network(11.into(), 1, 0); + + log::info!("{:?}", SubtensorModule::get_subnet_owner(1.into())); + //assert_eq!(SubtensorModule::::get_subnet_owner(1), ); + + // Run the migration to transfer ownership + let hex = + hex_literal::hex!["feabaafee293d3b76dae304e2f9d885f77d2b17adab9e17e921b321eccd61c77"]; + crate::migrations::migrate_transfer_ownership_to_foundation::migrate_transfer_ownership_to_foundation::(hex); + + log::info!("new owner: {:?}", SubtensorModule::get_subnet_owner(1.into())); + }) +} + +#[test] +fn test_migration_delete_subnet_3() { + new_test_ext(1).execute_with(|| { + // Create subnet 3 + add_network(3.into(), 1, 0); + assert!(SubtensorModule::subnet_exists(3.into())); + + // Run the migration to transfer ownership + crate::migrations::migrate_delete_subnet_3::migrate_delete_subnet_3::(); + + assert!(!SubtensorModule::subnet_exists(3.into())); + }) +} + +#[test] +fn test_migration_delete_subnet_21() { + new_test_ext(1).execute_with(|| { + // Create subnet 21 + add_network(21.into(), 1, 0); + assert!(SubtensorModule::subnet_exists(21.into())); + + // Run the migration to transfer ownership + crate::migrations::migrate_delete_subnet_21::migrate_delete_subnet_21::(); + + assert!(!SubtensorModule::subnet_exists(21.into())); + }) +} diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index d9ab976293..fe3716a7bb 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -1,3 +1,16 @@ +//! Default test runtime and fixtures for `pallet-subtensor` unit tests. +//! +//! ## Search anchors +//! +//! | Helper | Owns | +//! |--------|------| +//! | [`new_test_ext`] / [`test_ext_with_balances`] | Externalities bootstrap | +//! | [`add_network`] / [`add_dynamic_network`] | Subnet setup | +//! | [`register_ok_neuron`] / [`setup_neuron_with_stake`] | Neuron + stake fixtures | +//! | [`step_block`] / [`run_to_block`] / [`step_epochs`] | Block / tempo advancement | +//! | [`setup_reserves`] / [`swap_tao_to_alpha`] | AMM reserve / swap helpers | +//! | [`mock_set_children`] | Parent/child key fixtures | + #![allow( clippy::arithmetic_side_effects, clippy::expect_used, @@ -382,7 +395,7 @@ impl crate::Config for Test { type LeaseDividendsDistributionInterval = LeaseDividendsDistributionInterval; type GetCommitments = (); type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; - type CommitmentsInterface = CommitmentsI; + type CommitmentsInterface = CommitmentsPurgeBridge; type AlphaAssets = AlphaAssets; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; type AuthorshipProvider = MockAuthorshipProvider; @@ -423,7 +436,7 @@ impl pallet_commitments::Config for Test { type MaxFields = TestMaxFields; type InitialDeposit = ConstTao<0>; type FieldDeposit = ConstTao<0>; - type TempoInterface = MockTempoInterface; + type SubtensorTempoBridge = MockTempoInterface; } pub struct OriginPrivilegeCmp; @@ -434,8 +447,8 @@ impl PrivilegeCmp for OriginPrivilegeCmp { } } -pub struct CommitmentsI; -impl CommitmentsInterface for CommitmentsI { +pub struct CommitmentsPurgeBridge; +impl CommitmentsInterface for CommitmentsPurgeBridge { fn purge_netuid( netuid: NetUid, weight_meter: &mut frame_support::weights::WeightMeter, diff --git a/pallets/subtensor/src/tests/mock_high_ed.rs b/pallets/subtensor/src/tests/mock_high_ed.rs index f991cab592..71d2fd6daa 100644 --- a/pallets/subtensor/src/tests/mock_high_ed.rs +++ b/pallets/subtensor/src/tests/mock_high_ed.rs @@ -1,3 +1,8 @@ +//! Alternate test runtime with a high existential deposit. +//! +//! Used by [`crate::tests::tao`] for TAO issuance / dust / ED-sensitive paths. +//! Prefer [`crate::tests::mock`] for ordinary pallet tests. + #![allow( clippy::arithmetic_side_effects, clippy::expect_used, @@ -304,7 +309,7 @@ impl crate::Config for Test { type LeaseDividendsDistributionInterval = LeaseDividendsDistributionInterval; type GetCommitments = (); type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; - type CommitmentsInterface = CommitmentsI; + type CommitmentsInterface = CommitmentsPurgeBridge; type AlphaAssets = AlphaAssets; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; type AuthorshipProvider = MockAuthorshipProvider; @@ -344,8 +349,8 @@ impl PrivilegeCmp for OriginPrivilegeCmp { } } -pub struct CommitmentsI; -impl CommitmentsInterface for CommitmentsI { +pub struct CommitmentsPurgeBridge; +impl CommitmentsInterface for CommitmentsPurgeBridge { fn purge_netuid( _netuid: NetUid, _weight_meter: &mut frame_support::weights::WeightMeter, diff --git a/pallets/subtensor/src/tests/mod.rs b/pallets/subtensor/src/tests/mod.rs index bbf2c4404a..091973bc22 100644 --- a/pallets/subtensor/src/tests/mod.rs +++ b/pallets/subtensor/src/tests/mod.rs @@ -1,3 +1,59 @@ +//! Integration and unit tests for `pallet-subtensor`. +//! +//! Flat files cover single concepts; split directories (`mod foo;` → `foo/mod.rs`) +//! hold the larger suites that were broken out by domain. +//! +//! ## Search anchors — split directories +//! +//! | Module | Owns | +//! |--------|------| +//! | [`math`] | Fixed-point helpers mirroring `epoch/math/` | +//! | [`weights`] | `set_weights`, commit–reveal, timelocked CRv3 | +//! | [`staking`] | Add/remove/move stake, take, share pools | +//! | [`migration`] | Storage / share-pool / coldkey migrations | +//! | [`locks`] | Stake locks, transfer, unlock schedules | +//! | [`children`] | Parent/child key maps and pending children | +//! | [`coinbase`] | Block-step emission, root / subnet coinbase | +//! | [`epoch`] | Yuma epoch / bonds / consensus timing | +//! | [`networks`] | Register / dissolve / prune / registration queue | +//! | [`swap_hotkey_with_subnet`] | Subnet-scoped hotkey swap | +//! +//! ## Search anchors — flat modules +//! +//! | Module | Owns | +//! |--------|------| +//! | [`mock`] / [`mock_high_ed`] | Test runtime + fixtures (`new_test_ext`, networks, stake) | +//! | [`auto_stake_hotkey`] | `set_coldkey_auto_stake_hotkey` | +//! | [`batch_tx`] | Utility `batch` nesting / allow-list | +//! | [`claim_root`] | Root alpha claim / thresholds | +//! | [`cleanup_tests`] | `remove_storage_entries_for_netuid` weight budgeting | +//! | [`coldkey_lineage`] / [`hotkey_lineage`] | Swap lineage recording | +//! | [`consensus`] | Synthetic consensus / map-consensus stress | +//! | [`delegate_info`] | RPC delegate info / return-per-1000 | +//! | [`destroy_alpha_tests`] | Dissolve-path destroy alpha in/out | +//! | [`dissolution`] | Subnet dissolve cleanup / netuid reuse | +//! | [`emission`] | `blocks_until_next_auto_epoch` | +//! | [`ensure`] | Subnet-owner / admin-window origin guards | +//! | [`epoch_logs`] | Epoch trace-log assertions | +//! | [`evm`] | `associate_evm_key` | +//! | [`leasing`] | Subnet leasing | +//! | [`mechanism`] | Multi-mechanism subnet state | +//! | [`move_stake`] | Cross-subnet / hotkey move stake | +//! | [`neuron_info`] | RPC neuron info getters | +//! | [`recycle_alpha`] | Alpha recycle into subnet | +//! | [`registration`] | Neuron / burned registration | +//! | [`remove_data_tests`] | Hotkey/coldkey data purge | +//! | [`serving`] | Axon / prometheus serve + identity | +//! | [`staking2`] | Dynamic-mechanism stake / swap paths | +//! | [`subnet`] | `do_start_call`, symbols, subnet lifecycle | +//! | [`subnet_emissions`] | Subnet emission share math | +//! | [`subnet_info`] | RPC hyperparams V3 | +//! | [`swap_coldkey`] / [`swap_hotkey`] | Full-account key swaps | +//! | [`tao`] | TAO issuance / high existential-deposit edge cases | +//! | [`tempo_control`] | Tempo / trigger-epoch / activity cutoff | +//! | [`uids`] | `replace_neuron` and uid maps | +//! | [`voting_power`] | Voting-power EMA tracking | + mod auto_stake_hotkey; mod batch_tx; mod children; diff --git a/pallets/subtensor/src/tests/move_stake.rs b/pallets/subtensor/src/tests/move_stake.rs index 45e3c6f806..bc2e4d0fba 100644 --- a/pallets/subtensor/src/tests/move_stake.rs +++ b/pallets/subtensor/src/tests/move_stake.rs @@ -1,3 +1,7 @@ +//! Tests for moving stake across hotkeys / subnets ([`crate::staking::move_stake`]). +//! +//! Covers limit orders, fees, partial moves, and cross-subnet alpha accounting. + #![allow(clippy::unwrap_used)] use approx::assert_abs_diff_eq; diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs deleted file mode 100644 index a1af973eb6..0000000000 --- a/pallets/subtensor/src/tests/networks.rs +++ /dev/null @@ -1,3863 +0,0 @@ -#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] - -use super::mock::*; -use crate::migrations::migrate_network_immunity_period; -use crate::staking::lock::LockState; -use crate::*; -use frame_support::{assert_err, assert_ok, weights::Weight}; -use frame_system::Config; -use sp_core::U256; -use sp_runtime::PerU16; -use sp_std::collections::{btree_map::BTreeMap, vec_deque::VecDeque}; -use substrate_fixed::types::{I96F32, U64F64, U96F32}; -use subtensor_runtime_common::{MechId, NetUidStorageIndex, TaoBalance}; -use subtensor_swap_interface::{Order, SwapHandler}; - -/// Run the same α-out destroy steps as `remove_data_for_dissolved_networks` (post-root-cleanup). -fn destroy_alpha_in_out_stakes_full_pipeline_for_test(netuid: NetUid) { - run_destroy_alpha_in_out_stakes_full_pipeline(netuid); -} - -#[test] -fn test_registration_ok() { - new_test_ext(1).execute_with(|| { - let block_number: u64 = 0; - let netuid = NetUid::from(2); - let tempo: u16 = 13; - let hotkey_account_id: U256 = U256::from(1); - let coldkey_account_id: U256 = U256::from(0); // Neighbour of the beast, har har - - add_network(netuid, tempo, 0); - - // Ensure reserves exist for any registration path that might touch swap/burn logic. - let reserve: u64 = 1_000_000_000_000; - setup_reserves( - netuid, - TaoBalance::from(reserve), - AlphaBalance::from(reserve), - ); - - // registration economics changed. Ensure the coldkey has enough spendable balance - add_balance_to_coldkey_account(&coldkey_account_id, TaoBalance::from(reserve)); - add_balance_to_coldkey_account(&hotkey_account_id, TaoBalance::from(reserve)); - - let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( - netuid, - block_number, - 129123813, - &hotkey_account_id, - ); - - // PoW register should succeed. - assert_ok!(SubtensorModule::register( - <::RuntimeOrigin>::signed(hotkey_account_id), - netuid, - block_number, - nonce, - work.clone(), - hotkey_account_id, - coldkey_account_id - )); - - assert_ok!(SubtensorModule::do_dissolve_network(netuid)); - assert!(!SubtensorModule::if_subnet_exist(netuid)); - }) -} - -#[test] -fn dissolve_no_stakers_no_alpha_no_emission() { - new_test_ext(0).execute_with(|| { - let cold = U256::from(1); - let hot = U256::from(2); - let net = add_dynamic_network(&hot, &cold); - - SubtensorModule::set_subnet_locked_balance(net, TaoBalance::from(0)); - SubnetTAO::::insert(net, TaoBalance::from(0)); - Emission::::insert(net, Vec::::new()); - - let before = SubtensorModule::get_coldkey_balance(&cold); - assert_ok!(SubtensorModule::do_dissolve_network(net)); - let after = SubtensorModule::get_coldkey_balance(&cold); - - // Balance should be unchanged (whatever the network-lock bookkeeping left there) - assert_eq!(after, before); - assert!(!SubtensorModule::if_subnet_exist(net)); - }); -} - -#[test] -fn dissolve_defers_cleanup_until_on_idle() { - new_test_ext(0).execute_with(|| { - let owner_cold = U256::from(11); - let owner_hot = U256::from(12); - let net = add_dynamic_network(&owner_hot, &owner_cold); - - // Set up EVM association data to verify it gets cleaned up too. - let evm_key = sp_core::H160::from_low_u64_be(42); - SubtensorModule::set_associated_evm_address(net, 0u16, evm_key, 1u64); - assert!(AssociatedEvmAddress::::contains_key(net, 0u16)); - assert!(!AssociatedUidsByEvmAddress::::get(net, evm_key).is_empty()); - - assert!(SubnetOwner::::contains_key(net)); - assert!(SubnetOwnerHotkey::::contains_key(net)); - assert!(NetworkRegisteredAt::::contains_key(net)); - assert!(!DissolveCleanupQueue::::get().contains(&net)); - - assert_ok!(SubtensorModule::do_dissolve_network(net)); - - // Network is no longer considered "existing" but data is not cleaned yet. - assert!(!SubtensorModule::if_subnet_exist(net)); - assert!(DissolveCleanupQueue::::get().contains(&net)); - assert!(SubnetOwner::::contains_key(net)); - assert!(NetworkRegisteredAt::::contains_key(net)); - // EVM data still present before on_idle cleanup. - assert!(AssociatedEvmAddress::::contains_key(net, 0u16)); - assert!(!AssociatedUidsByEvmAddress::::get(net, evm_key).is_empty()); - - // Cleanup happens in on_idle. - run_block_idle(); - assert!(!NetworkRegisteredAt::::contains_key(net)); - assert!(!SubnetOwner::::contains_key(net)); - assert!(!DissolveCleanupQueue::::get().contains(&net)); - // EVM data cleaned up as part of NetworkMapParameters phase. - assert!(!AssociatedEvmAddress::::contains_key(net, 0u16)); - assert!(AssociatedUidsByEvmAddress::::get(net, evm_key).is_empty()); - }); -} - -#[test] -fn dissolve_refunds_full_lock_cost_when_no_emission() { - new_test_ext(0).execute_with(|| { - let cold = U256::from(3); - let hot = U256::from(4); - let net = add_dynamic_network(&hot, &cold); - - // Mark this subnet as *legacy* so owner refund path is enabled. - let reg_at = NetworkRegisteredAt::::get(net); - NetworkRegistrationStartBlock::::put(reg_at.saturating_add(1)); - - let lock: TaoBalance = TaoBalance::from(1_000_000); - SubtensorModule::set_subnet_locked_balance(net, lock); - SubnetTAO::::insert(net, TaoBalance::from(0)); - Emission::::insert(net, Vec::::new()); - - let before = SubtensorModule::get_coldkey_balance(&cold); - assert_ok!(SubtensorModule::do_dissolve_network(net)); - run_block_idle(); - let after = SubtensorModule::get_coldkey_balance(&cold); - - assert_eq!(TaoBalance::from(after), TaoBalance::from(before) + lock); - }); -} - -#[test] -fn dissolve_single_alpha_out_staker_gets_all_tao() { - new_test_ext(0).execute_with(|| { - // 1. Owner & subnet - let owner_cold = U256::from(10); - let owner_hot = U256::from(20); - let net = add_dynamic_network(&owner_hot, &owner_cold); - remove_owner_registration_stake(net); - SubnetAlphaIn::::insert(net, AlphaBalance::ZERO); - SubnetProtocolAlpha::::insert(net, AlphaBalance::ZERO); - - // 2. Single α-out staker - let (s_hot, s_cold) = (U256::from(100), U256::from(200)); - AlphaV2::::insert((s_hot, s_cold, net), sf_from_u64(5_000u64)); - - // Entire TAO pot should be paid to staker's cold-key - let pot: u64 = 99_999; - SubnetTAO::::insert(net, TaoBalance::from(pot)); - SubtensorModule::set_subnet_locked_balance(net, 0.into()); - TotalHotkeyAlpha::::insert(s_hot, net, AlphaBalance::from(5_000u64)); - - // Cold-key balance before - let before = SubtensorModule::get_coldkey_balance(&s_cold); - - // Dissolve - assert_ok!(SubtensorModule::do_dissolve_network(net)); - run_block_idle(); - - // Cold-key received full pot - let after = SubtensorModule::get_coldkey_balance(&s_cold); - assert_eq!(after, before + pot.into()); - - // No α entries left for dissolved subnet - assert!(AlphaV2::::iter().all(|((_h, _c, n), _)| n != net)); - assert!(!SubnetTAO::::contains_key(net)); - }); -} - -#[allow(clippy::indexing_slicing)] -#[test] -fn dissolve_two_stakers_pro_rata_distribution() { - new_test_ext(0).execute_with(|| { - // Subnet + two stakers - let oc = U256::from(50); - let oh = U256::from(51); - let net = add_dynamic_network(&oh, &oc); - remove_owner_registration_stake(net); - SubnetAlphaIn::::insert(net, AlphaBalance::ZERO); - SubnetProtocolAlpha::::insert(net, AlphaBalance::ZERO); - - // Mark this subnet as *legacy* so owner refund path is enabled. - let reg_at = NetworkRegisteredAt::::get(net); - NetworkRegistrationStartBlock::::put(reg_at.saturating_add(1)); - - let (s1_hot, s1_cold, a1) = (U256::from(201), U256::from(301), 300u64); - let (s2_hot, s2_cold, a2) = (U256::from(202), U256::from(302), 700u64); - - AlphaV2::::insert((s1_hot, s1_cold, net), sf_from_u64(a1)); - AlphaV2::::insert((s2_hot, s2_cold, net), sf_from_u64(a2)); - - TotalHotkeyAlpha::::insert(s1_hot, net, AlphaBalance::from(a1)); - TotalHotkeyAlpha::::insert(s2_hot, net, AlphaBalance::from(a2)); - - let pot: u64 = 10_000; - SubnetTAO::::insert(net, TaoBalance::from(pot)); - SubtensorModule::set_subnet_locked_balance(net, 5_000.into()); // owner refund path present; emission = 0 - - // Cold-key balances before - let s1_before = SubtensorModule::get_coldkey_balance(&s1_cold); - let s2_before = SubtensorModule::get_coldkey_balance(&s2_cold); - let owner_before = SubtensorModule::get_coldkey_balance(&oc); - - // Expected τ shares with largest remainder - let total = (a1 + a2) as u128; - let prod1 = (a1 as u128) * (pot as u128); - let prod2 = (a2 as u128) * (pot as u128); - let share1 = (prod1 / total) as u64; - let share2 = (prod2 / total) as u64; - let mut distributed = share1 + share2; - let mut rem = [(s1_cold, prod1 % total), (s2_cold, prod2 % total)]; - if distributed < pot { - rem.sort_by_key(|&(_c, r)| core::cmp::Reverse(r)); - let leftover = pot - distributed; - for _ in 0..leftover as usize { - distributed += 1; - } - } - // Recompute exact expected shares using the same logic - let mut expected1 = share1; - let mut expected2 = share2; - if share1 + share2 < pot { - rem.sort_by_key(|&(_c, r)| core::cmp::Reverse(r)); - if rem[0].0 == s1_cold { - expected1 += 1; - } else { - expected2 += 1; - } - } - - // Dissolve - assert_ok!(SubtensorModule::do_dissolve_network(net)); - run_block_idle(); - - // Cold-keys received their τ shares - assert_eq!( - SubtensorModule::get_coldkey_balance(&s1_cold), - s1_before + expected1.into() - ); - assert_eq!( - SubtensorModule::get_coldkey_balance(&s2_cold), - s2_before + expected2.into() - ); - - // Owner refunded lock (no emission) - assert_eq!( - SubtensorModule::get_coldkey_balance(&oc), - owner_before + 5_000.into() - ); - - // α entries for dissolved subnet gone - assert!(AlphaV2::::iter().all(|((_h, _c, n), _)| n != net)); - }); -} - -#[test] -fn dissolve_owner_cut_refund_logic() { - new_test_ext(0).execute_with(|| { - let oc = U256::from(70); - let oh = U256::from(71); - let net = add_dynamic_network(&oh, &oc); - remove_owner_registration_stake(net); - - // Mark this subnet as *legacy* so owner refund path is enabled. - let reg_at = NetworkRegisteredAt::::get(net); - NetworkRegistrationStartBlock::::put(reg_at.saturating_add(1)); - - // One staker and a TAO pot (not relevant to refund amount). - let sh = U256::from(77); - let sc = U256::from(88); - mock_increase_stake_for_hotkey_and_coldkey_on_subnet( - &sh, - &sc, - net, - AlphaBalance::from(800u64), - ); - SubnetTAO::::insert(net, TaoBalance::from(1_000)); - - // Lock & emissions: total emitted α = 800. - let lock: TaoBalance = TaoBalance::from(2_000); - SubtensorModule::set_subnet_locked_balance(net, lock); - // ensure there was some Alpha issued - assert!(SubtensorModule::get_alpha_issuance(net).to_u64() > 0); - - // Owner cut = 11796 / 65535 (about 18%). - SubnetOwnerCut::::put(11_796u16); - - // Compute expected refund with the SAME math as the pallet. - let frac: U96F32 = SubtensorModule::get_float_subnet_owner_cut(); - let total_emitted_alpha: u64 = SubtensorModule::get_alpha_issuance(net).to_u64(); - let owner_alpha_u64: u64 = U96F32::from_num(total_emitted_alpha) - .saturating_mul(frac) - .floor() - .saturating_to_num::(); - - // Use the current alpha price to estimate the TAO equivalent. - let owner_emission_tao = { - let price: U96F32 = U96F32::saturating_from_num( - ::SwapInterface::current_alpha_price(net.into()), - ); - U96F32::from_num(owner_alpha_u64) - .saturating_mul(price) - .floor() - .saturating_to_num::() - .into() - }; - - let expected_refund: TaoBalance = lock.saturating_sub(owner_emission_tao); - - println!("expected_refund = {:?}", expected_refund); - - let before = SubtensorModule::get_coldkey_balance(&oc); - assert_ok!(SubtensorModule::do_dissolve_network(net)); - run_block_idle(); - let after = SubtensorModule::get_coldkey_balance(&oc); - - assert!(after > before); // some refund is expected - let gain: TaoBalance = after.saturating_sub(before.into()); - assert!( - gain >= expected_refund, - "owner should receive at least the lock-based refund: gain {gain:?} expected_refund {expected_refund:?}" - ); - }); -} - -#[test] -fn dissolve_zero_refund_when_emission_exceeds_lock() { - new_test_ext(0).execute_with(|| { - let oc = U256::from(1_000); - let oh = U256::from(2_000); - let net = add_dynamic_network(&oh, &oc); - remove_owner_registration_stake(net); - - SubtensorModule::set_subnet_locked_balance(net, TaoBalance::from(1_000)); - SubnetOwnerCut::::put(u16::MAX); // 100 % - Emission::::insert(net, vec![AlphaBalance::from(2_000)]); - - let before = SubtensorModule::get_coldkey_balance(&oc); - assert_ok!(SubtensorModule::do_dissolve_network(net)); - let after = SubtensorModule::get_coldkey_balance(&oc); - - assert_eq!(after, before); // no refund - }); -} - -#[test] -fn dissolve_nonexistent_subnet_fails() { - new_test_ext(0).execute_with(|| { - assert_err!( - SubtensorModule::do_dissolve_network(9_999.into()), - Error::::SubnetNotExists - ); - }); -} - -#[test] -fn dissolve_clears_all_per_subnet_storages() { - new_test_ext(0).execute_with(|| { - let owner_cold = U256::from(123); - let owner_hot = U256::from(456); - let net = add_dynamic_network(&owner_hot, &owner_cold); - - // ------------------------------------------------------------------ - // Populate each storage item with a minimal value of the CORRECT type - // ------------------------------------------------------------------ - // Core ownership / bookkeeping - SubnetOwner::::insert(net, owner_cold); - SubnetOwnerHotkey::::insert(net, owner_hot); - SubnetworkN::::insert(net, 0u16); - NetworksAdded::::insert(net, true); - NetworkRegisteredAt::::insert(net, 0u64); - - // Consensus vectors - Active::::insert(net, vec![true]); - Emission::::insert(net, vec![AlphaBalance::from(1)]); - Incentive::::insert(NetUidStorageIndex::from(net), vec![PerU16::from_parts(1)]); - Consensus::::insert(net, vec![PerU16::from_parts(1)]); - Dividends::::insert(net, vec![PerU16::from_parts(1)]); - LastUpdate::::insert(NetUidStorageIndex::from(net), vec![0u64]); - ValidatorPermit::::insert(net, vec![true]); - ValidatorTrust::::insert(net, vec![PerU16::from_parts(1)]); - - // Per‑net params - Tempo::::insert(net, 1u16); - Kappa::::insert(net, 1u16); - Difficulty::::insert(net, 1u64); - - MaxAllowedUids::::insert(net, 1u16); - ImmunityPeriod::::insert(net, 1u16); - ActivityCutoff::::insert(net, 1u16); - MinAllowedWeights::::insert(net, 1u16); - - RegistrationsThisInterval::::insert(net, 1u16); - POWRegistrationsThisInterval::::insert(net, 1u16); - BurnRegistrationsThisInterval::::insert(net, 1u16); - - // Pool / AMM counters - SubnetTAO::::insert(net, TaoBalance::from(1)); - SubnetAlphaInEmission::::insert(net, AlphaBalance::from(1)); - SubnetAlphaOutEmission::::insert(net, AlphaBalance::from(1)); - SubnetTaoInEmission::::insert(net, TaoBalance::from(1)); - SubnetVolume::::insert(net, 1u128); - - // Items now REMOVED (not zeroed) by dissolution - SubnetAlphaIn::::insert(net, AlphaBalance::from(2)); - SubnetAlphaOut::::insert(net, AlphaBalance::from(3)); - SubnetProtocolAlpha::::insert(net, AlphaBalance::from(4)); - - // Prefix / double-map collections - Keys::::insert(net, 0u16, owner_hot); - Bonds::::insert(NetUidStorageIndex::from(net), 0u16, vec![(0u16, 1u16)]); - Weights::::insert(NetUidStorageIndex::from(net), 0u16, vec![(1u16, 1u16)]); - - // Membership entry for the SAME hotkey as Keys - IsNetworkMember::::insert(owner_hot, net, true); - - // Token / price / provided reserves - TokenSymbol::::insert(net, b"XX".to_vec()); - SubnetMovingPrice::::insert(net, substrate_fixed::types::I96F32::from_num(1)); - - // TAO Flow - SubnetTaoFlow::::insert(net, 0i64); - SubnetEmaTaoFlow::::insert(net, (0u64, substrate_fixed::types::I64F64::from_num(0))); - - // Subnet locks - TransferToggle::::insert(net, true); - SubnetLocked::::insert(net, TaoBalance::from(1)); - LargestLocked::::insert(net, 1u64); - - // Subnet parameters & pending counters - FirstEmissionBlockNumber::::insert(net, 1u64); - SubnetMechanism::::insert(net, 1u16); - NetworkRegistrationAllowed::::insert(net, true); - NetworkPowRegistrationAllowed::::insert(net, true); - PendingServerEmission::::insert(net, AlphaBalance::from(1)); - PendingValidatorEmission::::insert(net, AlphaBalance::from(1)); - PendingRootAlphaDivs::::insert(net, AlphaBalance::from(1)); - PendingOwnerCut::::insert(net, AlphaBalance::from(1)); - MinerBurned::::insert(net, substrate_fixed::types::U96F32::from_num(1)); - BlocksSinceLastStep::::insert(net, 1u64); - LastMechansimStepBlock::::insert(net, 1u64); - ServingRateLimit::::insert(net, 1u64); - Rho::::insert(net, 1u16); - AlphaSigmoidSteepness::::insert(net, 1i16); - - // Weights/versioning/targets/limits - WeightsVersionKey::::insert(net, 1u64); - MaxAllowedValidators::::insert(net, 1u16); - AdjustmentInterval::::insert(net, 2u16); - BondsMovingAverage::::insert(net, 1u64); - BondsPenalty::::insert(net, 1u16); - BondsResetOn::::insert(net, true); - WeightsSetRateLimit::::insert(net, 1u64); - ValidatorPruneLen::::insert(net, 1u64); - ScalingLawPower::::insert(net, 1u16); - TargetRegistrationsPerInterval::::insert(net, 1u16); - AdjustmentAlpha::::insert(net, 1u64); - CommitRevealWeightsEnabled::::insert(net, true); - - // Burn/difficulty/adjustment - Burn::::insert(net, TaoBalance::from(1)); - MinBurn::::insert(net, TaoBalance::from(1)); - MaxBurn::::insert(net, TaoBalance::from(2)); - MinDifficulty::::insert(net, 1u64); - MaxDifficulty::::insert(net, 2u64); - RegistrationsThisBlock::::insert(net, 1u16); - EMAPriceHalvingBlocks::::insert(net, 1u64); - RAORecycledForRegistration::::insert(net, TaoBalance::from(1)); - - // Feature toggles - LiquidAlphaOn::::insert(net, true); - Yuma3On::::insert(net, true); - AlphaValues::::insert(net, (1u16, 2u16)); - SubtokenEnabled::::insert(net, true); - OwnerCutAutoLockEnabled::::insert(net, true); - ImmuneOwnerUidsLimit::::insert(net, 1u16); - - // Per‑subnet vectors / indexes - StakeWeight::::insert(net, vec![1u16]); - - // Uid/registration - Uids::::insert(net, owner_hot, 0u16); - BlockAtRegistration::::insert(net, 0u16, 1u64); - - // Per‑subnet dividends - AlphaDividendsPerSubnet::::insert(net, owner_hot, AlphaBalance::from(1)); - - // Parent/child topology + takes - ChildkeyTake::::insert(owner_hot, net, PerU16::from_parts(1)); - PendingChildKeys::::insert(net, owner_cold, (vec![(1u64, owner_hot)], 1u64)); - ChildKeys::::insert(owner_cold, net, vec![(1u64, owner_hot)]); - ParentKeys::::insert(owner_hot, net, vec![(1u64, owner_cold)]); - - // Hotkey swap timestamp for subnet - LastHotkeySwapOnNetuid::::insert(net, owner_cold, 1u64); - - // Axon/prometheus tx key timing (NMap) — ***correct key-tuple insertion*** - TransactionKeyLastBlock::::insert((owner_hot, net, 1u16), 1u64); - - // EVM association indexed by (netuid, uid) - SubtensorModule::set_associated_evm_address(net, 0u16, sp_core::H160::zero(), 1u64); - - // (Optional) subnet -> lease link - SubnetUidToLeaseId::::insert(net, 42u32); - - // ------------------------------------------------------------------ - // Dissolve - // ------------------------------------------------------------------ - assert_ok!(SubtensorModule::do_dissolve_network(net)); - run_block_idle(); - - // ------------------------------------------------------------------ - // Items that must be COMPLETELY REMOVED - // ------------------------------------------------------------------ - assert!(!SubnetOwner::::contains_key(net)); - assert!(!SubnetOwnerHotkey::::contains_key(net)); - assert!(!SubnetworkN::::contains_key(net)); - assert!(!NetworksAdded::::contains_key(net)); - assert!(!NetworkRegisteredAt::::contains_key(net)); - - // Consensus vectors removed - assert!(!Active::::contains_key(net)); - assert!(!Emission::::contains_key(net)); - assert!(!Incentive::::contains_key(NetUidStorageIndex::from( - net - ))); - assert!(!Consensus::::contains_key(net)); - assert!(!Dividends::::contains_key(net)); - assert!(!LastUpdate::::contains_key(NetUidStorageIndex::from( - net - ))); - - assert!(!ValidatorPermit::::contains_key(net)); - assert!(!ValidatorTrust::::contains_key(net)); - - // Per‑net params removed - assert!(!Tempo::::contains_key(net)); - assert!(!Kappa::::contains_key(net)); - assert!(!Difficulty::::contains_key(net)); - - assert!(!MaxAllowedUids::::contains_key(net)); - assert!(!ImmunityPeriod::::contains_key(net)); - assert!(!ActivityCutoff::::contains_key(net)); - assert!(!MinAllowedWeights::::contains_key(net)); - - assert!(!RegistrationsThisInterval::::contains_key(net)); - assert!(!POWRegistrationsThisInterval::::contains_key(net)); - assert!(!BurnRegistrationsThisInterval::::contains_key(net)); - - // Pool / AMM counters removed - assert!(!SubnetTAO::::contains_key(net)); - assert!(!SubnetAlphaInEmission::::contains_key(net)); - assert!(!SubnetAlphaOutEmission::::contains_key(net)); - assert!(!SubnetTaoInEmission::::contains_key(net)); - assert!(!SubnetVolume::::contains_key(net)); - assert!(!pallet_subtensor_swap::BalancerTaoReservoir::::contains_key(net)); - assert!(!pallet_subtensor_swap::BalancerAlphaReservoir::::contains_key(net)); - - // TAO Flow - assert!(!SubnetTaoFlow::::contains_key(net)); - assert!(!SubnetEmaTaoFlow::::contains_key(net)); - - // These are now REMOVED - assert!(!SubnetAlphaIn::::contains_key(net)); - assert!(!SubnetAlphaOut::::contains_key(net)); - assert!(!SubnetProtocolAlpha::::contains_key(net)); - - // Collections fully cleared - assert!(Keys::::iter_prefix(net).next().is_none()); - assert!( - Bonds::::iter_prefix(NetUidStorageIndex::from(net)) - .next() - .is_none() - ); - assert!( - Weights::::iter_prefix(NetUidStorageIndex::from(net)) - .next() - .is_none() - ); - assert!(!IsNetworkMember::::contains_key(owner_hot, net)); - - // Token / price / provided reserves - assert!(!TokenSymbol::::contains_key(net)); - assert!(!SubnetMovingPrice::::contains_key(net)); - - // Subnet locks - assert!(!TransferToggle::::contains_key(net)); - assert!(!SubnetLocked::::contains_key(net)); - assert!(!LargestLocked::::contains_key(net)); - - // Subnet parameters & pending counters - assert!(!FirstEmissionBlockNumber::::contains_key(net)); - assert!(!SubnetMechanism::::contains_key(net)); - assert!(!NetworkRegistrationAllowed::::contains_key(net)); - assert!(!NetworkPowRegistrationAllowed::::contains_key(net)); - assert!(!PendingServerEmission::::contains_key(net)); - assert!(!PendingValidatorEmission::::contains_key(net)); - assert!(!PendingRootAlphaDivs::::contains_key(net)); - assert!(!PendingOwnerCut::::contains_key(net)); - assert!(!MinerBurned::::contains_key(net)); - assert!(!BlocksSinceLastStep::::contains_key(net)); - assert!(!LastMechansimStepBlock::::contains_key(net)); - assert!(!ServingRateLimit::::contains_key(net)); - assert!(!Rho::::contains_key(net)); - assert!(!AlphaSigmoidSteepness::::contains_key(net)); - - // Weights/versioning/targets/limits - assert!(!WeightsVersionKey::::contains_key(net)); - assert!(!MaxAllowedValidators::::contains_key(net)); - assert!(!BondsMovingAverage::::contains_key(net)); - assert!(!BondsPenalty::::contains_key(net)); - assert!(!BondsResetOn::::contains_key(net)); - assert!(!WeightsSetRateLimit::::contains_key(net)); - assert!(!ValidatorPruneLen::::contains_key(net)); - assert!(!ScalingLawPower::::contains_key(net)); - assert!(!TargetRegistrationsPerInterval::::contains_key(net)); - assert!(!CommitRevealWeightsEnabled::::contains_key(net)); - - // Burn/difficulty/adjustment - assert!(!Burn::::contains_key(net)); - assert!(!MinBurn::::contains_key(net)); - assert!(!MaxBurn::::contains_key(net)); - assert!(!MinDifficulty::::contains_key(net)); - assert!(!MaxDifficulty::::contains_key(net)); - assert!(!RegistrationsThisBlock::::contains_key(net)); - assert!(!EMAPriceHalvingBlocks::::contains_key(net)); - assert!(!RAORecycledForRegistration::::contains_key(net)); - - // Feature toggles - assert!(!LiquidAlphaOn::::contains_key(net)); - assert!(!Yuma3On::::contains_key(net)); - assert!(!AlphaValues::::contains_key(net)); - assert!(!SubtokenEnabled::::contains_key(net)); - assert!(!OwnerCutAutoLockEnabled::::contains_key(net)); - assert!(!ImmuneOwnerUidsLimit::::contains_key(net)); - - // Per‑subnet vectors / indexes - assert!(!StakeWeight::::contains_key(net)); - - // Uid/registration - assert!(Uids::::get(net, owner_hot).is_none()); - assert!(!BlockAtRegistration::::contains_key(net, 0u16)); - - // Per‑subnet dividends - assert!(!AlphaDividendsPerSubnet::::contains_key( - net, owner_hot - )); - - // Parent/child topology + takes - assert!(!ChildkeyTake::::contains_key(owner_hot, net)); - assert!(!PendingChildKeys::::contains_key(net, owner_cold)); - assert!(!ChildKeys::::contains_key(owner_cold, net)); - assert!(!ParentKeys::::contains_key(owner_hot, net)); - - // Hotkey swap timestamp for subnet - assert!(!LastHotkeySwapOnNetuid::::contains_key( - net, owner_cold - )); - - // Axon/prometheus tx key timing (NMap) — ValueQuery (defaults to 0) - assert_eq!( - TransactionKeyLastBlock::::get((owner_hot, net, 1u16)), - 0u64 - ); - - // EVM association - assert!(AssociatedEvmAddress::::get(net, 0u16).is_none()); - assert!(AssociatedUidsByEvmAddress::::get(net, sp_core::H160::zero()).is_empty()); - - // Subnet -> lease link - assert!(!SubnetUidToLeaseId::::contains_key(net)); - - // ------------------------------------------------------------------ - // Final subnet removal confirmation - // ------------------------------------------------------------------ - assert!(!SubtensorModule::if_subnet_exist(net)); - }); -} - -#[test] -fn dissolve_materializes_nonzero_protocol_reservoirs_before_cleanup() { - new_test_ext(0).execute_with(|| { - let owner_cold = U256::from(123); - let owner_hot = U256::from(456); - let net = add_dynamic_network(&owner_hot, &owner_cold); - remove_owner_registration_stake(net); - - // Force the modern dissolve branch where pool alpha participates in - // the protocol denominator. - TaoInRefundDeploymentBlock::::put(0); - NetworkRegisteredAt::::insert(net, 1); - - let reservoir_tao = TaoBalance::from(100_u64); - let reservoir_alpha = AlphaBalance::from(100_u64); - let staker_hot = U256::from(789); - let staker_cold = U256::from(987); - - let subnet_account = SubtensorModule::get_subnet_account_id(net).unwrap(); - add_balance_to_coldkey_account(&subnet_account, reservoir_tao); - - SubnetTAO::::insert(net, TaoBalance::ZERO); - SubtensorModule::set_subnet_locked_balance(net, TaoBalance::ZERO); - SubnetAlphaIn::::insert(net, AlphaBalance::ZERO); - SubnetProtocolAlpha::::insert(net, AlphaBalance::ZERO); - AlphaV2::::insert((staker_hot, staker_cold, net), sf_from_u64(100u64)); - TotalHotkeyAlpha::::insert(staker_hot, net, AlphaBalance::from(100u64)); - pallet_subtensor_swap::BalancerTaoReservoir::::insert(net, reservoir_tao); - pallet_subtensor_swap::BalancerAlphaReservoir::::insert(net, reservoir_alpha); - - let staker_before = SubtensorModule::get_coldkey_balance(&staker_cold); - let issuance_before = TotalIssuance::::get(); - - assert_ok!(SubtensorModule::do_dissolve_network(net)); - - // do_dissolve_network only queues the destructive cleanup, but it must - // materialize pending protocol reservoirs before the queued cleanup can - // compute stake payouts. - assert!(!pallet_subtensor_swap::BalancerTaoReservoir::::contains_key(net)); - assert!(!pallet_subtensor_swap::BalancerAlphaReservoir::::contains_key(net)); - assert_eq!(SubnetTAO::::get(net), reservoir_tao); - assert_eq!(SubnetAlphaIn::::get(net), reservoir_alpha); - assert_eq!( - SubtensorModule::get_coldkey_balance(&staker_cold), - staker_before - ); - - run_block_idle(); - - // Reservoir alpha is treated like materialized protocol pool alpha. - // The staker owns half the denominator, so receives half the reservoir - // TAO pot; the protocol share is recycled. - assert_eq!( - SubtensorModule::get_coldkey_balance(&staker_cold), - staker_before + TaoBalance::from(50_u64) - ); - assert!(TotalIssuance::::get() < issuance_before); - assert!(!NetworksAdded::::contains_key(net)); - assert!(!SubnetOwner::::contains_key(net)); - assert!(!SubnetAlphaIn::::contains_key(net)); - assert!(!SubnetProtocolAlpha::::contains_key(net)); - assert!(!pallet_subtensor_swap::BalancerTaoReservoir::::contains_key(net)); - assert!(!pallet_subtensor_swap::BalancerAlphaReservoir::::contains_key(net)); - }); -} - -#[test] -fn dissolve_alpha_out_but_zero_tao_no_rewards() { - new_test_ext(0).execute_with(|| { - let oc = U256::from(21); - let oh = U256::from(22); - let net = add_dynamic_network(&oh, &oc); - - let sh = U256::from(23); - let sc = U256::from(24); - - AlphaV2::::insert((sh, sc, net), sf_from_u64(1_000u64)); - SubnetTAO::::insert(net, TaoBalance::from(0)); // zero TAO - SubtensorModule::set_subnet_locked_balance(net, TaoBalance::from(0)); - Emission::::insert(net, Vec::::new()); - TotalHotkeyAlpha::::insert(sh, net, AlphaBalance::from(1_000u64)); - - let before = SubtensorModule::get_coldkey_balance(&sc); - assert_ok!(SubtensorModule::do_dissolve_network(net)); - run_block_idle(); - let after = SubtensorModule::get_coldkey_balance(&sc); - - // No reward distributed, α-out cleared. - assert_eq!(after, before); - assert!(AlphaV2::::iter().next().is_none()); - }); -} - -#[test] -fn dissolve_decrements_total_networks() { - new_test_ext(0).execute_with(|| { - let total_before = TotalNetworks::::get(); - - let cold = U256::from(41); - let hot = U256::from(42); - let net = add_dynamic_network(&hot, &cold); - - // Add 100 TAO to subnet account (lock) - let subnet_account = SubtensorModule::get_subnet_account_id(net).unwrap(); - add_balance_to_coldkey_account(&subnet_account, 100_000_000_000_u64.into()); - - // Sanity: adding network increments the counter. - assert_eq!(TotalNetworks::::get(), total_before + 1); - - assert_ok!(SubtensorModule::do_dissolve_network(net)); - assert_eq!(TotalNetworks::::get(), total_before); - }); -} - -#[test] -fn dissolve_rounding_remainder_distribution() { - new_test_ext(0).execute_with(|| { - // 1. Build subnet with two α-out stakers (3 & 2 α) - let oc = U256::from(61); - let oh = U256::from(62); - let net = add_dynamic_network(&oh, &oc); - remove_owner_registration_stake(net); - SubnetAlphaIn::::insert(net, AlphaBalance::ZERO); - SubnetProtocolAlpha::::insert(net, AlphaBalance::ZERO); - - let (s1h, s1c) = (U256::from(63), U256::from(64)); - let (s2h, s2c) = (U256::from(65), U256::from(66)); - - AlphaV2::::insert((s1h, s1c, net), sf_from_u64(3u64)); - AlphaV2::::insert((s2h, s2c, net), sf_from_u64(2u64)); - - SubnetTAO::::insert(net, TaoBalance::from(1)); // TAO pot = 1 - SubtensorModule::set_subnet_locked_balance(net, TaoBalance::from(0)); - - TotalHotkeyAlpha::::insert(s1h, net, AlphaBalance::from(3u64)); - TotalHotkeyAlpha::::insert(s2h, net, AlphaBalance::from(2u64)); - - // Cold-key balances before - let c1_before = SubtensorModule::get_coldkey_balance(&s1c); - let c2_before = SubtensorModule::get_coldkey_balance(&s2c); - - // 3. Run full dissolve flow - assert_ok!(SubtensorModule::do_dissolve_network(net)); - run_block_idle(); - - // 4. s1 (larger remainder) should get +1 τ on cold-key - let c1_after = SubtensorModule::get_coldkey_balance(&s1c); - let c2_after = SubtensorModule::get_coldkey_balance(&s2c); - - assert_eq!(c1_after, c1_before + 1.into()); - assert_eq!(c2_after, c2_before); - - // α records for subnet gone; TAO key gone - assert!(AlphaV2::::iter().all(|((_h, _c, n), _)| n != net)); - assert!(!SubnetTAO::::contains_key(net)); - }); -} - -#[test] -fn dissolve_protocol_alpha_share_is_not_paid_to_users() { - new_test_ext(0).execute_with(|| { - let owner_cold = U256::from(610); - let owner_hot = U256::from(620); - let net = add_dynamic_network(&owner_hot, &owner_cold); - remove_owner_registration_stake(net); - - // Make this subnet pre-deploy for protocol-alpha accounting. - let reg_at = NetworkRegisteredAt::::get(net); - TaoInRefundDeploymentBlock::::put(reg_at.saturating_add(1)); - SubtensorModule::set_subnet_locked_balance(net, TaoBalance::ZERO); - - // Alpha-in is the AMM pool reserve and must NOT participate in the - // deregistration settlement for pre-deploy subnets. Only the chain-bought - // cached protocol - // alpha is converted to TAO pro-rata, exactly like every staker's alpha. - SubnetAlphaIn::::insert(net, AlphaBalance::from(100u64)); - SubnetProtocolAlpha::::insert(net, AlphaBalance::from(50u64)); - - let staker_hot = U256::from(630); - let staker_cold = U256::from(640); - AlphaV2::::insert((staker_hot, staker_cold, net), sf_from_u64(50u64)); - TotalHotkeyAlpha::::insert(staker_hot, net, AlphaBalance::from(50u64)); - - let pot: u64 = 200; - SubnetTAO::::insert(net, TaoBalance::from(pot)); - - let staker_before = SubtensorModule::get_coldkey_balance(&staker_cold); - let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); - - assert_ok!(SubtensorModule::do_dissolve_network(net)); - - run_block_idle(); - - // User gets 50 / (100 alpha-in + 50 cached protocol alpha + 50 user alpha) - // of the TAO pot. The protocol share is withheld from user/owner payout. - // Settlement denominator = 50 cached protocol alpha + 50 user alpha = 100 - // (alpha-in is excluded). The user therefore gets 50/100 of the 200 TAO pot, - // i.e. 100 TAO. The chain-bought alpha's 100 TAO share is withheld from the - // user/owner payout (it is recycled back to the chain, see the dedicated - // recycling test below). - assert_eq!( - SubtensorModule::get_coldkey_balance(&staker_cold), - staker_before + 100.into() - ); - // The owner is not paid the protocol share either (locked balance is zero, so - // there is no refund path that could leak it). - assert_eq!( - SubtensorModule::get_coldkey_balance(&owner_cold), - owner_before - ); - assert!(!SubnetProtocolAlpha::::contains_key(net)); - }); -} - -#[test] -fn dissolve_protocol_alpha_post_deploy_includes_alpha_in() { - new_test_ext(0).execute_with(|| { - let owner_cold = U256::from(611); - let owner_hot = U256::from(621); - - let net = add_dynamic_network(&owner_hot, &owner_cold); - remove_owner_registration_stake(net); - - // Make this subnet post-deploy for protocol-alpha accounting. - TaoInRefundDeploymentBlock::::put(100); - NetworkRegisteredAt::::insert(net, 101); - - SubtensorModule::set_subnet_locked_balance(net, TaoBalance::ZERO); - - SubnetAlphaIn::::insert(net, AlphaBalance::from(100u64)); - SubnetProtocolAlpha::::insert(net, AlphaBalance::from(50u64)); - - let staker_hot = U256::from(631); - let staker_cold = U256::from(641); - - AlphaV2::::insert((staker_hot, staker_cold, net), sf_from_u64(50u64)); - TotalHotkeyAlpha::::insert(staker_hot, net, AlphaBalance::from(50u64)); - - let pot: u64 = 200; - SubnetTAO::::insert(net, TaoBalance::from(pot)); - - let staker_before = SubtensorModule::get_coldkey_balance(&staker_cold); - let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); - - assert_ok!(SubtensorModule::do_dissolve_network(net)); - run_block_idle(); - - // Post-deploy denominator = 100 alpha-in + 50 cached protocol alpha - // + 50 user alpha = 200. The user gets 50/200 of the 200 TAO pot. - assert_eq!( - SubtensorModule::get_coldkey_balance(&staker_cold), - staker_before + 50.into() - ); - - assert_eq!( - SubtensorModule::get_coldkey_balance(&owner_cold), - owner_before - ); - - assert!(!SubnetProtocolAlpha::::contains_key(net)); - }); -} -#[test] -fn dissolve_chain_bought_alpha_is_converted_to_tao_and_recycled() { - new_test_ext(0).execute_with(|| { - let owner_cold = U256::from(710); - let owner_hot = U256::from(720); - let net = add_dynamic_network(&owner_hot, &owner_cold); - remove_owner_registration_stake(net); - - // Make this subnet pre-deploy for protocol-alpha accounting. - let reg_at = NetworkRegisteredAt::::get(net); - TaoInRefundDeploymentBlock::::put(reg_at.saturating_add(1)); - // No owner refund path: any TAO left on the subnet account is recycled. - SubtensorModule::set_subnet_locked_balance(net, TaoBalance::ZERO); - - // Alpha-in is present but ignored on the pre-deploy branch. The cached - // protocol alpha is the only claimant, so the entire pot is recycled. - SubnetAlphaIn::::insert(net, AlphaBalance::from(123u64)); - SubnetProtocolAlpha::::insert(net, AlphaBalance::from(100u64)); - - let pot: u64 = 100; - SubnetTAO::::insert(net, TaoBalance::from(pot)); - - let issuance_before = TotalIssuance::::get(); - let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); - - assert_ok!(SubtensorModule::do_dissolve_network(net)); - run_block_idle(); - - // There are no stakers, so the entire pot is the chain-bought alpha's TAO - // share. It is not paid to the owner; instead it is recycled back to the - // chain, which removes it from existence and reduces total issuance. - assert_eq!( - SubtensorModule::get_coldkey_balance(&owner_cold), - owner_before - ); - assert!( - TotalIssuance::::get() < issuance_before, - "recycling the chain-bought alpha's TAO must reduce total issuance" - ); - assert!(!SubnetProtocolAlpha::::contains_key(net)); - }); -} - -#[test] -fn destroy_alpha_out_multiple_stakers_pro_rata() { - new_test_ext(0).execute_with(|| { - // 1. Owner & subnet - let owner_cold = U256::from(10); - let owner_hot = U256::from(20); - let netuid = add_dynamic_network(&owner_hot, &owner_cold); - remove_owner_registration_stake(netuid); - - // Mark this subnet as *legacy* so owner refund path is enabled. - let reg_at = NetworkRegisteredAt::::get(netuid); - NetworkRegistrationStartBlock::::put(reg_at.saturating_add(1)); - - // 2. Two stakers on that subnet - let (c1, h1) = (U256::from(111), U256::from(211)); - let (c2, h2) = (U256::from(222), U256::from(333)); - register_ok_neuron(netuid, h1, c1, 0); - register_ok_neuron(netuid, h2, c2, 0); - - // 3. Stake 30 : 70 (s1 : s2) in TAO - let min_total = DefaultMinStake::::get(); - let min_total_u64: u64 = min_total.into(); - let s1: u64 = 3u64 * min_total_u64; - let s2: u64 = 7u64 * min_total_u64; - - add_balance_to_coldkey_account(&c1, (s1 + 50_000).into()); - add_balance_to_coldkey_account(&c2, (s2 + 50_000).into()); - - assert_ok!(SubtensorModule::do_add_stake( - RuntimeOrigin::signed(c1), - h1, - netuid, - s1.into() - )); - assert_ok!(SubtensorModule::do_add_stake( - RuntimeOrigin::signed(c2), - h2, - netuid, - s2.into() - )); - - // 4. α-out snapshot - - SubnetAlphaIn::::insert(netuid, AlphaBalance::ZERO); - SubnetProtocolAlpha::::insert(netuid, AlphaBalance::ZERO); - let a1: u128 = sf_to_u128(&AlphaV2::::get((h1, c1, netuid))); - let a2: u128 = sf_to_u128(&AlphaV2::::get((h2, c2, netuid))); - let atotal = a1 + a2; - - // 5. TAO pot & lock - let tao_pot: u64 = 10_000; - SubnetTAO::::insert(netuid, TaoBalance::from(tao_pot)); - SubtensorModule::set_subnet_locked_balance(netuid, TaoBalance::from(5_000)); - - // 6. Balances before - let c1_before = SubtensorModule::get_coldkey_balance(&c1); - let c2_before = SubtensorModule::get_coldkey_balance(&c2); - let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); - - // 7. Run the (now credit-to-coldkey) logic - destroy_alpha_in_out_stakes_full_pipeline_for_test(netuid); - - // 8. Expected τ shares via largest remainder - let prod1 = (tao_pot as u128) * a1; - let prod2 = (tao_pot as u128) * a2; - let mut s1_share = (prod1 / atotal) as u64; - let mut s2_share = (prod2 / atotal) as u64; - let distributed = s1_share + s2_share; - if distributed < tao_pot { - // Assign leftover to larger remainder - let r1 = prod1 % atotal; - let r2 = prod2 % atotal; - if r1 >= r2 { - s1_share += 1; - } else { - s2_share += 1; - } - } - - // 9. Cold-key balances must have increased accordingly - assert_eq!( - SubtensorModule::get_coldkey_balance(&c1), - c1_before + s1_share.into() - ); - assert_eq!( - SubtensorModule::get_coldkey_balance(&c2), - c2_before + s2_share.into() - ); - - // 10. Owner refund (5 000 τ) to cold-key (no emission) - assert_eq!( - SubtensorModule::get_coldkey_balance(&owner_cold), - owner_before + 5_000.into() - ); - - // 11. α entries cleared for the subnet - assert!(!AlphaV2::::contains_key((h1, c1, netuid))); - assert!(!AlphaV2::::contains_key((h2, c2, netuid))); - }); -} - -#[test] -fn destroy_alpha_in_out_stakes_cleans_locking_coldkeys() { - new_test_ext(0).execute_with(|| { - let owner_cold = U256::from(10); - let owner_hot = U256::from(20); - let netuid = add_dynamic_network(&owner_hot, &owner_cold); - remove_owner_registration_stake(netuid); - - let coldkey = U256::from(111); - let hotkey = U256::from(222); - let other_netuid = NetUid::from(u16::from(netuid) + 1); - let lock = LockState { - locked_mass: 10u64.into(), - conviction: U64F64::from_num(1), - last_update: 1, - }; - - Lock::::insert((coldkey, netuid, hotkey), lock.clone()); - LockingColdkeys::::insert((netuid, hotkey, coldkey), ()); - Lock::::insert((coldkey, other_netuid, hotkey), lock); - LockingColdkeys::::insert((other_netuid, hotkey, coldkey), ()); - - DissolveCleanupQueue::::set(vec![netuid]); - run_block_idle(); - - assert!(!Lock::::contains_key((coldkey, netuid, hotkey))); - assert!(!LockingColdkeys::::contains_key(( - netuid, hotkey, coldkey - ))); - assert!(Lock::::contains_key((coldkey, other_netuid, hotkey))); - assert!(LockingColdkeys::::contains_key(( - other_netuid, - hotkey, - coldkey - ))); - }); -} - -#[test] -fn destroy_alpha_in_out_stakes_cleans_all_lock_aggregates() { - new_test_ext(0).execute_with(|| { - let owner_cold = U256::from(10); - let owner_hot = U256::from(20); - let netuid = add_dynamic_network(&owner_hot, &owner_cold); - remove_owner_registration_stake(netuid); - - let coldkey = U256::from(111); - let hotkey = U256::from(222); - let other_netuid = NetUid::from(u16::from(netuid) + 1); - let lock = LockState { - locked_mass: 10u64.into(), - conviction: U64F64::from_num(1), - last_update: 1, - }; - - HotkeyLock::::insert(netuid, hotkey, lock.clone()); - DecayingHotkeyLock::::insert(netuid, hotkey, lock.clone()); - OwnerLock::::insert(netuid, lock.clone()); - DecayingOwnerLock::::insert(netuid, lock.clone()); - DecayingLock::::insert(coldkey, netuid, false); - - HotkeyLock::::insert(other_netuid, hotkey, lock.clone()); - DecayingHotkeyLock::::insert(other_netuid, hotkey, lock.clone()); - OwnerLock::::insert(other_netuid, lock.clone()); - DecayingOwnerLock::::insert(other_netuid, lock); - DecayingLock::::insert(coldkey, other_netuid, false); - - DissolveCleanupQueue::::set(vec![netuid]); - run_block_idle(); - - assert!(!HotkeyLock::::contains_key(netuid, hotkey)); - assert!(!DecayingHotkeyLock::::contains_key(netuid, hotkey)); - assert!(!OwnerLock::::contains_key(netuid)); - assert!(!DecayingOwnerLock::::contains_key(netuid)); - assert!(!DecayingLock::::contains_key(coldkey, netuid)); - - assert!(HotkeyLock::::contains_key(other_netuid, hotkey)); - assert!(DecayingHotkeyLock::::contains_key( - other_netuid, - hotkey - )); - assert!(OwnerLock::::contains_key(other_netuid)); - assert!(DecayingOwnerLock::::contains_key(other_netuid)); - assert!(DecayingLock::::contains_key(coldkey, other_netuid)); - }); -} - -#[allow(clippy::indexing_slicing)] -#[test] -fn destroy_alpha_out_many_stakers_complex_distribution() { - new_test_ext(0).execute_with(|| { - // ── 1) create subnet with 20 stakers ──────────────────────────────── - let owner_cold = U256::from(1_000); - let owner_hot = U256::from(2_000); - let netuid = add_dynamic_network(&owner_hot, &owner_cold); - remove_owner_registration_stake(netuid); - SubtensorModule::set_max_registrations_per_block(netuid, 1_000u16); - SubtensorModule::set_target_registrations_per_interval(netuid, 1_000u16); - - // Mark this subnet as *legacy* so owner refund path is enabled. - let reg_at = NetworkRegisteredAt::::get(netuid); - NetworkRegistrationStartBlock::::put(reg_at.saturating_add(1)); - - // Runtime-exact min amount = min_stake + fee - let min_amount = { - let min_stake = DefaultMinStake::::get(); - let fee = ::SwapInterface::approx_fee_amount( - netuid.into(), - min_stake, - ); - // Double the fees because fee is calculated for min_stake, not for min_amount - min_stake + fee * 2.into() - }; - - const N: usize = 20; - let mut cold = [U256::zero(); N]; - let mut hot = [U256::zero(); N]; - let mut stake = [0u64; N]; - - let min_amount_u64: u64 = min_amount.into(); - for i in 0..N { - cold[i] = U256::from(10_000 + 2 * i as u32); - hot[i] = U256::from(10_001 + 2 * i as u32); - stake[i] = (i as u64 + 1u64) * min_amount_u64; // multiples of min_amount - - register_ok_neuron(netuid, hot[i], cold[i], 0); - add_balance_to_coldkey_account(&cold[i], (stake[i] + 100_000).into()); - - assert_ok!(SubtensorModule::do_add_stake( - RuntimeOrigin::signed(cold[i]), - hot[i], - netuid, - stake[i].into() - )); - } - - // ── 2) α-out snapshot ─────────────────────────────────────────────── - let mut alpha = [0u128; N]; - let mut alpha_sum: u128 = 0; - for i in 0..N { - alpha[i] = sf_to_u128(&AlphaV2::::get((hot[i], cold[i], netuid))); - alpha_sum += alpha[i]; - } - - // ── 3) TAO pot & subnet lock ──────────────────────────────────────── - let tao_pot: u64 = 123_456; - let lock: u64 = 30_000; - SubnetTAO::::insert(netuid, TaoBalance::from(tao_pot)); - SubtensorModule::set_subnet_locked_balance(netuid, TaoBalance::from(lock)); - - // ensure there was some Alpha issued - assert!(SubtensorModule::get_alpha_issuance(netuid).to_u64() > 0); - - // Owner already earned some emission; owner-cut = 50 % - SubnetOwnerCut::::put(32_768u16); // ~ 0.5 in fixed-point - - // ── 4) balances before ────────────────────────────────────────────── - let mut bal_before = [TaoBalance::new(0); N]; - for i in 0..N { - bal_before[i] = SubtensorModule::get_coldkey_balance(&cold[i]); - } - let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); - - // ── 5) expected τ share per pallet algorithm (incl. remainder) ───── - - SubnetAlphaIn::::insert(netuid, AlphaBalance::ZERO); - SubnetProtocolAlpha::::insert(netuid, AlphaBalance::ZERO); - let mut share = [0u64; N]; - let mut rem = [0u128; N]; - let mut paid: u128 = 0; - - for i in 0..N { - let prod = tao_pot as u128 * alpha[i]; - share[i] = (prod / alpha_sum) as u64; - rem[i] = prod % alpha_sum; - paid += share[i] as u128; - } - let leftover = tao_pot as u128 - paid; - let mut idx: Vec<_> = (0..N).collect(); - idx.sort_by_key(|i| core::cmp::Reverse(rem[*i])); - for i in 0..leftover as usize { - share[idx[i]] += 1; - } - - // ── 5b) expected owner refund with price-aware emission deduction ─── - let frac: U96F32 = SubtensorModule::get_float_subnet_owner_cut(); - let total_emitted_alpha: u64 = SubtensorModule::get_alpha_issuance(netuid).to_u64(); - let owner_alpha_u64: u64 = U96F32::from_num(total_emitted_alpha) - .saturating_mul(frac) - .floor() - .saturating_to_num::(); - - let owner_emission_tao: u64 = { - // Fallback matches the pallet's fallback - let price: U96F32 = U96F32::from_num( - ::SwapInterface::current_alpha_price(netuid.into()), - ); - U96F32::from_num(owner_alpha_u64) - .saturating_mul(price) - .floor() - .saturating_to_num::() - }; - - let expected_refund = lock.saturating_sub(owner_emission_tao); - - // ── 6) run distribution (credits τ to coldkeys, wipes α state) ───── - destroy_alpha_in_out_stakes_full_pipeline_for_test(netuid); - - // ── 7) post checks ────────────────────────────────────────────────── - for i in 0..N { - // cold-key balances increased by expected τ share - assert_eq!( - SubtensorModule::get_coldkey_balance(&cold[i]), - bal_before[i] + share[i].into(), - "staker {i} cold-key balance changed unexpectedly" - ); - } - - // owner refund - assert_eq!( - SubtensorModule::get_coldkey_balance(&owner_cold), - owner_before + expected_refund.into() - ); - - // α cleared for dissolved subnet & related counters reset - assert!(AlphaV2::::iter().all(|((_h, _c, n), _)| n != netuid)); - assert_eq!(SubnetAlphaIn::::get(netuid), 0.into()); - assert_eq!(SubnetAlphaOut::::get(netuid), 0.into()); - assert_eq!(SubtensorModule::get_subnet_locked_balance(netuid), 0.into()); - }); -} - -#[test] -fn destroy_alpha_out_refund_gating_by_registration_block() { - // ────────────────────────────────────────────────────────────────────── - // Case A: LEGACY subnet → refund applied - // ────────────────────────────────────────────────────────────────────── - new_test_ext(0).execute_with(|| { - // Owner + subnet - let owner_cold = U256::from(10_000); - let owner_hot = U256::from(20_000); - let netuid = add_dynamic_network(&owner_hot, &owner_cold); - remove_owner_registration_stake(netuid); - - // Mark as *legacy*: registered_at < start_block - let reg_at = NetworkRegisteredAt::::get(netuid); - NetworkRegistrationStartBlock::::put(reg_at.saturating_add(1)); - - // Lock and (nonzero) emissions - let lock_u64: u64 = 50_000; - SubtensorModule::set_subnet_locked_balance(netuid, TaoBalance::from(lock_u64)); - // Owner cut ≈ 50% - SubnetOwnerCut::::put(32_768u16); - - // give some stake to other key - let other_cold = U256::from(1_234); - let other_hot = U256::from(2_345); - mock_increase_stake_for_hotkey_and_coldkey_on_subnet( - &other_hot, - &other_cold, - netuid, - AlphaBalance::from(30u64), // not nearly enough to cover the lock - ); - - // ensure there was some Alpha issued - assert!(SubtensorModule::get_alpha_issuance(netuid).to_u64() > 0); - - // Compute expected refund using the same math as the pallet - let frac: U96F32 = SubtensorModule::get_float_subnet_owner_cut(); - let total_emitted_alpha: u64 = SubtensorModule::get_alpha_issuance(netuid).to_u64(); - let owner_alpha_u64: u64 = U96F32::from_num(total_emitted_alpha) - .saturating_mul(frac) - .floor() - .saturating_to_num::(); - - let owner_emission_tao_u64 = { - let price: U96F32 = U96F32::from_num( - ::SwapInterface::current_alpha_price(netuid.into()), - ); - U96F32::from_num(owner_alpha_u64) - .saturating_mul(price) - .floor() - .saturating_to_num::() - }; - - let expected_refund: u64 = lock_u64.saturating_sub(owner_emission_tao_u64); - - // Balances before - let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); - - // Run the path under test - let mut weight_meter = - frame_support::weights::WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)); - // total alpha tracked in CurrentDissolveCleanupStatus; - // distributed tao tracked in CurrentDissolveCleanupStatus; - { - let mut status = dissolve_cleanup_status(netuid); - status.subnet_total_alpha_value = Some(0); - SubtensorModule::destroy_alpha_in_out_stakes(netuid, &mut weight_meter, &mut status); - } - - // Owner received their refund… - let owner_after = SubtensorModule::get_coldkey_balance(&owner_cold); - assert_eq!(owner_after, owner_before + expected_refund.into()); - - // …and the lock is always cleared to zero by destroy_alpha_in_out_stakes. - assert_eq!( - SubtensorModule::get_subnet_locked_balance(netuid), - TaoBalance::from(0u64) - ); - }); - - // ────────────────────────────────────────────────────────────────────── - // Case B: NON‑LEGACY subnet → NO refund; - // ────────────────────────────────────────────────────────────────────── - new_test_ext(0).execute_with(|| { - // Owner + subnet - let owner_cold = U256::from(1_111); - let owner_hot = U256::from(2_222); - let netuid = add_dynamic_network(&owner_hot, &owner_cold); - remove_owner_registration_stake(netuid); - - // Explicitly set start_block <= registered_at to make it non‑legacy. - let reg_at = NetworkRegisteredAt::::get(netuid); - NetworkRegistrationStartBlock::::put(reg_at); - - // Lock and emissions present (should be ignored for refund) - let lock_u64: u64 = 42_000; - SubtensorModule::set_subnet_locked_balance(netuid, TaoBalance::from(lock_u64)); - // give some stake to other key - let other_cold = U256::from(1_234); - let other_hot = U256::from(2_345); - mock_increase_stake_for_hotkey_and_coldkey_on_subnet( - &other_hot, - &other_cold, - netuid, - AlphaBalance::from(300u64), // not nearly enough to cover the lock - ); - // ensure there was some Alpha issued - assert!(SubtensorModule::get_alpha_issuance(netuid).to_u64() > 0); - SubnetOwnerCut::::put(32_768u16); // ~50% - - // Balances before - let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); - - // Run the path under test - let mut weight_meter = - frame_support::weights::WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)); - // total alpha tracked in CurrentDissolveCleanupStatus; - // distributed tao tracked in CurrentDissolveCleanupStatus; - { - let mut status = dissolve_cleanup_status(netuid); - status.subnet_total_alpha_value = Some(0); - SubtensorModule::destroy_alpha_in_out_stakes(netuid, &mut weight_meter, &mut status); - } - - // No refund for non‑legacy - let owner_after = SubtensorModule::get_coldkey_balance(&owner_cold); - assert_eq!(owner_after, owner_before); - - // Lock is still cleared to zero by the routine - assert_eq!( - SubtensorModule::get_subnet_locked_balance(netuid), - TaoBalance::from(0u64) - ); - }); - - // ────────────────────────────────────────────────────────────────────── - // Case C: LEGACY subnet but lock = 0 → no refund; - // ────────────────────────────────────────────────────────────────────── - new_test_ext(0).execute_with(|| { - // Owner + subnet - let owner_cold = U256::from(9_999); - let owner_hot = U256::from(8_888); - let netuid = add_dynamic_network(&owner_hot, &owner_cold); - remove_owner_registration_stake(netuid); - - // Mark as *legacy* - let reg_at = NetworkRegisteredAt::::get(netuid); - NetworkRegistrationStartBlock::::put(reg_at.saturating_add(1)); - - // lock = 0; emissions present (must not matter) - SubtensorModule::set_subnet_locked_balance(netuid, TaoBalance::from(0u64)); - SubnetAlphaOut::::insert(netuid, AlphaBalance::from(10_000)); - // ensure there was some Alpha issued - assert!(SubtensorModule::get_alpha_issuance(netuid).to_u64() > 0); - SubnetOwnerCut::::put(32_768u16); // ~50% - - let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); - let mut weight_meter = - frame_support::weights::WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)); - { - let mut status = dissolve_cleanup_status(netuid); - status.subnet_total_alpha_value = Some(0); - SubtensorModule::destroy_alpha_in_out_stakes(netuid, &mut weight_meter, &mut status); - } - let owner_after = SubtensorModule::get_coldkey_balance(&owner_cold); - - // No refund possible when lock = 0 - assert_eq!(owner_after, owner_before); - assert_eq!( - SubtensorModule::get_subnet_locked_balance(netuid), - TaoBalance::from(0u64) - ); - }); -} - -#[test] -fn prune_none_with_no_networks() { - new_test_ext(0).execute_with(|| { - assert_eq!(SubtensorModule::get_network_to_prune(), None); - }); -} - -#[test] -fn prune_none_when_all_networks_immune() { - new_test_ext(0).execute_with(|| { - // two fresh networks → still inside immunity window - let n1 = add_dynamic_network(&U256::from(2), &U256::from(1)); - let _n2 = add_dynamic_network(&U256::from(4), &U256::from(3)); - - // emissions don’t matter while immune - Emission::::insert(n1, vec![AlphaBalance::from(10)]); - - assert_eq!(SubtensorModule::get_network_to_prune(), None); - }); -} - -#[test] -fn prune_selects_network_with_lowest_price() { - new_test_ext(0).execute_with(|| { - let n1 = add_dynamic_network(&U256::from(20), &U256::from(10)); - let n2 = add_dynamic_network(&U256::from(40), &U256::from(30)); - - // make both networks eligible (past immunity) - let imm = SubtensorModule::get_network_immunity_period(); - System::set_block_number(imm + 10); - - // n1 has lower price → should be pruned - SubnetMovingPrice::::insert(n1, I96F32::from_num(1)); - SubnetMovingPrice::::insert(n2, I96F32::from_num(10)); - - assert_eq!(SubtensorModule::get_network_to_prune(), Some(n1)); - }); -} - -#[test] -fn prune_ignores_immune_network_even_if_lower_price() { - new_test_ext(0).execute_with(|| { - // create mature network n1 first - let n1 = add_dynamic_network(&U256::from(22), &U256::from(11)); - - let imm = SubtensorModule::get_network_immunity_period(); - System::set_block_number(imm + 5); // advance → n1 now mature - - // create second network n2 *inside* immunity - let n2 = add_dynamic_network(&U256::from(44), &U256::from(33)); - - // prices: n2 lower but immune; n1 must be selected - SubnetMovingPrice::::insert(n1, I96F32::from_num(5)); - SubnetMovingPrice::::insert(n2, I96F32::from_num(1)); - - System::set_block_number(imm + 10); // still immune for n2 - assert_eq!(SubtensorModule::get_network_to_prune(), Some(n1)); - }); -} - -#[test] -fn prune_tie_on_price_earlier_registration_wins() { - new_test_ext(0).execute_with(|| { - // n1 registered first - let n1 = add_dynamic_network(&U256::from(66), &U256::from(55)); - - // advance 1 block, then register n2 (later timestamp) - System::set_block_number(1); - let n2 = add_dynamic_network(&U256::from(88), &U256::from(77)); - - // push past immunity for both - let imm = SubtensorModule::get_network_immunity_period(); - System::set_block_number(imm + 20); - - // identical prices → tie; earlier (n1) must be chosen - SubnetMovingPrice::::insert(n1, I96F32::from_num(7)); - SubnetMovingPrice::::insert(n2, I96F32::from_num(7)); - - assert_eq!(SubtensorModule::get_network_to_prune(), Some(n1)); - }); -} - -#[test] -fn prune_selection_complex_state_exhaustive() { - new_test_ext(0).execute_with(|| { - let imm = SubtensorModule::get_network_immunity_period(); - - // --------------------------------------------------------------------- - // Build a rich topology of networks with controlled registration times. - // --------------------------------------------------------------------- - // n1 + n2 in the same block (equal timestamp) to test "tie + same time". - System::set_block_number(0); - let n1 = add_dynamic_network(&U256::from(101), &U256::from(201)); - let n2 = add_dynamic_network(&U256::from(102), &U256::from(202)); // same registered_at as n1 - - // Later registrations (strictly greater timestamp than n1/n2) - System::set_block_number(1); - let n3 = add_dynamic_network(&U256::from(103), &U256::from(203)); - - System::set_block_number(2); - let n4 = add_dynamic_network(&U256::from(104), &U256::from(204)); - - // Create *immune* networks that will remain ineligible initially, - // even if their price is the lowest. - System::set_block_number(imm + 5); - let n5 = add_dynamic_network(&U256::from(105), &U256::from(205)); // immune at first - - System::set_block_number(imm + 6); - let n6 = add_dynamic_network(&U256::from(106), &U256::from(206)); // immune at first - - // Add 100 TAO to subnet accounts (lock) - let subnet_account1 = SubtensorModule::get_subnet_account_id(n1).unwrap(); - let subnet_account2 = SubtensorModule::get_subnet_account_id(n2).unwrap(); - let subnet_account3 = SubtensorModule::get_subnet_account_id(n3).unwrap(); - let subnet_account4 = SubtensorModule::get_subnet_account_id(n4).unwrap(); - let subnet_account5 = SubtensorModule::get_subnet_account_id(n5).unwrap(); - let subnet_account6 = SubtensorModule::get_subnet_account_id(n6).unwrap(); - add_balance_to_coldkey_account(&subnet_account1, 100_000_000_000_u64.into()); - add_balance_to_coldkey_account(&subnet_account2, 100_000_000_000_u64.into()); - add_balance_to_coldkey_account(&subnet_account3, 100_000_000_000_u64.into()); - add_balance_to_coldkey_account(&subnet_account4, 100_000_000_000_u64.into()); - add_balance_to_coldkey_account(&subnet_account5, 100_000_000_000_u64.into()); - add_balance_to_coldkey_account(&subnet_account6, 100_000_000_000_u64.into()); - - // (Root is ignored by the selector.) - let root = NetUid::ROOT; - - // --------------------------------------------------------------------- - // Drive pruning via the EMA/moving price used by `get_network_to_prune()`. - // We set the moving prices directly to create deterministic selections. - // - // Intended prices: - // n1: 25, n2: 25, n3: 100, n4: 1, n5: 0 (immune initially), n6: 0 (immune initially) - // --------------------------------------------------------------------- - SubnetMovingPrice::::insert(n1, I96F32::from_num(25)); - SubnetMovingPrice::::insert(n2, I96F32::from_num(25)); - SubnetMovingPrice::::insert(n3, I96F32::from_num(100)); - SubnetMovingPrice::::insert(n4, I96F32::from_num(1)); - SubnetMovingPrice::::insert(n5, I96F32::from_num(0)); - SubnetMovingPrice::::insert(n6, I96F32::from_num(0)); - - // --------------------------------------------------------------------- - // Phase A: Only n1..n4 are mature → lowest price (n4=1) should win. - // --------------------------------------------------------------------- - System::set_block_number(imm + 10); - assert_eq!( - SubtensorModule::get_network_to_prune(), - Some(n4), - "Among mature nets (n1..n4), n4 has price=1 (lowest) and should be chosen." - ); - - // --------------------------------------------------------------------- - // Phase B: Tie on price with *same registration time* (n1 vs n2). - // Raise n4's price to 25 so {n1=25, n2=25, n3=100, n4=25}. - // n1 and n2 share the *same registered_at*. The tie should keep the - // first encountered (stable iteration by key order) → n1. - // --------------------------------------------------------------------- - SubnetMovingPrice::::insert(n4, I96F32::from_num(25)); // n4 now 25 - assert_eq!( - SubtensorModule::get_network_to_prune(), - Some(n1), - "Tie on price with equal timestamps (n1,n2) → first encountered (n1) should persist." - ); - - // --------------------------------------------------------------------- - // Phase C: Tie on price with *different registration times*. - // Make n3 price=25 as well. Now n1,n2,n3,n4 all have price=25. - // Earliest registration among them is n1 (block 0). - // --------------------------------------------------------------------- - SubnetMovingPrice::::insert(n3, I96F32::from_num(25)); - assert_eq!( - SubtensorModule::get_network_to_prune(), - Some(n1), - "Tie on price across multiple nets → earliest registration (n1) wins." - ); - - // --------------------------------------------------------------------- - // Phase D: Immune networks ignored even if strictly cheaper (0). - // n5 and n6 price=0 but still immune at (imm + 10). Ensure they are - // ignored and selection remains n1. - // --------------------------------------------------------------------- - let now = System::block_number(); - assert!( - now < NetworkRegisteredAt::::get(n5) + imm, - "n5 is immune at current block" - ); - assert!( - now < NetworkRegisteredAt::::get(n6) + imm, - "n6 is immune at current block" - ); - assert_eq!( - SubtensorModule::get_network_to_prune(), - Some(n1), - "Immune nets (n5,n6) must be ignored despite lower price." - ); - - // --------------------------------------------------------------------- - // Phase E: If *all* networks are immune → return None. - // Move clock back before any network's immunity expires. - // --------------------------------------------------------------------- - System::set_block_number(0); - assert_eq!( - SubtensorModule::get_network_to_prune(), - None, - "With all networks immune, there is no prunable candidate." - ); - - // --------------------------------------------------------------------- - // Phase F: Advance beyond immunity for n5 & n6. - // Both n5 and n6 now eligible with price=0 (lowest). - // Tie on price; earlier registration between n5 and n6 is n5. - // --------------------------------------------------------------------- - System::set_block_number(2 * imm + 10); - assert!( - System::block_number() >= NetworkRegisteredAt::::get(n5) + imm, - "n5 has matured" - ); - assert!( - System::block_number() >= NetworkRegisteredAt::::get(n6) + imm, - "n6 has matured" - ); - assert_eq!( - SubtensorModule::get_network_to_prune(), - Some(n5), - "After immunity, n5 (price=0) should win; tie with n6 broken by earlier registration." - ); - - // --------------------------------------------------------------------- - // Phase G: Create *sparse* netuids and ensure selection is stable. - // Remove n5; now n6 (price=0) should be selected. - // This validates robustness to holes / non-contiguous netuids. - // --------------------------------------------------------------------- - assert_ok!(SubtensorModule::do_dissolve_network(n5)); - assert_eq!( - SubtensorModule::get_network_to_prune(), - Some(n6), - "After removing n5, next-lowest (n6=0) should be chosen even with sparse netuids." - ); - - // --------------------------------------------------------------------- - // Phase H: Dynamic price changes. - // Make n6 expensive (price 100); make n3 cheapest (price 1). - // --------------------------------------------------------------------- - SubnetMovingPrice::::insert(n6, I96F32::from_num(100)); - SubnetMovingPrice::::insert(n3, I96F32::from_num(1)); - assert_eq!( - SubtensorModule::get_network_to_prune(), - Some(n3), - "Dynamic changes: n3 set to price=1 (lowest among eligibles) → should be pruned." - ); - - // --------------------------------------------------------------------- - // Phase I: Tie again (n2 vs n3) but earlier registration must win. - // Give n2 the same price as n3; n2 registered at block 0, n3 at block 1. - // n2 should be chosen. - // --------------------------------------------------------------------- - SubnetMovingPrice::::insert(n2, I96F32::from_num(1)); - assert_eq!( - SubtensorModule::get_network_to_prune(), - Some(n2), - "Tie on price across n2 (earlier reg) and n3 → n2 wins by timestamp." - ); - - // --------------------------------------------------------------------- - // (Extra) Mark n2 as 'not added' to assert we honor the `added` flag, - // then restore it to avoid side-effects on subsequent tests. - // --------------------------------------------------------------------- - NetworksAdded::::insert(n2, false); - assert_ne!( - SubtensorModule::get_network_to_prune(), - Some(n2), - "`added=false` must exclude n2 from consideration." - ); - NetworksAdded::::insert(n2, true); - - // Root is always ignored even if cheapest (get_moving_alpha_price returns 1 for ROOT). - assert_ne!( - SubtensorModule::get_network_to_prune(), - Some(root), - "ROOT must never be selected for pruning." - ); - }); -} - -#[test] -fn get_subnet_account_id_some_while_dissolved_cleanup_pending() { - new_test_ext(1).execute_with(|| { - let cold = U256::from(44_001); - let hot = U256::from(44_002); - let net = add_dynamic_network(&hot, &cold); - assert_ok!(SubtensorModule::do_dissolve_network(net)); - assert!(!SubtensorModule::if_subnet_exist(net)); - assert!(DissolveCleanupQueue::::get().contains(&net)); - assert!( - SubtensorModule::get_subnet_account_id(net).is_some(), - "subnet TAO account must stay derivable during async dissolve cleanup" - ); - }); -} - -#[test] -fn register_network_skips_dissolved_netuid() { - new_test_ext(0).execute_with(|| { - let dissolved = NetUid::from(1); - DissolveCleanupQueue::::put(vec![dissolved]); - - let cold = U256::from(60); - let hot = U256::from(61); - let needed: u64 = SubtensorModule::get_network_lock_cost().into(); - add_balance_to_coldkey_account(&cold, needed.saturating_mul(10).into()); - - assert_ok!(SubtensorModule::do_register_network( - RuntimeOrigin::signed(cold), - &hot, - 1, - None, - )); - - assert!(!NetworksAdded::::get(dissolved)); - let expected = NetUid::from(2); - assert!(NetworksAdded::::get(expected)); - assert_eq!(SubnetOwner::::get(expected), cold); - }); -} - -#[test] -fn register_network_fails_before_prune_keeps_existing() { - new_test_ext(0).execute_with(|| { - SubnetLimit::::put(1u16); - - let n_cold = U256::from(41); - let n_hot = U256::from(42); - let net = add_dynamic_network(&n_hot, &n_cold); - - let imm = SubtensorModule::get_network_immunity_period(); - System::set_block_number(imm + 50); - Emission::::insert(net, vec![AlphaBalance::from(10)]); - - let caller_cold = U256::from(50); - let caller_hot = U256::from(51); - - assert_err!( - SubtensorModule::do_register_network( - RuntimeOrigin::signed(caller_cold), - &caller_hot, - 1, - None, - ), - Error::::CannotAffordLockCost - ); - - assert!(SubtensorModule::if_subnet_exist(net)); - assert_eq!(TotalNetworks::::get(), 1); - }); -} - -#[test] -fn test_migrate_network_immunity_period() { - new_test_ext(0).execute_with(|| { - // -------------------------------------------------------------------- - // ‼️ PRE-CONDITIONS - // -------------------------------------------------------------------- - assert_ne!(NetworkImmunityPeriod::::get(), 864_000); - assert!( - !HasMigrationRun::::get(b"migrate_network_immunity_period".to_vec()), - "HasMigrationRun should be false before migration" - ); - - // -------------------------------------------------------------------- - // ▶️ RUN MIGRATION - // -------------------------------------------------------------------- - let weight = migrate_network_immunity_period::migrate_network_immunity_period::(); - - // -------------------------------------------------------------------- - // ✅ POST-CONDITIONS - // -------------------------------------------------------------------- - assert_eq!( - NetworkImmunityPeriod::::get(), - 864_000, - "NetworkImmunityPeriod should now be 864_000" - ); - - assert!( - HasMigrationRun::::get(b"migrate_network_immunity_period".to_vec()), - "HasMigrationRun should be true after migration" - ); - - assert!(weight != Weight::zero(), "migration weight should be > 0"); - }); -} - -// #[test] -// fn test_schedule_dissolve_network_execution() { -// new_test_ext(1).execute_with(|| { -// let block_number: u64 = 0; -// let netuid = NetUid::from(2); -// let tempo: u16 = 13; -// let hotkey_account_id: U256 = U256::from(1); -// let coldkey_account_id = U256::from(0); // Neighbour of the beast, har har -// let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( -// netuid, -// block_number, -// 129123813, -// &hotkey_account_id, -// ); - -// //add network -// add_network(netuid, tempo, 0); - -// assert_ok!(SubtensorModule::register( -// <::RuntimeOrigin>::signed(hotkey_account_id), -// netuid, -// block_number, -// nonce, -// work.clone(), -// hotkey_account_id, -// coldkey_account_id -// )); - -// assert!(SubtensorModule::if_subnet_exist(netuid)); - -// assert_ok!(SubtensorModule::schedule_dissolve_network( -// <::RuntimeOrigin>::signed(coldkey_account_id), -// netuid -// )); - -// let current_block = System::block_number(); -// let execution_block = current_block + DissolveNetworkScheduleDuration::::get(); - -// System::assert_last_event( -// Event::DissolveNetworkScheduled { -// account: coldkey_account_id, -// netuid, -// execution_block, -// } -// .into(), -// ); - -// run_to_block(execution_block); -// assert!(!SubtensorModule::if_subnet_exist(netuid)); -// }) -// } - -// #[test] -// fn test_non_owner_schedule_dissolve_network_execution() { -// new_test_ext(1).execute_with(|| { -// let block_number: u64 = 0; -// let netuid = NetUid::from(2); -// let tempo: u16 = 13; -// let hotkey_account_id: U256 = U256::from(1); -// let coldkey_account_id = U256::from(0); // Neighbour of the beast, har har -// let non_network_owner_account_id = U256::from(2); // -// let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( -// netuid, -// block_number, -// 129123813, -// &hotkey_account_id, -// ); - -// //add network -// add_network(netuid, tempo, 0); - -// assert_ok!(SubtensorModule::register( -// <::RuntimeOrigin>::signed(hotkey_account_id), -// netuid, -// block_number, -// nonce, -// work.clone(), -// hotkey_account_id, -// coldkey_account_id -// )); - -// assert!(SubtensorModule::if_subnet_exist(netuid)); - -// assert_ok!(SubtensorModule::schedule_dissolve_network( -// <::RuntimeOrigin>::signed(non_network_owner_account_id), -// netuid -// )); - -// let current_block = System::block_number(); -// let execution_block = current_block + DissolveNetworkScheduleDuration::::get(); - -// System::assert_last_event( -// Event::DissolveNetworkScheduled { -// account: non_network_owner_account_id, -// netuid, -// execution_block, -// } -// .into(), -// ); - -// run_to_block(execution_block); -// // network exists since the caller is no the network owner -// assert!(SubtensorModule::if_subnet_exist(netuid)); -// }) -// } - -// #[test] -// fn test_new_owner_schedule_dissolve_network_execution() { -// new_test_ext(1).execute_with(|| { -// let block_number: u64 = 0; -// let netuid = NetUid::from(2); -// let tempo: u16 = 13; -// let hotkey_account_id: U256 = U256::from(1); -// let coldkey_account_id = U256::from(0); // Neighbour of the beast, har har -// let new_network_owner_account_id = U256::from(2); // -// let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( -// netuid, -// block_number, -// 129123813, -// &hotkey_account_id, -// ); - -// //add network -// add_network(netuid, tempo, 0); - -// assert_ok!(SubtensorModule::register( -// <::RuntimeOrigin>::signed(hotkey_account_id), -// netuid, -// block_number, -// nonce, -// work.clone(), -// hotkey_account_id, -// coldkey_account_id -// )); - -// assert!(SubtensorModule::if_subnet_exist(netuid)); - -// // the account is not network owner when schedule the call -// assert_ok!(SubtensorModule::schedule_dissolve_network( -// <::RuntimeOrigin>::signed(new_network_owner_account_id), -// netuid -// )); - -// let current_block = System::block_number(); -// let execution_block = current_block + DissolveNetworkScheduleDuration::::get(); - -// System::assert_last_event( -// Event::DissolveNetworkScheduled { -// account: new_network_owner_account_id, -// netuid, -// execution_block, -// } -// .into(), -// ); -// run_to_block(current_block + 1); -// // become network owner after call scheduled -// crate::SubnetOwner::::insert(netuid, new_network_owner_account_id); - -// run_to_block(execution_block); -// // network exists since the caller is no the network owner -// assert!(!SubtensorModule::if_subnet_exist(netuid)); -// }) -// } - -// #[test] -// fn test_schedule_dissolve_network_execution_with_coldkey_swap() { -// new_test_ext(1).execute_with(|| { -// let block_number: u64 = 0; -// let netuid = NetUid::from(2); -// let tempo: u16 = 13; -// let hotkey_account_id: U256 = U256::from(1); -// let coldkey_account_id = U256::from(0); // Neighbour of the beast, har har -// let new_network_owner_account_id = U256::from(2); // - -// add_balance_to_coldkey_account(&coldkey_account_id, 1000000000000000); - -// let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( -// netuid, -// block_number, -// 129123813, -// &hotkey_account_id, -// ); - -// //add network -// add_network(netuid, tempo, 0); - -// assert_ok!(SubtensorModule::register( -// <::RuntimeOrigin>::signed(hotkey_account_id), -// netuid, -// block_number, -// nonce, -// work.clone(), -// hotkey_account_id, -// coldkey_account_id -// )); - -// assert!(SubtensorModule::if_subnet_exist(netuid)); - -// // the account is not network owner when schedule the call -// assert_ok!(SubtensorModule::schedule_swap_coldkey( -// <::RuntimeOrigin>::signed(coldkey_account_id), -// new_network_owner_account_id -// )); - -// let current_block = System::block_number(); -// let execution_block = current_block + ColdkeySwapScheduleDuration::::get(); - -// run_to_block(execution_block - 1); - -// // the account is not network owner when schedule the call -// assert_ok!(SubtensorModule::schedule_dissolve_network( -// <::RuntimeOrigin>::signed(new_network_owner_account_id), -// netuid -// )); - -// System::assert_last_event( -// Event::DissolveNetworkScheduled { -// account: new_network_owner_account_id, -// netuid, -// execution_block: DissolveNetworkScheduleDuration::::get() + execution_block -// - 1, -// } -// .into(), -// ); - -// run_to_block(execution_block); -// assert_eq!( -// crate::SubnetOwner::::get(netuid), -// new_network_owner_account_id -// ); - -// let current_block = System::block_number(); -// let execution_block = current_block + DissolveNetworkScheduleDuration::::get(); - -// run_to_block(execution_block); -// // network exists since the caller is no the network owner -// assert!(!SubtensorModule::if_subnet_exist(netuid)); -// }) -// } - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::networks::test_register_subnet_low_lock_cost --exact --show-output --nocapture -#[test] -fn test_register_subnet_low_lock_cost() { - new_test_ext(1).execute_with(|| { - NetworkMinLockCost::::set(TaoBalance::from(1_000)); - NetworkLastLockCost::::set(TaoBalance::from(1_000)); - - // Make sure lock cost is lower than 100 TAO - let lock_cost = SubtensorModule::get_network_lock_cost(); - assert!(lock_cost < 100_000_000_000_u64.into()); - - let subnet_owner_coldkey = U256::from(1); - let subnet_owner_hotkey = U256::from(2); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - assert!(SubtensorModule::if_subnet_exist(netuid)); - - // Ensure that both Subnet TAO and Subnet Alpha In equal to (actual) lock_cost - assert_eq!(SubnetTAO::::get(netuid), lock_cost); - assert_eq!( - SubnetAlphaIn::::get(netuid), - lock_cost.to_u64().into() - ); - }) -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::networks::test_register_subnet_high_lock_cost --exact --show-output --nocapture -#[test] -fn test_register_subnet_high_lock_cost() { - new_test_ext(1).execute_with(|| { - let lock_cost = TaoBalance::from(1_000_000_000_000_u64); - NetworkMinLockCost::::set(lock_cost); - NetworkLastLockCost::::set(lock_cost); - - // Make sure lock cost is higher than 100 TAO - let lock_cost = SubtensorModule::get_network_lock_cost(); - assert!(lock_cost >= 1_000_000_000_000_u64.into()); - - let subnet_owner_coldkey = U256::from(1); - let subnet_owner_hotkey = U256::from(2); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - assert!(SubtensorModule::if_subnet_exist(netuid)); - - // Ensure that both Subnet TAO and Subnet Alpha In equal to 100 TAO - assert_eq!(SubnetTAO::::get(netuid), lock_cost); - assert_eq!( - SubnetAlphaIn::::get(netuid), - lock_cost.to_u64().into() - ); - }) -} - -#[test] -fn test_tempo_greater_than_weight_set_rate_limit() { - new_test_ext(1).execute_with(|| { - let subnet_owner_hotkey = U256::from(1); - let subnet_owner_coldkey = U256::from(2); - - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - // Get tempo - let tempo = SubtensorModule::get_tempo(netuid); - - let weights_set_rate_limit = SubtensorModule::get_weights_set_rate_limit(netuid); - - assert!(tempo as u64 >= weights_set_rate_limit); - }) -} - -#[test] -fn massive_dissolve_refund_and_reregistration_flow_is_lossless_and_cleans_state() { - new_test_ext(0).execute_with(|| { - // ──────────────────────────────────────────────────────────────────── - // 0) Constants and helpers (distinct hotkeys & coldkeys) - // ──────────────────────────────────────────────────────────────────── - const NUM_NETS: usize = 4; - - // Six LP coldkeys - let cold_lps: [U256; 6] = [ - U256::from(3001), - U256::from(3002), - U256::from(3003), - U256::from(3004), - U256::from(3005), - U256::from(3006), - ]; - - // For each coldkey, define two DISTINCT hotkeys it owns. - let mut cold_to_hots: BTreeMap = BTreeMap::new(); - for &c in cold_lps.iter() { - let h1 = U256::from(c.low_u64().saturating_add(100_000)); - let h2 = U256::from(c.low_u64().saturating_add(200_000)); - cold_to_hots.insert(c, [h1, h2]); - } - - // Distinct τ pot sizes per net. - let pots: [u64; NUM_NETS] = [12_345, 23_456, 34_567, 45_678]; - - let lp_sets_per_net: [&[U256]; NUM_NETS] = [ - &cold_lps[0..4], // net0: A,B,C,D - &cold_lps[2..6], // net1: C,D,E,F - &cold_lps[0..6], // net2: A..F - &cold_lps[1..5], // net3: B,C,D,E - ]; - - // ──────────────────────────────────────────────────────────────────── - // 1) Create many subnets, fix price at tick=0 - // ──────────────────────────────────────────────────────────────────── - let mut nets: Vec = Vec::new(); - for i in 0..NUM_NETS { - let owner_hot = U256::from(10_000 + (i as u64)); - let owner_cold = U256::from(20_000 + (i as u64)); - let net = add_dynamic_network(&owner_hot, &owner_cold); - remove_owner_registration_stake(net); - SubtensorModule::set_max_registrations_per_block(net, 1_000u16); - SubtensorModule::set_target_registrations_per_interval(net, 1_000u16); - Emission::::insert(net, Vec::::new()); - SubtensorModule::set_subnet_locked_balance(net, TaoBalance::from(0)); - - nets.push(net); - } - - // Map net → index for quick lookups. - let mut net_index: BTreeMap = BTreeMap::new(); - for (i, &n) in nets.iter().enumerate() { - net_index.insert(n, i); - } - - // ──────────────────────────────────────────────────────────────────── - // 2) Pre-create a handful of small (hot, cold) pairs so accounts exist - // ──────────────────────────────────────────────────────────────────── - for id in 0u64..10 { - let cold_acc = U256::from(1_000_000 + id); - let hot_acc = U256::from(2_000_000 + id); - for &net in nets.iter() { - register_ok_neuron(net, hot_acc, cold_acc, 100_000 + id); - } - } - - // ──────────────────────────────────────────────────────────────────── - // 3) LPs per net: register each (hot, cold), massive τ prefund, and stake - // ──────────────────────────────────────────────────────────────────── - for &cold in cold_lps.iter() { - add_balance_to_coldkey_account(&cold, 1_000_000_000_000_u64.into()); - } - - // τ balances before LP adds (after staking): - let mut tao_before: BTreeMap = BTreeMap::new(); - - // Ordered α snapshot per net at **pair granularity** (pre‑LP): - let mut alpha_pairs_per_net: BTreeMap> = BTreeMap::new(); - - // Register both hotkeys for each participating cold on each net and stake τ→α. - for (ni, &net) in nets.iter().enumerate() { - let participants = lp_sets_per_net[ni]; - for &cold in participants.iter() { - let [hot1, hot2] = cold_to_hots[&cold]; - - // Ensure (hot, cold) neurons exist on this net. - register_ok_neuron( - net, - hot1, - cold, - (ni as u64) * 10_000 + (hot1.low_u64() % 10_000), - ); - register_ok_neuron( - net, - hot2, - cold, - (ni as u64) * 10_000 + (hot2.low_u64() % 10_000) + 1, - ); - - // Stake τ (split across the two hotkeys). - let base: u64 = - 5_000_000 + ((ni as u64) * 1_000_000) + ((cold.low_u64() % 10) * 250_000); - let stake1: u64 = base.saturating_mul(3) / 5; // 60% - let stake2: u64 = base.saturating_sub(stake1); // 40% - - assert_ok!(SubtensorModule::do_add_stake( - RuntimeOrigin::signed(cold), - hot1, - net, - stake1.into() - )); - assert_ok!(SubtensorModule::do_add_stake( - RuntimeOrigin::signed(cold), - hot2, - net, - stake2.into() - )); - } - } - - // Record τ balances now (post‑stake, pre‑LP). - for &cold in cold_lps.iter() { - tao_before.insert(cold, SubtensorModule::get_coldkey_balance(&cold).into()); - } - - // Capture **pair‑level** α snapshot per net (pre‑LP). - for ((hot, cold, net), amt) in AlphaV2::::iter() { - if let Some(&ni) = net_index.get(&net) - && lp_sets_per_net[ni].contains(&cold) { - let a: u128 = sf_to_u128(&amt); - if a > 0 { - alpha_pairs_per_net - .entry(net) - .or_default() - .push(((hot, cold), a)); - } - } - } - - // Snapshot τ balances AFTER LP adds (to measure actual principal debit). - let mut tao_after_adds: BTreeMap = BTreeMap::new(); - for &cold in cold_lps.iter() { - tao_after_adds.insert(cold, SubtensorModule::get_coldkey_balance(&cold)); - } - - // ──────────────────────────────────────────────────────────────────── - // 5) Compute Hamilton-apportionment BASE shares per cold and total leftover - // from the **pair-level** pre‑LP α snapshot; also count pairs per cold. - // ──────────────────────────────────────────────────────────────────── - for &net in nets.iter() { - SubnetAlphaIn::::insert(net, AlphaBalance::ZERO); - SubnetProtocolAlpha::::insert(net, AlphaBalance::ZERO); - } - - let mut base_share_cold: BTreeMap = - cold_lps.iter().copied().map(|c| (c, 0_u64)).collect(); - let mut pair_count_cold: BTreeMap = - cold_lps.iter().copied().map(|c| (c, 0_u32)).collect(); - - let mut leftover_total: u64 = 0; - - for (ni, &net) in nets.iter().enumerate() { - let pot = pots[ni]; - let pairs = alpha_pairs_per_net.get(&net).cloned().unwrap_or_default(); - if pot == 0 || pairs.is_empty() { - continue; - } - let total_alpha: u128 = pairs.iter().map(|(_, a)| *a).sum(); - if total_alpha == 0 { - continue; - } - - let mut base_sum_net: u64 = 0; - for ((_, cold), a) in pairs.iter().copied() { - // quota = a * pot / total_alpha - let prod: u128 = a.saturating_mul(pot as u128); - let base: u64 = (prod / total_alpha) as u64; - base_sum_net = base_sum_net.saturating_add(base); - *base_share_cold.entry(cold).or_default() = - base_share_cold[&cold].saturating_add(base); - *pair_count_cold.entry(cold).or_default() += 1; - } - let leftover_net = pot.saturating_sub(base_sum_net); - leftover_total = leftover_total.saturating_add(leftover_net); - } - - // ──────────────────────────────────────────────────────────────────── - // 6) Seed τ pots and dissolve *all* networks (liquidates LPs + refunds) - // ──────────────────────────────────────────────────────────────────── - for (ni, &net) in nets.iter().enumerate() { - SubnetTAO::::insert(net, TaoBalance::from(pots[ni])); - } - for &net in nets.iter() { - assert_ok!(SubtensorModule::do_dissolve_network(net)); - run_block_idle(); - } - - // ──────────────────────────────────────────────────────────────────── - // 7) Assertions: τ balances, α gone, nets removed, swap state clean - // (Hamilton invariants enforced at cold-level without relying on tie-break) - // ──────────────────────────────────────────────────────────────────── - // Collect actual pot credits per cold (principal cancels out against adds when comparing before→after). - let mut actual_pot_cold: BTreeMap = - cold_lps.iter().copied().map(|c| (c, 0_u64)).collect(); - for &cold in cold_lps.iter() { - let before = tao_before[&cold]; - let after = SubtensorModule::get_coldkey_balance(&cold); - actual_pot_cold.insert(cold, after.saturating_sub(before.into()).into()); - } - - // (a) Sum of actual pot credits equals total pots. - let total_actual: u64 = actual_pot_cold.values().copied().sum(); - let total_pots: u64 = pots.iter().copied().sum(); - assert_eq!( - total_actual, total_pots, - "total τ pot credited across colds must equal sum of pots" - ); - - // (b) Each cold’s pot is within Hamilton bounds: base ≤ actual ≤ base + #pairs. - let mut extra_accum: u64 = 0; - for &cold in cold_lps.iter() { - let base = *base_share_cold.get(&cold).unwrap_or(&0); - let pairs = *pair_count_cold.get(&cold).unwrap_or(&0) as u64; - let actual = *actual_pot_cold.get(&cold).unwrap_or(&0); - - assert!( - actual >= base, - "cold {cold:?} actual pot {actual} is below base {base}" - ); - assert!( - actual <= base.saturating_add(pairs), - "cold {cold:?} actual pot {actual} exceeds base + pairs ({base} + {pairs})" - ); - - extra_accum = extra_accum.saturating_add(actual.saturating_sub(base)); - } - - // (c) The total “extra beyond base” equals the computed leftover_total across nets. - assert_eq!( - extra_accum, leftover_total, - "sum of extras beyond base must equal total leftover" - ); - - // (d) τ principal was fully refunded (compare after_adds → after). - for &cold in cold_lps.iter() { - let before = tao_before[&cold]; - let mid = tao_after_adds[&cold]; - let after = SubtensorModule::get_coldkey_balance(&cold); - let principal_actual = before.saturating_sub(mid); - let actual_pot = after.saturating_sub(before.into()); - assert_eq!( - after.saturating_sub(mid.into()), - principal_actual.saturating_add(actual_pot.into()).into(), - "cold {cold:?} τ balance incorrect vs 'after_adds'" - ); - } - - // For each dissolved net, check α ledgers gone, network removed, and swap state clean. - for &net in nets.iter() { - assert!( - AlphaV2::::iter().all(|((_h, _c, n), _)| n != net), - "alpha ledger not fully cleared for net {net:?}" - ); - assert!( - !SubtensorModule::if_subnet_exist(net), - "subnet {net:?} still exists" - ); - assert!( - !pallet_subtensor_swap::PalSwapInitialized::::get(net), - "PalSwapInitialized still set" - ); - } - - // ──────────────────────────────────────────────────────────────────── - // 8) Re-register a fresh subnet and re‑stake using the pallet’s min rule - // Assert αΔ equals the sim-swap result for the exact τ staked. - // ──────────────────────────────────────────────────────────────────── - let new_owner_hot = U256::from(99_000); - let new_owner_cold = U256::from(99_001); - let net_new = add_dynamic_network(&new_owner_hot, &new_owner_cold); - remove_owner_registration_stake(net_new); - SubtensorModule::set_max_registrations_per_block(net_new, 1_000u16); - SubtensorModule::set_target_registrations_per_interval(net_new, 1_000u16); - Emission::::insert(net_new, Vec::::new()); - SubtensorModule::set_subnet_locked_balance(net_new, TaoBalance::from(0)); - - // Compute the exact min stake per the pallet rule: DefaultMinStake + fee(DefaultMinStake). - let min_stake = DefaultMinStake::::get(); - let order = GetAlphaForTao::::with_amount(min_stake); - let fee_for_min = pallet_subtensor_swap::Pallet::::sim_swap( - net_new, - order, - ) - .map(|r| r.fee_paid) - .unwrap_or_else(|_e| { - as subtensor_swap_interface::SwapHandler>::approx_fee_amount(net_new, min_stake) - }); - let min_amount_required = min_stake.saturating_add(fee_for_min).to_u64(); - - // Re‑stake from three coldkeys; choose a specific DISTINCT hotkey per cold. - for &cold in &cold_lps[0..3] { - let [hot1, _hot2] = cold_to_hots[&cold]; - register_ok_neuron(net_new, hot1, cold, 7777); - - let before_tao = SubtensorModule::get_coldkey_balance(&cold); - let a_prev: u64 = sf_to_u128(&AlphaV2::::get((hot1, cold, net_new))) as u64; - - // Expected α for this exact τ, using the same sim path as the pallet. - let order = GetAlphaForTao::::with_amount(min_amount_required); - let expected_alpha_out = pallet_subtensor_swap::Pallet::::sim_swap( - net_new, - order, - ) - .map(|r| r.amount_paid_out) - .expect("sim_swap must succeed for fresh net and min amount"); - - assert_ok!(SubtensorModule::do_add_stake( - RuntimeOrigin::signed(cold), - hot1, - net_new, - min_amount_required.into() - )); - - let after_tao = SubtensorModule::get_coldkey_balance(&cold); - let a_new: u64 = sf_to_u128(&AlphaV2::::get((hot1, cold, net_new))) as u64; - let a_delta = a_new.saturating_sub(a_prev); - - // τ decreased by exactly the amount we sent. - assert_eq!( - after_tao, - before_tao.saturating_sub(min_amount_required.into()), - "τ did not decrease by the min required restake amount for cold {cold:?}" - ); - - // α minted equals the simulated swap’s net out for that same τ. - assert_eq!( - a_delta, expected_alpha_out.to_u64(), - "α minted mismatch for cold {cold:?} (hot {hot1:?}) on new net (αΔ {a_delta}, expected {expected_alpha_out})" - ); - } - }); -} - -#[test] -fn dissolve_clears_all_mechanism_scoped_maps_for_all_mechanisms() { - new_test_ext(0).execute_with(|| { - // Create a subnet we can dissolve. - let owner_cold = U256::from(123); - let owner_hot = U256::from(456); - let net = add_dynamic_network(&owner_hot, &owner_cold); - - // Add 100 TAO to subnet account (lock) - let subnet_account = SubtensorModule::get_subnet_account_id(net).unwrap(); - add_balance_to_coldkey_account(&subnet_account, 100_000_000_000_u64.into()); - - // We'll use two mechanisms for this subnet. - MechanismCountCurrent::::insert(net, MechId::from(2)); - let m0 = MechId::from(0u8); - let m1 = MechId::from(1u8); - - let idx0 = SubtensorModule::get_mechanism_storage_index(net, m0); - let idx1 = SubtensorModule::get_mechanism_storage_index(net, m1); - - // Minimal content to ensure each storage actually has keys for BOTH mechanisms. - - // --- Weights (DMAP: (netuid_index, uid) -> Vec<(dest_uid, weight_u16)>) - Weights::::insert(idx0, 0u16, vec![(1u16, 1u16)]); - Weights::::insert(idx1, 0u16, vec![(2u16, 1u16)]); - - // --- Bonds (DMAP: (netuid_index, uid) -> Vec<(dest_uid, weight_u16)>) - Bonds::::insert(idx0, 0u16, vec![(1u16, 1u16)]); - Bonds::::insert(idx1, 0u16, vec![(2u16, 1u16)]); - - // --- TimelockedWeightCommits (DMAP: (netuid_index, epoch) -> VecDeque<...>) - let hotkey = U256::from(1); - TimelockedWeightCommits::::insert( - idx0, - 1u64, - VecDeque::from([(hotkey, 1u64, Default::default(), Default::default())]), - ); - TimelockedWeightCommits::::insert( - idx1, - 2u64, - VecDeque::from([(hotkey, 2u64, Default::default(), Default::default())]), - ); - - // --- Incentive (MAP: netuid_index -> Vec) - Incentive::::insert(idx0, vec![PerU16::from_parts(1), PerU16::from_parts(2)]); - Incentive::::insert(idx1, vec![PerU16::from_parts(3), PerU16::from_parts(4)]); - - // --- LastUpdate (MAP: netuid_index -> Vec) - LastUpdate::::insert(idx0, vec![42u64]); - LastUpdate::::insert(idx1, vec![84u64]); - - // Sanity: keys are present before dissolve. - assert!(Weights::::contains_key(idx0, 0u16)); - assert!(Weights::::contains_key(idx1, 0u16)); - assert!(Bonds::::contains_key(idx0, 0u16)); - assert!(Bonds::::contains_key(idx1, 0u16)); - assert!(TimelockedWeightCommits::::contains_key(idx0, 1u64)); - assert!(TimelockedWeightCommits::::contains_key(idx1, 2u64)); - assert!(Incentive::::contains_key(idx0)); - assert!(Incentive::::contains_key(idx1)); - assert!(LastUpdate::::contains_key(idx0)); - assert!(LastUpdate::::contains_key(idx1)); - assert!(MechanismCountCurrent::::contains_key(net)); - - // --- Dissolve the subnet --- - assert_ok!(SubtensorModule::do_dissolve_network(net)); - run_block_idle(); - - // After dissolve, ALL mechanism-scoped items must be cleared for ALL mechanisms. - - // Weights/Bonds double-maps should have no entries under either index. - assert!(Weights::::iter_prefix(idx0).next().is_none()); - assert!(Weights::::iter_prefix(idx1).next().is_none()); - assert!(Bonds::::iter_prefix(idx0).next().is_none()); - assert!(Bonds::::iter_prefix(idx1).next().is_none()); - - // WeightCommits (OptionQuery) should have no keys remaining. - assert!(WeightCommits::::iter_prefix(idx0).next().is_none()); - assert!(WeightCommits::::iter_prefix(idx1).next().is_none()); - assert!(!WeightCommits::::contains_key(idx0, owner_hot)); - assert!(!WeightCommits::::contains_key(idx1, owner_cold)); - - // TimelockedWeightCommits (ValueQuery) — ensure both prefix spaces empty and keys gone. - assert!( - TimelockedWeightCommits::::iter_prefix(idx0) - .next() - .is_none() - ); - assert!( - TimelockedWeightCommits::::iter_prefix(idx1) - .next() - .is_none() - ); - assert!(!TimelockedWeightCommits::::contains_key(idx0, 1u64)); - assert!(!TimelockedWeightCommits::::contains_key(idx1, 2u64)); - - // Single-map per-mechanism vectors cleared. - assert!(!Incentive::::contains_key(idx0)); - assert!(!Incentive::::contains_key(idx1)); - assert!(!LastUpdate::::contains_key(idx0)); - assert!(!LastUpdate::::contains_key(idx1)); - - // MechanismCountCurrent cleared - assert!(!MechanismCountCurrent::::contains_key(net)); - }); -} - -#[test] -fn dissolve_clears_all_lock_maps_for_removed_network() { - new_test_ext(0).execute_with(|| { - // Create a subnet we can dissolve. - let owner_cold = U256::from(123); - let owner_hot = U256::from(456); - let net = add_dynamic_network(&owner_hot, &owner_cold); - - // Add TAO to subnet account so dissolve can proceed. - let subnet_account = SubtensorModule::get_subnet_account_id(net).unwrap(); - add_balance_to_coldkey_account(&subnet_account, 100_000_000_000_u64.into()); - - // Non-owner coldkeys / hotkeys. - let cold_1 = U256::from(1001); - let cold_2 = U256::from(1002); - let hot_1 = U256::from(2001); - let hot_2 = U256::from(2002); - - // Another subnet to ensure dissolve only clears `net`. - let other_net = NetUid::from(u16::from(net) + 1); - - // Explicit LockState initialization - let lock_a = LockState { - locked_mass: 10u64.into(), - conviction: U64F64::from_num(1.5), - last_update: 1, - }; - - let lock_b = LockState { - locked_mass: 20u64.into(), - conviction: U64F64::from_num(2.5), - last_update: 2, - }; - - // --- Lock: (coldkey, netuid, hotkey) - Lock::::insert((cold_1, net, hot_1), lock_a.clone()); - LockingColdkeys::::insert((net, hot_1, cold_1), ()); - Lock::::insert((cold_2, net, hot_2), lock_b.clone()); - LockingColdkeys::::insert((net, hot_2, cold_2), ()); - - // Same cold/hot on another net should survive. - Lock::::insert((cold_1, other_net, hot_1), lock_a.clone()); - LockingColdkeys::::insert((other_net, hot_1, cold_1), ()); - - // --- HotkeyLock - HotkeyLock::::insert(net, hot_1, lock_a.clone()); - HotkeyLock::::insert(net, hot_2, lock_b.clone()); - HotkeyLock::::insert(other_net, hot_1, lock_a.clone()); - - // --- DecayingHotkeyLock - DecayingHotkeyLock::::insert(net, hot_1, lock_a.clone()); - DecayingHotkeyLock::::insert(net, hot_2, lock_b.clone()); - DecayingHotkeyLock::::insert(other_net, hot_1, lock_a.clone()); - - // --- OwnerLock - OwnerLock::::insert(net, lock_a.clone()); - OwnerLock::::insert(other_net, lock_b.clone()); - - // --- DecayingLock - DecayingLock::::insert(cold_1, net, false); - DecayingLock::::insert(cold_2, net, false); - DecayingLock::::insert(cold_1, other_net, false); - - // Sanity checks before dissolve - assert!(Lock::::contains_key((cold_1, net, hot_1))); - assert!(Lock::::contains_key((cold_2, net, hot_2))); - assert!(LockingColdkeys::::contains_key((net, hot_1, cold_1))); - assert!(LockingColdkeys::::contains_key((net, hot_2, cold_2))); - - assert!(HotkeyLock::::contains_key(net, hot_1)); - assert!(HotkeyLock::::contains_key(net, hot_2)); - - assert!(DecayingHotkeyLock::::contains_key(net, hot_1)); - assert!(DecayingHotkeyLock::::contains_key(net, hot_2)); - - assert!(OwnerLock::::contains_key(net)); - - assert!(DecayingLock::::contains_key(cold_1, net)); - assert!(DecayingLock::::contains_key(cold_2, net)); - - // Sanity: other net keys are present before dissolve. - assert!(Lock::::contains_key((cold_1, other_net, hot_1))); - assert!(LockingColdkeys::::contains_key(( - other_net, hot_1, cold_1 - ))); - assert!(HotkeyLock::::contains_key(other_net, hot_1)); - assert!(DecayingHotkeyLock::::contains_key(other_net, hot_1)); - assert!(OwnerLock::::contains_key(other_net)); - assert!(DecayingLock::::contains_key(cold_1, other_net)); - - // --- Dissolve --- - assert_ok!(SubtensorModule::do_dissolve_network(net)); - run_block_idle(); - - // Ensure removed - assert!(!Lock::::contains_key((cold_1, net, hot_1))); - assert!(!Lock::::contains_key((cold_2, net, hot_2))); - assert!(!LockingColdkeys::::contains_key((net, hot_1, cold_1))); - assert!(!LockingColdkeys::::contains_key((net, hot_2, cold_2))); - - assert!(!HotkeyLock::::contains_key(net, hot_1)); - assert!(!HotkeyLock::::contains_key(net, hot_2)); - assert!(HotkeyLock::::iter_prefix(net).next().is_none()); - - assert!(!DecayingHotkeyLock::::contains_key(net, hot_1)); - assert!(!DecayingHotkeyLock::::contains_key(net, hot_2)); - assert!( - DecayingHotkeyLock::::iter_prefix(net) - .next() - .is_none() - ); - - assert!(!OwnerLock::::contains_key(net)); - - assert!(!DecayingLock::::contains_key(cold_1, net)); - assert!(!DecayingLock::::contains_key(cold_2, net)); - - // Ensure other_net is untouched - assert!(Lock::::contains_key((cold_1, other_net, hot_1))); - assert!(LockingColdkeys::::contains_key(( - other_net, hot_1, cold_1 - ))); - assert!(HotkeyLock::::contains_key(other_net, hot_1)); - assert!(DecayingHotkeyLock::::contains_key(other_net, hot_1)); - assert!(OwnerLock::::contains_key(other_net)); - assert!(DecayingLock::::contains_key(cold_1, other_net)); - }); -} - -fn owner_alpha_from_lock_and_price(lock_cost_u64: u64, price: U64F64) -> u64 { - let alpha = (U64F64::from_num(lock_cost_u64) - .checked_div(price) - .unwrap_or_default()) - .floor(); - - if alpha > U64F64::from_num(u64::MAX) { - u64::MAX - } else { - alpha.to_num::() - } -} - -#[test] -fn median_subnet_alpha_price_returns_one_when_no_eligible_subnet_prices() { - new_test_ext(0).execute_with(|| { - let one = U64F64::from_num(1u64); - - // Empty state. - assert_eq!(SubtensorModule::get_median_subnet_alpha_price(), one); - - // ROOT must be ignored. - NetworksAdded::::insert(NetUid::ROOT, true); - assert_eq!(SubtensorModule::get_median_subnet_alpha_price(), one); - - // Zero-priced subnet must be ignored. - let zero_cold = U256::from(101); - let zero_hot = U256::from(102); - let zero_netuid = add_dynamic_network(&zero_hot, &zero_cold); - setup_reserves(zero_netuid, TaoBalance::ZERO, AlphaBalance::from(100u64)); - assert_eq!( - ::SwapInterface::current_alpha_price(zero_netuid.into()), - U64F64::from_num(0u64) - ); - assert_eq!(SubtensorModule::get_median_subnet_alpha_price(), one); - - // added=false subnet must be ignored as well. - let hidden_cold = U256::from(103); - let hidden_hot = U256::from(104); - let hidden_netuid = add_dynamic_network(&hidden_hot, &hidden_cold); - setup_reserves( - hidden_netuid, - TaoBalance::from(900u64), - AlphaBalance::from(100u64), - ); - NetworksAdded::::insert(hidden_netuid, false); - - assert_eq!(SubtensorModule::get_median_subnet_alpha_price(), one); - }); -} - -#[test] -fn median_subnet_alpha_price_returns_middle_value_for_odd_unsorted_prices() { - new_test_ext(0).execute_with(|| { - let n1 = add_dynamic_network(&U256::from(201), &U256::from(200)); - let n2 = add_dynamic_network(&U256::from(203), &U256::from(202)); - let n3 = add_dynamic_network(&U256::from(205), &U256::from(204)); - - // Unsorted prices: 7, 2, 5 -> median should be 5. - setup_reserves(n1, TaoBalance::from(700u64), AlphaBalance::from(100u64)); - setup_reserves(n2, TaoBalance::from(200u64), AlphaBalance::from(100u64)); - setup_reserves(n3, TaoBalance::from(500u64), AlphaBalance::from(100u64)); - - assert_eq!( - ::SwapInterface::current_alpha_price(n1.into()), - U96F32::from_num(7u64) - ); - assert_eq!( - ::SwapInterface::current_alpha_price(n2.into()), - U96F32::from_num(2u64) - ); - assert_eq!( - ::SwapInterface::current_alpha_price(n3.into()), - U96F32::from_num(5u64) - ); - - assert_eq!( - SubtensorModule::get_median_subnet_alpha_price(), - U96F32::from_num(5u64) - ); - }); -} - -#[test] -fn median_subnet_alpha_price_averages_even_prices_and_ignores_root_zero_and_unadded() { - new_test_ext(0).execute_with(|| { - // If ROOT were included, its price would be 1 and change the median. - NetworksAdded::::insert(NetUid::ROOT, true); - - let n1 = add_dynamic_network(&U256::from(301), &U256::from(300)); // eligible, price 2 - let n2 = add_dynamic_network(&U256::from(303), &U256::from(302)); // hidden, price 4 - let n3 = add_dynamic_network(&U256::from(305), &U256::from(304)); // eligible, price 8 - let n4 = add_dynamic_network(&U256::from(307), &U256::from(306)); // zero, price 0 - - setup_reserves(n1, TaoBalance::from(200u64), AlphaBalance::from(100u64)); - setup_reserves(n2, TaoBalance::from(400u64), AlphaBalance::from(100u64)); - setup_reserves(n3, TaoBalance::from(800u64), AlphaBalance::from(100u64)); - setup_reserves(n4, TaoBalance::ZERO, AlphaBalance::from(100u64)); - - NetworksAdded::::insert(n2, false); - - assert_eq!( - ::SwapInterface::current_alpha_price(n1.into()), - U96F32::from_num(2u64) - ); - assert_eq!( - ::SwapInterface::current_alpha_price(n2.into()), - U96F32::from_num(4u64) - ); - assert_eq!( - ::SwapInterface::current_alpha_price(n3.into()), - U96F32::from_num(8u64) - ); - assert_eq!( - ::SwapInterface::current_alpha_price(n4.into()), - U96F32::from_num(0u64) - ); - - // Eligible prices are only {2, 8}, so the median is (2 + 8) / 2 = 5. - assert_eq!( - SubtensorModule::get_median_subnet_alpha_price(), - U96F32::from_num(5u64) - ); - }); -} - -#[test] -fn register_network_seeds_first_subnet_from_fallback_price_one_and_keeps_lock_in_pool() { - new_test_ext(1).execute_with(|| { - let new_cold = U256::from(1001); - let new_hot = U256::from(1002); - let new_netuid = SubtensorModule::get_next_netuid(); - - let lock_cost_u64: u64 = SubtensorModule::get_network_lock_cost().into(); - let pre_registration_median = SubtensorModule::get_median_subnet_alpha_price(); - - let pool_initial_tao = SubtensorModule::get_network_min_lock(); - let pool_initial_tao_u64 = pool_initial_tao.to_u64(); - let total_pool_tao_u64 = lock_cost_u64.max(pool_initial_tao_u64); - let owner_alpha_tao_equivalent_u64 = - total_pool_tao_u64.saturating_sub(pool_initial_tao_u64); - - let expected_pool_alpha_u64 = - owner_alpha_from_lock_and_price(total_pool_tao_u64, pre_registration_median); - let expected_pool_alpha: AlphaBalance = expected_pool_alpha_u64.into(); - - let expected_owner_alpha_u64 = owner_alpha_from_lock_and_price( - owner_alpha_tao_equivalent_u64, - pre_registration_median, - ); - let expected_owner_alpha: AlphaBalance = expected_owner_alpha_u64.into(); - - let expected_alpha_issuance: AlphaBalance = expected_pool_alpha_u64 - .saturating_add(expected_owner_alpha_u64) - .into(); - - let expected_recycled: TaoBalance = lock_cost_u64.saturating_sub(total_pool_tao_u64).into(); - - assert_eq!(pre_registration_median, U96F32::from_num(1u64)); - assert_eq!(expected_pool_alpha_u64, total_pool_tao_u64); - assert_eq!(expected_owner_alpha_u64, owner_alpha_tao_equivalent_u64); - assert_eq!(expected_recycled, TaoBalance::ZERO); - - add_balance_to_coldkey_account(&new_cold, lock_cost_u64.saturating_mul(2).into()); - - assert_ok!(SubtensorModule::do_register_network( - RuntimeOrigin::signed(new_cold), - &new_hot, - 1, - None, - )); - - assert!(SubtensorModule::if_subnet_exist(new_netuid)); - assert_eq!(TotalNetworks::::get(), 1); - assert_eq!(SubnetOwner::::get(new_netuid), new_cold); - assert_eq!(SubnetOwnerHotkey::::get(new_netuid), new_hot); - assert_eq!( - SubtensorModule::get_subnet_locked_balance(new_netuid), - TaoBalance::from(lock_cost_u64) - ); - - assert_eq!( - SubnetTAO::::get(new_netuid), - TaoBalance::from(total_pool_tao_u64) - ); - assert_eq!(SubnetAlphaIn::::get(new_netuid), expected_pool_alpha); - assert_eq!( - SubnetAlphaOut::::get(new_netuid), - expected_owner_alpha - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &new_hot, &new_cold, new_netuid, - ), - expected_owner_alpha - ); - assert_eq!( - TotalHotkeyAlpha::::get(new_hot, new_netuid), - expected_owner_alpha - ); - assert_eq!( - SubtensorModule::get_alpha_issuance(new_netuid), - expected_alpha_issuance - ); - assert_eq!( - RAORecycledForRegistration::::get(new_netuid), - expected_recycled - ); - - assert_eq!( - ::SwapInterface::current_alpha_price(new_netuid.into()), - U96F32::from_num(1u64) - ); - - System::assert_last_event(Event::NetworkAdded(new_netuid, 1).into()); - }); -} - -#[test] -fn register_network_seeds_new_subnet_from_even_median_snapshot() { - new_test_ext(0).execute_with(|| { - let n1 = add_dynamic_network(&U256::from(1201), &U256::from(1200)); - let n2 = add_dynamic_network(&U256::from(1203), &U256::from(1202)); - - // Existing prices are {5, 2} -> pre-registration median is 3.5. - setup_reserves(n1, TaoBalance::from(500u64), AlphaBalance::from(100u64)); - setup_reserves(n2, TaoBalance::from(200u64), AlphaBalance::from(100u64)); - - let pre_registration_median = SubtensorModule::get_median_subnet_alpha_price(); - assert_eq!(pre_registration_median, U96F32::from_num(3.5)); - - let new_cold = U256::from(1300); - let new_hot = U256::from(1301); - let new_netuid = SubtensorModule::get_next_netuid(); - - let lock_cost_u64: u64 = SubtensorModule::get_network_lock_cost().into(); - let pool_initial_tao_u64 = SubtensorModule::get_network_min_lock().to_u64(); - let total_pool_tao_u64 = lock_cost_u64.max(pool_initial_tao_u64); - let owner_alpha_tao_equivalent_u64 = - total_pool_tao_u64.saturating_sub(pool_initial_tao_u64); - - let expected_pool_alpha_u64 = - owner_alpha_from_lock_and_price(total_pool_tao_u64, pre_registration_median); - let expected_pool_alpha: AlphaBalance = expected_pool_alpha_u64.into(); - - let expected_owner_alpha_u64 = owner_alpha_from_lock_and_price( - owner_alpha_tao_equivalent_u64, - pre_registration_median, - ); - let expected_owner_alpha: AlphaBalance = expected_owner_alpha_u64.into(); - - add_balance_to_coldkey_account(&new_cold, lock_cost_u64.saturating_mul(2).into()); - - assert_ok!(SubtensorModule::do_register_network( - RuntimeOrigin::signed(new_cold), - &new_hot, - 1, - None, - )); - - let new_subnet_price = - ::SwapInterface::current_alpha_price(new_netuid.into()); - let post_registration_median = SubtensorModule::get_median_subnet_alpha_price(); - - assert!(SubtensorModule::if_subnet_exist(new_netuid)); - assert_eq!(SubnetOwner::::get(new_netuid), new_cold); - assert_eq!(SubnetOwnerHotkey::::get(new_netuid), new_hot); - assert_eq!( - SubtensorModule::get_subnet_locked_balance(new_netuid), - TaoBalance::from(lock_cost_u64) - ); - - assert_eq!( - SubnetTAO::::get(new_netuid), - TaoBalance::from(total_pool_tao_u64) - ); - assert_eq!(SubnetAlphaIn::::get(new_netuid), expected_pool_alpha); - assert_eq!( - SubnetAlphaOut::::get(new_netuid), - expected_owner_alpha - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &new_hot, &new_cold, new_netuid, - ), - expected_owner_alpha - ); - assert_eq!( - TotalHotkeyAlpha::::get(new_hot, new_netuid), - expected_owner_alpha - ); - - // The new subnet is seeded from the pre-registration median snapshot, - // so it is no longer initialized at the old 1:1 seed price. - assert_ne!(new_subnet_price, U96F32::from_num(1u64)); - assert!(new_subnet_price >= pre_registration_median); - - // With prices {2, seeded_price, 5}, the live median becomes the new subnet price. - assert_eq!(post_registration_median, new_subnet_price); - - // A 1:1 seed would have alpha_in == tao_in, which should not happen here. - let wrong_price_one_pool_alpha: AlphaBalance = total_pool_tao_u64.into(); - assert_ne!( - SubnetAlphaIn::::get(new_netuid), - wrong_price_one_pool_alpha - ); - }); -} - -#[test] -fn register_network_fails_without_balance_and_does_not_write_owner_alpha_state() { - new_test_ext(0).execute_with(|| { - let cold = U256::from(2001); - let hot = U256::from(2002); - let would_be_netuid = SubtensorModule::get_next_netuid(); - - assert_eq!( - SubtensorModule::get_coldkey_balance(&cold), - TaoBalance::ZERO - ); - - assert_err!( - SubtensorModule::do_register_network(RuntimeOrigin::signed(cold), &hot, 1, None,), - Error::::CannotAffordLockCost - ); - - assert!(!SubtensorModule::if_subnet_exist(would_be_netuid)); - assert_eq!(TotalNetworks::::get(), 0); - assert_eq!( - SubnetAlphaIn::::get(would_be_netuid), - AlphaBalance::ZERO - ); - assert_eq!( - SubnetAlphaOut::::get(would_be_netuid), - AlphaBalance::ZERO - ); - assert_eq!( - SubtensorModule::get_subnet_locked_balance(would_be_netuid), - TaoBalance::ZERO - ); - assert_eq!( - RAORecycledForRegistration::::get(would_be_netuid), - TaoBalance::ZERO - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hot, - &cold, - would_be_netuid, - ), - AlphaBalance::ZERO - ); - }); -} - -#[test] -fn register_network_non_associated_hotkey_does_not_withdraw_or_write_owner_alpha_state() { - new_test_ext(0).execute_with(|| { - let original_cold = U256::from(3001); - let shared_hot = U256::from(3002); - let existing_netuid = add_dynamic_network(&shared_hot, &original_cold); - - let original_stake_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &shared_hot, - &original_cold, - existing_netuid, - ); - let original_alpha_out_before = SubnetAlphaOut::::get(existing_netuid); - - let attacker_cold = U256::from(3003); - let would_be_netuid = SubtensorModule::get_next_netuid(); - let lock_cost_u64: u64 = SubtensorModule::get_network_lock_cost().into(); - - add_balance_to_coldkey_account(&attacker_cold, lock_cost_u64.into()); - let attacker_balance_before = SubtensorModule::get_coldkey_balance(&attacker_cold); - - assert_err!( - SubtensorModule::do_register_network( - RuntimeOrigin::signed(attacker_cold), - &shared_hot, - 1, - None, - ), - Error::::NonAssociatedColdKey - ); - - // Attacker was not charged. - assert_eq!( - SubtensorModule::get_coldkey_balance(&attacker_cold), - attacker_balance_before - ); - - // Existing owner state is untouched. - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &shared_hot, - &original_cold, - existing_netuid, - ), - original_stake_before - ); - assert_eq!( - SubnetAlphaOut::::get(existing_netuid), - original_alpha_out_before - ); - assert_eq!(SubnetOwner::::get(existing_netuid), original_cold); - - // No new subnet / owner-alpha state was written. - assert!(!SubtensorModule::if_subnet_exist(would_be_netuid)); - assert_eq!(TotalNetworks::::get(), 1); - assert_eq!( - SubnetAlphaOut::::get(would_be_netuid), - AlphaBalance::ZERO - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &shared_hot, - &attacker_cold, - would_be_netuid, - ), - AlphaBalance::ZERO - ); - }); -} - -#[test] -fn registered_subnet_counter_bumps_on_first_registration() { - new_test_ext(1).execute_with(|| { - let cold = U256::from(1); - let hot = U256::from(2); - - let netuid = add_dynamic_network(&hot, &cold); - - assert_eq!( - SubtensorModule::get_registered_subnet_counter(netuid), - 1, - "first registration of a netuid must leave counter == 1" - ); - }); -} - -#[test] -fn registered_subnet_counter_is_independent_per_netuid() { - new_test_ext(1).execute_with(|| { - let n1 = add_dynamic_network(&U256::from(10), &U256::from(11)); - let n2 = add_dynamic_network(&U256::from(20), &U256::from(21)); - - assert_ne!(n1, n2); - assert_eq!(SubtensorModule::get_registered_subnet_counter(n1), 1); - assert_eq!(SubtensorModule::get_registered_subnet_counter(n2), 1); - }); -} - -#[test] -fn registered_subnet_counter_survives_dissolve_and_bumps_on_reregistration() { - new_test_ext(1).execute_with(|| { - // Force reuse of the same netuid on re-registration by pinning the - // active subnet cap so the next registration must prune. - SubtensorModule::set_max_subnets(2); - - let owner_cold = U256::from(100); - let owner_hot = U256::from(101); - let netuid = add_dynamic_network(&owner_hot, &owner_cold); - assert_eq!(SubtensorModule::get_registered_subnet_counter(netuid), 1); - - // Dissolve: counter is intentionally *not* cleared — stale consumers - // can still detect the pre-dereg lifetime if they stored the counter - // value they observed at approval time. - assert_ok!(SubtensorModule::do_dissolve_network(netuid)); - run_block_idle(); - assert!(!SubtensorModule::if_subnet_exist(netuid)); - assert_eq!( - SubtensorModule::get_registered_subnet_counter(netuid), - 1, - "dissolve must not clear or reset the counter" - ); - - // Re-register. With the cap pinned, the prune selector reuses the - // freed netuid; the counter bumps to 2 so that any state still keyed - // to the prior value becomes unreachable under the new registration. - let reg_netuid = add_dynamic_network(&owner_hot, &owner_cold); - assert_eq!( - reg_netuid, netuid, - "the pruned netuid should be reused under the subnet cap" - ); - assert_eq!( - SubtensorModule::get_registered_subnet_counter(netuid), - 2, - "re-registration must bump counter" - ); - }); -} - -#[test] -fn dissolve_async_cleanup_leaves_phase_unset_until_idle_finishes() { - new_test_ext(0).execute_with(|| { - let owner_cold = U256::from(910); - let owner_hot = U256::from(911); - let net = add_dynamic_network(&owner_hot, &owner_cold); - - assert_ok!(SubtensorModule::do_dissolve_network(net)); - assert!( - DissolveCleanupQueue::::get().contains(&net), - "dissolved netuid should be queued for on_idle cleanup" - ); - assert!( - CurrentDissolveCleanupStatus::::get().is_none(), - "global cleanup phase is only driven from on_idle (not from do_dissolve_network)" - ); - - run_block_idle(); - - assert!( - !DissolveCleanupQueue::::get().contains(&net), - "idle cleanup should drain the dissolved net from the queue" - ); - assert!( - CurrentDissolveCleanupStatus::::get().is_none(), - "when the queue is empty, global cleanup phase storage must be cleared" - ); - }); -} - -#[test] -fn dissolve_full_on_idle_emits_dissolved_network_data_cleaned_and_clears_phase() { - // `frame_system::Pallet::events()` stays empty at block #0 in the test externalities; - // use a non-zero block like other event-asserting tests (`recycle_alpha`, etc.). - new_test_ext(1).execute_with(|| { - let owner_cold = U256::from(930); - let owner_hot = U256::from(931); - let net = add_dynamic_network(&owner_hot, &owner_cold); - - assert_ok!(SubtensorModule::do_dissolve_network(net)); - System::reset_events(); - run_block_idle(); - - assert!( - System::events().iter().any(|e| { - matches!( - &e.event, - RuntimeEvent::SubtensorModule(Event::NetworkDissolveCleanupCompleted { netuid: n }) - if *n == net - ) - }), - "expected NetworkDissolveCleanupCompleted after async dissolve pipeline" - ); - assert!( - CurrentDissolveCleanupStatus::::get().is_none(), - "global cleanup phase storage must be cleared when the queue is empty" - ); - }); -} - -#[test] -fn dissolve_two_networks_fifo_cleanup_drains_queue() { - new_test_ext(0).execute_with(|| { - let n1 = add_dynamic_network(&U256::from(940), &U256::from(941)); - let n2 = add_dynamic_network(&U256::from(942), &U256::from(943)); - - assert_ok!(SubtensorModule::do_dissolve_network(n1)); - assert_ok!(SubtensorModule::do_dissolve_network(n2)); - assert_eq!(DissolveCleanupQueue::::get(), vec![n1, n2]); - - let mut guard = 0u32; - while !DissolveCleanupQueue::::get().is_empty() { - guard = guard.saturating_add(1); - assert!( - guard < 256, - "dissolve cleanup should drain in finite idle passes (guard={guard})" - ); - run_block_idle(); - } - - assert!(!SubtensorModule::if_subnet_exist(n1)); - assert!(!SubtensorModule::if_subnet_exist(n2)); - assert!( - CurrentDissolveCleanupStatus::::get().is_none(), - "no stale phase after queue drain" - ); - }); -} - -#[test] -fn set_new_network_state_registers_subnet_with_expected_state() { - new_test_ext(1).execute_with(|| { - let cold = U256::from(9001); - let hot = U256::from(9002); - let lock_amount = SubtensorModule::get_network_lock_cost(); - add_balance_to_coldkey_account(&cold, lock_amount.saturating_mul(2.into()).into()); - TotalIssuance::::mutate(|total| *total = total.saturating_add(lock_amount)); - - let median_price = SubtensorModule::get_median_subnet_alpha_price(); - let netuid = SubtensorModule::get_next_netuid(); - - assert_ok!(SubtensorModule::set_new_network_state( - &cold, - &hot, - 1, - None, - lock_amount, - median_price, - None, - )); - - assert!(SubtensorModule::if_subnet_exist(netuid)); - assert_eq!(SubnetOwner::::get(netuid), cold); - assert_eq!(SubnetMechanism::::get(netuid), 1); - assert_eq!(SubnetLocked::::get(netuid), lock_amount); - assert_eq!( - SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hot), - Ok(0) - ); - }); -} - -#[test] -fn register_network_queues_when_waiting_for_dissolve_cleanup() { - new_test_ext(0).execute_with(|| { - SubnetLimit::::put(2u16); - - let n1 = add_dynamic_network(&U256::from(9102), &U256::from(9101)); - let _n2 = add_dynamic_network(&U256::from(9202), &U256::from(9201)); - - assert_ok!(SubtensorModule::do_dissolve_network(n1)); - assert!(DissolveCleanupQueue::::get().contains(&n1)); - - let cold = U256::from(9301); - let hot = U256::from(9302); - let lock_amount = SubtensorModule::get_network_lock_cost(); - add_balance_to_coldkey_account(&cold, lock_amount.saturating_mul(2.into()).into()); - TotalIssuance::::mutate(|total| *total = total.saturating_add(lock_amount)); - - assert_ok!(SubtensorModule::do_register_network( - RuntimeOrigin::signed(cold), - &hot, - 1, - None, - )); - - assert_eq!(NetworkRegistrationQueue::::get().len(), 1); - assert_eq!(NetworkRegistrationQueue::::get()[0].coldkey, cold); - assert_eq!(TotalNetworks::::get(), 1); - assert!(!SubtensorModule::hotkey_account_exists(&hot)); - }); -} - -#[test] -fn process_network_registration_queue_registers_after_cleanup_slot_available() { - new_test_ext(0).execute_with(|| { - SubnetLimit::::put(2u16); - - let n1 = add_dynamic_network(&U256::from(9402), &U256::from(9401)); - let n2 = add_dynamic_network(&U256::from(9502), &U256::from(9501)); - - assert_ok!(SubtensorModule::do_dissolve_network(n1)); - assert!(DissolveCleanupQueue::::get().contains(&n1)); - - let cold = U256::from(9601); - let hot = U256::from(9602); - let lock_amount = SubtensorModule::get_network_lock_cost(); - add_balance_to_coldkey_account(&cold, lock_amount.saturating_mul(3.into()).into()); - TotalIssuance::::mutate(|total| *total = total.saturating_add(lock_amount)); - - assert_ok!(SubtensorModule::do_register_network( - RuntimeOrigin::signed(cold), - &hot, - 1, - None, - )); - assert_eq!(NetworkRegistrationQueue::::get().len(), 1); - - // Simulate dissolve cleanup completing and freeing a subnet slot. - DissolveCleanupQueue::::kill(); - - run_network_registration_queue(); - - assert!(NetworkRegistrationQueue::::get().is_empty()); - assert!(SubtensorModule::hotkey_account_exists(&hot)); - assert_eq!(TotalNetworks::::get(), 2); - - let registered_netuid = NetworksAdded::::iter() - .find(|(netuid, added)| *added && *netuid != n2) - .map(|(netuid, _)| netuid) - .expect("queued registration should create a new subnet"); - assert_eq!(SubnetOwner::::get(registered_netuid), cold); - }); -} - -#[test] -fn register_network_prune_registers_registration_queued() { - new_test_ext(0).execute_with(|| { - SubnetLimit::::put(2u16); - - let n1 = add_dynamic_network(&U256::from(9702), &U256::from(9701)); - let n2 = add_dynamic_network(&U256::from(9802), &U256::from(9801)); - - let imm = SubtensorModule::get_network_immunity_period(); - System::set_block_number(imm + 100); - Emission::::insert(n1, vec![AlphaBalance::from(1)]); - Emission::::insert(n2, vec![AlphaBalance::from(1_000)]); - - let cold = U256::from(9901); - let hot = U256::from(9902); - let lock_amount = SubtensorModule::get_network_lock_cost(); - add_balance_to_coldkey_account(&cold, lock_amount.saturating_mul(10.into()).into()); - TotalIssuance::::mutate(|total| *total = total.saturating_add(lock_amount)); - - assert_ok!(SubtensorModule::do_register_network( - RuntimeOrigin::signed(cold), - &hot, - 1, - None, - )); - - assert!(NetworkRegistrationQueue::::get().len() == 1); - assert!(DissolveCleanupQueue::::get().contains(&n1)); - assert!(!NetworksAdded::::get(n1)); - }); -} - -#[test] -fn set_new_network_state_fails_when_subnet_limit_reached() { - new_test_ext(1).execute_with(|| { - SubnetLimit::::put(1u16); - let _n1 = add_dynamic_network(&U256::from(10_002), &U256::from(10_001)); - - let cold = U256::from(10_011); - let hot = U256::from(10_012); - let lock_amount = SubtensorModule::get_network_lock_cost(); - add_balance_to_coldkey_account(&cold, lock_amount.saturating_mul(2.into()).into()); - - assert_err!( - SubtensorModule::set_new_network_state( - &cold, - &hot, - 1, - None, - lock_amount, - SubtensorModule::get_median_subnet_alpha_price(), - None, - ), - Error::::SubnetLimitReached - ); - - // No partial state was written. - assert_eq!(TotalNetworks::::get(), 1); - assert!(!SubtensorModule::hotkey_account_exists(&hot)); - }); -} - -#[test] -fn set_new_network_state_stores_identity_and_emits_events() { - new_test_ext(1).execute_with(|| { - let cold = U256::from(10_101); - let hot = U256::from(10_102); - let lock_amount = SubtensorModule::get_network_lock_cost(); - add_balance_to_coldkey_account(&cold, lock_amount.saturating_mul(2.into()).into()); - - let identity = SubnetIdentityOfV3 { - subnet_name: b"my subnet".to_vec(), - github_repo: b"https://github.com/example/repo".to_vec(), - subnet_contact: b"contact@example.com".to_vec(), - subnet_url: b"https://example.com".to_vec(), - discord: b"discord".to_vec(), - description: b"description".to_vec(), - logo_url: b"https://example.com/logo.png".to_vec(), - additional: b"".to_vec(), - }; - - let netuid = SubtensorModule::get_next_netuid(); - System::reset_events(); - - assert_ok!(SubtensorModule::set_new_network_state( - &cold, - &hot, - 1, - Some(identity.clone()), - lock_amount, - SubtensorModule::get_median_subnet_alpha_price(), - None, - )); - - assert_eq!(SubnetIdentitiesV3::::get(netuid), Some(identity)); - let events = System::events(); - assert!(events.iter().any(|e| matches!( - &e.event, - RuntimeEvent::SubtensorModule(Event::SubnetIdentitySet(n)) if *n == netuid - ))); - assert!(events.iter().any(|e| matches!( - &e.event, - RuntimeEvent::SubtensorModule(Event::NetworkAdded(n, m)) if *n == netuid && *m == 1 - ))); - }); -} - -#[test] -fn set_new_network_state_uses_provided_median_price_for_pool_alpha() { - new_test_ext(1).execute_with(|| { - let cold = U256::from(10_201); - let hot = U256::from(10_202); - - // Lock twice the min lock so the pool is seeded from the actual lock amount. - let min_lock = SubtensorModule::get_network_min_lock(); - let lock_amount = min_lock.saturating_mul(2.into()); - add_balance_to_coldkey_account(&cold, lock_amount.saturating_mul(2.into()).into()); - - let netuid = SubtensorModule::get_next_netuid(); - let price = U64F64::from_num(2); - - assert_ok!(SubtensorModule::set_new_network_state( - &cold, - &hot, - 1, - None, - lock_amount, - price, - None, - )); - - // Pool TAO equals the actual lock; alpha reserve is tao / price. - assert_eq!(SubnetTAO::::get(netuid), lock_amount); - let expected_alpha: u64 = u64::from(lock_amount) / 2; - assert_eq!( - SubnetAlphaIn::::get(netuid), - AlphaBalance::from(expected_alpha) - ); - }); -} - -#[test] -fn set_new_network_state_seeds_pool_with_min_lock_floor() { - new_test_ext(1).execute_with(|| { - let cold = U256::from(10_301); - let hot = U256::from(10_302); - add_balance_to_coldkey_account(&cold, 1_000_000_000.into()); - - let netuid = SubtensorModule::get_next_netuid(); - let min_lock = SubtensorModule::get_network_min_lock(); - - // Zero lock: the pool must still be seeded with the min lock floor. - assert_ok!(SubtensorModule::set_new_network_state( - &cold, - &hot, - 1, - None, - TaoBalance::ZERO, - U64F64::from_num(1), - None, - )); - - assert_eq!(SubnetTAO::::get(netuid), min_lock); - assert_eq!( - SubnetAlphaIn::::get(netuid), - AlphaBalance::from(u64::from(min_lock)) - ); - assert_eq!(SubnetLocked::::get(netuid), TaoBalance::ZERO); - }); -} - -#[test] -fn set_new_network_state_fund_locked_releases_balance_lock() { - new_test_ext(1).execute_with(|| { - let cold = U256::from(10_401); - let hot = U256::from(10_402); - let lock_amount = SubtensorModule::get_network_lock_cost(); - add_balance_to_coldkey_account(&cold, lock_amount.saturating_mul(2.into()).into()); - - let lock_id = NetworkRegistrationLockId::::get(); - let mut identifier = [0u8; 8]; - identifier[..4].copy_from_slice(b"rglk"); - identifier[4..8].copy_from_slice(&lock_id.to_le_bytes()); - - assert_ok!(SubtensorModule::lock_network_registration_cost( - &cold, - lock_amount.into(), - 0 - )); - assert!( - pallet_balances::Locks::::get(cold) - .iter() - .any(|l| l.id == identifier), - "registration lock must exist before processing" - ); - - let netuid = SubtensorModule::get_next_netuid(); - - assert_ok!(SubtensorModule::set_new_network_state( - &cold, - &hot, - 1, - None, - lock_amount, - SubtensorModule::get_median_subnet_alpha_price(), - Some(lock_id), - )); - - assert!( - pallet_balances::Locks::::get(cold) - .iter() - .all(|l| l.id != identifier), - "registration lock must be released after processing" - ); - assert!(SubtensorModule::if_subnet_exist(netuid)); - assert_eq!(SubnetLocked::::get(netuid), lock_amount); - }); -} - -#[test] -fn process_network_registration_queue_noop_when_empty() { - new_test_ext(1).execute_with(|| { - let networks_before = TotalNetworks::::get(); - - SubtensorModule::process_network_registration_queue(); - - assert!(NetworkRegistrationQueue::::get().is_empty()); - assert_eq!(TotalNetworks::::get(), networks_before); - }); -} - -#[test] -fn process_network_registration_queue_waits_for_cleanup_completion() { - new_test_ext(0).execute_with(|| { - SubnetLimit::::put(2u16); - - let n1 = add_dynamic_network(&U256::from(10_502), &U256::from(10_501)); - let _n2 = add_dynamic_network(&U256::from(10_602), &U256::from(10_601)); - - assert_ok!(SubtensorModule::do_dissolve_network(n1)); - - let cold = U256::from(10_701); - let hot = U256::from(10_702); - let lock_amount = SubtensorModule::get_network_lock_cost(); - add_balance_to_coldkey_account(&cold, lock_amount.saturating_mul(2.into()).into()); - - assert_ok!(SubtensorModule::do_register_network( - RuntimeOrigin::signed(cold), - &hot, - 1, - None, - )); - assert_eq!(NetworkRegistrationQueue::::get().len(), 1); - - // Cleanup is still pending: the queued registration must not be released. - SubtensorModule::process_network_registration_queue(); - - assert_eq!(NetworkRegistrationQueue::::get().len(), 1); - assert!(!SubtensorModule::hotkey_account_exists(&hot)); - assert_eq!(TotalNetworks::::get(), 1); - - // Once cleanup completes, the same call releases the registration. - DissolveCleanupQueue::::kill(); - SubtensorModule::process_network_registration_queue(); - - assert!(NetworkRegistrationQueue::::get().is_empty()); - assert!(SubtensorModule::hotkey_account_exists(&hot)); - assert_eq!(TotalNetworks::::get(), 2); - }); -} - -#[test] -fn process_network_registration_queue_processes_one_entry_per_call() { - new_test_ext(0).execute_with(|| { - SubnetLimit::::put(3u16); - - let n1 = add_dynamic_network(&U256::from(10_802), &U256::from(10_801)); - let n2 = add_dynamic_network(&U256::from(10_902), &U256::from(10_901)); - let _n3 = add_dynamic_network(&U256::from(11_002), &U256::from(11_001)); - - assert_ok!(SubtensorModule::do_dissolve_network(n1)); - assert_ok!(SubtensorModule::do_dissolve_network(n2)); - assert_eq!(DissolveCleanupQueue::::get().len(), 2); - - let cold_a = U256::from(11_101); - let hot_a = U256::from(11_102); - let cold_b = U256::from(11_201); - let hot_b = U256::from(11_202); - for cold in [&cold_a, &cold_b] { - let lock_amount = SubtensorModule::get_network_lock_cost(); - add_balance_to_coldkey_account(cold, lock_amount.saturating_mul(2.into()).into()); - } - - assert_ok!(SubtensorModule::do_register_network( - RuntimeOrigin::signed(cold_a), - &hot_a, - 1, - None, - )); - assert_ok!(SubtensorModule::do_register_network( - RuntimeOrigin::signed(cold_b), - &hot_b, - 1, - None, - )); - assert_eq!(NetworkRegistrationQueue::::get().len(), 2); - - DissolveCleanupQueue::::kill(); - - // First call processes only the first (FIFO) entry. - SubtensorModule::process_network_registration_queue(); - assert_eq!(NetworkRegistrationQueue::::get().len(), 1); - assert!(SubtensorModule::hotkey_account_exists(&hot_a)); - assert!(!SubtensorModule::hotkey_account_exists(&hot_b)); - assert_eq!(NetworkRegistrationQueue::::get()[0].coldkey, cold_b); - - // Second call processes the remaining entry. - SubtensorModule::process_network_registration_queue(); - assert!(NetworkRegistrationQueue::::get().is_empty()); - assert!(SubtensorModule::hotkey_account_exists(&hot_b)); - assert_eq!(TotalNetworks::::get(), 3); - }); -} - -#[test] -fn process_network_registration_queue_unlocks_funds_and_charges_coldkey() { - new_test_ext(0).execute_with(|| { - SubnetLimit::::put(2u16); - - let n1 = add_dynamic_network(&U256::from(11_302), &U256::from(11_301)); - let n2 = add_dynamic_network(&U256::from(11_402), &U256::from(11_401)); - - assert_ok!(SubtensorModule::do_dissolve_network(n1)); - - let cold = U256::from(11_501); - let hot = U256::from(11_502); - let lock_amount = SubtensorModule::get_network_lock_cost(); - let lock_id = NetworkRegistrationLockId::::get(); - let mut identifier = [0u8; 8]; - identifier[..4].copy_from_slice(b"rglk"); - identifier[4..8].copy_from_slice(&lock_id.to_le_bytes()); - add_balance_to_coldkey_account(&cold, lock_amount.saturating_mul(3.into()).into()); - - assert_ok!(SubtensorModule::do_register_network( - RuntimeOrigin::signed(cold), - &hot, - 1, - None, - )); - - // Funds are locked while queued. - assert!( - pallet_balances::Locks::::get(cold) - .iter() - .any(|l| l.id == identifier) - ); - let queued_lock = NetworkRegistrationQueue::::get()[0].lock_amount; - // Use free balance: the reducible balance is already reduced by the lock. - let balance_before = pallet_balances::Pallet::::free_balance(cold); - - DissolveCleanupQueue::::kill(); - SubtensorModule::process_network_registration_queue(); - - // Lock released and the lock cost transferred to the new subnet. - assert!( - pallet_balances::Locks::::get(cold) - .iter() - .all(|l| l.id != identifier) - ); - let balance_after = pallet_balances::Pallet::::free_balance(cold); - assert_eq!(balance_before.saturating_sub(balance_after), queued_lock); - - let new_netuid = NetworksAdded::::iter() - .find(|(netuid, added)| *added && *netuid != n2) - .map(|(netuid, _)| netuid) - .expect("queued registration should create a new subnet"); - assert_eq!(SubnetOwner::::get(new_netuid), cold); - assert_eq!(SubnetLocked::::get(new_netuid), queued_lock); - }); -} diff --git a/pallets/subtensor/src/tests/networks/destroy_alpha_stakes.rs b/pallets/subtensor/src/tests/networks/destroy_alpha_stakes.rs new file mode 100644 index 0000000000..99aee01998 --- /dev/null +++ b/pallets/subtensor/src/tests/networks/destroy_alpha_stakes.rs @@ -0,0 +1,513 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! `destroy_alpha_in_out_stakes` pro-rata payouts, lock cleanup, and refund gating. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn destroy_alpha_out_multiple_stakers_pro_rata() { + new_test_ext(0).execute_with(|| { + // 1. Owner & subnet + let owner_cold = U256::from(10); + let owner_hot = U256::from(20); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + remove_owner_registration_stake(netuid); + + // Mark this subnet as *legacy* so owner refund path is enabled. + let reg_at = NetworkRegisteredAt::::get(netuid); + NetworkRegistrationStartBlock::::put(reg_at.saturating_add(1)); + + // 2. Two stakers on that subnet + let (c1, h1) = (U256::from(111), U256::from(211)); + let (c2, h2) = (U256::from(222), U256::from(333)); + register_ok_neuron(netuid, h1, c1, 0); + register_ok_neuron(netuid, h2, c2, 0); + + // 3. Stake 30 : 70 (s1 : s2) in TAO + let min_total = DefaultMinStake::::get(); + let min_total_u64: u64 = min_total.into(); + let s1: u64 = 3u64 * min_total_u64; + let s2: u64 = 7u64 * min_total_u64; + + add_balance_to_coldkey_account(&c1, (s1 + 50_000).into()); + add_balance_to_coldkey_account(&c2, (s2 + 50_000).into()); + + assert_ok!(SubtensorModule::do_add_stake( + RuntimeOrigin::signed(c1), + h1, + netuid, + s1.into() + )); + assert_ok!(SubtensorModule::do_add_stake( + RuntimeOrigin::signed(c2), + h2, + netuid, + s2.into() + )); + + // 4. α-out snapshot + + SubnetAlphaIn::::insert(netuid, AlphaBalance::ZERO); + SubnetProtocolAlpha::::insert(netuid, AlphaBalance::ZERO); + let a1: u128 = sf_to_u128(&AlphaV2::::get((h1, c1, netuid))); + let a2: u128 = sf_to_u128(&AlphaV2::::get((h2, c2, netuid))); + let atotal = a1 + a2; + + // 5. TAO pot & lock + let tao_pot: u64 = 10_000; + SubnetTAO::::insert(netuid, TaoBalance::from(tao_pot)); + SubtensorModule::set_subnet_locked_balance(netuid, TaoBalance::from(5_000)); + + // 6. Balances before + let c1_before = SubtensorModule::get_coldkey_balance(&c1); + let c2_before = SubtensorModule::get_coldkey_balance(&c2); + let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); + + // 7. Run the (now credit-to-coldkey) logic + destroy_alpha_in_out_stakes_full_pipeline_for_test(netuid); + + // 8. Expected τ shares via largest remainder + let prod1 = (tao_pot as u128) * a1; + let prod2 = (tao_pot as u128) * a2; + let mut s1_share = (prod1 / atotal) as u64; + let mut s2_share = (prod2 / atotal) as u64; + let distributed = s1_share + s2_share; + if distributed < tao_pot { + // Assign leftover to larger remainder + let r1 = prod1 % atotal; + let r2 = prod2 % atotal; + if r1 >= r2 { + s1_share += 1; + } else { + s2_share += 1; + } + } + + // 9. Cold-key balances must have increased accordingly + assert_eq!( + SubtensorModule::get_coldkey_balance(&c1), + c1_before + s1_share.into() + ); + assert_eq!( + SubtensorModule::get_coldkey_balance(&c2), + c2_before + s2_share.into() + ); + + // 10. Owner refund (5 000 τ) to cold-key (no emission) + assert_eq!( + SubtensorModule::get_coldkey_balance(&owner_cold), + owner_before + 5_000.into() + ); + + // 11. α entries cleared for the subnet + assert!(!AlphaV2::::contains_key((h1, c1, netuid))); + assert!(!AlphaV2::::contains_key((h2, c2, netuid))); + }); +} + +#[test] +fn destroy_alpha_in_out_stakes_cleans_locking_coldkeys() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(10); + let owner_hot = U256::from(20); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + remove_owner_registration_stake(netuid); + + let coldkey = U256::from(111); + let hotkey = U256::from(222); + let other_netuid = NetUid::from(u16::from(netuid) + 1); + let lock = LockState { + locked_mass: 10u64.into(), + conviction: U64F64::from_num(1), + last_update: 1, + }; + + Lock::::insert((coldkey, netuid, hotkey), lock.clone()); + LockingColdkeys::::insert((netuid, hotkey, coldkey), ()); + Lock::::insert((coldkey, other_netuid, hotkey), lock); + LockingColdkeys::::insert((other_netuid, hotkey, coldkey), ()); + + DissolveCleanupQueue::::set(vec![netuid]); + run_block_idle(); + + assert!(!Lock::::contains_key((coldkey, netuid, hotkey))); + assert!(!LockingColdkeys::::contains_key(( + netuid, hotkey, coldkey + ))); + assert!(Lock::::contains_key((coldkey, other_netuid, hotkey))); + assert!(LockingColdkeys::::contains_key(( + other_netuid, + hotkey, + coldkey + ))); + }); +} + +#[test] +fn destroy_alpha_in_out_stakes_cleans_all_lock_aggregates() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(10); + let owner_hot = U256::from(20); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + remove_owner_registration_stake(netuid); + + let coldkey = U256::from(111); + let hotkey = U256::from(222); + let other_netuid = NetUid::from(u16::from(netuid) + 1); + let lock = LockState { + locked_mass: 10u64.into(), + conviction: U64F64::from_num(1), + last_update: 1, + }; + + HotkeyLock::::insert(netuid, hotkey, lock.clone()); + DecayingHotkeyLock::::insert(netuid, hotkey, lock.clone()); + OwnerLock::::insert(netuid, lock.clone()); + DecayingOwnerLock::::insert(netuid, lock.clone()); + DecayingLock::::insert(coldkey, netuid, false); + + HotkeyLock::::insert(other_netuid, hotkey, lock.clone()); + DecayingHotkeyLock::::insert(other_netuid, hotkey, lock.clone()); + OwnerLock::::insert(other_netuid, lock.clone()); + DecayingOwnerLock::::insert(other_netuid, lock); + DecayingLock::::insert(coldkey, other_netuid, false); + + DissolveCleanupQueue::::set(vec![netuid]); + run_block_idle(); + + assert!(!HotkeyLock::::contains_key(netuid, hotkey)); + assert!(!DecayingHotkeyLock::::contains_key(netuid, hotkey)); + assert!(!OwnerLock::::contains_key(netuid)); + assert!(!DecayingOwnerLock::::contains_key(netuid)); + assert!(!DecayingLock::::contains_key(coldkey, netuid)); + + assert!(HotkeyLock::::contains_key(other_netuid, hotkey)); + assert!(DecayingHotkeyLock::::contains_key( + other_netuid, + hotkey + )); + assert!(OwnerLock::::contains_key(other_netuid)); + assert!(DecayingOwnerLock::::contains_key(other_netuid)); + assert!(DecayingLock::::contains_key(coldkey, other_netuid)); + }); +} + +#[allow(clippy::indexing_slicing)] +#[test] +fn destroy_alpha_out_many_stakers_complex_distribution() { + new_test_ext(0).execute_with(|| { + // ── 1) create subnet with 20 stakers ──────────────────────────────── + let owner_cold = U256::from(1_000); + let owner_hot = U256::from(2_000); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + remove_owner_registration_stake(netuid); + SubtensorModule::set_max_registrations_per_block(netuid, 1_000u16); + SubtensorModule::set_target_registrations_per_interval(netuid, 1_000u16); + + // Mark this subnet as *legacy* so owner refund path is enabled. + let reg_at = NetworkRegisteredAt::::get(netuid); + NetworkRegistrationStartBlock::::put(reg_at.saturating_add(1)); + + // Runtime-exact min amount = min_stake + fee + let min_amount = { + let min_stake = DefaultMinStake::::get(); + let fee = ::SwapInterface::approx_fee_amount( + netuid.into(), + min_stake, + ); + // Double the fees because fee is calculated for min_stake, not for min_amount + min_stake + fee * 2.into() + }; + + const N: usize = 20; + let mut cold = [U256::zero(); N]; + let mut hot = [U256::zero(); N]; + let mut stake = [0u64; N]; + + let min_amount_u64: u64 = min_amount.into(); + for i in 0..N { + cold[i] = U256::from(10_000 + 2 * i as u32); + hot[i] = U256::from(10_001 + 2 * i as u32); + stake[i] = (i as u64 + 1u64) * min_amount_u64; // multiples of min_amount + + register_ok_neuron(netuid, hot[i], cold[i], 0); + add_balance_to_coldkey_account(&cold[i], (stake[i] + 100_000).into()); + + assert_ok!(SubtensorModule::do_add_stake( + RuntimeOrigin::signed(cold[i]), + hot[i], + netuid, + stake[i].into() + )); + } + + // ── 2) α-out snapshot ─────────────────────────────────────────────── + let mut alpha = [0u128; N]; + let mut alpha_sum: u128 = 0; + for i in 0..N { + alpha[i] = sf_to_u128(&AlphaV2::::get((hot[i], cold[i], netuid))); + alpha_sum += alpha[i]; + } + + // ── 3) TAO pot & subnet lock ──────────────────────────────────────── + let tao_pot: u64 = 123_456; + let lock: u64 = 30_000; + SubnetTAO::::insert(netuid, TaoBalance::from(tao_pot)); + SubtensorModule::set_subnet_locked_balance(netuid, TaoBalance::from(lock)); + + // ensure there was some Alpha issued + assert!(SubtensorModule::get_alpha_issuance(netuid).to_u64() > 0); + + // Owner already earned some emission; owner-cut = 50 % + SubnetOwnerCut::::put(32_768u16); // ~ 0.5 in fixed-point + + // ── 4) balances before ────────────────────────────────────────────── + let mut bal_before = [TaoBalance::new(0); N]; + for i in 0..N { + bal_before[i] = SubtensorModule::get_coldkey_balance(&cold[i]); + } + let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); + + // ── 5) expected τ share per pallet algorithm (incl. remainder) ───── + + SubnetAlphaIn::::insert(netuid, AlphaBalance::ZERO); + SubnetProtocolAlpha::::insert(netuid, AlphaBalance::ZERO); + let mut share = [0u64; N]; + let mut rem = [0u128; N]; + let mut paid: u128 = 0; + + for i in 0..N { + let prod = tao_pot as u128 * alpha[i]; + share[i] = (prod / alpha_sum) as u64; + rem[i] = prod % alpha_sum; + paid += share[i] as u128; + } + let leftover = tao_pot as u128 - paid; + let mut idx: Vec<_> = (0..N).collect(); + idx.sort_by_key(|i| core::cmp::Reverse(rem[*i])); + for i in 0..leftover as usize { + share[idx[i]] += 1; + } + + // ── 5b) expected owner refund with price-aware emission deduction ─── + let frac: U96F32 = SubtensorModule::get_float_subnet_owner_cut(); + let total_emitted_alpha: u64 = SubtensorModule::get_alpha_issuance(netuid).to_u64(); + let owner_alpha_u64: u64 = U96F32::from_num(total_emitted_alpha) + .saturating_mul(frac) + .floor() + .saturating_to_num::(); + + let owner_emission_tao: u64 = { + // Fallback matches the pallet's fallback + let price: U96F32 = U96F32::from_num( + ::SwapInterface::current_alpha_price(netuid.into()), + ); + U96F32::from_num(owner_alpha_u64) + .saturating_mul(price) + .floor() + .saturating_to_num::() + }; + + let expected_refund = lock.saturating_sub(owner_emission_tao); + + // ── 6) run distribution (credits τ to coldkeys, wipes α state) ───── + destroy_alpha_in_out_stakes_full_pipeline_for_test(netuid); + + // ── 7) post checks ────────────────────────────────────────────────── + for i in 0..N { + // cold-key balances increased by expected τ share + assert_eq!( + SubtensorModule::get_coldkey_balance(&cold[i]), + bal_before[i] + share[i].into(), + "staker {i} cold-key balance changed unexpectedly" + ); + } + + // owner refund + assert_eq!( + SubtensorModule::get_coldkey_balance(&owner_cold), + owner_before + expected_refund.into() + ); + + // α cleared for dissolved subnet & related counters reset + assert!(AlphaV2::::iter().all(|((_h, _c, n), _)| n != netuid)); + assert_eq!(SubnetAlphaIn::::get(netuid), 0.into()); + assert_eq!(SubnetAlphaOut::::get(netuid), 0.into()); + assert_eq!(SubtensorModule::get_subnet_locked_balance(netuid), 0.into()); + }); +} + +#[test] +fn destroy_alpha_out_refund_gating_by_registration_block() { + // ────────────────────────────────────────────────────────────────────── + // Case A: LEGACY subnet → refund applied + // ────────────────────────────────────────────────────────────────────── + new_test_ext(0).execute_with(|| { + // Owner + subnet + let owner_cold = U256::from(10_000); + let owner_hot = U256::from(20_000); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + remove_owner_registration_stake(netuid); + + // Mark as *legacy*: registered_at < start_block + let reg_at = NetworkRegisteredAt::::get(netuid); + NetworkRegistrationStartBlock::::put(reg_at.saturating_add(1)); + + // Lock and (nonzero) emissions + let lock_u64: u64 = 50_000; + SubtensorModule::set_subnet_locked_balance(netuid, TaoBalance::from(lock_u64)); + // Owner cut ≈ 50% + SubnetOwnerCut::::put(32_768u16); + + // give some stake to other key + let other_cold = U256::from(1_234); + let other_hot = U256::from(2_345); + mock_increase_stake_for_hotkey_and_coldkey_on_subnet( + &other_hot, + &other_cold, + netuid, + AlphaBalance::from(30u64), // not nearly enough to cover the lock + ); + + // ensure there was some Alpha issued + assert!(SubtensorModule::get_alpha_issuance(netuid).to_u64() > 0); + + // Compute expected refund using the same math as the pallet + let frac: U96F32 = SubtensorModule::get_float_subnet_owner_cut(); + let total_emitted_alpha: u64 = SubtensorModule::get_alpha_issuance(netuid).to_u64(); + let owner_alpha_u64: u64 = U96F32::from_num(total_emitted_alpha) + .saturating_mul(frac) + .floor() + .saturating_to_num::(); + + let owner_emission_tao_u64 = { + let price: U96F32 = U96F32::from_num( + ::SwapInterface::current_alpha_price(netuid.into()), + ); + U96F32::from_num(owner_alpha_u64) + .saturating_mul(price) + .floor() + .saturating_to_num::() + }; + + let expected_refund: u64 = lock_u64.saturating_sub(owner_emission_tao_u64); + + // Balances before + let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); + + // Run the path under test + let mut weight_meter = + frame_support::weights::WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)); + // total alpha tracked in CurrentDissolveCleanupStatus; + // distributed tao tracked in CurrentDissolveCleanupStatus; + { + let mut status = dissolve_cleanup_status(netuid); + status.subnet_total_alpha_value = Some(0); + SubtensorModule::destroy_alpha_in_out_stakes(netuid, &mut weight_meter, &mut status); + } + + // Owner received their refund… + let owner_after = SubtensorModule::get_coldkey_balance(&owner_cold); + assert_eq!(owner_after, owner_before + expected_refund.into()); + + // …and the lock is always cleared to zero by destroy_alpha_in_out_stakes. + assert_eq!( + SubtensorModule::get_subnet_locked_balance(netuid), + TaoBalance::from(0u64) + ); + }); + + // ────────────────────────────────────────────────────────────────────── + // Case B: NON‑LEGACY subnet → NO refund; + // ────────────────────────────────────────────────────────────────────── + new_test_ext(0).execute_with(|| { + // Owner + subnet + let owner_cold = U256::from(1_111); + let owner_hot = U256::from(2_222); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + remove_owner_registration_stake(netuid); + + // Explicitly set start_block <= registered_at to make it non‑legacy. + let reg_at = NetworkRegisteredAt::::get(netuid); + NetworkRegistrationStartBlock::::put(reg_at); + + // Lock and emissions present (should be ignored for refund) + let lock_u64: u64 = 42_000; + SubtensorModule::set_subnet_locked_balance(netuid, TaoBalance::from(lock_u64)); + // give some stake to other key + let other_cold = U256::from(1_234); + let other_hot = U256::from(2_345); + mock_increase_stake_for_hotkey_and_coldkey_on_subnet( + &other_hot, + &other_cold, + netuid, + AlphaBalance::from(300u64), // not nearly enough to cover the lock + ); + // ensure there was some Alpha issued + assert!(SubtensorModule::get_alpha_issuance(netuid).to_u64() > 0); + SubnetOwnerCut::::put(32_768u16); // ~50% + + // Balances before + let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); + + // Run the path under test + let mut weight_meter = + frame_support::weights::WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)); + // total alpha tracked in CurrentDissolveCleanupStatus; + // distributed tao tracked in CurrentDissolveCleanupStatus; + { + let mut status = dissolve_cleanup_status(netuid); + status.subnet_total_alpha_value = Some(0); + SubtensorModule::destroy_alpha_in_out_stakes(netuid, &mut weight_meter, &mut status); + } + + // No refund for non‑legacy + let owner_after = SubtensorModule::get_coldkey_balance(&owner_cold); + assert_eq!(owner_after, owner_before); + + // Lock is still cleared to zero by the routine + assert_eq!( + SubtensorModule::get_subnet_locked_balance(netuid), + TaoBalance::from(0u64) + ); + }); + + // ────────────────────────────────────────────────────────────────────── + // Case C: LEGACY subnet but lock = 0 → no refund; + // ────────────────────────────────────────────────────────────────────── + new_test_ext(0).execute_with(|| { + // Owner + subnet + let owner_cold = U256::from(9_999); + let owner_hot = U256::from(8_888); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + remove_owner_registration_stake(netuid); + + // Mark as *legacy* + let reg_at = NetworkRegisteredAt::::get(netuid); + NetworkRegistrationStartBlock::::put(reg_at.saturating_add(1)); + + // lock = 0; emissions present (must not matter) + SubtensorModule::set_subnet_locked_balance(netuid, TaoBalance::from(0u64)); + SubnetAlphaOut::::insert(netuid, AlphaBalance::from(10_000)); + // ensure there was some Alpha issued + assert!(SubtensorModule::get_alpha_issuance(netuid).to_u64() > 0); + SubnetOwnerCut::::put(32_768u16); // ~50% + + let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); + let mut weight_meter = + frame_support::weights::WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)); + { + let mut status = dissolve_cleanup_status(netuid); + status.subnet_total_alpha_value = Some(0); + SubtensorModule::destroy_alpha_in_out_stakes(netuid, &mut weight_meter, &mut status); + } + let owner_after = SubtensorModule::get_coldkey_balance(&owner_cold); + + // No refund possible when lock = 0 + assert_eq!(owner_after, owner_before); + assert_eq!( + SubtensorModule::get_subnet_locked_balance(netuid), + TaoBalance::from(0u64) + ); + }); +} diff --git a/pallets/subtensor/src/tests/networks/dissolve_async_cleanup.rs b/pallets/subtensor/src/tests/networks/dissolve_async_cleanup.rs new file mode 100644 index 0000000000..b291d39874 --- /dev/null +++ b/pallets/subtensor/src/tests/networks/dissolve_async_cleanup.rs @@ -0,0 +1,149 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! Async dissolve cleanup queue, on_idle phases, and pending-subnet account id. + +use super::prelude::*; + +#[test] +fn dissolve_defers_cleanup_until_on_idle() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(11); + let owner_hot = U256::from(12); + let net = add_dynamic_network(&owner_hot, &owner_cold); + + // Set up EVM association data to verify it gets cleaned up too. + let evm_key = sp_core::H160::from_low_u64_be(42); + SubtensorModule::set_associated_evm_address(net, 0u16, evm_key, 1u64); + assert!(AssociatedEvmAddress::::contains_key(net, 0u16)); + assert!(!AssociatedUidsByEvmAddress::::get(net, evm_key).is_empty()); + + assert!(SubnetOwner::::contains_key(net)); + assert!(SubnetOwnerHotkey::::contains_key(net)); + assert!(NetworkRegisteredAt::::contains_key(net)); + assert!(!DissolveCleanupQueue::::get().contains(&net)); + + assert_ok!(SubtensorModule::do_dissolve_network(net)); + + // Network is no longer considered "existing" but data is not cleaned yet. + assert!(!SubtensorModule::subnet_exists(net)); + assert!(DissolveCleanupQueue::::get().contains(&net)); + assert!(SubnetOwner::::contains_key(net)); + assert!(NetworkRegisteredAt::::contains_key(net)); + // EVM data still present before on_idle cleanup. + assert!(AssociatedEvmAddress::::contains_key(net, 0u16)); + assert!(!AssociatedUidsByEvmAddress::::get(net, evm_key).is_empty()); + + // Cleanup happens in on_idle. + run_block_idle(); + assert!(!NetworkRegisteredAt::::contains_key(net)); + assert!(!SubnetOwner::::contains_key(net)); + assert!(!DissolveCleanupQueue::::get().contains(&net)); + // EVM data cleaned up as part of NetworkMapParameters phase. + assert!(!AssociatedEvmAddress::::contains_key(net, 0u16)); + assert!(AssociatedUidsByEvmAddress::::get(net, evm_key).is_empty()); + }); +} + +#[test] +fn get_subnet_account_id_some_while_dissolved_cleanup_pending() { + new_test_ext(1).execute_with(|| { + let cold = U256::from(44_001); + let hot = U256::from(44_002); + let net = add_dynamic_network(&hot, &cold); + assert_ok!(SubtensorModule::do_dissolve_network(net)); + assert!(!SubtensorModule::subnet_exists(net)); + assert!(DissolveCleanupQueue::::get().contains(&net)); + assert!( + SubtensorModule::get_subnet_account_id(net).is_some(), + "subnet TAO account must stay derivable during async dissolve cleanup" + ); + }); +} + +#[test] +fn dissolve_async_cleanup_leaves_phase_unset_until_idle_finishes() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(910); + let owner_hot = U256::from(911); + let net = add_dynamic_network(&owner_hot, &owner_cold); + + assert_ok!(SubtensorModule::do_dissolve_network(net)); + assert!( + DissolveCleanupQueue::::get().contains(&net), + "dissolved netuid should be queued for on_idle cleanup" + ); + assert!( + CurrentDissolveCleanupStatus::::get().is_none(), + "global cleanup phase is only driven from on_idle (not from do_dissolve_network)" + ); + + run_block_idle(); + + assert!( + !DissolveCleanupQueue::::get().contains(&net), + "idle cleanup should drain the dissolved net from the queue" + ); + assert!( + CurrentDissolveCleanupStatus::::get().is_none(), + "when the queue is empty, global cleanup phase storage must be cleared" + ); + }); +} + +#[test] +fn dissolve_full_on_idle_emits_dissolved_network_data_cleaned_and_clears_phase() { + // `frame_system::Pallet::events()` stays empty at block #0 in the test externalities; + // use a non-zero block like other event-asserting tests (`recycle_alpha`, etc.). + new_test_ext(1).execute_with(|| { + let owner_cold = U256::from(930); + let owner_hot = U256::from(931); + let net = add_dynamic_network(&owner_hot, &owner_cold); + + assert_ok!(SubtensorModule::do_dissolve_network(net)); + System::reset_events(); + run_block_idle(); + + assert!( + System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::NetworkDissolveCleanupCompleted { netuid: n }) + if *n == net + ) + }), + "expected NetworkDissolveCleanupCompleted after async dissolve pipeline" + ); + assert!( + CurrentDissolveCleanupStatus::::get().is_none(), + "global cleanup phase storage must be cleared when the queue is empty" + ); + }); +} + +#[test] +fn dissolve_two_networks_fifo_cleanup_drains_queue() { + new_test_ext(0).execute_with(|| { + let n1 = add_dynamic_network(&U256::from(940), &U256::from(941)); + let n2 = add_dynamic_network(&U256::from(942), &U256::from(943)); + + assert_ok!(SubtensorModule::do_dissolve_network(n1)); + assert_ok!(SubtensorModule::do_dissolve_network(n2)); + assert_eq!(DissolveCleanupQueue::::get(), vec![n1, n2]); + + let mut guard = 0u32; + while !DissolveCleanupQueue::::get().is_empty() { + guard = guard.saturating_add(1); + assert!( + guard < 256, + "dissolve cleanup should drain in finite idle passes (guard={guard})" + ); + run_block_idle(); + } + + assert!(!SubtensorModule::subnet_exists(n1)); + assert!(!SubtensorModule::subnet_exists(n2)); + assert!( + CurrentDissolveCleanupStatus::::get().is_none(), + "no stale phase after queue drain" + ); + }); +} diff --git a/pallets/subtensor/src/tests/networks/dissolve_refunds.rs b/pallets/subtensor/src/tests/networks/dissolve_refunds.rs new file mode 100644 index 0000000000..db1fa090b3 --- /dev/null +++ b/pallets/subtensor/src/tests/networks/dissolve_refunds.rs @@ -0,0 +1,580 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! `do_dissolve_network` refund, pro-rata TAO, and protocol-alpha share behavior. + +use super::prelude::*; + +#[test] +fn dissolve_no_stakers_no_alpha_no_emission() { + new_test_ext(0).execute_with(|| { + let cold = U256::from(1); + let hot = U256::from(2); + let net = add_dynamic_network(&hot, &cold); + + SubtensorModule::set_subnet_locked_balance(net, TaoBalance::from(0)); + SubnetTAO::::insert(net, TaoBalance::from(0)); + Emission::::insert(net, Vec::::new()); + + let before = SubtensorModule::get_coldkey_balance(&cold); + assert_ok!(SubtensorModule::do_dissolve_network(net)); + let after = SubtensorModule::get_coldkey_balance(&cold); + + // Balance should be unchanged (whatever the network-lock bookkeeping left there) + assert_eq!(after, before); + assert!(!SubtensorModule::subnet_exists(net)); + }); +} + +#[test] +fn dissolve_refunds_full_lock_cost_when_no_emission() { + new_test_ext(0).execute_with(|| { + let cold = U256::from(3); + let hot = U256::from(4); + let net = add_dynamic_network(&hot, &cold); + + // Mark this subnet as *legacy* so owner refund path is enabled. + let reg_at = NetworkRegisteredAt::::get(net); + NetworkRegistrationStartBlock::::put(reg_at.saturating_add(1)); + + let lock: TaoBalance = TaoBalance::from(1_000_000); + SubtensorModule::set_subnet_locked_balance(net, lock); + SubnetTAO::::insert(net, TaoBalance::from(0)); + Emission::::insert(net, Vec::::new()); + + let before = SubtensorModule::get_coldkey_balance(&cold); + assert_ok!(SubtensorModule::do_dissolve_network(net)); + run_block_idle(); + let after = SubtensorModule::get_coldkey_balance(&cold); + + assert_eq!(TaoBalance::from(after), TaoBalance::from(before) + lock); + }); +} + +#[test] +fn dissolve_single_alpha_out_staker_gets_all_tao() { + new_test_ext(0).execute_with(|| { + // 1. Owner & subnet + let owner_cold = U256::from(10); + let owner_hot = U256::from(20); + let net = add_dynamic_network(&owner_hot, &owner_cold); + remove_owner_registration_stake(net); + SubnetAlphaIn::::insert(net, AlphaBalance::ZERO); + SubnetProtocolAlpha::::insert(net, AlphaBalance::ZERO); + + // 2. Single α-out staker + let (s_hot, s_cold) = (U256::from(100), U256::from(200)); + AlphaV2::::insert((s_hot, s_cold, net), sf_from_u64(5_000u64)); + + // Entire TAO pot should be paid to staker's cold-key + let pot: u64 = 99_999; + SubnetTAO::::insert(net, TaoBalance::from(pot)); + SubtensorModule::set_subnet_locked_balance(net, 0.into()); + TotalHotkeyAlpha::::insert(s_hot, net, AlphaBalance::from(5_000u64)); + + // Cold-key balance before + let before = SubtensorModule::get_coldkey_balance(&s_cold); + + // Dissolve + assert_ok!(SubtensorModule::do_dissolve_network(net)); + run_block_idle(); + + // Cold-key received full pot + let after = SubtensorModule::get_coldkey_balance(&s_cold); + assert_eq!(after, before + pot.into()); + + // No α entries left for dissolved subnet + assert!(AlphaV2::::iter().all(|((_h, _c, n), _)| n != net)); + assert!(!SubnetTAO::::contains_key(net)); + }); +} + +#[allow(clippy::indexing_slicing)] +#[test] +fn dissolve_two_stakers_pro_rata_distribution() { + new_test_ext(0).execute_with(|| { + // Subnet + two stakers + let oc = U256::from(50); + let oh = U256::from(51); + let net = add_dynamic_network(&oh, &oc); + remove_owner_registration_stake(net); + SubnetAlphaIn::::insert(net, AlphaBalance::ZERO); + SubnetProtocolAlpha::::insert(net, AlphaBalance::ZERO); + + // Mark this subnet as *legacy* so owner refund path is enabled. + let reg_at = NetworkRegisteredAt::::get(net); + NetworkRegistrationStartBlock::::put(reg_at.saturating_add(1)); + + let (s1_hot, s1_cold, a1) = (U256::from(201), U256::from(301), 300u64); + let (s2_hot, s2_cold, a2) = (U256::from(202), U256::from(302), 700u64); + + AlphaV2::::insert((s1_hot, s1_cold, net), sf_from_u64(a1)); + AlphaV2::::insert((s2_hot, s2_cold, net), sf_from_u64(a2)); + + TotalHotkeyAlpha::::insert(s1_hot, net, AlphaBalance::from(a1)); + TotalHotkeyAlpha::::insert(s2_hot, net, AlphaBalance::from(a2)); + + let pot: u64 = 10_000; + SubnetTAO::::insert(net, TaoBalance::from(pot)); + SubtensorModule::set_subnet_locked_balance(net, 5_000.into()); // owner refund path present; emission = 0 + + // Cold-key balances before + let s1_before = SubtensorModule::get_coldkey_balance(&s1_cold); + let s2_before = SubtensorModule::get_coldkey_balance(&s2_cold); + let owner_before = SubtensorModule::get_coldkey_balance(&oc); + + // Expected τ shares with largest remainder + let total = (a1 + a2) as u128; + let prod1 = (a1 as u128) * (pot as u128); + let prod2 = (a2 as u128) * (pot as u128); + let share1 = (prod1 / total) as u64; + let share2 = (prod2 / total) as u64; + let mut distributed = share1 + share2; + let mut rem = [(s1_cold, prod1 % total), (s2_cold, prod2 % total)]; + if distributed < pot { + rem.sort_by_key(|&(_c, r)| core::cmp::Reverse(r)); + let leftover = pot - distributed; + for _ in 0..leftover as usize { + distributed += 1; + } + } + // Recompute exact expected shares using the same logic + let mut expected1 = share1; + let mut expected2 = share2; + if share1 + share2 < pot { + rem.sort_by_key(|&(_c, r)| core::cmp::Reverse(r)); + if rem[0].0 == s1_cold { + expected1 += 1; + } else { + expected2 += 1; + } + } + + // Dissolve + assert_ok!(SubtensorModule::do_dissolve_network(net)); + run_block_idle(); + + // Cold-keys received their τ shares + assert_eq!( + SubtensorModule::get_coldkey_balance(&s1_cold), + s1_before + expected1.into() + ); + assert_eq!( + SubtensorModule::get_coldkey_balance(&s2_cold), + s2_before + expected2.into() + ); + + // Owner refunded lock (no emission) + assert_eq!( + SubtensorModule::get_coldkey_balance(&oc), + owner_before + 5_000.into() + ); + + // α entries for dissolved subnet gone + assert!(AlphaV2::::iter().all(|((_h, _c, n), _)| n != net)); + }); +} + +#[test] +fn dissolve_owner_cut_refund_logic() { + new_test_ext(0).execute_with(|| { + let oc = U256::from(70); + let oh = U256::from(71); + let net = add_dynamic_network(&oh, &oc); + remove_owner_registration_stake(net); + + // Mark this subnet as *legacy* so owner refund path is enabled. + let reg_at = NetworkRegisteredAt::::get(net); + NetworkRegistrationStartBlock::::put(reg_at.saturating_add(1)); + + // One staker and a TAO pot (not relevant to refund amount). + let sh = U256::from(77); + let sc = U256::from(88); + mock_increase_stake_for_hotkey_and_coldkey_on_subnet( + &sh, + &sc, + net, + AlphaBalance::from(800u64), + ); + SubnetTAO::::insert(net, TaoBalance::from(1_000)); + + // Lock & emissions: total emitted α = 800. + let lock: TaoBalance = TaoBalance::from(2_000); + SubtensorModule::set_subnet_locked_balance(net, lock); + // ensure there was some Alpha issued + assert!(SubtensorModule::get_alpha_issuance(net).to_u64() > 0); + + // Owner cut = 11796 / 65535 (about 18%). + SubnetOwnerCut::::put(11_796u16); + + // Compute expected refund with the SAME math as the pallet. + let frac: U96F32 = SubtensorModule::get_float_subnet_owner_cut(); + let total_emitted_alpha: u64 = SubtensorModule::get_alpha_issuance(net).to_u64(); + let owner_alpha_u64: u64 = U96F32::from_num(total_emitted_alpha) + .saturating_mul(frac) + .floor() + .saturating_to_num::(); + + // Use the current alpha price to estimate the TAO equivalent. + let owner_emission_tao = { + let price: U96F32 = U96F32::saturating_from_num( + ::SwapInterface::current_alpha_price(net.into()), + ); + U96F32::from_num(owner_alpha_u64) + .saturating_mul(price) + .floor() + .saturating_to_num::() + .into() + }; + + let expected_refund: TaoBalance = lock.saturating_sub(owner_emission_tao); + + println!("expected_refund = {:?}", expected_refund); + + let before = SubtensorModule::get_coldkey_balance(&oc); + assert_ok!(SubtensorModule::do_dissolve_network(net)); + run_block_idle(); + let after = SubtensorModule::get_coldkey_balance(&oc); + + assert!(after > before); // some refund is expected + let gain: TaoBalance = after.saturating_sub(before.into()); + assert!( + gain >= expected_refund, + "owner should receive at least the lock-based refund: gain {gain:?} expected_refund {expected_refund:?}" + ); + }); +} + +#[test] +fn dissolve_zero_refund_when_emission_exceeds_lock() { + new_test_ext(0).execute_with(|| { + let oc = U256::from(1_000); + let oh = U256::from(2_000); + let net = add_dynamic_network(&oh, &oc); + remove_owner_registration_stake(net); + + SubtensorModule::set_subnet_locked_balance(net, TaoBalance::from(1_000)); + SubnetOwnerCut::::put(u16::MAX); // 100 % + Emission::::insert(net, vec![AlphaBalance::from(2_000)]); + + let before = SubtensorModule::get_coldkey_balance(&oc); + assert_ok!(SubtensorModule::do_dissolve_network(net)); + let after = SubtensorModule::get_coldkey_balance(&oc); + + assert_eq!(after, before); // no refund + }); +} + +#[test] +fn dissolve_nonexistent_subnet_fails() { + new_test_ext(0).execute_with(|| { + assert_err!( + SubtensorModule::do_dissolve_network(9_999.into()), + Error::::SubnetNotExists + ); + }); +} + +#[test] +fn dissolve_materializes_nonzero_protocol_reservoirs_before_cleanup() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(123); + let owner_hot = U256::from(456); + let net = add_dynamic_network(&owner_hot, &owner_cold); + remove_owner_registration_stake(net); + + // Force the modern dissolve branch where pool alpha participates in + // the protocol denominator. + TaoInRefundDeploymentBlock::::put(0); + NetworkRegisteredAt::::insert(net, 1); + + let reservoir_tao = TaoBalance::from(100_u64); + let reservoir_alpha = AlphaBalance::from(100_u64); + let staker_hot = U256::from(789); + let staker_cold = U256::from(987); + + let subnet_account = SubtensorModule::get_subnet_account_id(net).unwrap(); + add_balance_to_coldkey_account(&subnet_account, reservoir_tao); + + SubnetTAO::::insert(net, TaoBalance::ZERO); + SubtensorModule::set_subnet_locked_balance(net, TaoBalance::ZERO); + SubnetAlphaIn::::insert(net, AlphaBalance::ZERO); + SubnetProtocolAlpha::::insert(net, AlphaBalance::ZERO); + AlphaV2::::insert((staker_hot, staker_cold, net), sf_from_u64(100u64)); + TotalHotkeyAlpha::::insert(staker_hot, net, AlphaBalance::from(100u64)); + pallet_subtensor_swap::BalancerTaoReservoir::::insert(net, reservoir_tao); + pallet_subtensor_swap::BalancerAlphaReservoir::::insert(net, reservoir_alpha); + + let staker_before = SubtensorModule::get_coldkey_balance(&staker_cold); + let issuance_before = TotalIssuance::::get(); + + assert_ok!(SubtensorModule::do_dissolve_network(net)); + + // do_dissolve_network only queues the destructive cleanup, but it must + // materialize pending protocol reservoirs before the queued cleanup can + // compute stake payouts. + assert!(!pallet_subtensor_swap::BalancerTaoReservoir::::contains_key(net)); + assert!(!pallet_subtensor_swap::BalancerAlphaReservoir::::contains_key(net)); + assert_eq!(SubnetTAO::::get(net), reservoir_tao); + assert_eq!(SubnetAlphaIn::::get(net), reservoir_alpha); + assert_eq!( + SubtensorModule::get_coldkey_balance(&staker_cold), + staker_before + ); + + run_block_idle(); + + // Reservoir alpha is treated like materialized protocol pool alpha. + // The staker owns half the denominator, so receives half the reservoir + // TAO pot; the protocol share is recycled. + assert_eq!( + SubtensorModule::get_coldkey_balance(&staker_cold), + staker_before + TaoBalance::from(50_u64) + ); + assert!(TotalIssuance::::get() < issuance_before); + assert!(!NetworksAdded::::contains_key(net)); + assert!(!SubnetOwner::::contains_key(net)); + assert!(!SubnetAlphaIn::::contains_key(net)); + assert!(!SubnetProtocolAlpha::::contains_key(net)); + assert!(!pallet_subtensor_swap::BalancerTaoReservoir::::contains_key(net)); + assert!(!pallet_subtensor_swap::BalancerAlphaReservoir::::contains_key(net)); + }); +} + +#[test] +fn dissolve_alpha_out_but_zero_tao_no_rewards() { + new_test_ext(0).execute_with(|| { + let oc = U256::from(21); + let oh = U256::from(22); + let net = add_dynamic_network(&oh, &oc); + + let sh = U256::from(23); + let sc = U256::from(24); + + AlphaV2::::insert((sh, sc, net), sf_from_u64(1_000u64)); + SubnetTAO::::insert(net, TaoBalance::from(0)); // zero TAO + SubtensorModule::set_subnet_locked_balance(net, TaoBalance::from(0)); + Emission::::insert(net, Vec::::new()); + TotalHotkeyAlpha::::insert(sh, net, AlphaBalance::from(1_000u64)); + + let before = SubtensorModule::get_coldkey_balance(&sc); + assert_ok!(SubtensorModule::do_dissolve_network(net)); + run_block_idle(); + let after = SubtensorModule::get_coldkey_balance(&sc); + + // No reward distributed, α-out cleared. + assert_eq!(after, before); + assert!(AlphaV2::::iter().next().is_none()); + }); +} + +#[test] +fn dissolve_decrements_total_networks() { + new_test_ext(0).execute_with(|| { + let total_before = TotalNetworks::::get(); + + let cold = U256::from(41); + let hot = U256::from(42); + let net = add_dynamic_network(&hot, &cold); + + // Add 100 TAO to subnet account (lock) + let subnet_account = SubtensorModule::get_subnet_account_id(net).unwrap(); + add_balance_to_coldkey_account(&subnet_account, 100_000_000_000_u64.into()); + + // Sanity: adding network increments the counter. + assert_eq!(TotalNetworks::::get(), total_before + 1); + + assert_ok!(SubtensorModule::do_dissolve_network(net)); + assert_eq!(TotalNetworks::::get(), total_before); + }); +} + +#[test] +fn dissolve_rounding_remainder_distribution() { + new_test_ext(0).execute_with(|| { + // 1. Build subnet with two α-out stakers (3 & 2 α) + let oc = U256::from(61); + let oh = U256::from(62); + let net = add_dynamic_network(&oh, &oc); + remove_owner_registration_stake(net); + SubnetAlphaIn::::insert(net, AlphaBalance::ZERO); + SubnetProtocolAlpha::::insert(net, AlphaBalance::ZERO); + + let (s1h, s1c) = (U256::from(63), U256::from(64)); + let (s2h, s2c) = (U256::from(65), U256::from(66)); + + AlphaV2::::insert((s1h, s1c, net), sf_from_u64(3u64)); + AlphaV2::::insert((s2h, s2c, net), sf_from_u64(2u64)); + + SubnetTAO::::insert(net, TaoBalance::from(1)); // TAO pot = 1 + SubtensorModule::set_subnet_locked_balance(net, TaoBalance::from(0)); + + TotalHotkeyAlpha::::insert(s1h, net, AlphaBalance::from(3u64)); + TotalHotkeyAlpha::::insert(s2h, net, AlphaBalance::from(2u64)); + + // Cold-key balances before + let c1_before = SubtensorModule::get_coldkey_balance(&s1c); + let c2_before = SubtensorModule::get_coldkey_balance(&s2c); + + // 3. Run full dissolve flow + assert_ok!(SubtensorModule::do_dissolve_network(net)); + run_block_idle(); + + // 4. s1 (larger remainder) should get +1 τ on cold-key + let c1_after = SubtensorModule::get_coldkey_balance(&s1c); + let c2_after = SubtensorModule::get_coldkey_balance(&s2c); + + assert_eq!(c1_after, c1_before + 1.into()); + assert_eq!(c2_after, c2_before); + + // α records for subnet gone; TAO key gone + assert!(AlphaV2::::iter().all(|((_h, _c, n), _)| n != net)); + assert!(!SubnetTAO::::contains_key(net)); + }); +} + +#[test] +fn dissolve_protocol_alpha_share_is_not_paid_to_users() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(610); + let owner_hot = U256::from(620); + let net = add_dynamic_network(&owner_hot, &owner_cold); + remove_owner_registration_stake(net); + + // Make this subnet pre-deploy for protocol-alpha accounting. + let reg_at = NetworkRegisteredAt::::get(net); + TaoInRefundDeploymentBlock::::put(reg_at.saturating_add(1)); + SubtensorModule::set_subnet_locked_balance(net, TaoBalance::ZERO); + + // Alpha-in is the AMM pool reserve and must NOT participate in the + // deregistration settlement for pre-deploy subnets. Only the chain-bought + // cached protocol + // alpha is converted to TAO pro-rata, exactly like every staker's alpha. + SubnetAlphaIn::::insert(net, AlphaBalance::from(100u64)); + SubnetProtocolAlpha::::insert(net, AlphaBalance::from(50u64)); + + let staker_hot = U256::from(630); + let staker_cold = U256::from(640); + AlphaV2::::insert((staker_hot, staker_cold, net), sf_from_u64(50u64)); + TotalHotkeyAlpha::::insert(staker_hot, net, AlphaBalance::from(50u64)); + + let pot: u64 = 200; + SubnetTAO::::insert(net, TaoBalance::from(pot)); + + let staker_before = SubtensorModule::get_coldkey_balance(&staker_cold); + let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); + + assert_ok!(SubtensorModule::do_dissolve_network(net)); + + run_block_idle(); + + // User gets 50 / (100 alpha-in + 50 cached protocol alpha + 50 user alpha) + // of the TAO pot. The protocol share is withheld from user/owner payout. + // Settlement denominator = 50 cached protocol alpha + 50 user alpha = 100 + // (alpha-in is excluded). The user therefore gets 50/100 of the 200 TAO pot, + // i.e. 100 TAO. The chain-bought alpha's 100 TAO share is withheld from the + // user/owner payout (it is recycled back to the chain, see the dedicated + // recycling test below). + assert_eq!( + SubtensorModule::get_coldkey_balance(&staker_cold), + staker_before + 100.into() + ); + // The owner is not paid the protocol share either (locked balance is zero, so + // there is no refund path that could leak it). + assert_eq!( + SubtensorModule::get_coldkey_balance(&owner_cold), + owner_before + ); + assert!(!SubnetProtocolAlpha::::contains_key(net)); + }); +} + +#[test] +fn dissolve_protocol_alpha_post_deploy_includes_alpha_in() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(611); + let owner_hot = U256::from(621); + + let net = add_dynamic_network(&owner_hot, &owner_cold); + remove_owner_registration_stake(net); + + // Make this subnet post-deploy for protocol-alpha accounting. + TaoInRefundDeploymentBlock::::put(100); + NetworkRegisteredAt::::insert(net, 101); + + SubtensorModule::set_subnet_locked_balance(net, TaoBalance::ZERO); + + SubnetAlphaIn::::insert(net, AlphaBalance::from(100u64)); + SubnetProtocolAlpha::::insert(net, AlphaBalance::from(50u64)); + + let staker_hot = U256::from(631); + let staker_cold = U256::from(641); + + AlphaV2::::insert((staker_hot, staker_cold, net), sf_from_u64(50u64)); + TotalHotkeyAlpha::::insert(staker_hot, net, AlphaBalance::from(50u64)); + + let pot: u64 = 200; + SubnetTAO::::insert(net, TaoBalance::from(pot)); + + let staker_before = SubtensorModule::get_coldkey_balance(&staker_cold); + let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); + + assert_ok!(SubtensorModule::do_dissolve_network(net)); + run_block_idle(); + + // Post-deploy denominator = 100 alpha-in + 50 cached protocol alpha + // + 50 user alpha = 200. The user gets 50/200 of the 200 TAO pot. + assert_eq!( + SubtensorModule::get_coldkey_balance(&staker_cold), + staker_before + 50.into() + ); + + assert_eq!( + SubtensorModule::get_coldkey_balance(&owner_cold), + owner_before + ); + + assert!(!SubnetProtocolAlpha::::contains_key(net)); + }); +} + +#[test] +fn dissolve_chain_bought_alpha_is_converted_to_tao_and_recycled() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(710); + let owner_hot = U256::from(720); + let net = add_dynamic_network(&owner_hot, &owner_cold); + remove_owner_registration_stake(net); + + // Make this subnet pre-deploy for protocol-alpha accounting. + let reg_at = NetworkRegisteredAt::::get(net); + TaoInRefundDeploymentBlock::::put(reg_at.saturating_add(1)); + // No owner refund path: any TAO left on the subnet account is recycled. + SubtensorModule::set_subnet_locked_balance(net, TaoBalance::ZERO); + + // Alpha-in is present but ignored on the pre-deploy branch. The cached + // protocol alpha is the only claimant, so the entire pot is recycled. + SubnetAlphaIn::::insert(net, AlphaBalance::from(123u64)); + SubnetProtocolAlpha::::insert(net, AlphaBalance::from(100u64)); + + let pot: u64 = 100; + SubnetTAO::::insert(net, TaoBalance::from(pot)); + + let issuance_before = TotalIssuance::::get(); + let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); + + assert_ok!(SubtensorModule::do_dissolve_network(net)); + run_block_idle(); + + // There are no stakers, so the entire pot is the chain-bought alpha's TAO + // share. It is not paid to the owner; instead it is recycled back to the + // chain, which removes it from existence and reduces total issuance. + assert_eq!( + SubtensorModule::get_coldkey_balance(&owner_cold), + owner_before + ); + assert!( + TotalIssuance::::get() < issuance_before, + "recycling the chain-bought alpha's TAO must reduce total issuance" + ); + assert!(!SubnetProtocolAlpha::::contains_key(net)); + }); +} diff --git a/pallets/subtensor/src/tests/networks/dissolve_storage_cleanup.rs b/pallets/subtensor/src/tests/networks/dissolve_storage_cleanup.rs new file mode 100644 index 0000000000..83fbb9ce0a --- /dev/null +++ b/pallets/subtensor/src/tests/networks/dissolve_storage_cleanup.rs @@ -0,0 +1,564 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! Dissolve clears per-subnet, mechanism-scoped, and lock map storage. + +use super::prelude::*; + +#[test] +fn dissolve_clears_all_per_subnet_storages() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(123); + let owner_hot = U256::from(456); + let net = add_dynamic_network(&owner_hot, &owner_cold); + + // ------------------------------------------------------------------ + // Populate each storage item with a minimal value of the CORRECT type + // ------------------------------------------------------------------ + // Core ownership / bookkeeping + SubnetOwner::::insert(net, owner_cold); + SubnetOwnerHotkey::::insert(net, owner_hot); + SubnetworkN::::insert(net, 0u16); + NetworksAdded::::insert(net, true); + NetworkRegisteredAt::::insert(net, 0u64); + + // Consensus vectors + Active::::insert(net, vec![true]); + Emission::::insert(net, vec![AlphaBalance::from(1)]); + Incentive::::insert(NetUidStorageIndex::from(net), vec![PerU16::from_parts(1)]); + Consensus::::insert(net, vec![PerU16::from_parts(1)]); + Dividends::::insert(net, vec![PerU16::from_parts(1)]); + LastUpdate::::insert(NetUidStorageIndex::from(net), vec![0u64]); + ValidatorPermit::::insert(net, vec![true]); + ValidatorTrust::::insert(net, vec![PerU16::from_parts(1)]); + + // Per‑net params + Tempo::::insert(net, 1u16); + Kappa::::insert(net, 1u16); + Difficulty::::insert(net, 1u64); + + MaxAllowedUids::::insert(net, 1u16); + ImmunityPeriod::::insert(net, 1u16); + ActivityCutoff::::insert(net, 1u16); + MinAllowedWeights::::insert(net, 1u16); + + RegistrationsThisInterval::::insert(net, 1u16); + POWRegistrationsThisInterval::::insert(net, 1u16); + BurnRegistrationsThisInterval::::insert(net, 1u16); + + // Pool / AMM counters + SubnetTAO::::insert(net, TaoBalance::from(1)); + SubnetAlphaInEmission::::insert(net, AlphaBalance::from(1)); + SubnetAlphaOutEmission::::insert(net, AlphaBalance::from(1)); + SubnetTaoInEmission::::insert(net, TaoBalance::from(1)); + SubnetVolume::::insert(net, 1u128); + + // Items now REMOVED (not zeroed) by dissolution + SubnetAlphaIn::::insert(net, AlphaBalance::from(2)); + SubnetAlphaOut::::insert(net, AlphaBalance::from(3)); + SubnetProtocolAlpha::::insert(net, AlphaBalance::from(4)); + + // Prefix / double-map collections + Keys::::insert(net, 0u16, owner_hot); + Bonds::::insert(NetUidStorageIndex::from(net), 0u16, vec![(0u16, 1u16)]); + Weights::::insert(NetUidStorageIndex::from(net), 0u16, vec![(1u16, 1u16)]); + + // Membership entry for the SAME hotkey as Keys + IsNetworkMember::::insert(owner_hot, net, true); + + // Token / price / provided reserves + TokenSymbol::::insert(net, b"XX".to_vec()); + SubnetMovingPrice::::insert(net, substrate_fixed::types::I96F32::from_num(1)); + + // TAO Flow + SubnetTaoFlow::::insert(net, 0i64); + SubnetEmaTaoFlow::::insert(net, (0u64, substrate_fixed::types::I64F64::from_num(0))); + + // Subnet locks + TransferToggle::::insert(net, true); + SubnetLocked::::insert(net, TaoBalance::from(1)); + LargestLocked::::insert(net, 1u64); + + // Subnet parameters & pending counters + FirstEmissionBlockNumber::::insert(net, 1u64); + SubnetMechanism::::insert(net, 1u16); + NetworkRegistrationAllowed::::insert(net, true); + NetworkPowRegistrationAllowed::::insert(net, true); + PendingServerEmission::::insert(net, AlphaBalance::from(1)); + PendingValidatorEmission::::insert(net, AlphaBalance::from(1)); + PendingRootAlphaDivs::::insert(net, AlphaBalance::from(1)); + PendingOwnerCut::::insert(net, AlphaBalance::from(1)); + MinerBurned::::insert(net, substrate_fixed::types::U96F32::from_num(1)); + BlocksSinceLastStep::::insert(net, 1u64); + LastMechansimStepBlock::::insert(net, 1u64); + ServingRateLimit::::insert(net, 1u64); + Rho::::insert(net, 1u16); + AlphaSigmoidSteepness::::insert(net, 1i16); + + // Weights/versioning/targets/limits + WeightsVersionKey::::insert(net, 1u64); + MaxAllowedValidators::::insert(net, 1u16); + AdjustmentInterval::::insert(net, 2u16); + BondsMovingAverage::::insert(net, 1u64); + BondsPenalty::::insert(net, 1u16); + BondsResetOn::::insert(net, true); + WeightsSetRateLimit::::insert(net, 1u64); + ValidatorPruneLen::::insert(net, 1u64); + ScalingLawPower::::insert(net, 1u16); + TargetRegistrationsPerInterval::::insert(net, 1u16); + AdjustmentAlpha::::insert(net, 1u64); + CommitRevealWeightsEnabled::::insert(net, true); + + // Burn/difficulty/adjustment + Burn::::insert(net, TaoBalance::from(1)); + MinBurn::::insert(net, TaoBalance::from(1)); + MaxBurn::::insert(net, TaoBalance::from(2)); + MinDifficulty::::insert(net, 1u64); + MaxDifficulty::::insert(net, 2u64); + RegistrationsThisBlock::::insert(net, 1u16); + EMAPriceHalvingBlocks::::insert(net, 1u64); + RAORecycledForRegistration::::insert(net, TaoBalance::from(1)); + + // Feature toggles + LiquidAlphaOn::::insert(net, true); + Yuma3On::::insert(net, true); + AlphaValues::::insert(net, (1u16, 2u16)); + SubtokenEnabled::::insert(net, true); + OwnerCutAutoLockEnabled::::insert(net, true); + ImmuneOwnerUidsLimit::::insert(net, 1u16); + + // Per‑subnet vectors / indexes + StakeWeight::::insert(net, vec![1u16]); + + // Uid/registration + Uids::::insert(net, owner_hot, 0u16); + BlockAtRegistration::::insert(net, 0u16, 1u64); + + // Per‑subnet dividends + AlphaDividendsPerSubnet::::insert(net, owner_hot, AlphaBalance::from(1)); + + // Parent/child topology + takes + ChildkeyTake::::insert(owner_hot, net, PerU16::from_parts(1)); + PendingChildKeys::::insert(net, owner_cold, (vec![(1u64, owner_hot)], 1u64)); + ChildKeys::::insert(owner_cold, net, vec![(1u64, owner_hot)]); + ParentKeys::::insert(owner_hot, net, vec![(1u64, owner_cold)]); + + // Hotkey swap timestamp for subnet + LastHotkeySwapOnNetuid::::insert(net, owner_cold, 1u64); + + // Axon/prometheus tx key timing (NMap) — ***correct key-tuple insertion*** + TransactionKeyLastBlock::::insert((owner_hot, net, 1u16), 1u64); + + // EVM association indexed by (netuid, uid) + SubtensorModule::set_associated_evm_address(net, 0u16, sp_core::H160::zero(), 1u64); + + // (Optional) subnet -> lease link + SubnetUidToLeaseId::::insert(net, 42u32); + + // ------------------------------------------------------------------ + // Dissolve + // ------------------------------------------------------------------ + assert_ok!(SubtensorModule::do_dissolve_network(net)); + run_block_idle(); + + // ------------------------------------------------------------------ + // Items that must be COMPLETELY REMOVED + // ------------------------------------------------------------------ + assert!(!SubnetOwner::::contains_key(net)); + assert!(!SubnetOwnerHotkey::::contains_key(net)); + assert!(!SubnetworkN::::contains_key(net)); + assert!(!NetworksAdded::::contains_key(net)); + assert!(!NetworkRegisteredAt::::contains_key(net)); + + // Consensus vectors removed + assert!(!Active::::contains_key(net)); + assert!(!Emission::::contains_key(net)); + assert!(!Incentive::::contains_key(NetUidStorageIndex::from( + net + ))); + assert!(!Consensus::::contains_key(net)); + assert!(!Dividends::::contains_key(net)); + assert!(!LastUpdate::::contains_key(NetUidStorageIndex::from( + net + ))); + + assert!(!ValidatorPermit::::contains_key(net)); + assert!(!ValidatorTrust::::contains_key(net)); + + // Per‑net params removed + assert!(!Tempo::::contains_key(net)); + assert!(!Kappa::::contains_key(net)); + assert!(!Difficulty::::contains_key(net)); + + assert!(!MaxAllowedUids::::contains_key(net)); + assert!(!ImmunityPeriod::::contains_key(net)); + assert!(!ActivityCutoff::::contains_key(net)); + assert!(!MinAllowedWeights::::contains_key(net)); + + assert!(!RegistrationsThisInterval::::contains_key(net)); + assert!(!POWRegistrationsThisInterval::::contains_key(net)); + assert!(!BurnRegistrationsThisInterval::::contains_key(net)); + + // Pool / AMM counters removed + assert!(!SubnetTAO::::contains_key(net)); + assert!(!SubnetAlphaInEmission::::contains_key(net)); + assert!(!SubnetAlphaOutEmission::::contains_key(net)); + assert!(!SubnetTaoInEmission::::contains_key(net)); + assert!(!SubnetVolume::::contains_key(net)); + assert!(!pallet_subtensor_swap::BalancerTaoReservoir::::contains_key(net)); + assert!(!pallet_subtensor_swap::BalancerAlphaReservoir::::contains_key(net)); + + // TAO Flow + assert!(!SubnetTaoFlow::::contains_key(net)); + assert!(!SubnetEmaTaoFlow::::contains_key(net)); + + // These are now REMOVED + assert!(!SubnetAlphaIn::::contains_key(net)); + assert!(!SubnetAlphaOut::::contains_key(net)); + assert!(!SubnetProtocolAlpha::::contains_key(net)); + + // Collections fully cleared + assert!(Keys::::iter_prefix(net).next().is_none()); + assert!( + Bonds::::iter_prefix(NetUidStorageIndex::from(net)) + .next() + .is_none() + ); + assert!( + Weights::::iter_prefix(NetUidStorageIndex::from(net)) + .next() + .is_none() + ); + assert!(!IsNetworkMember::::contains_key(owner_hot, net)); + + // Token / price / provided reserves + assert!(!TokenSymbol::::contains_key(net)); + assert!(!SubnetMovingPrice::::contains_key(net)); + + // Subnet locks + assert!(!TransferToggle::::contains_key(net)); + assert!(!SubnetLocked::::contains_key(net)); + assert!(!LargestLocked::::contains_key(net)); + + // Subnet parameters & pending counters + assert!(!FirstEmissionBlockNumber::::contains_key(net)); + assert!(!SubnetMechanism::::contains_key(net)); + assert!(!NetworkRegistrationAllowed::::contains_key(net)); + assert!(!NetworkPowRegistrationAllowed::::contains_key(net)); + assert!(!PendingServerEmission::::contains_key(net)); + assert!(!PendingValidatorEmission::::contains_key(net)); + assert!(!PendingRootAlphaDivs::::contains_key(net)); + assert!(!PendingOwnerCut::::contains_key(net)); + assert!(!MinerBurned::::contains_key(net)); + assert!(!BlocksSinceLastStep::::contains_key(net)); + assert!(!LastMechansimStepBlock::::contains_key(net)); + assert!(!ServingRateLimit::::contains_key(net)); + assert!(!Rho::::contains_key(net)); + assert!(!AlphaSigmoidSteepness::::contains_key(net)); + + // Weights/versioning/targets/limits + assert!(!WeightsVersionKey::::contains_key(net)); + assert!(!MaxAllowedValidators::::contains_key(net)); + assert!(!BondsMovingAverage::::contains_key(net)); + assert!(!BondsPenalty::::contains_key(net)); + assert!(!BondsResetOn::::contains_key(net)); + assert!(!WeightsSetRateLimit::::contains_key(net)); + assert!(!ValidatorPruneLen::::contains_key(net)); + assert!(!ScalingLawPower::::contains_key(net)); + assert!(!TargetRegistrationsPerInterval::::contains_key(net)); + assert!(!CommitRevealWeightsEnabled::::contains_key(net)); + + // Burn/difficulty/adjustment + assert!(!Burn::::contains_key(net)); + assert!(!MinBurn::::contains_key(net)); + assert!(!MaxBurn::::contains_key(net)); + assert!(!MinDifficulty::::contains_key(net)); + assert!(!MaxDifficulty::::contains_key(net)); + assert!(!RegistrationsThisBlock::::contains_key(net)); + assert!(!EMAPriceHalvingBlocks::::contains_key(net)); + assert!(!RAORecycledForRegistration::::contains_key(net)); + + // Feature toggles + assert!(!LiquidAlphaOn::::contains_key(net)); + assert!(!Yuma3On::::contains_key(net)); + assert!(!AlphaValues::::contains_key(net)); + assert!(!SubtokenEnabled::::contains_key(net)); + assert!(!OwnerCutAutoLockEnabled::::contains_key(net)); + assert!(!ImmuneOwnerUidsLimit::::contains_key(net)); + + // Per‑subnet vectors / indexes + assert!(!StakeWeight::::contains_key(net)); + + // Uid/registration + assert!(Uids::::get(net, owner_hot).is_none()); + assert!(!BlockAtRegistration::::contains_key(net, 0u16)); + + // Per‑subnet dividends + assert!(!AlphaDividendsPerSubnet::::contains_key( + net, owner_hot + )); + + // Parent/child topology + takes + assert!(!ChildkeyTake::::contains_key(owner_hot, net)); + assert!(!PendingChildKeys::::contains_key(net, owner_cold)); + assert!(!ChildKeys::::contains_key(owner_cold, net)); + assert!(!ParentKeys::::contains_key(owner_hot, net)); + + // Hotkey swap timestamp for subnet + assert!(!LastHotkeySwapOnNetuid::::contains_key( + net, owner_cold + )); + + // Axon/prometheus tx key timing (NMap) — ValueQuery (defaults to 0) + assert_eq!( + TransactionKeyLastBlock::::get((owner_hot, net, 1u16)), + 0u64 + ); + + // EVM association + assert!(AssociatedEvmAddress::::get(net, 0u16).is_none()); + assert!(AssociatedUidsByEvmAddress::::get(net, sp_core::H160::zero()).is_empty()); + + // Subnet -> lease link + assert!(!SubnetUidToLeaseId::::contains_key(net)); + + // ------------------------------------------------------------------ + // Final subnet removal confirmation + // ------------------------------------------------------------------ + assert!(!SubtensorModule::subnet_exists(net)); + }); +} + +#[test] +fn dissolve_clears_all_mechanism_scoped_maps_for_all_mechanisms() { + new_test_ext(0).execute_with(|| { + // Create a subnet we can dissolve. + let owner_cold = U256::from(123); + let owner_hot = U256::from(456); + let net = add_dynamic_network(&owner_hot, &owner_cold); + + // Add 100 TAO to subnet account (lock) + let subnet_account = SubtensorModule::get_subnet_account_id(net).unwrap(); + add_balance_to_coldkey_account(&subnet_account, 100_000_000_000_u64.into()); + + // We'll use two mechanisms for this subnet. + MechanismCountCurrent::::insert(net, MechId::from(2)); + let m0 = MechId::from(0u8); + let m1 = MechId::from(1u8); + + let idx0 = SubtensorModule::get_mechanism_storage_index(net, m0); + let idx1 = SubtensorModule::get_mechanism_storage_index(net, m1); + + // Minimal content to ensure each storage actually has keys for BOTH mechanisms. + + // --- Weights (DMAP: (netuid_index, uid) -> Vec<(dest_uid, weight_u16)>) + Weights::::insert(idx0, 0u16, vec![(1u16, 1u16)]); + Weights::::insert(idx1, 0u16, vec![(2u16, 1u16)]); + + // --- Bonds (DMAP: (netuid_index, uid) -> Vec<(dest_uid, weight_u16)>) + Bonds::::insert(idx0, 0u16, vec![(1u16, 1u16)]); + Bonds::::insert(idx1, 0u16, vec![(2u16, 1u16)]); + + // --- TimelockedWeightCommits (DMAP: (netuid_index, epoch) -> VecDeque<...>) + let hotkey = U256::from(1); + TimelockedWeightCommits::::insert( + idx0, + 1u64, + VecDeque::from([(hotkey, 1u64, Default::default(), Default::default())]), + ); + TimelockedWeightCommits::::insert( + idx1, + 2u64, + VecDeque::from([(hotkey, 2u64, Default::default(), Default::default())]), + ); + + // --- Incentive (MAP: netuid_index -> Vec) + Incentive::::insert(idx0, vec![PerU16::from_parts(1), PerU16::from_parts(2)]); + Incentive::::insert(idx1, vec![PerU16::from_parts(3), PerU16::from_parts(4)]); + + // --- LastUpdate (MAP: netuid_index -> Vec) + LastUpdate::::insert(idx0, vec![42u64]); + LastUpdate::::insert(idx1, vec![84u64]); + + // Sanity: keys are present before dissolve. + assert!(Weights::::contains_key(idx0, 0u16)); + assert!(Weights::::contains_key(idx1, 0u16)); + assert!(Bonds::::contains_key(idx0, 0u16)); + assert!(Bonds::::contains_key(idx1, 0u16)); + assert!(TimelockedWeightCommits::::contains_key(idx0, 1u64)); + assert!(TimelockedWeightCommits::::contains_key(idx1, 2u64)); + assert!(Incentive::::contains_key(idx0)); + assert!(Incentive::::contains_key(idx1)); + assert!(LastUpdate::::contains_key(idx0)); + assert!(LastUpdate::::contains_key(idx1)); + assert!(MechanismCountCurrent::::contains_key(net)); + + // --- Dissolve the subnet --- + assert_ok!(SubtensorModule::do_dissolve_network(net)); + run_block_idle(); + + // After dissolve, ALL mechanism-scoped items must be cleared for ALL mechanisms. + + // Weights/Bonds double-maps should have no entries under either index. + assert!(Weights::::iter_prefix(idx0).next().is_none()); + assert!(Weights::::iter_prefix(idx1).next().is_none()); + assert!(Bonds::::iter_prefix(idx0).next().is_none()); + assert!(Bonds::::iter_prefix(idx1).next().is_none()); + + // WeightCommits (OptionQuery) should have no keys remaining. + assert!(WeightCommits::::iter_prefix(idx0).next().is_none()); + assert!(WeightCommits::::iter_prefix(idx1).next().is_none()); + assert!(!WeightCommits::::contains_key(idx0, owner_hot)); + assert!(!WeightCommits::::contains_key(idx1, owner_cold)); + + // TimelockedWeightCommits (ValueQuery) — ensure both prefix spaces empty and keys gone. + assert!( + TimelockedWeightCommits::::iter_prefix(idx0) + .next() + .is_none() + ); + assert!( + TimelockedWeightCommits::::iter_prefix(idx1) + .next() + .is_none() + ); + assert!(!TimelockedWeightCommits::::contains_key(idx0, 1u64)); + assert!(!TimelockedWeightCommits::::contains_key(idx1, 2u64)); + + // Single-map per-mechanism vectors cleared. + assert!(!Incentive::::contains_key(idx0)); + assert!(!Incentive::::contains_key(idx1)); + assert!(!LastUpdate::::contains_key(idx0)); + assert!(!LastUpdate::::contains_key(idx1)); + + // MechanismCountCurrent cleared + assert!(!MechanismCountCurrent::::contains_key(net)); + }); +} + +#[test] +fn dissolve_clears_all_lock_maps_for_removed_network() { + new_test_ext(0).execute_with(|| { + // Create a subnet we can dissolve. + let owner_cold = U256::from(123); + let owner_hot = U256::from(456); + let net = add_dynamic_network(&owner_hot, &owner_cold); + + // Add TAO to subnet account so dissolve can proceed. + let subnet_account = SubtensorModule::get_subnet_account_id(net).unwrap(); + add_balance_to_coldkey_account(&subnet_account, 100_000_000_000_u64.into()); + + // Non-owner coldkeys / hotkeys. + let cold_1 = U256::from(1001); + let cold_2 = U256::from(1002); + let hot_1 = U256::from(2001); + let hot_2 = U256::from(2002); + + // Another subnet to ensure dissolve only clears `net`. + let other_net = NetUid::from(u16::from(net) + 1); + + // Explicit LockState initialization + let lock_a = LockState { + locked_mass: 10u64.into(), + conviction: U64F64::from_num(1.5), + last_update: 1, + }; + + let lock_b = LockState { + locked_mass: 20u64.into(), + conviction: U64F64::from_num(2.5), + last_update: 2, + }; + + // --- Lock: (coldkey, netuid, hotkey) + Lock::::insert((cold_1, net, hot_1), lock_a.clone()); + LockingColdkeys::::insert((net, hot_1, cold_1), ()); + Lock::::insert((cold_2, net, hot_2), lock_b.clone()); + LockingColdkeys::::insert((net, hot_2, cold_2), ()); + + // Same cold/hot on another net should survive. + Lock::::insert((cold_1, other_net, hot_1), lock_a.clone()); + LockingColdkeys::::insert((other_net, hot_1, cold_1), ()); + + // --- HotkeyLock + HotkeyLock::::insert(net, hot_1, lock_a.clone()); + HotkeyLock::::insert(net, hot_2, lock_b.clone()); + HotkeyLock::::insert(other_net, hot_1, lock_a.clone()); + + // --- DecayingHotkeyLock + DecayingHotkeyLock::::insert(net, hot_1, lock_a.clone()); + DecayingHotkeyLock::::insert(net, hot_2, lock_b.clone()); + DecayingHotkeyLock::::insert(other_net, hot_1, lock_a.clone()); + + // --- OwnerLock + OwnerLock::::insert(net, lock_a.clone()); + OwnerLock::::insert(other_net, lock_b.clone()); + + // --- DecayingLock + DecayingLock::::insert(cold_1, net, false); + DecayingLock::::insert(cold_2, net, false); + DecayingLock::::insert(cold_1, other_net, false); + + // Sanity checks before dissolve + assert!(Lock::::contains_key((cold_1, net, hot_1))); + assert!(Lock::::contains_key((cold_2, net, hot_2))); + assert!(LockingColdkeys::::contains_key((net, hot_1, cold_1))); + assert!(LockingColdkeys::::contains_key((net, hot_2, cold_2))); + + assert!(HotkeyLock::::contains_key(net, hot_1)); + assert!(HotkeyLock::::contains_key(net, hot_2)); + + assert!(DecayingHotkeyLock::::contains_key(net, hot_1)); + assert!(DecayingHotkeyLock::::contains_key(net, hot_2)); + + assert!(OwnerLock::::contains_key(net)); + + assert!(DecayingLock::::contains_key(cold_1, net)); + assert!(DecayingLock::::contains_key(cold_2, net)); + + // Sanity: other net keys are present before dissolve. + assert!(Lock::::contains_key((cold_1, other_net, hot_1))); + assert!(LockingColdkeys::::contains_key(( + other_net, hot_1, cold_1 + ))); + assert!(HotkeyLock::::contains_key(other_net, hot_1)); + assert!(DecayingHotkeyLock::::contains_key(other_net, hot_1)); + assert!(OwnerLock::::contains_key(other_net)); + assert!(DecayingLock::::contains_key(cold_1, other_net)); + + // --- Dissolve --- + assert_ok!(SubtensorModule::do_dissolve_network(net)); + run_block_idle(); + + // Ensure removed + assert!(!Lock::::contains_key((cold_1, net, hot_1))); + assert!(!Lock::::contains_key((cold_2, net, hot_2))); + assert!(!LockingColdkeys::::contains_key((net, hot_1, cold_1))); + assert!(!LockingColdkeys::::contains_key((net, hot_2, cold_2))); + + assert!(!HotkeyLock::::contains_key(net, hot_1)); + assert!(!HotkeyLock::::contains_key(net, hot_2)); + assert!(HotkeyLock::::iter_prefix(net).next().is_none()); + + assert!(!DecayingHotkeyLock::::contains_key(net, hot_1)); + assert!(!DecayingHotkeyLock::::contains_key(net, hot_2)); + assert!( + DecayingHotkeyLock::::iter_prefix(net) + .next() + .is_none() + ); + + assert!(!OwnerLock::::contains_key(net)); + + assert!(!DecayingLock::::contains_key(cold_1, net)); + assert!(!DecayingLock::::contains_key(cold_2, net)); + + // Ensure other_net is untouched + assert!(Lock::::contains_key((cold_1, other_net, hot_1))); + assert!(LockingColdkeys::::contains_key(( + other_net, hot_1, cold_1 + ))); + assert!(HotkeyLock::::contains_key(other_net, hot_1)); + assert!(DecayingHotkeyLock::::contains_key(other_net, hot_1)); + assert!(OwnerLock::::contains_key(other_net)); + assert!(DecayingLock::::contains_key(cold_1, other_net)); + }); +} diff --git a/pallets/subtensor/src/tests/networks/helpers.rs b/pallets/subtensor/src/tests/networks/helpers.rs new file mode 100644 index 0000000000..88c3f7756e --- /dev/null +++ b/pallets/subtensor/src/tests/networks/helpers.rs @@ -0,0 +1,22 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! Shared fixtures for network dissolve / register-owner-alpha tests. + +use super::prelude::*; + +/// Run the same α-out destroy steps as `remove_data_for_dissolved_networks` (post-root-cleanup). +pub(super) fn destroy_alpha_in_out_stakes_full_pipeline_for_test(netuid: NetUid) { + run_destroy_alpha_in_out_stakes_full_pipeline(netuid); +} + +pub(super) fn owner_alpha_from_lock_and_price(lock_cost_u64: u64, price: U64F64) -> u64 { + let alpha = (U64F64::from_num(lock_cost_u64) + .checked_div(price) + .unwrap_or_default()) + .floor(); + + if alpha > U64F64::from_num(u64::MAX) { + u64::MAX + } else { + alpha.to_num::() + } +} diff --git a/pallets/subtensor/src/tests/networks/massive_dissolve_reregistration.rs b/pallets/subtensor/src/tests/networks/massive_dissolve_reregistration.rs new file mode 100644 index 0000000000..f152f6914a --- /dev/null +++ b/pallets/subtensor/src/tests/networks/massive_dissolve_reregistration.rs @@ -0,0 +1,351 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! End-to-end dissolve refund + re-registration lossless flow. + +use super::prelude::*; + +#[test] +fn massive_dissolve_refund_and_reregistration_flow_is_lossless_and_cleans_state() { + new_test_ext(0).execute_with(|| { + // ──────────────────────────────────────────────────────────────────── + // 0) Constants and helpers (distinct hotkeys & coldkeys) + // ──────────────────────────────────────────────────────────────────── + const NUM_NETS: usize = 4; + + // Six LP coldkeys + let cold_lps: [U256; 6] = [ + U256::from(3001), + U256::from(3002), + U256::from(3003), + U256::from(3004), + U256::from(3005), + U256::from(3006), + ]; + + // For each coldkey, define two DISTINCT hotkeys it owns. + let mut cold_to_hots: BTreeMap = BTreeMap::new(); + for &c in cold_lps.iter() { + let h1 = U256::from(c.low_u64().saturating_add(100_000)); + let h2 = U256::from(c.low_u64().saturating_add(200_000)); + cold_to_hots.insert(c, [h1, h2]); + } + + // Distinct τ pot sizes per net. + let pots: [u64; NUM_NETS] = [12_345, 23_456, 34_567, 45_678]; + + let lp_sets_per_net: [&[U256]; NUM_NETS] = [ + &cold_lps[0..4], // net0: A,B,C,D + &cold_lps[2..6], // net1: C,D,E,F + &cold_lps[0..6], // net2: A..F + &cold_lps[1..5], // net3: B,C,D,E + ]; + + // ──────────────────────────────────────────────────────────────────── + // 1) Create many subnets, fix price at tick=0 + // ──────────────────────────────────────────────────────────────────── + let mut nets: Vec = Vec::new(); + for i in 0..NUM_NETS { + let owner_hot = U256::from(10_000 + (i as u64)); + let owner_cold = U256::from(20_000 + (i as u64)); + let net = add_dynamic_network(&owner_hot, &owner_cold); + remove_owner_registration_stake(net); + SubtensorModule::set_max_registrations_per_block(net, 1_000u16); + SubtensorModule::set_target_registrations_per_interval(net, 1_000u16); + Emission::::insert(net, Vec::::new()); + SubtensorModule::set_subnet_locked_balance(net, TaoBalance::from(0)); + + nets.push(net); + } + + // Map net → index for quick lookups. + let mut net_index: BTreeMap = BTreeMap::new(); + for (i, &n) in nets.iter().enumerate() { + net_index.insert(n, i); + } + + // ──────────────────────────────────────────────────────────────────── + // 2) Pre-create a handful of small (hot, cold) pairs so accounts exist + // ──────────────────────────────────────────────────────────────────── + for id in 0u64..10 { + let cold_acc = U256::from(1_000_000 + id); + let hot_acc = U256::from(2_000_000 + id); + for &net in nets.iter() { + register_ok_neuron(net, hot_acc, cold_acc, 100_000 + id); + } + } + + // ──────────────────────────────────────────────────────────────────── + // 3) LPs per net: register each (hot, cold), massive τ prefund, and stake + // ──────────────────────────────────────────────────────────────────── + for &cold in cold_lps.iter() { + add_balance_to_coldkey_account(&cold, 1_000_000_000_000_u64.into()); + } + + // τ balances before LP adds (after staking): + let mut tao_before: BTreeMap = BTreeMap::new(); + + // Ordered α snapshot per net at **pair granularity** (pre‑LP): + let mut alpha_pairs_per_net: BTreeMap> = BTreeMap::new(); + + // Register both hotkeys for each participating cold on each net and stake τ→α. + for (ni, &net) in nets.iter().enumerate() { + let participants = lp_sets_per_net[ni]; + for &cold in participants.iter() { + let [hot1, hot2] = cold_to_hots[&cold]; + + // Ensure (hot, cold) neurons exist on this net. + register_ok_neuron( + net, + hot1, + cold, + (ni as u64) * 10_000 + (hot1.low_u64() % 10_000), + ); + register_ok_neuron( + net, + hot2, + cold, + (ni as u64) * 10_000 + (hot2.low_u64() % 10_000) + 1, + ); + + // Stake τ (split across the two hotkeys). + let base: u64 = + 5_000_000 + ((ni as u64) * 1_000_000) + ((cold.low_u64() % 10) * 250_000); + let stake1: u64 = base.saturating_mul(3) / 5; // 60% + let stake2: u64 = base.saturating_sub(stake1); // 40% + + assert_ok!(SubtensorModule::do_add_stake( + RuntimeOrigin::signed(cold), + hot1, + net, + stake1.into() + )); + assert_ok!(SubtensorModule::do_add_stake( + RuntimeOrigin::signed(cold), + hot2, + net, + stake2.into() + )); + } + } + + // Record τ balances now (post‑stake, pre‑LP). + for &cold in cold_lps.iter() { + tao_before.insert(cold, SubtensorModule::get_coldkey_balance(&cold).into()); + } + + // Capture **pair‑level** α snapshot per net (pre‑LP). + for ((hot, cold, net), amt) in AlphaV2::::iter() { + if let Some(&ni) = net_index.get(&net) + && lp_sets_per_net[ni].contains(&cold) { + let a: u128 = sf_to_u128(&amt); + if a > 0 { + alpha_pairs_per_net + .entry(net) + .or_default() + .push(((hot, cold), a)); + } + } + } + + // Snapshot τ balances AFTER LP adds (to measure actual principal debit). + let mut tao_after_adds: BTreeMap = BTreeMap::new(); + for &cold in cold_lps.iter() { + tao_after_adds.insert(cold, SubtensorModule::get_coldkey_balance(&cold)); + } + + // ──────────────────────────────────────────────────────────────────── + // 5) Compute Hamilton-apportionment BASE shares per cold and total leftover + // from the **pair-level** pre‑LP α snapshot; also count pairs per cold. + // ──────────────────────────────────────────────────────────────────── + for &net in nets.iter() { + SubnetAlphaIn::::insert(net, AlphaBalance::ZERO); + SubnetProtocolAlpha::::insert(net, AlphaBalance::ZERO); + } + + let mut base_share_cold: BTreeMap = + cold_lps.iter().copied().map(|c| (c, 0_u64)).collect(); + let mut pair_count_cold: BTreeMap = + cold_lps.iter().copied().map(|c| (c, 0_u32)).collect(); + + let mut leftover_total: u64 = 0; + + for (ni, &net) in nets.iter().enumerate() { + let pot = pots[ni]; + let pairs = alpha_pairs_per_net.get(&net).cloned().unwrap_or_default(); + if pot == 0 || pairs.is_empty() { + continue; + } + let total_alpha: u128 = pairs.iter().map(|(_, a)| *a).sum(); + if total_alpha == 0 { + continue; + } + + let mut base_sum_net: u64 = 0; + for ((_, cold), a) in pairs.iter().copied() { + // quota = a * pot / total_alpha + let prod: u128 = a.saturating_mul(pot as u128); + let base: u64 = (prod / total_alpha) as u64; + base_sum_net = base_sum_net.saturating_add(base); + *base_share_cold.entry(cold).or_default() = + base_share_cold[&cold].saturating_add(base); + *pair_count_cold.entry(cold).or_default() += 1; + } + let leftover_net = pot.saturating_sub(base_sum_net); + leftover_total = leftover_total.saturating_add(leftover_net); + } + + // ──────────────────────────────────────────────────────────────────── + // 6) Seed τ pots and dissolve *all* networks (liquidates LPs + refunds) + // ──────────────────────────────────────────────────────────────────── + for (ni, &net) in nets.iter().enumerate() { + SubnetTAO::::insert(net, TaoBalance::from(pots[ni])); + } + for &net in nets.iter() { + assert_ok!(SubtensorModule::do_dissolve_network(net)); + run_block_idle(); + } + + // ──────────────────────────────────────────────────────────────────── + // 7) Assertions: τ balances, α gone, nets removed, swap state clean + // (Hamilton invariants enforced at cold-level without relying on tie-break) + // ──────────────────────────────────────────────────────────────────── + // Collect actual pot credits per cold (principal cancels out against adds when comparing before→after). + let mut actual_pot_cold: BTreeMap = + cold_lps.iter().copied().map(|c| (c, 0_u64)).collect(); + for &cold in cold_lps.iter() { + let before = tao_before[&cold]; + let after = SubtensorModule::get_coldkey_balance(&cold); + actual_pot_cold.insert(cold, after.saturating_sub(before.into()).into()); + } + + // (a) Sum of actual pot credits equals total pots. + let total_actual: u64 = actual_pot_cold.values().copied().sum(); + let total_pots: u64 = pots.iter().copied().sum(); + assert_eq!( + total_actual, total_pots, + "total τ pot credited across colds must equal sum of pots" + ); + + // (b) Each cold’s pot is within Hamilton bounds: base ≤ actual ≤ base + #pairs. + let mut extra_accum: u64 = 0; + for &cold in cold_lps.iter() { + let base = *base_share_cold.get(&cold).unwrap_or(&0); + let pairs = *pair_count_cold.get(&cold).unwrap_or(&0) as u64; + let actual = *actual_pot_cold.get(&cold).unwrap_or(&0); + + assert!( + actual >= base, + "cold {cold:?} actual pot {actual} is below base {base}" + ); + assert!( + actual <= base.saturating_add(pairs), + "cold {cold:?} actual pot {actual} exceeds base + pairs ({base} + {pairs})" + ); + + extra_accum = extra_accum.saturating_add(actual.saturating_sub(base)); + } + + // (c) The total “extra beyond base” equals the computed leftover_total across nets. + assert_eq!( + extra_accum, leftover_total, + "sum of extras beyond base must equal total leftover" + ); + + // (d) τ principal was fully refunded (compare after_adds → after). + for &cold in cold_lps.iter() { + let before = tao_before[&cold]; + let mid = tao_after_adds[&cold]; + let after = SubtensorModule::get_coldkey_balance(&cold); + let principal_actual = before.saturating_sub(mid); + let actual_pot = after.saturating_sub(before.into()); + assert_eq!( + after.saturating_sub(mid.into()), + principal_actual.saturating_add(actual_pot.into()).into(), + "cold {cold:?} τ balance incorrect vs 'after_adds'" + ); + } + + // For each dissolved net, check α ledgers gone, network removed, and swap state clean. + for &net in nets.iter() { + assert!( + AlphaV2::::iter().all(|((_h, _c, n), _)| n != net), + "alpha ledger not fully cleared for net {net:?}" + ); + assert!( + !SubtensorModule::subnet_exists(net), + "subnet {net:?} still exists" + ); + assert!( + !pallet_subtensor_swap::PalSwapInitialized::::get(net), + "PalSwapInitialized still set" + ); + } + + // ──────────────────────────────────────────────────────────────────── + // 8) Re-register a fresh subnet and re‑stake using the pallet’s min rule + // Assert αΔ equals the sim-swap result for the exact τ staked. + // ──────────────────────────────────────────────────────────────────── + let new_owner_hot = U256::from(99_000); + let new_owner_cold = U256::from(99_001); + let net_new = add_dynamic_network(&new_owner_hot, &new_owner_cold); + remove_owner_registration_stake(net_new); + SubtensorModule::set_max_registrations_per_block(net_new, 1_000u16); + SubtensorModule::set_target_registrations_per_interval(net_new, 1_000u16); + Emission::::insert(net_new, Vec::::new()); + SubtensorModule::set_subnet_locked_balance(net_new, TaoBalance::from(0)); + + // Compute the exact min stake per the pallet rule: DefaultMinStake + fee(DefaultMinStake). + let min_stake = DefaultMinStake::::get(); + let order = GetAlphaForTao::::with_amount(min_stake); + let fee_for_min = pallet_subtensor_swap::Pallet::::sim_swap( + net_new, + order, + ) + .map(|r| r.fee_paid) + .unwrap_or_else(|_e| { + as subtensor_swap_interface::SwapHandler>::approx_fee_amount(net_new, min_stake) + }); + let min_amount_required = min_stake.saturating_add(fee_for_min).to_u64(); + + // Re‑stake from three coldkeys; choose a specific DISTINCT hotkey per cold. + for &cold in &cold_lps[0..3] { + let [hot1, _hot2] = cold_to_hots[&cold]; + register_ok_neuron(net_new, hot1, cold, 7777); + + let before_tao = SubtensorModule::get_coldkey_balance(&cold); + let a_prev: u64 = sf_to_u128(&AlphaV2::::get((hot1, cold, net_new))) as u64; + + // Expected α for this exact τ, using the same sim path as the pallet. + let order = GetAlphaForTao::::with_amount(min_amount_required); + let expected_alpha_out = pallet_subtensor_swap::Pallet::::sim_swap( + net_new, + order, + ) + .map(|r| r.amount_paid_out) + .expect("sim_swap must succeed for fresh net and min amount"); + + assert_ok!(SubtensorModule::do_add_stake( + RuntimeOrigin::signed(cold), + hot1, + net_new, + min_amount_required.into() + )); + + let after_tao = SubtensorModule::get_coldkey_balance(&cold); + let a_new: u64 = sf_to_u128(&AlphaV2::::get((hot1, cold, net_new))) as u64; + let a_delta = a_new.saturating_sub(a_prev); + + // τ decreased by exactly the amount we sent. + assert_eq!( + after_tao, + before_tao.saturating_sub(min_amount_required.into()), + "τ did not decrease by the min required restake amount for cold {cold:?}" + ); + + // α minted equals the simulated swap’s net out for that same τ. + assert_eq!( + a_delta, expected_alpha_out.to_u64(), + "α minted mismatch for cold {cold:?} (hot {hot1:?}) on new net (αΔ {a_delta}, expected {expected_alpha_out})" + ); + } + }); +} diff --git a/pallets/subtensor/src/tests/networks/median_subnet_alpha_price.rs b/pallets/subtensor/src/tests/networks/median_subnet_alpha_price.rs new file mode 100644 index 0000000000..485d9e4694 --- /dev/null +++ b/pallets/subtensor/src/tests/networks/median_subnet_alpha_price.rs @@ -0,0 +1,117 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! `get_median_subnet_alpha_price` odd/even and eligibility filters. + +use super::prelude::*; + +#[test] +fn median_subnet_alpha_price_returns_one_when_no_eligible_subnet_prices() { + new_test_ext(0).execute_with(|| { + let one = U64F64::from_num(1u64); + + // Empty state. + assert_eq!(SubtensorModule::get_median_subnet_alpha_price(), one); + + // ROOT must be ignored. + NetworksAdded::::insert(NetUid::ROOT, true); + assert_eq!(SubtensorModule::get_median_subnet_alpha_price(), one); + + // Zero-priced subnet must be ignored. + let zero_cold = U256::from(101); + let zero_hot = U256::from(102); + let zero_netuid = add_dynamic_network(&zero_hot, &zero_cold); + setup_reserves(zero_netuid, TaoBalance::ZERO, AlphaBalance::from(100u64)); + assert_eq!( + ::SwapInterface::current_alpha_price(zero_netuid.into()), + U64F64::from_num(0u64) + ); + assert_eq!(SubtensorModule::get_median_subnet_alpha_price(), one); + + // added=false subnet must be ignored as well. + let hidden_cold = U256::from(103); + let hidden_hot = U256::from(104); + let hidden_netuid = add_dynamic_network(&hidden_hot, &hidden_cold); + setup_reserves( + hidden_netuid, + TaoBalance::from(900u64), + AlphaBalance::from(100u64), + ); + NetworksAdded::::insert(hidden_netuid, false); + + assert_eq!(SubtensorModule::get_median_subnet_alpha_price(), one); + }); +} + +#[test] +fn median_subnet_alpha_price_returns_middle_value_for_odd_unsorted_prices() { + new_test_ext(0).execute_with(|| { + let n1 = add_dynamic_network(&U256::from(201), &U256::from(200)); + let n2 = add_dynamic_network(&U256::from(203), &U256::from(202)); + let n3 = add_dynamic_network(&U256::from(205), &U256::from(204)); + + // Unsorted prices: 7, 2, 5 -> median should be 5. + setup_reserves(n1, TaoBalance::from(700u64), AlphaBalance::from(100u64)); + setup_reserves(n2, TaoBalance::from(200u64), AlphaBalance::from(100u64)); + setup_reserves(n3, TaoBalance::from(500u64), AlphaBalance::from(100u64)); + + assert_eq!( + ::SwapInterface::current_alpha_price(n1.into()), + U96F32::from_num(7u64) + ); + assert_eq!( + ::SwapInterface::current_alpha_price(n2.into()), + U96F32::from_num(2u64) + ); + assert_eq!( + ::SwapInterface::current_alpha_price(n3.into()), + U96F32::from_num(5u64) + ); + + assert_eq!( + SubtensorModule::get_median_subnet_alpha_price(), + U96F32::from_num(5u64) + ); + }); +} + +#[test] +fn median_subnet_alpha_price_averages_even_prices_and_ignores_root_zero_and_unadded() { + new_test_ext(0).execute_with(|| { + // If ROOT were included, its price would be 1 and change the median. + NetworksAdded::::insert(NetUid::ROOT, true); + + let n1 = add_dynamic_network(&U256::from(301), &U256::from(300)); // eligible, price 2 + let n2 = add_dynamic_network(&U256::from(303), &U256::from(302)); // hidden, price 4 + let n3 = add_dynamic_network(&U256::from(305), &U256::from(304)); // eligible, price 8 + let n4 = add_dynamic_network(&U256::from(307), &U256::from(306)); // zero, price 0 + + setup_reserves(n1, TaoBalance::from(200u64), AlphaBalance::from(100u64)); + setup_reserves(n2, TaoBalance::from(400u64), AlphaBalance::from(100u64)); + setup_reserves(n3, TaoBalance::from(800u64), AlphaBalance::from(100u64)); + setup_reserves(n4, TaoBalance::ZERO, AlphaBalance::from(100u64)); + + NetworksAdded::::insert(n2, false); + + assert_eq!( + ::SwapInterface::current_alpha_price(n1.into()), + U96F32::from_num(2u64) + ); + assert_eq!( + ::SwapInterface::current_alpha_price(n2.into()), + U96F32::from_num(4u64) + ); + assert_eq!( + ::SwapInterface::current_alpha_price(n3.into()), + U96F32::from_num(8u64) + ); + assert_eq!( + ::SwapInterface::current_alpha_price(n4.into()), + U96F32::from_num(0u64) + ); + + // Eligible prices are only {2, 8}, so the median is (2 + 8) / 2 = 5. + assert_eq!( + SubtensorModule::get_median_subnet_alpha_price(), + U96F32::from_num(5u64) + ); + }); +} diff --git a/pallets/subtensor/src/tests/networks/migrate_network_immunity.rs b/pallets/subtensor/src/tests/networks/migrate_network_immunity.rs new file mode 100644 index 0000000000..436ff6ba12 --- /dev/null +++ b/pallets/subtensor/src/tests/networks/migrate_network_immunity.rs @@ -0,0 +1,281 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! `migrate_network_immunity_period` storage migration coverage. + +use super::prelude::*; + +#[test] +fn test_migrate_network_immunity_period() { + new_test_ext(0).execute_with(|| { + // -------------------------------------------------------------------- + // ‼️ PRE-CONDITIONS + // -------------------------------------------------------------------- + assert_ne!(NetworkImmunityPeriod::::get(), 864_000); + assert!( + !HasMigrationRun::::get(b"migrate_network_immunity_period".to_vec()), + "HasMigrationRun should be false before migration" + ); + + // -------------------------------------------------------------------- + // ▶️ RUN MIGRATION + // -------------------------------------------------------------------- + let weight = migrate_network_immunity_period::migrate_network_immunity_period::(); + + // -------------------------------------------------------------------- + // ✅ POST-CONDITIONS + // -------------------------------------------------------------------- + assert_eq!( + NetworkImmunityPeriod::::get(), + 864_000, + "NetworkImmunityPeriod should now be 864_000" + ); + + assert!( + HasMigrationRun::::get(b"migrate_network_immunity_period".to_vec()), + "HasMigrationRun should be true after migration" + ); + + assert!(weight != Weight::zero(), "migration weight should be > 0"); + }); +} + +// #[test] +// fn test_schedule_dissolve_network_execution() { +// new_test_ext(1).execute_with(|| { +// let block_number: u64 = 0; +// let netuid = NetUid::from(2); +// let tempo: u16 = 13; +// let hotkey_account_id: U256 = U256::from(1); +// let coldkey_account_id = U256::from(0); // Neighbour of the beast, har har +// let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( +// netuid, +// block_number, +// 129123813, +// &hotkey_account_id, +// ); + +// //add network +// add_network(netuid, tempo, 0); + +// assert_ok!(SubtensorModule::register( +// <::RuntimeOrigin>::signed(hotkey_account_id), +// netuid, +// block_number, +// nonce, +// work.clone(), +// hotkey_account_id, +// coldkey_account_id +// )); + +// assert!(SubtensorModule::subnet_exists(netuid)); + +// assert_ok!(SubtensorModule::schedule_dissolve_network( +// <::RuntimeOrigin>::signed(coldkey_account_id), +// netuid +// )); + +// let current_block = System::block_number(); +// let execution_block = current_block + DissolveNetworkScheduleDuration::::get(); + +// System::assert_last_event( +// Event::DissolveNetworkScheduled { +// account: coldkey_account_id, +// netuid, +// execution_block, +// } +// .into(), +// ); + +// run_to_block(execution_block); +// assert!(!SubtensorModule::subnet_exists(netuid)); +// }) +// } + +// #[test] +// fn test_non_owner_schedule_dissolve_network_execution() { +// new_test_ext(1).execute_with(|| { +// let block_number: u64 = 0; +// let netuid = NetUid::from(2); +// let tempo: u16 = 13; +// let hotkey_account_id: U256 = U256::from(1); +// let coldkey_account_id = U256::from(0); // Neighbour of the beast, har har +// let non_network_owner_account_id = U256::from(2); // +// let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( +// netuid, +// block_number, +// 129123813, +// &hotkey_account_id, +// ); + +// //add network +// add_network(netuid, tempo, 0); + +// assert_ok!(SubtensorModule::register( +// <::RuntimeOrigin>::signed(hotkey_account_id), +// netuid, +// block_number, +// nonce, +// work.clone(), +// hotkey_account_id, +// coldkey_account_id +// )); + +// assert!(SubtensorModule::subnet_exists(netuid)); + +// assert_ok!(SubtensorModule::schedule_dissolve_network( +// <::RuntimeOrigin>::signed(non_network_owner_account_id), +// netuid +// )); + +// let current_block = System::block_number(); +// let execution_block = current_block + DissolveNetworkScheduleDuration::::get(); + +// System::assert_last_event( +// Event::DissolveNetworkScheduled { +// account: non_network_owner_account_id, +// netuid, +// execution_block, +// } +// .into(), +// ); + +// run_to_block(execution_block); +// // network exists since the caller is no the network owner +// assert!(SubtensorModule::subnet_exists(netuid)); +// }) +// } + +// #[test] +// fn test_new_owner_schedule_dissolve_network_execution() { +// new_test_ext(1).execute_with(|| { +// let block_number: u64 = 0; +// let netuid = NetUid::from(2); +// let tempo: u16 = 13; +// let hotkey_account_id: U256 = U256::from(1); +// let coldkey_account_id = U256::from(0); // Neighbour of the beast, har har +// let new_network_owner_account_id = U256::from(2); // +// let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( +// netuid, +// block_number, +// 129123813, +// &hotkey_account_id, +// ); + +// //add network +// add_network(netuid, tempo, 0); + +// assert_ok!(SubtensorModule::register( +// <::RuntimeOrigin>::signed(hotkey_account_id), +// netuid, +// block_number, +// nonce, +// work.clone(), +// hotkey_account_id, +// coldkey_account_id +// )); + +// assert!(SubtensorModule::subnet_exists(netuid)); + +// // the account is not network owner when schedule the call +// assert_ok!(SubtensorModule::schedule_dissolve_network( +// <::RuntimeOrigin>::signed(new_network_owner_account_id), +// netuid +// )); + +// let current_block = System::block_number(); +// let execution_block = current_block + DissolveNetworkScheduleDuration::::get(); + +// System::assert_last_event( +// Event::DissolveNetworkScheduled { +// account: new_network_owner_account_id, +// netuid, +// execution_block, +// } +// .into(), +// ); +// run_to_block(current_block + 1); +// // become network owner after call scheduled +// crate::SubnetOwner::::insert(netuid, new_network_owner_account_id); + +// run_to_block(execution_block); +// // network exists since the caller is no the network owner +// assert!(!SubtensorModule::subnet_exists(netuid)); +// }) +// } + +// #[test] +// fn test_schedule_dissolve_network_execution_with_coldkey_swap() { +// new_test_ext(1).execute_with(|| { +// let block_number: u64 = 0; +// let netuid = NetUid::from(2); +// let tempo: u16 = 13; +// let hotkey_account_id: U256 = U256::from(1); +// let coldkey_account_id = U256::from(0); // Neighbour of the beast, har har +// let new_network_owner_account_id = U256::from(2); // + +// add_balance_to_coldkey_account(&coldkey_account_id, 1000000000000000); + +// let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( +// netuid, +// block_number, +// 129123813, +// &hotkey_account_id, +// ); + +// //add network +// add_network(netuid, tempo, 0); + +// assert_ok!(SubtensorModule::register( +// <::RuntimeOrigin>::signed(hotkey_account_id), +// netuid, +// block_number, +// nonce, +// work.clone(), +// hotkey_account_id, +// coldkey_account_id +// )); + +// assert!(SubtensorModule::subnet_exists(netuid)); + +// // the account is not network owner when schedule the call +// assert_ok!(SubtensorModule::schedule_swap_coldkey( +// <::RuntimeOrigin>::signed(coldkey_account_id), +// new_network_owner_account_id +// )); + +// let current_block = System::block_number(); +// let execution_block = current_block + ColdkeySwapScheduleDuration::::get(); + +// run_to_block(execution_block - 1); + +// // the account is not network owner when schedule the call +// assert_ok!(SubtensorModule::schedule_dissolve_network( +// <::RuntimeOrigin>::signed(new_network_owner_account_id), +// netuid +// )); + +// System::assert_last_event( +// Event::DissolveNetworkScheduled { +// account: new_network_owner_account_id, +// netuid, +// execution_block: DissolveNetworkScheduleDuration::::get() + execution_block +// - 1, +// } +// .into(), +// ); + +// run_to_block(execution_block); +// assert_eq!( +// crate::SubnetOwner::::get(netuid), +// new_network_owner_account_id +// ); + +// let current_block = System::block_number(); +// let execution_block = current_block + DissolveNetworkScheduleDuration::::get(); + +// run_to_block(execution_block); +// // network exists since the caller is no the network owner +// assert!(!SubtensorModule::subnet_exists(netuid)); +// }) +// } + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::networks::test_register_subnet_low_lock_cost --exact --show-output --nocapture diff --git a/pallets/subtensor/src/tests/networks/mod.rs b/pallets/subtensor/src/tests/networks/mod.rs new file mode 100644 index 0000000000..394cfa86f5 --- /dev/null +++ b/pallets/subtensor/src/tests/networks/mod.rs @@ -0,0 +1,39 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! Integration tests for subnet register / dissolve / prune / registration-queue. +//! +//! Split from the former monolithic `tests/networks.rs` into concept modules. +//! +//! ## Search anchors +//! +//! | Module | Owns | +//! |--------|------| +//! | [`helpers`] | dissolve pipeline + owner-alpha price fixtures | +//! | [`dissolve_refunds`] | lock refund, pro-rata TAO, protocol-alpha share | +//! | [`dissolve_storage_cleanup`] | per-subnet / mechanism / lock map purge | +//! | [`dissolve_async_cleanup`] | on_idle cleanup queue and phases | +//! | [`destroy_alpha_stakes`] | α-in/out stake destroy payouts and lock cleanup | +//! | [`prune_network`] | lowest-price prune selection and immunity | +//! | [`register_network`] | register network, lock cost, owner-alpha seeding | +//! | [`median_subnet_alpha_price`] | median α price for new subnet pool seed | +//! | [`registered_subnet_counter`] | per-netuid registration counter | +//! | [`migrate_network_immunity`] | network immunity period migration | +//! | [`set_new_network_state`] | `set_new_network_state` pool / identity / limits | +//! | [`network_registration_queue`] | deferred registration after dissolve cleanup | +//! | [`massive_dissolve_reregistration`] | lossless dissolve + re-register flow | +//! | [`tempo_rate_limit`] | tempo vs weight-set rate limit gate | + +mod destroy_alpha_stakes; +mod dissolve_async_cleanup; +mod dissolve_refunds; +mod dissolve_storage_cleanup; +mod helpers; +mod massive_dissolve_reregistration; +mod median_subnet_alpha_price; +mod migrate_network_immunity; +mod network_registration_queue; +mod prelude; +mod prune_network; +mod register_network; +mod registered_subnet_counter; +mod set_new_network_state; +mod tempo_rate_limit; diff --git a/pallets/subtensor/src/tests/networks/network_registration_queue.rs b/pallets/subtensor/src/tests/networks/network_registration_queue.rs new file mode 100644 index 0000000000..4deee4c6b3 --- /dev/null +++ b/pallets/subtensor/src/tests/networks/network_registration_queue.rs @@ -0,0 +1,271 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! Deferred `NetworkRegistrationQueue` processing after dissolve cleanup. + +use super::prelude::*; + +#[test] +fn register_network_queues_when_waiting_for_dissolve_cleanup() { + new_test_ext(0).execute_with(|| { + SubnetLimit::::put(2u16); + + let n1 = add_dynamic_network(&U256::from(9102), &U256::from(9101)); + let _n2 = add_dynamic_network(&U256::from(9202), &U256::from(9201)); + + assert_ok!(SubtensorModule::do_dissolve_network(n1)); + assert!(DissolveCleanupQueue::::get().contains(&n1)); + + let cold = U256::from(9301); + let hot = U256::from(9302); + let lock_amount = SubtensorModule::get_network_lock_cost(); + add_balance_to_coldkey_account(&cold, lock_amount.saturating_mul(2.into()).into()); + TotalIssuance::::mutate(|total| *total = total.saturating_add(lock_amount)); + + assert_ok!(SubtensorModule::do_register_network( + RuntimeOrigin::signed(cold), + &hot, + 1, + None, + )); + + assert_eq!(NetworkRegistrationQueue::::get().len(), 1); + assert_eq!(NetworkRegistrationQueue::::get()[0].coldkey, cold); + assert_eq!(TotalNetworks::::get(), 1); + assert!(!SubtensorModule::hotkey_account_exists(&hot)); + }); +} + +#[test] +fn process_network_registration_queue_registers_after_cleanup_slot_available() { + new_test_ext(0).execute_with(|| { + SubnetLimit::::put(2u16); + + let n1 = add_dynamic_network(&U256::from(9402), &U256::from(9401)); + let n2 = add_dynamic_network(&U256::from(9502), &U256::from(9501)); + + assert_ok!(SubtensorModule::do_dissolve_network(n1)); + assert!(DissolveCleanupQueue::::get().contains(&n1)); + + let cold = U256::from(9601); + let hot = U256::from(9602); + let lock_amount = SubtensorModule::get_network_lock_cost(); + add_balance_to_coldkey_account(&cold, lock_amount.saturating_mul(3.into()).into()); + TotalIssuance::::mutate(|total| *total = total.saturating_add(lock_amount)); + + assert_ok!(SubtensorModule::do_register_network( + RuntimeOrigin::signed(cold), + &hot, + 1, + None, + )); + assert_eq!(NetworkRegistrationQueue::::get().len(), 1); + + // Simulate dissolve cleanup completing and freeing a subnet slot. + DissolveCleanupQueue::::kill(); + + run_network_registration_queue(); + + assert!(NetworkRegistrationQueue::::get().is_empty()); + assert!(SubtensorModule::hotkey_account_exists(&hot)); + assert_eq!(TotalNetworks::::get(), 2); + + let registered_netuid = NetworksAdded::::iter() + .find(|(netuid, added)| *added && *netuid != n2) + .map(|(netuid, _)| netuid) + .expect("queued registration should create a new subnet"); + assert_eq!(SubnetOwner::::get(registered_netuid), cold); + }); +} + +#[test] +fn register_network_prune_registers_registration_queued() { + new_test_ext(0).execute_with(|| { + SubnetLimit::::put(2u16); + + let n1 = add_dynamic_network(&U256::from(9702), &U256::from(9701)); + let n2 = add_dynamic_network(&U256::from(9802), &U256::from(9801)); + + let imm = SubtensorModule::get_network_immunity_period(); + System::set_block_number(imm + 100); + Emission::::insert(n1, vec![AlphaBalance::from(1)]); + Emission::::insert(n2, vec![AlphaBalance::from(1_000)]); + + let cold = U256::from(9901); + let hot = U256::from(9902); + let lock_amount = SubtensorModule::get_network_lock_cost(); + add_balance_to_coldkey_account(&cold, lock_amount.saturating_mul(10.into()).into()); + TotalIssuance::::mutate(|total| *total = total.saturating_add(lock_amount)); + + assert_ok!(SubtensorModule::do_register_network( + RuntimeOrigin::signed(cold), + &hot, + 1, + None, + )); + + assert!(NetworkRegistrationQueue::::get().len() == 1); + assert!(DissolveCleanupQueue::::get().contains(&n1)); + assert!(!NetworksAdded::::get(n1)); + }); +} + +#[test] +fn process_network_registration_queue_noop_when_empty() { + new_test_ext(1).execute_with(|| { + let networks_before = TotalNetworks::::get(); + + SubtensorModule::process_network_registration_queue(); + + assert!(NetworkRegistrationQueue::::get().is_empty()); + assert_eq!(TotalNetworks::::get(), networks_before); + }); +} + +#[test] +fn process_network_registration_queue_waits_for_cleanup_completion() { + new_test_ext(0).execute_with(|| { + SubnetLimit::::put(2u16); + + let n1 = add_dynamic_network(&U256::from(10_502), &U256::from(10_501)); + let _n2 = add_dynamic_network(&U256::from(10_602), &U256::from(10_601)); + + assert_ok!(SubtensorModule::do_dissolve_network(n1)); + + let cold = U256::from(10_701); + let hot = U256::from(10_702); + let lock_amount = SubtensorModule::get_network_lock_cost(); + add_balance_to_coldkey_account(&cold, lock_amount.saturating_mul(2.into()).into()); + + assert_ok!(SubtensorModule::do_register_network( + RuntimeOrigin::signed(cold), + &hot, + 1, + None, + )); + assert_eq!(NetworkRegistrationQueue::::get().len(), 1); + + // Cleanup is still pending: the queued registration must not be released. + SubtensorModule::process_network_registration_queue(); + + assert_eq!(NetworkRegistrationQueue::::get().len(), 1); + assert!(!SubtensorModule::hotkey_account_exists(&hot)); + assert_eq!(TotalNetworks::::get(), 1); + + // Once cleanup completes, the same call releases the registration. + DissolveCleanupQueue::::kill(); + SubtensorModule::process_network_registration_queue(); + + assert!(NetworkRegistrationQueue::::get().is_empty()); + assert!(SubtensorModule::hotkey_account_exists(&hot)); + assert_eq!(TotalNetworks::::get(), 2); + }); +} + +#[test] +fn process_network_registration_queue_processes_one_entry_per_call() { + new_test_ext(0).execute_with(|| { + SubnetLimit::::put(3u16); + + let n1 = add_dynamic_network(&U256::from(10_802), &U256::from(10_801)); + let n2 = add_dynamic_network(&U256::from(10_902), &U256::from(10_901)); + let _n3 = add_dynamic_network(&U256::from(11_002), &U256::from(11_001)); + + assert_ok!(SubtensorModule::do_dissolve_network(n1)); + assert_ok!(SubtensorModule::do_dissolve_network(n2)); + assert_eq!(DissolveCleanupQueue::::get().len(), 2); + + let cold_a = U256::from(11_101); + let hot_a = U256::from(11_102); + let cold_b = U256::from(11_201); + let hot_b = U256::from(11_202); + for cold in [&cold_a, &cold_b] { + let lock_amount = SubtensorModule::get_network_lock_cost(); + add_balance_to_coldkey_account(cold, lock_amount.saturating_mul(2.into()).into()); + } + + assert_ok!(SubtensorModule::do_register_network( + RuntimeOrigin::signed(cold_a), + &hot_a, + 1, + None, + )); + assert_ok!(SubtensorModule::do_register_network( + RuntimeOrigin::signed(cold_b), + &hot_b, + 1, + None, + )); + assert_eq!(NetworkRegistrationQueue::::get().len(), 2); + + DissolveCleanupQueue::::kill(); + + // First call processes only the first (FIFO) entry. + SubtensorModule::process_network_registration_queue(); + assert_eq!(NetworkRegistrationQueue::::get().len(), 1); + assert!(SubtensorModule::hotkey_account_exists(&hot_a)); + assert!(!SubtensorModule::hotkey_account_exists(&hot_b)); + assert_eq!(NetworkRegistrationQueue::::get()[0].coldkey, cold_b); + + // Second call processes the remaining entry. + SubtensorModule::process_network_registration_queue(); + assert!(NetworkRegistrationQueue::::get().is_empty()); + assert!(SubtensorModule::hotkey_account_exists(&hot_b)); + assert_eq!(TotalNetworks::::get(), 3); + }); +} + +#[test] +fn process_network_registration_queue_unlocks_funds_and_charges_coldkey() { + new_test_ext(0).execute_with(|| { + SubnetLimit::::put(2u16); + + let n1 = add_dynamic_network(&U256::from(11_302), &U256::from(11_301)); + let n2 = add_dynamic_network(&U256::from(11_402), &U256::from(11_401)); + + assert_ok!(SubtensorModule::do_dissolve_network(n1)); + + let cold = U256::from(11_501); + let hot = U256::from(11_502); + let lock_amount = SubtensorModule::get_network_lock_cost(); + let lock_id = NetworkRegistrationLockId::::get(); + let mut identifier = [0u8; 8]; + identifier[..4].copy_from_slice(b"rglk"); + identifier[4..8].copy_from_slice(&lock_id.to_le_bytes()); + add_balance_to_coldkey_account(&cold, lock_amount.saturating_mul(3.into()).into()); + + assert_ok!(SubtensorModule::do_register_network( + RuntimeOrigin::signed(cold), + &hot, + 1, + None, + )); + + // Funds are locked while queued. + assert!( + pallet_balances::Locks::::get(cold) + .iter() + .any(|l| l.id == identifier) + ); + let queued_lock = NetworkRegistrationQueue::::get()[0].lock_amount; + // Use free balance: the reducible balance is already reduced by the lock. + let balance_before = pallet_balances::Pallet::::free_balance(cold); + + DissolveCleanupQueue::::kill(); + SubtensorModule::process_network_registration_queue(); + + // Lock released and the lock cost transferred to the new subnet. + assert!( + pallet_balances::Locks::::get(cold) + .iter() + .all(|l| l.id != identifier) + ); + let balance_after = pallet_balances::Pallet::::free_balance(cold); + assert_eq!(balance_before.saturating_sub(balance_after), queued_lock); + + let new_netuid = NetworksAdded::::iter() + .find(|(netuid, added)| *added && *netuid != n2) + .map(|(netuid, _)| netuid) + .expect("queued registration should create a new subnet"); + assert_eq!(SubnetOwner::::get(new_netuid), cold); + assert_eq!(SubnetLocked::::get(new_netuid), queued_lock); + }); +} diff --git a/pallets/subtensor/src/tests/networks/prelude.rs b/pallets/subtensor/src/tests/networks/prelude.rs new file mode 100644 index 0000000000..3be1529d34 --- /dev/null +++ b/pallets/subtensor/src/tests/networks/prelude.rs @@ -0,0 +1,16 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! Shared imports for network unit tests. + +pub use frame_support::{assert_err, assert_ok, weights::Weight}; +pub use frame_system::Config; +pub use sp_core::U256; +pub use sp_runtime::PerU16; +pub use sp_std::collections::{btree_map::BTreeMap, vec_deque::VecDeque}; +pub use substrate_fixed::types::{I96F32, U64F64, U96F32}; +pub use subtensor_runtime_common::{MechId, NetUidStorageIndex, TaoBalance}; +pub use subtensor_swap_interface::{Order, SwapHandler}; + +pub use super::super::mock::*; +pub use crate::migrations::migrate_network_immunity_period; +pub use crate::staking::lock::LockState; +pub use crate::*; diff --git a/pallets/subtensor/src/tests/networks/prune_network.rs b/pallets/subtensor/src/tests/networks/prune_network.rs new file mode 100644 index 0000000000..245870813e --- /dev/null +++ b/pallets/subtensor/src/tests/networks/prune_network.rs @@ -0,0 +1,288 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! Subnet prune selection by price, immunity, and registration time. + +use super::prelude::*; + +#[test] +fn prune_none_with_no_networks() { + new_test_ext(0).execute_with(|| { + assert_eq!(SubtensorModule::get_network_to_prune(), None); + }); +} + +#[test] +fn prune_none_when_all_networks_immune() { + new_test_ext(0).execute_with(|| { + // two fresh networks → still inside immunity window + let n1 = add_dynamic_network(&U256::from(2), &U256::from(1)); + let _n2 = add_dynamic_network(&U256::from(4), &U256::from(3)); + + // emissions don’t matter while immune + Emission::::insert(n1, vec![AlphaBalance::from(10)]); + + assert_eq!(SubtensorModule::get_network_to_prune(), None); + }); +} + +#[test] +fn prune_selects_network_with_lowest_price() { + new_test_ext(0).execute_with(|| { + let n1 = add_dynamic_network(&U256::from(20), &U256::from(10)); + let n2 = add_dynamic_network(&U256::from(40), &U256::from(30)); + + // make both networks eligible (past immunity) + let imm = SubtensorModule::get_network_immunity_period(); + System::set_block_number(imm + 10); + + // n1 has lower price → should be pruned + SubnetMovingPrice::::insert(n1, I96F32::from_num(1)); + SubnetMovingPrice::::insert(n2, I96F32::from_num(10)); + + assert_eq!(SubtensorModule::get_network_to_prune(), Some(n1)); + }); +} + +#[test] +fn prune_ignores_immune_network_even_if_lower_price() { + new_test_ext(0).execute_with(|| { + // create mature network n1 first + let n1 = add_dynamic_network(&U256::from(22), &U256::from(11)); + + let imm = SubtensorModule::get_network_immunity_period(); + System::set_block_number(imm + 5); // advance → n1 now mature + + // create second network n2 *inside* immunity + let n2 = add_dynamic_network(&U256::from(44), &U256::from(33)); + + // prices: n2 lower but immune; n1 must be selected + SubnetMovingPrice::::insert(n1, I96F32::from_num(5)); + SubnetMovingPrice::::insert(n2, I96F32::from_num(1)); + + System::set_block_number(imm + 10); // still immune for n2 + assert_eq!(SubtensorModule::get_network_to_prune(), Some(n1)); + }); +} + +#[test] +fn prune_tie_on_price_earlier_registration_wins() { + new_test_ext(0).execute_with(|| { + // n1 registered first + let n1 = add_dynamic_network(&U256::from(66), &U256::from(55)); + + // advance 1 block, then register n2 (later timestamp) + System::set_block_number(1); + let n2 = add_dynamic_network(&U256::from(88), &U256::from(77)); + + // push past immunity for both + let imm = SubtensorModule::get_network_immunity_period(); + System::set_block_number(imm + 20); + + // identical prices → tie; earlier (n1) must be chosen + SubnetMovingPrice::::insert(n1, I96F32::from_num(7)); + SubnetMovingPrice::::insert(n2, I96F32::from_num(7)); + + assert_eq!(SubtensorModule::get_network_to_prune(), Some(n1)); + }); +} + +#[test] +fn prune_selection_complex_state_exhaustive() { + new_test_ext(0).execute_with(|| { + let imm = SubtensorModule::get_network_immunity_period(); + + // --------------------------------------------------------------------- + // Build a rich topology of networks with controlled registration times. + // --------------------------------------------------------------------- + // n1 + n2 in the same block (equal timestamp) to test "tie + same time". + System::set_block_number(0); + let n1 = add_dynamic_network(&U256::from(101), &U256::from(201)); + let n2 = add_dynamic_network(&U256::from(102), &U256::from(202)); // same registered_at as n1 + + // Later registrations (strictly greater timestamp than n1/n2) + System::set_block_number(1); + let n3 = add_dynamic_network(&U256::from(103), &U256::from(203)); + + System::set_block_number(2); + let n4 = add_dynamic_network(&U256::from(104), &U256::from(204)); + + // Create *immune* networks that will remain ineligible initially, + // even if their price is the lowest. + System::set_block_number(imm + 5); + let n5 = add_dynamic_network(&U256::from(105), &U256::from(205)); // immune at first + + System::set_block_number(imm + 6); + let n6 = add_dynamic_network(&U256::from(106), &U256::from(206)); // immune at first + + // Add 100 TAO to subnet accounts (lock) + let subnet_account1 = SubtensorModule::get_subnet_account_id(n1).unwrap(); + let subnet_account2 = SubtensorModule::get_subnet_account_id(n2).unwrap(); + let subnet_account3 = SubtensorModule::get_subnet_account_id(n3).unwrap(); + let subnet_account4 = SubtensorModule::get_subnet_account_id(n4).unwrap(); + let subnet_account5 = SubtensorModule::get_subnet_account_id(n5).unwrap(); + let subnet_account6 = SubtensorModule::get_subnet_account_id(n6).unwrap(); + add_balance_to_coldkey_account(&subnet_account1, 100_000_000_000_u64.into()); + add_balance_to_coldkey_account(&subnet_account2, 100_000_000_000_u64.into()); + add_balance_to_coldkey_account(&subnet_account3, 100_000_000_000_u64.into()); + add_balance_to_coldkey_account(&subnet_account4, 100_000_000_000_u64.into()); + add_balance_to_coldkey_account(&subnet_account5, 100_000_000_000_u64.into()); + add_balance_to_coldkey_account(&subnet_account6, 100_000_000_000_u64.into()); + + // (Root is ignored by the selector.) + let root = NetUid::ROOT; + + // --------------------------------------------------------------------- + // Drive pruning via the EMA/moving price used by `get_network_to_prune()`. + // We set the moving prices directly to create deterministic selections. + // + // Intended prices: + // n1: 25, n2: 25, n3: 100, n4: 1, n5: 0 (immune initially), n6: 0 (immune initially) + // --------------------------------------------------------------------- + SubnetMovingPrice::::insert(n1, I96F32::from_num(25)); + SubnetMovingPrice::::insert(n2, I96F32::from_num(25)); + SubnetMovingPrice::::insert(n3, I96F32::from_num(100)); + SubnetMovingPrice::::insert(n4, I96F32::from_num(1)); + SubnetMovingPrice::::insert(n5, I96F32::from_num(0)); + SubnetMovingPrice::::insert(n6, I96F32::from_num(0)); + + // --------------------------------------------------------------------- + // Phase A: Only n1..n4 are mature → lowest price (n4=1) should win. + // --------------------------------------------------------------------- + System::set_block_number(imm + 10); + assert_eq!( + SubtensorModule::get_network_to_prune(), + Some(n4), + "Among mature nets (n1..n4), n4 has price=1 (lowest) and should be chosen." + ); + + // --------------------------------------------------------------------- + // Phase B: Tie on price with *same registration time* (n1 vs n2). + // Raise n4's price to 25 so {n1=25, n2=25, n3=100, n4=25}. + // n1 and n2 share the *same registered_at*. The tie should keep the + // first encountered (stable iteration by key order) → n1. + // --------------------------------------------------------------------- + SubnetMovingPrice::::insert(n4, I96F32::from_num(25)); // n4 now 25 + assert_eq!( + SubtensorModule::get_network_to_prune(), + Some(n1), + "Tie on price with equal timestamps (n1,n2) → first encountered (n1) should persist." + ); + + // --------------------------------------------------------------------- + // Phase C: Tie on price with *different registration times*. + // Make n3 price=25 as well. Now n1,n2,n3,n4 all have price=25. + // Earliest registration among them is n1 (block 0). + // --------------------------------------------------------------------- + SubnetMovingPrice::::insert(n3, I96F32::from_num(25)); + assert_eq!( + SubtensorModule::get_network_to_prune(), + Some(n1), + "Tie on price across multiple nets → earliest registration (n1) wins." + ); + + // --------------------------------------------------------------------- + // Phase D: Immune networks ignored even if strictly cheaper (0). + // n5 and n6 price=0 but still immune at (imm + 10). Ensure they are + // ignored and selection remains n1. + // --------------------------------------------------------------------- + let now = System::block_number(); + assert!( + now < NetworkRegisteredAt::::get(n5) + imm, + "n5 is immune at current block" + ); + assert!( + now < NetworkRegisteredAt::::get(n6) + imm, + "n6 is immune at current block" + ); + assert_eq!( + SubtensorModule::get_network_to_prune(), + Some(n1), + "Immune nets (n5,n6) must be ignored despite lower price." + ); + + // --------------------------------------------------------------------- + // Phase E: If *all* networks are immune → return None. + // Move clock back before any network's immunity expires. + // --------------------------------------------------------------------- + System::set_block_number(0); + assert_eq!( + SubtensorModule::get_network_to_prune(), + None, + "With all networks immune, there is no prunable candidate." + ); + + // --------------------------------------------------------------------- + // Phase F: Advance beyond immunity for n5 & n6. + // Both n5 and n6 now eligible with price=0 (lowest). + // Tie on price; earlier registration between n5 and n6 is n5. + // --------------------------------------------------------------------- + System::set_block_number(2 * imm + 10); + assert!( + System::block_number() >= NetworkRegisteredAt::::get(n5) + imm, + "n5 has matured" + ); + assert!( + System::block_number() >= NetworkRegisteredAt::::get(n6) + imm, + "n6 has matured" + ); + assert_eq!( + SubtensorModule::get_network_to_prune(), + Some(n5), + "After immunity, n5 (price=0) should win; tie with n6 broken by earlier registration." + ); + + // --------------------------------------------------------------------- + // Phase G: Create *sparse* netuids and ensure selection is stable. + // Remove n5; now n6 (price=0) should be selected. + // This validates robustness to holes / non-contiguous netuids. + // --------------------------------------------------------------------- + assert_ok!(SubtensorModule::do_dissolve_network(n5)); + assert_eq!( + SubtensorModule::get_network_to_prune(), + Some(n6), + "After removing n5, next-lowest (n6=0) should be chosen even with sparse netuids." + ); + + // --------------------------------------------------------------------- + // Phase H: Dynamic price changes. + // Make n6 expensive (price 100); make n3 cheapest (price 1). + // --------------------------------------------------------------------- + SubnetMovingPrice::::insert(n6, I96F32::from_num(100)); + SubnetMovingPrice::::insert(n3, I96F32::from_num(1)); + assert_eq!( + SubtensorModule::get_network_to_prune(), + Some(n3), + "Dynamic changes: n3 set to price=1 (lowest among eligibles) → should be pruned." + ); + + // --------------------------------------------------------------------- + // Phase I: Tie again (n2 vs n3) but earlier registration must win. + // Give n2 the same price as n3; n2 registered at block 0, n3 at block 1. + // n2 should be chosen. + // --------------------------------------------------------------------- + SubnetMovingPrice::::insert(n2, I96F32::from_num(1)); + assert_eq!( + SubtensorModule::get_network_to_prune(), + Some(n2), + "Tie on price across n2 (earlier reg) and n3 → n2 wins by timestamp." + ); + + // --------------------------------------------------------------------- + // (Extra) Mark n2 as 'not added' to assert we honor the `added` flag, + // then restore it to avoid side-effects on subsequent tests. + // --------------------------------------------------------------------- + NetworksAdded::::insert(n2, false); + assert_ne!( + SubtensorModule::get_network_to_prune(), + Some(n2), + "`added=false` must exclude n2 from consideration." + ); + NetworksAdded::::insert(n2, true); + + // Root is always ignored even if cheapest (get_moving_alpha_price returns 1 for ROOT). + assert_ne!( + SubtensorModule::get_network_to_prune(), + Some(root), + "ROOT must never be selected for pruning." + ); + }); +} diff --git a/pallets/subtensor/src/tests/networks/register_network.rs b/pallets/subtensor/src/tests/networks/register_network.rs new file mode 100644 index 0000000000..296c724eaf --- /dev/null +++ b/pallets/subtensor/src/tests/networks/register_network.rs @@ -0,0 +1,457 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! `do_register_network` / PoW register paths, lock cost, owner-alpha seeding. + +use super::helpers::*; +use super::prelude::*; + +#[test] +fn test_registration_ok() { + new_test_ext(1).execute_with(|| { + let block_number: u64 = 0; + let netuid = NetUid::from(2); + let tempo: u16 = 13; + let hotkey_account_id: U256 = U256::from(1); + let coldkey_account_id: U256 = U256::from(0); // Neighbour of the beast, har har + + add_network(netuid, tempo, 0); + + // Ensure reserves exist for any registration path that might touch swap/burn logic. + let reserve: u64 = 1_000_000_000_000; + setup_reserves( + netuid, + TaoBalance::from(reserve), + AlphaBalance::from(reserve), + ); + + // registration economics changed. Ensure the coldkey has enough spendable balance + add_balance_to_coldkey_account(&coldkey_account_id, TaoBalance::from(reserve)); + add_balance_to_coldkey_account(&hotkey_account_id, TaoBalance::from(reserve)); + + let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( + netuid, + block_number, + 129123813, + &hotkey_account_id, + ); + + // PoW register should succeed. + assert_ok!(SubtensorModule::register( + <::RuntimeOrigin>::signed(hotkey_account_id), + netuid, + block_number, + nonce, + work.clone(), + hotkey_account_id, + coldkey_account_id + )); + + assert_ok!(SubtensorModule::do_dissolve_network(netuid)); + assert!(!SubtensorModule::subnet_exists(netuid)); + }) +} + +#[test] +fn register_network_skips_dissolved_netuid() { + new_test_ext(0).execute_with(|| { + let dissolved = NetUid::from(1); + DissolveCleanupQueue::::put(vec![dissolved]); + + let cold = U256::from(60); + let hot = U256::from(61); + let needed: u64 = SubtensorModule::get_network_lock_cost().into(); + add_balance_to_coldkey_account(&cold, needed.saturating_mul(10).into()); + + assert_ok!(SubtensorModule::do_register_network( + RuntimeOrigin::signed(cold), + &hot, + 1, + None, + )); + + assert!(!NetworksAdded::::get(dissolved)); + let expected = NetUid::from(2); + assert!(NetworksAdded::::get(expected)); + assert_eq!(SubnetOwner::::get(expected), cold); + }); +} + +#[test] +fn register_network_fails_before_prune_keeps_existing() { + new_test_ext(0).execute_with(|| { + SubnetLimit::::put(1u16); + + let n_cold = U256::from(41); + let n_hot = U256::from(42); + let net = add_dynamic_network(&n_hot, &n_cold); + + let imm = SubtensorModule::get_network_immunity_period(); + System::set_block_number(imm + 50); + Emission::::insert(net, vec![AlphaBalance::from(10)]); + + let caller_cold = U256::from(50); + let caller_hot = U256::from(51); + + assert_err!( + SubtensorModule::do_register_network( + RuntimeOrigin::signed(caller_cold), + &caller_hot, + 1, + None, + ), + Error::::CannotAffordLockCost + ); + + assert!(SubtensorModule::subnet_exists(net)); + assert_eq!(TotalNetworks::::get(), 1); + }); +} + +#[test] +fn test_register_subnet_low_lock_cost() { + new_test_ext(1).execute_with(|| { + NetworkMinLockCost::::set(TaoBalance::from(1_000)); + NetworkLastLockCost::::set(TaoBalance::from(1_000)); + + // Make sure lock cost is lower than 100 TAO + let lock_cost = SubtensorModule::get_network_lock_cost(); + assert!(lock_cost < 100_000_000_000_u64.into()); + + let subnet_owner_coldkey = U256::from(1); + let subnet_owner_hotkey = U256::from(2); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + assert!(SubtensorModule::subnet_exists(netuid)); + + // Ensure that both Subnet TAO and Subnet Alpha In equal to (actual) lock_cost + assert_eq!(SubnetTAO::::get(netuid), lock_cost); + assert_eq!( + SubnetAlphaIn::::get(netuid), + lock_cost.to_u64().into() + ); + }) +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::networks::test_register_subnet_high_lock_cost --exact --show-output --nocapture + +#[test] +fn test_register_subnet_high_lock_cost() { + new_test_ext(1).execute_with(|| { + let lock_cost = TaoBalance::from(1_000_000_000_000_u64); + NetworkMinLockCost::::set(lock_cost); + NetworkLastLockCost::::set(lock_cost); + + // Make sure lock cost is higher than 100 TAO + let lock_cost = SubtensorModule::get_network_lock_cost(); + assert!(lock_cost >= 1_000_000_000_000_u64.into()); + + let subnet_owner_coldkey = U256::from(1); + let subnet_owner_hotkey = U256::from(2); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + assert!(SubtensorModule::subnet_exists(netuid)); + + // Ensure that both Subnet TAO and Subnet Alpha In equal to 100 TAO + assert_eq!(SubnetTAO::::get(netuid), lock_cost); + assert_eq!( + SubnetAlphaIn::::get(netuid), + lock_cost.to_u64().into() + ); + }) +} + +#[test] +fn register_network_seeds_first_subnet_from_fallback_price_one_and_keeps_lock_in_pool() { + new_test_ext(1).execute_with(|| { + let new_cold = U256::from(1001); + let new_hot = U256::from(1002); + let new_netuid = SubtensorModule::get_next_netuid(); + + let lock_cost_u64: u64 = SubtensorModule::get_network_lock_cost().into(); + let pre_registration_median = SubtensorModule::get_median_subnet_alpha_price(); + + let pool_initial_tao = SubtensorModule::get_network_min_lock(); + let pool_initial_tao_u64 = pool_initial_tao.to_u64(); + let total_pool_tao_u64 = lock_cost_u64.max(pool_initial_tao_u64); + let owner_alpha_tao_equivalent_u64 = + total_pool_tao_u64.saturating_sub(pool_initial_tao_u64); + + let expected_pool_alpha_u64 = + owner_alpha_from_lock_and_price(total_pool_tao_u64, pre_registration_median); + let expected_pool_alpha: AlphaBalance = expected_pool_alpha_u64.into(); + + let expected_owner_alpha_u64 = owner_alpha_from_lock_and_price( + owner_alpha_tao_equivalent_u64, + pre_registration_median, + ); + let expected_owner_alpha: AlphaBalance = expected_owner_alpha_u64.into(); + + let expected_alpha_issuance: AlphaBalance = expected_pool_alpha_u64 + .saturating_add(expected_owner_alpha_u64) + .into(); + + let expected_recycled: TaoBalance = lock_cost_u64.saturating_sub(total_pool_tao_u64).into(); + + assert_eq!(pre_registration_median, U96F32::from_num(1u64)); + assert_eq!(expected_pool_alpha_u64, total_pool_tao_u64); + assert_eq!(expected_owner_alpha_u64, owner_alpha_tao_equivalent_u64); + assert_eq!(expected_recycled, TaoBalance::ZERO); + + add_balance_to_coldkey_account(&new_cold, lock_cost_u64.saturating_mul(2).into()); + + assert_ok!(SubtensorModule::do_register_network( + RuntimeOrigin::signed(new_cold), + &new_hot, + 1, + None, + )); + + assert!(SubtensorModule::subnet_exists(new_netuid)); + assert_eq!(TotalNetworks::::get(), 1); + assert_eq!(SubnetOwner::::get(new_netuid), new_cold); + assert_eq!(SubnetOwnerHotkey::::get(new_netuid), new_hot); + assert_eq!( + SubtensorModule::get_subnet_locked_balance(new_netuid), + TaoBalance::from(lock_cost_u64) + ); + + assert_eq!( + SubnetTAO::::get(new_netuid), + TaoBalance::from(total_pool_tao_u64) + ); + assert_eq!(SubnetAlphaIn::::get(new_netuid), expected_pool_alpha); + assert_eq!( + SubnetAlphaOut::::get(new_netuid), + expected_owner_alpha + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &new_hot, &new_cold, new_netuid, + ), + expected_owner_alpha + ); + assert_eq!( + TotalHotkeyAlpha::::get(new_hot, new_netuid), + expected_owner_alpha + ); + assert_eq!( + SubtensorModule::get_alpha_issuance(new_netuid), + expected_alpha_issuance + ); + assert_eq!( + RAORecycledForRegistration::::get(new_netuid), + expected_recycled + ); + + assert_eq!( + ::SwapInterface::current_alpha_price(new_netuid.into()), + U96F32::from_num(1u64) + ); + + System::assert_last_event(Event::NetworkAdded(new_netuid, 1).into()); + }); +} + +#[test] +fn register_network_seeds_new_subnet_from_even_median_snapshot() { + new_test_ext(0).execute_with(|| { + let n1 = add_dynamic_network(&U256::from(1201), &U256::from(1200)); + let n2 = add_dynamic_network(&U256::from(1203), &U256::from(1202)); + + // Existing prices are {5, 2} -> pre-registration median is 3.5. + setup_reserves(n1, TaoBalance::from(500u64), AlphaBalance::from(100u64)); + setup_reserves(n2, TaoBalance::from(200u64), AlphaBalance::from(100u64)); + + let pre_registration_median = SubtensorModule::get_median_subnet_alpha_price(); + assert_eq!(pre_registration_median, U96F32::from_num(3.5)); + + let new_cold = U256::from(1300); + let new_hot = U256::from(1301); + let new_netuid = SubtensorModule::get_next_netuid(); + + let lock_cost_u64: u64 = SubtensorModule::get_network_lock_cost().into(); + let pool_initial_tao_u64 = SubtensorModule::get_network_min_lock().to_u64(); + let total_pool_tao_u64 = lock_cost_u64.max(pool_initial_tao_u64); + let owner_alpha_tao_equivalent_u64 = + total_pool_tao_u64.saturating_sub(pool_initial_tao_u64); + + let expected_pool_alpha_u64 = + owner_alpha_from_lock_and_price(total_pool_tao_u64, pre_registration_median); + let expected_pool_alpha: AlphaBalance = expected_pool_alpha_u64.into(); + + let expected_owner_alpha_u64 = owner_alpha_from_lock_and_price( + owner_alpha_tao_equivalent_u64, + pre_registration_median, + ); + let expected_owner_alpha: AlphaBalance = expected_owner_alpha_u64.into(); + + add_balance_to_coldkey_account(&new_cold, lock_cost_u64.saturating_mul(2).into()); + + assert_ok!(SubtensorModule::do_register_network( + RuntimeOrigin::signed(new_cold), + &new_hot, + 1, + None, + )); + + let new_subnet_price = + ::SwapInterface::current_alpha_price(new_netuid.into()); + let post_registration_median = SubtensorModule::get_median_subnet_alpha_price(); + + assert!(SubtensorModule::subnet_exists(new_netuid)); + assert_eq!(SubnetOwner::::get(new_netuid), new_cold); + assert_eq!(SubnetOwnerHotkey::::get(new_netuid), new_hot); + assert_eq!( + SubtensorModule::get_subnet_locked_balance(new_netuid), + TaoBalance::from(lock_cost_u64) + ); + + assert_eq!( + SubnetTAO::::get(new_netuid), + TaoBalance::from(total_pool_tao_u64) + ); + assert_eq!(SubnetAlphaIn::::get(new_netuid), expected_pool_alpha); + assert_eq!( + SubnetAlphaOut::::get(new_netuid), + expected_owner_alpha + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &new_hot, &new_cold, new_netuid, + ), + expected_owner_alpha + ); + assert_eq!( + TotalHotkeyAlpha::::get(new_hot, new_netuid), + expected_owner_alpha + ); + + // The new subnet is seeded from the pre-registration median snapshot, + // so it is no longer initialized at the old 1:1 seed price. + assert_ne!(new_subnet_price, U96F32::from_num(1u64)); + assert!(new_subnet_price >= pre_registration_median); + + // With prices {2, seeded_price, 5}, the live median becomes the new subnet price. + assert_eq!(post_registration_median, new_subnet_price); + + // A 1:1 seed would have alpha_in == tao_in, which should not happen here. + let wrong_price_one_pool_alpha: AlphaBalance = total_pool_tao_u64.into(); + assert_ne!( + SubnetAlphaIn::::get(new_netuid), + wrong_price_one_pool_alpha + ); + }); +} + +#[test] +fn register_network_fails_without_balance_and_does_not_write_owner_alpha_state() { + new_test_ext(0).execute_with(|| { + let cold = U256::from(2001); + let hot = U256::from(2002); + let would_be_netuid = SubtensorModule::get_next_netuid(); + + assert_eq!( + SubtensorModule::get_coldkey_balance(&cold), + TaoBalance::ZERO + ); + + assert_err!( + SubtensorModule::do_register_network(RuntimeOrigin::signed(cold), &hot, 1, None,), + Error::::CannotAffordLockCost + ); + + assert!(!SubtensorModule::subnet_exists(would_be_netuid)); + assert_eq!(TotalNetworks::::get(), 0); + assert_eq!( + SubnetAlphaIn::::get(would_be_netuid), + AlphaBalance::ZERO + ); + assert_eq!( + SubnetAlphaOut::::get(would_be_netuid), + AlphaBalance::ZERO + ); + assert_eq!( + SubtensorModule::get_subnet_locked_balance(would_be_netuid), + TaoBalance::ZERO + ); + assert_eq!( + RAORecycledForRegistration::::get(would_be_netuid), + TaoBalance::ZERO + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hot, + &cold, + would_be_netuid, + ), + AlphaBalance::ZERO + ); + }); +} + +#[test] +fn register_network_non_associated_hotkey_does_not_withdraw_or_write_owner_alpha_state() { + new_test_ext(0).execute_with(|| { + let original_cold = U256::from(3001); + let shared_hot = U256::from(3002); + let existing_netuid = add_dynamic_network(&shared_hot, &original_cold); + + let original_stake_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &shared_hot, + &original_cold, + existing_netuid, + ); + let original_alpha_out_before = SubnetAlphaOut::::get(existing_netuid); + + let attacker_cold = U256::from(3003); + let would_be_netuid = SubtensorModule::get_next_netuid(); + let lock_cost_u64: u64 = SubtensorModule::get_network_lock_cost().into(); + + add_balance_to_coldkey_account(&attacker_cold, lock_cost_u64.into()); + let attacker_balance_before = SubtensorModule::get_coldkey_balance(&attacker_cold); + + assert_err!( + SubtensorModule::do_register_network( + RuntimeOrigin::signed(attacker_cold), + &shared_hot, + 1, + None, + ), + Error::::NonAssociatedColdKey + ); + + // Attacker was not charged. + assert_eq!( + SubtensorModule::get_coldkey_balance(&attacker_cold), + attacker_balance_before + ); + + // Existing owner state is untouched. + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &shared_hot, + &original_cold, + existing_netuid, + ), + original_stake_before + ); + assert_eq!( + SubnetAlphaOut::::get(existing_netuid), + original_alpha_out_before + ); + assert_eq!(SubnetOwner::::get(existing_netuid), original_cold); + + // No new subnet / owner-alpha state was written. + assert!(!SubtensorModule::subnet_exists(would_be_netuid)); + assert_eq!(TotalNetworks::::get(), 1); + assert_eq!( + SubnetAlphaOut::::get(would_be_netuid), + AlphaBalance::ZERO + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &shared_hot, + &attacker_cold, + would_be_netuid, + ), + AlphaBalance::ZERO + ); + }); +} diff --git a/pallets/subtensor/src/tests/networks/registered_subnet_counter.rs b/pallets/subtensor/src/tests/networks/registered_subnet_counter.rs new file mode 100644 index 0000000000..1dea08c6f4 --- /dev/null +++ b/pallets/subtensor/src/tests/networks/registered_subnet_counter.rs @@ -0,0 +1,72 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! `RegisteredSubnetCounter` bumps across dissolve / re-registration. + +use super::prelude::*; + +#[test] +fn registered_subnet_counter_bumps_on_first_registration() { + new_test_ext(1).execute_with(|| { + let cold = U256::from(1); + let hot = U256::from(2); + + let netuid = add_dynamic_network(&hot, &cold); + + assert_eq!( + SubtensorModule::get_registered_subnet_counter(netuid), + 1, + "first registration of a netuid must leave counter == 1" + ); + }); +} + +#[test] +fn registered_subnet_counter_is_independent_per_netuid() { + new_test_ext(1).execute_with(|| { + let n1 = add_dynamic_network(&U256::from(10), &U256::from(11)); + let n2 = add_dynamic_network(&U256::from(20), &U256::from(21)); + + assert_ne!(n1, n2); + assert_eq!(SubtensorModule::get_registered_subnet_counter(n1), 1); + assert_eq!(SubtensorModule::get_registered_subnet_counter(n2), 1); + }); +} + +#[test] +fn registered_subnet_counter_survives_dissolve_and_bumps_on_reregistration() { + new_test_ext(1).execute_with(|| { + // Force reuse of the same netuid on re-registration by pinning the + // active subnet cap so the next registration must prune. + SubtensorModule::set_max_subnets(2); + + let owner_cold = U256::from(100); + let owner_hot = U256::from(101); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + assert_eq!(SubtensorModule::get_registered_subnet_counter(netuid), 1); + + // Dissolve: counter is intentionally *not* cleared — stale consumers + // can still detect the pre-dereg lifetime if they stored the counter + // value they observed at approval time. + assert_ok!(SubtensorModule::do_dissolve_network(netuid)); + run_block_idle(); + assert!(!SubtensorModule::subnet_exists(netuid)); + assert_eq!( + SubtensorModule::get_registered_subnet_counter(netuid), + 1, + "dissolve must not clear or reset the counter" + ); + + // Re-register. With the cap pinned, the prune selector reuses the + // freed netuid; the counter bumps to 2 so that any state still keyed + // to the prior value becomes unreachable under the new registration. + let reg_netuid = add_dynamic_network(&owner_hot, &owner_cold); + assert_eq!( + reg_netuid, netuid, + "the pruned netuid should be reused under the subnet cap" + ); + assert_eq!( + SubtensorModule::get_registered_subnet_counter(netuid), + 2, + "re-registration must bump counter" + ); + }); +} diff --git a/pallets/subtensor/src/tests/networks/set_new_network_state.rs b/pallets/subtensor/src/tests/networks/set_new_network_state.rs new file mode 100644 index 0000000000..b3b35303c8 --- /dev/null +++ b/pallets/subtensor/src/tests/networks/set_new_network_state.rs @@ -0,0 +1,224 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! `set_new_network_state` pool seeding, identity, limit, and fund-locked paths. + +use super::prelude::*; + +#[test] +fn set_new_network_state_registers_subnet_with_expected_state() { + new_test_ext(1).execute_with(|| { + let cold = U256::from(9001); + let hot = U256::from(9002); + let lock_amount = SubtensorModule::get_network_lock_cost(); + add_balance_to_coldkey_account(&cold, lock_amount.saturating_mul(2.into()).into()); + TotalIssuance::::mutate(|total| *total = total.saturating_add(lock_amount)); + + let median_price = SubtensorModule::get_median_subnet_alpha_price(); + let netuid = SubtensorModule::get_next_netuid(); + + assert_ok!(SubtensorModule::set_new_network_state( + &cold, + &hot, + 1, + None, + lock_amount, + median_price, + None, + )); + + assert!(SubtensorModule::subnet_exists(netuid)); + assert_eq!(SubnetOwner::::get(netuid), cold); + assert_eq!(SubnetMechanism::::get(netuid), 1); + assert_eq!(SubnetLocked::::get(netuid), lock_amount); + assert_eq!( + SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hot), + Ok(0) + ); + }); +} + +#[test] +fn set_new_network_state_fails_when_subnet_limit_reached() { + new_test_ext(1).execute_with(|| { + SubnetLimit::::put(1u16); + let _n1 = add_dynamic_network(&U256::from(10_002), &U256::from(10_001)); + + let cold = U256::from(10_011); + let hot = U256::from(10_012); + let lock_amount = SubtensorModule::get_network_lock_cost(); + add_balance_to_coldkey_account(&cold, lock_amount.saturating_mul(2.into()).into()); + + assert_err!( + SubtensorModule::set_new_network_state( + &cold, + &hot, + 1, + None, + lock_amount, + SubtensorModule::get_median_subnet_alpha_price(), + None, + ), + Error::::SubnetLimitReached + ); + + // No partial state was written. + assert_eq!(TotalNetworks::::get(), 1); + assert!(!SubtensorModule::hotkey_account_exists(&hot)); + }); +} + +#[test] +fn set_new_network_state_stores_identity_and_emits_events() { + new_test_ext(1).execute_with(|| { + let cold = U256::from(10_101); + let hot = U256::from(10_102); + let lock_amount = SubtensorModule::get_network_lock_cost(); + add_balance_to_coldkey_account(&cold, lock_amount.saturating_mul(2.into()).into()); + + let identity = SubnetIdentityOfV3 { + subnet_name: b"my subnet".to_vec(), + github_repo: b"https://github.com/example/repo".to_vec(), + subnet_contact: b"contact@example.com".to_vec(), + subnet_url: b"https://example.com".to_vec(), + discord: b"discord".to_vec(), + description: b"description".to_vec(), + logo_url: b"https://example.com/logo.png".to_vec(), + additional: b"".to_vec(), + }; + + let netuid = SubtensorModule::get_next_netuid(); + System::reset_events(); + + assert_ok!(SubtensorModule::set_new_network_state( + &cold, + &hot, + 1, + Some(identity.clone()), + lock_amount, + SubtensorModule::get_median_subnet_alpha_price(), + None, + )); + + assert_eq!(SubnetIdentitiesV3::::get(netuid), Some(identity)); + let events = System::events(); + assert!(events.iter().any(|e| matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::SubnetIdentitySet(n)) if *n == netuid + ))); + assert!(events.iter().any(|e| matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::NetworkAdded(n, m)) if *n == netuid && *m == 1 + ))); + }); +} + +#[test] +fn set_new_network_state_uses_provided_median_price_for_pool_alpha() { + new_test_ext(1).execute_with(|| { + let cold = U256::from(10_201); + let hot = U256::from(10_202); + + // Lock twice the min lock so the pool is seeded from the actual lock amount. + let min_lock = SubtensorModule::get_network_min_lock(); + let lock_amount = min_lock.saturating_mul(2.into()); + add_balance_to_coldkey_account(&cold, lock_amount.saturating_mul(2.into()).into()); + + let netuid = SubtensorModule::get_next_netuid(); + let price = U64F64::from_num(2); + + assert_ok!(SubtensorModule::set_new_network_state( + &cold, + &hot, + 1, + None, + lock_amount, + price, + None, + )); + + // Pool TAO equals the actual lock; alpha reserve is tao / price. + assert_eq!(SubnetTAO::::get(netuid), lock_amount); + let expected_alpha: u64 = u64::from(lock_amount) / 2; + assert_eq!( + SubnetAlphaIn::::get(netuid), + AlphaBalance::from(expected_alpha) + ); + }); +} + +#[test] +fn set_new_network_state_seeds_pool_with_min_lock_floor() { + new_test_ext(1).execute_with(|| { + let cold = U256::from(10_301); + let hot = U256::from(10_302); + add_balance_to_coldkey_account(&cold, 1_000_000_000.into()); + + let netuid = SubtensorModule::get_next_netuid(); + let min_lock = SubtensorModule::get_network_min_lock(); + + // Zero lock: the pool must still be seeded with the min lock floor. + assert_ok!(SubtensorModule::set_new_network_state( + &cold, + &hot, + 1, + None, + TaoBalance::ZERO, + U64F64::from_num(1), + None, + )); + + assert_eq!(SubnetTAO::::get(netuid), min_lock); + assert_eq!( + SubnetAlphaIn::::get(netuid), + AlphaBalance::from(u64::from(min_lock)) + ); + assert_eq!(SubnetLocked::::get(netuid), TaoBalance::ZERO); + }); +} + +#[test] +fn set_new_network_state_fund_locked_releases_balance_lock() { + new_test_ext(1).execute_with(|| { + let cold = U256::from(10_401); + let hot = U256::from(10_402); + let lock_amount = SubtensorModule::get_network_lock_cost(); + add_balance_to_coldkey_account(&cold, lock_amount.saturating_mul(2.into()).into()); + + let lock_id = NetworkRegistrationLockId::::get(); + let mut identifier = [0u8; 8]; + identifier[..4].copy_from_slice(b"rglk"); + identifier[4..8].copy_from_slice(&lock_id.to_le_bytes()); + + assert_ok!(SubtensorModule::lock_network_registration_cost( + &cold, + lock_amount.into(), + 0 + )); + assert!( + pallet_balances::Locks::::get(cold) + .iter() + .any(|l| l.id == identifier), + "registration lock must exist before processing" + ); + + let netuid = SubtensorModule::get_next_netuid(); + + assert_ok!(SubtensorModule::set_new_network_state( + &cold, + &hot, + 1, + None, + lock_amount, + SubtensorModule::get_median_subnet_alpha_price(), + Some(lock_id), + )); + + assert!( + pallet_balances::Locks::::get(cold) + .iter() + .all(|l| l.id != identifier), + "registration lock must be released after processing" + ); + assert!(SubtensorModule::subnet_exists(netuid)); + assert_eq!(SubnetLocked::::get(netuid), lock_amount); + }); +} diff --git a/pallets/subtensor/src/tests/networks/tempo_rate_limit.rs b/pallets/subtensor/src/tests/networks/tempo_rate_limit.rs new file mode 100644 index 0000000000..6adb3a5978 --- /dev/null +++ b/pallets/subtensor/src/tests/networks/tempo_rate_limit.rs @@ -0,0 +1,21 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! Tempo must exceed weight-set rate limit when registering a network. + +use super::prelude::*; + +#[test] +fn test_tempo_greater_than_weight_set_rate_limit() { + new_test_ext(1).execute_with(|| { + let subnet_owner_hotkey = U256::from(1); + let subnet_owner_coldkey = U256::from(2); + + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + // Get tempo + let tempo = SubtensorModule::get_tempo(netuid); + + let weights_set_rate_limit = SubtensorModule::get_weights_set_rate_limit(netuid); + + assert!(tempo as u64 >= weights_set_rate_limit); + }) +} diff --git a/pallets/subtensor/src/tests/neuron_info.rs b/pallets/subtensor/src/tests/neuron_info.rs index a954ef6e26..4234ed5bdc 100644 --- a/pallets/subtensor/src/tests/neuron_info.rs +++ b/pallets/subtensor/src/tests/neuron_info.rs @@ -1,3 +1,7 @@ +//! Tests for RPC neuron-info getters ([`crate::rpc_info`]). +//! +//! Covers `get_neuron` / `get_neurons` empty and populated cases. + use super::mock::*; use sp_core::U256; diff --git a/pallets/subtensor/src/tests/recycle_alpha.rs b/pallets/subtensor/src/tests/recycle_alpha.rs index 3da1112972..28b320c6d1 100644 --- a/pallets/subtensor/src/tests/recycle_alpha.rs +++ b/pallets/subtensor/src/tests/recycle_alpha.rs @@ -1,3 +1,7 @@ +//! Tests for recycling alpha into a subnet ([`crate::staking::recycle_alpha`]). +//! +//! Covers recycle amount bounds, ownership, and reserve accounting. + use super::mock; use super::mock::*; use crate::*; @@ -26,7 +30,7 @@ fn test_recycle_success() { let _ = SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); register_ok_neuron(netuid, hotkey, coldkey, 0); - assert!(SubtensorModule::if_subnet_exist(netuid)); + assert!(SubtensorModule::subnet_exists(netuid)); // add stake to coldkey-hotkey pair so we can recycle it let stake = 200_000; @@ -82,7 +86,7 @@ fn test_recycle_two_stakers() { let _ = SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); register_ok_neuron(netuid, hotkey, coldkey, 0); - assert!(SubtensorModule::if_subnet_exist(netuid)); + assert!(SubtensorModule::subnet_exists(netuid)); // add stake to coldkey-hotkey pair so we can recycle it let stake = 200_000; @@ -152,7 +156,7 @@ fn test_recycle_staker_is_nominator() { let _ = SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); register_ok_neuron(netuid, hotkey, coldkey, 0); - assert!(SubtensorModule::if_subnet_exist(netuid)); + assert!(SubtensorModule::subnet_exists(netuid)); // add stake to coldkey-hotkey pair so we can recycle it let stake = 200_000; @@ -225,7 +229,7 @@ fn test_burn_success() { let _ = SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); register_ok_neuron(netuid, hotkey, coldkey, 0); - assert!(SubtensorModule::if_subnet_exist(netuid)); + assert!(SubtensorModule::subnet_exists(netuid)); // add stake to coldkey-hotkey pair so we can recycle it let stake = 200_000; @@ -281,7 +285,7 @@ fn test_burn_staker_is_nominator() { let _ = SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); register_ok_neuron(netuid, hotkey, coldkey, 0); - assert!(SubtensorModule::if_subnet_exist(netuid)); + assert!(SubtensorModule::subnet_exists(netuid)); // add stake to coldkey-hotkey pair so we can recycle it let stake = 200_000; @@ -351,7 +355,7 @@ fn test_burn_two_stakers() { let _ = SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); register_ok_neuron(netuid, hotkey, coldkey, 0); - assert!(SubtensorModule::if_subnet_exist(netuid)); + assert!(SubtensorModule::subnet_exists(netuid)); // add stake to coldkey-hotkey pair so we can recycle it let stake = 200_000; @@ -561,7 +565,7 @@ fn test_recycle_precision() { Balances::make_free_balance_be(&coldkey, 1_000_000_000.into()); // sanity check - assert!(SubtensorModule::if_subnet_exist(netuid)); + assert!(SubtensorModule::subnet_exists(netuid)); // add stake to coldkey-hotkey pair so we can recycle it increase_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake.into(), netuid); @@ -607,7 +611,7 @@ fn test_burn_precision() { Balances::make_free_balance_be(&coldkey, 1_000_000_000.into()); // sanity check - assert!(SubtensorModule::if_subnet_exist(netuid)); + assert!(SubtensorModule::subnet_exists(netuid)); // add stake to coldkey-hotkey pair so we can recycle it increase_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake.into(), netuid); diff --git a/pallets/subtensor/src/tests/registration.rs b/pallets/subtensor/src/tests/registration.rs index cd3d040bae..81bedd8e91 100644 --- a/pallets/subtensor/src/tests/registration.rs +++ b/pallets/subtensor/src/tests/registration.rs @@ -1,3 +1,7 @@ +//! Tests for neuron and burned registration ([`crate::subnets`] registration paths). +//! +//! Covers cost curves, immunity, rate limits, and root-neuron registration. + #![allow(clippy::unwrap_used)] use crate::*; @@ -1136,7 +1140,7 @@ fn test_update_registration_prices_for_networks_many_half_lives_over_thousands_o // Root subnet: only root gets RegistrationsThisInterval reset here. let root = NetUid::from(0); - if !SubtensorModule::if_subnet_exist(root) { + if !SubtensorModule::subnet_exists(root) { SubtensorModule::init_new_network(root, 2); } diff --git a/pallets/subtensor/src/tests/remove_data_tests.rs b/pallets/subtensor/src/tests/remove_data_tests.rs index 134be2e89f..b10daad389 100644 --- a/pallets/subtensor/src/tests/remove_data_tests.rs +++ b/pallets/subtensor/src/tests/remove_data_tests.rs @@ -1,3 +1,7 @@ +//! Tests for purging hotkey/coldkey associated storage on removal. +//! +//! Covers dissolve/cleanup data wipe for stake indexes, serves, and identities. + #![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] use super::mock::*; @@ -144,7 +148,7 @@ fn test_remove_data_for_dissolved_networks_all_phases() { ); // Verify the subnet no longer exists - assert!(!SubtensorModule::if_subnet_exist(netuid)); + assert!(!SubtensorModule::subnet_exists(netuid)); }); } @@ -403,7 +407,7 @@ fn test_remove_data_for_dissolved_networks_via_on_idle() { ); // Verify the subnet no longer exists - assert!(!SubtensorModule::if_subnet_exist(netuid)); + assert!(!SubtensorModule::subnet_exists(netuid)); // Verify data has been cleaned up assert_eq!(SubtensorModule::get_subnet_owner(netuid), U256::from(0)); @@ -819,7 +823,7 @@ fn test_clean_up_hotkey_swap_records() { )); // Call the function and get the returned weight - let returned_weight = SubtensorModule::clean_up_hotkey_swap_records(block_number.into()); + let returned_weight = SubtensorModule::purge_expired_hotkey_swap_on_netuid_records(block_number.into()); // After the function call, for netuid_1: // - The old record (coldkey_old, swap_block_old) should be removed because swap_block_old + interval < block_number diff --git a/pallets/subtensor/src/tests/serving.rs b/pallets/subtensor/src/tests/serving.rs index 373ca98c99..8fc5d0a066 100644 --- a/pallets/subtensor/src/tests/serving.rs +++ b/pallets/subtensor/src/tests/serving.rs @@ -1,4 +1,9 @@ +//! Tests for axon / prometheus serving and identity ([`crate::guards::check_serving_endpoints`]). +//! +//! Covers IP validation, rate limits, TLS metadata, and coldkey/subnet identity. + #![allow(clippy::expect_used, clippy::unwrap_used)] + use super::mock::*; use crate::Error; diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs deleted file mode 100644 index 19af1e22b7..0000000000 --- a/pallets/subtensor/src/tests/staking.rs +++ /dev/null @@ -1,6006 +0,0 @@ -#![allow(clippy::unwrap_used)] -#![allow(clippy::arithmetic_side_effects)] - -use approx::assert_abs_diff_eq; -use frame_support::dispatch::{DispatchClass, GetDispatchInfo, Pays}; -use frame_support::sp_runtime::DispatchError; -use frame_support::{assert_err, assert_noop, assert_ok, traits::Currency}; -use frame_system::RawOrigin; -use safe_math::FixedExt; -use share_pool::SafeFloat; -use sp_core::{Get, H256, U256}; -use sp_runtime::PerU16; -use substrate_fixed::traits::FromFixed; -use substrate_fixed::types::{I96F32, I110F18, U64F64, U96F32}; -use subtensor_runtime_common::{AlphaBalance, NetUid, NetUidStorageIndex, TaoBalance, Token}; -use subtensor_swap_interface::{Order, SwapHandler}; - -use super::mock; -use super::mock::*; -use crate::*; - -/*********************************************************** - staking::add_stake() tests -************************************************************/ - -#[test] -fn test_delegate_take_dispatch_info_pays_fee() { - new_test_ext(1).execute_with(|| { - let hotkey = U256::from(1); - let take = PerU16::from_parts(SubtensorModule::get_min_delegate_take()); - - let decrease_take_call = - RuntimeCall::SubtensorModule(SubtensorCall::decrease_take { hotkey, take }); - let decrease_take_dispatch_info = decrease_take_call.get_dispatch_info(); - assert_eq!(decrease_take_dispatch_info.class, DispatchClass::Normal); - assert_eq!(decrease_take_dispatch_info.pays_fee, Pays::Yes); - - let increase_take_call = - RuntimeCall::SubtensorModule(SubtensorCall::increase_take { hotkey, take }); - let increase_take_dispatch_info = increase_take_call.get_dispatch_info(); - assert_eq!(increase_take_dispatch_info.class, DispatchClass::Normal); - assert_eq!(increase_take_dispatch_info.pays_fee, Pays::Yes); - }); -} - -#[test] -fn test_add_stake_dispatch_info_ok() { - new_test_ext(1).execute_with(|| { - let hotkey = U256::from(0); - let amount_staked = TaoBalance::from(5000); - let netuid = NetUid::from(1); - let call = RuntimeCall::SubtensorModule(SubtensorCall::add_stake { - hotkey, - netuid, - amount_staked, - }); - let di = call.get_dispatch_info(); - assert_eq!(di.extension_weight, frame_support::weights::Weight::zero(),); - assert_eq!(di.class, DispatchClass::Normal,); - assert_eq!(di.pays_fee, Pays::Yes,); - }); -} -#[test] -fn test_add_stake_ok_no_emission() { - new_test_ext(1).execute_with(|| { - let hotkey_account_id = U256::from(533453); - let coldkey_account_id = U256::from(55453); - let amount = DefaultMinStake::::get().to_u64() * 10; - - //add network - let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); - remove_owner_registration_stake(netuid); - - mock::setup_reserves( - netuid, - (amount * 1_000_000).into(), - (amount * 10_000_000).into(), - ); - - // Give it some $$$ in his coldkey balance - add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); - - // Check we have zero staked before transfer - assert_eq!( - SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), - TaoBalance::ZERO - ); - - // Also total stake should be equal to the network initial lock - assert_eq!( - SubtensorModule::get_total_stake(), - SubtensorModule::get_network_min_lock() - ); - - // Transfer to hotkey account, and check if the result is ok - let (alpha_staked, fee) = mock::swap_tao_to_alpha(netuid, amount.into()); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - amount.into() - )); - - let (tao_expected, _) = mock::swap_alpha_to_tao(netuid, alpha_staked); - let approx_fee = ::SwapInterface::approx_fee_amount( - netuid.into(), - TaoBalance::from(amount), - ); - - assert_abs_diff_eq!( - SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), - tao_expected + approx_fee, // swap returns value after fee, so we need to compensate it - epsilon = 10000.into(), - ); - - // Check if stake has increased - assert_abs_diff_eq!( - SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), - (amount - fee).into(), - epsilon = 10000.into() - ); - - // Check if balance has decreased - assert_eq!( - SubtensorModule::get_coldkey_balance(&coldkey_account_id), - 1.into() - ); - - // Check if total stake has increased accordingly. - assert_eq!( - SubtensorModule::get_total_stake(), - SubtensorModule::get_network_min_lock() + amount.into() - ); - }); -} - -#[test] -fn test_dividends_with_run_to_block() { - new_test_ext(1).execute_with(|| { - let neuron_src_hotkey_id = U256::from(1); - let neuron_dest_hotkey_id = U256::from(2); - let coldkey_account_id = U256::from(667); - let hotkey_account_id = U256::from(668); - let initial_stake: u64 = 5000; - - // add network - let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); - Tempo::::insert(netuid, 13); - - // Register neuron(s) - SubtensorModule::set_max_registrations_per_block(netuid, 3); - SubtensorModule::set_max_allowed_uids(1.into(), 5); - - register_ok_neuron(netuid, neuron_src_hotkey_id, coldkey_account_id, 192213123); - register_ok_neuron(netuid, neuron_dest_hotkey_id, coldkey_account_id, 12323); - - // Add some stake to src in ALPHA units. - let src_alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &neuron_src_hotkey_id, - &coldkey_account_id, - netuid, - ); - - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &neuron_src_hotkey_id, - &coldkey_account_id, - netuid, - AlphaBalance::from(initial_stake), - ); - - let src_alpha_after_add = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &neuron_src_hotkey_id, - &coldkey_account_id, - netuid, - ); - - assert_eq!( - src_alpha_after_add, - src_alpha_before + AlphaBalance::from(initial_stake), - "Src alpha stake did not increase correctly" - ); - - // Check if all three neurons are registered (dynamic subnet owner + 2 registrations). - assert_eq!(SubtensorModule::get_subnetwork_n(netuid), 3); - - // Run a couple of blocks (may change prices / emission, but shouldn't move stake away). - run_to_block(2); - - // Re-check ALPHA stake (not TAO value). - let src_alpha_after_blocks = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &neuron_src_hotkey_id, - &coldkey_account_id, - netuid, - ); - let dest_alpha_after_blocks = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &neuron_dest_hotkey_id, - &coldkey_account_id, - netuid, - ); - - // Src stake should not decrease; dest stake should still be zero (no stake transfer/dividends). - assert!( - src_alpha_after_blocks >= src_alpha_after_add, - "Src alpha stake unexpectedly decreased" - ); - assert!( - dest_alpha_after_blocks.is_zero(), - "Dest alpha stake unexpectedly increased" - ); - }); -} - -#[test] -fn test_add_stake_err_signature() { - new_test_ext(1).execute_with(|| { - let hotkey_account_id = U256::from(654); // bogus - let amount = 20000; // Not used - let netuid = NetUid::from(1); - - assert_err!( - SubtensorModule::add_stake( - RawOrigin::None.into(), - hotkey_account_id, - netuid, - amount.into() - ), - DispatchError::BadOrigin - ); - }); -} - -#[test] -fn test_add_stake_not_registered_key_pair() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1); - let subnet_owner_hotkey = U256::from(2); - let coldkey_account_id = U256::from(435445); - let hotkey_account_id = U256::from(54544); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - let amount = DefaultMinStake::::get().to_u64() * 10; - add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); - assert_err!( - SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - amount.into() - ), - Error::::HotKeyAccountNotExists - ); - }); -} - -#[test] -fn test_add_stake_ok_neuron_does_not_belong_to_coldkey() { - new_test_ext(1).execute_with(|| { - let coldkey_id = U256::from(544); - let hotkey_id = U256::from(54544); - let other_cold_key = U256::from(99498); - let netuid = add_dynamic_network(&hotkey_id, &coldkey_id); - let stake = DefaultMinStake::::get() * 10.into(); - - // Give it some $$$ in his coldkey balance - add_balance_to_coldkey_account(&other_cold_key, stake.into()); - - // Perform the request which is signed by a different cold key - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(other_cold_key), - hotkey_id, - netuid, - stake, - )); - }); -} - -#[test] -fn test_add_stake_err_not_enough_belance() { - new_test_ext(1).execute_with(|| { - let coldkey_id = U256::from(544); - let hotkey_id = U256::from(54544); - let stake = DefaultMinStake::::get() * 10.into(); - let netuid = add_dynamic_network(&hotkey_id, &coldkey_id); - - // Lets try to stake with 0 balance in cold key account - assert!(SubtensorModule::get_coldkey_balance(&coldkey_id) < stake); - assert_err!( - SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey_id), - hotkey_id, - netuid, - stake, - ), - Error::::NotEnoughBalanceToStake - ); - }); -} - -#[test] -#[ignore] -fn test_add_stake_total_issuance_no_change() { - // When we add stake, the total issuance of the balances pallet should not change - // this is because the stake should be part of the coldkey account balance (reserved/locked) - new_test_ext(1).execute_with(|| { - let hotkey_account_id = U256::from(561337); - let coldkey_account_id = U256::from(61337); - let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); - - // Give it some $$$ in his coldkey balance - let initial_balance = 10000; - add_balance_to_coldkey_account(&coldkey_account_id, initial_balance.into()); - - // Check we have zero staked before transfer - let initial_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id); - assert_eq!(initial_stake, TaoBalance::ZERO); - - // Check total balance is equal to initial balance - let initial_total_balance = Balances::total_balance(&coldkey_account_id); - assert_eq!(initial_total_balance, initial_balance.into()); - - // Check total issuance is equal to initial balance - let initial_total_issuance = Balances::total_issuance(); - assert_eq!(initial_total_issuance, initial_balance.into()); - - // Also total stake should be zero - assert_eq!(SubtensorModule::get_total_stake(), TaoBalance::ZERO); - - // Stake to hotkey account, and check if the result is ok - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - 10000.into() - )); - - // Check if stake has increased - let new_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id); - assert_eq!(new_stake, 10000.into()); - - // Check if free balance has decreased - let new_free_balance = SubtensorModule::get_coldkey_balance(&coldkey_account_id); - assert_eq!(new_free_balance, 0.into()); - - // Check if total stake has increased accordingly. - assert_eq!(SubtensorModule::get_total_stake(), 10000.into()); - - // Check if total issuance has remained the same. (no fee, includes reserved/locked balance) - let total_issuance = Balances::total_issuance(); - assert_eq!(total_issuance, initial_total_issuance); - }); -} - -#[test] -fn test_remove_stake_ok_no_emission() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1); - let subnet_owner_hotkey = U256::from(2); - let coldkey_account_id = U256::from(4343); - let hotkey_account_id = U256::from(4968585); - let amount = DefaultMinStake::::get() * 10.into(); - - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); - - // Clear any implicit existing stake so we can fully remove exactly `amount` - let existing = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - ); - if !existing.is_zero() { - SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - existing, - ); - } - - // Create stake without relying on any emission/weights assumptions - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - amount.to_u64().into(), - ); - - let expected_stake: AlphaBalance = amount.to_u64().into(); - let epsilon_stake: AlphaBalance = (amount.to_u64() / 1000).into(); - - assert_abs_diff_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid - ), - expected_stake, - epsilon = epsilon_stake - ); - - // Snapshot baselines before we top up SubnetTAO / TotalStake - let base_total_stake = SubtensorModule::get_total_stake(); - let balance_before = SubtensorModule::get_coldkey_balance(&coldkey_account_id); - - // Add subnet TAO so remove_stake can pay out (keep original pattern) - let (amount_tao, fee) = mock::swap_alpha_to_tao(netuid, amount.to_u64().into()); - SubnetTAO::::mutate(netuid, |v| *v += amount_tao + fee.into()); - TotalStake::::mutate(|v| *v += amount_tao + fee.into()); - - // Do the magic - assert_ok!(SubtensorModule::remove_stake( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - amount.to_u64().into() - )); - - // we do not expect the exact amount due to slippage, but it must increase meaningfully - let balance_after = SubtensorModule::get_coldkey_balance(&coldkey_account_id); - assert!(balance_after > balance_before); - assert!( - (balance_after - balance_before) > amount / 10.into() * 9.into() - fee.into(), - "Payout lower than expected lower bound" - ); - - // All stake removed - assert!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid - ) - .is_zero() - ); - - // Total stake should net-increase only by fee (everything else returned) - assert_abs_diff_eq!( - SubtensorModule::get_total_stake(), - base_total_stake + fee.into(), - epsilon = SubtensorModule::get_total_stake() / 100_000.into() - ); - }); -} - -#[test] -fn test_remove_stake_amount_too_low() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1); - let subnet_owner_hotkey = U256::from(2); - let coldkey_account_id = U256::from(4343); - let hotkey_account_id = U256::from(4968585); - let amount: u64 = 10_000; - - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); - - // Ensure deterministic starting stake for this (hotkey,coldkey,netuid) - let existing = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - ); - if !existing.is_zero() { - SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - existing, - ); - } - - // Give the neuron some stake to remove - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - amount.into(), - ); - - // Removing zero should fail - assert_noop!( - SubtensorModule::remove_stake( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - AlphaBalance::ZERO - ), - Error::::AmountTooLow - ); - }); -} - -#[test] -fn test_remove_stake_below_min_stake() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1); - let subnet_owner_hotkey = U256::from(2); - let coldkey_account_id = U256::from(4343); - let hotkey_account_id = U256::from(4968585); - - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); - - // Clear any implicit existing stake so the test always starts below-min - let existing = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - ); - if !existing.is_zero() { - SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - existing, - ); - } - - let min_stake = DefaultMinStake::::get(); - let amount = AlphaBalance::from(min_stake.to_u64() / 2); - - // Give the neuron some *below-min* stake to remove - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - amount, - ); - - // Unstake less than full stake -> leaves a non-zero remainder below min -> errors - assert_noop!( - SubtensorModule::remove_stake( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - amount - 1.into() - ), - Error::::AmountTooLow - ); - - // Unstaking full stake - works - assert_ok!(SubtensorModule::remove_stake( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - amount - )); - assert!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - ) - .is_zero() - ); - }); -} - -#[test] -fn test_add_stake_partial_below_min_stake_fails() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1); - let subnet_owner_hotkey = U256::from(2); - let coldkey_account_id = U256::from(4343); - let hotkey_account_id = U256::from(4968585); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); - - // Stake TAO amount is above min stake - let min_stake = DefaultMinStake::::get(); - let amount = min_stake.to_u64() * 2; - add_balance_to_coldkey_account( - &coldkey_account_id, - TaoBalance::from(amount) + ExistentialDeposit::get(), - ); - - // Setup reserves - mock::setup_reserves(netuid, (amount * 10).into(), (amount * 10).into()); - - // Force the swap to initialize - ::SwapInterface::init_swap(netuid, None); - - // Get the current price - let current_price = - ::SwapInterface::current_alpha_price(netuid.into()); - assert!(current_price.to_num::() > 0.0); - - // Set "max spend" to ~1 TAO around current price - let current_price_scaled = (current_price.to_num::() * 1_000_000_000_f64) as u64; - let max_spend = current_price_scaled.saturating_add(1); - - // Add stake with partial flag on - assert_err!( - SubtensorModule::add_stake_limit( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - amount.into(), - max_spend.into(), - true - ), - Error::::AmountTooLow - ); - - // Price should be unchanged on failure - let new_current_price = - ::SwapInterface::current_alpha_price(netuid.into()); - assert_eq!(new_current_price, current_price); - }); -} - -#[test] -fn test_remove_stake_err_signature() { - new_test_ext(1).execute_with(|| { - let hotkey_account_id = U256::from(4968585); - let amount = AlphaBalance::from(10000); // Amount to be removed - let netuid = NetUid::from(1); - - assert_err!( - SubtensorModule::remove_stake( - RawOrigin::None.into(), - hotkey_account_id, - netuid, - amount, - ), - DispatchError::BadOrigin - ); - }); -} - -#[test] -fn test_remove_stake_ok_hotkey_does_not_belong_to_coldkey() { - new_test_ext(1).execute_with(|| { - let coldkey_id = U256::from(544); - let hotkey_id = U256::from(54544); - let other_cold_key = U256::from(99498); - let amount = DefaultMinStake::::get().to_u64() * 10; - let netuid = add_dynamic_network(&hotkey_id, &coldkey_id); - - // Give the neuron some stake to remove - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_id, - &other_cold_key, - netuid, - amount.into(), - ); - - assert_ok!(SubtensorModule::remove_stake( - RuntimeOrigin::signed(other_cold_key), - hotkey_id, - netuid, - amount.into(), - )); - }); -} - -#[test] -fn test_remove_stake_no_enough_stake() { - new_test_ext(1).execute_with(|| { - let coldkey_id = U256::from(544); - let hotkey_id = U256::from(54544); - let amount = DefaultMinStake::::get().to_u64() * 10; - let netuid = add_dynamic_network(&hotkey_id, &coldkey_id); - remove_owner_registration_stake(netuid); - - assert_eq!( - SubtensorModule::get_total_stake_for_hotkey(&hotkey_id), - TaoBalance::ZERO - ); - - assert_err!( - SubtensorModule::remove_stake( - RuntimeOrigin::signed(coldkey_id), - hotkey_id, - netuid, - amount.into(), - ), - Error::::AmountTooLow - ); - }); -} - -#[test] -fn test_remove_stake_total_balance_no_change() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1); - let subnet_owner_hotkey = U256::from(2); - let hotkey_account_id = U256::from(571337); - let coldkey_account_id = U256::from(71337); - let amount: u64 = DefaultMinStake::::get().to_u64() * 10; - - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); - - // Set fee rate to 0 so that alpha fee is not moved to block producer - pallet_subtensor_swap::FeeRate::::insert(netuid, 0); - let fee: u64 = 0; - - // Clear any implicit existing stake so the test is deterministic - let existing = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - ); - if !existing.is_zero() { - SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - existing, - ); - } - - let balance_before = SubtensorModule::get_coldkey_balance(&coldkey_account_id); - let total_balance_before = Balances::total_balance(&coldkey_account_id); - let base_total_stake = SubtensorModule::get_total_stake(); - - // Give the neuron some stake to remove - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - amount.into(), - ); - - // Add subnet TAO for the equivalent amount added at price - let amount_tao = U96F32::from_num(amount) - * U96F32::from_num( - ::SwapInterface::current_alpha_price(netuid.into()), - ); - let amount_tao: TaoBalance = amount_tao.to_num::().into(); - SubnetTAO::::mutate(netuid, |v| *v += amount_tao); - TotalStake::::mutate(|v| *v += amount_tao); - - // Remove stake - assert_ok!(SubtensorModule::remove_stake( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - amount.into() - )); - - let balance_after = SubtensorModule::get_coldkey_balance(&coldkey_account_id); - let total_balance_after = Balances::total_balance(&coldkey_account_id); - - // Free balance should increase by roughly the TAO paid out (net of swap mechanics) - assert!(balance_after > balance_before); - assert!( - (balance_after - balance_before) > amount_tao / 10.into() * 9.into() - fee.into(), - "Payout lower than expected lower bound" - ); - - // Total balance should track the same change (since stake becomes free) - assert!(total_balance_after > total_balance_before); - - // Total stake should net-increase only by fee - assert_abs_diff_eq!( - SubtensorModule::get_total_stake(), - base_total_stake + fee.into(), - epsilon = SubtensorModule::get_total_stake() / 10_000_000.into() - ); - - assert_abs_diff_eq!( - total_balance_after - total_balance_before, - amount_tao - fee.into(), - epsilon = TaoBalance::from(amount) / 1000.into() - ); - }); -} - -#[test] -fn test_add_stake_insufficient_liquidity() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let hotkey = U256::from(2); - let coldkey = U256::from(3); - let amount_staked = DefaultMinStake::::get().to_u64() * 10; - - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - let _ = SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); - add_balance_to_coldkey_account(&coldkey, amount_staked.into()); - - // Set the liquidity at lowest possible value so that all staking requests fail - let reserve = u64::from(mock::SwapMinimumReserve::get()) - 1; - mock::setup_reserves(netuid, reserve.into(), reserve.into()); - - // Check the error - assert_noop!( - SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - amount_staked.into() - ), - Error::::InsufficientLiquidity - ); - }); -} - -/// cargo test --package pallet-subtensor --lib -- tests::staking::test_add_stake_input_reserve_too_low_fails --exact --show-output -#[test] -fn test_add_stake_input_reserve_too_low_fails() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let hotkey = U256::from(2); - let coldkey = U256::from(3); - let amount_staked = DefaultMinStake::::get().to_u64() * 10; - - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - let _ = SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); - add_balance_to_coldkey_account(&coldkey, amount_staked.into()); - - // Set the liquidity at lowest possible value so that all staking requests fail - let reserve_alpha = 1_000_000_000_u64; - let reserve_tao = u64::from(mock::SwapMinimumReserve::get()) - 1; - mock::setup_reserves(netuid, reserve_tao.into(), reserve_alpha.into()); - - // The output-side reserve is sufficient, but the input-side reserve is too small for the - // requested swap under the 1000x input-reserve cap. - assert_noop!( - SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - amount_staked.into() - ), - Error::::InsufficientLiquidity - ); - }); -} - -/// cargo test --package pallet-subtensor --lib -- tests::staking::test_add_stake_insufficient_liquidity_one_side_fail --exact --show-output -#[test] -fn test_add_stake_insufficient_liquidity_one_side_fail() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let hotkey = U256::from(2); - let coldkey = U256::from(3); - let amount_staked = DefaultMinStake::::get().to_u64() * 10; - - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - let _ = SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); - add_balance_to_coldkey_account(&coldkey, amount_staked.into()); - - // Set the liquidity at lowest possible value so that all staking requests fail - let reserve_alpha = u64::from(mock::SwapMinimumReserve::get()) - 1; - let reserve_tao = u64::from(mock::SwapMinimumReserve::get()); - mock::setup_reserves(netuid, reserve_tao.into(), reserve_alpha.into()); - - // Check the error - assert_noop!( - SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - amount_staked.into() - ), - Error::::InsufficientLiquidity - ); - }); -} - -#[test] -fn test_remove_stake_insufficient_liquidity() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let hotkey = U256::from(2); - let coldkey = U256::from(3); - let amount_staked = DefaultMinStake::::get().to_u64() * 10; - - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - let _ = SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); - add_balance_to_coldkey_account(&coldkey, amount_staked.into()); - - // Simulate stake for hotkey - let reserve = u64::MAX / 1000; - mock::setup_reserves(netuid, reserve.into(), reserve.into()); - - let alpha = SubtensorModule::stake_into_subnet( - &hotkey, - &coldkey, - netuid, - amount_staked.into(), - ::SwapInterface::max_price(), - false, - ) - .unwrap(); - - // Set the liquidity at lowest possible value so that all staking requests fail - let reserve = u64::from(mock::SwapMinimumReserve::get()) - 1; - mock::setup_reserves(netuid, reserve.into(), reserve.into()); - - // Check the error - assert_noop!( - SubtensorModule::remove_stake(RuntimeOrigin::signed(coldkey), hotkey, netuid, alpha), - Error::::InsufficientLiquidity - ); - - // Mock more liquidity - remove becomes successful - SubnetTAO::::insert(netuid, TaoBalance::from(amount_staked + 1)); - SubnetAlphaIn::::insert(netuid, AlphaBalance::from(alpha.to_u64() / 1000 + 1)); - assert_ok!(SubtensorModule::remove_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - alpha - ),); - }); -} - -#[test] -fn test_remove_stake_total_issuance_no_change() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1); - let subnet_owner_hotkey = U256::from(2); - let hotkey_account_id = U256::from(581337); - let coldkey_account_id = U256::from(81337); - let amount: u64 = DefaultMinStake::::get().to_u64() * 10; - - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); - - // Set fee rate to 0 so that alpha fee is not moved to block producer - pallet_subtensor_swap::FeeRate::::insert(netuid, 0); - - // Ensure the coldkey has at least 'amount' more balance available for staking - add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); - - mock::setup_reserves(netuid, (amount * 100).into(), (amount * 100).into()); - - // Baselines (after registration + funding) - let balance_before_stake = SubtensorModule::get_coldkey_balance(&coldkey_account_id); - let issuance_before = Balances::total_issuance(); - let base_total_stake = SubtensorModule::get_total_stake(); - - // Stake exactly `amount` TAO - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - TaoBalance::from(amount), - )); - - let issuance_after_stake = Balances::total_issuance(); - - // Staking burns `amount` from balances issuance in this system design. - assert_abs_diff_eq!(issuance_before, issuance_after_stake, epsilon = 1.into()); - - // Remove all stake - let stake_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - ); - - assert_ok!(SubtensorModule::remove_stake( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - stake_alpha, - )); - - let issuance_after_unstake = Balances::total_issuance(); - - // Ground-truth fee/loss is the net issuance reduction after stake+unstake. - let fee_balance = issuance_before.saturating_sub(issuance_after_unstake); - let total_fee_actual: u64 = fee_balance.into(); - - // Final coldkey balance should be baseline minus the effective fee. - let balance_after = SubtensorModule::get_coldkey_balance(&coldkey_account_id); - assert_abs_diff_eq!( - balance_after, - (balance_before_stake.saturating_sub(total_fee_actual.into())).into(), - epsilon = 50.into() - ); - - // Stake should be cleared. - assert!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid - ) - .is_zero() - ); - - // Total stake should only increase by what stayed in pools (fees/rounding). - assert_abs_diff_eq!( - SubtensorModule::get_total_stake(), - base_total_stake + TaoBalance::from(total_fee_actual), - epsilon = TaoBalance::from(500u64) - ); - }); -} - -// cargo test --package pallet-subtensor --lib -- tests::staking::test_remove_prev_epoch_stake --exact --show-output --nocapture -#[test] -fn test_remove_prev_epoch_stake() { - new_test_ext(1).execute_with(|| { - // Test case: (amount_to_stake, AlphaDividendsPerSubnet, TotalHotkeyAlphaLastEpoch, expected_fee) - [ - // No previous epoch stake and low hotkey stake - ( - DefaultMinStake::::get().to_u64() * 10, - 0_u64, - 1000_u64, - ), - // Same, but larger amount to stake - we get 0.005% for unstake - (1_000_000_000, 0_u64, 1000_u64), - (100_000_000_000, 0_u64, 1000_u64), - // Lower previous epoch stake than current stake - // Staking/unstaking 100 TAO, divs / total = 0.1 => fee is 1 TAO - (100_000_000_000, 1_000_000_000_u64, 10_000_000_000_u64), - // Staking/unstaking 100 TAO, divs / total = 0.001 => fee is 0.01 TAO - (100_000_000_000, 10_000_000_u64, 10_000_000_000_u64), - // Higher previous epoch stake than current stake - (1_000_000_000, 100_000_000_000_u64, 100_000_000_000_000_u64), - ] - .into_iter() - .for_each(|(amount_to_stake, alpha_divs, hotkey_alpha)| { - let alpha_divs = AlphaBalance::from(alpha_divs); - let hotkey_alpha = AlphaBalance::from(hotkey_alpha); - let subnet_owner_coldkey = U256::from(1); - let subnet_owner_hotkey = U256::from(2); - let hotkey_account_id = U256::from(581337); - let coldkey_account_id = U256::from(81337); - let amount = amount_to_stake; - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); - - // Give it some $$$ in his coldkey balance - add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); - AlphaDividendsPerSubnet::::insert(netuid, hotkey_account_id, alpha_divs); - TotalHotkeyAlphaLastEpoch::::insert(hotkey_account_id, netuid, hotkey_alpha); - let balance_before = SubtensorModule::get_coldkey_balance(&coldkey_account_id); - mock::setup_reserves( - netuid, - (amount_to_stake * 10).into(), - (amount_to_stake * 10).into(), - ); - - // Stake to hotkey account, and check if the result is ok - let (_, fee) = mock::swap_tao_to_alpha(netuid, amount.into()); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - amount.into() - )); - - // Remove all stake - let stake = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - ); - - let fee = mock::swap_alpha_to_tao(netuid, stake).1 + fee; - assert_ok!(SubtensorModule::remove_stake( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - stake - )); - - // Measure actual fee - let balance_after = SubtensorModule::get_coldkey_balance(&coldkey_account_id); - let actual_fee = balance_before - balance_after; - - assert_abs_diff_eq!(actual_fee, fee.into(), epsilon = (fee / 100).into()); - }); - }); -} - -// cargo test --package pallet-subtensor --lib -- tests::staking::test_staking_sets_div_variables --exact --show-output --nocapture -#[test] -fn test_staking_sets_div_variables() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1); - let subnet_owner_hotkey = U256::from(2); - let hotkey_account_id = U256::from(581337); - let coldkey_account_id = U256::from(81337); - let amount = 100_000_000_000_u64; - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - remove_owner_registration_stake(netuid); - let tempo = 10; - Tempo::::insert(netuid, tempo); - register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); - - // Give it some $$$ in his coldkey balance - add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); - - // Verify that divident variables are clear in the beginning - assert_eq!( - AlphaDividendsPerSubnet::::get(netuid, hotkey_account_id), - AlphaBalance::ZERO - ); - assert_eq!( - TotalHotkeyAlphaLastEpoch::::get(hotkey_account_id, netuid), - AlphaBalance::ZERO - ); - - // Stake to hotkey account, and check if the result is ok - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - amount.into() - )); - - // Verify that divident variables are still clear in the beginning - assert_eq!( - AlphaDividendsPerSubnet::::get(netuid, hotkey_account_id), - AlphaBalance::ZERO - ); - assert_eq!( - TotalHotkeyAlphaLastEpoch::::get(hotkey_account_id, netuid), - AlphaBalance::ZERO - ); - - // Wait for 1 epoch - step_epochs(1, netuid); - - // Verify that divident variables have been set - let stake = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - ); - - assert!( - AlphaDividendsPerSubnet::::get(netuid, hotkey_account_id) > AlphaBalance::ZERO - ); - assert_abs_diff_eq!( - TotalHotkeyAlphaLastEpoch::::get(hotkey_account_id, netuid), - stake, - epsilon = stake / 100_000.into() - ); - }); -} - -/*********************************************************** - staking::get_coldkey_balance() tests -************************************************************/ -#[test] -fn test_get_coldkey_balance_no_balance() { - new_test_ext(1).execute_with(|| { - let coldkey_account_id = U256::from(5454); // arbitrary - let result = SubtensorModule::get_coldkey_balance(&coldkey_account_id); - - // Arbitrary account should have 0 balance - assert_eq!(result, 0.into()); - }); -} - -#[test] -fn test_get_coldkey_balance_with_balance() { - new_test_ext(1).execute_with(|| { - let coldkey_account_id = U256::from(5454); // arbitrary - let amount = 1337; - - // Put the balance on the account - add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); - - let result = SubtensorModule::get_coldkey_balance(&coldkey_account_id); - - // Arbitrary account should have 0 balance - assert_eq!(result, amount.into()); - }); -} - -// /*********************************************************** -// staking::increase_stake_for_hotkey_and_coldkey_on_subnet() tests -// ************************************************************/ -#[test] -fn test_add_stake_to_hotkey_account_ok() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1); - let subnet_owner_hotkey = U256::from(2); - let hotkey_id = U256::from(5445); - let coldkey_id = U256::from(5443433); - let amount: u64 = 10_000; - - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - register_ok_neuron(netuid, hotkey_id, coldkey_id, 192213123); - - let base_total_stake = SubtensorModule::get_total_stake(); - - // Check stake in ALPHA units for this hotkey/coldkey/netuid triple. - let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_id, - &coldkey_id, - netuid, - ); - - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_id, - &coldkey_id, - netuid, - AlphaBalance::from(amount), - ); - - let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_id, - &coldkey_id, - netuid, - ); - - assert_eq!( - alpha_after, - alpha_before + AlphaBalance::from(amount), - "Alpha stake did not increase by the expected amount" - ); - - // Total stake should never decrease when we increase stake. - let total_stake_after = SubtensorModule::get_total_stake(); - assert!( - total_stake_after >= base_total_stake, - "Total stake unexpectedly decreased after increasing stake" - ); - }); -} - -/************************************************************ - staking::remove_stake_from_hotkey_account() tests -************************************************************/ -#[test] -fn test_remove_stake_from_hotkey_account() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1); - let subnet_owner_hotkey = U256::from(2); - let hotkey_id = U256::from(5445); - let coldkey_id = U256::from(5443433); - let amount: AlphaBalance = 10_000u64.into(); - - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - register_ok_neuron(netuid, hotkey_id, coldkey_id, 192213123); - - // Baselines before adding stake. - let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_id, - &coldkey_id, - netuid, - ); - let total_before = SubtensorModule::get_total_stake_for_hotkey(&hotkey_id); - - // Add alpha stake directly through the internal helper. - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_id, - &coldkey_id, - netuid, - amount, - ); - - // Alpha stake should increase by exactly the credited alpha amount. - let alpha_after_add = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_id, - &coldkey_id, - netuid, - ); - assert_eq!(alpha_after_add, alpha_before.saturating_add(amount)); - - // Tao-equivalent total stake should have increased from baseline. - assert!(SubtensorModule::get_total_stake_for_hotkey(&hotkey_id) > total_before); - - // Remove exactly the same alpha amount. - SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_id, - &coldkey_id, - netuid, - amount, - ); - - // Alpha stake should return to its original baseline. - let alpha_after_remove = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_id, - &coldkey_id, - netuid, - ); - assert_eq!(alpha_after_remove, alpha_before); - - // Tao-equivalent total stake should also return to baseline. - assert_abs_diff_eq!( - SubtensorModule::get_total_stake_for_hotkey(&hotkey_id), - total_before, - epsilon = 10.into() - ); - }); -} - -#[test] -fn test_remove_stake_from_hotkey_account_registered_in_various_networks() { - new_test_ext(1).execute_with(|| { - let hotkey_id = U256::from(5445); - let coldkey_id = U256::from(5443433); - let amount: u64 = 10_000; - let netuid = add_dynamic_network(&hotkey_id, &coldkey_id); - remove_owner_registration_stake(netuid); - let netuid_ex = add_dynamic_network(&hotkey_id, &coldkey_id); - remove_owner_registration_stake(netuid_ex); - - let neuron_uid = match SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey_id) { - Ok(k) => k, - Err(e) => panic!("Error: {e:?}"), - }; - - let neuron_uid_ex = match SubtensorModule::get_uid_for_net_and_hotkey(netuid_ex, &hotkey_id) - { - Ok(k) => k, - Err(e) => panic!("Error: {e:?}"), - }; - - // Add some stake that can be removed - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_id, - &coldkey_id, - netuid, - amount.into(), - ); - - assert_eq!( - SubtensorModule::get_stake_for_uid_and_subnetwork(netuid, neuron_uid), - amount.into() - ); - assert_eq!( - SubtensorModule::get_stake_for_uid_and_subnetwork(netuid_ex, neuron_uid_ex), - AlphaBalance::ZERO - ); - - // Remove all stake - SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_id, - &coldkey_id, - netuid, - amount.into(), - ); - - // - assert_eq!( - SubtensorModule::get_stake_for_uid_and_subnetwork(netuid, neuron_uid), - AlphaBalance::ZERO - ); - assert_eq!( - SubtensorModule::get_stake_for_uid_and_subnetwork(netuid_ex, neuron_uid_ex), - AlphaBalance::ZERO - ); - }); -} - -// /************************************************************ -// staking::increase_total_stake() tests -// ************************************************************/ -#[test] -fn test_increase_total_stake_ok() { - new_test_ext(1).execute_with(|| { - let increment = TaoBalance::from(10000); - assert_eq!(SubtensorModule::get_total_stake(), TaoBalance::ZERO); - SubtensorModule::increase_total_stake(increment); - assert_eq!(SubtensorModule::get_total_stake(), increment); - }); -} - -// /************************************************************ -// staking::decrease_total_stake() tests -// ************************************************************/ -#[test] -fn test_decrease_total_stake_ok() { - new_test_ext(1).execute_with(|| { - let initial_total_stake = TaoBalance::from(10000); - let decrement = TaoBalance::from(5000); - - SubtensorModule::increase_total_stake(initial_total_stake); - SubtensorModule::decrease_total_stake(decrement); - - // The total stake remaining should be the difference between the initial stake and the decrement - assert_eq!( - SubtensorModule::get_total_stake(), - initial_total_stake - decrement - ); - }); -} - -// /************************************************************ -// staking::add_balance_to_coldkey_account() tests -// ************************************************************/ -#[test] -fn test_add_balance_to_coldkey_account_ok() { - new_test_ext(1).execute_with(|| { - let coldkey_id = U256::from(4444322); - let amount = 50000; - add_balance_to_coldkey_account(&coldkey_id, amount.into()); - assert_eq!( - SubtensorModule::get_coldkey_balance(&coldkey_id), - amount.into() - ); - }); -} - -// /*********************************************************** -// staking::remove_balance_from_coldkey_account() tests -// ************************************************************/ -#[test] -fn test_remove_balance_from_coldkey_account_ok() { - new_test_ext(1).execute_with(|| { - let coldkey_account_id = U256::from(434324); // Random - let amount = 10000; // Arbitrary - let netuid = NetUid::from(1); - // Put some $$ on the bank - add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); - NetworksAdded::::insert(netuid, true); - assert_eq!( - SubtensorModule::get_coldkey_balance(&coldkey_account_id), - amount.into() - ); - // Should be able to withdraw without hassle - let result = - SubtensorModule::transfer_tao_to_subnet(netuid, &coldkey_account_id, amount.into()); - assert!(result.is_ok()); - }); -} - -#[test] -fn test_remove_balance_from_coldkey_account_failed() { - new_test_ext(1).execute_with(|| { - let coldkey_account_id = U256::from(434324); // Random - let amount = 10000; // Arbitrary - - let netuid = NetUid::from(1); - NetworksAdded::::insert(netuid, true); - - // Try to remove stake from the coldkey account. This should fail, - // as there is no balance, nor does the account exist - let result = - SubtensorModule::transfer_tao_to_subnet(netuid, &coldkey_account_id, amount.into()); - assert_eq!(result, Err(Error::::InsufficientTaoBalance.into())); - }); -} - -//************************************************************ -// staking::hotkey_belongs_to_coldkey() tests -// ************************************************************/ -#[test] -fn test_hotkey_belongs_to_coldkey_ok() { - new_test_ext(1).execute_with(|| { - let hotkey_id = U256::from(4434334); - let coldkey_id = U256::from(34333); - let netuid = NetUid::from(1); - let tempo: u16 = 13; - let start_nonce: u64 = 0; - add_network(netuid, tempo, 0); - register_ok_neuron(netuid, hotkey_id, coldkey_id, start_nonce); - assert_eq!( - SubtensorModule::get_owning_coldkey_for_hotkey(&hotkey_id), - coldkey_id - ); - }); -} -// /************************************************************ -// staking::can_remove_balance_from_coldkey_account() tests -// ************************************************************/ -#[test] -fn test_can_remove_balane_from_coldkey_account_ok() { - new_test_ext(1).execute_with(|| { - let coldkey_id = U256::from(87987984); - let initial_amount = 10000; - let remove_amount = 5000; - add_balance_to_coldkey_account(&coldkey_id, initial_amount.into()); - assert!(SubtensorModule::can_remove_balance_from_coldkey_account( - &coldkey_id, - remove_amount.into() - )); - }); -} - -#[test] -fn test_can_remove_balance_from_coldkey_account_err_insufficient_balance() { - new_test_ext(1).execute_with(|| { - let coldkey_id = U256::from(87987984); - let initial_amount = 10000; - let remove_amount = 20000; - add_balance_to_coldkey_account(&coldkey_id, initial_amount.into()); - assert!(!SubtensorModule::can_remove_balance_from_coldkey_account( - &coldkey_id, - remove_amount.into() - )); - }); -} -/************************************************************ - staking::has_enough_stake() tests -************************************************************/ -#[test] -fn test_has_enough_stake_yes() { - new_test_ext(1).execute_with(|| { - let hotkey_id = U256::from(4334); - let coldkey_id = U256::from(87989); - let intial_amount = 10_000; - let netuid = NetUid::from(add_dynamic_network(&hotkey_id, &coldkey_id)); - remove_owner_registration_stake(netuid); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_id, - &coldkey_id, - netuid, - intial_amount.into(), - ); - - assert_abs_diff_eq!( - SubtensorModule::get_total_stake_for_hotkey(&hotkey_id), - intial_amount.into(), - epsilon = 2.into() - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_id, - &coldkey_id, - netuid - ), - intial_amount.into() - ); - assert_ok!(SubtensorModule::calculate_reduced_stake_on_subnet( - &hotkey_id, - &coldkey_id, - netuid, - (intial_amount / 2).into() - )); - }); -} - -#[test] -fn test_has_enough_stake_no() { - new_test_ext(1).execute_with(|| { - let hotkey_id = U256::from(4334); - let coldkey_id = U256::from(87989); - let intial_amount = 10_000; - let netuid = add_dynamic_network(&hotkey_id, &coldkey_id); - remove_owner_registration_stake(netuid); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_id, - &coldkey_id, - netuid, - intial_amount.into(), - ); - - assert_abs_diff_eq!( - SubtensorModule::get_total_stake_for_hotkey(&hotkey_id), - intial_amount.into(), - epsilon = 2.into() - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_id, - &coldkey_id, - netuid - ), - intial_amount.into() - ); - assert_err!( - SubtensorModule::calculate_reduced_stake_on_subnet( - &hotkey_id, - &coldkey_id, - netuid, - (intial_amount * 2).into() - ), - Error::::NotEnoughStakeToWithdraw - ); - }); -} - -#[test] -fn test_has_enough_stake_no_for_zero() { - new_test_ext(1).execute_with(|| { - let hotkey_id = U256::from(4334); - let coldkey_id = U256::from(87989); - let intial_amount = 0; - let netuid = add_dynamic_network(&hotkey_id, &coldkey_id); - remove_owner_registration_stake(netuid); - - assert_eq!( - SubtensorModule::get_total_stake_for_hotkey(&hotkey_id), - intial_amount.into() - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_id, - &coldkey_id, - netuid - ), - intial_amount.into() - ); - assert_err!( - SubtensorModule::calculate_reduced_stake_on_subnet( - &hotkey_id, - &coldkey_id, - netuid, - 1_000.into() - ), - Error::::NotEnoughStakeToWithdraw - ); - }); -} - -#[test] -fn test_non_existent_account() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &U256::from(0), - &(U256::from(0)), - netuid, - 10.into(), - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &U256::from(0), - &U256::from(0), - netuid - ), - 10.into() - ); - // No subnets => no iteration => zero total stake - assert_eq!( - SubtensorModule::get_total_stake_for_hotkey(&(U256::from(0))), - TaoBalance::ZERO - ); - }); -} - -/************************************************************ - staking::delegating -************************************************************/ - -#[test] -fn test_faucet_ok() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(123560); - - log::info!("Creating work for submission to faucet..."); - - let block_number = SubtensorModule::get_current_block_as_u64(); - let difficulty: U256 = U256::from(10_000_000); - let mut nonce: u64 = 0; - let mut work: H256 = SubtensorModule::create_seal_hash(block_number, nonce, &coldkey); - while !SubtensorModule::hash_meets_difficulty(&work, difficulty) { - nonce += 1; - work = SubtensorModule::create_seal_hash(block_number, nonce, &coldkey); - } - let vec_work: Vec = SubtensorModule::hash_to_vec(work); - - log::info!("Faucet state: {}", cfg!(feature = "pow-faucet")); - - #[cfg(feature = "pow-faucet")] - assert_ok!(SubtensorModule::do_faucet( - RuntimeOrigin::signed(coldkey), - block_number, - nonce, - vec_work - )); - - #[cfg(not(feature = "pow-faucet"))] - assert_ok!(SubtensorModule::do_faucet( - RuntimeOrigin::signed(coldkey), - block_number, - nonce, - vec_work - )); - }); -} - -/// This test ensures that the clear_small_nominations function works as expected. -/// It creates a network with two hotkeys and two coldkeys, and then registers a nominator account for each hotkey. -/// When we call set_nominator_min_required_stake, it should clear all small nominations that are below the minimum required stake. -/// -/// cargo test --package pallet-subtensor --lib -- tests::staking::test_clear_small_nominations --exact --show-output -#[test] -fn test_clear_small_nominations() { - new_test_ext(0).execute_with(|| { - // Create subnet and accounts. - let subnet_owner_coldkey = U256::from(10); - let subnet_owner_hotkey = U256::from(20); - let hot1 = U256::from(1); - let hot2 = U256::from(2); - let cold1 = U256::from(3); - let cold2 = U256::from(4); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - let amount = DefaultMinStake::::get() * 10.into(); - let fee = DefaultMinStake::::get(); - let init_balance = amount + fee + ExistentialDeposit::get(); - - // Set fee rate to 0 so that alpha fee is not moved to block producer - pallet_subtensor_swap::FeeRate::::insert(netuid, 0); - - // Register hot1. - register_ok_neuron(netuid, hot1, cold1, 0); - Delegates::::insert( - hot1, - PerU16::from_parts(SubtensorModule::get_min_delegate_take()), - ); - assert_eq!(SubtensorModule::get_owning_coldkey_for_hotkey(&hot1), cold1); - - // Register hot2. - register_ok_neuron(netuid, hot2, cold2, 0); - Delegates::::insert( - hot2, - PerU16::from_parts(SubtensorModule::get_min_delegate_take()), - ); - assert_eq!(SubtensorModule::get_owning_coldkey_for_hotkey(&hot2), cold2); - - // Add stake cold1 --> hot1 (non delegation.) - add_balance_to_coldkey_account(&cold1, init_balance); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(cold1), - hot1, - netuid, - amount.into() - )); - let alpha_stake1 = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hot1, &cold1, netuid); - let unstake_amount1 = AlphaBalance::from(alpha_stake1.to_u64() * 997 / 1000); - let small1 = alpha_stake1 - unstake_amount1; - assert_ok!(SubtensorModule::remove_stake( - RuntimeOrigin::signed(cold1), - hot1, - netuid, - unstake_amount1 - )); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hot1, &cold1, netuid), - small1 - ); - - // Add stake cold2 --> hot1 (is delegation.) - add_balance_to_coldkey_account(&cold2, init_balance); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(cold2), - hot1, - netuid, - amount.into() - )); - let alpha_stake2 = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hot1, &cold2, netuid); - let unstake_amount2 = AlphaBalance::from(alpha_stake2.to_u64() * 997 / 1000); - let small2 = alpha_stake2 - unstake_amount2; - assert_ok!(SubtensorModule::remove_stake( - RuntimeOrigin::signed(cold2), - hot1, - netuid, - unstake_amount2 - )); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hot1, &cold2, netuid), - small2 - ); - - let balance1_before_cleaning = Balances::free_balance(cold1); - let balance2_before_cleaning = Balances::free_balance(cold2); - - // Run clear all small nominations when min stake is zero (noop) - SubtensorModule::set_nominator_min_required_stake(0); - assert_eq!(SubtensorModule::get_nominator_min_required_stake(), 0); - SubtensorModule::clear_small_nominations(); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hot1, &cold1, netuid), - small1 - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hot1, &cold2, netuid), - small2 - ); - - // Set min nomination to above small1 and small2 - let total_hot1_stake_before = TotalHotkeyAlpha::::get(hot1, netuid); - let total_stake_before = TotalStake::::get(); - SubtensorModule::set_nominator_min_required_stake( - (small1.to_u64().min(small2.to_u64()) * 2).into(), - ); - - // Run clear all small nominations (removes delegations under 10) - SubtensorModule::clear_small_nominations(); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hot1, &cold1, netuid), - small1 - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hot1, &cold2, netuid), - AlphaBalance::ZERO - ); - - // Balances have been added back into accounts. - let balance1_after_cleaning = Balances::free_balance(cold1); - let balance2_after_cleaning = Balances::free_balance(cold2); - assert_eq!(balance1_before_cleaning, balance1_after_cleaning); - assert!(balance2_before_cleaning < balance2_after_cleaning); - - assert_abs_diff_eq!( - TotalHotkeyAlpha::::get(hot1, netuid), - total_hot1_stake_before - small2, - epsilon = 1.into() - ); - assert!(TotalStake::::get() < total_stake_before); - }); -} - -// Verify delegate take can be decreased -#[test] -fn test_delegate_take_can_be_decreased() { - new_test_ext(1).execute_with(|| { - // Make account - let hotkey0 = U256::from(1); - let coldkey0 = U256::from(3); - - // Add balance - add_balance_to_coldkey_account(&coldkey0, 100000.into()); - - // Register the neuron to a new network - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - register_ok_neuron(netuid, hotkey0, coldkey0, 124124); - - // Coldkey / hotkey 0 become delegates with 9% take - Delegates::::insert( - hotkey0, - PerU16::from_parts(SubtensorModule::get_min_delegate_take()), - ); - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - SubtensorModule::get_min_delegate_take() - ); - - // Coldkey / hotkey 0 decreases take to 5%. This should fail as the minimum take is 9% - assert_err!( - SubtensorModule::do_decrease_take( - RuntimeOrigin::signed(coldkey0), - hotkey0, - PerU16::from_parts(u16::MAX / 20) - ), - Error::::DelegateTakeTooLow - ); - }); -} - -// Verify delegate take can be decreased -#[test] -fn test_can_set_min_take_ok() { - new_test_ext(1).execute_with(|| { - // Make account - let hotkey0 = U256::from(1); - let coldkey0 = U256::from(3); - - // Add balance - add_balance_to_coldkey_account(&coldkey0, 100000.into()); - - // Register the neuron to a new network - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - register_ok_neuron(netuid, hotkey0, coldkey0, 124124); - - // Coldkey / hotkey 0 become delegates - Delegates::::insert(hotkey0, PerU16::from_parts(u16::MAX / 10)); - - // Coldkey / hotkey 0 decreases take to min - assert_ok!(SubtensorModule::do_decrease_take( - RuntimeOrigin::signed(coldkey0), - hotkey0, - PerU16::from_parts(SubtensorModule::get_min_delegate_take()) - )); - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - SubtensorModule::get_min_delegate_take() - ); - }); -} - -// Verify delegate take can not be increased with do_decrease_take -#[test] -fn test_delegate_take_can_not_be_increased_with_decrease_take() { - new_test_ext(1).execute_with(|| { - // Make account - let hotkey0 = U256::from(1); - let coldkey0 = U256::from(3); - - // Add balance - add_balance_to_coldkey_account(&coldkey0, 100000.into()); - - // Register the neuron to a new network - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - register_ok_neuron(netuid, hotkey0, coldkey0, 124124); - - // Set min take - Delegates::::insert( - hotkey0, - PerU16::from_parts(SubtensorModule::get_min_delegate_take()), - ); - - // Coldkey / hotkey 0 tries to increase take to 12.5% - assert_eq!( - SubtensorModule::do_decrease_take( - RuntimeOrigin::signed(coldkey0), - hotkey0, - PerU16::from_parts(SubtensorModule::get_max_delegate_take()) - ), - Err(Error::::DelegateTakeTooLow.into()) - ); - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - SubtensorModule::get_min_delegate_take() - ); - }); -} - -// Verify delegate take can be increased -#[test] -fn test_delegate_take_can_be_increased() { - new_test_ext(1).execute_with(|| { - // Make account - let hotkey0 = U256::from(1); - let coldkey0 = U256::from(3); - - // Add balance - add_balance_to_coldkey_account(&coldkey0, 100000.into()); - - // Register the neuron to a new network - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - register_ok_neuron(netuid, hotkey0, coldkey0, 124124); - - // Coldkey / hotkey 0 become delegates with 9% take - Delegates::::insert( - hotkey0, - PerU16::from_parts(SubtensorModule::get_min_delegate_take()), - ); - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - SubtensorModule::get_min_delegate_take() - ); - - step_block(1 + InitialTxDelegateTakeRateLimit::get() as u16); - - // Coldkey / hotkey 0 decreases take to 12.5% - assert_ok!(SubtensorModule::do_increase_take( - RuntimeOrigin::signed(coldkey0), - hotkey0, - PerU16::from_parts(u16::MAX / 8) - )); - assert_eq!(SubtensorModule::get_hotkey_take(&hotkey0), u16::MAX / 8); - }); -} - -// Verify delegate take can not be decreased with increase_take -#[test] -fn test_delegate_take_can_not_be_decreased_with_increase_take() { - new_test_ext(1).execute_with(|| { - // Make account - let hotkey0 = U256::from(1); - let coldkey0 = U256::from(3); - - // Add balance - add_balance_to_coldkey_account(&coldkey0, 100000.into()); - - // Register the neuron to a new network - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - register_ok_neuron(netuid, hotkey0, coldkey0, 124124); - - // Coldkey / hotkey 0 become delegates with 9% take - Delegates::::insert( - hotkey0, - PerU16::from_parts(SubtensorModule::get_min_delegate_take()), - ); - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - SubtensorModule::get_min_delegate_take() - ); - - // Coldkey / hotkey 0 tries to decrease take to 5% - assert_eq!( - SubtensorModule::do_increase_take( - RuntimeOrigin::signed(coldkey0), - hotkey0, - PerU16::from_parts(u16::MAX / 20) - ), - Err(Error::::DelegateTakeTooLow.into()) - ); - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - SubtensorModule::get_min_delegate_take() - ); - }); -} - -// Verify delegate take can be increased up to InitialDefaultDelegateTake (18%) -#[test] -fn test_delegate_take_can_be_increased_to_limit() { - new_test_ext(1).execute_with(|| { - // Make account - let hotkey0 = U256::from(1); - let coldkey0 = U256::from(3); - - // Add balance - add_balance_to_coldkey_account(&coldkey0, 100000.into()); - - // Register the neuron to a new network - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - register_ok_neuron(netuid, hotkey0, coldkey0, 124124); - - // Coldkey / hotkey 0 become delegates with 9% take - Delegates::::insert( - hotkey0, - PerU16::from_parts(SubtensorModule::get_min_delegate_take()), - ); - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - SubtensorModule::get_min_delegate_take() - ); - - step_block(1 + InitialTxDelegateTakeRateLimit::get() as u16); - - // Coldkey / hotkey 0 tries to increase take to InitialDefaultDelegateTake+1 - assert_ok!(SubtensorModule::do_increase_take( - RuntimeOrigin::signed(coldkey0), - hotkey0, - PerU16::from_parts(InitialDefaultDelegateTake::get()) - )); - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - InitialDefaultDelegateTake::get() - ); - }); -} - -// Verify delegate take can not be increased above InitialDefaultDelegateTake (18%) -#[test] -fn test_delegate_take_can_not_be_increased_beyond_limit() { - new_test_ext(1).execute_with(|| { - // Make account - let hotkey0 = U256::from(1); - let coldkey0 = U256::from(3); - - // Add balance - add_balance_to_coldkey_account(&coldkey0, 100000.into()); - - // Register the neuron to a new network - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - register_ok_neuron(netuid, hotkey0, coldkey0, 124124); - - // Coldkey / hotkey 0 become delegates with 9% take - Delegates::::insert( - hotkey0, - PerU16::from_parts(SubtensorModule::get_min_delegate_take()), - ); - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - SubtensorModule::get_min_delegate_take() - ); - - // Coldkey / hotkey 0 tries to increase take to InitialDefaultDelegateTake+1 - // (Disable this check if InitialDefaultDelegateTake is u16::MAX) - if InitialDefaultDelegateTake::get() != u16::MAX { - assert_eq!( - SubtensorModule::do_increase_take( - RuntimeOrigin::signed(coldkey0), - hotkey0, - PerU16::from_parts(InitialDefaultDelegateTake::get() + 1) - ), - Err(Error::::DelegateTakeTooHigh.into()) - ); - } - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - SubtensorModule::get_min_delegate_take() - ); - }); -} - -// Test rate-limiting on increase_take -#[test] -fn test_rate_limits_enforced_on_increase_take() { - new_test_ext(1).execute_with(|| { - // Make account - let hotkey0 = U256::from(1); - let coldkey0 = U256::from(3); - - // Add balance - add_balance_to_coldkey_account(&coldkey0, 100000.into()); - - // Register the neuron to a new network - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - register_ok_neuron(netuid, hotkey0, coldkey0, 124124); - - // Coldkey / hotkey 0 become delegates with 9% take - Delegates::::insert( - hotkey0, - PerU16::from_parts(SubtensorModule::get_min_delegate_take()), - ); - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - SubtensorModule::get_min_delegate_take() - ); - - // Increase take first time - assert_ok!(SubtensorModule::do_increase_take( - RuntimeOrigin::signed(coldkey0), - hotkey0, - PerU16::from_parts(SubtensorModule::get_min_delegate_take() + 1) - )); - - // Increase again - assert_eq!( - SubtensorModule::do_increase_take( - RuntimeOrigin::signed(coldkey0), - hotkey0, - PerU16::from_parts(SubtensorModule::get_min_delegate_take() + 2) - ), - Err(Error::::DelegateTxRateLimitExceeded.into()) - ); - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - SubtensorModule::get_min_delegate_take() + 1 - ); - - step_block(1 + InitialTxDelegateTakeRateLimit::get() as u16); - - // Can increase after waiting - assert_ok!(SubtensorModule::do_increase_take( - RuntimeOrigin::signed(coldkey0), - hotkey0, - PerU16::from_parts(SubtensorModule::get_min_delegate_take() + 2) - )); - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - SubtensorModule::get_min_delegate_take() + 2 - ); - }); -} - -// Test rate-limiting on an increase take just after a decrease take -// Prevents a Validator from decreasing take and then increasing it immediately after. -#[test] -fn test_rate_limits_enforced_on_decrease_before_increase_take() { - new_test_ext(1).execute_with(|| { - // Make account - let hotkey0 = U256::from(1); - let coldkey0 = U256::from(3); - - // Add balance - add_balance_to_coldkey_account(&coldkey0, 100000.into()); - - // Register the neuron to a new network - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - register_ok_neuron(netuid, hotkey0, coldkey0, 124124); - - // Coldkey / hotkey 0 become delegates with 9% take - Delegates::::insert( - hotkey0, - PerU16::from_parts(SubtensorModule::get_min_delegate_take() + 1), - ); - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - SubtensorModule::get_min_delegate_take() + 1 - ); - - // Decrease take - assert_ok!(SubtensorModule::do_decrease_take( - RuntimeOrigin::signed(coldkey0), - hotkey0, - PerU16::from_parts(SubtensorModule::get_min_delegate_take()) - )); // Verify decrease - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - SubtensorModule::get_min_delegate_take() - ); - - // Increase take immediately after - assert_eq!( - SubtensorModule::do_increase_take( - RuntimeOrigin::signed(coldkey0), - hotkey0, - PerU16::from_parts(SubtensorModule::get_min_delegate_take() + 1) - ), - Err(Error::::DelegateTxRateLimitExceeded.into()) - ); // Verify no change - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - SubtensorModule::get_min_delegate_take() - ); - - step_block(1 + InitialTxDelegateTakeRateLimit::get() as u16); - - // Can increase after waiting - assert_ok!(SubtensorModule::do_increase_take( - RuntimeOrigin::signed(coldkey0), - hotkey0, - PerU16::from_parts(SubtensorModule::get_min_delegate_take() + 1) - )); // Verify increase - assert_eq!( - SubtensorModule::get_hotkey_take(&hotkey0), - SubtensorModule::get_min_delegate_take() + 1 - ); - }); -} - -// cargo test --package pallet-subtensor --lib -- tests::staking::test_get_total_delegated_stake_after_unstaking --exact --show-output -#[test] -fn test_get_total_delegated_stake_after_unstaking() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let delegate_coldkey = U256::from(1); - let delegate_hotkey = U256::from(2); - let delegator = U256::from(3); - let initial_stake = DefaultMinStake::::get().to_u64() * 10; - let existential_deposit = ExistentialDeposit::get(); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - register_ok_neuron(netuid, delegate_hotkey, delegate_coldkey, 0); - - // Add balance to delegator - add_balance_to_coldkey_account(&delegator, initial_stake.into()); - - // Delegate stake - let (_, fee) = mock::swap_tao_to_alpha(netuid, initial_stake.into()); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(delegator), - delegate_hotkey, - netuid, - initial_stake.into() - )); - - // Check initial delegated stake - assert_abs_diff_eq!( - SubtensorModule::get_total_stake_for_coldkey(&delegator), - (initial_stake - u64::from(existential_deposit) - fee).into(), - epsilon = TaoBalance::from(initial_stake / 100), - ); - assert_abs_diff_eq!( - SubtensorModule::get_total_stake_for_hotkey(&delegate_hotkey), - (initial_stake - u64::from(existential_deposit) - fee).into(), - epsilon = TaoBalance::from(initial_stake / 100), - ); - let delegated_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &delegate_hotkey, - &delegator, - netuid, - ); - // Unstake part of the delegation - let unstake_amount_alpha = delegated_alpha / 2.into(); - assert_ok!(SubtensorModule::remove_stake( - RuntimeOrigin::signed(delegator), - delegate_hotkey, - netuid, - unstake_amount_alpha.into() - )); - let current_price = U96F32::from_num( - ::SwapInterface::current_alpha_price(netuid.into()), - ); - - // Calculate the expected delegated stake - let unstake_amount = - (current_price * U96F32::from_num(unstake_amount_alpha)).to_num::(); - let expected_delegated_stake: u64 = - initial_stake - unstake_amount - u64::from(existential_deposit) - fee; - - // Debug prints - log::debug!("Initial stake: {initial_stake}"); - log::debug!("Unstake amount: {unstake_amount}"); - log::debug!("Existential deposit: {existential_deposit}"); - log::debug!("Expected delegated stake: {expected_delegated_stake}"); - log::debug!( - "Actual delegated stake: {}", - SubtensorModule::get_total_stake_for_coldkey(&delegate_coldkey) - ); - - // Check the total delegated stake after unstaking - assert_abs_diff_eq!( - SubtensorModule::get_total_stake_for_coldkey(&delegator), - expected_delegated_stake.into(), - epsilon = TaoBalance::from(expected_delegated_stake / 1000), - ); - assert_abs_diff_eq!( - SubtensorModule::get_total_stake_for_hotkey(&delegate_hotkey), - expected_delegated_stake.into(), - epsilon = TaoBalance::from(expected_delegated_stake / 1000), - ); - }); -} - -#[test] -fn test_get_total_delegated_stake_no_delegations() { - new_test_ext(1).execute_with(|| { - let delegate = U256::from(1); - let coldkey = U256::from(2); - let netuid = NetUid::from(1u16); - - add_network(netuid, 1, 0); - register_ok_neuron(netuid, delegate, coldkey, 0); - - // Check that there's no delegated stake - assert_eq!( - SubtensorModule::get_total_stake_for_coldkey(&delegate), - TaoBalance::ZERO - ); - }); -} - -#[test] -fn test_get_total_delegated_stake_single_delegator() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let delegate_coldkey = U256::from(1); - let delegate_hotkey = U256::from(2); - let delegator = U256::from(3); - let stake_amount = DefaultMinStake::::get().to_u64() * 10 - 1; - let existential_deposit = ExistentialDeposit::get(); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - register_ok_neuron(netuid, delegate_hotkey, delegate_coldkey, 0); - - // Add stake from delegator - add_balance_to_coldkey_account(&delegator, stake_amount.into()); - - let (_, fee) = mock::swap_tao_to_alpha(netuid, stake_amount.into()); - - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(delegator), - delegate_hotkey, - netuid, - stake_amount.into() - )); - - // Debug prints - log::debug!("Delegate coldkey: {delegate_coldkey:?}"); - log::debug!("Delegate hotkey: {delegate_hotkey:?}"); - log::debug!("Delegator: {delegator:?}"); - log::debug!("Stake amount: {stake_amount}"); - log::debug!("Existential deposit: {existential_deposit}"); - log::debug!( - "Total stake for hotkey: {}", - SubtensorModule::get_total_stake_for_hotkey(&delegate_hotkey) - ); - log::debug!( - "Delegated stake for coldkey: {}", - SubtensorModule::get_total_stake_for_coldkey(&delegate_coldkey) - ); - - // Calculate expected delegated stake - let expected_delegated_stake = stake_amount - u64::from(existential_deposit) - fee; - let actual_delegated_stake = SubtensorModule::get_total_stake_for_hotkey(&delegate_hotkey); - let actual_delegator_stake = SubtensorModule::get_total_stake_for_coldkey(&delegator); - - assert_abs_diff_eq!( - actual_delegated_stake, - expected_delegated_stake.into(), - epsilon = TaoBalance::from(expected_delegated_stake / 100), - ); - assert_abs_diff_eq!( - actual_delegator_stake, - expected_delegated_stake.into(), - epsilon = TaoBalance::from(expected_delegated_stake / 100), - ); - }); -} - -#[test] -fn test_get_alpha_share_stake_multiple_delegators() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let hotkey1 = U256::from(2); - let hotkey2 = U256::from(20); - let coldkey1 = U256::from(3); - let coldkey2 = U256::from(4); - let existential_deposit = TaoBalance::from(2); - let stake1 = DefaultMinStake::::get() * 10.into(); - let stake2 = DefaultMinStake::::get() * 10.into() - 1.into(); - - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - register_ok_neuron(netuid, hotkey1, coldkey1, 0); - register_ok_neuron(netuid, hotkey2, coldkey2, 0); - - // Add stake from delegator1 - add_balance_to_coldkey_account(&coldkey1, stake1 + existential_deposit); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey1), - hotkey1, - netuid, - stake1 - )); - - // Add stake from delegator2 - add_balance_to_coldkey_account(&coldkey2, stake2 + existential_deposit); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey2), - hotkey2, - netuid, - stake2 - )); - - // Calculate expected total delegated stake - let alpha1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey1, &coldkey1, netuid, - ); - let alpha2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey2, &coldkey2, netuid, - ); - let expected_total_stake = alpha1 + alpha2; - let actual_total_stake = SubtensorModule::get_alpha_share_pool(hotkey1, netuid) - .get_value(&coldkey1) - + SubtensorModule::get_alpha_share_pool(hotkey2, netuid).get_value(&coldkey2); - - // Total subnet stake should match the sum of delegators' stakes minus existential deposits. - assert_abs_diff_eq!( - AlphaBalance::from(actual_total_stake), - expected_total_stake, - epsilon = expected_total_stake / 1000.into() - ); - }); -} - -#[test] -fn test_get_total_delegated_stake_exclude_owner_stake() { - new_test_ext(1).execute_with(|| { - let delegate_coldkey = U256::from(1); - let delegate_hotkey = U256::from(2); - let delegator = U256::from(3); - let owner_stake = DefaultMinStake::::get().to_u64() * 10; - let delegator_stake = DefaultMinStake::::get().to_u64() * 10 - 1; - - let netuid = add_dynamic_network(&delegate_hotkey, &delegate_coldkey); - remove_owner_registration_stake(netuid); - - // Add owner stake - add_balance_to_coldkey_account(&delegate_coldkey, owner_stake.into()); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(delegate_coldkey), - delegate_hotkey, - netuid, - owner_stake.into() - )); - - // Add delegator stake - add_balance_to_coldkey_account(&delegator, delegator_stake.into()); - let (_, fee) = mock::swap_tao_to_alpha(netuid, delegator_stake.into()); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(delegator), - delegate_hotkey, - netuid, - delegator_stake.into() - )); - - // Check the total delegated stake (should exclude owner's stake) - let expected_delegated_stake = delegator_stake - fee; - let actual_delegated_stake = - SubtensorModule::get_total_stake_for_coldkey(&delegate_coldkey); - - assert_abs_diff_eq!( - actual_delegated_stake, - expected_delegated_stake.into(), - epsilon = TaoBalance::from(expected_delegated_stake / 100) - ); - }); -} - -/// Test that emission is distributed correctly between one validator, one -/// vali-miner, and one miner -#[test] -fn test_mining_emission_distribution_validator_valiminer_miner() { - new_test_ext(1).execute_with(|| { - let validator_coldkey = U256::from(1); - let validator_hotkey = U256::from(2); - let validator_miner_coldkey = U256::from(3); - let validator_miner_hotkey = U256::from(4); - let miner_coldkey = U256::from(5); - let miner_hotkey = U256::from(6); - let netuid = NetUid::from(1); - let subnet_tempo = 10; - let stake = TaoBalance::from(100_000_000_000_u64); - - // Add network, register hotkeys, and setup network parameters - add_network(netuid, subnet_tempo, 0); - register_ok_neuron(netuid, validator_hotkey, validator_coldkey, 0); - register_ok_neuron(netuid, validator_miner_hotkey, validator_miner_coldkey, 1); - register_ok_neuron(netuid, miner_hotkey, miner_coldkey, 2); - add_balance_to_coldkey_account(&validator_coldkey, stake + ExistentialDeposit::get()); - add_balance_to_coldkey_account(&validator_miner_coldkey, stake + ExistentialDeposit::get()); - add_balance_to_coldkey_account(&miner_coldkey, stake + ExistentialDeposit::get()); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - step_block(subnet_tempo); - SubnetOwnerCut::::set(0); - // There are two validators and three neurons - MaxAllowedUids::::set(netuid, 3); - SubtensorModule::set_max_allowed_validators(netuid, 2); - - // Setup stakes: - // Stake from validator - // Stake from valiminer - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(validator_coldkey), - validator_hotkey, - netuid, - stake.into() - )); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(validator_miner_coldkey), - validator_miner_hotkey, - netuid, - stake.into() - )); - - // Setup YUMA so that it creates emissions - Weights::::insert(NetUidStorageIndex::from(netuid), 0, vec![(1, 0xFFFF)]); - Weights::::insert(NetUidStorageIndex::from(netuid), 1, vec![(2, 0xFFFF)]); - BlockAtRegistration::::set(netuid, 0, 1); - BlockAtRegistration::::set(netuid, 1, 1); - BlockAtRegistration::::set(netuid, 2, 1); - LastUpdate::::set(NetUidStorageIndex::from(netuid), vec![2, 2, 2]); - Kappa::::set(netuid, u16::MAX / 5); - ActivityCutoff::::set(netuid, u16::MAX); // makes all stake active - ValidatorPermit::::insert(netuid, vec![true, true, false]); - - // Run run_coinbase until emissions are drained - let validator_stake_before = - SubtensorModule::get_total_stake_for_coldkey(&validator_coldkey); - let valiminer_stake_before = - SubtensorModule::get_total_stake_for_coldkey(&validator_miner_coldkey); - let miner_stake_before = SubtensorModule::get_total_stake_for_coldkey(&miner_coldkey); - - step_block(subnet_tempo); - - // Verify how emission is split between keys - // - Owner cut is zero => 50% goes to miners and 50% goes to validators - // - Validator gets 25% because there are two validators - // - Valiminer gets 25% as a validator and 25% as miner - // - Miner gets 25% as miner - let validator_emission = SubtensorModule::get_total_stake_for_coldkey(&validator_coldkey) - - validator_stake_before; - let valiminer_emission = - SubtensorModule::get_total_stake_for_coldkey(&validator_miner_coldkey) - - valiminer_stake_before; - let miner_emission = - SubtensorModule::get_total_stake_for_coldkey(&miner_coldkey) - miner_stake_before; - let total_emission = validator_emission + valiminer_emission + miner_emission; - - assert_abs_diff_eq!( - validator_emission, - total_emission / 4.into(), - epsilon = 10.into() - ); - assert_abs_diff_eq!( - valiminer_emission, - total_emission / 2.into(), - epsilon = 10.into() - ); - assert_abs_diff_eq!( - miner_emission, - total_emission / 4.into(), - epsilon = 10.into() - ); - }); -} - -// Verify staking too low amount is impossible -#[test] -fn test_staking_too_little_fails() { - new_test_ext(1).execute_with(|| { - let hotkey_account_id = U256::from(533453); - let coldkey_account_id = U256::from(55453); - let amount = 10_000; - - //add network - let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); - - // Give it some $$$ in his coldkey balance - add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); - - // Coldkey / hotkey 0 decreases take to 5%. This should fail as the minimum take is 9% - assert_err!( - SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - 1.into() - ), - Error::::AmountTooLow - ); - }); -} - -// cargo test --package pallet-subtensor --lib -- tests::staking::test_add_stake_fee_goes_to_subnet_tao --exact --show-output --nocapture -#[ignore = "fee now goes to liquidity provider"] -#[test] -fn test_add_stake_fee_goes_to_subnet_tao() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let hotkey = U256::from(2); - let coldkey = U256::from(3); - let existential_deposit = ExistentialDeposit::get(); - let tao_to_stake = DefaultMinStake::::get() * 10.into(); - - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - let _ = SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); - let subnet_tao_before = SubnetTAO::::get(netuid); - - // Add stake - add_balance_to_coldkey_account(&coldkey, tao_to_stake); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - tao_to_stake - )); - - // Calculate expected stake - let expected_alpha = AlphaBalance::from((tao_to_stake - existential_deposit).to_u64()); - let actual_alpha = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); - let subnet_tao_after = SubnetTAO::::get(netuid); - - // Total subnet stake should match the sum of delegators' stakes minus existential deposits. - assert_abs_diff_eq!( - actual_alpha, - expected_alpha, - epsilon = expected_alpha / 1000.into() - ); - - // Subnet TAO should have increased by the full tao_to_stake amount - assert_abs_diff_eq!( - subnet_tao_before + tao_to_stake, - subnet_tao_after, - epsilon = 10.into() - ); - }); -} - -// cargo test --package pallet-subtensor --lib -- tests::staking::test_remove_stake_fee_goes_to_subnet_tao --exact --show-output --nocapture -#[ignore = "fees no go to liquidity providers"] -#[test] -fn test_remove_stake_fee_goes_to_subnet_tao() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let hotkey = U256::from(2); - let coldkey = U256::from(3); - let tao_to_stake = DefaultMinStake::::get() * 10.into(); - - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - let _ = SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); - let subnet_tao_before = SubnetTAO::::get(netuid); - - // Add stake - add_balance_to_coldkey_account(&coldkey, tao_to_stake.into()); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - tao_to_stake - )); - - // Remove all stake - let alpha_to_unstake = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); - assert_ok!(SubtensorModule::remove_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - alpha_to_unstake - )); - let subnet_tao_after = SubnetTAO::::get(netuid); - - // Subnet TAO should have increased by 2x fee as a result of staking + unstaking - assert_abs_diff_eq!( - subnet_tao_before, - subnet_tao_after, - epsilon = (alpha_to_unstake.to_u64() / 1000).into() - ); - - // User balance should decrease by 2x fee as a result of staking + unstaking - let balance_after = SubtensorModule::get_coldkey_balance(&coldkey); - assert_abs_diff_eq!( - balance_after, - tao_to_stake, - epsilon = tao_to_stake / 1000.into() - ); - }); -} - -// cargo test --package pallet-subtensor --lib -- tests::staking::test_remove_stake_fee_realistic_values --exact --show-output --nocapture -#[ignore = "fees are now calculated on the SwapInterface side"] -#[test] -fn test_remove_stake_fee_realistic_values() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let hotkey = U256::from(2); - let coldkey = U256::from(3); - let alpha_to_unstake = AlphaBalance::from(111_180_000_000_u64); - let alpha_divs = AlphaBalance::from(2_816_190); - - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - let _ = SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); - - // Mock a realistic scenario: - // Subnet 1 has 3896 TAO and 128_011 Alpha in reserves, which - // makes its price ~0.03. - // A hotkey has 111 Alpha stake and is unstaking all Alpha. - // Alpha dividends of this hotkey are ~0.0028 - // This makes fee be equal ~0.0028 Alpha ~= 84000 rao - let tao_reserve = 3_896_056_559_708_u64; - let alpha_in = 128_011_331_299_964_u64; - mock::setup_reserves(netuid, tao_reserve.into(), alpha_in.into()); - AlphaDividendsPerSubnet::::insert(netuid, hotkey, alpha_divs); - TotalHotkeyAlphaLastEpoch::::insert(hotkey, netuid, alpha_to_unstake); - - // Add stake first time to init TotalHotkeyAlpha - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - netuid, - alpha_to_unstake, - ); - - // Remove stake to measure fee - let balance_before = SubtensorModule::get_coldkey_balance(&coldkey); - let (expected_tao, expected_fee) = mock::swap_alpha_to_tao(netuid, alpha_to_unstake); - - assert_ok!(SubtensorModule::remove_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - alpha_to_unstake - )); - - // Calculate expected fee - let balance_after = SubtensorModule::get_coldkey_balance(&coldkey); - // FIXME since fee is calculated by SwapInterface and the values here are after fees, the - // actual_fee is 0. but it's left here to discuss in review - let actual_fee = expected_tao - (balance_after - balance_before); - log::info!("Actual fee: {actual_fee:?}"); - - assert_abs_diff_eq!( - actual_fee, - expected_fee.into(), - epsilon = (expected_fee / 1000).into() - ); - }); -} - -#[test] -fn test_stake_overflow() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let coldkey_account_id = U256::from(435445); - let hotkey_account_id = U256::from(54544); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); - - // Maximum possible: Max TAO supply less already-issued balance. - let amount = 21_000_000_000_000_000_u64 - u64::from(Balances::total_issuance()); - - // Give it some $$$ in his coldkey balance - add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); - - // Setup liquidity with 21M TAO values - mock::setup_reserves(netuid, amount.into(), amount.into()); - - let total_stake_before = SubtensorModule::get_total_stake(); - - // Stake and check if the result is ok - let (expected_alpha, _) = mock::swap_tao_to_alpha(netuid, amount.into()); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - amount.into() - )); - - // Check if stake has increased properly - assert_abs_diff_eq!( - SubtensorModule::get_stake_for_hotkey_on_subnet(&hotkey_account_id, netuid), - expected_alpha, - epsilon = 1.into() - ); - - // Check if total stake has increased accordingly. - assert_abs_diff_eq!( - SubtensorModule::get_total_stake(), - total_stake_before + amount.into(), - epsilon = 1.into() - ); - }); -} - -#[test] -fn test_max_amount_add_root() { - new_test_ext(0).execute_with(|| { - // 0 price on root => max is 0 - assert_eq!( - SubtensorModule::get_max_amount_add(NetUid::ROOT, TaoBalance::ZERO), - Ok(0u64.into()) - ); - - // 0.999999... price on root => max is 0 - assert_eq!( - SubtensorModule::get_max_amount_add(NetUid::ROOT, TaoBalance::from(999_999_999)), - Ok(0u64.into()) - ); - - // 1.0 price on root => max is u64::MAX - assert_eq!( - SubtensorModule::get_max_amount_add(NetUid::ROOT, TaoBalance::from(1_000_000_000)), - Ok(u64::MAX) - ); - - // 1.000...001 price on root => max is u64::MAX - assert_eq!( - SubtensorModule::get_max_amount_add(NetUid::ROOT, TaoBalance::from(1_000_000_001)), - Ok(u64::MAX) - ); - - // 2.0 price on root => max is u64::MAX - assert_eq!( - SubtensorModule::get_max_amount_add(NetUid::ROOT, TaoBalance::from(2_000_000_000)), - Ok(u64::MAX) - ); - }); -} - -#[test] -fn test_max_amount_add_stable() { - new_test_ext(0).execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - - // 0 price => max is 0 - assert_eq!( - SubtensorModule::get_max_amount_add(netuid, TaoBalance::ZERO), - Ok(0u64.into()) - ); - - // 0.999999... price => max is 0 - assert_eq!( - SubtensorModule::get_max_amount_add(netuid, TaoBalance::from(999_999_999)), - Ok(0u64.into()) - ); - - // 1.0 price => max is u64::MAX - assert_eq!( - SubtensorModule::get_max_amount_add(netuid, TaoBalance::from(1_000_000_000)), - Ok(u64::MAX) - ); - - // 1.000...001 price => max is u64::MAX - assert_eq!( - SubtensorModule::get_max_amount_add(netuid, TaoBalance::from(1_000_000_001)), - Ok(u64::MAX) - ); - - // 2.0 price => max is u64::MAX - assert_eq!( - SubtensorModule::get_max_amount_add(netuid, TaoBalance::from(2_000_000_000)), - Ok(u64::MAX) - ); - }); -} - -// cargo test --package pallet-subtensor --lib -- tests::staking::test_max_amount_add_dynamic --exact --show-output -#[test] -fn test_max_amount_add_dynamic() { - // tao_in, alpha_in, limit_price, expected_max_swappable (with 0.05% fees) - [ - // Zero handling (no panics) - ( - 1_000_000_000, - 1_000_000_000, - 0, - Err(DispatchError::from( - pallet_subtensor_swap::Error::::PriceLimitExceeded, - )), - ), - // Low bounds - (100, 100, 1_100_000_000, Ok(4)), - (1_000, 1_000, 1_100_000_000, Ok(48)), - (10_000, 10_000, 1_100_000_000, Ok(488)), - // Basic math - (1_000_000, 1_000_000, 4_000_000_000, Ok(1_000_500)), - (1_000_000, 1_000_000, 9_000_000_000, Ok(2_001_000)), - (1_000_000, 1_000_000, 16_000_000_000, Ok(3_001_500)), - ( - 1_000_000_000_000, - 1_000_000_000_000, - 16_000_000_000, - Ok(3_001_500_000_000), - ), - // Normal range values with edge cases - ( - 150_000_000_000, - 100_000_000_000, - 0, - Err(DispatchError::from( - pallet_subtensor_swap::Error::::PriceLimitExceeded, - )), - ), - ( - 150_000_000_000, - 100_000_000_000, - 100_000_000, - Err(DispatchError::from( - pallet_subtensor_swap::Error::::PriceLimitExceeded, - )), - ), - ( - 150_000_000_000, - 100_000_000_000, - 500_000_000, - Err(DispatchError::from( - pallet_subtensor_swap::Error::::PriceLimitExceeded, - )), - ), - ( - 150_000_000_000, - 100_000_000_000, - 1_499_999_999, - Err(DispatchError::from( - pallet_subtensor_swap::Error::::PriceLimitExceeded, - )), - ), - ( - 150_000_000_000, - 100_000_000_000, - 1_500_000_000, - Err(DispatchError::from( - pallet_subtensor_swap::Error::::PriceLimitExceeded, - )), - ), - (150_000_000_000, 100_000_000_000, 1_500_000_001, Ok(49)), - ( - 150_000_000_000, - 100_000_000_000, - 6_000_000_000, - Ok(150_075_000_000), - ), - // Miscellaneous overflows and underflows - (u64::MAX / 2, u64::MAX, u64::MAX, Ok(u64::MAX)), - ] - .into_iter() - .for_each(|(tao_in, alpha_in, limit_price, expected_max_swappable)| { - new_test_ext(0).execute_with(|| { - let alpha_in = AlphaBalance::from(alpha_in); - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - // Forse-set alpha in and tao reserve to achieve relative price of subnets - SubnetTAO::::insert(netuid, TaoBalance::from(tao_in)); - SubnetAlphaIn::::insert(netuid, alpha_in); - - // Force the swap to initialize - ::SwapInterface::init_swap(netuid, None); - - if !alpha_in.is_zero() { - let expected_price = U96F32::from_num(tao_in) / U96F32::from_num(alpha_in); - assert_abs_diff_eq!( - ::SwapInterface::current_alpha_price(netuid.into()) - .to_num::(), - expected_price.to_num::(), - epsilon = expected_price.to_num::() / 1_000_f64 - ); - } - - match expected_max_swappable { - Err(e) => assert_err!( - SubtensorModule::get_max_amount_add(netuid, limit_price.into()), - e - ), - Ok(v) => assert_abs_diff_eq!( - SubtensorModule::get_max_amount_add(netuid, limit_price.into()).unwrap(), - v, - epsilon = v / 10000 - ), - } - }); - }); -} - -#[test] -fn test_max_amount_remove_root() { - new_test_ext(0).execute_with(|| { - // 0 price on root => max is u64::MAX - assert_eq!( - SubtensorModule::get_max_amount_remove(NetUid::ROOT, TaoBalance::ZERO), - Ok(AlphaBalance::MAX) - ); - - // 0.5 price on root => max is u64::MAX - assert_eq!( - SubtensorModule::get_max_amount_remove(NetUid::ROOT, TaoBalance::from(500_000_000)), - Ok(AlphaBalance::MAX) - ); - - // 0.999999... price on root => max is u64::MAX - assert_eq!( - SubtensorModule::get_max_amount_remove(NetUid::ROOT, TaoBalance::from(999_999_999)), - Ok(AlphaBalance::MAX) - ); - - // 1.0 price on root => max is u64::MAX - assert_eq!( - SubtensorModule::get_max_amount_remove(NetUid::ROOT, TaoBalance::from(1_000_000_000)), - Ok(AlphaBalance::MAX) - ); - - // 1.000...001 price on root => max is 0 - assert_eq!( - SubtensorModule::get_max_amount_remove(NetUid::ROOT, TaoBalance::from(1_000_000_001)), - Ok(0u64.into()) - ); - - // 2.0 price on root => max is 0 - assert_eq!( - SubtensorModule::get_max_amount_remove(NetUid::ROOT, TaoBalance::from(2_000_000_000)), - Ok(0u64.into()) - ); - }); -} - -#[test] -fn test_max_amount_remove_stable() { - new_test_ext(0).execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - - // 0 price => max is u64::MAX - assert_eq!( - SubtensorModule::get_max_amount_remove(netuid, TaoBalance::ZERO), - Ok(AlphaBalance::MAX) - ); - - // 0.999999... price => max is u64::MAX - assert_eq!( - SubtensorModule::get_max_amount_remove(netuid, TaoBalance::from(999_999_999)), - Ok(AlphaBalance::MAX) - ); - - // 1.0 price => max is u64::MAX - assert_eq!( - SubtensorModule::get_max_amount_remove(netuid, TaoBalance::from(1_000_000_000)), - Ok(AlphaBalance::MAX) - ); - - // 1.000...001 price => max is 0 - assert_eq!( - SubtensorModule::get_max_amount_remove(netuid, TaoBalance::from(1_000_000_001)), - Ok(0u64.into()) - ); - - // 2.0 price => max is 0 - assert_eq!( - SubtensorModule::get_max_amount_remove(netuid, TaoBalance::from(2_000_000_000)), - Ok(0u64.into()) - ); - }); -} - -// cargo test --package pallet-subtensor --lib -- tests::staking::test_max_amount_remove_dynamic --exact --show-output -#[test] -fn test_max_amount_remove_dynamic() { - new_test_ext(0).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - // tao_in, alpha_in, limit_price, expected_max_swappable (+ 0.05% fee) - [ - // Zero handling (no panics) - ( - 0_u64, - 1_000_000_000_u64, - 100, - Err(DispatchError::from( - pallet_subtensor_swap::Error::::ReservesTooLow, - )), - ), - ( - 1_000_000_000, - 0, - 100, - Err(DispatchError::from( - pallet_subtensor_swap::Error::::PriceLimitExceeded, - )), - ), - (10_000_000_000, 10_000_000_000, 0, Ok(10_000_000_000_000)), - // Low bounds (numbers are empirical, it is only important that result - // is sharply decreasing when limit price increases) - (1_000, 1_000, 0, Ok(1_000_000)), - (1_001, 1_001, 0, Ok(1_001_000)), - (1_001, 1_001, 1, Ok(1_001_000)), - (1_001, 1_001, 2, Ok(1_001_000)), - (1_001, 1_001, 1_001, Ok(1_001_000)), - (1_001, 1_001, 10_000, Ok(17_472)), - (1_001, 1_001, 100_000, Ok(17_472)), - (1_001, 1_001, 1_000_000, Ok(17_472)), - (1_001, 1_001, 10_000_000, Ok(9_013)), - (1_001, 1_001, 100_000_000, Ok(2_165)), - // Basic math - (1_000_000, 1_000_000, 250_000_000, Ok(1_010_000)), - (1_000_000, 1_000_000, 62_500_000, Ok(3_030_000)), - ( - 1_000_000_000_000, - 1_000_000_000_000, - 62_500_000, - Ok(3_030_000_000_000), - ), - // Normal range values with edge cases and sanity checks - (200_000_000_000, 100_000_000_000, 0, Ok(100_000_000_000_000)), - ( - 200_000_000_000, - 100_000_000_000, - 500_000_000, - Ok(101_000_000_000), - ), - ( - 200_000_000_000, - 100_000_000_000, - 125_000_000, - Ok(303_000_000_000), - ), - ( - 200_000_000_000, - 100_000_000_000, - 2_000_000_000, - Err(DispatchError::from( - pallet_subtensor_swap::Error::::PriceLimitExceeded, - )), - ), - ( - 200_000_000_000, - 100_000_000_000, - 2_000_000_001, - Err(DispatchError::from( - pallet_subtensor_swap::Error::::PriceLimitExceeded, - )), - ), - (200_000_000_000, 100_000_000_000, 1_999_999_999, Ok(24)), - (200_000_000_000, 100_000_000_000, 1_999_999_990, Ok(250)), - // Miscellaneous overflows and underflows - ( - 21_000_000_000_000_000, - 1_000_000, - 21_000_000_000_000_000, - Ok(17_455_533), - ), - (21_000_000_000_000_000, 1_000_000, u64::MAX, Ok(67_000)), - ( - 21_000_000_000_000_000, - 1_000_000_000_000_000_000, - u64::MAX, - Err(DispatchError::from( - pallet_subtensor_swap::Error::::PriceLimitExceeded, - )), - ), - ( - 21_000_000_000_000_000, - 1_000_000_000_000_000_000, - 20_000_000, - Ok(24_700_000_000_000_000), - ), - ( - 21_000_000_000_000_000, - 21_000_000_000_000_000, - 999_999_999, - Ok(10_605_000), - ), - ( - 21_000_000_000_000_000, - 21_000_000_000_000_000, - 0, - Ok(u64::MAX), - ), - ] - .into_iter() - .for_each(|(tao_in, alpha_in, limit_price, expected_max_swappable)| { - let alpha_in = AlphaBalance::from(alpha_in); - // Forse-set alpha in and tao reserve to achieve relative price of subnets - SubnetTAO::::insert(netuid, TaoBalance::from(tao_in)); - SubnetAlphaIn::::insert(netuid, alpha_in); - - if !alpha_in.is_zero() { - let expected_price = U64F64::from_num(tao_in) / U64F64::from_num(alpha_in); - assert_eq!( - ::SwapInterface::current_alpha_price(netuid.into()), - expected_price - ); - } - - match expected_max_swappable { - Err(e) => assert_err!( - SubtensorModule::get_max_amount_remove(netuid, limit_price.into()), - DispatchError::from(e) - ), - Ok(v) => { - let v = AlphaBalance::from(v); - let actual = - SubtensorModule::get_max_amount_remove(netuid, limit_price.into()).unwrap(); - let epsilon = v / 100.into(); - let diff = actual.max(v).saturating_sub(actual.min(v)); - assert!( - diff <= epsilon, - "max remove mismatch: tao_in={tao_in}, alpha_in={alpha_in:?}, limit_price={limit_price}, actual={actual:?}, expected={v:?}, epsilon={epsilon:?}", - ); - } - } - }); - }); -} - -// cargo test --package pallet-subtensor --lib -- tests::staking::test_max_amount_move_root_root --exact --show-output -#[test] -fn test_max_amount_move_root_root() { - new_test_ext(0).execute_with(|| { - // 0 price on (root, root) exchange => max is u64::MAX - assert_eq!( - SubtensorModule::get_max_amount_move(NetUid::ROOT, NetUid::ROOT, TaoBalance::ZERO), - Ok(AlphaBalance::MAX) - ); - - // 0.5 price on (root, root) => max is u64::MAX - assert_eq!( - SubtensorModule::get_max_amount_move( - NetUid::ROOT, - NetUid::ROOT, - TaoBalance::from(500_000_000) - ), - Ok(AlphaBalance::MAX) - ); - - // 0.999999... price on (root, root) => max is u64::MAX - assert_eq!( - SubtensorModule::get_max_amount_move( - NetUid::ROOT, - NetUid::ROOT, - TaoBalance::from(999_999_999) - ), - Ok(AlphaBalance::MAX) - ); - - // 1.0 price on (root, root) => max is u64::MAX - assert_eq!( - SubtensorModule::get_max_amount_move( - NetUid::ROOT, - NetUid::ROOT, - TaoBalance::from(1_000_000_000) - ), - Ok(AlphaBalance::MAX) - ); - - // 1.000...001 price on (root, root) => max is 0 - assert_eq!( - SubtensorModule::get_max_amount_move( - NetUid::ROOT, - NetUid::ROOT, - TaoBalance::from(1_000_000_001) - ), - Ok(0u64.into()) - ); - - // 2.0 price on (root, root) => max is 0 - assert_eq!( - SubtensorModule::get_max_amount_move( - NetUid::ROOT, - NetUid::ROOT, - TaoBalance::from(2_000_000_000) - ), - Ok(0u64.into()) - ); - }); -} - -// cargo test --package pallet-subtensor --lib -- tests::staking::test_max_amount_move_root_stable --exact --show-output -#[test] -fn test_max_amount_move_root_stable() { - new_test_ext(0).execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - - // 0 price on (root, stable) exchange => max is u64::MAX - assert_eq!( - SubtensorModule::get_max_amount_move(NetUid::ROOT, netuid, TaoBalance::ZERO), - Ok(AlphaBalance::MAX) - ); - - // 0.5 price on (root, stable) => max is u64::MAX - assert_eq!( - SubtensorModule::get_max_amount_move( - NetUid::ROOT, - netuid, - TaoBalance::from(500_000_000) - ), - Ok(AlphaBalance::MAX) - ); - - // 0.999999... price on (root, stable) => max is u64::MAX - assert_eq!( - SubtensorModule::get_max_amount_move( - NetUid::ROOT, - netuid, - TaoBalance::from(999_999_999) - ), - Ok(AlphaBalance::MAX) - ); - - // 1.0 price on (root, stable) => max is u64::MAX - assert_eq!( - SubtensorModule::get_max_amount_move( - NetUid::ROOT, - netuid, - TaoBalance::from(1_000_000_000) - ), - Ok(AlphaBalance::MAX) - ); - - // 1.000...001 price on (root, stable) => max is 0 - assert_eq!( - SubtensorModule::get_max_amount_move( - NetUid::ROOT, - netuid, - TaoBalance::from(1_000_000_001) - ), - Ok(0u64.into()) - ); - - // 2.0 price on (root, stable) => max is 0 - assert_eq!( - SubtensorModule::get_max_amount_move( - NetUid::ROOT, - netuid, - TaoBalance::from(2_000_000_000) - ), - Ok(0u64.into()) - ); - }); -} - -// cargo test --package pallet-subtensor --lib -- tests::staking::test_max_amount_move_stable_dynamic --exact --show-output -#[test] -fn test_max_amount_move_stable_dynamic() { - new_test_ext(0).execute_with(|| { - // Add stable subnet - let stable_netuid = NetUid::from(1); - add_network(stable_netuid, 1, 0); - - // Add dynamic subnet - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let dynamic_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - // Force-set alpha in and tao reserve to make price equal 0.5 - let tao_reserve = TaoBalance::from(50_000_000_000_u64); - let alpha_in = AlphaBalance::from(100_000_000_000_u64); - SubnetTAO::::insert(dynamic_netuid, tao_reserve); - SubnetAlphaIn::::insert(dynamic_netuid, alpha_in); - let current_price = - ::SwapInterface::current_alpha_price(dynamic_netuid.into()); - assert_eq!(current_price, U96F32::from_num(0.5)); - - // The tests below just mimic the add_stake_limit tests for reverted price - - // 0 price => max is u64::MAX - assert_eq!( - SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, TaoBalance::ZERO), - Ok(AlphaBalance::MAX) - ); - - // 2.0 price => max is 0 - assert_eq!( - SubtensorModule::get_max_amount_move( - stable_netuid, - dynamic_netuid, - TaoBalance::from(2_000_000_000) - ), - Err(pallet_subtensor_swap::Error::::PriceLimitExceeded.into()) - ); - - // 3.0 price => max is 0 - assert_eq!( - SubtensorModule::get_max_amount_move( - stable_netuid, - dynamic_netuid, - TaoBalance::from(3_000_000_000_u64) - ), - Err(pallet_subtensor_swap::Error::::PriceLimitExceeded.into()) - ); - - // 2x price => max is 1x TAO - assert_abs_diff_eq!( - SubtensorModule::get_max_amount_move( - stable_netuid, - dynamic_netuid, - TaoBalance::from(500_000_000) - ) - .unwrap(), - AlphaBalance::from(tao_reserve.to_u64() + (tao_reserve.to_u64() as f64 * 0.003) as u64), - epsilon = AlphaBalance::from(tao_reserve.to_u64() / 100), - ); - - // Precision test: - // 1.99999..9000 price => max > 0 - assert!( - SubtensorModule::get_max_amount_move( - stable_netuid, - dynamic_netuid, - TaoBalance::from(1_999_999_000) - ) - .unwrap() - > AlphaBalance::ZERO - ); - - // Max price doesn't panic and returns something meaningful - assert_eq!( - SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, TaoBalance::MAX), - Err(pallet_subtensor_swap::Error::::PriceLimitExceeded.into()) - ); - assert_eq!( - SubtensorModule::get_max_amount_move( - stable_netuid, - dynamic_netuid, - TaoBalance::MAX - 1.into() - ), - Err(pallet_subtensor_swap::Error::::PriceLimitExceeded.into()) - ); - assert_eq!( - SubtensorModule::get_max_amount_move( - stable_netuid, - dynamic_netuid, - TaoBalance::MAX / 2.into() - ), - Err(pallet_subtensor_swap::Error::::PriceLimitExceeded.into()) - ); - }); -} - -// cargo test --package pallet-subtensor --lib -- tests::staking::test_max_amount_move_dynamic_stable --exact --show-output -#[test] -fn test_max_amount_move_dynamic_stable() { - new_test_ext(0).execute_with(|| { - // Add stable subnet - let stable_netuid = NetUid::from(1); - add_network(stable_netuid, 1, 0); - - // Add dynamic subnet - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let dynamic_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - // Forse-set alpha in and tao reserve to make price equal 1.5 - let tao_reserve = TaoBalance::from(150_000_000_000_u64); - let alpha_in = AlphaBalance::from(100_000_000_000_u64); - SubnetTAO::::insert(dynamic_netuid, tao_reserve); - SubnetAlphaIn::::insert(dynamic_netuid, alpha_in); - let current_price = - ::SwapInterface::current_alpha_price(dynamic_netuid.into()); - assert_eq!(current_price, U96F32::from_num(1.5)); - - // The tests below just mimic the remove_stake_limit tests - - // 0 price => max is capped at 1000x input reserve - assert_eq!( - SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, TaoBalance::ZERO), - Ok(alpha_in.saturating_mul(1_000.into())) - ); - - // Low price values don't blow things up - assert!( - SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, 1.into()).unwrap() - > AlphaBalance::ZERO - ); - assert!( - SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, 2.into()).unwrap() - > AlphaBalance::ZERO - ); - assert!( - SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, 3.into()).unwrap() - > AlphaBalance::ZERO - ); - - // 1.5000...1 price => max is 0 - assert_eq!( - SubtensorModule::get_max_amount_move( - dynamic_netuid, - stable_netuid, - 1_500_000_001.into() - ), - Err(pallet_subtensor_swap::Error::::PriceLimitExceeded.into()) - ); - - // 1.5 price => max is 0 because of non-zero slippage - assert_abs_diff_eq!( - SubtensorModule::get_max_amount_move( - dynamic_netuid, - stable_netuid, - 1_500_000_000.into() - ) - .unwrap_or(AlphaBalance::ZERO), - AlphaBalance::ZERO, - epsilon = 10_000.into() - ); - - // 1/4 price => max is 1x Alpha - assert_abs_diff_eq!( - SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, 375_000_000.into()) - .unwrap(), - alpha_in + alpha_in / 2000.into(), // + 0.05% fee - epsilon = alpha_in / 10_000.into(), - ); - - // Precision test: - // 1.499999.. price => max > 0 - assert!( - SubtensorModule::get_max_amount_move( - dynamic_netuid, - stable_netuid, - 1_499_999_999.into() - ) - .unwrap() - > AlphaBalance::ZERO - ); - - // Max price doesn't panic and returns something meaningful - assert!( - SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, TaoBalance::MAX) - .unwrap_or(AlphaBalance::ZERO) - < 21_000_000_000_000_000_u64.into() - ); - assert!( - SubtensorModule::get_max_amount_move( - dynamic_netuid, - stable_netuid, - TaoBalance::MAX - 1.into() - ) - .unwrap_or(AlphaBalance::ZERO) - < 21_000_000_000_000_000_u64.into() - ); - assert!( - SubtensorModule::get_max_amount_move( - dynamic_netuid, - stable_netuid, - TaoBalance::MAX / 2.into() - ) - .unwrap_or(AlphaBalance::ZERO) - < 21_000_000_000_000_000_u64.into() - ); - }); -} - -// cargo test --package pallet-subtensor --lib -- tests::staking::test_max_amount_move_dynamic_dynamic --exact --show-output -#[test] -fn test_max_amount_move_dynamic_dynamic() { - new_test_ext(0).execute_with(|| { - // Add two dynamic subnets - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let origin_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - let destination_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - // Test cases are generated with help with this limit-staking calculator: - // https://docs.google.com/spreadsheets/d/1pfU-PVycd3I4DbJIc0GjtPohy4CbhdV6CWqgiy__jKE - // This is for reference only; verify before use. - // - // CSV backup for this spreadhsheet: - // - // SubnetTAO 1,AlphaIn 1,SubnetTAO 2,AlphaIn 2,,initial price,limit price,max swappable - // 150,100,100,100,,=(A2/B2)/(C2/D2),0.1,=(D2*A2-B2*C2*G2)/(G2*(A2+C2)) - // - // tao_in_1, alpha_in_1, tao_in_2, alpha_in_2, limit_price, expected_max_swappable, precision - [ - // Zero handling (no panics) - ( - 0_u64, - 1_000_000_000_u64, - 1_000_000_000_u64, - 1_000_000_000_u64, - 100, - 0, - 1_u64, - ), - (1_000_000_000, 0, 1_000_000_000, 1_000_000_000, 100, 0, 1), - (1_000_000_000, 1_000_000_000, 0, 1_000_000_000, 100, 0, 1), - (1_000_000_000, 1_000_000_000, 1_000_000_000, 0, 100, 0, 1), - // Low bounds - (1, 1, 1, 1, 0, u64::MAX, 1), - (1, 1, 1, 1, 1, 500_000_000, 1), - (1, 1, 1, 1, 2, 250_000_000, 1), - (1, 1, 1, 1, 3, 166_666_666, 1), - (1, 1, 1, 1, 4, 125_000_000, 1), - (1, 1, 1, 1, 1_000, 500_000, 1), - // Basic math - (1_000, 1_000, 1_000, 1_000, 500_000_000, 500, 1), - (1_000, 1_000, 1_000, 1_000, 100_000_000, 4_500, 1), - // Normal range values edge cases - ( - 150_000_000_000, - 100_000_000_000, - 100_000_000_000, - 100_000_000_000, - 100_000_000, - 560_000_000_000, - 1_000_000, - ), - ( - 150_000_000_000, - 100_000_000_000, - 100_000_000_000, - 100_000_000_000, - 500_000_000, - 80_000_000_000, - 1_000_000, - ), - ( - 150_000_000_000, - 100_000_000_000, - 100_000_000_000, - 100_000_000_000, - 750_000_000, - 40_000_000_000, - 1_000_000, - ), - ( - 150_000_000_000, - 100_000_000_000, - 100_000_000_000, - 100_000_000_000, - 1_000_000_000, - 20_000_000_000, - 1_000, - ), - ( - 150_000_000_000, - 100_000_000_000, - 100_000_000_000, - 100_000_000_000, - 1_250_000_000, - 8_000_000_000, - 1_000, - ), - ( - 150_000_000_000, - 100_000_000_000, - 100_000_000_000, - 100_000_000_000, - 1_499_999_999, - 27, - 1, - ), - ( - 150_000_000_000, - 100_000_000_000, - 100_000_000_000, - 100_000_000_000, - 1_500_000_000, - 0, - 1, - ), - ( - 150_000_000_000, - 100_000_000_000, - 100_000_000_000, - 100_000_000_000, - 1_500_000_001, - 0, - 1, - ), - ( - 150_000_000_000, - 100_000_000_000, - 100_000_000_000, - 100_000_000_000, - 1_500_001_000, - 0, - 1, - ), - ( - 150_000_000_000, - 100_000_000_000, - 100_000_000_000, - 100_000_000_000, - 2_000_000_000, - 0, - 1, - ), - ( - 150_000_000_000, - 100_000_000_000, - 100_000_000_000, - 100_000_000_000, - u64::MAX, - 0, - 1, - ), - ( - 100_000_000_000, - 200_000_000_000, - 300_000_000_000, - 400_000_000_000, - 500_000_000, - 50_000_000_000, - 1_000, - ), - // Miscellaneous overflows - ( - 1_000_000_000, - 1_000_000_000, - 1_000_000_000, - 1_000_000_000, - 1, - 499_999_999_500_000_000, - 100_000_000, - ), - ( - 1_000_000, - 1_000_000, - 21_000_000_000_000_000, - 1_000_000_000_000_000_000_u64, - 1, - 48_000_000_000_000_000, - 1_000_000_000_000_000, - ), - ( - 150_000_000_000, - 100_000_000_000, - 100_000_000_000, - 100_000_000_000, - u64::MAX, - 0, - 1, - ), - ( - 1_000_000, - 1_000_000, - 21_000_000_000_000_000, - 1_000_000_000_000_000_000_u64, - u64::MAX, - 0, - 1, - ), - ] - .iter() - .for_each( - |&( - tao_in_1, - alpha_in_1, - tao_in_2, - alpha_in_2, - limit_price, - expected_max_swappable, - precision, - )| { - let expected_max_swappable = AlphaBalance::from(expected_max_swappable); - // Forse-set alpha in and tao reserve to achieve relative price of subnets - SubnetTAO::::insert(origin_netuid, TaoBalance::from(tao_in_1)); - SubnetAlphaIn::::insert(origin_netuid, AlphaBalance::from(alpha_in_1)); - SubnetTAO::::insert(destination_netuid, TaoBalance::from(tao_in_2)); - SubnetAlphaIn::::insert(destination_netuid, AlphaBalance::from(alpha_in_2)); - - if !alpha_in_1.is_zero() && !alpha_in_2.is_zero() { - let origin_price = tao_in_1 as f64 / alpha_in_1 as f64; - let dest_price = tao_in_2 as f64 / alpha_in_2 as f64; - if dest_price != 0. { - let expected_price = origin_price / dest_price; - assert_abs_diff_eq!( - (::SwapInterface::current_alpha_price( - origin_netuid.into() - ) / ::SwapInterface::current_alpha_price( - destination_netuid.into() - )) - .to_num::(), - expected_price, - epsilon = 0.000_000_001 - ); - } - } - - assert_abs_diff_eq!( - SubtensorModule::get_max_amount_move( - origin_netuid, - destination_netuid, - limit_price.into() - ) - .unwrap_or(AlphaBalance::ZERO), - expected_max_swappable, - epsilon = precision.into() - ); - }, - ); - }); -} - -#[test] -fn test_add_stake_limit_ok() { - new_test_ext(1).execute_with(|| { - let hotkey_account_id = U256::from(533453); - let coldkey_account_id = U256::from(55453); - let amount = 900_000_000_000; // over the maximum - - // add network - let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); - remove_owner_registration_stake(netuid); - - // Forse-set alpha in and tao reserve to make price equal 1.5 - let tao_reserve = TaoBalance::from(150_000_000_000_u64); - let alpha_in = AlphaBalance::from(100_000_000_000_u64); - mock::setup_reserves(netuid, tao_reserve, alpha_in); - let current_price = - ::SwapInterface::current_alpha_price(netuid.into()); - assert_eq!(current_price, U96F32::from_num(1.5)); - - // Give it some $$$ in his coldkey balance - add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); - - // Setup limit price so that it doesn't peak above 4x of current price - // The amount that can be executed at this price is 450 TAO only - // Alpha produced will be equal to 75 = 450*100/(450+150) - let limit_price = TaoBalance::from(24_000_000_000_u64); - let expected_executed_stake = AlphaBalance::from(75_000_000_000_u64); - - // Add stake with slippage safety and check if the result is ok - assert_ok!(SubtensorModule::add_stake_limit( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - amount.into(), - limit_price, - true - )); - - // Check if stake has increased only by 75 Alpha - assert_abs_diff_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid - ), - expected_executed_stake, - epsilon = expected_executed_stake / 1000.into(), - ); - - // Check that 450 TAO less fees balance still remains free on coldkey - let fee = ::SwapInterface::approx_fee_amount( - netuid.into(), - TaoBalance::from(amount / 2), - ) - .to_u64() as f64; - assert_abs_diff_eq!( - SubtensorModule::get_coldkey_balance(&coldkey_account_id), - (amount / 2 - fee as u64).into(), - epsilon = (amount / 2 / 1000).into() - ); - - // Check that price has updated to ~24 = (150+450) / (100 - 75) - let exp_price = U96F32::from_num(24.0); - let current_price = - ::SwapInterface::current_alpha_price(netuid.into()); - assert_abs_diff_eq!( - exp_price.to_num::(), - current_price.to_num::(), - epsilon = 0.001, - ); - }); -} - -#[test] -fn test_add_stake_limit_fill_or_kill() { - new_test_ext(1).execute_with(|| { - let hotkey_account_id = U256::from(533453); - let coldkey_account_id = U256::from(55453); - let amount = 900_000_000_000_u64; // over the maximum - - // add network - let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); - - // Force-set alpha in and tao reserve to make price equal 1.5 - let tao_reserve = TaoBalance::from(150_000_000_000_u64); - let alpha_in = AlphaBalance::from(100_000_000_000_u64); - SubnetTAO::::insert(netuid, tao_reserve); - SubnetAlphaIn::::insert(netuid, alpha_in); - let current_price = - ::SwapInterface::current_alpha_price(netuid.into()); - // FIXME it's failing because in the swap pallet, the alpha price is set only after an - // initial swap - assert_eq!(current_price, U96F32::from_num(1.5)); - - // Give it some $$$ in his coldkey balance - add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); - - // Setup limit price so that it doesn't peak above 4x of current price - // The amount that can be executed at this price is 450 TAO only - // Alpha produced will be equal to 25 = 100 - 450*100/(150+450) - let limit_price = TaoBalance::from(24_000_000_000_u64); - - // Add stake with slippage safety and check if it fails - assert_noop!( - SubtensorModule::add_stake_limit( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - amount.into(), - limit_price, - false - ), - Error::::SlippageTooHigh - ); - - // Lower the amount and it should succeed now - let amount_ok = TaoBalance::from(150_000_000_000_u64); // fits the maximum - assert_ok!(SubtensorModule::add_stake_limit( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - amount_ok, - limit_price, - false - )); - }); -} - -#[test] -fn test_add_stake_limit_rejects_input_over_swap_reserve_cap() { - new_test_ext(1).execute_with(|| { - let hotkey_account_id = U256::from(533454); - let coldkey_account_id = U256::from(55454); - - let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); - let tao_reserve = TaoBalance::from(1_000_u64); - mock::setup_reserves(netuid, tao_reserve, AlphaBalance::from(1_000_000_000_u64)); - - let amount = tao_reserve.saturating_mul(1_000.into()) + TaoBalance::from(1_u64); - add_balance_to_coldkey_account(&coldkey_account_id, amount); - - assert_noop!( - SubtensorModule::add_stake_limit( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - amount, - ::SwapInterface::max_price(), - true - ), - Error::::InsufficientLiquidity - ); - }); -} - -#[test] -fn test_add_stake_limit_partial_zero_max_stake_amount_error() { - new_test_ext(1).execute_with(|| { - let hotkey_account_id = U256::from(533453); - let coldkey_account_id = U256::from(55453); - - // Exact values from the error: - // https://taostats.io/extrinsic/5338471-0009?network=finney - let amount = 19980000000_u64; - let limit_price = TaoBalance::from(26953618); - let tao_reserve = TaoBalance::from(5_032_494_439_940_u64); - let alpha_in = AlphaBalance::from(186_268_425_402_874_u64); - - let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); - SubnetTAO::::insert(netuid, tao_reserve); - SubnetAlphaIn::::insert(netuid, alpha_in); - - add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); - - assert_noop!( - SubtensorModule::add_stake_limit( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - amount.into(), - limit_price, - true - ), - DispatchError::from(pallet_subtensor_swap::Error::::PriceLimitExceeded) - ); - }); -} - -#[test] -fn test_remove_stake_limit_ok() { - new_test_ext(1).execute_with(|| { - let hotkey_account_id = U256::from(533453); - let coldkey_account_id = U256::from(55453); - let stake_amount = TaoBalance::from(300_000_000_000_u64); - - // add network - let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); - add_balance_to_coldkey_account( - &coldkey_account_id, - stake_amount + ExistentialDeposit::get(), - ); - - // Forse-set sufficient reserves - let tao_reserve = TaoBalance::from(100_000_000_000_u64); - let alpha_in = AlphaBalance::from(100_000_000_000_u64); - SubnetTAO::::insert(netuid, tao_reserve); - SubnetAlphaIn::::insert(netuid, alpha_in); - - // Stake to hotkey account, and check if the result is ok - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - stake_amount - )); - let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - ); - - // Setup limit price to 99% of current price - let current_price = - ::SwapInterface::current_alpha_price(netuid.into()); - let limit_price = (current_price.to_num::() * 990_000_000_f64) as u64; - - // Alpha unstaked - calculated using formula from delta_in() - let expected_alpha_reduction = (0.00138 * (alpha_in.to_u64() as f64)) as u64; - let fee: u64 = (expected_alpha_reduction as f64 * 0.003) as u64; - - // Remove stake with slippage safety - assert_ok!(SubtensorModule::remove_stake_limit( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - alpha_before / 2.into(), - limit_price.into(), - true - )); - let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - ); - - // Check if stake has decreased properly - assert_abs_diff_eq!( - alpha_before - alpha_after, - AlphaBalance::from(expected_alpha_reduction + fee), - epsilon = AlphaBalance::from(expected_alpha_reduction / 10), - ); - }); -} - -#[test] -fn test_remove_stake_limit_fill_or_kill() { - new_test_ext(1).execute_with(|| { - let hotkey_account_id = U256::from(533453); - let coldkey_account_id = U256::from(55453); - let stake_amount = AlphaBalance::from(300_000_000_000_u64); - let unstake_amount = AlphaBalance::from(150_000_000_000_u64); - - // add network - let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); - - // Give the neuron some stake to remove - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - stake_amount, - ); - - // Forse-set alpha in and tao reserve to make price equal 1.5 - let tao_reserve = TaoBalance::from(150_000_000_000_u64); - let alpha_in = AlphaBalance::from(100_000_000_000_u64); - SubnetTAO::::insert(netuid, tao_reserve); - SubnetAlphaIn::::insert(netuid, alpha_in); - let current_price = - ::SwapInterface::current_alpha_price(netuid.into()); - assert_eq!(current_price, U96F32::from_num(1.5)); - - // Setup limit price so that it doesn't drop by more than 10% from current price - let limit_price = TaoBalance::from(1_350_000_000); - - // Remove stake with slippage safety - fails - assert_noop!( - SubtensorModule::remove_stake_limit( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - unstake_amount, - limit_price, - false - ), - Error::::SlippageTooHigh - ); - - // Lower the amount: Should succeed - assert_ok!(SubtensorModule::remove_stake_limit( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - unstake_amount / 100.into(), - limit_price.into(), - false - ),); - }); -} - -#[test] -// RUST_LOG=info cargo test --package pallet-subtensor --lib -- tests::staking::test_add_stake_specific_stake_into_subnet_fail --exact --show-output -fn test_add_stake_specific_stake_into_subnet_fail() { - new_test_ext(1).execute_with(|| { - let sn_owner_coldkey = U256::from(55453); - - let hotkey_account_id = U256::from(533453); - let coldkey_account_id = U256::from(55454); - let hotkey_owner_account_id = U256::from(533454); - - let existing_shares: U64F64 = - U64F64::from_num(161_986_254).saturating_div(U64F64::from_num(u64::MAX)); - let existing_stake = AlphaBalance::from(36_711_495_953_u64); - - let tao_in = TaoBalance::from(2_409_892_148_947_u64); - let alpha_in = AlphaBalance::from(15_358_708_513_716_u64); - - let tao_staked = TaoBalance::from(200_000_000); - - //add network - let netuid = add_dynamic_network(&sn_owner_coldkey, &sn_owner_coldkey); - - // Register hotkey on netuid - register_ok_neuron(netuid, hotkey_account_id, hotkey_owner_account_id, 0); - // Check we have zero staked - assert_eq!( - SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), - TaoBalance::ZERO - ); - - // Set a hotkey pool for the hotkey - let mut hotkey_pool = SubtensorModule::get_alpha_share_pool(hotkey_account_id, netuid); - hotkey_pool.update_value_for_one(&hotkey_owner_account_id, 1234); // Doesn't matter, will be overridden - - // Adjust the total hotkey stake and shares to match the existing values - TotalHotkeyShares::::insert(hotkey_account_id, netuid, existing_shares); - TotalHotkeyAlpha::::insert(hotkey_account_id, netuid, existing_stake); - - // Make the hotkey a delegate - Delegates::::insert(hotkey_account_id, PerU16::zero()); - - // Setup Subnet pool - SubnetAlphaIn::::insert(netuid, alpha_in); - SubnetTAO::::insert(netuid, tao_in); - - // Give TAO balance to coldkey - add_balance_to_coldkey_account(&coldkey_account_id, tao_staked + 1_000_000_000.into()); - - // Add stake as new hotkey - let order = GetAlphaForTao::::with_amount(tao_staked); - let expected_alpha = ::SwapInterface::swap( - netuid.into(), - order, - ::SwapInterface::max_price(), - false, - true, - ) - .map(|v| v.amount_paid_out) - .unwrap_or_default(); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - tao_staked, - )); - - // Check we have non-zero staked - assert!(expected_alpha > AlphaBalance::ZERO); - assert_abs_diff_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid - ), - expected_alpha, - epsilon = expected_alpha / 1000.into() - ); - }); -} - -// cargo test --package pallet-subtensor --lib -- tests::staking::test_remove_99_999_per_cent_stake_works_precisely --exact --show-output -#[test] -fn test_remove_99_9991_per_cent_stake_works_precisely() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1); - let subnet_owner_hotkey = U256::from(2); - let hotkey_account_id = U256::from(581337); - let coldkey_account_id = U256::from(81337); - let amount = 10_000_000_000_u64; - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); - - // Set fee rate to 0 so that alpha fee is not moved to block producer. - pallet_subtensor_swap::FeeRate::::insert(netuid, 0); - - // Give it some $$$ in his coldkey balance (in addition to any leftover buffer from registration) - add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); - - // Stake to hotkey account. - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - amount.into() - )); - - // Remove 99.9991% stake. - let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - ); - let coldkey_balance_before_remove = - SubtensorModule::get_coldkey_balance(&coldkey_account_id); - - let remove_amount = AlphaBalance::from( - (U64F64::from_num(alpha) * U64F64::from_num(0.999991)).to_num::(), - ); - - // Expected TAO returned by swapping exactly the removed alpha. - let (expected_returned_balance, _) = mock::swap_alpha_to_tao(netuid, remove_amount); - - assert_ok!(SubtensorModule::remove_stake( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - remove_amount, - )); - - // Compare the returned delta, not the absolute coldkey balance, because - // registration / staking can leave a small pre-existing balance on coldkey. - let coldkey_balance_after_remove = - SubtensorModule::get_coldkey_balance(&coldkey_account_id); - let actual_returned_balance = TaoBalance::from( - coldkey_balance_after_remove - .to_u64() - .saturating_sub(coldkey_balance_before_remove.to_u64()), - ); - - assert_abs_diff_eq!( - actual_returned_balance, - expected_returned_balance, - epsilon = 10.into(), - ); - - assert!(!SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id).is_zero()); - - let new_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - ); - assert_eq!(new_alpha, alpha - remove_amount); - }); -} - -// cargo test --package pallet-subtensor --lib -- tests::staking::test_remove_99_9989_per_cent_stake_leaves_a_little --exact --show-output -#[test] -fn test_remove_99_9989_per_cent_stake_leaves_a_little() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1); - let subnet_owner_hotkey = U256::from(2); - let hotkey_account_id = U256::from(581337); - let coldkey_account_id = U256::from(81337); - let amount = 10_000_000_000; - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); - - // Set fee rate to 0 so that alpha fee is not moved to block producer - // to avoid false success in this test - pallet_subtensor_swap::FeeRate::::insert(netuid, 0); - - // Give it some $$$ in his coldkey balance - add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); - - // Stake to hotkey account, and check if the result is ok - let (_, fee) = mock::swap_tao_to_alpha(netuid, amount.into()); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - amount.into() - )); - - // Remove 99.9989% stake - let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - ); - let fee = - mock::swap_alpha_to_tao(netuid, ((alpha.to_u64() as f64 * 0.99) as u64).into()).1 + fee; - assert_ok!(SubtensorModule::remove_stake( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - (U64F64::from_num(alpha.to_u64()) * U64F64::from_num(0.99)) - .to_num::() - .into() - )); - - // Check that all alpha was unstaked and 99% TAO balance was returned (less fees) - // let fee = ::SwapInterface::approx_fee_amount(netuid.into(), (amount as f64 * 0.99) as u64); - assert_abs_diff_eq!( - SubtensorModule::get_coldkey_balance(&coldkey_account_id).to_u64(), - (amount as f64 * 0.99) as u64 - fee, - epsilon = amount / 1000, - ); - assert_abs_diff_eq!( - SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id).to_u64(), - (amount as f64 * 0.01) as u64, - epsilon = amount / 1000, - ); - let new_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - ); - assert_abs_diff_eq!( - new_alpha, - AlphaBalance::from((alpha.to_u64() as f64 * 0.01) as u64), - epsilon = 10.into() - ); - }); -} - -#[test] -fn test_move_stake_limit_partial() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let stake_amount = AlphaBalance::from(150_000_000_000_u64); - let move_amount = AlphaBalance::from(150_000_000_000_u64); - - // add network - let origin_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - let destination_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - register_ok_neuron(origin_netuid, hotkey, coldkey, 192213123); - register_ok_neuron(destination_netuid, hotkey, coldkey, 192213123); - - // Give the neuron some stake to remove - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - origin_netuid, - stake_amount, - ); - - // Registration now goes through the burn/swap path, which initializes swap V3 state. - // Clear that state first so the manual reserve fixture below actually controls price. - let mut origin_weight_meter = - frame_support::weights::WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)); - assert!( - ::SwapInterface::clear_protocol_liquidity( - origin_netuid, - &mut origin_weight_meter - ) - ); - let mut destination_weight_meter = - frame_support::weights::WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)); - assert!( - ::SwapInterface::clear_protocol_liquidity( - destination_netuid, - &mut destination_weight_meter - ) - ); - - // Force-set alpha in and tao reserve to make price equal 1.5 on both origin and destination, - // but there's much more liquidity on destination, so its price wouldn't go up when restaked. - let tao_reserve = TaoBalance::from(150_000_000_000_u64); - let alpha_in = AlphaBalance::from(100_000_000_000_u64); - - SubnetTAO::::insert(origin_netuid, tao_reserve); - SubnetAlphaIn::::insert(origin_netuid, alpha_in); - - SubnetTAO::::insert(destination_netuid, tao_reserve * 100_000.into()); - SubnetAlphaIn::::insert(destination_netuid, alpha_in * 100_000.into()); - - let origin_price = - ::SwapInterface::current_alpha_price(origin_netuid.into()); - let destination_price = - ::SwapInterface::current_alpha_price(destination_netuid.into()); - - assert_eq!(origin_price, U96F32::from_num(1.5)); - assert_eq!(destination_price, U96F32::from_num(1.5)); - - // The relative price between origin and destination subnets is 1. - // Setup limit relative price so that it doesn't drop by more than 1% from current price. - let limit_price = TaoBalance::from(990_000_000_u64); - - // Move stake with slippage safety - executes partially - assert_ok!(SubtensorModule::swap_stake_limit( - RuntimeOrigin::signed(coldkey), - hotkey, - origin_netuid, - destination_netuid, - move_amount, - limit_price, - true, - )); - - let new_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - origin_netuid, - ); - - assert_abs_diff_eq!( - new_alpha, - AlphaBalance::from(149_000_000_000_u64), - epsilon = 100_000_000.into() - ); - }); -} - -/// cargo test --package pallet-subtensor --lib -- tests::staking::test_unstake_all_hits_liquidity_min --exact --show-output -#[test] -fn test_unstake_all_hits_liquidity_min() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - let stake_amount = AlphaBalance::from(190_000_000_000_u64); // 190 Alpha - - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - register_ok_neuron(netuid, hotkey, coldkey, 192213123); - // Give the neuron some stake to remove - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - netuid, - stake_amount, - ); - - // Setup the Alpha pool so that removing all the Alpha will bring liqudity below the minimum - let remaining_tao = TaoBalance::from(u64::from(mock::SwapMinimumReserve::get()) - 1); - let alpha_reserves = AlphaBalance::from(stake_amount.to_u64() + 10_000_000); - mock::setup_reserves(netuid, remaining_tao, alpha_reserves); - - // Try to unstake, but we reduce liquidity too far - - assert_ok!(SubtensorModule::unstake_all( - RuntimeOrigin::signed(coldkey), - hotkey, - )); - - // Expect nothing to be unstaked - let new_alpha = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); - assert_abs_diff_eq!(new_alpha, stake_amount, epsilon = AlphaBalance::ZERO); - }); -} - -#[test] -fn test_unstake_all_alpha_hits_liquidity_min() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - let stake_amount = TaoBalance::from(100_000_000_000_u64); // 100 TAO - - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - register_ok_neuron(netuid, hotkey, coldkey, 192213123); - add_balance_to_coldkey_account(&coldkey, stake_amount + ExistentialDeposit::get()); - // Give the neuron some stake to remove - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - stake_amount - )); - - // Setup the pool so that removing all the TAO will bring liqudity below the minimum - let remaining_tao = I96F32::from_num(u64::from(mock::SwapMinimumReserve::get()) - 1) - .saturating_sub(I96F32::from(1)); - let alpha = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); - let alpha_reserves = I110F18::from(u64::from(alpha) + 10_000_000); - - let k = I110F18::from_fixed(remaining_tao) - .saturating_mul(alpha_reserves.saturating_add(I110F18::from(u64::from(alpha)))); - let tao_reserves = k.safe_div(alpha_reserves); - - mock::setup_reserves( - netuid, - (tao_reserves.to_num::() / 100_u64).into(), - alpha_reserves.to_num::().into(), - ); - - // Try to unstake, but we reduce liquidity too far - - assert_err!( - SubtensorModule::unstake_all_alpha(RuntimeOrigin::signed(coldkey), hotkey), - Error::::AmountTooLow - ); - - // Expect nothing to be unstaked - let new_alpha = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); - assert_eq!(new_alpha, alpha); - }); -} - -#[test] -fn test_unstake_all_alpha_works() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - let stake_amount = TaoBalance::from(190_000_000_000_u64); // 190 TAO - - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - register_ok_neuron(netuid, hotkey, coldkey, 192213123); - add_balance_to_coldkey_account(&coldkey, stake_amount + ExistentialDeposit::get()); - - // Give the neuron some stake to remove - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - stake_amount - )); - - // Setup the pool so that removing all the TAO will keep liq above min - mock::setup_reserves( - netuid, - stake_amount * 10.into(), - u64::from(stake_amount * 100.into()).into(), - ); - - // Unstake all alpha to root - assert_ok!(SubtensorModule::unstake_all_alpha( - RuntimeOrigin::signed(coldkey), - hotkey, - )); - - let new_alpha = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); - assert_abs_diff_eq!(new_alpha, AlphaBalance::ZERO, epsilon = 1_000.into()); - let new_root = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - NetUid::ROOT, - ); - assert!(new_root > 100_000.into()); - }); -} - -#[test] -fn test_unstake_all_works() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - let stake_amount = TaoBalance::from(190_000_000_000_u64); // 190 TAO - - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - register_ok_neuron(netuid, hotkey, coldkey, 192213123); - add_balance_to_coldkey_account(&coldkey, stake_amount + ExistentialDeposit::get()); - - // Give the neuron some stake to remove - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - stake_amount - )); - - // Setup the pool so that removing all the TAO will keep liq above min - mock::setup_reserves( - netuid, - stake_amount * 10.into(), - u64::from(stake_amount * 100.into()).into(), - ); - // Unstake all alpha to free balance - assert_ok!(SubtensorModule::unstake_all( - RuntimeOrigin::signed(coldkey), - hotkey, - )); - - let new_alpha = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); - assert_abs_diff_eq!(new_alpha, AlphaBalance::ZERO, epsilon = 1_000.into()); - let new_balance = SubtensorModule::get_coldkey_balance(&coldkey); - assert!(new_balance > 100_000.into()); - }); -} - -#[test] -fn test_stake_into_subnet_ok() { - new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(1); - let owner_coldkey = U256::from(2); - let hotkey = U256::from(3); - let coldkey = U256::from(4); - let amount = 100_000_000; - - // add network - let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); - - // Forse-set alpha in and tao reserve to make price equal 0.01 - let tao_reserve = TaoBalance::from(100_000_000_000_u64); - let alpha_in = AlphaBalance::from(1_000_000_000_000_u64); - mock::setup_reserves(netuid, tao_reserve, alpha_in); - let current_price = - ::SwapInterface::current_alpha_price(netuid.into()) - .to_num::(); - - // Initialize swap v3 - let order = GetAlphaForTao::::with_amount(0); - assert_ok!(::SwapInterface::swap( - netuid.into(), - order, - TaoBalance::MAX, - false, - true - )); - - // Add stake with slippage safety and check if the result is ok - let large_balance = 20_000_000_000_000_000_u64; - add_balance_to_coldkey_account(&coldkey, large_balance.into()); - assert_ok!(SubtensorModule::stake_into_subnet( - &hotkey, - &coldkey, - netuid, - amount.into(), - large_balance.into(), - false, - )); - let fee_rate = pallet_subtensor_swap::FeeRate::::get(NetUid::from(netuid)) as f64 - / u16::MAX as f64; - let expected_stake = (amount as f64) * (1. - fee_rate) / current_price; - - // Check if stake has increased - assert_abs_diff_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid) - .to_u64() as f64, - expected_stake, - epsilon = expected_stake / 1000., - ); - }); -} - -#[test] -fn test_stake_into_subnet_low_amount() { - new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(1); - let owner_coldkey = U256::from(2); - let hotkey = U256::from(3); - let coldkey = U256::from(4); - let amount = 10; - - // add network - let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); - - // Forse-set alpha in and tao reserve to make price equal 0.1 - let tao_reserve = TaoBalance::from(100_000_000_000_u64); - let alpha_in = AlphaBalance::from(1_000_000_000_000_u64); - mock::setup_reserves(netuid, tao_reserve, alpha_in); - let current_price = - ::SwapInterface::current_alpha_price(netuid.into()) - .to_num::(); - - // Initialize swap - let order = GetAlphaForTao::::with_amount(0); - assert_ok!(::SwapInterface::swap( - netuid.into(), - order, - TaoBalance::MAX, - false, - true - )); - - // Add stake with slippage safety and check if the result is ok - let large_balance = 20_000_000_000_000_000_u64; - add_balance_to_coldkey_account(&coldkey, large_balance.into()); - assert_ok!(SubtensorModule::stake_into_subnet( - &hotkey, - &coldkey, - netuid, - amount.into(), - large_balance.into(), - false, - )); - let expected_stake = (amount as f64) * 0.997 / current_price; - - // Check if stake has increased - assert_abs_diff_eq!( - u64::from(SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, &coldkey, netuid - )) as f64, - expected_stake, - epsilon = expected_stake / 100. - ); - }); -} - -#[test] -fn test_unstake_from_subnet_low_amount() { - new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(1); - let owner_coldkey = U256::from(2); - let hotkey = U256::from(3); - let coldkey = U256::from(4); - let amount = 10; - - // add network - let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); - - // Forse-set alpha in and tao reserve to make price equal 0.01 - let tao_reserve = TaoBalance::from(100_000_000_000_u64); - let alpha_in = AlphaBalance::from(1_000_000_000_000_u64); - mock::setup_reserves(netuid, tao_reserve, alpha_in); - - // Initialize swap v3 - let order = GetAlphaForTao::::with_amount(0); - assert_ok!(::SwapInterface::swap( - netuid.into(), - order, - TaoBalance::MAX, - false, - true - )); - - // Add stake and check if the result is ok - let large_balance = 20_000_000_000_000_000_u64; - add_balance_to_coldkey_account(&coldkey, large_balance.into()); - assert_ok!(SubtensorModule::stake_into_subnet( - &hotkey, - &coldkey, - netuid, - amount.into(), - large_balance.into(), - false, - )); - - // Remove stake - let alpha = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); - assert_ok!(SubtensorModule::unstake_from_subnet( - &hotkey, - &coldkey, - &coldkey, - netuid, - alpha, - TaoBalance::ZERO, - false, - )); - - // Check if stake is zero - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid), - AlphaBalance::ZERO, - ); - }); -} - -#[test] -fn test_stake_into_subnet_prohibitive_limit() { - new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(1); - let owner_coldkey = U256::from(2); - let coldkey = U256::from(4); - let amount = 100_000_000; - - // add network - let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); - add_balance_to_coldkey_account(&coldkey, amount.into()); - - // Forse-set alpha in and tao reserve to make price equal 0.01 - let tao_reserve = TaoBalance::from(100_000_000_000_u64); - let alpha_in = AlphaBalance::from(1_000_000_000_000_u64); - mock::setup_reserves(netuid, tao_reserve, alpha_in); - - // Initialize swap v3 - let order = GetAlphaForTao::::with_amount(0); - assert_ok!(::SwapInterface::swap( - netuid.into(), - order, - TaoBalance::MAX, - false, - true - )); - - // Add stake and check if the result is ok - // Use prohibitive limit price - assert_err!( - SubtensorModule::add_stake_limit( - RuntimeOrigin::signed(coldkey), - owner_hotkey, - netuid, - amount.into(), - TaoBalance::ZERO, - true, - ), - DispatchError::from(pallet_subtensor_swap::Error::::PriceLimitExceeded) - ); - - // Check if stake has NOT increased - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &owner_hotkey, - &coldkey, - netuid - ), - AlphaBalance::ZERO - ); - - // Check if balance has NOT decreased - assert_eq!( - SubtensorModule::get_coldkey_balance(&coldkey), - amount.into() - ); - }); -} - -#[test] -fn test_unstake_from_subnet_prohibitive_limit() { - new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(1); - let owner_coldkey = U256::from(2); - let coldkey = U256::from(4); - let amount = 100_000_000; - - // add network - let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); - add_balance_to_coldkey_account(&coldkey, amount.into()); - - // Forse-set alpha in and tao reserve to make price equal 0.01 - let tao_reserve = TaoBalance::from(100_000_000_000_u64); - let alpha_in = AlphaBalance::from(1_000_000_000_000_u64); - mock::setup_reserves(netuid, tao_reserve, alpha_in); - - // Initialize swap v3 - let order = GetAlphaForTao::::with_amount(0); - assert_ok!(::SwapInterface::swap( - netuid.into(), - order, - TaoBalance::MAX, - false, - true - )); - - // Add stake and check if the result is ok - assert_ok!(SubtensorModule::stake_into_subnet( - &owner_hotkey, - &coldkey, - netuid, - amount.into(), - TaoBalance::MAX, - false, - )); - - // Remove stake - // Use prohibitive limit price - let balance_before = SubtensorModule::get_coldkey_balance(&coldkey); - let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &owner_hotkey, - &coldkey, - netuid, - ); - assert_err!( - SubtensorModule::remove_stake_limit( - RuntimeOrigin::signed(coldkey), - owner_hotkey, - netuid, - alpha, - TaoBalance::MAX, - true, - ), - DispatchError::from(pallet_subtensor_swap::Error::::PriceLimitExceeded) - ); - - // Check if stake has NOT decreased - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &owner_hotkey, - &coldkey, - netuid - ), - alpha - ); - - // Check if balance has NOT increased - assert_eq!( - SubtensorModule::get_coldkey_balance(&coldkey), - balance_before, - ); - }); -} - -#[test] -fn test_unstake_full_amount() { - new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(1); - let owner_coldkey = U256::from(2); - let coldkey = U256::from(4); - let amount = 100_000_000; - - // add network - let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); - add_balance_to_coldkey_account(&coldkey, amount.into()); - - // Forse-set alpha in and tao reserve to make price equal 0.01 - let tao_reserve = TaoBalance::from(100_000_000_000_u64); - let alpha_in = AlphaBalance::from(1_000_000_000_000_u64); - mock::setup_reserves(netuid, tao_reserve, alpha_in); - - // Initialize swap v3 - let order = GetAlphaForTao::::with_amount(0); - assert_ok!(::SwapInterface::swap( - netuid.into(), - order, - TaoBalance::MAX, - false, - true - )); - - // Add stake and check if the result is ok - assert_ok!(SubtensorModule::stake_into_subnet( - &owner_hotkey, - &coldkey, - netuid, - amount.into(), - TaoBalance::MAX, - false, - )); - - // Remove stake - // Use prohibitive limit price - let balance_before = SubtensorModule::get_coldkey_balance(&coldkey); - let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &owner_hotkey, - &coldkey, - netuid, - ); - assert_ok!(SubtensorModule::remove_stake( - RuntimeOrigin::signed(coldkey), - owner_hotkey, - netuid, - alpha, - )); - - // Check if stake is zero - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &owner_hotkey, - &coldkey, - netuid - ), - AlphaBalance::ZERO - ); - - // Check if balance has increased accordingly - let balance_after = SubtensorModule::get_coldkey_balance(&coldkey); - let actual_balance_increase = u64::from(balance_after - balance_before) as f64; - let fee_rate = pallet_subtensor_swap::FeeRate::::get(NetUid::from(netuid)) as f64 - / u16::MAX as f64; - let expected_balance_increase = amount as f64 * (1. - fee_rate) / (1. + fee_rate); - assert_abs_diff_eq!( - actual_balance_increase, - expected_balance_increase, - epsilon = expected_balance_increase / 10_000. - ); - }); -} - -/// Test correctness of swap fees: -/// 1. TAO is not minted or burned -/// 2. Fees match FeeRate -#[test] -fn test_swap_fees_tao_correctness() { - new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(1); - let owner_coldkey = U256::from(2); - let coldkey = U256::from(4); - let block_builder = U256::from(12345u64); - let amount = TaoBalance::from(1_000_000_000_u64); - let owner_balance_before = amount * 10.into(); - let user_balance_before = amount * 100.into(); - - // add network - let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); - add_balance_to_coldkey_account(&owner_coldkey, owner_balance_before); - add_balance_to_coldkey_account(&coldkey, user_balance_before); - - // Forse-set alpha in and tao reserve to make price equal 0.25 - let tao_reserve = TaoBalance::from(100_000_000_000_u64); - let alpha_in = AlphaBalance::from(400_000_000_000_u64); - mock::setup_reserves(netuid, tao_reserve, alpha_in); - - // Check starting "total TAO" - let block_builder_balance_before = SubtensorModule::get_coldkey_balance(&block_builder); - let total_tao_before = user_balance_before - + owner_balance_before - + SubnetTAO::::get(netuid) - + block_builder_balance_before; - - // Get alpha for owner - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(owner_coldkey), - owner_hotkey, - netuid, - amount.into(), - )); - - // Add owner coldkey Alpha as concentrated liquidity - // between current price current price + 0.01 - let current_price = - ::SwapInterface::current_alpha_price(netuid.into()) - .to_num::() - + 0.0001; - let limit_price = current_price + 0.01; - - // Limit-buy and then sell all alpha for user to hit owner liquidity - assert_ok!(SubtensorModule::add_stake_limit( - RuntimeOrigin::signed(coldkey), - owner_hotkey, - netuid, - amount.into(), - ((limit_price * u64::MAX as f64) as u64).into(), - true - )); - - let user_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &owner_hotkey, - &coldkey, - netuid, - ); - assert_ok!(SubtensorModule::remove_stake( - RuntimeOrigin::signed(coldkey), - owner_hotkey, - netuid, - user_alpha, - )); - - // TODO: This block is for balancer swap - // Cause tao fees to propagate to SubnetTAO - // let (claimed_tao_fees, _) = - // ::SwapInterface::adjust_protocol_liquidity( - // netuid, - // 0.into(), - // 0.into(), - // ); - // SubnetTAO::::mutate(netuid, |tao| *tao += claimed_tao_fees); - - // Check ending "total TAO" - let owner_balance_after = SubtensorModule::get_coldkey_balance(&owner_coldkey); - let user_balance_after = SubtensorModule::get_coldkey_balance(&coldkey); - let block_builder_balance_after = SubtensorModule::get_coldkey_balance(&block_builder); - - let total_tao_after = user_balance_after - + owner_balance_after - + SubnetTAO::::get(netuid) - + block_builder_balance_after; - - // Total TAO does not change, leave some epsilon for rounding - assert_abs_diff_eq!(total_tao_before, total_tao_after, epsilon = 2.into()); - }); -} - -#[test] -fn test_increase_stake_for_hotkey_and_coldkey_on_subnet_adds_to_staking_hotkeys_map() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let coldkey1 = U256::from(2); - let hotkey = U256::from(3); - - let netuid = NetUid::from(1); - let stake_amount = 100_000_000_000_u64; - - // Check no entry in the staking hotkeys map - assert!(!StakingHotkeys::::contains_key(coldkey)); - // insert manually - StakingHotkeys::::insert(coldkey, Vec::::new()); - // check entry has no hotkey - assert!(!StakingHotkeys::::get(coldkey).contains(&hotkey)); - - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey, - netuid, - stake_amount.into(), - ); - - // Check entry exists in the staking hotkeys map - assert!(StakingHotkeys::::contains_key(coldkey)); - // check entry has hotkey - assert!(StakingHotkeys::::get(coldkey).contains(&hotkey)); - - // Check no entry in the staking hotkeys map for coldkey1 - assert!(!StakingHotkeys::::contains_key(coldkey1)); - - // Run increase stake for hotkey and coldkey1 on subnet - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &coldkey1, - netuid, - stake_amount.into(), - ); - - // Check entry exists in the staking hotkeys map for coldkey1 - assert!(StakingHotkeys::::contains_key(coldkey1)); - // check entry has hotkey - assert!(StakingHotkeys::::get(coldkey1).contains(&hotkey)); - }); -} - -#[test] -fn test_remove_stake_full_limit_ok() { - new_test_ext(1).execute_with(|| { - let hotkey_account_id = U256::from(1); - let coldkey_account_id = U256::from(2); - let stake_amount = AlphaBalance::from(10_000_000_000_u64); - - // add network - let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); - remove_owner_registration_stake(netuid); - - // Give the neuron some stake to remove - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - stake_amount, - ); - - let tao_reserve = TaoBalance::from(100_000_000_000_u64); - let alpha_in = AlphaBalance::from(100_000_000_000_u64); - SubnetTAO::::insert(netuid, tao_reserve); - SubnetAlphaIn::::insert(netuid, alpha_in); - - let limit_price = TaoBalance::from(90_000_000); - - // Remove stake with slippage safety - assert_ok!(SubtensorModule::remove_stake_full_limit( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - Some(limit_price), - )); - - // Check if stake has decreased to zero - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid - ), - AlphaBalance::ZERO - ); - - let new_balance = SubtensorModule::get_coldkey_balance(&coldkey_account_id); - assert_abs_diff_eq!( - new_balance, - 9_086_000_000_u64.into(), - epsilon = 1_000_000.into() - ); - }); -} - -#[test] -fn test_remove_stake_full_limit_fails_slippage_too_high() { - new_test_ext(1).execute_with(|| { - let hotkey_account_id = U256::from(1); - let coldkey_account_id = U256::from(2); - let stake_amount = AlphaBalance::from(10_000_000_000_u64); - - // add network - let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); - - // Give the neuron some stake to remove - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - stake_amount, - ); - - let tao_reserve = TaoBalance::from(100_000_000_000_u64); - let alpha_in = AlphaBalance::from(100_000_000_000_u64); - SubnetTAO::::insert(netuid, tao_reserve); - SubnetAlphaIn::::insert(netuid, alpha_in); - - let invalid_limit_price = TaoBalance::from(910_000_000_u64); - - // Remove stake with slippage safety - assert_err!( - SubtensorModule::remove_stake_full_limit( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - Some(invalid_limit_price), - ), - Error::::SlippageTooHigh - ); - }); -} - -#[test] -fn test_remove_stake_full_limit_ok_with_no_limit_price() { - new_test_ext(1).execute_with(|| { - let hotkey_account_id = U256::from(1); - let coldkey_account_id = U256::from(2); - let stake_amount = AlphaBalance::from(10_000_000_000_u64); - - // add network - let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); - remove_owner_registration_stake(netuid); - - // Give the neuron some stake to remove - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid, - stake_amount, - ); - - let tao_reserve = TaoBalance::from(100_000_000_000_u64); - let alpha_in = AlphaBalance::from(100_000_000_000_u64); - SubnetTAO::::insert(netuid, tao_reserve); - SubnetAlphaIn::::insert(netuid, alpha_in); - - // Remove stake with slippage safety - assert_ok!(SubtensorModule::remove_stake_full_limit( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - netuid, - None, - )); - - // Check if stake has decreased to zero - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - netuid - ), - AlphaBalance::ZERO - ); - - let new_balance = SubtensorModule::get_coldkey_balance(&coldkey_account_id); - assert_abs_diff_eq!( - new_balance, - 9_086_000_000_u64.into(), - epsilon = 1_000_000.into() - ); - }); -} - -/// This test verifies that minimum stake amount is sufficient to move price and apply -/// non-zero staking fees -#[test] -fn test_default_min_stake_sufficiency() { - new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(1); - let owner_coldkey = U256::from(2); - let coldkey = U256::from(4); - let min_tao_stake = DefaultMinStake::::get() * 2.into(); - let amount = min_tao_stake; - let owner_balance_before = amount * 10.into(); - let user_balance_before = amount * 100.into(); - - // add network - let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); - add_balance_to_coldkey_account(&owner_coldkey, owner_balance_before); - add_balance_to_coldkey_account(&coldkey, user_balance_before); - let fee_rate = pallet_subtensor_swap::FeeRate::::get(NetUid::from(netuid)) as f64 - / u16::MAX as f64; - - // Set some extreme, but realistic TAO and Alpha reserves to minimize slippage - // 1% of TAO max supply - // 0.01 Alpha price - let tao_reserve = TaoBalance::from(210_000_000_000_000_u64); - let alpha_in = AlphaBalance::from(21_000_000_000_000_000_u64); - mock::setup_reserves(netuid, tao_reserve, alpha_in); - let current_price_before = - ::SwapInterface::current_alpha_price(netuid.into()); - - // Stake and unstake - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey), - owner_hotkey, - netuid, - amount.into(), - )); - let fee_stake = (fee_rate * u64::from(amount) as f64) as u64; - let current_price_after_stake = - ::SwapInterface::current_alpha_price(netuid.into()); - let user_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &owner_hotkey, - &coldkey, - netuid, - ); - assert_ok!(SubtensorModule::remove_stake( - RuntimeOrigin::signed(coldkey), - owner_hotkey, - netuid, - user_alpha, - )); - let fee_unstake = (fee_rate * user_alpha.to_u64() as f64) as u64; - let current_price_after_unstake = - ::SwapInterface::current_alpha_price(netuid.into()); - - assert!(fee_stake > 0); - assert!(fee_unstake > 0); - assert!(current_price_after_stake > current_price_before); - assert!(current_price_after_stake > current_price_after_unstake); - }); -} - -#[test] -fn test_large_swap() { - new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(1); - let owner_coldkey = U256::from(2); - let coldkey = U256::from(100); - - // add network - let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_000_u64.into()); - let swap_amount = TaoBalance::from(100_000_000_000_000_u64); - let tao = TaoBalance::from(swap_amount.to_u64() / 1000); - let alpha = AlphaBalance::from(1_000_000_000_000_000_u64); - SubnetTAO::::insert(netuid, tao); - SubnetAlphaIn::::insert(netuid, alpha); - - // Force the swap to initialize - ::SwapInterface::init_swap(netuid, None); - - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey), - owner_hotkey, - netuid, - swap_amount, - )); - }); -} - -// cargo test --package pallet-subtensor --lib -- tests::staking::test_add_root_updates_counters --exact --show-output -#[test] -fn test_add_root_updates_counters() { - new_test_ext(0).execute_with(|| { - let hotkey_account_id = U256::from(561337); - let coldkey_account_id = U256::from(61337); - add_network(NetUid::ROOT, 10, 0); - assert_ok!(SubtensorModule::root_register( - RuntimeOrigin::signed(coldkey_account_id).clone(), - hotkey_account_id, - )); - let stake_amount = TaoBalance::from(1_000_000_000_u64); - - // Give it some $$$ in his coldkey balance - let initial_balance = stake_amount + ExistentialDeposit::get(); - add_balance_to_coldkey_account(&coldkey_account_id, initial_balance); - - // Setup SubnetAlphaIn (because we are going to stake) - SubnetAlphaIn::::insert(NetUid::ROOT, AlphaBalance::from(stake_amount.to_u64())); - - // Stake to hotkey account, and check if the result is ok - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - NetUid::ROOT, - stake_amount - )); - - // Check if stake has increased - let new_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id); - assert_eq!(new_stake, stake_amount); - - // Check if total stake has increased accordingly. - assert_eq!(SubtensorModule::get_total_stake(), stake_amount); - - // SubnetTAO updated - assert_eq!(SubnetTAO::::get(NetUid::ROOT), stake_amount); - - // SubnetAlphaIn updated - assert_eq!(SubnetAlphaIn::::get(NetUid::ROOT), 0.into()); - - // SubnetAlphaOut updated - assert_eq!( - SubnetAlphaOut::::get(NetUid::ROOT), - AlphaBalance::from(stake_amount.to_u64()) - ); - - // SubnetVolume updated - assert_eq!( - SubnetVolume::::get(NetUid::ROOT), - stake_amount.to_u64() as u128 - ); - }); -} - -// cargo test --package pallet-subtensor --lib -- tests::staking::test_remove_root_updates_counters --exact --show-output -#[test] -fn test_remove_root_updates_counters() { - new_test_ext(0).execute_with(|| { - let hotkey_account_id = U256::from(561337); - let coldkey_account_id = U256::from(61337); - add_network(NetUid::ROOT, 10, 0); - assert_ok!(SubtensorModule::root_register( - RuntimeOrigin::signed(coldkey_account_id).clone(), - hotkey_account_id, - )); - let stake_amount = TaoBalance::from(1_000_000_000); - - // Give it some $$$ in his coldkey balance - let initial_balance = stake_amount + ExistentialDeposit::get(); - add_balance_to_coldkey_account(&coldkey_account_id, initial_balance); - - // Setup existing stake - mock_increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &coldkey_account_id, - NetUid::ROOT, - AlphaBalance::from(stake_amount.to_u64()), - ); - - // Setup TotalStake, SubnetAlphaOut and SubnetTAO (because we are going to unstake) - TotalStake::::set(stake_amount); - SubnetTAO::::insert(NetUid::ROOT, stake_amount); - SubnetAlphaOut::::insert(NetUid::ROOT, AlphaBalance::from(stake_amount.to_u64())); - - // Stake to hotkey account, and check if the result is ok - assert_ok!(SubtensorModule::remove_stake( - RuntimeOrigin::signed(coldkey_account_id), - hotkey_account_id, - NetUid::ROOT, - AlphaBalance::from(stake_amount.to_u64()) - )); - - // Check if stake has been decreased - let new_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id); - assert_eq!(new_stake, 0.into()); - - // Check if total stake has decreased accordingly. - assert_eq!(SubtensorModule::get_total_stake(), 0.into()); - - // SubnetTAO updated - assert_eq!(SubnetTAO::::get(NetUid::ROOT), 0.into()); - - // SubnetAlphaIn updated - assert_eq!( - SubnetAlphaIn::::get(NetUid::ROOT), - AlphaBalance::from(stake_amount.to_u64()) - ); - - // SubnetAlphaOut updated - assert_eq!(SubnetAlphaOut::::get(NetUid::ROOT), 0.into()); - - // SubnetVolume updated - assert_eq!( - SubnetVolume::::get(NetUid::ROOT), - stake_amount.to_u64() as u128 - ); - }); -} - -// cargo test --package pallet-subtensor --lib -- tests::staking::test_staking_records_flow --exact --show-output -#[test] -fn test_staking_records_flow() { - new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(1); - let owner_coldkey = U256::from(2); - let hotkey = U256::from(3); - let coldkey = U256::from(4); - let amount = 100_000_000; - - // add network - let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); - - // Forse-set alpha in and tao reserve to make price equal 0.01 - let tao_reserve = TaoBalance::from(100_000_000_000_u64); - let alpha_in = AlphaBalance::from(1_000_000_000_000_u64); - mock::setup_reserves(netuid, tao_reserve, alpha_in); - - // Initialize swap v3 - SubtensorModule::swap_tao_for_alpha( - netuid, - TaoBalance::ZERO, - 1_000_000_000_000_u64.into(), - false, - ) - .unwrap(); - - // Add stake with slippage safety and check if the result is ok - let large_balance = 20_000_000_000_000_000_u64; - add_balance_to_coldkey_account(&coldkey, large_balance.into()); - assert_ok!(SubtensorModule::stake_into_subnet( - &hotkey, - &coldkey, - netuid, - amount.into(), - large_balance.into(), - false, - )); - let fee_rate = pallet_subtensor_swap::FeeRate::::get(NetUid::from(netuid)) as f64 - / u16::MAX as f64; - let expected_flow = (amount as f64) * (1. - fee_rate); - - // Check that flow has been recorded (less unstaking fees) - assert_abs_diff_eq!( - SubnetTaoFlow::::get(netuid), - expected_flow as i64, - epsilon = 1_i64 - ); - - // Remove stake - let alpha = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); - assert_ok!(SubtensorModule::unstake_from_subnet( - &hotkey, - &coldkey, - &coldkey, - netuid, - alpha, - TaoBalance::ZERO, - false, - )); - - // Check that outflow has been recorded (less unstaking fees) - // The block builder will receive a fraction of the fees in alpha and will be forced - // to unstake it. So, the additional out-flow is recorded for this. - let unstaked_block_builder_fraction = 1.; - let expected_unstake_fee = - expected_flow * fee_rate * (1. - unstaked_block_builder_fraction); - assert_abs_diff_eq!( - SubnetTaoFlow::::get(netuid), - expected_unstake_fee as i64, - epsilon = ((expected_unstake_fee / 100.0) as i64).max(1) - ); - }); -} - -// cargo test --package pallet-subtensor --lib -- tests::staking::test_lazy_sharepool_migration_get_stake_reads_from_deprecated_alpha_map --exact --nocapture -#[test] -fn test_lazy_sharepool_migration_get_stake_reads_from_deprecated_alpha_map() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - let netuid = add_dynamic_network(&hotkey, &coldkey); - let stake = 200_000_u64; - - // add stake to deprecated Alpha map - Alpha::::insert((hotkey, coldkey, netuid), U64F64::from(1_u64)); - TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); - TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); - - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid), - AlphaBalance::from(stake) - ); - }); -} - -#[test] -fn test_lazy_sharepool_migration_get_stake_reads_from_alpha_v2_map() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - let netuid = add_dynamic_network(&hotkey, &coldkey); - let stake = 200_000_u64; - - // add stake to AlphaV2 map - AlphaV2::::insert((hotkey, coldkey, netuid), SafeFloat::from(1_u64)); - TotalHotkeySharesV2::::insert(hotkey, netuid, SafeFloat::from(1_u64)); - TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); - - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid), - AlphaBalance::from(stake) - ); - }); -} - -#[test] -fn test_lazy_sharepool_migration_get_stake_reads_from_cross_alpha_maps() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - let netuid = add_dynamic_network(&hotkey, &coldkey); - let stake = 200_000_u64; - - // add stake to Alpha map - Alpha::::insert((hotkey, coldkey, netuid), U64F64::from(1_u64)); - // but total shares are in TotalHotkeySharesV2 map (already migrated) - TotalHotkeySharesV2::::insert(hotkey, netuid, SafeFloat::from(1_u64)); - TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); - - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid), - AlphaBalance::from(stake) - ); - }); -} - -#[test] -fn test_lazy_sharepool_migration_staking_causes_migration() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - let netuid = add_dynamic_network(&hotkey, &coldkey); - let stake = 200_000_u64; - - // add stake to deprecated Alpha map - Alpha::::insert((hotkey, coldkey, netuid), U64F64::from(1_u64)); - TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); - TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); - - // Stake more via stake_into_subnet - increase_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake.into(), netuid); - - // Verify that deprecated v1 map values are gone - assert!(Alpha::::try_get((&hotkey, &coldkey, netuid)).is_err()); - assert!(TotalHotkeyShares::::try_get(hotkey, netuid).is_err()); - - // Verify that v2 map values are present - let migrated_share = AlphaV2::::get((&hotkey, &coldkey, netuid)); - let migrated_denominator = TotalHotkeySharesV2::::get(hotkey, netuid); - - assert_abs_diff_eq!( - f64::from((migrated_share.div(&migrated_denominator)).unwrap()), - 1.0, - epsilon = 0.000000000000001 - ); - }); -} - -#[test] -fn test_sharepool_dataops_get_value_v1() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - let netuid = add_dynamic_network(&hotkey, &coldkey); - let stake = 200_000_u64; - - // add stake to deprecated Alpha map - Alpha::::insert((hotkey, coldkey, netuid), U64F64::from(1_u64)); - TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); - TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); - - // Get real share pool and read get_value - let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); - let actual_value = share_pool.get_value(&coldkey); - - assert_eq!(actual_value, stake); - }); -} - -#[test] -fn test_sharepool_dataops_get_value_v2() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - let netuid = add_dynamic_network(&hotkey, &coldkey); - let stake = 200_000_u64; - - // add stake to AlphaV2 map - let share = sf_from_u64(1_u64); - AlphaV2::::insert((hotkey, coldkey, netuid), share.clone()); - TotalHotkeySharesV2::::insert(hotkey, netuid, share); - TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); - - // Get real share pool and read get_value - let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); - let actual_value = share_pool.get_value(&coldkey); - - assert_eq!(actual_value, stake); - }); -} - -#[test] -fn test_sharepool_dataops_get_value_mixed_v1_v2() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - let netuid = add_dynamic_network(&hotkey, &coldkey); - let stake = 200_000_u64; - - // add stake to deprecated Alpha map and new THS v2 map - let share = sf_from_u64(1_u64); - Alpha::::insert((hotkey, coldkey, netuid), U64F64::from(1_u64)); - TotalHotkeySharesV2::::insert(hotkey, netuid, share); - TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); - - // Get real share pool and read get_value - let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); - let actual_value = share_pool.get_value(&coldkey); - - assert_eq!(actual_value, stake); - }); -} - -#[test] -fn test_sharepool_dataops_get_value_mixed_v2_v1() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - let netuid = add_dynamic_network(&hotkey, &coldkey); - let stake = 200_000_u64; - - // add stake to new AlphaV2 map and deprecated THS map - let share = sf_from_u64(1_u64); - AlphaV2::::insert((hotkey, coldkey, netuid), share); - TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); - TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); - - // Get real share pool and read get_value - let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); - let actual_value = share_pool.get_value(&coldkey); - - assert_eq!(actual_value, stake); - }); -} - -#[test] -fn test_sharepool_dataops_get_value_from_shares_v1() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - let netuid = add_dynamic_network(&hotkey, &coldkey); - let stake = 200_000_u64; - - // add stake to deprecated THS map - TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); - TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); - - // Get real share pool and read get_value_from_shares - let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); - let current_share = SafeFloat::from(U64F64::from(1_u64)); - let actual_value = share_pool.get_value_from_shares(current_share); - - assert_eq!(actual_value, stake); - }); -} - -#[test] -fn test_sharepool_dataops_get_value_from_shares_v2() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - let netuid = add_dynamic_network(&hotkey, &coldkey); - let stake = 200_000_u64; - - // add stake to new THS v2 map - let share = sf_from_u64(1_u64); - TotalHotkeySharesV2::::insert(hotkey, netuid, share); - TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); - - // Get real share pool and read get_value_from_shares - let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); - let current_share = SafeFloat::from(U64F64::from(1_u64)); - let actual_value = share_pool.get_value_from_shares(current_share); - - assert_eq!(actual_value, stake); - }); -} - -#[test] -fn test_sharepool_dataops_update_value_for_all() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - let netuid = add_dynamic_network(&hotkey, &coldkey); - let stake = 200_000_u64; - - // add stake to new AlphaV2 map - let share = sf_from_u64(1_u64); - AlphaV2::::insert((hotkey, coldkey, netuid), share.clone()); - TotalHotkeySharesV2::::insert(hotkey, netuid, share); - TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); - - // Get real share pool and call update_value_for_all - let mut share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); - share_pool.update_value_for_all(stake as i64); - let actual_value = share_pool.get_value(&coldkey); - assert_eq!(actual_value, stake * 2); - - share_pool.update_value_for_all(-(stake as i64)); - let actual_value = share_pool.get_value(&coldkey); - assert_eq!(actual_value, stake); - }); -} - -#[test] -fn test_sharepool_dataops_update_value_for_one_v1_with_migration() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - let netuid = add_dynamic_network(&hotkey, &coldkey); - let stake = 200_000_u64; - - // add stake to deprecated Alpha and THS maps - Alpha::::insert((hotkey, coldkey, netuid), U64F64::from(1_u64)); - TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); - TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); - - // Get real share pool and call update_value_for_one - let mut share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); - share_pool.update_value_for_one(&coldkey, stake as i64); - let actual_value = share_pool.get_value(&coldkey); - assert_eq!(actual_value, stake * 2); - - // Verify deletion from deprecated - assert!(!Alpha::::contains_key((hotkey, coldkey, netuid))); - assert!(!TotalHotkeyShares::::contains_key(hotkey, netuid)); - }); -} - -#[test] -fn test_sharepool_dataops_update_value_for_one_v2() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - let netuid = add_dynamic_network(&hotkey, &coldkey); - let stake = 200_000_u64; - - // add stake to new AlphaV2 and THS maps - let share = sf_from_u64(1_u64); - AlphaV2::::insert((hotkey, coldkey, netuid), share.clone()); - TotalHotkeySharesV2::::insert(hotkey, netuid, share); - TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); - - // Get real share pool and call update_value_for_one - let mut share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); - share_pool.update_value_for_one(&coldkey, stake as i64); - let actual_value = share_pool.get_value(&coldkey); - assert_eq!(actual_value, stake * 2); - }); -} - -#[test] -fn test_sharepool_dataops_update_value_for_one_mixed_v1_v2() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - let netuid = add_dynamic_network(&hotkey, &coldkey); - let stake = 200_000_u64; - - // add stake to deprecated Alpha and new THS v2 maps - let share = sf_from_u64(1_u64); - Alpha::::insert((hotkey, coldkey, netuid), U64F64::from(1_u64)); - TotalHotkeySharesV2::::insert(hotkey, netuid, share); - TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); - - // Get real share pool and call update_value_for_one - let mut share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); - share_pool.update_value_for_one(&coldkey, stake as i64); - let actual_value = share_pool.get_value(&coldkey); - assert_eq!(actual_value, stake * 2); - - // Verify deletion from deprecated - assert!(!Alpha::::contains_key((hotkey, coldkey, netuid))); - }); -} - -#[test] -fn test_sharepool_dataops_update_value_for_one_mixed_v2_v1() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - let netuid = add_dynamic_network(&hotkey, &coldkey); - let stake = 200_000_u64; - - // add stake to new AlphaV2 and deprecated THS maps - let share = sf_from_u64(1_u64); - AlphaV2::::insert((hotkey, coldkey, netuid), share); - TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); - TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); - - // Get real share pool and call update_value_for_one - let mut share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); - share_pool.update_value_for_one(&coldkey, stake as i64); - let actual_value = share_pool.get_value(&coldkey); - assert_eq!(actual_value, stake * 2); - - // Verify deletion from deprecated - assert!(!TotalHotkeyShares::::contains_key(hotkey, netuid)); - }); -} - -#[test] -fn test_sharepool_dataops_get_value_returns_zero_on_non_existing_v1() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - let netuid = add_dynamic_network(&hotkey, &coldkey); - remove_owner_registration_stake(netuid); - let stake = 200_000_u64; - - // add to deprecated THS map, but no value in Alpha map - TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); - TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); - - // Get real share pool and read get_value - let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); - let actual_value = share_pool.get_value(&coldkey); - assert_eq!(actual_value, 0_u64); - }); -} - -#[test] -fn test_sharepool_dataops_get_value_returns_zero_on_non_existing_v2() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - let netuid = add_dynamic_network(&hotkey, &coldkey); - remove_owner_registration_stake(netuid); - let stake = 200_000_u64; - - // add to THSV2 map, but no value in AlphaV2 map - let share = sf_from_u64(1_u64); - TotalHotkeySharesV2::::insert(hotkey, netuid, share); - TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); - - // Get real share pool and read get_value - let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); - let actual_value = share_pool.get_value(&coldkey); - assert_eq!(actual_value, 0_u64); - }); -} - -#[test] -fn test_sharepool_dataops_try_get_value_returns_err_on_non_existing_v1() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - let netuid = add_dynamic_network(&hotkey, &coldkey); - remove_owner_registration_stake(netuid); - let stake = 200_000_u64; - - // add to deprecated THS map, but no value in Alpha map - TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); - TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); - - // Get real share pool and read get_value - let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); - let maybe_actual_value = share_pool.try_get_value(&coldkey); - assert!(maybe_actual_value.is_err()); - }); -} - -#[test] -fn test_sharepool_dataops_try_get_value_returns_err_on_non_existing_v2() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - - let netuid = add_dynamic_network(&hotkey, &coldkey); - remove_owner_registration_stake(netuid); - let stake = 200_000_u64; - - // add to THSV2 map, but no value in AlphaV2 map - let share = sf_from_u64(1_u64); - TotalHotkeySharesV2::::insert(hotkey, netuid, share); - TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); - - // Get real share pool and read get_value - let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); - let maybe_actual_value = share_pool.try_get_value(&coldkey); - assert!(maybe_actual_value.is_err()); - }); -} diff --git a/pallets/subtensor/src/tests/staking/add_stake.rs b/pallets/subtensor/src/tests/staking/add_stake.rs new file mode 100644 index 0000000000..477d5fa388 --- /dev/null +++ b/pallets/subtensor/src/tests/staking/add_stake.rs @@ -0,0 +1,920 @@ +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] +//! Tests for [`crate::staking::add_stake`] and stake-into-subnet paths. + +use approx::assert_abs_diff_eq; +use frame_support::dispatch::{DispatchClass, GetDispatchInfo, Pays}; +use frame_support::sp_runtime::DispatchError; +use frame_support::{assert_err, assert_noop, assert_ok, traits::Currency}; +use frame_system::RawOrigin; +use sp_core::{Get, U256}; +use sp_runtime::PerU16; +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; +use subtensor_swap_interface::{Order, SwapHandler}; + +use super::super::mock; +use super::super::mock::*; +use crate::*; + +#[test] +fn test_add_stake_dispatch_info_ok() { + new_test_ext(1).execute_with(|| { + let hotkey = U256::from(0); + let amount_staked = TaoBalance::from(5000); + let netuid = NetUid::from(1); + let call = RuntimeCall::SubtensorModule(SubtensorCall::add_stake { + hotkey, + netuid, + amount_staked, + }); + let di = call.get_dispatch_info(); + assert_eq!(di.extension_weight, frame_support::weights::Weight::zero(),); + assert_eq!(di.class, DispatchClass::Normal,); + assert_eq!(di.pays_fee, Pays::Yes,); + }); +} + +#[test] +fn test_add_stake_ok_no_emission() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(533453); + let coldkey_account_id = U256::from(55453); + let amount = DefaultMinStake::::get().to_u64() * 10; + + //add network + let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + remove_owner_registration_stake(netuid); + + mock::setup_reserves( + netuid, + (amount * 1_000_000).into(), + (amount * 10_000_000).into(), + ); + + // Give it some $$$ in his coldkey balance + add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); + + // Check we have zero staked before transfer + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), + TaoBalance::ZERO + ); + + // Also total stake should be equal to the network initial lock + assert_eq!( + SubtensorModule::get_total_stake(), + SubtensorModule::get_network_min_lock() + ); + + // Transfer to hotkey account, and check if the result is ok + let (alpha_staked, fee) = mock::swap_tao_to_alpha(netuid, amount.into()); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount.into() + )); + + let (tao_expected, _) = mock::swap_alpha_to_tao(netuid, alpha_staked); + let approx_fee = ::SwapInterface::approx_fee_amount( + netuid.into(), + TaoBalance::from(amount), + ); + + assert_abs_diff_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), + tao_expected + approx_fee, // swap returns value after fee, so we need to compensate it + epsilon = 10000.into(), + ); + + // Check if stake has increased + assert_abs_diff_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), + (amount - fee).into(), + epsilon = 10000.into() + ); + + // Check if balance has decreased + assert_eq!( + SubtensorModule::get_coldkey_balance(&coldkey_account_id), + 1.into() + ); + + // Check if total stake has increased accordingly. + assert_eq!( + SubtensorModule::get_total_stake(), + SubtensorModule::get_network_min_lock() + amount.into() + ); + }); +} + +#[test] +fn test_add_stake_err_signature() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(654); // bogus + let amount = 20000; // Not used + let netuid = NetUid::from(1); + + assert_err!( + SubtensorModule::add_stake( + RawOrigin::None.into(), + hotkey_account_id, + netuid, + amount.into() + ), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn test_add_stake_not_registered_key_pair() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1); + let subnet_owner_hotkey = U256::from(2); + let coldkey_account_id = U256::from(435445); + let hotkey_account_id = U256::from(54544); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + let amount = DefaultMinStake::::get().to_u64() * 10; + add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); + assert_err!( + SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount.into() + ), + Error::::HotKeyAccountNotExists + ); + }); +} + +#[test] +fn test_add_stake_ok_neuron_does_not_belong_to_coldkey() { + new_test_ext(1).execute_with(|| { + let coldkey_id = U256::from(544); + let hotkey_id = U256::from(54544); + let other_cold_key = U256::from(99498); + let netuid = add_dynamic_network(&hotkey_id, &coldkey_id); + let stake = DefaultMinStake::::get() * 10.into(); + + // Give it some $$$ in his coldkey balance + add_balance_to_coldkey_account(&other_cold_key, stake.into()); + + // Perform the request which is signed by a different cold key + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(other_cold_key), + hotkey_id, + netuid, + stake, + )); + }); +} + +#[test] +fn test_add_stake_err_not_enough_belance() { + new_test_ext(1).execute_with(|| { + let coldkey_id = U256::from(544); + let hotkey_id = U256::from(54544); + let stake = DefaultMinStake::::get() * 10.into(); + let netuid = add_dynamic_network(&hotkey_id, &coldkey_id); + + // Lets try to stake with 0 balance in cold key account + assert!(SubtensorModule::get_coldkey_balance(&coldkey_id) < stake); + assert_err!( + SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey_id), + hotkey_id, + netuid, + stake, + ), + Error::::NotEnoughBalanceToStake + ); + }); +} + +#[test] +#[ignore] +fn test_add_stake_total_issuance_no_change() { + // When we add stake, the total issuance of the balances pallet should not change + // this is because the stake should be part of the coldkey account balance (reserved/locked) + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(561337); + let coldkey_account_id = U256::from(61337); + let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + + // Give it some $$$ in his coldkey balance + let initial_balance = 10000; + add_balance_to_coldkey_account(&coldkey_account_id, initial_balance.into()); + + // Check we have zero staked before transfer + let initial_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id); + assert_eq!(initial_stake, TaoBalance::ZERO); + + // Check total balance is equal to initial balance + let initial_total_balance = Balances::total_balance(&coldkey_account_id); + assert_eq!(initial_total_balance, initial_balance.into()); + + // Check total issuance is equal to initial balance + let initial_total_issuance = Balances::total_issuance(); + assert_eq!(initial_total_issuance, initial_balance.into()); + + // Also total stake should be zero + assert_eq!(SubtensorModule::get_total_stake(), TaoBalance::ZERO); + + // Stake to hotkey account, and check if the result is ok + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + 10000.into() + )); + + // Check if stake has increased + let new_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id); + assert_eq!(new_stake, 10000.into()); + + // Check if free balance has decreased + let new_free_balance = SubtensorModule::get_coldkey_balance(&coldkey_account_id); + assert_eq!(new_free_balance, 0.into()); + + // Check if total stake has increased accordingly. + assert_eq!(SubtensorModule::get_total_stake(), 10000.into()); + + // Check if total issuance has remained the same. (no fee, includes reserved/locked balance) + let total_issuance = Balances::total_issuance(); + assert_eq!(total_issuance, initial_total_issuance); + }); +} + +#[test] +fn test_add_stake_partial_below_min_stake_fails() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1); + let subnet_owner_hotkey = U256::from(2); + let coldkey_account_id = U256::from(4343); + let hotkey_account_id = U256::from(4968585); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); + + // Stake TAO amount is above min stake + let min_stake = DefaultMinStake::::get(); + let amount = min_stake.to_u64() * 2; + add_balance_to_coldkey_account( + &coldkey_account_id, + TaoBalance::from(amount) + ExistentialDeposit::get(), + ); + + // Setup reserves + mock::setup_reserves(netuid, (amount * 10).into(), (amount * 10).into()); + + // Force the swap to initialize + ::SwapInterface::init_swap(netuid, None); + + // Get the current price + let current_price = + ::SwapInterface::current_alpha_price(netuid.into()); + assert!(current_price.to_num::() > 0.0); + + // Set "max spend" to ~1 TAO around current price + let current_price_scaled = (current_price.to_num::() * 1_000_000_000_f64) as u64; + let max_spend = current_price_scaled.saturating_add(1); + + // Add stake with partial flag on + assert_err!( + SubtensorModule::add_stake_limit( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount.into(), + max_spend.into(), + true + ), + Error::::AmountTooLow + ); + + // Price should be unchanged on failure + let new_current_price = + ::SwapInterface::current_alpha_price(netuid.into()); + assert_eq!(new_current_price, current_price); + }); +} + +#[test] +fn test_add_stake_insufficient_liquidity() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let hotkey = U256::from(2); + let coldkey = U256::from(3); + let amount_staked = DefaultMinStake::::get().to_u64() * 10; + + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + let _ = SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + add_balance_to_coldkey_account(&coldkey, amount_staked.into()); + + // Set the liquidity at lowest possible value so that all staking requests fail + let reserve = u64::from(mock::SwapMinimumReserve::get()) - 1; + mock::setup_reserves(netuid, reserve.into(), reserve.into()); + + // Check the error + assert_noop!( + SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + amount_staked.into() + ), + Error::::InsufficientLiquidity + ); + }); +} + +/// cargo test --package pallet-subtensor --lib -- tests::staking::add_stake::test_add_stake_input_reserve_too_low_fails --exact --show-output +#[test] +fn test_add_stake_input_reserve_too_low_fails() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let hotkey = U256::from(2); + let coldkey = U256::from(3); + let amount_staked = DefaultMinStake::::get().to_u64() * 10; + + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + let _ = SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + add_balance_to_coldkey_account(&coldkey, amount_staked.into()); + + // Set the liquidity at lowest possible value so that all staking requests fail + let reserve_alpha = 1_000_000_000_u64; + let reserve_tao = u64::from(mock::SwapMinimumReserve::get()) - 1; + mock::setup_reserves(netuid, reserve_tao.into(), reserve_alpha.into()); + + // The output-side reserve is sufficient, but the input-side reserve is too small for the + // requested swap under the 1000x input-reserve cap. + assert_noop!( + SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + amount_staked.into() + ), + Error::::InsufficientLiquidity + ); + }); +} + +/// cargo test --package pallet-subtensor --lib -- tests::staking::add_stake::test_add_stake_insufficient_liquidity_one_side_fail --exact --show-output +#[test] +fn test_add_stake_insufficient_liquidity_one_side_fail() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let hotkey = U256::from(2); + let coldkey = U256::from(3); + let amount_staked = DefaultMinStake::::get().to_u64() * 10; + + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + let _ = SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + add_balance_to_coldkey_account(&coldkey, amount_staked.into()); + + // Set the liquidity at lowest possible value so that all staking requests fail + let reserve_alpha = u64::from(mock::SwapMinimumReserve::get()) - 1; + let reserve_tao = u64::from(mock::SwapMinimumReserve::get()); + mock::setup_reserves(netuid, reserve_tao.into(), reserve_alpha.into()); + + // Check the error + assert_noop!( + SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + amount_staked.into() + ), + Error::::InsufficientLiquidity + ); + }); +} + +// /*********************************************************** +// staking::increase_stake_for_hotkey_and_coldkey_on_subnet() tests +// ************************************************************/ +#[test] +fn test_add_stake_to_hotkey_account_ok() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1); + let subnet_owner_hotkey = U256::from(2); + let hotkey_id = U256::from(5445); + let coldkey_id = U256::from(5443433); + let amount: u64 = 10_000; + + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey_id, coldkey_id, 192213123); + + let base_total_stake = SubtensorModule::get_total_stake(); + + // Check stake in ALPHA units for this hotkey/coldkey/netuid triple. + let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_id, + &coldkey_id, + netuid, + ); + + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_id, + &coldkey_id, + netuid, + AlphaBalance::from(amount), + ); + + let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_id, + &coldkey_id, + netuid, + ); + + assert_eq!( + alpha_after, + alpha_before + AlphaBalance::from(amount), + "Alpha stake did not increase by the expected amount" + ); + + // Total stake should never decrease when we increase stake. + let total_stake_after = SubtensorModule::get_total_stake(); + assert!( + total_stake_after >= base_total_stake, + "Total stake unexpectedly decreased after increasing stake" + ); + }); +} + +// Verify staking too low amount is impossible +#[test] +fn test_staking_too_little_fails() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(533453); + let coldkey_account_id = U256::from(55453); + let amount = 10_000; + + //add network + let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + + // Give it some $$$ in his coldkey balance + add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); + + // Coldkey / hotkey 0 decreases take to 5%. This should fail as the minimum take is 9% + assert_err!( + SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + 1.into() + ), + Error::::AmountTooLow + ); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::staking::add_stake::test_add_stake_fee_goes_to_subnet_tao --exact --show-output --nocapture +#[ignore = "fee now goes to liquidity provider"] +#[test] +fn test_add_stake_fee_goes_to_subnet_tao() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let hotkey = U256::from(2); + let coldkey = U256::from(3); + let existential_deposit = ExistentialDeposit::get(); + let tao_to_stake = DefaultMinStake::::get() * 10.into(); + + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + let _ = SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + let subnet_tao_before = SubnetTAO::::get(netuid); + + // Add stake + add_balance_to_coldkey_account(&coldkey, tao_to_stake); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + tao_to_stake + )); + + // Calculate expected stake + let expected_alpha = AlphaBalance::from((tao_to_stake - existential_deposit).to_u64()); + let actual_alpha = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + let subnet_tao_after = SubnetTAO::::get(netuid); + + // Total subnet stake should match the sum of delegators' stakes minus existential deposits. + assert_abs_diff_eq!( + actual_alpha, + expected_alpha, + epsilon = expected_alpha / 1000.into() + ); + + // Subnet TAO should have increased by the full tao_to_stake amount + assert_abs_diff_eq!( + subnet_tao_before + tao_to_stake, + subnet_tao_after, + epsilon = 10.into() + ); + }); +} + +#[test] +fn test_stake_overflow() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let coldkey_account_id = U256::from(435445); + let hotkey_account_id = U256::from(54544); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); + + // Maximum possible: Max TAO supply less already-issued balance. + let amount = 21_000_000_000_000_000_u64 - u64::from(Balances::total_issuance()); + + // Give it some $$$ in his coldkey balance + add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); + + // Setup liquidity with 21M TAO values + mock::setup_reserves(netuid, amount.into(), amount.into()); + + let total_stake_before = SubtensorModule::get_total_stake(); + + // Stake and check if the result is ok + let (expected_alpha, _) = mock::swap_tao_to_alpha(netuid, amount.into()); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount.into() + )); + + // Check if stake has increased properly + assert_abs_diff_eq!( + SubtensorModule::get_stake_for_hotkey_on_subnet(&hotkey_account_id, netuid), + expected_alpha, + epsilon = 1.into() + ); + + // Check if total stake has increased accordingly. + assert_abs_diff_eq!( + SubtensorModule::get_total_stake(), + total_stake_before + amount.into(), + epsilon = 1.into() + ); + }); +} + +#[test] +// RUST_LOG=info cargo test --package pallet-subtensor --lib -- tests::staking::add_stake::test_add_stake_specific_stake_into_subnet_fail --exact --show-output +fn test_add_stake_specific_stake_into_subnet_fail() { + new_test_ext(1).execute_with(|| { + let sn_owner_coldkey = U256::from(55453); + + let hotkey_account_id = U256::from(533453); + let coldkey_account_id = U256::from(55454); + let hotkey_owner_account_id = U256::from(533454); + + let existing_shares: U64F64 = + U64F64::from_num(161_986_254).saturating_div(U64F64::from_num(u64::MAX)); + let existing_stake = AlphaBalance::from(36_711_495_953_u64); + + let tao_in = TaoBalance::from(2_409_892_148_947_u64); + let alpha_in = AlphaBalance::from(15_358_708_513_716_u64); + + let tao_staked = TaoBalance::from(200_000_000); + + //add network + let netuid = add_dynamic_network(&sn_owner_coldkey, &sn_owner_coldkey); + + // Register hotkey on netuid + register_ok_neuron(netuid, hotkey_account_id, hotkey_owner_account_id, 0); + // Check we have zero staked + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), + TaoBalance::ZERO + ); + + // Set a hotkey pool for the hotkey + let mut hotkey_pool = SubtensorModule::get_alpha_share_pool(hotkey_account_id, netuid); + hotkey_pool.update_value_for_one(&hotkey_owner_account_id, 1234); // Doesn't matter, will be overridden + + // Adjust the total hotkey stake and shares to match the existing values + TotalHotkeyShares::::insert(hotkey_account_id, netuid, existing_shares); + TotalHotkeyAlpha::::insert(hotkey_account_id, netuid, existing_stake); + + // Make the hotkey a delegate + Delegates::::insert(hotkey_account_id, PerU16::zero()); + + // Setup Subnet pool + SubnetAlphaIn::::insert(netuid, alpha_in); + SubnetTAO::::insert(netuid, tao_in); + + // Give TAO balance to coldkey + add_balance_to_coldkey_account(&coldkey_account_id, tao_staked + 1_000_000_000.into()); + + // Add stake as new hotkey + let order = GetAlphaForTao::::with_amount(tao_staked); + let expected_alpha = ::SwapInterface::swap( + netuid.into(), + order, + ::SwapInterface::max_price(), + false, + true, + ) + .map(|v| v.amount_paid_out) + .unwrap_or_default(); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + tao_staked, + )); + + // Check we have non-zero staked + assert!(expected_alpha > AlphaBalance::ZERO); + assert_abs_diff_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid + ), + expected_alpha, + epsilon = expected_alpha / 1000.into() + ); + }); +} + +#[test] +fn test_stake_into_subnet_ok() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let hotkey = U256::from(3); + let coldkey = U256::from(4); + let amount = 100_000_000; + + // add network + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + // Forse-set alpha in and tao reserve to make price equal 0.01 + let tao_reserve = TaoBalance::from(100_000_000_000_u64); + let alpha_in = AlphaBalance::from(1_000_000_000_000_u64); + mock::setup_reserves(netuid, tao_reserve, alpha_in); + let current_price = + ::SwapInterface::current_alpha_price(netuid.into()) + .to_num::(); + + // Initialize swap v3 + let order = GetAlphaForTao::::with_amount(0); + assert_ok!(::SwapInterface::swap( + netuid.into(), + order, + TaoBalance::MAX, + false, + true + )); + + // Add stake with slippage safety and check if the result is ok + let large_balance = 20_000_000_000_000_000_u64; + add_balance_to_coldkey_account(&coldkey, large_balance.into()); + assert_ok!(SubtensorModule::stake_into_subnet( + &hotkey, + &coldkey, + netuid, + amount.into(), + large_balance.into(), + false, + )); + let fee_rate = pallet_subtensor_swap::FeeRate::::get(NetUid::from(netuid)) as f64 + / u16::MAX as f64; + let expected_stake = (amount as f64) * (1. - fee_rate) / current_price; + + // Check if stake has increased + assert_abs_diff_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid) + .to_u64() as f64, + expected_stake, + epsilon = expected_stake / 1000., + ); + }); +} + +#[test] +fn test_stake_into_subnet_low_amount() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let hotkey = U256::from(3); + let coldkey = U256::from(4); + let amount = 10; + + // add network + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + // Forse-set alpha in and tao reserve to make price equal 0.1 + let tao_reserve = TaoBalance::from(100_000_000_000_u64); + let alpha_in = AlphaBalance::from(1_000_000_000_000_u64); + mock::setup_reserves(netuid, tao_reserve, alpha_in); + let current_price = + ::SwapInterface::current_alpha_price(netuid.into()) + .to_num::(); + + // Initialize swap + let order = GetAlphaForTao::::with_amount(0); + assert_ok!(::SwapInterface::swap( + netuid.into(), + order, + TaoBalance::MAX, + false, + true + )); + + // Add stake with slippage safety and check if the result is ok + let large_balance = 20_000_000_000_000_000_u64; + add_balance_to_coldkey_account(&coldkey, large_balance.into()); + assert_ok!(SubtensorModule::stake_into_subnet( + &hotkey, + &coldkey, + netuid, + amount.into(), + large_balance.into(), + false, + )); + let expected_stake = (amount as f64) * 0.997 / current_price; + + // Check if stake has increased + assert_abs_diff_eq!( + u64::from(SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid + )) as f64, + expected_stake, + epsilon = expected_stake / 100. + ); + }); +} + +#[test] +fn test_stake_into_subnet_prohibitive_limit() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let coldkey = U256::from(4); + let amount = 100_000_000; + + // add network + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + add_balance_to_coldkey_account(&coldkey, amount.into()); + + // Forse-set alpha in and tao reserve to make price equal 0.01 + let tao_reserve = TaoBalance::from(100_000_000_000_u64); + let alpha_in = AlphaBalance::from(1_000_000_000_000_u64); + mock::setup_reserves(netuid, tao_reserve, alpha_in); + + // Initialize swap v3 + let order = GetAlphaForTao::::with_amount(0); + assert_ok!(::SwapInterface::swap( + netuid.into(), + order, + TaoBalance::MAX, + false, + true + )); + + // Add stake and check if the result is ok + // Use prohibitive limit price + assert_err!( + SubtensorModule::add_stake_limit( + RuntimeOrigin::signed(coldkey), + owner_hotkey, + netuid, + amount.into(), + TaoBalance::ZERO, + true, + ), + DispatchError::from(pallet_subtensor_swap::Error::::PriceLimitExceeded) + ); + + // Check if stake has NOT increased + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &owner_hotkey, + &coldkey, + netuid + ), + AlphaBalance::ZERO + ); + + // Check if balance has NOT decreased + assert_eq!( + SubtensorModule::get_coldkey_balance(&coldkey), + amount.into() + ); + }); +} + +#[test] +fn test_increase_stake_for_hotkey_and_coldkey_on_subnet_adds_to_staking_hotkeys_map() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let coldkey1 = U256::from(2); + let hotkey = U256::from(3); + + let netuid = NetUid::from(1); + let stake_amount = 100_000_000_000_u64; + + // Check no entry in the staking hotkeys map + assert!(!StakingHotkeys::::contains_key(coldkey)); + // insert manually + StakingHotkeys::::insert(coldkey, Vec::::new()); + // check entry has no hotkey + assert!(!StakingHotkeys::::get(coldkey).contains(&hotkey)); + + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + stake_amount.into(), + ); + + // Check entry exists in the staking hotkeys map + assert!(StakingHotkeys::::contains_key(coldkey)); + // check entry has hotkey + assert!(StakingHotkeys::::get(coldkey).contains(&hotkey)); + + // Check no entry in the staking hotkeys map for coldkey1 + assert!(!StakingHotkeys::::contains_key(coldkey1)); + + // Run increase stake for hotkey and coldkey1 on subnet + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey1, + netuid, + stake_amount.into(), + ); + + // Check entry exists in the staking hotkeys map for coldkey1 + assert!(StakingHotkeys::::contains_key(coldkey1)); + // check entry has hotkey + assert!(StakingHotkeys::::get(coldkey1).contains(&hotkey)); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::staking::add_stake::test_add_root_updates_counters --exact --show-output +#[test] +fn test_add_root_updates_counters() { + new_test_ext(0).execute_with(|| { + let hotkey_account_id = U256::from(561337); + let coldkey_account_id = U256::from(61337); + add_network(NetUid::ROOT, 10, 0); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(coldkey_account_id).clone(), + hotkey_account_id, + )); + let stake_amount = TaoBalance::from(1_000_000_000_u64); + + // Give it some $$$ in his coldkey balance + let initial_balance = stake_amount + ExistentialDeposit::get(); + add_balance_to_coldkey_account(&coldkey_account_id, initial_balance); + + // Setup SubnetAlphaIn (because we are going to stake) + SubnetAlphaIn::::insert(NetUid::ROOT, AlphaBalance::from(stake_amount.to_u64())); + + // Stake to hotkey account, and check if the result is ok + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + NetUid::ROOT, + stake_amount + )); + + // Check if stake has increased + let new_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id); + assert_eq!(new_stake, stake_amount); + + // Check if total stake has increased accordingly. + assert_eq!(SubtensorModule::get_total_stake(), stake_amount); + + // SubnetTAO updated + assert_eq!(SubnetTAO::::get(NetUid::ROOT), stake_amount); + + // SubnetAlphaIn updated + assert_eq!(SubnetAlphaIn::::get(NetUid::ROOT), 0.into()); + + // SubnetAlphaOut updated + assert_eq!( + SubnetAlphaOut::::get(NetUid::ROOT), + AlphaBalance::from(stake_amount.to_u64()) + ); + + // SubnetVolume updated + assert_eq!( + SubnetVolume::::get(NetUid::ROOT), + stake_amount.to_u64() as u128 + ); + }); +} diff --git a/pallets/subtensor/src/tests/staking/add_stake_limit.rs b/pallets/subtensor/src/tests/staking/add_stake_limit.rs new file mode 100644 index 0000000000..6ecdeebc3d --- /dev/null +++ b/pallets/subtensor/src/tests/staking/add_stake_limit.rs @@ -0,0 +1,395 @@ +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] +//! Tests for [`crate::staking::add_stake`] limit / max-amount add paths. + +use approx::assert_abs_diff_eq; +use frame_support::sp_runtime::DispatchError; +use frame_support::{assert_err, assert_noop, assert_ok}; +use sp_core::U256; +use substrate_fixed::types::U96F32; +use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; +use subtensor_swap_interface::SwapHandler; + +use super::super::mock; +use super::super::mock::*; +use crate::*; + +#[test] +fn test_max_amount_add_root() { + new_test_ext(0).execute_with(|| { + // 0 price on root => max is 0 + assert_eq!( + SubtensorModule::get_max_amount_add(NetUid::ROOT, TaoBalance::ZERO), + Ok(0u64.into()) + ); + + // 0.999999... price on root => max is 0 + assert_eq!( + SubtensorModule::get_max_amount_add(NetUid::ROOT, TaoBalance::from(999_999_999)), + Ok(0u64.into()) + ); + + // 1.0 price on root => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_add(NetUid::ROOT, TaoBalance::from(1_000_000_000)), + Ok(u64::MAX) + ); + + // 1.000...001 price on root => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_add(NetUid::ROOT, TaoBalance::from(1_000_000_001)), + Ok(u64::MAX) + ); + + // 2.0 price on root => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_add(NetUid::ROOT, TaoBalance::from(2_000_000_000)), + Ok(u64::MAX) + ); + }); +} + +#[test] +fn test_max_amount_add_stable() { + new_test_ext(0).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + + // 0 price => max is 0 + assert_eq!( + SubtensorModule::get_max_amount_add(netuid, TaoBalance::ZERO), + Ok(0u64.into()) + ); + + // 0.999999... price => max is 0 + assert_eq!( + SubtensorModule::get_max_amount_add(netuid, TaoBalance::from(999_999_999)), + Ok(0u64.into()) + ); + + // 1.0 price => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_add(netuid, TaoBalance::from(1_000_000_000)), + Ok(u64::MAX) + ); + + // 1.000...001 price => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_add(netuid, TaoBalance::from(1_000_000_001)), + Ok(u64::MAX) + ); + + // 2.0 price => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_add(netuid, TaoBalance::from(2_000_000_000)), + Ok(u64::MAX) + ); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::staking::add_stake_limit::test_max_amount_add_dynamic --exact --show-output +#[test] +fn test_max_amount_add_dynamic() { + // tao_in, alpha_in, limit_price, expected_max_swappable (with 0.05% fees) + [ + // Zero handling (no panics) + ( + 1_000_000_000, + 1_000_000_000, + 0, + Err(DispatchError::from( + pallet_subtensor_swap::Error::::PriceLimitExceeded, + )), + ), + // Low bounds + (100, 100, 1_100_000_000, Ok(4)), + (1_000, 1_000, 1_100_000_000, Ok(48)), + (10_000, 10_000, 1_100_000_000, Ok(488)), + // Basic math + (1_000_000, 1_000_000, 4_000_000_000, Ok(1_000_500)), + (1_000_000, 1_000_000, 9_000_000_000, Ok(2_001_000)), + (1_000_000, 1_000_000, 16_000_000_000, Ok(3_001_500)), + ( + 1_000_000_000_000, + 1_000_000_000_000, + 16_000_000_000, + Ok(3_001_500_000_000), + ), + // Normal range values with edge cases + ( + 150_000_000_000, + 100_000_000_000, + 0, + Err(DispatchError::from( + pallet_subtensor_swap::Error::::PriceLimitExceeded, + )), + ), + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000, + Err(DispatchError::from( + pallet_subtensor_swap::Error::::PriceLimitExceeded, + )), + ), + ( + 150_000_000_000, + 100_000_000_000, + 500_000_000, + Err(DispatchError::from( + pallet_subtensor_swap::Error::::PriceLimitExceeded, + )), + ), + ( + 150_000_000_000, + 100_000_000_000, + 1_499_999_999, + Err(DispatchError::from( + pallet_subtensor_swap::Error::::PriceLimitExceeded, + )), + ), + ( + 150_000_000_000, + 100_000_000_000, + 1_500_000_000, + Err(DispatchError::from( + pallet_subtensor_swap::Error::::PriceLimitExceeded, + )), + ), + (150_000_000_000, 100_000_000_000, 1_500_000_001, Ok(49)), + ( + 150_000_000_000, + 100_000_000_000, + 6_000_000_000, + Ok(150_075_000_000), + ), + // Miscellaneous overflows and underflows + (u64::MAX / 2, u64::MAX, u64::MAX, Ok(u64::MAX)), + ] + .into_iter() + .for_each(|(tao_in, alpha_in, limit_price, expected_max_swappable)| { + new_test_ext(0).execute_with(|| { + let alpha_in = AlphaBalance::from(alpha_in); + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + // Forse-set alpha in and tao reserve to achieve relative price of subnets + SubnetTAO::::insert(netuid, TaoBalance::from(tao_in)); + SubnetAlphaIn::::insert(netuid, alpha_in); + + // Force the swap to initialize + ::SwapInterface::init_swap(netuid, None); + + if !alpha_in.is_zero() { + let expected_price = U96F32::from_num(tao_in) / U96F32::from_num(alpha_in); + assert_abs_diff_eq!( + ::SwapInterface::current_alpha_price(netuid.into()) + .to_num::(), + expected_price.to_num::(), + epsilon = expected_price.to_num::() / 1_000_f64 + ); + } + + match expected_max_swappable { + Err(e) => assert_err!( + SubtensorModule::get_max_amount_add(netuid, limit_price.into()), + e + ), + Ok(v) => assert_abs_diff_eq!( + SubtensorModule::get_max_amount_add(netuid, limit_price.into()).unwrap(), + v, + epsilon = v / 10000 + ), + } + }); + }); +} + +#[test] +fn test_add_stake_limit_ok() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(533453); + let coldkey_account_id = U256::from(55453); + let amount = 900_000_000_000; // over the maximum + + // add network + let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + remove_owner_registration_stake(netuid); + + // Forse-set alpha in and tao reserve to make price equal 1.5 + let tao_reserve = TaoBalance::from(150_000_000_000_u64); + let alpha_in = AlphaBalance::from(100_000_000_000_u64); + mock::setup_reserves(netuid, tao_reserve, alpha_in); + let current_price = + ::SwapInterface::current_alpha_price(netuid.into()); + assert_eq!(current_price, U96F32::from_num(1.5)); + + // Give it some $$$ in his coldkey balance + add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); + + // Setup limit price so that it doesn't peak above 4x of current price + // The amount that can be executed at this price is 450 TAO only + // Alpha produced will be equal to 75 = 450*100/(450+150) + let limit_price = TaoBalance::from(24_000_000_000_u64); + let expected_executed_stake = AlphaBalance::from(75_000_000_000_u64); + + // Add stake with slippage safety and check if the result is ok + assert_ok!(SubtensorModule::add_stake_limit( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount.into(), + limit_price, + true + )); + + // Check if stake has increased only by 75 Alpha + assert_abs_diff_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid + ), + expected_executed_stake, + epsilon = expected_executed_stake / 1000.into(), + ); + + // Check that 450 TAO less fees balance still remains free on coldkey + let fee = ::SwapInterface::approx_fee_amount( + netuid.into(), + TaoBalance::from(amount / 2), + ) + .to_u64() as f64; + assert_abs_diff_eq!( + SubtensorModule::get_coldkey_balance(&coldkey_account_id), + (amount / 2 - fee as u64).into(), + epsilon = (amount / 2 / 1000).into() + ); + + // Check that price has updated to ~24 = (150+450) / (100 - 75) + let exp_price = U96F32::from_num(24.0); + let current_price = + ::SwapInterface::current_alpha_price(netuid.into()); + assert_abs_diff_eq!( + exp_price.to_num::(), + current_price.to_num::(), + epsilon = 0.001, + ); + }); +} + +#[test] +fn test_add_stake_limit_fill_or_kill() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(533453); + let coldkey_account_id = U256::from(55453); + let amount = 900_000_000_000_u64; // over the maximum + + // add network + let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + + // Force-set alpha in and tao reserve to make price equal 1.5 + let tao_reserve = TaoBalance::from(150_000_000_000_u64); + let alpha_in = AlphaBalance::from(100_000_000_000_u64); + SubnetTAO::::insert(netuid, tao_reserve); + SubnetAlphaIn::::insert(netuid, alpha_in); + let current_price = + ::SwapInterface::current_alpha_price(netuid.into()); + // FIXME it's failing because in the swap pallet, the alpha price is set only after an + // initial swap + assert_eq!(current_price, U96F32::from_num(1.5)); + + // Give it some $$$ in his coldkey balance + add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); + + // Setup limit price so that it doesn't peak above 4x of current price + // The amount that can be executed at this price is 450 TAO only + // Alpha produced will be equal to 25 = 100 - 450*100/(150+450) + let limit_price = TaoBalance::from(24_000_000_000_u64); + + // Add stake with slippage safety and check if it fails + assert_noop!( + SubtensorModule::add_stake_limit( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount.into(), + limit_price, + false + ), + Error::::SlippageTooHigh + ); + + // Lower the amount and it should succeed now + let amount_ok = TaoBalance::from(150_000_000_000_u64); // fits the maximum + assert_ok!(SubtensorModule::add_stake_limit( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount_ok, + limit_price, + false + )); + }); +} + +#[test] +fn test_add_stake_limit_rejects_input_over_swap_reserve_cap() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(533454); + let coldkey_account_id = U256::from(55454); + + let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + let tao_reserve = TaoBalance::from(1_000_u64); + mock::setup_reserves(netuid, tao_reserve, AlphaBalance::from(1_000_000_000_u64)); + + let amount = tao_reserve.saturating_mul(1_000.into()) + TaoBalance::from(1_u64); + add_balance_to_coldkey_account(&coldkey_account_id, amount); + + assert_noop!( + SubtensorModule::add_stake_limit( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount, + ::SwapInterface::max_price(), + true + ), + Error::::InsufficientLiquidity + ); + }); +} + +#[test] +fn test_add_stake_limit_partial_zero_max_stake_amount_error() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(533453); + let coldkey_account_id = U256::from(55453); + + // Exact values from the error: + // https://taostats.io/extrinsic/5338471-0009?network=finney + let amount = 19980000000_u64; + let limit_price = TaoBalance::from(26953618); + let tao_reserve = TaoBalance::from(5_032_494_439_940_u64); + let alpha_in = AlphaBalance::from(186_268_425_402_874_u64); + + let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + SubnetTAO::::insert(netuid, tao_reserve); + SubnetAlphaIn::::insert(netuid, alpha_in); + + add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); + + assert_noop!( + SubtensorModule::add_stake_limit( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount.into(), + limit_price, + true + ), + DispatchError::from(pallet_subtensor_swap::Error::::PriceLimitExceeded) + ); + }); +} diff --git a/pallets/subtensor/src/tests/staking/delegate_take.rs b/pallets/subtensor/src/tests/staking/delegate_take.rs new file mode 100644 index 0000000000..7829313a03 --- /dev/null +++ b/pallets/subtensor/src/tests/staking/delegate_take.rs @@ -0,0 +1,439 @@ +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] +//! Tests for [`crate::staking::increase_take`] / [`crate::staking::decrease_take`]. + +use frame_support::dispatch::{DispatchClass, GetDispatchInfo, Pays}; +use frame_support::{assert_err, assert_ok}; +use sp_core::U256; +use sp_runtime::PerU16; +use subtensor_runtime_common::NetUid; + +use super::super::mock::*; +use crate::*; + +/*********************************************************** + staking::delegate_take tests +************************************************************/ + +#[test] +fn test_delegate_take_dispatch_info_pays_fee() { + new_test_ext(1).execute_with(|| { + let hotkey = U256::from(1); + let take = PerU16::from_parts(SubtensorModule::get_min_delegate_take()); + + let decrease_take_call = + RuntimeCall::SubtensorModule(SubtensorCall::decrease_take { hotkey, take }); + let decrease_take_dispatch_info = decrease_take_call.get_dispatch_info(); + assert_eq!(decrease_take_dispatch_info.class, DispatchClass::Normal); + assert_eq!(decrease_take_dispatch_info.pays_fee, Pays::Yes); + + let increase_take_call = + RuntimeCall::SubtensorModule(SubtensorCall::increase_take { hotkey, take }); + let increase_take_dispatch_info = increase_take_call.get_dispatch_info(); + assert_eq!(increase_take_dispatch_info.class, DispatchClass::Normal); + assert_eq!(increase_take_dispatch_info.pays_fee, Pays::Yes); + }); +} + +// Verify delegate take can be decreased +#[test] +fn test_delegate_take_can_be_decreased() { + new_test_ext(1).execute_with(|| { + // Make account + let hotkey0 = U256::from(1); + let coldkey0 = U256::from(3); + + // Add balance + add_balance_to_coldkey_account(&coldkey0, 100000.into()); + + // Register the neuron to a new network + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey0, coldkey0, 124124); + + // Coldkey / hotkey 0 become delegates with 9% take + Delegates::::insert( + hotkey0, + PerU16::from_parts(SubtensorModule::get_min_delegate_take()), + ); + assert_eq!( + SubtensorModule::get_hotkey_take(&hotkey0), + SubtensorModule::get_min_delegate_take() + ); + + // Coldkey / hotkey 0 decreases take to 5%. This should fail as the minimum take is 9% + assert_err!( + SubtensorModule::do_decrease_take( + RuntimeOrigin::signed(coldkey0), + hotkey0, + PerU16::from_parts(u16::MAX / 20) + ), + Error::::DelegateTakeTooLow + ); + }); +} + +// Verify delegate take can be decreased +#[test] +fn test_can_set_min_take_ok() { + new_test_ext(1).execute_with(|| { + // Make account + let hotkey0 = U256::from(1); + let coldkey0 = U256::from(3); + + // Add balance + add_balance_to_coldkey_account(&coldkey0, 100000.into()); + + // Register the neuron to a new network + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey0, coldkey0, 124124); + + // Coldkey / hotkey 0 become delegates + Delegates::::insert(hotkey0, PerU16::from_parts(u16::MAX / 10)); + + // Coldkey / hotkey 0 decreases take to min + assert_ok!(SubtensorModule::do_decrease_take( + RuntimeOrigin::signed(coldkey0), + hotkey0, + PerU16::from_parts(SubtensorModule::get_min_delegate_take()) + )); + assert_eq!( + SubtensorModule::get_hotkey_take(&hotkey0), + SubtensorModule::get_min_delegate_take() + ); + }); +} + +// Verify delegate take can not be increased with do_decrease_take +#[test] +fn test_delegate_take_can_not_be_increased_with_decrease_take() { + new_test_ext(1).execute_with(|| { + // Make account + let hotkey0 = U256::from(1); + let coldkey0 = U256::from(3); + + // Add balance + add_balance_to_coldkey_account(&coldkey0, 100000.into()); + + // Register the neuron to a new network + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey0, coldkey0, 124124); + + // Set min take + Delegates::::insert( + hotkey0, + PerU16::from_parts(SubtensorModule::get_min_delegate_take()), + ); + + // Coldkey / hotkey 0 tries to increase take to 12.5% + assert_eq!( + SubtensorModule::do_decrease_take( + RuntimeOrigin::signed(coldkey0), + hotkey0, + PerU16::from_parts(SubtensorModule::get_max_delegate_take()) + ), + Err(Error::::DelegateTakeTooLow.into()) + ); + assert_eq!( + SubtensorModule::get_hotkey_take(&hotkey0), + SubtensorModule::get_min_delegate_take() + ); + }); +} + +// Verify delegate take can be increased +#[test] +fn test_delegate_take_can_be_increased() { + new_test_ext(1).execute_with(|| { + // Make account + let hotkey0 = U256::from(1); + let coldkey0 = U256::from(3); + + // Add balance + add_balance_to_coldkey_account(&coldkey0, 100000.into()); + + // Register the neuron to a new network + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey0, coldkey0, 124124); + + // Coldkey / hotkey 0 become delegates with 9% take + Delegates::::insert( + hotkey0, + PerU16::from_parts(SubtensorModule::get_min_delegate_take()), + ); + assert_eq!( + SubtensorModule::get_hotkey_take(&hotkey0), + SubtensorModule::get_min_delegate_take() + ); + + step_block(1 + InitialTxDelegateTakeRateLimit::get() as u16); + + // Coldkey / hotkey 0 decreases take to 12.5% + assert_ok!(SubtensorModule::do_increase_take( + RuntimeOrigin::signed(coldkey0), + hotkey0, + PerU16::from_parts(u16::MAX / 8) + )); + assert_eq!(SubtensorModule::get_hotkey_take(&hotkey0), u16::MAX / 8); + }); +} + +// Verify delegate take can not be decreased with increase_take +#[test] +fn test_delegate_take_can_not_be_decreased_with_increase_take() { + new_test_ext(1).execute_with(|| { + // Make account + let hotkey0 = U256::from(1); + let coldkey0 = U256::from(3); + + // Add balance + add_balance_to_coldkey_account(&coldkey0, 100000.into()); + + // Register the neuron to a new network + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey0, coldkey0, 124124); + + // Coldkey / hotkey 0 become delegates with 9% take + Delegates::::insert( + hotkey0, + PerU16::from_parts(SubtensorModule::get_min_delegate_take()), + ); + assert_eq!( + SubtensorModule::get_hotkey_take(&hotkey0), + SubtensorModule::get_min_delegate_take() + ); + + // Coldkey / hotkey 0 tries to decrease take to 5% + assert_eq!( + SubtensorModule::do_increase_take( + RuntimeOrigin::signed(coldkey0), + hotkey0, + PerU16::from_parts(u16::MAX / 20) + ), + Err(Error::::DelegateTakeTooLow.into()) + ); + assert_eq!( + SubtensorModule::get_hotkey_take(&hotkey0), + SubtensorModule::get_min_delegate_take() + ); + }); +} + +// Verify delegate take can be increased up to InitialDefaultDelegateTake (18%) +#[test] +fn test_delegate_take_can_be_increased_to_limit() { + new_test_ext(1).execute_with(|| { + // Make account + let hotkey0 = U256::from(1); + let coldkey0 = U256::from(3); + + // Add balance + add_balance_to_coldkey_account(&coldkey0, 100000.into()); + + // Register the neuron to a new network + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey0, coldkey0, 124124); + + // Coldkey / hotkey 0 become delegates with 9% take + Delegates::::insert( + hotkey0, + PerU16::from_parts(SubtensorModule::get_min_delegate_take()), + ); + assert_eq!( + SubtensorModule::get_hotkey_take(&hotkey0), + SubtensorModule::get_min_delegate_take() + ); + + step_block(1 + InitialTxDelegateTakeRateLimit::get() as u16); + + // Coldkey / hotkey 0 tries to increase take to InitialDefaultDelegateTake+1 + assert_ok!(SubtensorModule::do_increase_take( + RuntimeOrigin::signed(coldkey0), + hotkey0, + PerU16::from_parts(InitialDefaultDelegateTake::get()) + )); + assert_eq!( + SubtensorModule::get_hotkey_take(&hotkey0), + InitialDefaultDelegateTake::get() + ); + }); +} + +// Verify delegate take can not be increased above InitialDefaultDelegateTake (18%) +#[test] +fn test_delegate_take_can_not_be_increased_beyond_limit() { + new_test_ext(1).execute_with(|| { + // Make account + let hotkey0 = U256::from(1); + let coldkey0 = U256::from(3); + + // Add balance + add_balance_to_coldkey_account(&coldkey0, 100000.into()); + + // Register the neuron to a new network + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey0, coldkey0, 124124); + + // Coldkey / hotkey 0 become delegates with 9% take + Delegates::::insert( + hotkey0, + PerU16::from_parts(SubtensorModule::get_min_delegate_take()), + ); + assert_eq!( + SubtensorModule::get_hotkey_take(&hotkey0), + SubtensorModule::get_min_delegate_take() + ); + + // Coldkey / hotkey 0 tries to increase take to InitialDefaultDelegateTake+1 + // (Disable this check if InitialDefaultDelegateTake is u16::MAX) + if InitialDefaultDelegateTake::get() != u16::MAX { + assert_eq!( + SubtensorModule::do_increase_take( + RuntimeOrigin::signed(coldkey0), + hotkey0, + PerU16::from_parts(InitialDefaultDelegateTake::get() + 1) + ), + Err(Error::::DelegateTakeTooHigh.into()) + ); + } + assert_eq!( + SubtensorModule::get_hotkey_take(&hotkey0), + SubtensorModule::get_min_delegate_take() + ); + }); +} + +// Test rate-limiting on increase_take +#[test] +fn test_rate_limits_enforced_on_increase_take() { + new_test_ext(1).execute_with(|| { + // Make account + let hotkey0 = U256::from(1); + let coldkey0 = U256::from(3); + + // Add balance + add_balance_to_coldkey_account(&coldkey0, 100000.into()); + + // Register the neuron to a new network + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey0, coldkey0, 124124); + + // Coldkey / hotkey 0 become delegates with 9% take + Delegates::::insert( + hotkey0, + PerU16::from_parts(SubtensorModule::get_min_delegate_take()), + ); + assert_eq!( + SubtensorModule::get_hotkey_take(&hotkey0), + SubtensorModule::get_min_delegate_take() + ); + + // Increase take first time + assert_ok!(SubtensorModule::do_increase_take( + RuntimeOrigin::signed(coldkey0), + hotkey0, + PerU16::from_parts(SubtensorModule::get_min_delegate_take() + 1) + )); + + // Increase again + assert_eq!( + SubtensorModule::do_increase_take( + RuntimeOrigin::signed(coldkey0), + hotkey0, + PerU16::from_parts(SubtensorModule::get_min_delegate_take() + 2) + ), + Err(Error::::DelegateTxRateLimitExceeded.into()) + ); + assert_eq!( + SubtensorModule::get_hotkey_take(&hotkey0), + SubtensorModule::get_min_delegate_take() + 1 + ); + + step_block(1 + InitialTxDelegateTakeRateLimit::get() as u16); + + // Can increase after waiting + assert_ok!(SubtensorModule::do_increase_take( + RuntimeOrigin::signed(coldkey0), + hotkey0, + PerU16::from_parts(SubtensorModule::get_min_delegate_take() + 2) + )); + assert_eq!( + SubtensorModule::get_hotkey_take(&hotkey0), + SubtensorModule::get_min_delegate_take() + 2 + ); + }); +} + +// Test rate-limiting on an increase take just after a decrease take +// Prevents a Validator from decreasing take and then increasing it immediately after. +#[test] +fn test_rate_limits_enforced_on_decrease_before_increase_take() { + new_test_ext(1).execute_with(|| { + // Make account + let hotkey0 = U256::from(1); + let coldkey0 = U256::from(3); + + // Add balance + add_balance_to_coldkey_account(&coldkey0, 100000.into()); + + // Register the neuron to a new network + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey0, coldkey0, 124124); + + // Coldkey / hotkey 0 become delegates with 9% take + Delegates::::insert( + hotkey0, + PerU16::from_parts(SubtensorModule::get_min_delegate_take() + 1), + ); + assert_eq!( + SubtensorModule::get_hotkey_take(&hotkey0), + SubtensorModule::get_min_delegate_take() + 1 + ); + + // Decrease take + assert_ok!(SubtensorModule::do_decrease_take( + RuntimeOrigin::signed(coldkey0), + hotkey0, + PerU16::from_parts(SubtensorModule::get_min_delegate_take()) + )); // Verify decrease + assert_eq!( + SubtensorModule::get_hotkey_take(&hotkey0), + SubtensorModule::get_min_delegate_take() + ); + + // Increase take immediately after + assert_eq!( + SubtensorModule::do_increase_take( + RuntimeOrigin::signed(coldkey0), + hotkey0, + PerU16::from_parts(SubtensorModule::get_min_delegate_take() + 1) + ), + Err(Error::::DelegateTxRateLimitExceeded.into()) + ); // Verify no change + assert_eq!( + SubtensorModule::get_hotkey_take(&hotkey0), + SubtensorModule::get_min_delegate_take() + ); + + step_block(1 + InitialTxDelegateTakeRateLimit::get() as u16); + + // Can increase after waiting + assert_ok!(SubtensorModule::do_increase_take( + RuntimeOrigin::signed(coldkey0), + hotkey0, + PerU16::from_parts(SubtensorModule::get_min_delegate_take() + 1) + )); // Verify increase + assert_eq!( + SubtensorModule::get_hotkey_take(&hotkey0), + SubtensorModule::get_min_delegate_take() + 1 + ); + }); +} diff --git a/pallets/subtensor/src/tests/staking/helpers.rs b/pallets/subtensor/src/tests/staking/helpers.rs new file mode 100644 index 0000000000..ed33d012ba --- /dev/null +++ b/pallets/subtensor/src/tests/staking/helpers.rs @@ -0,0 +1,1139 @@ +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] +//! Tests for [`crate::staking::helpers`] balances, ownership, nominations, and stake totals. + +use approx::assert_abs_diff_eq; +use frame_support::{assert_err, assert_ok}; +use sp_core::{Get, H256, U256}; +use sp_runtime::PerU16; +use substrate_fixed::types::U96F32; +use subtensor_runtime_common::{AlphaBalance, NetUid, NetUidStorageIndex, TaoBalance, Token}; +use subtensor_swap_interface::SwapHandler; + +use super::super::mock; +use super::super::mock::*; +use crate::*; + +#[test] +fn test_dividends_with_run_to_block() { + new_test_ext(1).execute_with(|| { + let neuron_src_hotkey_id = U256::from(1); + let neuron_dest_hotkey_id = U256::from(2); + let coldkey_account_id = U256::from(667); + let hotkey_account_id = U256::from(668); + let initial_stake: u64 = 5000; + + // add network + let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + Tempo::::insert(netuid, 13); + + // Register neuron(s) + SubtensorModule::set_max_registrations_per_block(netuid, 3); + SubtensorModule::set_max_allowed_uids(1.into(), 5); + + register_ok_neuron(netuid, neuron_src_hotkey_id, coldkey_account_id, 192213123); + register_ok_neuron(netuid, neuron_dest_hotkey_id, coldkey_account_id, 12323); + + // Add some stake to src in ALPHA units. + let src_alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &neuron_src_hotkey_id, + &coldkey_account_id, + netuid, + ); + + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &neuron_src_hotkey_id, + &coldkey_account_id, + netuid, + AlphaBalance::from(initial_stake), + ); + + let src_alpha_after_add = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &neuron_src_hotkey_id, + &coldkey_account_id, + netuid, + ); + + assert_eq!( + src_alpha_after_add, + src_alpha_before + AlphaBalance::from(initial_stake), + "Src alpha stake did not increase correctly" + ); + + // Check if all three neurons are registered (dynamic subnet owner + 2 registrations). + assert_eq!(SubtensorModule::get_subnetwork_n(netuid), 3); + + // Run a couple of blocks (may change prices / emission, but shouldn't move stake away). + run_to_block(2); + + // Re-check ALPHA stake (not TAO value). + let src_alpha_after_blocks = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &neuron_src_hotkey_id, + &coldkey_account_id, + netuid, + ); + let dest_alpha_after_blocks = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &neuron_dest_hotkey_id, + &coldkey_account_id, + netuid, + ); + + // Src stake should not decrease; dest stake should still be zero (no stake transfer/dividends). + assert!( + src_alpha_after_blocks >= src_alpha_after_add, + "Src alpha stake unexpectedly decreased" + ); + assert!( + dest_alpha_after_blocks.is_zero(), + "Dest alpha stake unexpectedly increased" + ); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::staking::helpers::test_staking_sets_div_variables --exact --show-output --nocapture +#[test] +fn test_staking_sets_div_variables() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1); + let subnet_owner_hotkey = U256::from(2); + let hotkey_account_id = U256::from(581337); + let coldkey_account_id = U256::from(81337); + let amount = 100_000_000_000_u64; + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + remove_owner_registration_stake(netuid); + let tempo = 10; + Tempo::::insert(netuid, tempo); + register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); + + // Give it some $$$ in his coldkey balance + add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); + + // Verify that divident variables are clear in the beginning + assert_eq!( + AlphaDividendsPerSubnet::::get(netuid, hotkey_account_id), + AlphaBalance::ZERO + ); + assert_eq!( + TotalHotkeyAlphaLastEpoch::::get(hotkey_account_id, netuid), + AlphaBalance::ZERO + ); + + // Stake to hotkey account, and check if the result is ok + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount.into() + )); + + // Verify that divident variables are still clear in the beginning + assert_eq!( + AlphaDividendsPerSubnet::::get(netuid, hotkey_account_id), + AlphaBalance::ZERO + ); + assert_eq!( + TotalHotkeyAlphaLastEpoch::::get(hotkey_account_id, netuid), + AlphaBalance::ZERO + ); + + // Wait for 1 epoch + step_epochs(1, netuid); + + // Verify that divident variables have been set + let stake = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ); + + assert!( + AlphaDividendsPerSubnet::::get(netuid, hotkey_account_id) > AlphaBalance::ZERO + ); + assert_abs_diff_eq!( + TotalHotkeyAlphaLastEpoch::::get(hotkey_account_id, netuid), + stake, + epsilon = stake / 100_000.into() + ); + }); +} + +/*********************************************************** + staking::get_coldkey_balance() tests +************************************************************/ +#[test] +fn test_get_coldkey_balance_no_balance() { + new_test_ext(1).execute_with(|| { + let coldkey_account_id = U256::from(5454); // arbitrary + let result = SubtensorModule::get_coldkey_balance(&coldkey_account_id); + + // Arbitrary account should have 0 balance + assert_eq!(result, 0.into()); + }); +} + +#[test] +fn test_get_coldkey_balance_with_balance() { + new_test_ext(1).execute_with(|| { + let coldkey_account_id = U256::from(5454); // arbitrary + let amount = 1337; + + // Put the balance on the account + add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); + + let result = SubtensorModule::get_coldkey_balance(&coldkey_account_id); + + // Arbitrary account should have 0 balance + assert_eq!(result, amount.into()); + }); +} + +// /************************************************************ +// staking::increase_total_stake() tests +// ************************************************************/ +#[test] +fn test_increase_total_stake_ok() { + new_test_ext(1).execute_with(|| { + let increment = TaoBalance::from(10000); + assert_eq!(SubtensorModule::get_total_stake(), TaoBalance::ZERO); + SubtensorModule::increase_total_stake(increment); + assert_eq!(SubtensorModule::get_total_stake(), increment); + }); +} + +// /************************************************************ +// staking::decrease_total_stake() tests +// ************************************************************/ +#[test] +fn test_decrease_total_stake_ok() { + new_test_ext(1).execute_with(|| { + let initial_total_stake = TaoBalance::from(10000); + let decrement = TaoBalance::from(5000); + + SubtensorModule::increase_total_stake(initial_total_stake); + SubtensorModule::decrease_total_stake(decrement); + + // The total stake remaining should be the difference between the initial stake and the decrement + assert_eq!( + SubtensorModule::get_total_stake(), + initial_total_stake - decrement + ); + }); +} + +// /************************************************************ +// staking::add_balance_to_coldkey_account() tests +// ************************************************************/ +#[test] +fn test_add_balance_to_coldkey_account_ok() { + new_test_ext(1).execute_with(|| { + let coldkey_id = U256::from(4444322); + let amount = 50000; + add_balance_to_coldkey_account(&coldkey_id, amount.into()); + assert_eq!( + SubtensorModule::get_coldkey_balance(&coldkey_id), + amount.into() + ); + }); +} + +// /*********************************************************** +// staking::remove_balance_from_coldkey_account() tests +// ************************************************************/ +#[test] +fn test_remove_balance_from_coldkey_account_ok() { + new_test_ext(1).execute_with(|| { + let coldkey_account_id = U256::from(434324); // Random + let amount = 10000; // Arbitrary + let netuid = NetUid::from(1); + // Put some $$ on the bank + add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); + NetworksAdded::::insert(netuid, true); + assert_eq!( + SubtensorModule::get_coldkey_balance(&coldkey_account_id), + amount.into() + ); + // Should be able to withdraw without hassle + let result = + SubtensorModule::transfer_tao_to_subnet(netuid, &coldkey_account_id, amount.into()); + assert!(result.is_ok()); + }); +} + +#[test] +fn test_remove_balance_from_coldkey_account_failed() { + new_test_ext(1).execute_with(|| { + let coldkey_account_id = U256::from(434324); // Random + let amount = 10000; // Arbitrary + + let netuid = NetUid::from(1); + NetworksAdded::::insert(netuid, true); + + // Try to remove stake from the coldkey account. This should fail, + // as there is no balance, nor does the account exist + let result = + SubtensorModule::transfer_tao_to_subnet(netuid, &coldkey_account_id, amount.into()); + assert_eq!(result, Err(Error::::InsufficientTaoBalance.into())); + }); +} + +//************************************************************ +// staking::hotkey_belongs_to_coldkey() tests +// ************************************************************/ +#[test] +fn test_hotkey_belongs_to_coldkey_ok() { + new_test_ext(1).execute_with(|| { + let hotkey_id = U256::from(4434334); + let coldkey_id = U256::from(34333); + let netuid = NetUid::from(1); + let tempo: u16 = 13; + let start_nonce: u64 = 0; + add_network(netuid, tempo, 0); + register_ok_neuron(netuid, hotkey_id, coldkey_id, start_nonce); + assert_eq!( + SubtensorModule::get_owning_coldkey_for_hotkey(&hotkey_id), + coldkey_id + ); + }); +} + +// /************************************************************ +// staking::can_remove_balance_from_coldkey_account() tests +// ************************************************************/ +#[test] +fn test_can_remove_balane_from_coldkey_account_ok() { + new_test_ext(1).execute_with(|| { + let coldkey_id = U256::from(87987984); + let initial_amount = 10000; + let remove_amount = 5000; + add_balance_to_coldkey_account(&coldkey_id, initial_amount.into()); + assert!(SubtensorModule::can_remove_balance_from_coldkey_account( + &coldkey_id, + remove_amount.into() + )); + }); +} + +#[test] +fn test_can_remove_balance_from_coldkey_account_err_insufficient_balance() { + new_test_ext(1).execute_with(|| { + let coldkey_id = U256::from(87987984); + let initial_amount = 10000; + let remove_amount = 20000; + add_balance_to_coldkey_account(&coldkey_id, initial_amount.into()); + assert!(!SubtensorModule::can_remove_balance_from_coldkey_account( + &coldkey_id, + remove_amount.into() + )); + }); +} + +/************************************************************ + staking::has_enough_stake() tests +************************************************************/ +#[test] +fn test_has_enough_stake_yes() { + new_test_ext(1).execute_with(|| { + let hotkey_id = U256::from(4334); + let coldkey_id = U256::from(87989); + let intial_amount = 10_000; + let netuid = NetUid::from(add_dynamic_network(&hotkey_id, &coldkey_id)); + remove_owner_registration_stake(netuid); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_id, + &coldkey_id, + netuid, + intial_amount.into(), + ); + + assert_abs_diff_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey_id), + intial_amount.into(), + epsilon = 2.into() + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_id, + &coldkey_id, + netuid + ), + intial_amount.into() + ); + assert_ok!(SubtensorModule::calculate_reduced_stake_on_subnet( + &hotkey_id, + &coldkey_id, + netuid, + (intial_amount / 2).into() + )); + }); +} + +#[test] +fn test_has_enough_stake_no() { + new_test_ext(1).execute_with(|| { + let hotkey_id = U256::from(4334); + let coldkey_id = U256::from(87989); + let intial_amount = 10_000; + let netuid = add_dynamic_network(&hotkey_id, &coldkey_id); + remove_owner_registration_stake(netuid); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_id, + &coldkey_id, + netuid, + intial_amount.into(), + ); + + assert_abs_diff_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey_id), + intial_amount.into(), + epsilon = 2.into() + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_id, + &coldkey_id, + netuid + ), + intial_amount.into() + ); + assert_err!( + SubtensorModule::calculate_reduced_stake_on_subnet( + &hotkey_id, + &coldkey_id, + netuid, + (intial_amount * 2).into() + ), + Error::::NotEnoughStakeToWithdraw + ); + }); +} + +#[test] +fn test_has_enough_stake_no_for_zero() { + new_test_ext(1).execute_with(|| { + let hotkey_id = U256::from(4334); + let coldkey_id = U256::from(87989); + let intial_amount = 0; + let netuid = add_dynamic_network(&hotkey_id, &coldkey_id); + remove_owner_registration_stake(netuid); + + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey_id), + intial_amount.into() + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_id, + &coldkey_id, + netuid + ), + intial_amount.into() + ); + assert_err!( + SubtensorModule::calculate_reduced_stake_on_subnet( + &hotkey_id, + &coldkey_id, + netuid, + 1_000.into() + ), + Error::::NotEnoughStakeToWithdraw + ); + }); +} + +#[test] +fn test_non_existent_account() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &U256::from(0), + &(U256::from(0)), + netuid, + 10.into(), + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &U256::from(0), + &U256::from(0), + netuid + ), + 10.into() + ); + // No subnets => no iteration => zero total stake + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&(U256::from(0))), + TaoBalance::ZERO + ); + }); +} + +/************************************************************ + staking::delegating +************************************************************/ + +#[test] +fn test_faucet_ok() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(123560); + + log::info!("Creating work for submission to faucet..."); + + let block_number = SubtensorModule::get_current_block_as_u64(); + let difficulty: U256 = U256::from(10_000_000); + let mut nonce: u64 = 0; + let mut work: H256 = SubtensorModule::create_seal_hash(block_number, nonce, &coldkey); + while !SubtensorModule::hash_meets_difficulty(&work, difficulty) { + nonce += 1; + work = SubtensorModule::create_seal_hash(block_number, nonce, &coldkey); + } + let vec_work: Vec = SubtensorModule::hash_to_vec(work); + + log::info!("Faucet state: {}", cfg!(feature = "pow-faucet")); + + #[cfg(feature = "pow-faucet")] + assert_ok!(SubtensorModule::do_faucet( + RuntimeOrigin::signed(coldkey), + block_number, + nonce, + vec_work + )); + + #[cfg(not(feature = "pow-faucet"))] + assert_ok!(SubtensorModule::do_faucet( + RuntimeOrigin::signed(coldkey), + block_number, + nonce, + vec_work + )); + }); +} + +/// This test ensures that the clear_small_nominations function works as expected. +/// It creates a network with two hotkeys and two coldkeys, and then registers a nominator account for each hotkey. +/// When we call set_nominator_min_required_stake, it should clear all small nominations that are below the minimum required stake. +/// +/// cargo test --package pallet-subtensor --lib -- tests::staking::helpers::test_clear_small_nominations --exact --show-output +#[test] +fn test_clear_small_nominations() { + new_test_ext(0).execute_with(|| { + // Create subnet and accounts. + let subnet_owner_coldkey = U256::from(10); + let subnet_owner_hotkey = U256::from(20); + let hot1 = U256::from(1); + let hot2 = U256::from(2); + let cold1 = U256::from(3); + let cold2 = U256::from(4); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + let amount = DefaultMinStake::::get() * 10.into(); + let fee = DefaultMinStake::::get(); + let init_balance = amount + fee + ExistentialDeposit::get(); + + // Set fee rate to 0 so that alpha fee is not moved to block producer + pallet_subtensor_swap::FeeRate::::insert(netuid, 0); + + // Register hot1. + register_ok_neuron(netuid, hot1, cold1, 0); + Delegates::::insert( + hot1, + PerU16::from_parts(SubtensorModule::get_min_delegate_take()), + ); + assert_eq!(SubtensorModule::get_owning_coldkey_for_hotkey(&hot1), cold1); + + // Register hot2. + register_ok_neuron(netuid, hot2, cold2, 0); + Delegates::::insert( + hot2, + PerU16::from_parts(SubtensorModule::get_min_delegate_take()), + ); + assert_eq!(SubtensorModule::get_owning_coldkey_for_hotkey(&hot2), cold2); + + // Add stake cold1 --> hot1 (non delegation.) + add_balance_to_coldkey_account(&cold1, init_balance); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(cold1), + hot1, + netuid, + amount.into() + )); + let alpha_stake1 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hot1, &cold1, netuid); + let unstake_amount1 = AlphaBalance::from(alpha_stake1.to_u64() * 997 / 1000); + let small1 = alpha_stake1 - unstake_amount1; + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(cold1), + hot1, + netuid, + unstake_amount1 + )); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hot1, &cold1, netuid), + small1 + ); + + // Add stake cold2 --> hot1 (is delegation.) + add_balance_to_coldkey_account(&cold2, init_balance); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(cold2), + hot1, + netuid, + amount.into() + )); + let alpha_stake2 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hot1, &cold2, netuid); + let unstake_amount2 = AlphaBalance::from(alpha_stake2.to_u64() * 997 / 1000); + let small2 = alpha_stake2 - unstake_amount2; + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(cold2), + hot1, + netuid, + unstake_amount2 + )); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hot1, &cold2, netuid), + small2 + ); + + let balance1_before_cleaning = Balances::free_balance(cold1); + let balance2_before_cleaning = Balances::free_balance(cold2); + + // Run clear all small nominations when min stake is zero (noop) + SubtensorModule::set_nominator_min_required_stake(0); + assert_eq!(SubtensorModule::get_nominator_min_required_stake(), 0); + SubtensorModule::clear_small_nominations(); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hot1, &cold1, netuid), + small1 + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hot1, &cold2, netuid), + small2 + ); + + // Set min nomination to above small1 and small2 + let total_hot1_stake_before = TotalHotkeyAlpha::::get(hot1, netuid); + let total_stake_before = TotalStake::::get(); + SubtensorModule::set_nominator_min_required_stake( + (small1.to_u64().min(small2.to_u64()) * 2).into(), + ); + + // Run clear all small nominations (removes delegations under 10) + SubtensorModule::clear_small_nominations(); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hot1, &cold1, netuid), + small1 + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hot1, &cold2, netuid), + AlphaBalance::ZERO + ); + + // Balances have been added back into accounts. + let balance1_after_cleaning = Balances::free_balance(cold1); + let balance2_after_cleaning = Balances::free_balance(cold2); + assert_eq!(balance1_before_cleaning, balance1_after_cleaning); + assert!(balance2_before_cleaning < balance2_after_cleaning); + + assert_abs_diff_eq!( + TotalHotkeyAlpha::::get(hot1, netuid), + total_hot1_stake_before - small2, + epsilon = 1.into() + ); + assert!(TotalStake::::get() < total_stake_before); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::staking::helpers::test_get_total_delegated_stake_after_unstaking --exact --show-output +#[test] +fn test_get_total_delegated_stake_after_unstaking() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let delegate_coldkey = U256::from(1); + let delegate_hotkey = U256::from(2); + let delegator = U256::from(3); + let initial_stake = DefaultMinStake::::get().to_u64() * 10; + let existential_deposit = ExistentialDeposit::get(); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + register_ok_neuron(netuid, delegate_hotkey, delegate_coldkey, 0); + + // Add balance to delegator + add_balance_to_coldkey_account(&delegator, initial_stake.into()); + + // Delegate stake + let (_, fee) = mock::swap_tao_to_alpha(netuid, initial_stake.into()); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(delegator), + delegate_hotkey, + netuid, + initial_stake.into() + )); + + // Check initial delegated stake + assert_abs_diff_eq!( + SubtensorModule::get_total_stake_for_coldkey(&delegator), + (initial_stake - u64::from(existential_deposit) - fee).into(), + epsilon = TaoBalance::from(initial_stake / 100), + ); + assert_abs_diff_eq!( + SubtensorModule::get_total_stake_for_hotkey(&delegate_hotkey), + (initial_stake - u64::from(existential_deposit) - fee).into(), + epsilon = TaoBalance::from(initial_stake / 100), + ); + let delegated_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &delegate_hotkey, + &delegator, + netuid, + ); + // Unstake part of the delegation + let unstake_amount_alpha = delegated_alpha / 2.into(); + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(delegator), + delegate_hotkey, + netuid, + unstake_amount_alpha.into() + )); + let current_price = U96F32::from_num( + ::SwapInterface::current_alpha_price(netuid.into()), + ); + + // Calculate the expected delegated stake + let unstake_amount = + (current_price * U96F32::from_num(unstake_amount_alpha)).to_num::(); + let expected_delegated_stake: u64 = + initial_stake - unstake_amount - u64::from(existential_deposit) - fee; + + // Debug prints + log::debug!("Initial stake: {initial_stake}"); + log::debug!("Unstake amount: {unstake_amount}"); + log::debug!("Existential deposit: {existential_deposit}"); + log::debug!("Expected delegated stake: {expected_delegated_stake}"); + log::debug!( + "Actual delegated stake: {}", + SubtensorModule::get_total_stake_for_coldkey(&delegate_coldkey) + ); + + // Check the total delegated stake after unstaking + assert_abs_diff_eq!( + SubtensorModule::get_total_stake_for_coldkey(&delegator), + expected_delegated_stake.into(), + epsilon = TaoBalance::from(expected_delegated_stake / 1000), + ); + assert_abs_diff_eq!( + SubtensorModule::get_total_stake_for_hotkey(&delegate_hotkey), + expected_delegated_stake.into(), + epsilon = TaoBalance::from(expected_delegated_stake / 1000), + ); + }); +} + +#[test] +fn test_get_total_delegated_stake_no_delegations() { + new_test_ext(1).execute_with(|| { + let delegate = U256::from(1); + let coldkey = U256::from(2); + let netuid = NetUid::from(1u16); + + add_network(netuid, 1, 0); + register_ok_neuron(netuid, delegate, coldkey, 0); + + // Check that there's no delegated stake + assert_eq!( + SubtensorModule::get_total_stake_for_coldkey(&delegate), + TaoBalance::ZERO + ); + }); +} + +#[test] +fn test_get_total_delegated_stake_single_delegator() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let delegate_coldkey = U256::from(1); + let delegate_hotkey = U256::from(2); + let delegator = U256::from(3); + let stake_amount = DefaultMinStake::::get().to_u64() * 10 - 1; + let existential_deposit = ExistentialDeposit::get(); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + register_ok_neuron(netuid, delegate_hotkey, delegate_coldkey, 0); + + // Add stake from delegator + add_balance_to_coldkey_account(&delegator, stake_amount.into()); + + let (_, fee) = mock::swap_tao_to_alpha(netuid, stake_amount.into()); + + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(delegator), + delegate_hotkey, + netuid, + stake_amount.into() + )); + + // Debug prints + log::debug!("Delegate coldkey: {delegate_coldkey:?}"); + log::debug!("Delegate hotkey: {delegate_hotkey:?}"); + log::debug!("Delegator: {delegator:?}"); + log::debug!("Stake amount: {stake_amount}"); + log::debug!("Existential deposit: {existential_deposit}"); + log::debug!( + "Total stake for hotkey: {}", + SubtensorModule::get_total_stake_for_hotkey(&delegate_hotkey) + ); + log::debug!( + "Delegated stake for coldkey: {}", + SubtensorModule::get_total_stake_for_coldkey(&delegate_coldkey) + ); + + // Calculate expected delegated stake + let expected_delegated_stake = stake_amount - u64::from(existential_deposit) - fee; + let actual_delegated_stake = SubtensorModule::get_total_stake_for_hotkey(&delegate_hotkey); + let actual_delegator_stake = SubtensorModule::get_total_stake_for_coldkey(&delegator); + + assert_abs_diff_eq!( + actual_delegated_stake, + expected_delegated_stake.into(), + epsilon = TaoBalance::from(expected_delegated_stake / 100), + ); + assert_abs_diff_eq!( + actual_delegator_stake, + expected_delegated_stake.into(), + epsilon = TaoBalance::from(expected_delegated_stake / 100), + ); + }); +} + +#[test] +fn test_get_alpha_share_stake_multiple_delegators() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let hotkey1 = U256::from(2); + let hotkey2 = U256::from(20); + let coldkey1 = U256::from(3); + let coldkey2 = U256::from(4); + let existential_deposit = TaoBalance::from(2); + let stake1 = DefaultMinStake::::get() * 10.into(); + let stake2 = DefaultMinStake::::get() * 10.into() - 1.into(); + + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey1, coldkey1, 0); + register_ok_neuron(netuid, hotkey2, coldkey2, 0); + + // Add stake from delegator1 + add_balance_to_coldkey_account(&coldkey1, stake1 + existential_deposit); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey1), + hotkey1, + netuid, + stake1 + )); + + // Add stake from delegator2 + add_balance_to_coldkey_account(&coldkey2, stake2 + existential_deposit); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey2), + hotkey2, + netuid, + stake2 + )); + + // Calculate expected total delegated stake + let alpha1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, &coldkey1, netuid, + ); + let alpha2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey2, &coldkey2, netuid, + ); + let expected_total_stake = alpha1 + alpha2; + let actual_total_stake = SubtensorModule::get_alpha_share_pool(hotkey1, netuid) + .get_value(&coldkey1) + + SubtensorModule::get_alpha_share_pool(hotkey2, netuid).get_value(&coldkey2); + + // Total subnet stake should match the sum of delegators' stakes minus existential deposits. + assert_abs_diff_eq!( + AlphaBalance::from(actual_total_stake), + expected_total_stake, + epsilon = expected_total_stake / 1000.into() + ); + }); +} + +#[test] +fn test_get_total_delegated_stake_exclude_owner_stake() { + new_test_ext(1).execute_with(|| { + let delegate_coldkey = U256::from(1); + let delegate_hotkey = U256::from(2); + let delegator = U256::from(3); + let owner_stake = DefaultMinStake::::get().to_u64() * 10; + let delegator_stake = DefaultMinStake::::get().to_u64() * 10 - 1; + + let netuid = add_dynamic_network(&delegate_hotkey, &delegate_coldkey); + remove_owner_registration_stake(netuid); + + // Add owner stake + add_balance_to_coldkey_account(&delegate_coldkey, owner_stake.into()); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(delegate_coldkey), + delegate_hotkey, + netuid, + owner_stake.into() + )); + + // Add delegator stake + add_balance_to_coldkey_account(&delegator, delegator_stake.into()); + let (_, fee) = mock::swap_tao_to_alpha(netuid, delegator_stake.into()); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(delegator), + delegate_hotkey, + netuid, + delegator_stake.into() + )); + + // Check the total delegated stake (should exclude owner's stake) + let expected_delegated_stake = delegator_stake - fee; + let actual_delegated_stake = + SubtensorModule::get_total_stake_for_coldkey(&delegate_coldkey); + + assert_abs_diff_eq!( + actual_delegated_stake, + expected_delegated_stake.into(), + epsilon = TaoBalance::from(expected_delegated_stake / 100) + ); + }); +} + +/// Test that emission is distributed correctly between one validator, one +/// vali-miner, and one miner +#[test] +fn test_mining_emission_distribution_validator_valiminer_miner() { + new_test_ext(1).execute_with(|| { + let validator_coldkey = U256::from(1); + let validator_hotkey = U256::from(2); + let validator_miner_coldkey = U256::from(3); + let validator_miner_hotkey = U256::from(4); + let miner_coldkey = U256::from(5); + let miner_hotkey = U256::from(6); + let netuid = NetUid::from(1); + let subnet_tempo = 10; + let stake = TaoBalance::from(100_000_000_000_u64); + + // Add network, register hotkeys, and setup network parameters + add_network(netuid, subnet_tempo, 0); + register_ok_neuron(netuid, validator_hotkey, validator_coldkey, 0); + register_ok_neuron(netuid, validator_miner_hotkey, validator_miner_coldkey, 1); + register_ok_neuron(netuid, miner_hotkey, miner_coldkey, 2); + add_balance_to_coldkey_account(&validator_coldkey, stake + ExistentialDeposit::get()); + add_balance_to_coldkey_account(&validator_miner_coldkey, stake + ExistentialDeposit::get()); + add_balance_to_coldkey_account(&miner_coldkey, stake + ExistentialDeposit::get()); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + step_block(subnet_tempo); + SubnetOwnerCut::::set(0); + // There are two validators and three neurons + MaxAllowedUids::::set(netuid, 3); + SubtensorModule::set_max_allowed_validators(netuid, 2); + + // Setup stakes: + // Stake from validator + // Stake from valiminer + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(validator_coldkey), + validator_hotkey, + netuid, + stake.into() + )); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(validator_miner_coldkey), + validator_miner_hotkey, + netuid, + stake.into() + )); + + // Setup YUMA so that it creates emissions + Weights::::insert(NetUidStorageIndex::from(netuid), 0, vec![(1, 0xFFFF)]); + Weights::::insert(NetUidStorageIndex::from(netuid), 1, vec![(2, 0xFFFF)]); + BlockAtRegistration::::set(netuid, 0, 1); + BlockAtRegistration::::set(netuid, 1, 1); + BlockAtRegistration::::set(netuid, 2, 1); + LastUpdate::::set(NetUidStorageIndex::from(netuid), vec![2, 2, 2]); + Kappa::::set(netuid, u16::MAX / 5); + ActivityCutoff::::set(netuid, u16::MAX); // makes all stake active + ValidatorPermit::::insert(netuid, vec![true, true, false]); + + // Run run_coinbase until emissions are drained + let validator_stake_before = + SubtensorModule::get_total_stake_for_coldkey(&validator_coldkey); + let valiminer_stake_before = + SubtensorModule::get_total_stake_for_coldkey(&validator_miner_coldkey); + let miner_stake_before = SubtensorModule::get_total_stake_for_coldkey(&miner_coldkey); + + step_block(subnet_tempo); + + // Verify how emission is split between keys + // - Owner cut is zero => 50% goes to miners and 50% goes to validators + // - Validator gets 25% because there are two validators + // - Valiminer gets 25% as a validator and 25% as miner + // - Miner gets 25% as miner + let validator_emission = SubtensorModule::get_total_stake_for_coldkey(&validator_coldkey) + - validator_stake_before; + let valiminer_emission = + SubtensorModule::get_total_stake_for_coldkey(&validator_miner_coldkey) + - valiminer_stake_before; + let miner_emission = + SubtensorModule::get_total_stake_for_coldkey(&miner_coldkey) - miner_stake_before; + let total_emission = validator_emission + valiminer_emission + miner_emission; + + assert_abs_diff_eq!( + validator_emission, + total_emission / 4.into(), + epsilon = 10.into() + ); + assert_abs_diff_eq!( + valiminer_emission, + total_emission / 2.into(), + epsilon = 10.into() + ); + assert_abs_diff_eq!( + miner_emission, + total_emission / 4.into(), + epsilon = 10.into() + ); + }); +} + +/// This test verifies that minimum stake amount is sufficient to move price and apply +/// non-zero staking fees +#[test] +fn test_default_min_stake_sufficiency() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let coldkey = U256::from(4); + let min_tao_stake = DefaultMinStake::::get() * 2.into(); + let amount = min_tao_stake; + let owner_balance_before = amount * 10.into(); + let user_balance_before = amount * 100.into(); + + // add network + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + add_balance_to_coldkey_account(&owner_coldkey, owner_balance_before); + add_balance_to_coldkey_account(&coldkey, user_balance_before); + let fee_rate = pallet_subtensor_swap::FeeRate::::get(NetUid::from(netuid)) as f64 + / u16::MAX as f64; + + // Set some extreme, but realistic TAO and Alpha reserves to minimize slippage + // 1% of TAO max supply + // 0.01 Alpha price + let tao_reserve = TaoBalance::from(210_000_000_000_000_u64); + let alpha_in = AlphaBalance::from(21_000_000_000_000_000_u64); + mock::setup_reserves(netuid, tao_reserve, alpha_in); + let current_price_before = + ::SwapInterface::current_alpha_price(netuid.into()); + + // Stake and unstake + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + owner_hotkey, + netuid, + amount.into(), + )); + let fee_stake = (fee_rate * u64::from(amount) as f64) as u64; + let current_price_after_stake = + ::SwapInterface::current_alpha_price(netuid.into()); + let user_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &owner_hotkey, + &coldkey, + netuid, + ); + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(coldkey), + owner_hotkey, + netuid, + user_alpha, + )); + let fee_unstake = (fee_rate * user_alpha.to_u64() as f64) as u64; + let current_price_after_unstake = + ::SwapInterface::current_alpha_price(netuid.into()); + + assert!(fee_stake > 0); + assert!(fee_unstake > 0); + assert!(current_price_after_stake > current_price_before); + assert!(current_price_after_stake > current_price_after_unstake); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::staking::helpers::test_staking_records_flow --exact --show-output +#[test] +fn test_staking_records_flow() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let hotkey = U256::from(3); + let coldkey = U256::from(4); + let amount = 100_000_000; + + // add network + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + // Forse-set alpha in and tao reserve to make price equal 0.01 + let tao_reserve = TaoBalance::from(100_000_000_000_u64); + let alpha_in = AlphaBalance::from(1_000_000_000_000_u64); + mock::setup_reserves(netuid, tao_reserve, alpha_in); + + // Initialize swap v3 + SubtensorModule::swap_tao_for_alpha( + netuid, + TaoBalance::ZERO, + 1_000_000_000_000_u64.into(), + false, + ) + .unwrap(); + + // Add stake with slippage safety and check if the result is ok + let large_balance = 20_000_000_000_000_000_u64; + add_balance_to_coldkey_account(&coldkey, large_balance.into()); + assert_ok!(SubtensorModule::stake_into_subnet( + &hotkey, + &coldkey, + netuid, + amount.into(), + large_balance.into(), + false, + )); + let fee_rate = pallet_subtensor_swap::FeeRate::::get(NetUid::from(netuid)) as f64 + / u16::MAX as f64; + let expected_flow = (amount as f64) * (1. - fee_rate); + + // Check that flow has been recorded (less unstaking fees) + assert_abs_diff_eq!( + SubnetTaoFlow::::get(netuid), + expected_flow as i64, + epsilon = 1_i64 + ); + + // Remove stake + let alpha = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + assert_ok!(SubtensorModule::unstake_from_subnet( + &hotkey, + &coldkey, + &coldkey, + netuid, + alpha, + TaoBalance::ZERO, + false, + )); + + // Check that outflow has been recorded (less unstaking fees) + // The block builder will receive a fraction of the fees in alpha and will be forced + // to unstake it. So, the additional out-flow is recorded for this. + let unstaked_block_builder_fraction = 1.; + let expected_unstake_fee = + expected_flow * fee_rate * (1. - unstaked_block_builder_fraction); + assert_abs_diff_eq!( + SubnetTaoFlow::::get(netuid), + expected_unstake_fee as i64, + epsilon = ((expected_unstake_fee / 100.0) as i64).max(1) + ); + }); +} diff --git a/pallets/subtensor/src/tests/staking/mod.rs b/pallets/subtensor/src/tests/staking/mod.rs new file mode 100644 index 0000000000..a3f640e65b --- /dev/null +++ b/pallets/subtensor/src/tests/staking/mod.rs @@ -0,0 +1,30 @@ +#![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] +//! Unit tests for [`crate::staking`] add/remove/move stake, take, helpers, and share pools. +//! +//! Layout mirrors `staking/` so each concept module has a matching test file where practical. +//! +//! ## Search anchors +//! +//! | Module | Owns | +//! |--------|------| +//! | [`add_stake`] | `add_stake` / stake-into-subnet / add-root | +//! | [`add_stake_limit`] | add-limit / max-amount add | +//! | [`remove_stake`] | `remove_stake` core / fees / precision | +//! | [`remove_stake_limit`] | remove-limit / max-amount remove | +//! | [`unstake`] | unstake-all / unstake-from-subnet / full unstake | +//! | [`move_stake`] | max-amount move and move-limit partial | +//! | [`delegate_take`] | increase/decrease take and rate limits | +//! | [`helpers`] | balances, ownership, nominations, delegated totals | +//! | [`stake_utils`] | swap fee correctness and large swaps | +//! | [`sharepool`] | lazy share-pool migration and Alpha data-ops | + +mod add_stake; +mod add_stake_limit; +mod delegate_take; +mod helpers; +mod move_stake; +mod remove_stake; +mod remove_stake_limit; +mod sharepool; +mod stake_utils; +mod unstake; diff --git a/pallets/subtensor/src/tests/staking/move_stake.rs b/pallets/subtensor/src/tests/staking/move_stake.rs new file mode 100644 index 0000000000..b1b85ef312 --- /dev/null +++ b/pallets/subtensor/src/tests/staking/move_stake.rs @@ -0,0 +1,684 @@ +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] +//! Tests for [`crate::staking::move_stake`] max-amount / limit partial paths. + +use approx::assert_abs_diff_eq; +use frame_support::assert_ok; +use sp_core::U256; +use substrate_fixed::types::U96F32; +use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; +use subtensor_swap_interface::SwapHandler; + +use super::super::mock::*; +use crate::*; + +// cargo test --package pallet-subtensor --lib -- tests::staking::move_stake::test_max_amount_move_root_root --exact --show-output +#[test] +fn test_max_amount_move_root_root() { + new_test_ext(0).execute_with(|| { + // 0 price on (root, root) exchange => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_move(NetUid::ROOT, NetUid::ROOT, TaoBalance::ZERO), + Ok(AlphaBalance::MAX) + ); + + // 0.5 price on (root, root) => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_move( + NetUid::ROOT, + NetUid::ROOT, + TaoBalance::from(500_000_000) + ), + Ok(AlphaBalance::MAX) + ); + + // 0.999999... price on (root, root) => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_move( + NetUid::ROOT, + NetUid::ROOT, + TaoBalance::from(999_999_999) + ), + Ok(AlphaBalance::MAX) + ); + + // 1.0 price on (root, root) => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_move( + NetUid::ROOT, + NetUid::ROOT, + TaoBalance::from(1_000_000_000) + ), + Ok(AlphaBalance::MAX) + ); + + // 1.000...001 price on (root, root) => max is 0 + assert_eq!( + SubtensorModule::get_max_amount_move( + NetUid::ROOT, + NetUid::ROOT, + TaoBalance::from(1_000_000_001) + ), + Ok(0u64.into()) + ); + + // 2.0 price on (root, root) => max is 0 + assert_eq!( + SubtensorModule::get_max_amount_move( + NetUid::ROOT, + NetUid::ROOT, + TaoBalance::from(2_000_000_000) + ), + Ok(0u64.into()) + ); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::staking::move_stake::test_max_amount_move_root_stable --exact --show-output +#[test] +fn test_max_amount_move_root_stable() { + new_test_ext(0).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + + // 0 price on (root, stable) exchange => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_move(NetUid::ROOT, netuid, TaoBalance::ZERO), + Ok(AlphaBalance::MAX) + ); + + // 0.5 price on (root, stable) => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_move( + NetUid::ROOT, + netuid, + TaoBalance::from(500_000_000) + ), + Ok(AlphaBalance::MAX) + ); + + // 0.999999... price on (root, stable) => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_move( + NetUid::ROOT, + netuid, + TaoBalance::from(999_999_999) + ), + Ok(AlphaBalance::MAX) + ); + + // 1.0 price on (root, stable) => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_move( + NetUid::ROOT, + netuid, + TaoBalance::from(1_000_000_000) + ), + Ok(AlphaBalance::MAX) + ); + + // 1.000...001 price on (root, stable) => max is 0 + assert_eq!( + SubtensorModule::get_max_amount_move( + NetUid::ROOT, + netuid, + TaoBalance::from(1_000_000_001) + ), + Ok(0u64.into()) + ); + + // 2.0 price on (root, stable) => max is 0 + assert_eq!( + SubtensorModule::get_max_amount_move( + NetUid::ROOT, + netuid, + TaoBalance::from(2_000_000_000) + ), + Ok(0u64.into()) + ); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::staking::move_stake::test_max_amount_move_stable_dynamic --exact --show-output +#[test] +fn test_max_amount_move_stable_dynamic() { + new_test_ext(0).execute_with(|| { + // Add stable subnet + let stable_netuid = NetUid::from(1); + add_network(stable_netuid, 1, 0); + + // Add dynamic subnet + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let dynamic_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + // Force-set alpha in and tao reserve to make price equal 0.5 + let tao_reserve = TaoBalance::from(50_000_000_000_u64); + let alpha_in = AlphaBalance::from(100_000_000_000_u64); + SubnetTAO::::insert(dynamic_netuid, tao_reserve); + SubnetAlphaIn::::insert(dynamic_netuid, alpha_in); + let current_price = + ::SwapInterface::current_alpha_price(dynamic_netuid.into()); + assert_eq!(current_price, U96F32::from_num(0.5)); + + // The tests below just mimic the add_stake_limit tests for reverted price + + // 0 price => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, TaoBalance::ZERO), + Ok(AlphaBalance::MAX) + ); + + // 2.0 price => max is 0 + assert_eq!( + SubtensorModule::get_max_amount_move( + stable_netuid, + dynamic_netuid, + TaoBalance::from(2_000_000_000) + ), + Err(pallet_subtensor_swap::Error::::PriceLimitExceeded.into()) + ); + + // 3.0 price => max is 0 + assert_eq!( + SubtensorModule::get_max_amount_move( + stable_netuid, + dynamic_netuid, + TaoBalance::from(3_000_000_000_u64) + ), + Err(pallet_subtensor_swap::Error::::PriceLimitExceeded.into()) + ); + + // 2x price => max is 1x TAO + assert_abs_diff_eq!( + SubtensorModule::get_max_amount_move( + stable_netuid, + dynamic_netuid, + TaoBalance::from(500_000_000) + ) + .unwrap(), + AlphaBalance::from(tao_reserve.to_u64() + (tao_reserve.to_u64() as f64 * 0.003) as u64), + epsilon = AlphaBalance::from(tao_reserve.to_u64() / 100), + ); + + // Precision test: + // 1.99999..9000 price => max > 0 + assert!( + SubtensorModule::get_max_amount_move( + stable_netuid, + dynamic_netuid, + TaoBalance::from(1_999_999_000) + ) + .unwrap() + > AlphaBalance::ZERO + ); + + // Max price doesn't panic and returns something meaningful + assert_eq!( + SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, TaoBalance::MAX), + Err(pallet_subtensor_swap::Error::::PriceLimitExceeded.into()) + ); + assert_eq!( + SubtensorModule::get_max_amount_move( + stable_netuid, + dynamic_netuid, + TaoBalance::MAX - 1.into() + ), + Err(pallet_subtensor_swap::Error::::PriceLimitExceeded.into()) + ); + assert_eq!( + SubtensorModule::get_max_amount_move( + stable_netuid, + dynamic_netuid, + TaoBalance::MAX / 2.into() + ), + Err(pallet_subtensor_swap::Error::::PriceLimitExceeded.into()) + ); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::staking::move_stake::test_max_amount_move_dynamic_stable --exact --show-output +#[test] +fn test_max_amount_move_dynamic_stable() { + new_test_ext(0).execute_with(|| { + // Add stable subnet + let stable_netuid = NetUid::from(1); + add_network(stable_netuid, 1, 0); + + // Add dynamic subnet + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let dynamic_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + // Forse-set alpha in and tao reserve to make price equal 1.5 + let tao_reserve = TaoBalance::from(150_000_000_000_u64); + let alpha_in = AlphaBalance::from(100_000_000_000_u64); + SubnetTAO::::insert(dynamic_netuid, tao_reserve); + SubnetAlphaIn::::insert(dynamic_netuid, alpha_in); + let current_price = + ::SwapInterface::current_alpha_price(dynamic_netuid.into()); + assert_eq!(current_price, U96F32::from_num(1.5)); + + // The tests below just mimic the remove_stake_limit tests + + // 0 price => max is capped at 1000x input reserve + assert_eq!( + SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, TaoBalance::ZERO), + Ok(alpha_in.saturating_mul(1_000.into())) + ); + + // Low price values don't blow things up + assert!( + SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, 1.into()).unwrap() + > AlphaBalance::ZERO + ); + assert!( + SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, 2.into()).unwrap() + > AlphaBalance::ZERO + ); + assert!( + SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, 3.into()).unwrap() + > AlphaBalance::ZERO + ); + + // 1.5000...1 price => max is 0 + assert_eq!( + SubtensorModule::get_max_amount_move( + dynamic_netuid, + stable_netuid, + 1_500_000_001.into() + ), + Err(pallet_subtensor_swap::Error::::PriceLimitExceeded.into()) + ); + + // 1.5 price => max is 0 because of non-zero slippage + assert_abs_diff_eq!( + SubtensorModule::get_max_amount_move( + dynamic_netuid, + stable_netuid, + 1_500_000_000.into() + ) + .unwrap_or(AlphaBalance::ZERO), + AlphaBalance::ZERO, + epsilon = 10_000.into() + ); + + // 1/4 price => max is 1x Alpha + assert_abs_diff_eq!( + SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, 375_000_000.into()) + .unwrap(), + alpha_in + alpha_in / 2000.into(), // + 0.05% fee + epsilon = alpha_in / 10_000.into(), + ); + + // Precision test: + // 1.499999.. price => max > 0 + assert!( + SubtensorModule::get_max_amount_move( + dynamic_netuid, + stable_netuid, + 1_499_999_999.into() + ) + .unwrap() + > AlphaBalance::ZERO + ); + + // Max price doesn't panic and returns something meaningful + assert!( + SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, TaoBalance::MAX) + .unwrap_or(AlphaBalance::ZERO) + < 21_000_000_000_000_000_u64.into() + ); + assert!( + SubtensorModule::get_max_amount_move( + dynamic_netuid, + stable_netuid, + TaoBalance::MAX - 1.into() + ) + .unwrap_or(AlphaBalance::ZERO) + < 21_000_000_000_000_000_u64.into() + ); + assert!( + SubtensorModule::get_max_amount_move( + dynamic_netuid, + stable_netuid, + TaoBalance::MAX / 2.into() + ) + .unwrap_or(AlphaBalance::ZERO) + < 21_000_000_000_000_000_u64.into() + ); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::staking::move_stake::test_max_amount_move_dynamic_dynamic --exact --show-output +#[test] +fn test_max_amount_move_dynamic_dynamic() { + new_test_ext(0).execute_with(|| { + // Add two dynamic subnets + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let origin_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + let destination_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + // Test cases are generated with help with this limit-staking calculator: + // https://docs.google.com/spreadsheets/d/1pfU-PVycd3I4DbJIc0GjtPohy4CbhdV6CWqgiy__jKE + // This is for reference only; verify before use. + // + // CSV backup for this spreadhsheet: + // + // SubnetTAO 1,AlphaIn 1,SubnetTAO 2,AlphaIn 2,,initial price,limit price,max swappable + // 150,100,100,100,,=(A2/B2)/(C2/D2),0.1,=(D2*A2-B2*C2*G2)/(G2*(A2+C2)) + // + // tao_in_1, alpha_in_1, tao_in_2, alpha_in_2, limit_price, expected_max_swappable, precision + [ + // Zero handling (no panics) + ( + 0_u64, + 1_000_000_000_u64, + 1_000_000_000_u64, + 1_000_000_000_u64, + 100, + 0, + 1_u64, + ), + (1_000_000_000, 0, 1_000_000_000, 1_000_000_000, 100, 0, 1), + (1_000_000_000, 1_000_000_000, 0, 1_000_000_000, 100, 0, 1), + (1_000_000_000, 1_000_000_000, 1_000_000_000, 0, 100, 0, 1), + // Low bounds + (1, 1, 1, 1, 0, u64::MAX, 1), + (1, 1, 1, 1, 1, 500_000_000, 1), + (1, 1, 1, 1, 2, 250_000_000, 1), + (1, 1, 1, 1, 3, 166_666_666, 1), + (1, 1, 1, 1, 4, 125_000_000, 1), + (1, 1, 1, 1, 1_000, 500_000, 1), + // Basic math + (1_000, 1_000, 1_000, 1_000, 500_000_000, 500, 1), + (1_000, 1_000, 1_000, 1_000, 100_000_000, 4_500, 1), + // Normal range values edge cases + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000, + 560_000_000_000, + 1_000_000, + ), + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + 500_000_000, + 80_000_000_000, + 1_000_000, + ), + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + 750_000_000, + 40_000_000_000, + 1_000_000, + ), + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + 1_000_000_000, + 20_000_000_000, + 1_000, + ), + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + 1_250_000_000, + 8_000_000_000, + 1_000, + ), + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + 1_499_999_999, + 27, + 1, + ), + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + 1_500_000_000, + 0, + 1, + ), + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + 1_500_000_001, + 0, + 1, + ), + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + 1_500_001_000, + 0, + 1, + ), + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + 2_000_000_000, + 0, + 1, + ), + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + u64::MAX, + 0, + 1, + ), + ( + 100_000_000_000, + 200_000_000_000, + 300_000_000_000, + 400_000_000_000, + 500_000_000, + 50_000_000_000, + 1_000, + ), + // Miscellaneous overflows + ( + 1_000_000_000, + 1_000_000_000, + 1_000_000_000, + 1_000_000_000, + 1, + 499_999_999_500_000_000, + 100_000_000, + ), + ( + 1_000_000, + 1_000_000, + 21_000_000_000_000_000, + 1_000_000_000_000_000_000_u64, + 1, + 48_000_000_000_000_000, + 1_000_000_000_000_000, + ), + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + u64::MAX, + 0, + 1, + ), + ( + 1_000_000, + 1_000_000, + 21_000_000_000_000_000, + 1_000_000_000_000_000_000_u64, + u64::MAX, + 0, + 1, + ), + ] + .iter() + .for_each( + |&( + tao_in_1, + alpha_in_1, + tao_in_2, + alpha_in_2, + limit_price, + expected_max_swappable, + precision, + )| { + let expected_max_swappable = AlphaBalance::from(expected_max_swappable); + // Forse-set alpha in and tao reserve to achieve relative price of subnets + SubnetTAO::::insert(origin_netuid, TaoBalance::from(tao_in_1)); + SubnetAlphaIn::::insert(origin_netuid, AlphaBalance::from(alpha_in_1)); + SubnetTAO::::insert(destination_netuid, TaoBalance::from(tao_in_2)); + SubnetAlphaIn::::insert(destination_netuid, AlphaBalance::from(alpha_in_2)); + + if !alpha_in_1.is_zero() && !alpha_in_2.is_zero() { + let origin_price = tao_in_1 as f64 / alpha_in_1 as f64; + let dest_price = tao_in_2 as f64 / alpha_in_2 as f64; + if dest_price != 0. { + let expected_price = origin_price / dest_price; + assert_abs_diff_eq!( + (::SwapInterface::current_alpha_price( + origin_netuid.into() + ) / ::SwapInterface::current_alpha_price( + destination_netuid.into() + )) + .to_num::(), + expected_price, + epsilon = 0.000_000_001 + ); + } + } + + assert_abs_diff_eq!( + SubtensorModule::get_max_amount_move( + origin_netuid, + destination_netuid, + limit_price.into() + ) + .unwrap_or(AlphaBalance::ZERO), + expected_max_swappable, + epsilon = precision.into() + ); + }, + ); + }); +} + +#[test] +fn test_move_stake_limit_partial() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let stake_amount = AlphaBalance::from(150_000_000_000_u64); + let move_amount = AlphaBalance::from(150_000_000_000_u64); + + // add network + let origin_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + let destination_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(origin_netuid, hotkey, coldkey, 192213123); + register_ok_neuron(destination_netuid, hotkey, coldkey, 192213123); + + // Give the neuron some stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + origin_netuid, + stake_amount, + ); + + // Registration now goes through the burn/swap path, which initializes swap V3 state. + // Clear that state first so the manual reserve fixture below actually controls price. + let mut origin_weight_meter = + frame_support::weights::WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)); + assert!( + ::SwapInterface::clear_protocol_liquidity( + origin_netuid, + &mut origin_weight_meter + ) + ); + let mut destination_weight_meter = + frame_support::weights::WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)); + assert!( + ::SwapInterface::clear_protocol_liquidity( + destination_netuid, + &mut destination_weight_meter + ) + ); + + // Force-set alpha in and tao reserve to make price equal 1.5 on both origin and destination, + // but there's much more liquidity on destination, so its price wouldn't go up when restaked. + let tao_reserve = TaoBalance::from(150_000_000_000_u64); + let alpha_in = AlphaBalance::from(100_000_000_000_u64); + + SubnetTAO::::insert(origin_netuid, tao_reserve); + SubnetAlphaIn::::insert(origin_netuid, alpha_in); + + SubnetTAO::::insert(destination_netuid, tao_reserve * 100_000.into()); + SubnetAlphaIn::::insert(destination_netuid, alpha_in * 100_000.into()); + + let origin_price = + ::SwapInterface::current_alpha_price(origin_netuid.into()); + let destination_price = + ::SwapInterface::current_alpha_price(destination_netuid.into()); + + assert_eq!(origin_price, U96F32::from_num(1.5)); + assert_eq!(destination_price, U96F32::from_num(1.5)); + + // The relative price between origin and destination subnets is 1. + // Setup limit relative price so that it doesn't drop by more than 1% from current price. + let limit_price = TaoBalance::from(990_000_000_u64); + + // Move stake with slippage safety - executes partially + assert_ok!(SubtensorModule::swap_stake_limit( + RuntimeOrigin::signed(coldkey), + hotkey, + origin_netuid, + destination_netuid, + move_amount, + limit_price, + true, + )); + + let new_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + origin_netuid, + ); + + assert_abs_diff_eq!( + new_alpha, + AlphaBalance::from(149_000_000_000_u64), + epsilon = 100_000_000.into() + ); + }); +} diff --git a/pallets/subtensor/src/tests/staking/remove_stake.rs b/pallets/subtensor/src/tests/staking/remove_stake.rs new file mode 100644 index 0000000000..d0b3bf3304 --- /dev/null +++ b/pallets/subtensor/src/tests/staking/remove_stake.rs @@ -0,0 +1,1044 @@ +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] +//! Tests for [`crate::staking::remove_stake`] core remove / fee / precision paths. + +use approx::assert_abs_diff_eq; +use frame_support::sp_runtime::DispatchError; +use frame_support::{assert_err, assert_noop, assert_ok, traits::Currency}; +use frame_system::RawOrigin; +use sp_core::{Get, U256}; +use substrate_fixed::types::{U64F64, U96F32}; +use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; +use subtensor_swap_interface::SwapHandler; + +use super::super::mock; +use super::super::mock::*; +use crate::*; + +#[test] +fn test_remove_stake_ok_no_emission() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1); + let subnet_owner_hotkey = U256::from(2); + let coldkey_account_id = U256::from(4343); + let hotkey_account_id = U256::from(4968585); + let amount = DefaultMinStake::::get() * 10.into(); + + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); + + // Clear any implicit existing stake so we can fully remove exactly `amount` + let existing = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ); + if !existing.is_zero() { + SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + existing, + ); + } + + // Create stake without relying on any emission/weights assumptions + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + amount.to_u64().into(), + ); + + let expected_stake: AlphaBalance = amount.to_u64().into(); + let epsilon_stake: AlphaBalance = (amount.to_u64() / 1000).into(); + + assert_abs_diff_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid + ), + expected_stake, + epsilon = epsilon_stake + ); + + // Snapshot baselines before we top up SubnetTAO / TotalStake + let base_total_stake = SubtensorModule::get_total_stake(); + let balance_before = SubtensorModule::get_coldkey_balance(&coldkey_account_id); + + // Add subnet TAO so remove_stake can pay out (keep original pattern) + let (amount_tao, fee) = mock::swap_alpha_to_tao(netuid, amount.to_u64().into()); + SubnetTAO::::mutate(netuid, |v| *v += amount_tao + fee.into()); + TotalStake::::mutate(|v| *v += amount_tao + fee.into()); + + // Do the magic + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount.to_u64().into() + )); + + // we do not expect the exact amount due to slippage, but it must increase meaningfully + let balance_after = SubtensorModule::get_coldkey_balance(&coldkey_account_id); + assert!(balance_after > balance_before); + assert!( + (balance_after - balance_before) > amount / 10.into() * 9.into() - fee.into(), + "Payout lower than expected lower bound" + ); + + // All stake removed + assert!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid + ) + .is_zero() + ); + + // Total stake should net-increase only by fee (everything else returned) + assert_abs_diff_eq!( + SubtensorModule::get_total_stake(), + base_total_stake + fee.into(), + epsilon = SubtensorModule::get_total_stake() / 100_000.into() + ); + }); +} + +#[test] +fn test_remove_stake_amount_too_low() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1); + let subnet_owner_hotkey = U256::from(2); + let coldkey_account_id = U256::from(4343); + let hotkey_account_id = U256::from(4968585); + let amount: u64 = 10_000; + + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); + + // Ensure deterministic starting stake for this (hotkey,coldkey,netuid) + let existing = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ); + if !existing.is_zero() { + SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + existing, + ); + } + + // Give the neuron some stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + amount.into(), + ); + + // Removing zero should fail + assert_noop!( + SubtensorModule::remove_stake( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + AlphaBalance::ZERO + ), + Error::::AmountTooLow + ); + }); +} + +#[test] +fn test_remove_stake_below_min_stake() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1); + let subnet_owner_hotkey = U256::from(2); + let coldkey_account_id = U256::from(4343); + let hotkey_account_id = U256::from(4968585); + + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); + + // Clear any implicit existing stake so the test always starts below-min + let existing = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ); + if !existing.is_zero() { + SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + existing, + ); + } + + let min_stake = DefaultMinStake::::get(); + let amount = AlphaBalance::from(min_stake.to_u64() / 2); + + // Give the neuron some *below-min* stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + amount, + ); + + // Unstake less than full stake -> leaves a non-zero remainder below min -> errors + assert_noop!( + SubtensorModule::remove_stake( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount - 1.into() + ), + Error::::AmountTooLow + ); + + // Unstaking full stake - works + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount + )); + assert!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ) + .is_zero() + ); + }); +} + +#[test] +fn test_remove_stake_err_signature() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(4968585); + let amount = AlphaBalance::from(10000); // Amount to be removed + let netuid = NetUid::from(1); + + assert_err!( + SubtensorModule::remove_stake( + RawOrigin::None.into(), + hotkey_account_id, + netuid, + amount, + ), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn test_remove_stake_ok_hotkey_does_not_belong_to_coldkey() { + new_test_ext(1).execute_with(|| { + let coldkey_id = U256::from(544); + let hotkey_id = U256::from(54544); + let other_cold_key = U256::from(99498); + let amount = DefaultMinStake::::get().to_u64() * 10; + let netuid = add_dynamic_network(&hotkey_id, &coldkey_id); + + // Give the neuron some stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_id, + &other_cold_key, + netuid, + amount.into(), + ); + + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(other_cold_key), + hotkey_id, + netuid, + amount.into(), + )); + }); +} + +#[test] +fn test_remove_stake_no_enough_stake() { + new_test_ext(1).execute_with(|| { + let coldkey_id = U256::from(544); + let hotkey_id = U256::from(54544); + let amount = DefaultMinStake::::get().to_u64() * 10; + let netuid = add_dynamic_network(&hotkey_id, &coldkey_id); + remove_owner_registration_stake(netuid); + + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey_id), + TaoBalance::ZERO + ); + + assert_err!( + SubtensorModule::remove_stake( + RuntimeOrigin::signed(coldkey_id), + hotkey_id, + netuid, + amount.into(), + ), + Error::::AmountTooLow + ); + }); +} + +#[test] +fn test_remove_stake_total_balance_no_change() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1); + let subnet_owner_hotkey = U256::from(2); + let hotkey_account_id = U256::from(571337); + let coldkey_account_id = U256::from(71337); + let amount: u64 = DefaultMinStake::::get().to_u64() * 10; + + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); + + // Set fee rate to 0 so that alpha fee is not moved to block producer + pallet_subtensor_swap::FeeRate::::insert(netuid, 0); + let fee: u64 = 0; + + // Clear any implicit existing stake so the test is deterministic + let existing = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ); + if !existing.is_zero() { + SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + existing, + ); + } + + let balance_before = SubtensorModule::get_coldkey_balance(&coldkey_account_id); + let total_balance_before = Balances::total_balance(&coldkey_account_id); + let base_total_stake = SubtensorModule::get_total_stake(); + + // Give the neuron some stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + amount.into(), + ); + + // Add subnet TAO for the equivalent amount added at price + let amount_tao = U96F32::from_num(amount) + * U96F32::from_num( + ::SwapInterface::current_alpha_price(netuid.into()), + ); + let amount_tao: TaoBalance = amount_tao.to_num::().into(); + SubnetTAO::::mutate(netuid, |v| *v += amount_tao); + TotalStake::::mutate(|v| *v += amount_tao); + + // Remove stake + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount.into() + )); + + let balance_after = SubtensorModule::get_coldkey_balance(&coldkey_account_id); + let total_balance_after = Balances::total_balance(&coldkey_account_id); + + // Free balance should increase by roughly the TAO paid out (net of swap mechanics) + assert!(balance_after > balance_before); + assert!( + (balance_after - balance_before) > amount_tao / 10.into() * 9.into() - fee.into(), + "Payout lower than expected lower bound" + ); + + // Total balance should track the same change (since stake becomes free) + assert!(total_balance_after > total_balance_before); + + // Total stake should net-increase only by fee + assert_abs_diff_eq!( + SubtensorModule::get_total_stake(), + base_total_stake + fee.into(), + epsilon = SubtensorModule::get_total_stake() / 10_000_000.into() + ); + + assert_abs_diff_eq!( + total_balance_after - total_balance_before, + amount_tao - fee.into(), + epsilon = TaoBalance::from(amount) / 1000.into() + ); + }); +} + +#[test] +fn test_remove_stake_insufficient_liquidity() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let hotkey = U256::from(2); + let coldkey = U256::from(3); + let amount_staked = DefaultMinStake::::get().to_u64() * 10; + + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + let _ = SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + add_balance_to_coldkey_account(&coldkey, amount_staked.into()); + + // Simulate stake for hotkey + let reserve = u64::MAX / 1000; + mock::setup_reserves(netuid, reserve.into(), reserve.into()); + + let alpha = SubtensorModule::stake_into_subnet( + &hotkey, + &coldkey, + netuid, + amount_staked.into(), + ::SwapInterface::max_price(), + false, + ) + .unwrap(); + + // Set the liquidity at lowest possible value so that all staking requests fail + let reserve = u64::from(mock::SwapMinimumReserve::get()) - 1; + mock::setup_reserves(netuid, reserve.into(), reserve.into()); + + // Check the error + assert_noop!( + SubtensorModule::remove_stake(RuntimeOrigin::signed(coldkey), hotkey, netuid, alpha), + Error::::InsufficientLiquidity + ); + + // Mock more liquidity - remove becomes successful + SubnetTAO::::insert(netuid, TaoBalance::from(amount_staked + 1)); + SubnetAlphaIn::::insert(netuid, AlphaBalance::from(alpha.to_u64() / 1000 + 1)); + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + alpha + ),); + }); +} + +#[test] +fn test_remove_stake_total_issuance_no_change() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1); + let subnet_owner_hotkey = U256::from(2); + let hotkey_account_id = U256::from(581337); + let coldkey_account_id = U256::from(81337); + let amount: u64 = DefaultMinStake::::get().to_u64() * 10; + + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); + + // Set fee rate to 0 so that alpha fee is not moved to block producer + pallet_subtensor_swap::FeeRate::::insert(netuid, 0); + + // Ensure the coldkey has at least 'amount' more balance available for staking + add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); + + mock::setup_reserves(netuid, (amount * 100).into(), (amount * 100).into()); + + // Baselines (after registration + funding) + let balance_before_stake = SubtensorModule::get_coldkey_balance(&coldkey_account_id); + let issuance_before = Balances::total_issuance(); + let base_total_stake = SubtensorModule::get_total_stake(); + + // Stake exactly `amount` TAO + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + TaoBalance::from(amount), + )); + + let issuance_after_stake = Balances::total_issuance(); + + // Staking burns `amount` from balances issuance in this system design. + assert_abs_diff_eq!(issuance_before, issuance_after_stake, epsilon = 1.into()); + + // Remove all stake + let stake_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ); + + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + stake_alpha, + )); + + let issuance_after_unstake = Balances::total_issuance(); + + // Ground-truth fee/loss is the net issuance reduction after stake+unstake. + let fee_balance = issuance_before.saturating_sub(issuance_after_unstake); + let total_fee_actual: u64 = fee_balance.into(); + + // Final coldkey balance should be baseline minus the effective fee. + let balance_after = SubtensorModule::get_coldkey_balance(&coldkey_account_id); + assert_abs_diff_eq!( + balance_after, + (balance_before_stake.saturating_sub(total_fee_actual.into())).into(), + epsilon = 50.into() + ); + + // Stake should be cleared. + assert!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid + ) + .is_zero() + ); + + // Total stake should only increase by what stayed in pools (fees/rounding). + assert_abs_diff_eq!( + SubtensorModule::get_total_stake(), + base_total_stake + TaoBalance::from(total_fee_actual), + epsilon = TaoBalance::from(500u64) + ); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::staking::remove_stake::test_remove_prev_epoch_stake --exact --show-output --nocapture +#[test] +fn test_remove_prev_epoch_stake() { + new_test_ext(1).execute_with(|| { + // Test case: (amount_to_stake, AlphaDividendsPerSubnet, TotalHotkeyAlphaLastEpoch, expected_fee) + [ + // No previous epoch stake and low hotkey stake + ( + DefaultMinStake::::get().to_u64() * 10, + 0_u64, + 1000_u64, + ), + // Same, but larger amount to stake - we get 0.005% for unstake + (1_000_000_000, 0_u64, 1000_u64), + (100_000_000_000, 0_u64, 1000_u64), + // Lower previous epoch stake than current stake + // Staking/unstaking 100 TAO, divs / total = 0.1 => fee is 1 TAO + (100_000_000_000, 1_000_000_000_u64, 10_000_000_000_u64), + // Staking/unstaking 100 TAO, divs / total = 0.001 => fee is 0.01 TAO + (100_000_000_000, 10_000_000_u64, 10_000_000_000_u64), + // Higher previous epoch stake than current stake + (1_000_000_000, 100_000_000_000_u64, 100_000_000_000_000_u64), + ] + .into_iter() + .for_each(|(amount_to_stake, alpha_divs, hotkey_alpha)| { + let alpha_divs = AlphaBalance::from(alpha_divs); + let hotkey_alpha = AlphaBalance::from(hotkey_alpha); + let subnet_owner_coldkey = U256::from(1); + let subnet_owner_hotkey = U256::from(2); + let hotkey_account_id = U256::from(581337); + let coldkey_account_id = U256::from(81337); + let amount = amount_to_stake; + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); + + // Give it some $$$ in his coldkey balance + add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); + AlphaDividendsPerSubnet::::insert(netuid, hotkey_account_id, alpha_divs); + TotalHotkeyAlphaLastEpoch::::insert(hotkey_account_id, netuid, hotkey_alpha); + let balance_before = SubtensorModule::get_coldkey_balance(&coldkey_account_id); + mock::setup_reserves( + netuid, + (amount_to_stake * 10).into(), + (amount_to_stake * 10).into(), + ); + + // Stake to hotkey account, and check if the result is ok + let (_, fee) = mock::swap_tao_to_alpha(netuid, amount.into()); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount.into() + )); + + // Remove all stake + let stake = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ); + + let fee = mock::swap_alpha_to_tao(netuid, stake).1 + fee; + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + stake + )); + + // Measure actual fee + let balance_after = SubtensorModule::get_coldkey_balance(&coldkey_account_id); + let actual_fee = balance_before - balance_after; + + assert_abs_diff_eq!(actual_fee, fee.into(), epsilon = (fee / 100).into()); + }); + }); +} + +/************************************************************ + staking::remove_stake_from_hotkey_account() tests +************************************************************/ +#[test] +fn test_remove_stake_from_hotkey_account() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1); + let subnet_owner_hotkey = U256::from(2); + let hotkey_id = U256::from(5445); + let coldkey_id = U256::from(5443433); + let amount: AlphaBalance = 10_000u64.into(); + + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey_id, coldkey_id, 192213123); + + // Baselines before adding stake. + let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_id, + &coldkey_id, + netuid, + ); + let total_before = SubtensorModule::get_total_stake_for_hotkey(&hotkey_id); + + // Add alpha stake directly through the internal helper. + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_id, + &coldkey_id, + netuid, + amount, + ); + + // Alpha stake should increase by exactly the credited alpha amount. + let alpha_after_add = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_id, + &coldkey_id, + netuid, + ); + assert_eq!(alpha_after_add, alpha_before.saturating_add(amount)); + + // Tao-equivalent total stake should have increased from baseline. + assert!(SubtensorModule::get_total_stake_for_hotkey(&hotkey_id) > total_before); + + // Remove exactly the same alpha amount. + SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_id, + &coldkey_id, + netuid, + amount, + ); + + // Alpha stake should return to its original baseline. + let alpha_after_remove = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_id, + &coldkey_id, + netuid, + ); + assert_eq!(alpha_after_remove, alpha_before); + + // Tao-equivalent total stake should also return to baseline. + assert_abs_diff_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey_id), + total_before, + epsilon = 10.into() + ); + }); +} + +#[test] +fn test_remove_stake_from_hotkey_account_registered_in_various_networks() { + new_test_ext(1).execute_with(|| { + let hotkey_id = U256::from(5445); + let coldkey_id = U256::from(5443433); + let amount: u64 = 10_000; + let netuid = add_dynamic_network(&hotkey_id, &coldkey_id); + remove_owner_registration_stake(netuid); + let netuid_ex = add_dynamic_network(&hotkey_id, &coldkey_id); + remove_owner_registration_stake(netuid_ex); + + let neuron_uid = match SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey_id) { + Ok(k) => k, + Err(e) => panic!("Error: {e:?}"), + }; + + let neuron_uid_ex = match SubtensorModule::get_uid_for_net_and_hotkey(netuid_ex, &hotkey_id) + { + Ok(k) => k, + Err(e) => panic!("Error: {e:?}"), + }; + + // Add some stake that can be removed + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_id, + &coldkey_id, + netuid, + amount.into(), + ); + + assert_eq!( + SubtensorModule::get_stake_for_uid_and_subnetwork(netuid, neuron_uid), + amount.into() + ); + assert_eq!( + SubtensorModule::get_stake_for_uid_and_subnetwork(netuid_ex, neuron_uid_ex), + AlphaBalance::ZERO + ); + + // Remove all stake + SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_id, + &coldkey_id, + netuid, + amount.into(), + ); + + // + assert_eq!( + SubtensorModule::get_stake_for_uid_and_subnetwork(netuid, neuron_uid), + AlphaBalance::ZERO + ); + assert_eq!( + SubtensorModule::get_stake_for_uid_and_subnetwork(netuid_ex, neuron_uid_ex), + AlphaBalance::ZERO + ); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::staking::remove_stake::test_remove_stake_fee_goes_to_subnet_tao --exact --show-output --nocapture +#[ignore = "fees no go to liquidity providers"] +#[test] +fn test_remove_stake_fee_goes_to_subnet_tao() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let hotkey = U256::from(2); + let coldkey = U256::from(3); + let tao_to_stake = DefaultMinStake::::get() * 10.into(); + + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + let _ = SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + let subnet_tao_before = SubnetTAO::::get(netuid); + + // Add stake + add_balance_to_coldkey_account(&coldkey, tao_to_stake.into()); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + tao_to_stake + )); + + // Remove all stake + let alpha_to_unstake = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + alpha_to_unstake + )); + let subnet_tao_after = SubnetTAO::::get(netuid); + + // Subnet TAO should have increased by 2x fee as a result of staking + unstaking + assert_abs_diff_eq!( + subnet_tao_before, + subnet_tao_after, + epsilon = (alpha_to_unstake.to_u64() / 1000).into() + ); + + // User balance should decrease by 2x fee as a result of staking + unstaking + let balance_after = SubtensorModule::get_coldkey_balance(&coldkey); + assert_abs_diff_eq!( + balance_after, + tao_to_stake, + epsilon = tao_to_stake / 1000.into() + ); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::staking::remove_stake::test_remove_stake_fee_realistic_values --exact --show-output --nocapture +#[ignore = "fees are now calculated on the SwapInterface side"] +#[test] +fn test_remove_stake_fee_realistic_values() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let hotkey = U256::from(2); + let coldkey = U256::from(3); + let alpha_to_unstake = AlphaBalance::from(111_180_000_000_u64); + let alpha_divs = AlphaBalance::from(2_816_190); + + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + let _ = SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + + // Mock a realistic scenario: + // Subnet 1 has 3896 TAO and 128_011 Alpha in reserves, which + // makes its price ~0.03. + // A hotkey has 111 Alpha stake and is unstaking all Alpha. + // Alpha dividends of this hotkey are ~0.0028 + // This makes fee be equal ~0.0028 Alpha ~= 84000 rao + let tao_reserve = 3_896_056_559_708_u64; + let alpha_in = 128_011_331_299_964_u64; + mock::setup_reserves(netuid, tao_reserve.into(), alpha_in.into()); + AlphaDividendsPerSubnet::::insert(netuid, hotkey, alpha_divs); + TotalHotkeyAlphaLastEpoch::::insert(hotkey, netuid, alpha_to_unstake); + + // Add stake first time to init TotalHotkeyAlpha + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + alpha_to_unstake, + ); + + // Remove stake to measure fee + let balance_before = SubtensorModule::get_coldkey_balance(&coldkey); + let (expected_tao, expected_fee) = mock::swap_alpha_to_tao(netuid, alpha_to_unstake); + + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + alpha_to_unstake + )); + + // Calculate expected fee + let balance_after = SubtensorModule::get_coldkey_balance(&coldkey); + // FIXME since fee is calculated by SwapInterface and the values here are after fees, the + // actual_fee is 0. but it's left here to discuss in review + let actual_fee = expected_tao - (balance_after - balance_before); + log::info!("Actual fee: {actual_fee:?}"); + + assert_abs_diff_eq!( + actual_fee, + expected_fee.into(), + epsilon = (expected_fee / 1000).into() + ); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::staking::test_remove_99_999_per_cent_stake_works_precisely --exact --show-output +#[test] +fn test_remove_99_9991_per_cent_stake_works_precisely() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1); + let subnet_owner_hotkey = U256::from(2); + let hotkey_account_id = U256::from(581337); + let coldkey_account_id = U256::from(81337); + let amount = 10_000_000_000_u64; + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); + + // Set fee rate to 0 so that alpha fee is not moved to block producer. + pallet_subtensor_swap::FeeRate::::insert(netuid, 0); + + // Give it some $$$ in his coldkey balance (in addition to any leftover buffer from registration) + add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); + + // Stake to hotkey account. + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount.into() + )); + + // Remove 99.9991% stake. + let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ); + let coldkey_balance_before_remove = + SubtensorModule::get_coldkey_balance(&coldkey_account_id); + + let remove_amount = AlphaBalance::from( + (U64F64::from_num(alpha) * U64F64::from_num(0.999991)).to_num::(), + ); + + // Expected TAO returned by swapping exactly the removed alpha. + let (expected_returned_balance, _) = mock::swap_alpha_to_tao(netuid, remove_amount); + + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + remove_amount, + )); + + // Compare the returned delta, not the absolute coldkey balance, because + // registration / staking can leave a small pre-existing balance on coldkey. + let coldkey_balance_after_remove = + SubtensorModule::get_coldkey_balance(&coldkey_account_id); + let actual_returned_balance = TaoBalance::from( + coldkey_balance_after_remove + .to_u64() + .saturating_sub(coldkey_balance_before_remove.to_u64()), + ); + + assert_abs_diff_eq!( + actual_returned_balance, + expected_returned_balance, + epsilon = 10.into(), + ); + + assert!(!SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id).is_zero()); + + let new_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ); + assert_eq!(new_alpha, alpha - remove_amount); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::staking::remove_stake::test_remove_99_9989_per_cent_stake_leaves_a_little --exact --show-output +#[test] +fn test_remove_99_9989_per_cent_stake_leaves_a_little() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1); + let subnet_owner_hotkey = U256::from(2); + let hotkey_account_id = U256::from(581337); + let coldkey_account_id = U256::from(81337); + let amount = 10_000_000_000; + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); + + // Set fee rate to 0 so that alpha fee is not moved to block producer + // to avoid false success in this test + pallet_subtensor_swap::FeeRate::::insert(netuid, 0); + + // Give it some $$$ in his coldkey balance + add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); + + // Stake to hotkey account, and check if the result is ok + let (_, fee) = mock::swap_tao_to_alpha(netuid, amount.into()); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount.into() + )); + + // Remove 99.9989% stake + let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ); + let fee = + mock::swap_alpha_to_tao(netuid, ((alpha.to_u64() as f64 * 0.99) as u64).into()).1 + fee; + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + (U64F64::from_num(alpha.to_u64()) * U64F64::from_num(0.99)) + .to_num::() + .into() + )); + + // Check that all alpha was unstaked and 99% TAO balance was returned (less fees) + // let fee = ::SwapInterface::approx_fee_amount(netuid.into(), (amount as f64 * 0.99) as u64); + assert_abs_diff_eq!( + SubtensorModule::get_coldkey_balance(&coldkey_account_id).to_u64(), + (amount as f64 * 0.99) as u64 - fee, + epsilon = amount / 1000, + ); + assert_abs_diff_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id).to_u64(), + (amount as f64 * 0.01) as u64, + epsilon = amount / 1000, + ); + let new_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ); + assert_abs_diff_eq!( + new_alpha, + AlphaBalance::from((alpha.to_u64() as f64 * 0.01) as u64), + epsilon = 10.into() + ); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::staking::remove_stake::test_remove_root_updates_counters --exact --show-output +#[test] +fn test_remove_root_updates_counters() { + new_test_ext(0).execute_with(|| { + let hotkey_account_id = U256::from(561337); + let coldkey_account_id = U256::from(61337); + add_network(NetUid::ROOT, 10, 0); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(coldkey_account_id).clone(), + hotkey_account_id, + )); + let stake_amount = TaoBalance::from(1_000_000_000); + + // Give it some $$$ in his coldkey balance + let initial_balance = stake_amount + ExistentialDeposit::get(); + add_balance_to_coldkey_account(&coldkey_account_id, initial_balance); + + // Setup existing stake + mock_increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + NetUid::ROOT, + AlphaBalance::from(stake_amount.to_u64()), + ); + + // Setup TotalStake, SubnetAlphaOut and SubnetTAO (because we are going to unstake) + TotalStake::::set(stake_amount); + SubnetTAO::::insert(NetUid::ROOT, stake_amount); + SubnetAlphaOut::::insert(NetUid::ROOT, AlphaBalance::from(stake_amount.to_u64())); + + // Stake to hotkey account, and check if the result is ok + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + NetUid::ROOT, + AlphaBalance::from(stake_amount.to_u64()) + )); + + // Check if stake has been decreased + let new_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id); + assert_eq!(new_stake, 0.into()); + + // Check if total stake has decreased accordingly. + assert_eq!(SubtensorModule::get_total_stake(), 0.into()); + + // SubnetTAO updated + assert_eq!(SubnetTAO::::get(NetUid::ROOT), 0.into()); + + // SubnetAlphaIn updated + assert_eq!( + SubnetAlphaIn::::get(NetUid::ROOT), + AlphaBalance::from(stake_amount.to_u64()) + ); + + // SubnetAlphaOut updated + assert_eq!(SubnetAlphaOut::::get(NetUid::ROOT), 0.into()); + + // SubnetVolume updated + assert_eq!( + SubnetVolume::::get(NetUid::ROOT), + stake_amount.to_u64() as u128 + ); + }); +} diff --git a/pallets/subtensor/src/tests/staking/remove_stake_limit.rs b/pallets/subtensor/src/tests/staking/remove_stake_limit.rs new file mode 100644 index 0000000000..31e8c64667 --- /dev/null +++ b/pallets/subtensor/src/tests/staking/remove_stake_limit.rs @@ -0,0 +1,509 @@ +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] +//! Tests for [`crate::staking::remove_stake`] limit / max-amount remove paths. + +use approx::assert_abs_diff_eq; +use frame_support::sp_runtime::DispatchError; +use frame_support::{assert_err, assert_noop, assert_ok}; +use sp_core::U256; +use substrate_fixed::types::{U64F64, U96F32}; +use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; +use subtensor_swap_interface::SwapHandler; + +use super::super::mock::*; +use crate::*; + +#[test] +fn test_max_amount_remove_root() { + new_test_ext(0).execute_with(|| { + // 0 price on root => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_remove(NetUid::ROOT, TaoBalance::ZERO), + Ok(AlphaBalance::MAX) + ); + + // 0.5 price on root => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_remove(NetUid::ROOT, TaoBalance::from(500_000_000)), + Ok(AlphaBalance::MAX) + ); + + // 0.999999... price on root => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_remove(NetUid::ROOT, TaoBalance::from(999_999_999)), + Ok(AlphaBalance::MAX) + ); + + // 1.0 price on root => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_remove(NetUid::ROOT, TaoBalance::from(1_000_000_000)), + Ok(AlphaBalance::MAX) + ); + + // 1.000...001 price on root => max is 0 + assert_eq!( + SubtensorModule::get_max_amount_remove(NetUid::ROOT, TaoBalance::from(1_000_000_001)), + Ok(0u64.into()) + ); + + // 2.0 price on root => max is 0 + assert_eq!( + SubtensorModule::get_max_amount_remove(NetUid::ROOT, TaoBalance::from(2_000_000_000)), + Ok(0u64.into()) + ); + }); +} + +#[test] +fn test_max_amount_remove_stable() { + new_test_ext(0).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + + // 0 price => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_remove(netuid, TaoBalance::ZERO), + Ok(AlphaBalance::MAX) + ); + + // 0.999999... price => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_remove(netuid, TaoBalance::from(999_999_999)), + Ok(AlphaBalance::MAX) + ); + + // 1.0 price => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_remove(netuid, TaoBalance::from(1_000_000_000)), + Ok(AlphaBalance::MAX) + ); + + // 1.000...001 price => max is 0 + assert_eq!( + SubtensorModule::get_max_amount_remove(netuid, TaoBalance::from(1_000_000_001)), + Ok(0u64.into()) + ); + + // 2.0 price => max is 0 + assert_eq!( + SubtensorModule::get_max_amount_remove(netuid, TaoBalance::from(2_000_000_000)), + Ok(0u64.into()) + ); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::staking::remove_stake_limit::test_max_amount_remove_dynamic --exact --show-output +#[test] +fn test_max_amount_remove_dynamic() { + new_test_ext(0).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + // tao_in, alpha_in, limit_price, expected_max_swappable (+ 0.05% fee) + [ + // Zero handling (no panics) + ( + 0_u64, + 1_000_000_000_u64, + 100, + Err(DispatchError::from( + pallet_subtensor_swap::Error::::ReservesTooLow, + )), + ), + ( + 1_000_000_000, + 0, + 100, + Err(DispatchError::from( + pallet_subtensor_swap::Error::::PriceLimitExceeded, + )), + ), + (10_000_000_000, 10_000_000_000, 0, Ok(10_000_000_000_000)), + // Low bounds (numbers are empirical, it is only important that result + // is sharply decreasing when limit price increases) + (1_000, 1_000, 0, Ok(1_000_000)), + (1_001, 1_001, 0, Ok(1_001_000)), + (1_001, 1_001, 1, Ok(1_001_000)), + (1_001, 1_001, 2, Ok(1_001_000)), + (1_001, 1_001, 1_001, Ok(1_001_000)), + (1_001, 1_001, 10_000, Ok(17_472)), + (1_001, 1_001, 100_000, Ok(17_472)), + (1_001, 1_001, 1_000_000, Ok(17_472)), + (1_001, 1_001, 10_000_000, Ok(9_013)), + (1_001, 1_001, 100_000_000, Ok(2_165)), + // Basic math + (1_000_000, 1_000_000, 250_000_000, Ok(1_010_000)), + (1_000_000, 1_000_000, 62_500_000, Ok(3_030_000)), + ( + 1_000_000_000_000, + 1_000_000_000_000, + 62_500_000, + Ok(3_030_000_000_000), + ), + // Normal range values with edge cases and sanity checks + (200_000_000_000, 100_000_000_000, 0, Ok(100_000_000_000_000)), + ( + 200_000_000_000, + 100_000_000_000, + 500_000_000, + Ok(101_000_000_000), + ), + ( + 200_000_000_000, + 100_000_000_000, + 125_000_000, + Ok(303_000_000_000), + ), + ( + 200_000_000_000, + 100_000_000_000, + 2_000_000_000, + Err(DispatchError::from( + pallet_subtensor_swap::Error::::PriceLimitExceeded, + )), + ), + ( + 200_000_000_000, + 100_000_000_000, + 2_000_000_001, + Err(DispatchError::from( + pallet_subtensor_swap::Error::::PriceLimitExceeded, + )), + ), + (200_000_000_000, 100_000_000_000, 1_999_999_999, Ok(24)), + (200_000_000_000, 100_000_000_000, 1_999_999_990, Ok(250)), + // Miscellaneous overflows and underflows + ( + 21_000_000_000_000_000, + 1_000_000, + 21_000_000_000_000_000, + Ok(17_455_533), + ), + (21_000_000_000_000_000, 1_000_000, u64::MAX, Ok(67_000)), + ( + 21_000_000_000_000_000, + 1_000_000_000_000_000_000, + u64::MAX, + Err(DispatchError::from( + pallet_subtensor_swap::Error::::PriceLimitExceeded, + )), + ), + ( + 21_000_000_000_000_000, + 1_000_000_000_000_000_000, + 20_000_000, + Ok(24_700_000_000_000_000), + ), + ( + 21_000_000_000_000_000, + 21_000_000_000_000_000, + 999_999_999, + Ok(10_605_000), + ), + ( + 21_000_000_000_000_000, + 21_000_000_000_000_000, + 0, + Ok(u64::MAX), + ), + ] + .into_iter() + .for_each(|(tao_in, alpha_in, limit_price, expected_max_swappable)| { + let alpha_in = AlphaBalance::from(alpha_in); + // Forse-set alpha in and tao reserve to achieve relative price of subnets + SubnetTAO::::insert(netuid, TaoBalance::from(tao_in)); + SubnetAlphaIn::::insert(netuid, alpha_in); + + if !alpha_in.is_zero() { + let expected_price = U64F64::from_num(tao_in) / U64F64::from_num(alpha_in); + assert_eq!( + ::SwapInterface::current_alpha_price(netuid.into()), + expected_price + ); + } + + match expected_max_swappable { + Err(e) => assert_err!( + SubtensorModule::get_max_amount_remove(netuid, limit_price.into()), + DispatchError::from(e) + ), + Ok(v) => { + let v = AlphaBalance::from(v); + let actual = + SubtensorModule::get_max_amount_remove(netuid, limit_price.into()).unwrap(); + let epsilon = v / 100.into(); + let diff = actual.max(v).saturating_sub(actual.min(v)); + assert!( + diff <= epsilon, + "max remove mismatch: tao_in={tao_in}, alpha_in={alpha_in:?}, limit_price={limit_price}, actual={actual:?}, expected={v:?}, epsilon={epsilon:?}", + ); + } + } + }); + }); +} + +#[test] +fn test_remove_stake_limit_ok() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(533453); + let coldkey_account_id = U256::from(55453); + let stake_amount = TaoBalance::from(300_000_000_000_u64); + + // add network + let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + add_balance_to_coldkey_account( + &coldkey_account_id, + stake_amount + ExistentialDeposit::get(), + ); + + // Forse-set sufficient reserves + let tao_reserve = TaoBalance::from(100_000_000_000_u64); + let alpha_in = AlphaBalance::from(100_000_000_000_u64); + SubnetTAO::::insert(netuid, tao_reserve); + SubnetAlphaIn::::insert(netuid, alpha_in); + + // Stake to hotkey account, and check if the result is ok + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + stake_amount + )); + let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ); + + // Setup limit price to 99% of current price + let current_price = + ::SwapInterface::current_alpha_price(netuid.into()); + let limit_price = (current_price.to_num::() * 990_000_000_f64) as u64; + + // Alpha unstaked - calculated using formula from delta_in() + let expected_alpha_reduction = (0.00138 * (alpha_in.to_u64() as f64)) as u64; + let fee: u64 = (expected_alpha_reduction as f64 * 0.003) as u64; + + // Remove stake with slippage safety + assert_ok!(SubtensorModule::remove_stake_limit( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + alpha_before / 2.into(), + limit_price.into(), + true + )); + let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ); + + // Check if stake has decreased properly + assert_abs_diff_eq!( + alpha_before - alpha_after, + AlphaBalance::from(expected_alpha_reduction + fee), + epsilon = AlphaBalance::from(expected_alpha_reduction / 10), + ); + }); +} + +#[test] +fn test_remove_stake_limit_fill_or_kill() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(533453); + let coldkey_account_id = U256::from(55453); + let stake_amount = AlphaBalance::from(300_000_000_000_u64); + let unstake_amount = AlphaBalance::from(150_000_000_000_u64); + + // add network + let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + + // Give the neuron some stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + stake_amount, + ); + + // Forse-set alpha in and tao reserve to make price equal 1.5 + let tao_reserve = TaoBalance::from(150_000_000_000_u64); + let alpha_in = AlphaBalance::from(100_000_000_000_u64); + SubnetTAO::::insert(netuid, tao_reserve); + SubnetAlphaIn::::insert(netuid, alpha_in); + let current_price = + ::SwapInterface::current_alpha_price(netuid.into()); + assert_eq!(current_price, U96F32::from_num(1.5)); + + // Setup limit price so that it doesn't drop by more than 10% from current price + let limit_price = TaoBalance::from(1_350_000_000); + + // Remove stake with slippage safety - fails + assert_noop!( + SubtensorModule::remove_stake_limit( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + unstake_amount, + limit_price, + false + ), + Error::::SlippageTooHigh + ); + + // Lower the amount: Should succeed + assert_ok!(SubtensorModule::remove_stake_limit( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + unstake_amount / 100.into(), + limit_price.into(), + false + ),); + }); +} + +#[test] +fn test_remove_stake_full_limit_ok() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(1); + let coldkey_account_id = U256::from(2); + let stake_amount = AlphaBalance::from(10_000_000_000_u64); + + // add network + let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + remove_owner_registration_stake(netuid); + + // Give the neuron some stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + stake_amount, + ); + + let tao_reserve = TaoBalance::from(100_000_000_000_u64); + let alpha_in = AlphaBalance::from(100_000_000_000_u64); + SubnetTAO::::insert(netuid, tao_reserve); + SubnetAlphaIn::::insert(netuid, alpha_in); + + let limit_price = TaoBalance::from(90_000_000); + + // Remove stake with slippage safety + assert_ok!(SubtensorModule::remove_stake_full_limit( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + Some(limit_price), + )); + + // Check if stake has decreased to zero + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid + ), + AlphaBalance::ZERO + ); + + let new_balance = SubtensorModule::get_coldkey_balance(&coldkey_account_id); + assert_abs_diff_eq!( + new_balance, + 9_086_000_000_u64.into(), + epsilon = 1_000_000.into() + ); + }); +} + +#[test] +fn test_remove_stake_full_limit_fails_slippage_too_high() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(1); + let coldkey_account_id = U256::from(2); + let stake_amount = AlphaBalance::from(10_000_000_000_u64); + + // add network + let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + + // Give the neuron some stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + stake_amount, + ); + + let tao_reserve = TaoBalance::from(100_000_000_000_u64); + let alpha_in = AlphaBalance::from(100_000_000_000_u64); + SubnetTAO::::insert(netuid, tao_reserve); + SubnetAlphaIn::::insert(netuid, alpha_in); + + let invalid_limit_price = TaoBalance::from(910_000_000_u64); + + // Remove stake with slippage safety + assert_err!( + SubtensorModule::remove_stake_full_limit( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + Some(invalid_limit_price), + ), + Error::::SlippageTooHigh + ); + }); +} + +#[test] +fn test_remove_stake_full_limit_ok_with_no_limit_price() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(1); + let coldkey_account_id = U256::from(2); + let stake_amount = AlphaBalance::from(10_000_000_000_u64); + + // add network + let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + remove_owner_registration_stake(netuid); + + // Give the neuron some stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + stake_amount, + ); + + let tao_reserve = TaoBalance::from(100_000_000_000_u64); + let alpha_in = AlphaBalance::from(100_000_000_000_u64); + SubnetTAO::::insert(netuid, tao_reserve); + SubnetAlphaIn::::insert(netuid, alpha_in); + + // Remove stake with slippage safety + assert_ok!(SubtensorModule::remove_stake_full_limit( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + None, + )); + + // Check if stake has decreased to zero + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid + ), + AlphaBalance::ZERO + ); + + let new_balance = SubtensorModule::get_coldkey_balance(&coldkey_account_id); + assert_abs_diff_eq!( + new_balance, + 9_086_000_000_u64.into(), + epsilon = 1_000_000.into() + ); + }); +} diff --git a/pallets/subtensor/src/tests/staking/sharepool.rs b/pallets/subtensor/src/tests/staking/sharepool.rs new file mode 100644 index 0000000000..bb6d1717de --- /dev/null +++ b/pallets/subtensor/src/tests/staking/sharepool.rs @@ -0,0 +1,460 @@ +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] +//! Tests for share-pool lazy migration and Alpha data-ops used by staking. + +use approx::assert_abs_diff_eq; +use share_pool::SafeFloat; +use sp_core::U256; +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::AlphaBalance; + +use super::super::mock::*; +use crate::*; + +// cargo test --package pallet-subtensor --lib -- tests::staking::sharepool::test_lazy_sharepool_migration_get_stake_reads_from_deprecated_alpha_map --exact --nocapture +#[test] +fn test_lazy_sharepool_migration_get_stake_reads_from_deprecated_alpha_map() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to deprecated Alpha map + Alpha::::insert((hotkey, coldkey, netuid), U64F64::from(1_u64)); + TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid), + AlphaBalance::from(stake) + ); + }); +} + +#[test] +fn test_lazy_sharepool_migration_get_stake_reads_from_alpha_v2_map() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to AlphaV2 map + AlphaV2::::insert((hotkey, coldkey, netuid), SafeFloat::from(1_u64)); + TotalHotkeySharesV2::::insert(hotkey, netuid, SafeFloat::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid), + AlphaBalance::from(stake) + ); + }); +} + +#[test] +fn test_lazy_sharepool_migration_get_stake_reads_from_cross_alpha_maps() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to Alpha map + Alpha::::insert((hotkey, coldkey, netuid), U64F64::from(1_u64)); + // but total shares are in TotalHotkeySharesV2 map (already migrated) + TotalHotkeySharesV2::::insert(hotkey, netuid, SafeFloat::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid), + AlphaBalance::from(stake) + ); + }); +} + +#[test] +fn test_lazy_sharepool_migration_staking_causes_migration() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to deprecated Alpha map + Alpha::::insert((hotkey, coldkey, netuid), U64F64::from(1_u64)); + TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Stake more via stake_into_subnet + increase_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake.into(), netuid); + + // Verify that deprecated v1 map values are gone + assert!(Alpha::::try_get((&hotkey, &coldkey, netuid)).is_err()); + assert!(TotalHotkeyShares::::try_get(hotkey, netuid).is_err()); + + // Verify that v2 map values are present + let migrated_share = AlphaV2::::get((&hotkey, &coldkey, netuid)); + let migrated_denominator = TotalHotkeySharesV2::::get(hotkey, netuid); + + assert_abs_diff_eq!( + f64::from((migrated_share.div(&migrated_denominator)).unwrap()), + 1.0, + epsilon = 0.000000000000001 + ); + }); +} + +#[test] +fn test_sharepool_dataops_get_value_v1() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to deprecated Alpha map + Alpha::::insert((hotkey, coldkey, netuid), U64F64::from(1_u64)); + TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and read get_value + let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + let actual_value = share_pool.get_value(&coldkey); + + assert_eq!(actual_value, stake); + }); +} + +#[test] +fn test_sharepool_dataops_get_value_v2() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to AlphaV2 map + let share = sf_from_u64(1_u64); + AlphaV2::::insert((hotkey, coldkey, netuid), share.clone()); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and read get_value + let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + let actual_value = share_pool.get_value(&coldkey); + + assert_eq!(actual_value, stake); + }); +} + +#[test] +fn test_sharepool_dataops_get_value_mixed_v1_v2() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to deprecated Alpha map and new THS v2 map + let share = sf_from_u64(1_u64); + Alpha::::insert((hotkey, coldkey, netuid), U64F64::from(1_u64)); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and read get_value + let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + let actual_value = share_pool.get_value(&coldkey); + + assert_eq!(actual_value, stake); + }); +} + +#[test] +fn test_sharepool_dataops_get_value_mixed_v2_v1() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to new AlphaV2 map and deprecated THS map + let share = sf_from_u64(1_u64); + AlphaV2::::insert((hotkey, coldkey, netuid), share); + TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and read get_value + let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + let actual_value = share_pool.get_value(&coldkey); + + assert_eq!(actual_value, stake); + }); +} + +#[test] +fn test_sharepool_dataops_get_value_from_shares_v1() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to deprecated THS map + TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and read get_value_from_shares + let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + let current_share = SafeFloat::from(U64F64::from(1_u64)); + let actual_value = share_pool.get_value_from_shares(current_share); + + assert_eq!(actual_value, stake); + }); +} + +#[test] +fn test_sharepool_dataops_get_value_from_shares_v2() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to new THS v2 map + let share = sf_from_u64(1_u64); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and read get_value_from_shares + let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + let current_share = SafeFloat::from(U64F64::from(1_u64)); + let actual_value = share_pool.get_value_from_shares(current_share); + + assert_eq!(actual_value, stake); + }); +} + +#[test] +fn test_sharepool_dataops_update_value_for_all() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to new AlphaV2 map + let share = sf_from_u64(1_u64); + AlphaV2::::insert((hotkey, coldkey, netuid), share.clone()); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and call update_value_for_all + let mut share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + share_pool.update_value_for_all(stake as i64); + let actual_value = share_pool.get_value(&coldkey); + assert_eq!(actual_value, stake * 2); + + share_pool.update_value_for_all(-(stake as i64)); + let actual_value = share_pool.get_value(&coldkey); + assert_eq!(actual_value, stake); + }); +} + +#[test] +fn test_sharepool_dataops_update_value_for_one_v1_with_migration() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to deprecated Alpha and THS maps + Alpha::::insert((hotkey, coldkey, netuid), U64F64::from(1_u64)); + TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and call update_value_for_one + let mut share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + share_pool.update_value_for_one(&coldkey, stake as i64); + let actual_value = share_pool.get_value(&coldkey); + assert_eq!(actual_value, stake * 2); + + // Verify deletion from deprecated + assert!(!Alpha::::contains_key((hotkey, coldkey, netuid))); + assert!(!TotalHotkeyShares::::contains_key(hotkey, netuid)); + }); +} + +#[test] +fn test_sharepool_dataops_update_value_for_one_v2() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to new AlphaV2 and THS maps + let share = sf_from_u64(1_u64); + AlphaV2::::insert((hotkey, coldkey, netuid), share.clone()); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and call update_value_for_one + let mut share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + share_pool.update_value_for_one(&coldkey, stake as i64); + let actual_value = share_pool.get_value(&coldkey); + assert_eq!(actual_value, stake * 2); + }); +} + +#[test] +fn test_sharepool_dataops_update_value_for_one_mixed_v1_v2() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to deprecated Alpha and new THS v2 maps + let share = sf_from_u64(1_u64); + Alpha::::insert((hotkey, coldkey, netuid), U64F64::from(1_u64)); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and call update_value_for_one + let mut share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + share_pool.update_value_for_one(&coldkey, stake as i64); + let actual_value = share_pool.get_value(&coldkey); + assert_eq!(actual_value, stake * 2); + + // Verify deletion from deprecated + assert!(!Alpha::::contains_key((hotkey, coldkey, netuid))); + }); +} + +#[test] +fn test_sharepool_dataops_update_value_for_one_mixed_v2_v1() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + let stake = 200_000_u64; + + // add stake to new AlphaV2 and deprecated THS maps + let share = sf_from_u64(1_u64); + AlphaV2::::insert((hotkey, coldkey, netuid), share); + TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and call update_value_for_one + let mut share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + share_pool.update_value_for_one(&coldkey, stake as i64); + let actual_value = share_pool.get_value(&coldkey); + assert_eq!(actual_value, stake * 2); + + // Verify deletion from deprecated + assert!(!TotalHotkeyShares::::contains_key(hotkey, netuid)); + }); +} + +#[test] +fn test_sharepool_dataops_get_value_returns_zero_on_non_existing_v1() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + remove_owner_registration_stake(netuid); + let stake = 200_000_u64; + + // add to deprecated THS map, but no value in Alpha map + TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and read get_value + let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + let actual_value = share_pool.get_value(&coldkey); + assert_eq!(actual_value, 0_u64); + }); +} + +#[test] +fn test_sharepool_dataops_get_value_returns_zero_on_non_existing_v2() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + remove_owner_registration_stake(netuid); + let stake = 200_000_u64; + + // add to THSV2 map, but no value in AlphaV2 map + let share = sf_from_u64(1_u64); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and read get_value + let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + let actual_value = share_pool.get_value(&coldkey); + assert_eq!(actual_value, 0_u64); + }); +} + +#[test] +fn test_sharepool_dataops_try_get_value_returns_err_on_non_existing_v1() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + remove_owner_registration_stake(netuid); + let stake = 200_000_u64; + + // add to deprecated THS map, but no value in Alpha map + TotalHotkeyShares::::insert(hotkey, netuid, U64F64::from(1_u64)); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and read get_value + let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + let maybe_actual_value = share_pool.try_get_value(&coldkey); + assert!(maybe_actual_value.is_err()); + }); +} + +#[test] +fn test_sharepool_dataops_try_get_value_returns_err_on_non_existing_v2() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + remove_owner_registration_stake(netuid); + let stake = 200_000_u64; + + // add to THSV2 map, but no value in AlphaV2 map + let share = sf_from_u64(1_u64); + TotalHotkeySharesV2::::insert(hotkey, netuid, share); + TotalHotkeyAlpha::::insert(hotkey, netuid, AlphaBalance::from(stake)); + + // Get real share pool and read get_value + let share_pool = SubtensorModule::get_alpha_share_pool(hotkey, netuid); + let maybe_actual_value = share_pool.try_get_value(&coldkey); + assert!(maybe_actual_value.is_err()); + }); +} diff --git a/pallets/subtensor/src/tests/staking/stake_utils.rs b/pallets/subtensor/src/tests/staking/stake_utils.rs new file mode 100644 index 0000000000..ab15de8c75 --- /dev/null +++ b/pallets/subtensor/src/tests/staking/stake_utils.rs @@ -0,0 +1,132 @@ +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] +//! Tests for [`crate::staking::stake_utils`] swap fee / large-swap helpers. + +use approx::assert_abs_diff_eq; +use frame_support::assert_ok; +use sp_core::U256; +use subtensor_runtime_common::{AlphaBalance, TaoBalance, Token}; +use subtensor_swap_interface::SwapHandler; + +use super::super::mock; +use super::super::mock::*; +use crate::*; + +#[test] +fn test_swap_fees_tao_correctness() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let coldkey = U256::from(4); + let block_builder = U256::from(12345u64); + let amount = TaoBalance::from(1_000_000_000_u64); + let owner_balance_before = amount * 10.into(); + let user_balance_before = amount * 100.into(); + + // add network + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + add_balance_to_coldkey_account(&owner_coldkey, owner_balance_before); + add_balance_to_coldkey_account(&coldkey, user_balance_before); + + // Forse-set alpha in and tao reserve to make price equal 0.25 + let tao_reserve = TaoBalance::from(100_000_000_000_u64); + let alpha_in = AlphaBalance::from(400_000_000_000_u64); + mock::setup_reserves(netuid, tao_reserve, alpha_in); + + // Check starting "total TAO" + let block_builder_balance_before = SubtensorModule::get_coldkey_balance(&block_builder); + let total_tao_before = user_balance_before + + owner_balance_before + + SubnetTAO::::get(netuid) + + block_builder_balance_before; + + // Get alpha for owner + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(owner_coldkey), + owner_hotkey, + netuid, + amount.into(), + )); + + // Add owner coldkey Alpha as concentrated liquidity + // between current price current price + 0.01 + let current_price = + ::SwapInterface::current_alpha_price(netuid.into()) + .to_num::() + + 0.0001; + let limit_price = current_price + 0.01; + + // Limit-buy and then sell all alpha for user to hit owner liquidity + assert_ok!(SubtensorModule::add_stake_limit( + RuntimeOrigin::signed(coldkey), + owner_hotkey, + netuid, + amount.into(), + ((limit_price * u64::MAX as f64) as u64).into(), + true + )); + + let user_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &owner_hotkey, + &coldkey, + netuid, + ); + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(coldkey), + owner_hotkey, + netuid, + user_alpha, + )); + + // TODO: This block is for balancer swap + // Cause tao fees to propagate to SubnetTAO + // let (claimed_tao_fees, _) = + // ::SwapInterface::adjust_protocol_liquidity( + // netuid, + // 0.into(), + // 0.into(), + // ); + // SubnetTAO::::mutate(netuid, |tao| *tao += claimed_tao_fees); + + // Check ending "total TAO" + let owner_balance_after = SubtensorModule::get_coldkey_balance(&owner_coldkey); + let user_balance_after = SubtensorModule::get_coldkey_balance(&coldkey); + let block_builder_balance_after = SubtensorModule::get_coldkey_balance(&block_builder); + + let total_tao_after = user_balance_after + + owner_balance_after + + SubnetTAO::::get(netuid) + + block_builder_balance_after; + + // Total TAO does not change, leave some epsilon for rounding + assert_abs_diff_eq!(total_tao_before, total_tao_after, epsilon = 2.into()); + }); +} + +#[test] +fn test_large_swap() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let coldkey = U256::from(100); + + // add network + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_000_u64.into()); + let swap_amount = TaoBalance::from(100_000_000_000_000_u64); + let tao = TaoBalance::from(swap_amount.to_u64() / 1000); + let alpha = AlphaBalance::from(1_000_000_000_000_000_u64); + SubnetTAO::::insert(netuid, tao); + SubnetAlphaIn::::insert(netuid, alpha); + + // Force the swap to initialize + ::SwapInterface::init_swap(netuid, None); + + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + owner_hotkey, + netuid, + swap_amount, + )); + }); +} diff --git a/pallets/subtensor/src/tests/staking/unstake.rs b/pallets/subtensor/src/tests/staking/unstake.rs new file mode 100644 index 0000000000..824b240a28 --- /dev/null +++ b/pallets/subtensor/src/tests/staking/unstake.rs @@ -0,0 +1,409 @@ +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] +//! Tests for [`crate::staking::remove_stake`] unstake-all / unstake-from-subnet paths. + +use approx::assert_abs_diff_eq; +use frame_support::sp_runtime::DispatchError; +use frame_support::{assert_err, assert_ok}; +use safe_math::FixedExt; +use sp_core::U256; +use substrate_fixed::traits::FromFixed; +use substrate_fixed::types::{I96F32, I110F18}; +use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; +use subtensor_swap_interface::{Order, SwapHandler}; + +use super::super::mock; +use super::super::mock::*; +use crate::*; + +/// cargo test --package pallet-subtensor --lib -- tests::staking::unstake::test_unstake_all_hits_liquidity_min --exact --show-output +#[test] +fn test_unstake_all_hits_liquidity_min() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let stake_amount = AlphaBalance::from(190_000_000_000_u64); // 190 Alpha + + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey, coldkey, 192213123); + // Give the neuron some stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + stake_amount, + ); + + // Setup the Alpha pool so that removing all the Alpha will bring liqudity below the minimum + let remaining_tao = TaoBalance::from(u64::from(mock::SwapMinimumReserve::get()) - 1); + let alpha_reserves = AlphaBalance::from(stake_amount.to_u64() + 10_000_000); + mock::setup_reserves(netuid, remaining_tao, alpha_reserves); + + // Try to unstake, but we reduce liquidity too far + + assert_ok!(SubtensorModule::unstake_all( + RuntimeOrigin::signed(coldkey), + hotkey, + )); + + // Expect nothing to be unstaked + let new_alpha = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + assert_abs_diff_eq!(new_alpha, stake_amount, epsilon = AlphaBalance::ZERO); + }); +} + +#[test] +fn test_unstake_all_alpha_hits_liquidity_min() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let stake_amount = TaoBalance::from(100_000_000_000_u64); // 100 TAO + + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey, coldkey, 192213123); + add_balance_to_coldkey_account(&coldkey, stake_amount + ExistentialDeposit::get()); + // Give the neuron some stake to remove + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + stake_amount + )); + + // Setup the pool so that removing all the TAO will bring liqudity below the minimum + let remaining_tao = I96F32::from_num(u64::from(mock::SwapMinimumReserve::get()) - 1) + .saturating_sub(I96F32::from(1)); + let alpha = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + let alpha_reserves = I110F18::from(u64::from(alpha) + 10_000_000); + + let k = I110F18::from_fixed(remaining_tao) + .saturating_mul(alpha_reserves.saturating_add(I110F18::from(u64::from(alpha)))); + let tao_reserves = k.safe_div(alpha_reserves); + + mock::setup_reserves( + netuid, + (tao_reserves.to_num::() / 100_u64).into(), + alpha_reserves.to_num::().into(), + ); + + // Try to unstake, but we reduce liquidity too far + + assert_err!( + SubtensorModule::unstake_all_alpha(RuntimeOrigin::signed(coldkey), hotkey), + Error::::AmountTooLow + ); + + // Expect nothing to be unstaked + let new_alpha = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + assert_eq!(new_alpha, alpha); + }); +} + +#[test] +fn test_unstake_all_alpha_works() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let stake_amount = TaoBalance::from(190_000_000_000_u64); // 190 TAO + + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey, coldkey, 192213123); + add_balance_to_coldkey_account(&coldkey, stake_amount + ExistentialDeposit::get()); + + // Give the neuron some stake to remove + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + stake_amount + )); + + // Setup the pool so that removing all the TAO will keep liq above min + mock::setup_reserves( + netuid, + stake_amount * 10.into(), + u64::from(stake_amount * 100.into()).into(), + ); + + // Unstake all alpha to root + assert_ok!(SubtensorModule::unstake_all_alpha( + RuntimeOrigin::signed(coldkey), + hotkey, + )); + + let new_alpha = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + assert_abs_diff_eq!(new_alpha, AlphaBalance::ZERO, epsilon = 1_000.into()); + let new_root = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + NetUid::ROOT, + ); + assert!(new_root > 100_000.into()); + }); +} + +#[test] +fn test_unstake_all_works() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let stake_amount = TaoBalance::from(190_000_000_000_u64); // 190 TAO + + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey, coldkey, 192213123); + add_balance_to_coldkey_account(&coldkey, stake_amount + ExistentialDeposit::get()); + + // Give the neuron some stake to remove + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + stake_amount + )); + + // Setup the pool so that removing all the TAO will keep liq above min + mock::setup_reserves( + netuid, + stake_amount * 10.into(), + u64::from(stake_amount * 100.into()).into(), + ); + // Unstake all alpha to free balance + assert_ok!(SubtensorModule::unstake_all( + RuntimeOrigin::signed(coldkey), + hotkey, + )); + + let new_alpha = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + assert_abs_diff_eq!(new_alpha, AlphaBalance::ZERO, epsilon = 1_000.into()); + let new_balance = SubtensorModule::get_coldkey_balance(&coldkey); + assert!(new_balance > 100_000.into()); + }); +} + +#[test] +fn test_unstake_from_subnet_low_amount() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let hotkey = U256::from(3); + let coldkey = U256::from(4); + let amount = 10; + + // add network + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + // Forse-set alpha in and tao reserve to make price equal 0.01 + let tao_reserve = TaoBalance::from(100_000_000_000_u64); + let alpha_in = AlphaBalance::from(1_000_000_000_000_u64); + mock::setup_reserves(netuid, tao_reserve, alpha_in); + + // Initialize swap v3 + let order = GetAlphaForTao::::with_amount(0); + assert_ok!(::SwapInterface::swap( + netuid.into(), + order, + TaoBalance::MAX, + false, + true + )); + + // Add stake and check if the result is ok + let large_balance = 20_000_000_000_000_000_u64; + add_balance_to_coldkey_account(&coldkey, large_balance.into()); + assert_ok!(SubtensorModule::stake_into_subnet( + &hotkey, + &coldkey, + netuid, + amount.into(), + large_balance.into(), + false, + )); + + // Remove stake + let alpha = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + assert_ok!(SubtensorModule::unstake_from_subnet( + &hotkey, + &coldkey, + &coldkey, + netuid, + alpha, + TaoBalance::ZERO, + false, + )); + + // Check if stake is zero + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid), + AlphaBalance::ZERO, + ); + }); +} + +#[test] +fn test_unstake_from_subnet_prohibitive_limit() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let coldkey = U256::from(4); + let amount = 100_000_000; + + // add network + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + add_balance_to_coldkey_account(&coldkey, amount.into()); + + // Forse-set alpha in and tao reserve to make price equal 0.01 + let tao_reserve = TaoBalance::from(100_000_000_000_u64); + let alpha_in = AlphaBalance::from(1_000_000_000_000_u64); + mock::setup_reserves(netuid, tao_reserve, alpha_in); + + // Initialize swap v3 + let order = GetAlphaForTao::::with_amount(0); + assert_ok!(::SwapInterface::swap( + netuid.into(), + order, + TaoBalance::MAX, + false, + true + )); + + // Add stake and check if the result is ok + assert_ok!(SubtensorModule::stake_into_subnet( + &owner_hotkey, + &coldkey, + netuid, + amount.into(), + TaoBalance::MAX, + false, + )); + + // Remove stake + // Use prohibitive limit price + let balance_before = SubtensorModule::get_coldkey_balance(&coldkey); + let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &owner_hotkey, + &coldkey, + netuid, + ); + assert_err!( + SubtensorModule::remove_stake_limit( + RuntimeOrigin::signed(coldkey), + owner_hotkey, + netuid, + alpha, + TaoBalance::MAX, + true, + ), + DispatchError::from(pallet_subtensor_swap::Error::::PriceLimitExceeded) + ); + + // Check if stake has NOT decreased + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &owner_hotkey, + &coldkey, + netuid + ), + alpha + ); + + // Check if balance has NOT increased + assert_eq!( + SubtensorModule::get_coldkey_balance(&coldkey), + balance_before, + ); + }); +} + +#[test] +fn test_unstake_full_amount() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let coldkey = U256::from(4); + let amount = 100_000_000; + + // add network + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + add_balance_to_coldkey_account(&coldkey, amount.into()); + + // Forse-set alpha in and tao reserve to make price equal 0.01 + let tao_reserve = TaoBalance::from(100_000_000_000_u64); + let alpha_in = AlphaBalance::from(1_000_000_000_000_u64); + mock::setup_reserves(netuid, tao_reserve, alpha_in); + + // Initialize swap v3 + let order = GetAlphaForTao::::with_amount(0); + assert_ok!(::SwapInterface::swap( + netuid.into(), + order, + TaoBalance::MAX, + false, + true + )); + + // Add stake and check if the result is ok + assert_ok!(SubtensorModule::stake_into_subnet( + &owner_hotkey, + &coldkey, + netuid, + amount.into(), + TaoBalance::MAX, + false, + )); + + // Remove stake + // Use prohibitive limit price + let balance_before = SubtensorModule::get_coldkey_balance(&coldkey); + let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &owner_hotkey, + &coldkey, + netuid, + ); + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(coldkey), + owner_hotkey, + netuid, + alpha, + )); + + // Check if stake is zero + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &owner_hotkey, + &coldkey, + netuid + ), + AlphaBalance::ZERO + ); + + // Check if balance has increased accordingly + let balance_after = SubtensorModule::get_coldkey_balance(&coldkey); + let actual_balance_increase = u64::from(balance_after - balance_before) as f64; + let fee_rate = pallet_subtensor_swap::FeeRate::::get(NetUid::from(netuid)) as f64 + / u16::MAX as f64; + let expected_balance_increase = amount as f64 * (1. - fee_rate) / (1. + fee_rate); + assert_abs_diff_eq!( + actual_balance_increase, + expected_balance_increase, + epsilon = expected_balance_increase / 10_000. + ); + }); +} diff --git a/pallets/subtensor/src/tests/staking2.rs b/pallets/subtensor/src/tests/staking2.rs index 1e05a3a3d2..8b392b2653 100644 --- a/pallets/subtensor/src/tests/staking2.rs +++ b/pallets/subtensor/src/tests/staking2.rs @@ -1,3 +1,7 @@ +//! Additional staking / dynamic-mechanism swap tests beyond [`crate::tests::staking`]. +//! +//! Covers TAO↔alpha stake base cases and fee/dispatch-info checks. + #![allow(clippy::unwrap_used)] use frame_support::{ diff --git a/pallets/subtensor/src/tests/subnet.rs b/pallets/subtensor/src/tests/subnet.rs index be1487b4ec..0e6b07f5aa 100644 --- a/pallets/subtensor/src/tests/subnet.rs +++ b/pallets/subtensor/src/tests/subnet.rs @@ -1,4 +1,9 @@ +//! Tests for subnet lifecycle helpers ([`crate::subnets::subnet`], symbols). +//! +//! Covers `do_start_call`, symbol allocation, and emission-start gating. + #![allow(clippy::expect_used, clippy::unwrap_used)] + use super::mock::*; use crate::subnets::symbols::{DEFAULT_SYMBOL, SYMBOLS}; use crate::*; @@ -887,13 +892,13 @@ fn test_is_subnet_account_id() { add_network(netuid, 10, 0); let account_id = SubtensorModule::get_subnet_account_id(netuid).unwrap(); - let roudtrip_netuid = SubtensorModule::is_subnet_account_id(&account_id); + let roudtrip_netuid = SubtensorModule::netuid_for_subnet_account(&account_id); assert_eq!(netuid, roudtrip_netuid.unwrap()); } // Not a subnet account let not_subnet_account_id = U256::from(1); - assert!(SubtensorModule::is_subnet_account_id(¬_subnet_account_id).is_none()); + assert!(SubtensorModule::netuid_for_subnet_account(¬_subnet_account_id).is_none()); }); } diff --git a/pallets/subtensor/src/tests/subnet_emissions.rs b/pallets/subtensor/src/tests/subnet_emissions.rs index ff56be851c..42002d327a 100644 --- a/pallets/subtensor/src/tests/subnet_emissions.rs +++ b/pallets/subtensor/src/tests/subnet_emissions.rs @@ -1,4 +1,9 @@ +//! Tests for subnet emission share / price-flow math ([`crate::coinbase`]). +//! +//! Covers `inplace_pow_normalize`, emission shares, and root-prop interactions. + #![allow(unused, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used)] + use super::mock::*; use crate::*; use alloc::{collections::BTreeMap, vec::Vec}; @@ -182,7 +187,7 @@ fn get_shares_ignores_root_prop_storage_when_prices_and_burns_match() { epsilon = 1e-9 ); - let shares = SubtensorModule::get_shares(&[n1, n2]); + let shares = SubtensorModule::subnet_emission_shares(&[n1, n2]); let s1 = shares.get(&n1).copied().unwrap().to_num::(); let s2 = shares.get(&n2).copied().unwrap().to_num::(); @@ -213,7 +218,7 @@ fn get_shares_ignores_root_prop_storage_when_prices_and_burns_match() { // SubnetEmaTaoFlow::::insert(n3, (block_num, i64f64(6_000.0))); // let subnets = vec![n1, n2, n3]; -// let shares = SubtensorModule::get_shares(&subnets); +// let shares = SubtensorModule::subnet_emission_shares(&subnets); // // Sum ≈ 1 // let sum: f64 = shares.values().map(|v| v.to_num::()).sum(); @@ -259,7 +264,7 @@ fn get_shares_ignores_root_prop_storage_when_prices_and_burns_match() { // SubnetEmaTaoFlow::::insert(n2, (block_num, i64f64(2e-9))); // let subnets = vec![n1, n2]; -// let shares = SubtensorModule::get_shares(&subnets); +// let shares = SubtensorModule::subnet_emission_shares(&subnets); // let sum: f64 = shares.values().map(|v| v.to_num::()).sum(); // assert_abs_diff_eq!(sum, 1.0_f64, epsilon = 1e-8); @@ -301,7 +306,7 @@ fn get_shares_ignores_root_prop_storage_when_prices_and_burns_match() { // SubnetEmaTaoFlow::::insert(n2, (block_num, i64f64(1.8e12))); // let subnets = vec![n1, n2]; -// let shares = SubtensorModule::get_shares(&subnets); +// let shares = SubtensorModule::subnet_emission_shares(&subnets); // let sum: f64 = shares.values().map(|v| v.to_num::()).sum(); // assert_abs_diff_eq!(sum, 1.0_f64, epsilon = 1e-9); @@ -360,7 +365,7 @@ fn seed_price_and_flow(n1: NetUid, n2: NetUid, price1: f64, price2: f64, flow1: // SubnetEmaTaoFlow::::insert(n1, (now, i64f64(-100.0))); // SubnetEmaTaoFlow::::insert(n2, (now, i64f64(500.0))); -// let shares = SubtensorModule::get_shares(&[n1, n2]); +// let shares = SubtensorModule::subnet_emission_shares(&[n1, n2]); // let s1 = shares.get(&n1).unwrap().to_num::(); // let s2 = shares.get(&n2).unwrap().to_num::(); @@ -400,7 +405,7 @@ fn seed_price_and_flow(n1: NetUid, n2: NetUid, price1: f64, price2: f64, flow1: // SubnetEmaTaoFlow::::insert(n1, (now, i64f64(-100.0))); // SubnetEmaTaoFlow::::insert(n2, (now, i64f64(-200.0))); -// let shares = SubtensorModule::get_shares(&[n1, n2]); +// let shares = SubtensorModule::subnet_emission_shares(&[n1, n2]); // let s1 = shares.get(&n1).unwrap().to_num::(); // let s2 = shares.get(&n2).unwrap().to_num::(); @@ -436,7 +441,7 @@ fn seed_price_and_flow(n1: NetUid, n2: NetUid, price1: f64, price2: f64, flow1: // SubnetEmaTaoFlow::::insert(n1, (now, i64f64(1000.0))); // SubnetEmaTaoFlow::::insert(n2, (now, i64f64(2000.0))); -// let shares = SubtensorModule::get_shares(&[n1, n2]); +// let shares = SubtensorModule::subnet_emission_shares(&[n1, n2]); // let s1 = shares.get(&n1).unwrap().to_num::(); // let s2 = shares.get(&n2).unwrap().to_num::(); @@ -475,7 +480,7 @@ fn seed_price_and_flow(n1: NetUid, n2: NetUid, price1: f64, price2: f64, flow1: // SubnetEmaTaoFlow::::insert(n1, (now, i64f64(flow1))); // SubnetEmaTaoFlow::::insert(n2, (now, i64f64(flow2))); -// let shares = SubtensorModule::get_shares(&[n1, n2]); +// let shares = SubtensorModule::subnet_emission_shares(&[n1, n2]); // let s1 = shares.get(&n1).unwrap().to_num::(); // let s2 = shares.get(&n2).unwrap().to_num::(); @@ -518,7 +523,7 @@ fn seed_price_and_flow(n1: NetUid, n2: NetUid, price1: f64, price2: f64, flow1: // SubnetEmaTaoFlow::::insert(n2, (now, i64f64(-300.0))); // SubnetEmaTaoFlow::::insert(n3, (now, i64f64(-400.0))); -// let shares = SubtensorModule::get_shares(&[n1, n2, n3]); +// let shares = SubtensorModule::subnet_emission_shares(&[n1, n2, n3]); // let s1 = shares.get(&n1).unwrap().to_num::(); // let s2 = shares.get(&n2).unwrap().to_num::(); // let s3 = shares.get(&n3).unwrap().to_num::(); diff --git a/pallets/subtensor/src/tests/subnet_info.rs b/pallets/subtensor/src/tests/subnet_info.rs index ef33dbc150..b8bd771802 100644 --- a/pallets/subtensor/src/tests/subnet_info.rs +++ b/pallets/subtensor/src/tests/subnet_info.rs @@ -1,3 +1,7 @@ +//! Tests for RPC subnet hyperparams V3 ([`crate::rpc_info::subnet_info`]). +//! +//! `EXPECTED_V3_NAMES` is the client contract: add a name here when adding a hyperparam. + #![allow(clippy::expect_used, clippy::unwrap_used)] use super::mock::*; @@ -50,7 +54,7 @@ const EXPECTED_V3_NAMES: &[&[u8]] = &[ b"collateral_drain_ratio", ]; -fn find<'a>(params: &'a [HyperparamEntry], name: &[u8]) -> &'a HyperparamValue { +fn find_hyperparam_value<'a>(params: &'a [HyperparamEntry], name: &[u8]) -> &'a HyperparamValue { ¶ms .iter() .find(|e| e.name == name) @@ -136,106 +140,118 @@ fn test_get_subnet_hyperparams_v3_values_reflect_storage() { // Bool variants assert_eq!( - find(p, b"registration_allowed"), + find_hyperparam_value(p, b"registration_allowed"), &HyperparamValue::Bool(false) ); assert_eq!( - find(p, b"commit_reveal_weights_enabled"), + find_hyperparam_value(p, b"commit_reveal_weights_enabled"), + &HyperparamValue::Bool(true) + ); + assert_eq!( + find_hyperparam_value(p, b"liquid_alpha_enabled"), &HyperparamValue::Bool(true) ); assert_eq!( - find(p, b"liquid_alpha_enabled"), + find_hyperparam_value(p, b"bonds_reset_enabled"), &HyperparamValue::Bool(true) ); assert_eq!( - find(p, b"bonds_reset_enabled"), + find_hyperparam_value(p, b"owner_cut_enabled"), &HyperparamValue::Bool(true) ); - assert_eq!(find(p, b"owner_cut_enabled"), &HyperparamValue::Bool(true)); assert_eq!( - find(p, b"owner_cut_auto_lock_enabled"), + find_hyperparam_value(p, b"owner_cut_auto_lock_enabled"), &HyperparamValue::Bool(true) ); // U16 variants - assert_eq!(find(p, b"kappa"), &HyperparamValue::U16(Compact(12))); assert_eq!( - find(p, b"immunity_period"), + find_hyperparam_value(p, b"kappa"), + &HyperparamValue::U16(Compact(12)) + ); + assert_eq!( + find_hyperparam_value(p, b"immunity_period"), &HyperparamValue::U16(Compact(13)) ); assert_eq!( - find(p, b"min_allowed_weights"), + find_hyperparam_value(p, b"min_allowed_weights"), &HyperparamValue::U16(Compact(14)) ); - assert_eq!(find(p, b"tempo"), &HyperparamValue::U16(Compact(16))); assert_eq!( - find(p, b"activity_cutoff"), + find_hyperparam_value(p, b"tempo"), + &HyperparamValue::U16(Compact(16)) + ); + assert_eq!( + find_hyperparam_value(p, b"activity_cutoff"), &HyperparamValue::U64(Compact(22)) ); assert_eq!( - find(p, b"activity_cutoff_factor"), + find_hyperparam_value(p, b"activity_cutoff_factor"), &HyperparamValue::U32(Compact(1375)) ); assert_eq!( - find(p, b"target_regs_per_interval"), + find_hyperparam_value(p, b"target_regs_per_interval"), &HyperparamValue::U16(Compact(24)) ); assert_eq!( - find(p, b"burn_half_life"), + find_hyperparam_value(p, b"burn_half_life"), &HyperparamValue::U16(Compact(33)) ); assert_eq!( - find(p, b"max_regs_per_block"), + find_hyperparam_value(p, b"max_regs_per_block"), &HyperparamValue::U16(Compact(28)) ); assert_eq!( - find(p, b"max_validators"), + find_hyperparam_value(p, b"max_validators"), &HyperparamValue::U16(Compact(30)) ); - assert_eq!(find(p, b"yuma_version"), &HyperparamValue::U16(Compact(3))); + assert_eq!( + find_hyperparam_value(p, b"yuma_version"), + &HyperparamValue::U16(Compact(3)) + ); // Effective min childkey take = max(global, per-subnet). assert_eq!( - find(p, b"min_childkey_take"), + find_hyperparam_value(p, b"min_childkey_take"), &HyperparamValue::U16(Compact(32)) ); // U64 variants assert_eq!( - find(p, b"weights_version"), + find_hyperparam_value(p, b"weights_version"), &HyperparamValue::U64(Compact(19)) ); assert_eq!( - find(p, b"weights_rate_limit"), + find_hyperparam_value(p, b"weights_rate_limit"), &HyperparamValue::U64(Compact(20)) ); assert_eq!( - find(p, b"bonds_moving_avg"), + find_hyperparam_value(p, b"bonds_moving_avg"), &HyperparamValue::U64(Compact(27)) ); assert_eq!( - find(p, b"serving_rate_limit"), + find_hyperparam_value(p, b"serving_rate_limit"), &HyperparamValue::U64(Compact(29)) ); // TaoBalance variants assert_eq!( - find(p, b"min_burn"), + find_hyperparam_value(p, b"min_burn"), &HyperparamValue::TaoBalance(Compact(TaoBalance::from(25u64))) ); assert_eq!( - find(p, b"max_burn"), + find_hyperparam_value(p, b"max_burn"), &HyperparamValue::TaoBalance(Compact(TaoBalance::from(26u64))) ); // I32F32 variant assert_eq!( - find(p, b"alpha_sigmoid_steepness"), + find_hyperparam_value(p, b"alpha_sigmoid_steepness"), &HyperparamValue::I32F32(I32F32::saturating_from_num(5)) ); // U64F64 variant assert_eq!( - find(p, b"burn_increase_mult"), + find_hyperparam_value(p, b"burn_increase_mult"), &HyperparamValue::U64F64(U64F64::saturating_from_num(2)) ); }); @@ -249,7 +265,7 @@ fn test_get_subnet_hyperparams_v3_yuma_version_reflects_flag() { SubtensorModule::set_yuma3_enabled(netuid, false); assert_eq!( - find( + find_hyperparam_value( &SubtensorModule::get_subnet_hyperparams_v3(netuid).unwrap(), b"yuma_version", ), @@ -258,7 +274,7 @@ fn test_get_subnet_hyperparams_v3_yuma_version_reflects_flag() { SubtensorModule::set_yuma3_enabled(netuid, true); assert_eq!( - find( + find_hyperparam_value( &SubtensorModule::get_subnet_hyperparams_v3(netuid).unwrap(), b"yuma_version", ), diff --git a/pallets/subtensor/src/tests/swap_coldkey.rs b/pallets/subtensor/src/tests/swap_coldkey.rs index 18edc2098e..1c99eb5b1c 100644 --- a/pallets/subtensor/src/tests/swap_coldkey.rs +++ b/pallets/subtensor/src/tests/swap_coldkey.rs @@ -1,3 +1,7 @@ +//! Tests for full coldkey swap ([`crate::swap::swap_coldkey`]). +//! +//! Covers ownership transfer, scheduled execution, stake/locks, and fee paths. + #![allow( unused, clippy::expect_used, @@ -755,7 +759,7 @@ fn test_do_swap_coldkey_preserves_new_coldkey_identity() { }; IdentitiesV2::::insert(new_coldkey, new_identity.clone()); - assert_ok!(SubtensorModule::do_swap_coldkey(&who, &new_coldkey,)); + assert_ok!(SubtensorModule::perform_coldkey_swap(&who, &new_coldkey,)); // Identity is preserved assert_eq!(IdentitiesV2::::get(who), Some(old_identity)); @@ -792,7 +796,7 @@ fn test_do_swap_coldkey_with_no_stake() { let old_coldkey = U256::from(1); let new_coldkey = U256::from(2); - assert_ok!(SubtensorModule::do_swap_coldkey(&old_coldkey, &new_coldkey)); + assert_ok!(SubtensorModule::perform_coldkey_swap(&old_coldkey, &new_coldkey)); assert_eq!( SubtensorModule::get_total_stake_for_coldkey(&old_coldkey), @@ -863,8 +867,8 @@ fn test_do_swap_coldkey_with_max_values() { netuid2, ); - assert_ok!(SubtensorModule::do_swap_coldkey(&old_coldkey, &new_coldkey,)); - assert_ok!(SubtensorModule::do_swap_coldkey( + assert_ok!(SubtensorModule::perform_coldkey_swap(&old_coldkey, &new_coldkey,)); + assert_ok!(SubtensorModule::perform_coldkey_swap( &old_coldkey2, &new_coldkey2, )); @@ -924,7 +928,7 @@ fn test_do_swap_coldkey_effect_on_delegated_stake() { let coldkey_stake_before = SubtensorModule::get_total_stake_for_coldkey(&old_coldkey); let delegator_stake_before = SubtensorModule::get_total_stake_for_coldkey(&delegator); - assert_ok!(SubtensorModule::do_swap_coldkey(&old_coldkey, &new_coldkey,)); + assert_ok!(SubtensorModule::perform_coldkey_swap(&old_coldkey, &new_coldkey,)); assert_abs_diff_eq!( SubtensorModule::get_total_stake_for_coldkey(&new_coldkey), @@ -1017,7 +1021,7 @@ fn test_swap_delegated_stake_for_coldkey() { let total_hotkey2_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey2); // Perform the swap - assert_ok!(SubtensorModule::do_swap_coldkey(&old_coldkey, &new_coldkey,)); + assert_ok!(SubtensorModule::perform_coldkey_swap(&old_coldkey, &new_coldkey,)); // Verify stake transfer assert_eq!( @@ -1344,7 +1348,7 @@ fn test_coldkey_swap_total() { SubtensorModule::get_total_stake_for_coldkey(&coldkey), ck_stake ); - assert_ok!(SubtensorModule::do_swap_coldkey(&coldkey, &new_coldkey,)); + assert_ok!(SubtensorModule::perform_coldkey_swap(&coldkey, &new_coldkey,)); assert_eq!( SubtensorModule::get_total_stake_for_coldkey(&new_coldkey), ck_stake @@ -1479,7 +1483,7 @@ fn test_do_swap_coldkey_effect_on_delegations() { )); // Perform the swap - assert_ok!(SubtensorModule::do_swap_coldkey(&coldkey, &new_coldkey,)); + assert_ok!(SubtensorModule::perform_coldkey_swap(&coldkey, &new_coldkey,)); // Verify stake was moved for the delegate let approx_total_stake = stake * 2.into() - (fee * 2).into(); @@ -2072,7 +2076,7 @@ fn test_do_swap_coldkey_migrates_miner_collateral() { }, ); - assert_ok!(SubtensorModule::do_swap_coldkey(&old_coldkey, &new_coldkey)); + assert_ok!(SubtensorModule::perform_coldkey_swap(&old_coldkey, &new_coldkey)); assert!( MinerCollateral::::get((netuid, hotkey, old_coldkey)).is_none(), @@ -2160,7 +2164,7 @@ fn test_do_swap_coldkey_migrates_zero_locked_min_collateral_floor() { "zero-locked floor must remain indexed" ); - assert_ok!(SubtensorModule::do_swap_coldkey(&old_coldkey, &new_coldkey)); + assert_ok!(SubtensorModule::perform_coldkey_swap(&old_coldkey, &new_coldkey)); assert!( MinerCollateral::::get((netuid, hotkey, old_coldkey)).is_none(), @@ -2184,7 +2188,7 @@ fn test_do_swap_coldkey_migrates_zero_locked_min_collateral_floor() { }); } -// Regression: a late failure inside do_swap_coldkey must roll back collateral +// Regression: a late failure inside perform_coldkey_swap must roll back collateral // migration together with the stake move (storage transaction). #[test] fn test_do_swap_coldkey_rolls_back_collateral_on_failure() { @@ -2243,7 +2247,7 @@ fn test_do_swap_coldkey_rolls_back_collateral_on_failure() { ); assert_noop!( - SubtensorModule::do_swap_coldkey(&old_coldkey, &new_coldkey), + SubtensorModule::perform_coldkey_swap(&old_coldkey, &new_coldkey), Error::::ActiveLockExists ); @@ -2307,7 +2311,7 @@ fn test_do_swap_coldkey_fails_closed_on_orphaned_miner_collateral() { assert!(ColdkeyCollateralHotkeys::::get(netuid, old_coldkey).is_empty()); assert_noop!( - SubtensorModule::do_swap_coldkey(&old_coldkey, &new_coldkey), + SubtensorModule::perform_coldkey_swap(&old_coldkey, &new_coldkey), Error::::ColdkeyCollateralIncomplete ); diff --git a/pallets/subtensor/src/tests/swap_hotkey.rs b/pallets/subtensor/src/tests/swap_hotkey.rs index 19ade1960f..dee67ce379 100644 --- a/pallets/subtensor/src/tests/swap_hotkey.rs +++ b/pallets/subtensor/src/tests/swap_hotkey.rs @@ -1,3 +1,7 @@ +//! Tests for full hotkey swap ([`crate::swap::swap_hotkey`]). +//! +//! For subnet-scoped swap, see [`crate::tests::swap_hotkey_with_subnet`]. + #![allow(unused, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used)] use approx::assert_abs_diff_eq; @@ -747,7 +751,7 @@ fn test_swap_hotkey_no_tx_rate_limit() { add_balance_to_coldkey_account(&coldkey, swap_cost + ExistentialDeposit::get()); // Perform the first swap - assert_ok!(SubtensorModule::do_swap_hotkey( + assert_ok!(SubtensorModule::perform_hotkey_swap( <::RuntimeOrigin>::signed(coldkey), &old_hotkey, &new_hotkey_1, @@ -759,7 +763,7 @@ fn test_swap_hotkey_no_tx_rate_limit() { // limit set above. Under the old rules the generic tx rate limit would reject this // second swap; with that limit removed it succeeds. step_block(interval as u16 + 1); - assert_ok!(SubtensorModule::do_swap_hotkey( + assert_ok!(SubtensorModule::perform_hotkey_swap( <::RuntimeOrigin>::signed(coldkey), &new_hotkey_1, &new_hotkey_2, @@ -788,7 +792,7 @@ fn test_do_swap_hotkey_err_not_owner() { // Attempt the swap with a non-owner coldkey assert_err!( - SubtensorModule::do_swap_hotkey( + SubtensorModule::perform_hotkey_swap( <::RuntimeOrigin>::signed(not_owner_coldkey), &old_hotkey, &new_hotkey, @@ -1106,7 +1110,7 @@ fn test_swap_hotkey_error_cases() { // Test not enough balance let swap_cost = SubtensorModule::get_key_swap_cost(); assert_err!( - SubtensorModule::do_swap_hotkey( + SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &old_hotkey, &new_hotkey, @@ -1121,7 +1125,7 @@ fn test_swap_hotkey_error_cases() { // Test new hotkey same as old assert_noop!( - SubtensorModule::do_swap_hotkey( + SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &old_hotkey, &old_hotkey, @@ -1134,7 +1138,7 @@ fn test_swap_hotkey_error_cases() { // Test new hotkey already registered IsNetworkMember::::insert(new_hotkey, NetUid::ROOT, true); assert_noop!( - SubtensorModule::do_swap_hotkey( + SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &old_hotkey, &new_hotkey, @@ -1147,7 +1151,7 @@ fn test_swap_hotkey_error_cases() { // Test non-associated coldkey assert_noop!( - SubtensorModule::do_swap_hotkey( + SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(wrong_coldkey), &old_hotkey, &new_hotkey, @@ -1158,7 +1162,7 @@ fn test_swap_hotkey_error_cases() { ); // Run the successful swap - assert_ok!(SubtensorModule::do_swap_hotkey( + assert_ok!(SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &old_hotkey, &new_hotkey, @@ -1207,7 +1211,7 @@ fn test_do_swap_hotkey_err_new_hotkey_not_clean_for_root() { // Full swap (netuid = None) — touches root, must fail. assert_noop!( - SubtensorModule::do_swap_hotkey( + SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &old_hotkey, &new_hotkey, @@ -1219,7 +1223,7 @@ fn test_do_swap_hotkey_err_new_hotkey_not_clean_for_root() { // Explicit root-subnet swap — also must fail. assert_noop!( - SubtensorModule::do_swap_hotkey( + SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &old_hotkey, &new_hotkey, @@ -1566,7 +1570,7 @@ fn test_swap_hotkey_swap_rate_limits() { SubtensorModule::set_last_tx_block_childkey(&old_hotkey, child_key_take_block); // Perform the swap - assert_ok!(SubtensorModule::do_swap_hotkey( + assert_ok!(SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &old_hotkey, &new_hotkey, @@ -1711,7 +1715,7 @@ fn ghsa_2026_011_subnet_swap_interval_bypassed_by_all_subnets_path() { // 1. Per-subnet swap old_hotkey -> hk_a on subnet N. This stamps // LastHotkeySwapOnNetuid(N, coldkey) = current block, opening the cooldown. - assert_ok!(SubtensorModule::do_swap_hotkey( + assert_ok!(SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &old_hotkey, &hk_a, @@ -1727,7 +1731,7 @@ fn ghsa_2026_011_subnet_swap_interval_bypassed_by_all_subnets_path() { // 2. CONTRAST (the rate limit works on the per-subnet path): // Immediately re-swapping on the SAME subnet within the interval fails. assert_err!( - SubtensorModule::do_swap_hotkey( + SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &hk_a, &hk_contrast, @@ -1750,7 +1754,7 @@ fn ghsa_2026_011_subnet_swap_interval_bypassed_by_all_subnets_path() { // immediate swap via netuid=None within the cooldown is rejected with the same // error and CANNOT bypass the per-subnet cooldown. assert_err!( - SubtensorModule::do_swap_hotkey( + SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &hk_a, &hk_b, @@ -1772,7 +1776,7 @@ fn ghsa_2026_011_subnet_swap_interval_bypassed_by_all_subnets_path() { step_block((interval + 1) as u16); let block_after = SubtensorModule::get_current_block_as_u64(); assert!(block_after > block.saturating_add(interval)); - assert_ok!(SubtensorModule::do_swap_hotkey( + assert_ok!(SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &hk_a, &hk_b, @@ -1840,7 +1844,7 @@ fn ghsa_2026_011_all_subnets_swap_covers_parent_key_subnets_not_child_side() { assert_eq!(LastHotkeySwapOnNetuid::::get(child_netuid, coldkey), 0); // All-subnets swap (netuid = None). - assert_ok!(SubtensorModule::do_swap_hotkey( + assert_ok!(SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(coldkey), &old_hotkey, &new_hotkey, @@ -2002,7 +2006,7 @@ fn hotkey_swap_has_no_hard_position_cap() { ); } - assert_ok!(SubtensorModule::do_swap_hotkey( + assert_ok!(SubtensorModule::perform_hotkey_swap( RuntimeOrigin::signed(owner), &old_hotkey, &new_hotkey, diff --git a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs deleted file mode 100644 index 450cc252c6..0000000000 --- a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs +++ /dev/null @@ -1,3251 +0,0 @@ -#![allow(unused, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used)] - -use approx::assert_abs_diff_eq; -use codec::Encode; -use frame_support::weights::Weight; -use frame_support::{assert_err, assert_noop, assert_ok}; -use frame_system::{Config, RawOrigin}; -use subtensor_runtime_common::{AlphaBalance, NetUidStorageIndex, TaoBalance, Token}; - -use super::mock::*; -use crate::*; -use share_pool::SafeFloat; -use sp_core::{Get, H160, H256, U256}; -use sp_runtime::{PerU16, SaturatedConversion}; -use std::collections::BTreeSet; -use substrate_fixed::types::{I96F32, U64F64}; - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_owner --exact --nocapture -#[test] -fn test_swap_owner() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - Owner::::insert(old_hotkey, coldkey); - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false, - )); - - assert_eq!(Owner::::get(old_hotkey), coldkey); - assert_eq!(Owner::::get(new_hotkey), coldkey); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_owned_hotkeys --exact --nocapture -#[test] -fn test_swap_owned_hotkeys() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - OwnedHotkeys::::insert(coldkey, vec![old_hotkey]); - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - )); - - let hotkeys = OwnedHotkeys::::get(coldkey); - assert!(hotkeys.contains(&old_hotkey)); - assert!(hotkeys.contains(&new_hotkey)); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_total_hotkey_stake --exact --nocapture -#[test] -fn test_swap_total_hotkey_stake() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - let amount = DefaultMinStake::::get().to_u64() * 10; - - let fee = (amount as f64 * 0.003) as u64; - - //add network - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - remove_owner_registration_stake(netuid); - - // Give it some $$$ in his coldkey balance - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - // Add stake - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey), - old_hotkey, - netuid, - amount.into() - )); - - // Check if stake has increased - assert_abs_diff_eq!( - SubtensorModule::get_total_stake_for_hotkey(&old_hotkey), - (amount - fee).into(), - epsilon = TaoBalance::from(amount / 100), - ); - assert_abs_diff_eq!( - SubtensorModule::get_total_stake_for_hotkey(&new_hotkey), - TaoBalance::ZERO, - epsilon = 1.into(), - ); - - // Swap hotkey - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - )); - - // Verify that total hotkey stake swapped - assert_abs_diff_eq!( - SubtensorModule::get_total_stake_for_hotkey(&old_hotkey), - TaoBalance::ZERO, - epsilon = 1.into(), - ); - assert_abs_diff_eq!( - SubtensorModule::get_total_stake_for_hotkey(&new_hotkey), - TaoBalance::from(amount - fee), - epsilon = TaoBalance::from(amount / 100), - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_delegates --exact --nocapture -#[test] -fn test_swap_delegates() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - Delegates::::insert(old_hotkey, PerU16::from_parts(100)); - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - )); - - assert!(Delegates::::contains_key(old_hotkey)); - assert!(!Delegates::::contains_key(new_hotkey)); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_subnet_membership --exact --nocapture -#[test] -fn test_swap_subnet_membership() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - IsNetworkMember::::insert(old_hotkey, netuid, true); - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - )); - - assert!(!IsNetworkMember::::contains_key(old_hotkey, netuid)); - assert!(IsNetworkMember::::get(new_hotkey, netuid)); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_uids_and_keys --exact --nocapture -#[test] -fn test_swap_uids_and_keys() { - new_test_ext(1).execute_with(|| { - let uid = 5u16; - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - IsNetworkMember::::insert(old_hotkey, netuid, true); - Uids::::insert(netuid, old_hotkey, uid); - Keys::::insert(netuid, uid, old_hotkey); - - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - )); - - assert_eq!(Uids::::get(netuid, old_hotkey), None); - assert_eq!(Uids::::get(netuid, new_hotkey), Some(uid)); - assert_eq!(Keys::::get(netuid, uid), new_hotkey); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_prometheus --exact --nocapture -#[test] -fn test_swap_prometheus() { - new_test_ext(1).execute_with(|| { - let uid = 5u16; - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - - let prometheus_info = PrometheusInfo::default(); - - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - IsNetworkMember::::insert(old_hotkey, netuid, true); - Prometheus::::insert(netuid, old_hotkey, prometheus_info.clone()); - - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - )); - - assert!(!Prometheus::::contains_key(netuid, old_hotkey)); - assert_eq!( - Prometheus::::get(netuid, new_hotkey), - Some(prometheus_info) - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_axons --exact --nocapture -#[test] -fn test_swap_axons() { - new_test_ext(1).execute_with(|| { - let uid = 5u16; - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - - let axon_info = AxonInfo::default(); - - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - IsNetworkMember::::insert(old_hotkey, netuid, true); - Axons::::insert(netuid, old_hotkey, axon_info.clone()); - - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - )); - - assert!(!Axons::::contains_key(netuid, old_hotkey)); - assert_eq!(Axons::::get(netuid, new_hotkey), Some(axon_info)); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_certificates --exact --nocapture -#[test] -fn test_swap_certificates() { - new_test_ext(1).execute_with(|| { - let uid = 5u16; - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - - let certificate = NeuronCertificate::try_from(vec![1, 2, 3]).unwrap(); - - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - IsNetworkMember::::insert(old_hotkey, netuid, true); - NeuronCertificates::::insert(netuid, old_hotkey, certificate.clone()); - - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - )); - - assert!(!NeuronCertificates::::contains_key( - netuid, old_hotkey - )); - assert_eq!( - NeuronCertificates::::get(netuid, new_hotkey), - Some(certificate) - ); - }); -} -use sp_std::collections::vec_deque::VecDeque; -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_weight_commits --exact --nocapture -#[test] -fn test_swap_weight_commits() { - new_test_ext(1).execute_with(|| { - let uid = 5u16; - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - - let mut weight_commits: VecDeque<(H256, u64, u64, u64)> = VecDeque::new(); - weight_commits.push_back((H256::from_low_u64_be(100), 200, 1, 1)); - - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - IsNetworkMember::::insert(old_hotkey, netuid, true); - WeightCommits::::insert( - NetUidStorageIndex::from(netuid), - old_hotkey, - weight_commits.clone(), - ); - - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - )); - - assert!(!WeightCommits::::contains_key( - NetUidStorageIndex::from(netuid), - old_hotkey - )); - assert_eq!( - WeightCommits::::get(NetUidStorageIndex::from(netuid), new_hotkey), - Some(weight_commits) - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_loaded_emission --exact --nocapture -#[test] -fn test_swap_loaded_emission() { - new_test_ext(1).execute_with(|| { - let uid = 5u16; - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - - let server_emission = 1000u64; - let validator_emission = 1000u64; - - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - IsNetworkMember::::insert(old_hotkey, netuid, true); - LoadedEmission::::insert( - netuid, - vec![(old_hotkey, server_emission, validator_emission)], - ); - - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - )); - - let new_loaded_emission = LoadedEmission::::get(netuid); - assert_eq!( - new_loaded_emission, - Some(vec![(new_hotkey, server_emission, validator_emission)]) - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_staking_hotkeys --exact --nocapture -#[test] -fn test_swap_staking_hotkeys() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - StakingHotkeys::::insert(coldkey, vec![old_hotkey]); - Alpha::::insert((old_hotkey, coldkey, netuid), U64F64::from_num(100)); - - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - )); - - let staking_hotkeys = StakingHotkeys::::get(coldkey); - assert!(staking_hotkeys.contains(&old_hotkey)); - assert!(staking_hotkeys.contains(&new_hotkey)); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey::test_swap_hotkey_with_multiple_coldkeys --exact --show-output --nocapture -#[test] -fn test_swap_hotkey_with_multiple_coldkeys() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey1 = U256::from(3); - let coldkey2 = U256::from(4); - - let stake = 1_000_000_000; - - StakingHotkeys::::insert(coldkey1, vec![old_hotkey]); - StakingHotkeys::::insert(coldkey2, vec![old_hotkey]); - SubtensorModule::create_account_if_non_existent(&coldkey1, &old_hotkey); - add_balance_to_coldkey_account(&coldkey1, 1_000_000_000_000_u64.into()); - add_balance_to_coldkey_account(&coldkey2, 1_000_000_000_000_u64.into()); - - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey1), - old_hotkey, - netuid, - stake.into() - )); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey2), - old_hotkey, - netuid, - TaoBalance::from(stake / 2) - )); - let stake1_before = SubtensorModule::get_total_stake_for_coldkey(&coldkey1); - let stake2_before = SubtensorModule::get_total_stake_for_coldkey(&coldkey2); - - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey1), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - )); - - assert_eq!( - SubtensorModule::get_total_stake_for_coldkey(&coldkey1), - SubtensorModule::get_total_stake_for_coldkey(&coldkey1), - ); - assert_eq!( - SubtensorModule::get_total_stake_for_coldkey(&coldkey2), - SubtensorModule::get_total_stake_for_coldkey(&coldkey2), - ); - - assert_eq!( - SubtensorModule::get_total_stake_for_coldkey(&coldkey1), - stake1_before - ); - assert_eq!( - SubtensorModule::get_total_stake_for_coldkey(&coldkey2), - stake2_before - ); - - assert!(StakingHotkeys::::get(coldkey1).contains(&new_hotkey)); - assert!(StakingHotkeys::::get(coldkey2).contains(&new_hotkey)); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_hotkey_with_multiple_subnets --exact --nocapture -#[test] -fn test_swap_hotkey_with_multiple_subnets() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let new_hotkey_2 = U256::from(3); - let coldkey = U256::from(4); - - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - let netuid1 = add_dynamic_network(&old_hotkey, &coldkey); - let netuid2 = add_dynamic_network(&old_hotkey, &coldkey); - - IsNetworkMember::::insert(old_hotkey, netuid1, true); - IsNetworkMember::::insert(old_hotkey, netuid2, true); - - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid1), - false - )); - - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey_2, - Some(netuid2), - false - )); - - assert!(IsNetworkMember::::get(new_hotkey, netuid1)); - assert!(IsNetworkMember::::get(new_hotkey_2, netuid2)); - assert!(!IsNetworkMember::::get(old_hotkey, netuid1)); - assert!(!IsNetworkMember::::get(old_hotkey, netuid2)); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_staking_hotkeys_multiple_coldkeys --exact --nocapture -#[test] -fn test_swap_staking_hotkeys_multiple_coldkeys() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey1 = U256::from(3); - let coldkey2 = U256::from(4); - let staker5 = U256::from(5); - - let stake = 1_000_000_000; - add_balance_to_coldkey_account(&coldkey1, 1_000_000_000_000_u64.into()); - add_balance_to_coldkey_account(&coldkey2, 1_000_000_000_000_u64.into()); - - // Set up initial state - StakingHotkeys::::insert(coldkey1, vec![old_hotkey]); - StakingHotkeys::::insert(coldkey2, vec![old_hotkey, staker5]); - - SubtensorModule::create_account_if_non_existent(&coldkey1, &old_hotkey); - - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey1), - old_hotkey, - netuid, - stake.into() - )); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey2), - old_hotkey, - netuid, - stake.into() - )); - - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey1), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - )); - - // Check if new_hotkey replaced old_hotkey in StakingHotkeys - assert!(StakingHotkeys::::get(coldkey1).contains(&new_hotkey)); - assert!(StakingHotkeys::::get(coldkey1).contains(&old_hotkey)); - - // Check if new_hotkey replaced old_hotkey for coldkey2 as well - assert!(StakingHotkeys::::get(coldkey2).contains(&new_hotkey)); - assert!(StakingHotkeys::::get(coldkey2).contains(&old_hotkey)); - assert!(StakingHotkeys::::get(coldkey2).contains(&staker5)); - // Other hotkeys should remain - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_hotkey_with_no_stake --exact --nocapture -#[test] -fn test_swap_hotkey_with_no_stake() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - // Set up initial state with no stake - Owner::::insert(old_hotkey, coldkey); - - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - )); - - // Check if ownership transferred - assert!(Owner::::contains_key(old_hotkey)); - assert_eq!(Owner::::get(new_hotkey), coldkey); - - // Ensure no unexpected changes in Stake - assert!(!Alpha::::contains_key((old_hotkey, coldkey, netuid))); - assert!(!Alpha::::contains_key((new_hotkey, coldkey, netuid))); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey::test_swap_hotkey_with_multiple_coldkeys_and_subnets --exact --show-output -#[test] -fn test_swap_hotkey_with_multiple_coldkeys_and_subnets() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let new_hotkey_2 = U256::from(3); - let coldkey1 = U256::from(4); - let coldkey2 = U256::from(5); - let netuid1 = NetUid::from(1); - let netuid2 = NetUid::from(2); - let stake = DefaultMinStake::::get().to_u64() * 10; - - // Set up initial state - add_network(netuid1, 1, 1); - add_network(netuid2, 1, 1); - register_ok_neuron(netuid1, old_hotkey, coldkey1, 1234); - register_ok_neuron(netuid2, old_hotkey, coldkey1, 1234); - - // Add balance to both coldkeys - add_balance_to_coldkey_account(&coldkey1, 1_000_000_000_000_u64.into()); - add_balance_to_coldkey_account(&coldkey2, 1_000_000_000_000_u64.into()); - - // Stake with coldkey1 - assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(coldkey1), - old_hotkey, - netuid1, - stake.into() - )); - - // Stake with coldkey2 also - assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(coldkey2), - old_hotkey, - netuid2, - stake.into() - )); - - let ck1_stake = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &old_hotkey, - &coldkey1, - netuid1, - ); - let ck2_stake = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &old_hotkey, - &coldkey2, - netuid2, - ); - assert!(!ck1_stake.is_zero()); - assert!(!ck2_stake.is_zero()); - let total_hk_stake = SubtensorModule::get_total_stake_for_hotkey(&old_hotkey); - assert!(!total_hk_stake.is_zero()); - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey1), - &old_hotkey, - &new_hotkey, - Some(netuid1), - false - )); - - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey1), - &old_hotkey, - &new_hotkey_2, - Some(netuid2), - false - )); - - // Check ownership transfer - assert_eq!( - SubtensorModule::get_owning_coldkey_for_hotkey(&new_hotkey), - coldkey1 - ); - assert!(!SubtensorModule::get_owned_hotkeys(&coldkey2).contains(&new_hotkey)); - assert_eq!( - SubtensorModule::get_owning_coldkey_for_hotkey(&new_hotkey_2), - coldkey1 - ); - assert!(!SubtensorModule::get_owned_hotkeys(&coldkey2).contains(&new_hotkey_2)); - - // Check stake transfer - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &new_hotkey, - &coldkey1, - netuid1 - ), - ck1_stake - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &new_hotkey_2, - &coldkey2, - netuid2 - ), - ck2_stake - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &old_hotkey, - &coldkey1, - netuid1 - ), - AlphaBalance::ZERO - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &old_hotkey, - &coldkey2, - netuid2 - ), - AlphaBalance::ZERO - ); - - // Check subnet membership transfer - assert!(SubtensorModule::is_hotkey_registered_on_network( - netuid1, - &new_hotkey - )); - assert!(SubtensorModule::is_hotkey_registered_on_network( - netuid2, - &new_hotkey_2 - )); - assert!(!SubtensorModule::is_hotkey_registered_on_network( - netuid1, - &old_hotkey - )); - assert!(!SubtensorModule::is_hotkey_registered_on_network( - netuid2, - &old_hotkey - )); - - // Check total stake transfer - assert_eq!( - SubtensorModule::get_total_stake_for_hotkey(&new_hotkey) - + SubtensorModule::get_total_stake_for_hotkey(&new_hotkey_2), - total_hk_stake - ); - assert_eq!( - SubtensorModule::get_total_stake_for_hotkey(&old_hotkey), - TaoBalance::ZERO - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_do_swap_hotkey_err_not_owner --exact --nocapture -#[test] -fn test_do_swap_hotkey_err_not_owner() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let tempo: u16 = 13; - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - let not_owner_coldkey = U256::from(4); - let swap_cost = TaoBalance::from(1_000_000_000u64); - - // Setup initial state - add_network(netuid, tempo, 0); - register_ok_neuron(netuid, old_hotkey, coldkey, 0); - add_balance_to_coldkey_account(¬_owner_coldkey, swap_cost); - - // Attempt the swap with a non-owner coldkey - assert_err!( - SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(not_owner_coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - ), - Error::::NonAssociatedColdKey - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_owner_old_hotkey_not_exist --exact --nocapture -#[test] -fn test_swap_owner_old_hotkey_not_exist() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - - let netuid = add_dynamic_network(&new_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - // Ensure old_hotkey does not exist - assert!(!Owner::::contains_key(old_hotkey)); - - // Perform the swap - assert_err!( - SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - ), - Error::::NonAssociatedColdKey - ); - - // Verify the swap - assert_eq!(Owner::::get(new_hotkey), coldkey); - assert!(!Owner::::contains_key(old_hotkey)); - }); -} - -// SKIP_WASM_BUILD=1 cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_owner_new_hotkey_owned_by_another_coldkey --exact --nocapture -#[test] -fn test_swap_owner_new_hotkey_owned_by_another_coldkey() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - let another_coldkey = U256::from(4); - - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - // new_hotkey already exists globally and is owned by a foreign coldkey, - // so the new-hotkey ownership check must reject the swap. - Owner::::insert(new_hotkey, another_coldkey); - - // Perform the swap - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_err!( - SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - ), - Error::::NonAssociatedColdKey - ); - - // Verify the swap - assert_eq!(Owner::::get(old_hotkey), coldkey); - assert!(Owner::::contains_key(old_hotkey)); - }); -} - -// SKIP_WASM_BUILD=1 cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_owner_new_hotkey_already_exists --exact --nocapture -#[test] -fn test_swap_owner_new_hotkey_already_exists() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - - let netuid = add_dynamic_network(&new_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - // old_hotkey is owned by coldkey; new_hotkey was already registered on `netuid` - // by add_dynamic_network (the condition under test). Do NOT reassign new_hotkey to - // a foreign coldkey — the new_hotkey-ownership check (NonAssociatedColdKey) would - // then fire before the already-registered-in-subnet check this test targets. - Owner::::insert(old_hotkey, coldkey); - - // Perform the swap - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_err!( - SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - ), - Error::::HotKeyAlreadyRegisteredInSubNet - ); - - // Verify the swap - assert_eq!(Owner::::get(old_hotkey), coldkey); - assert!(Owner::::contains_key(old_hotkey)); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_stake_success --exact --nocapture -#[test] -fn test_swap_stake_success() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - remove_owner_registration_stake(netuid); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - let amount = 10_000; - let shares = U64F64::from_num(10_000); - - // Initialize staking variables for old_hotkey - TotalHotkeyAlpha::::insert(old_hotkey, netuid, AlphaBalance::from(amount)); - TotalHotkeyAlphaLastEpoch::::insert( - old_hotkey, - netuid, - AlphaBalance::from(amount * 2), - ); - TotalHotkeyShares::::insert(old_hotkey, netuid, U64F64::from_num(shares)); - Alpha::::insert((old_hotkey, coldkey, netuid), U64F64::from_num(amount)); - AlphaDividendsPerSubnet::::insert(netuid, old_hotkey, AlphaBalance::from(amount)); - - // Perform the swap - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - ),); - - // Verify the swap - assert_eq!( - TotalHotkeyAlpha::::get(old_hotkey, netuid), - AlphaBalance::ZERO - ); - assert_eq!( - TotalHotkeyAlpha::::get(new_hotkey, netuid), - AlphaBalance::from(amount) - ); - assert_eq!( - TotalHotkeyAlphaLastEpoch::::get(old_hotkey, netuid), - AlphaBalance::ZERO - ); - assert_eq!( - TotalHotkeyAlphaLastEpoch::::get(new_hotkey, netuid), - AlphaBalance::from(amount * 2) - ); - assert_eq!( - TotalHotkeyShares::::get(old_hotkey, netuid), - U64F64::from_num(0) - ); - assert_eq!( - TotalHotkeyShares::::get(new_hotkey, netuid), - U64F64::from_num(0) - ); - assert_abs_diff_eq!( - f64::from(TotalHotkeySharesV2::::get(new_hotkey, netuid)), - shares.to_num::(), - epsilon = 0.0000000001 - ); - assert_eq!( - Alpha::::get((old_hotkey, coldkey, netuid)), - U64F64::from_num(0) - ); - assert_eq!( - Alpha::::get((new_hotkey, coldkey, netuid)), - U64F64::from_num(0) - ); - assert_eq!( - f64::from(AlphaV2::::get((new_hotkey, coldkey, netuid))), - amount as f64 - ); - assert_eq!( - AlphaDividendsPerSubnet::::get(netuid, old_hotkey), - AlphaBalance::ZERO - ); - assert_eq!( - AlphaDividendsPerSubnet::::get(netuid, new_hotkey), - AlphaBalance::from(amount) - ); - }); -} - -#[test] -fn test_swap_stake_v2_success() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - let amount = 10_000; - let shares = U64F64::from_num(10_000); - - // Initialize staking variables for old_hotkey - TotalHotkeyAlpha::::insert(old_hotkey, netuid, AlphaBalance::from(amount)); - TotalHotkeyAlphaLastEpoch::::insert( - old_hotkey, - netuid, - AlphaBalance::from(amount * 2), - ); - TotalHotkeySharesV2::::insert(old_hotkey, netuid, SafeFloat::from(shares)); - AlphaV2::::insert( - (old_hotkey, coldkey, netuid), - SafeFloat::from(U64F64::from_num(amount)), - ); - AlphaDividendsPerSubnet::::insert(netuid, old_hotkey, AlphaBalance::from(amount)); - - // Perform the swap - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false, - ),); - - // Verify the swap - assert_eq!( - TotalHotkeyAlpha::::get(old_hotkey, netuid), - AlphaBalance::ZERO - ); - assert_eq!( - TotalHotkeyAlpha::::get(new_hotkey, netuid), - AlphaBalance::from(amount) - ); - assert_eq!( - TotalHotkeyAlphaLastEpoch::::get(old_hotkey, netuid), - AlphaBalance::ZERO - ); - assert_eq!( - TotalHotkeyAlphaLastEpoch::::get(new_hotkey, netuid), - AlphaBalance::from(amount * 2) - ); - assert_eq!( - f64::from(TotalHotkeySharesV2::::get(old_hotkey, netuid)), - 0_f64 - ); - assert_abs_diff_eq!( - f64::from(TotalHotkeySharesV2::::get(new_hotkey, netuid)), - shares.to_num::(), - epsilon = 0.0000000001 - ); - assert_eq!( - f64::from(AlphaV2::::get((old_hotkey, coldkey, netuid))), - 0_f64 - ); - assert_eq!( - f64::from(AlphaV2::::get((new_hotkey, coldkey, netuid))), - amount as f64 - ); - assert_eq!( - AlphaDividendsPerSubnet::::get(netuid, old_hotkey), - AlphaBalance::ZERO - ); - assert_eq!( - AlphaDividendsPerSubnet::::get(netuid, new_hotkey), - AlphaBalance::from(amount) - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_hotkey_error_cases --exact --nocapture -#[test] -fn test_swap_hotkey_error_cases() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - let wrong_coldkey = U256::from(4); - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - - // Set up initial state - Owner::::insert(old_hotkey, coldkey); - TotalNetworks::::put(1); - SubtensorModule::set_last_tx_block(&coldkey, 0); - - // Test not enough balance - let swap_cost = SubtensorModule::get_key_swap_cost(); - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_err!( - SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - ), - Error::::NotEnoughBalanceToPaySwapHotKey - ); - - let initial_balance = SubtensorModule::get_key_swap_cost() + 1000.into(); - add_balance_to_coldkey_account(&coldkey, initial_balance); - - // Test new hotkey same as old - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_noop!( - SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &old_hotkey, - Some(netuid), - false - ), - Error::::NewHotKeyIsSameWithOld - ); - - // Test new hotkey already registered - IsNetworkMember::::insert(new_hotkey, netuid, true); - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_noop!( - SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - ), - Error::::HotKeyAlreadyRegisteredInSubNet - ); - IsNetworkMember::::remove(new_hotkey, netuid); - - // Test non-associated coldkey - assert_noop!( - SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(wrong_coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - ), - Error::::NonAssociatedColdKey - ); - - // Run the successful swap - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - ),); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_child_keys --exact --nocapture -#[test] -fn test_swap_child_keys() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - let children = vec![(100u64, U256::from(4)), (200u64, U256::from(5))]; - - // Initialize ChildKeys for old_hotkey - ChildKeys::::insert(old_hotkey, netuid, children.clone()); - - // Perform the swap - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - ),); - - // Verify the swap - assert_eq!(ChildKeys::::get(new_hotkey, netuid), children); - assert!(ChildKeys::::get(old_hotkey, netuid).is_empty()); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_child_keys_self_loop --exact --show-output -#[test] -#[allow(deprecated)] -fn test_swap_child_keys_self_loop() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - let amount = AlphaBalance::from(12345); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - // Only for checking - TotalHotkeyAlpha::::insert(old_hotkey, netuid, AlphaBalance::from(amount)); - - let children = vec![(200u64, new_hotkey)]; - - // Initialize ChildKeys for old_hotkey - ChildKeys::::insert(old_hotkey, netuid, children.clone()); - - // Perform the swap extrinsic - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_err!( - SubtensorModule::swap_hotkey( - RuntimeOrigin::signed(coldkey), - old_hotkey, - new_hotkey, - Some(netuid), - ), - Error::::InvalidChild - ); - - // Verify the swap didn't happen - assert_eq!(ChildKeys::::get(old_hotkey, netuid), children); - assert!(ChildKeys::::get(new_hotkey, netuid).is_empty()); - assert_eq!(TotalHotkeyAlpha::::get(old_hotkey, netuid), amount); - assert_eq!( - TotalHotkeyAlpha::::get(new_hotkey, netuid), - AlphaBalance::from(0) - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_parent_keys --exact --nocapture -#[test] -fn test_swap_parent_keys() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - let parents = vec![(100u64, U256::from(4)), (200u64, U256::from(5))]; - - // Initialize ParentKeys for old_hotkey - ParentKeys::::insert(old_hotkey, netuid, parents.clone()); - - // Initialize ChildKeys for parent - ChildKeys::::insert(U256::from(4), netuid, vec![(100u64, old_hotkey)]); - ChildKeys::::insert(U256::from(5), netuid, vec![(200u64, old_hotkey)]); - - // Perform the swap - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - ),); - - // Verify ParentKeys swap - assert_eq!(ParentKeys::::get(new_hotkey, netuid), parents); - assert!(ParentKeys::::get(old_hotkey, netuid).is_empty()); - - // Verify ChildKeys update for parents - assert_eq!( - ChildKeys::::get(U256::from(4), netuid), - vec![(100u64, new_hotkey)] - ); - assert_eq!( - ChildKeys::::get(U256::from(5), netuid), - vec![(200u64, new_hotkey)] - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_multiple_subnets --exact --nocapture -#[test] -fn test_swap_multiple_subnets() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let new_hotkey_2 = U256::from(3); - let coldkey = U256::from(4); - let netuid1 = add_dynamic_network(&old_hotkey, &coldkey); - let netuid2 = add_dynamic_network(&old_hotkey, &coldkey); - - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - let children1 = vec![(100u64, U256::from(4)), (200u64, U256::from(5))]; - let children2 = vec![(300u64, U256::from(6))]; - - // Initialize ChildKeys for old_hotkey in multiple subnets - ChildKeys::::insert(old_hotkey, netuid1, children1.clone()); - ChildKeys::::insert(old_hotkey, netuid2, children2.clone()); - - // Perform the swap - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid1), - false - ),); - - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey_2, - Some(netuid2), - false - ),); - - // Verify the swap for both subnets - assert_eq!(ChildKeys::::get(new_hotkey, netuid1), children1); - assert_eq!(ChildKeys::::get(new_hotkey_2, netuid2), children2); - assert!(ChildKeys::::get(old_hotkey, netuid1).is_empty()); - assert!(ChildKeys::::get(old_hotkey, netuid2).is_empty()); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_complex_parent_child_structure --exact --nocapture -#[test] -fn test_swap_complex_parent_child_structure() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - let parent1 = U256::from(4); - let parent2 = U256::from(5); - let child1 = U256::from(6); - let child2 = U256::from(7); - - // Set up complex parent-child structure - ParentKeys::::insert( - old_hotkey, - netuid, - vec![(100u64, parent1), (200u64, parent2)], - ); - ChildKeys::::insert(old_hotkey, netuid, vec![(300u64, child1), (400u64, child2)]); - ChildKeys::::insert( - parent1, - netuid, - vec![(100u64, old_hotkey), (500u64, U256::from(8))], - ); - ChildKeys::::insert( - parent2, - netuid, - vec![(200u64, old_hotkey), (600u64, U256::from(9))], - ); - - // Perform the swap - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - ),); - - // Verify ParentKeys swap - assert_eq!( - ParentKeys::::get(new_hotkey, netuid), - vec![(100u64, parent1), (200u64, parent2)] - ); - assert!(ParentKeys::::get(old_hotkey, netuid).is_empty()); - - // Verify ChildKeys swap - assert_eq!( - ChildKeys::::get(new_hotkey, netuid), - vec![(300u64, child1), (400u64, child2)] - ); - assert!(ChildKeys::::get(old_hotkey, netuid).is_empty()); - - // Verify parent's ChildKeys update - assert!(ChildKeys::::get(parent1, netuid).contains(&(500u64, U256::from(8))),); - assert!(ChildKeys::::get(parent1, netuid).contains(&(100u64, new_hotkey)),); - assert!(ChildKeys::::get(parent2, netuid).contains(&(600u64, U256::from(9))),); - assert!(ChildKeys::::get(parent2, netuid).contains(&(200u64, new_hotkey)),); - }); -} - -#[test] -fn test_swap_parent_hotkey_childkey_maps() { - new_test_ext(1).execute_with(|| { - let parent_old = U256::from(1); - let coldkey = U256::from(2); - let child = U256::from(3); - let child_other = U256::from(4); - let parent_new = U256::from(5); - - let netuid = add_dynamic_network(&parent_old, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - SubtensorModule::create_account_if_non_existent(&coldkey, &parent_old); - - // Set child and verify state maps - mock_set_children(&coldkey, &parent_old, netuid, &[(u64::MAX, child)]); - // Wait rate limit - step_rate_limit(&TransactionType::SetChildren, netuid); - // Schedule some pending child keys. - mock_schedule_children(&coldkey, &parent_old, netuid, &[(u64::MAX, child_other)]); - - assert_eq!( - ParentKeys::::get(child, netuid), - vec![(u64::MAX, parent_old)] - ); - assert_eq!( - ChildKeys::::get(parent_old, netuid), - vec![(u64::MAX, child)] - ); - let existing_pending_child_keys = PendingChildKeys::::get(netuid, parent_old); - assert_eq!(existing_pending_child_keys.0, vec![(u64::MAX, child_other)]); - - // Swap - - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &parent_old, - &parent_new, - Some(netuid), - false - ),); - - // Verify parent and child keys updates - assert_eq!( - ParentKeys::::get(child, netuid), - vec![(u64::MAX, parent_new)] - ); - assert_eq!( - ChildKeys::::get(parent_new, netuid), - vec![(u64::MAX, child)] - ); - assert_eq!( - PendingChildKeys::::get(netuid, parent_new), - existing_pending_child_keys // Entry under new hotkey. - ); - }) -} - -#[test] -fn test_swap_child_hotkey_childkey_maps() { - new_test_ext(1).execute_with(|| { - let parent = U256::from(1); - let coldkey = U256::from(2); - let child_old = U256::from(3); - let child_new = U256::from(4); - let netuid = add_dynamic_network(&child_old, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - SubtensorModule::create_account_if_non_existent(&coldkey, &child_old); - SubtensorModule::create_account_if_non_existent(&coldkey, &parent); - - // Set child and verify state maps - mock_set_children(&coldkey, &parent, netuid, &[(u64::MAX, child_old)]); - // Wait rate limit - step_rate_limit(&TransactionType::SetChildren, netuid); - // Schedule some pending child keys. - mock_schedule_children(&coldkey, &parent, netuid, &[(u64::MAX, child_old)]); - - assert_eq!( - ParentKeys::::get(child_old, netuid), - vec![(u64::MAX, parent)] - ); - assert_eq!( - ChildKeys::::get(parent, netuid), - vec![(u64::MAX, child_old)] - ); - let existing_pending_child_keys = PendingChildKeys::::get(netuid, parent); - assert_eq!(existing_pending_child_keys.0, vec![(u64::MAX, child_old)]); - - // Swap - - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &child_old, - &child_new, - Some(netuid), - false - ),); - - // Verify parent and child keys updates - assert_eq!( - ParentKeys::::get(child_new, netuid), - vec![(u64::MAX, parent)] - ); - assert_eq!( - ChildKeys::::get(parent, netuid), - vec![(u64::MAX, child_new)] - ); - assert_eq!( - PendingChildKeys::::get(netuid, parent), - (vec![(u64::MAX, child_new)], existing_pending_child_keys.1) // Same cooldown block. - ); - }) -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_hotkey_is_sn_owner_hotkey --exact --nocapture -#[test] -fn test_swap_hotkey_is_sn_owner_hotkey() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - - // Create dynamic network - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - // Check for SubnetOwnerHotkey - assert_eq!(SubnetOwnerHotkey::::get(netuid), old_hotkey); - - // Perform the swap - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - ),); - - // Check for SubnetOwnerHotkey - assert_eq!(SubnetOwnerHotkey::::get(netuid), new_hotkey); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_hotkey_swap_rate_limits --exact --nocapture -#[test] -fn test_swap_hotkey_swap_rate_limits() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - - let delegate_take_block = 4567; - let child_key_take_block = 8910; - - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - // Set the last delegate take block for the old hotkey - SubtensorModule::set_last_tx_block_delegate_take(&old_hotkey, delegate_take_block); - // Set last childkey take block for the old hotkey - SubtensorModule::set_last_tx_block_childkey(&old_hotkey, child_key_take_block); - - // Perform the swap - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - ),); - - // Check for new hotkey (LastTxBlock is no longer transferred: the generic tx rate - // limit was removed. - assert_eq!( - SubtensorModule::get_last_tx_block_delegate_take(&new_hotkey), - delegate_take_block - ); - assert_eq!( - SubtensorModule::get_last_tx_block_childkey_take(&new_hotkey), - child_key_take_block - ); - }); -} - -#[test] -fn test_swap_owner_failed_interval_not_passed() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - Owner::::insert(old_hotkey, coldkey); - assert_err!( - SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - ), - Error::::HotKeySwapOnSubnetIntervalNotPassed, - ); - }); -} - -#[test] -fn test_swap_owner_check_swap_block_set() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - Owner::::insert(old_hotkey, coldkey); - let new_block_number = System::block_number() + HotkeySwapOnSubnetInterval::get(); - System::set_block_number(new_block_number); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - )); - - assert_eq!( - LastHotkeySwapOnNetuid::::get(netuid, coldkey), - new_block_number - ); - }); -} - -#[test] -fn test_swap_owner_check_swap_record_clean_up() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - let netuid = add_dynamic_network(&old_hotkey, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - Owner::::insert(old_hotkey, coldkey); - let new_block_number = System::block_number() + HotkeySwapOnSubnetInterval::get(); - System::set_block_number(new_block_number); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - )); - - assert_eq!( - LastHotkeySwapOnNetuid::::get(netuid, coldkey), - new_block_number - ); - - step_block((HotkeySwapOnSubnetInterval::get() as u16 + u16::from(netuid)) * 2); - assert!(!LastHotkeySwapOnNetuid::::contains_key( - netuid, coldkey - )); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_revert_hotkey_swap_stake_is_not_lost --exact --nocapture -#[test] -fn test_revert_hotkey_swap_stake_is_not_lost() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let netuid2 = NetUid::from(2); - let tempo: u16 = 13; - let hk1 = U256::from(1); - let hk2 = U256::from(2); - let coldkey = U256::from(3); - let swap_cost = 1_000_000_000u64 * 2; - let stake2 = 1_000_000_000u64; - - // Setup - add_network(netuid, tempo, 0); - add_network(netuid2, tempo, 0); - register_ok_neuron(netuid, hk1, coldkey, 0); - register_ok_neuron(netuid2, hk1, coldkey, 0); - add_balance_to_coldkey_account(&coldkey, swap_cost.into()); - - let hk1_stake_before_increase = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey, netuid); - assert!( - hk1_stake_before_increase == 0.into(), - "hk1 should have empty stake" - ); - - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hk1, - &coldkey, - netuid, - 1_000_000_000u64.into(), - ); - - let hk1_stake_before_swap = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey, netuid); - assert!( - hk1_stake_before_swap == 1_000_000_000.into(), - "hk1 should have stake before swap" - ); - - step_block(20); - - assert_ok!(SubtensorModule::do_swap_hotkey( - <::RuntimeOrigin>::signed(coldkey), - &hk1, - &hk2, - Some(netuid), - false - )); - - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hk1, - &coldkey, - netuid, - stake2.into(), - ); - - step_block(20); - - let hk2_stake_before_revert = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk2, &coldkey, netuid); - let hk1_stake_before_revert = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey, netuid); - - assert_eq!(hk1_stake_before_revert, stake2.into()); - - // Revert: hk2 -> hk1 - assert_ok!(SubtensorModule::do_swap_hotkey( - <::RuntimeOrigin>::signed(coldkey), - &hk2, - &hk1, - Some(netuid), - false - )); - - let hk1_stake_after_revert = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey, netuid); - let hk2_stake_after_revert = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk2, &coldkey, netuid); - - assert_eq!( - hk1_stake_after_revert, - hk2_stake_before_revert + stake2.into(), - ); - - // hk2 should be empty - assert_eq!( - hk2_stake_after_revert, - 0.into(), - "hk2 should have no stake after revert" - ); - }); -} - -// Check swap hotkey with keep_stake doesn't affect stake and related storage maps -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_hotkey_swap_keep_stake --exact --nocapture -#[test] -fn test_hotkey_swap_keep_stake() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let tempo: u16 = 13; - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let child_key = U256::from(4); - let coldkey = U256::from(3); - let swap_cost = 1_000_000_000u64 * 2; - let stake_amount = 1_000_000_000u64; - let voting_power_value = 5_000_000_000_000_u64; - - // Setup - add_network(netuid, tempo, 0); - register_ok_neuron(netuid, old_hotkey, coldkey, 0); - add_balance_to_coldkey_account(&coldkey, swap_cost.into()); - - VotingPower::::insert(netuid, old_hotkey, voting_power_value); - assert_eq!( - SubtensorModule::get_voting_power(netuid, &old_hotkey), - voting_power_value - ); - - ChildKeys::::insert(old_hotkey, netuid, vec![(u64::MAX, child_key)]); - ParentKeys::::insert(child_key, netuid, vec![(u64::MAX, old_hotkey)]); - - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &old_hotkey, - &coldkey, - netuid, - stake_amount.into(), - ); - - assert!(SubtensorModule::is_hotkey_registered_on_network( - netuid, - &old_hotkey - )); - - step_block(20); - - let old_hotkey_stake_before_swap = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &old_hotkey, - &coldkey, - netuid, - ); - - assert_ok!(SubtensorModule::do_swap_hotkey( - <::RuntimeOrigin>::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - true - )); - - let old_hotkey_stake_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &old_hotkey, - &coldkey, - netuid, - ); - assert_eq!( - old_hotkey_stake_after, old_hotkey_stake_before_swap, - "old_hotkey stake must NOT change during keep_stake swap" - ); - - let new_hotkey_stake_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &new_hotkey, - &coldkey, - netuid, - ); - assert_eq!( - new_hotkey_stake_after, - 0.into(), - "new_hotkey should have no stake" - ); - - assert!( - SubtensorModule::is_hotkey_registered_on_network(netuid, &new_hotkey), - "new_hotkey should be registered on netuid" - ); - - assert!( - !SubtensorModule::is_hotkey_registered_on_network(netuid, &old_hotkey), - "old_hotkey should NOT be registered on netuid after swap" - ); - - let root_total_alpha = TotalHotkeyAlpha::::get(old_hotkey, netuid); - let child_total_alpha = TotalHotkeyAlpha::::get(new_hotkey, netuid); - assert!( - root_total_alpha > 0.into(), - "old_hotkey should retain TotalHotkeyAlpha" - ); - assert_eq!( - child_total_alpha, - 0.into(), - "new_hotkey should have zero TotalHotkeyAlpha" - ); - - let root_voting_power = VotingPower::::get(netuid, old_hotkey); - let child_voting_power = VotingPower::::get(netuid, new_hotkey); - assert!( - root_voting_power > 0, - "old_hotkey should retain VotingPower" - ); - assert_eq!( - child_voting_power, 0, - "new_hotkey should have zero VotingPower" - ); - - let old_hotkey_children = ChildKeys::::get(old_hotkey, netuid); - assert!( - !old_hotkey_children.iter().any(|(_, c)| *c == child_key), - "old_hotkey should NOT retain ChildKeys after swap" - ); - let new_hotkey_children = ChildKeys::::get(new_hotkey, netuid); - assert!( - new_hotkey_children.iter().any(|(_, c)| *c == child_key), - "new_hotkey should inherit ChildKeys from old_hotkey" - ); - - let child_key_parents = ParentKeys::::get(child_key, netuid); - assert!( - child_key_parents.iter().any(|(_, p)| *p == new_hotkey), - "child_key should have new_hotkey as parent after swap" - ); - assert!( - !child_key_parents.iter().any(|(_, p)| *p == old_hotkey), - "child_key should NOT have old_hotkey as parent after swap" - ); - }); -} -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_revert_hotkey_swap --exact --nocapture -// This test confirms, that the old hotkey can be reverted after the hotkey swap -#[test] -fn test_revert_hotkey_swap() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let netuid2 = NetUid::from(2); - let tempo: u16 = 13; - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(3); - let swap_cost = 1_000_000_000u64 * 2; - - // Setup initial state - add_network(netuid, tempo, 0); - add_network(netuid2, tempo, 0); - register_ok_neuron(netuid, old_hotkey, coldkey, 0); - register_ok_neuron(netuid2, old_hotkey, coldkey, 0); - add_balance_to_coldkey_account(&coldkey, swap_cost.into()); - step_block(20); - - // Perform the first swap (only on netuid) - assert_ok!(SubtensorModule::do_swap_hotkey( - <::RuntimeOrigin>::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - )); - - assert!(SubtensorModule::is_hotkey_registered_on_any_network( - &old_hotkey - )); - - step_block(20); - - assert_ok!(SubtensorModule::do_swap_hotkey( - <::RuntimeOrigin>::signed(coldkey), - &new_hotkey, - &old_hotkey, - Some(netuid), - false - )); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_revert_hotkey_swap_parent_hotkey_childkey_maps --exact --nocapture -#[test] -fn test_revert_hotkey_swap_parent_hotkey_childkey_maps() { - new_test_ext(1).execute_with(|| { - let hk1 = U256::from(1); - let coldkey = U256::from(2); - let child = U256::from(3); - let child_other = U256::from(4); - let hk2 = U256::from(5); - - let netuid = add_dynamic_network(&hk1, &coldkey); - let netuid2 = add_dynamic_network(&hk1, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - SubtensorModule::create_account_if_non_existent(&coldkey, &hk1); - - mock_set_children(&coldkey, &hk1, netuid, &[(u64::MAX, child)]); - step_rate_limit(&TransactionType::SetChildren, netuid); - mock_schedule_children(&coldkey, &hk1, netuid, &[(u64::MAX, child_other)]); - - assert_eq!( - ParentKeys::::get(child, netuid), - vec![(u64::MAX, hk1)] - ); - assert_eq!(ChildKeys::::get(hk1, netuid), vec![(u64::MAX, child)]); - let existing_pending_child_keys = PendingChildKeys::::get(netuid, hk1); - assert_eq!(existing_pending_child_keys.0, vec![(u64::MAX, child_other)]); - - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &hk1, - &hk2, - Some(netuid), - false - )); - - assert_eq!( - ParentKeys::::get(child, netuid), - vec![(u64::MAX, hk2)] - ); - assert_eq!(ChildKeys::::get(hk2, netuid), vec![(u64::MAX, child)]); - assert_eq!( - PendingChildKeys::::get(netuid, hk2), - existing_pending_child_keys - ); - assert!(ChildKeys::::get(hk1, netuid).is_empty()); - assert!(PendingChildKeys::::get(netuid, hk1).0.is_empty()); - - // Revert: hk2 -> hk1 - step_block(20); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &hk2, - &hk1, - Some(netuid), - false - )); - - assert_eq!( - ParentKeys::::get(child, netuid), - vec![(u64::MAX, hk1)], - "ParentKeys must point back to hk1 after revert" - ); - assert_eq!( - ChildKeys::::get(hk1, netuid), - vec![(u64::MAX, child)], - "ChildKeys must be restored to hk1 after revert" - ); - assert_eq!( - PendingChildKeys::::get(netuid, hk1), - existing_pending_child_keys, - "PendingChildKeys must be restored to hk1 after revert" - ); - - assert!( - ChildKeys::::get(hk2, netuid).is_empty(), - "hk2 must have no ChildKeys after revert" - ); - assert!( - PendingChildKeys::::get(netuid, hk2).0.is_empty(), - "hk2 must have no PendingChildKeys after revert" - ); - }) -} -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_revert_hotkey_swap_uids_and_keys --exact --nocapture -#[test] -fn test_revert_hotkey_swap_uids_and_keys() { - new_test_ext(1).execute_with(|| { - let uid = 5u16; - let hk1 = U256::from(1); - let hk2 = U256::from(2); - let coldkey = U256::from(3); - - let netuid = add_dynamic_network(&hk1, &coldkey); - let netuid2 = add_dynamic_network(&hk1, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - IsNetworkMember::::insert(hk1, netuid, true); - Uids::::insert(netuid, hk1, uid); - Keys::::insert(netuid, uid, hk1); - - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &hk1, - &hk2, - Some(netuid), - false - )); - - assert_eq!(Uids::::get(netuid, hk1), None); - assert_eq!(Uids::::get(netuid, hk2), Some(uid)); - assert_eq!(Keys::::get(netuid, uid), hk2); - - // Revert: hk2 -> hk1 - step_block(20); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &hk2, - &hk1, - Some(netuid), - false - )); - - assert_eq!( - Uids::::get(netuid, hk2), - None, - "hk2 must have no uid after revert" - ); - assert_eq!( - Uids::::get(netuid, hk1), - Some(uid), - "hk1 must have its uid restored after revert" - ); - assert_eq!( - Keys::::get(netuid, uid), - hk1, - "Keys must point back to hk1 after revert" - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_revert_hotkey_swap_auto_stake_destination --exact --nocapture -#[test] -fn test_revert_hotkey_swap_auto_stake_destination() { - new_test_ext(1).execute_with(|| { - let hk1 = U256::from(1); - let hk2 = U256::from(2); - let coldkey = U256::from(3); - let netuid = NetUid::from(2u16); - let netuid2 = NetUid::from(3u16); - let staker1 = U256::from(4); - let staker2 = U256::from(5); - let coldkeys = vec![staker1, staker2, coldkey]; - - add_network(netuid, 1, 0); - add_network(netuid2, 1, 0); - register_ok_neuron(netuid, hk1, coldkey, 0); - register_ok_neuron(netuid2, hk1, coldkey, 0); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - AutoStakeDestinationColdkeys::::insert(hk1, netuid, coldkeys.clone()); - AutoStakeDestination::::insert(coldkey, netuid, hk1); - AutoStakeDestination::::insert(staker1, netuid, hk1); - AutoStakeDestination::::insert(staker2, netuid, hk1); - - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &hk1, - &hk2, - Some(netuid), - false - )); - - assert_eq!( - AutoStakeDestinationColdkeys::::get(hk2, netuid), - coldkeys - ); - assert!(AutoStakeDestinationColdkeys::::get(hk1, netuid).is_empty()); - assert_eq!( - AutoStakeDestination::::get(coldkey, netuid), - Some(hk2) - ); - assert_eq!( - AutoStakeDestination::::get(staker1, netuid), - Some(hk2) - ); - assert_eq!( - AutoStakeDestination::::get(staker2, netuid), - Some(hk2) - ); - - // Revert: hk2 -> hk1 - step_block(20); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &hk2, - &hk1, - Some(netuid), - false - )); - - assert_eq!( - AutoStakeDestinationColdkeys::::get(hk1, netuid), - coldkeys, - "AutoStakeDestinationColdkeys must be restored to hk1 after revert" - ); - assert!( - AutoStakeDestinationColdkeys::::get(hk2, netuid).is_empty(), - "hk2 must have no AutoStakeDestinationColdkeys after revert" - ); - assert_eq!( - AutoStakeDestination::::get(coldkey, netuid), - Some(hk1), - "coldkey AutoStakeDestination must point back to hk1 after revert" - ); - assert_eq!( - AutoStakeDestination::::get(staker1, netuid), - Some(hk1), - "staker1 AutoStakeDestination must point back to hk1 after revert" - ); - assert_eq!( - AutoStakeDestination::::get(staker2, netuid), - Some(hk1), - "staker2 AutoStakeDestination must point back to hk1 after revert" - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_revert_hotkey_swap_subnet_owner --exact --nocapture -#[test] -fn test_revert_hotkey_swap_subnet_owner() { - new_test_ext(1).execute_with(|| { - let hk1 = U256::from(1); - let hk2 = U256::from(2); - let coldkey = U256::from(3); - - let netuid = add_dynamic_network(&hk1, &coldkey); - let netuid2 = add_dynamic_network(&hk1, &coldkey); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - assert_eq!(SubnetOwnerHotkey::::get(netuid), hk1); - - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &hk1, - &hk2, - Some(netuid), - false - )); - - assert_eq!( - SubnetOwnerHotkey::::get(netuid), - hk2, - "hk2 must be subnet owner after swap" - ); - - // Revert: hk2 -> hk1 - step_block(20); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &hk2, - &hk1, - Some(netuid), - false - )); - - assert_eq!( - SubnetOwnerHotkey::::get(netuid), - hk1, - "hk1 must be restored as subnet owner after revert" - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_revert_hotkey_swap_dividends --exact --nocapture -#[test] -fn test_revert_hotkey_swap_dividends() { - new_test_ext(1).execute_with(|| { - let hk1 = U256::from(1); - let hk2 = U256::from(2); - let coldkey = U256::from(3); - - let netuid = add_dynamic_network(&hk1, &coldkey); - remove_owner_registration_stake(netuid); - let netuid2 = add_dynamic_network(&hk1, &coldkey); - remove_owner_registration_stake(netuid2); - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); - - let amount = 10_000; - let shares = U64F64::from_num(10_000); - - TotalHotkeyAlpha::::insert(hk1, netuid, AlphaBalance::from(amount)); - TotalHotkeyAlphaLastEpoch::::insert(hk1, netuid, AlphaBalance::from(amount * 2)); - TotalHotkeyShares::::insert(hk1, netuid, U64F64::from_num(shares)); - Alpha::::insert((hk1, coldkey, netuid), U64F64::from_num(amount)); - AlphaDividendsPerSubnet::::insert(netuid, hk1, AlphaBalance::from(amount)); - - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &hk1, - &hk2, - Some(netuid), - false - )); - - assert_eq!( - TotalHotkeyAlpha::::get(hk1, netuid), - AlphaBalance::ZERO - ); - assert_eq!( - TotalHotkeyAlpha::::get(hk2, netuid), - AlphaBalance::from(amount) - ); - assert_eq!( - TotalHotkeyAlphaLastEpoch::::get(hk1, netuid), - AlphaBalance::ZERO - ); - assert_eq!( - TotalHotkeyAlphaLastEpoch::::get(hk2, netuid), - AlphaBalance::from(amount * 2) - ); - assert_eq!( - TotalHotkeyShares::::get(hk1, netuid), - U64F64::from_num(0) - ); - assert_eq!( - TotalHotkeyShares::::get(hk2, netuid), - U64F64::from_num(0) - ); - assert_eq!(TotalHotkeySharesV2::::get(hk2, netuid), shares.into()); - assert_eq!( - Alpha::::get((hk1, coldkey, netuid)), - U64F64::from_num(0) - ); - assert_eq!( - Alpha::::get((hk2, coldkey, netuid)), - U64F64::from_num(0) - ); - assert_eq!(AlphaV2::::get((hk2, coldkey, netuid)), amount.into()); - assert_eq!( - AlphaDividendsPerSubnet::::get(netuid, hk1), - AlphaBalance::ZERO - ); - assert_eq!( - AlphaDividendsPerSubnet::::get(netuid, hk2), - AlphaBalance::from(amount) - ); - - // Revert: hk2 -> hk1 - step_block(20); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &hk2, - &hk1, - Some(netuid), - false - )); - - assert_eq!( - TotalHotkeyAlpha::::get(hk2, netuid), - AlphaBalance::ZERO, - "hk2 TotalHotkeyAlpha must be zero after revert" - ); - assert_eq!( - TotalHotkeyAlpha::::get(hk1, netuid), - AlphaBalance::from(amount), - "hk1 TotalHotkeyAlpha must be restored after revert" - ); - assert_eq!( - TotalHotkeyAlphaLastEpoch::::get(hk2, netuid), - AlphaBalance::ZERO, - "hk2 TotalHotkeyAlphaLastEpoch must be zero after revert" - ); - assert_eq!( - TotalHotkeyAlphaLastEpoch::::get(hk1, netuid), - AlphaBalance::from(amount * 2), - "hk1 TotalHotkeyAlphaLastEpoch must be restored after revert" - ); - assert_eq!( - TotalHotkeyShares::::get(hk2, netuid), - U64F64::from_num(0), - "hk2 TotalHotkeyShares must be zero after revert" - ); - assert_eq!( - TotalHotkeyShares::::get(hk1, netuid), - U64F64::from_num(0), - "hk1 TotalHotkeyShares must be migrated to v2" - ); - assert_eq!( - TotalHotkeySharesV2::::get(hk1, netuid), - shares.into(), - "hk1 TotalHotkeyShares must be restored to v2 after revert" - ); - assert_eq!( - Alpha::::get((hk2, coldkey, netuid)), - U64F64::from_num(0), - "hk2 Alpha must be zero after revert" - ); - assert_eq!( - Alpha::::get((hk1, coldkey, netuid)), - U64F64::from_num(0), - "hk1 Alpha must be migrated to v2" - ); - assert_eq!( - AlphaV2::::get((hk1, coldkey, netuid)), - amount.into(), - "hk1 Alpha must be restored to v2 after revert" - ); - assert_eq!( - AlphaDividendsPerSubnet::::get(netuid, hk2), - AlphaBalance::ZERO, - "hk2 AlphaDividendsPerSubnet must be zero after revert" - ); - assert_eq!( - AlphaDividendsPerSubnet::::get(netuid, hk1), - AlphaBalance::from(amount), - "hk1 AlphaDividendsPerSubnet must be restored after revert" - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_revert_voting_power_transfers_on_hotkey_swap --exact --nocapture -#[test] -fn test_revert_voting_power_transfers_on_hotkey_swap() { - new_test_ext(1).execute_with(|| { - let hk1 = U256::from(1); - let hk2 = U256::from(99); - let coldkey = U256::from(2); - let netuid = add_dynamic_network(&hk1, &coldkey); - let voting_power_value = 5_000_000_000_000_u64; - - VotingPower::::insert(netuid, hk1, voting_power_value); - assert_eq!( - SubtensorModule::get_voting_power(netuid, &hk1), - voting_power_value - ); - assert_eq!(SubtensorModule::get_voting_power(netuid, &hk2), 0); - - SubtensorModule::swap_voting_power_for_hotkey(&hk1, &hk2, netuid); - - assert_eq!(SubtensorModule::get_voting_power(netuid, &hk1), 0); - assert_eq!( - SubtensorModule::get_voting_power(netuid, &hk2), - voting_power_value - ); - - // Revert: hk2 -> hk1 - SubtensorModule::swap_voting_power_for_hotkey(&hk2, &hk1, netuid); - - assert_eq!( - SubtensorModule::get_voting_power(netuid, &hk1), - voting_power_value, - "hk1 voting power must be fully restored after revert" - ); - assert_eq!( - SubtensorModule::get_voting_power(netuid, &hk2), - 0, - "hk2 must have no voting power after revert" - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_revert_claim_root_with_swap_hotkey --exact --nocapture -#[test] -fn test_revert_claim_root_with_swap_hotkey() { - new_test_ext(1).execute_with(|| { - let owner_coldkey = U256::from(1001); - let hk1 = U256::from(1002); - let hk2 = U256::from(1003); - let coldkey = U256::from(1004); - - let netuid = add_dynamic_network(&hk1, &owner_coldkey); - let netuid2 = add_dynamic_network(&hk1, &owner_coldkey); - - add_balance_to_coldkey_account(&owner_coldkey, 1_000_000_000_000_u64.into()); - SubtensorModule::set_tao_weight(u64::MAX); - - let root_stake = 2_000_000u64; - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hk1, - &coldkey, - NetUid::ROOT, - root_stake.into(), - ); - - let initial_total_hotkey_alpha = 10_000_000u64; - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hk1, - &owner_coldkey, - netuid, - initial_total_hotkey_alpha.into(), - ); - - let pending_root_alpha = 1_000_000u64; - SubtensorModule::distribute_emission( - netuid, - AlphaBalance::ZERO, - AlphaBalance::ZERO, - pending_root_alpha.into(), - AlphaBalance::ZERO, - ); - - assert_ok!(SubtensorModule::set_root_claim_type( - RuntimeOrigin::signed(coldkey), - RootClaimTypeEnum::Keep - )); - assert_ok!(SubtensorModule::claim_root( - RuntimeOrigin::signed(coldkey), - BTreeSet::from([netuid]) - )); - - let stake_after_claim: u64 = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey, netuid) - .into(); - - let hk1_root_claimed = RootClaimed::::get((netuid, &hk1, &coldkey)); - let hk1_claimable = *RootClaimable::::get(hk1).get(&netuid).unwrap(); - - assert_eq!(u128::from(stake_after_claim), hk1_root_claimed); - assert!(!RootClaimable::::get(hk2).contains_key(&netuid)); - - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(owner_coldkey), - &hk1, - &hk2, - Some(netuid), - false - )); - - assert_eq!( - RootClaimed::::get((netuid, &hk2, &coldkey)), - 0u128, - "hk2 RootClaimed must be zero after swap" - ); - assert_eq!( - RootClaimed::::get((netuid, &hk1, &coldkey)), - hk1_root_claimed, - "hk2 must have hk1's RootClaimed after swap" - ); - assert!(RootClaimable::::get(hk1).contains_key(&netuid)); - assert!(!RootClaimable::::get(hk2).contains_key(&netuid)); - - // Revert: hk2 -> hk1 - step_block(20); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(owner_coldkey), - &hk2, - &hk1, - Some(netuid), - false - )); - - assert_eq!( - RootClaimed::::get((netuid, &hk2, &coldkey)), - 0u128, - "hk2 RootClaimed must be zero after revert" - ); - assert_eq!( - RootClaimed::::get((netuid, &hk1, &coldkey)), - hk1_root_claimed, - "hk1 RootClaimed must be restored after revert" - ); - - assert!(!RootClaimable::::get(hk2).contains_key(&netuid)); - assert_eq!( - *RootClaimable::::get(hk1).get(&netuid).unwrap(), - hk1_claimable, - "hk1 RootClaimable must be restored after revert" - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_hotkey_with_existing_stake --exact --show-output -#[test] -fn test_swap_hotkey_with_existing_stake() { - new_test_ext(1).execute_with(|| { - let old_hotkey = U256::from(1); - let new_hotkey = U256::from(2); - let coldkey = U256::from(4); - let staker1 = U256::from(5); - let staker2 = U256::from(6); - let subnet_owner_coldkey = U256::from(1000); - let subnet_owner_hotkey = U256::from(1001); - let staked_tao_1 = 100_000_000; - let staked_tao_2 = 200_000_000; - let staked_tao_3 = 300_000_000; - let staked_tao_4 = 500_000_000; - - // Set up initial state - let netuid = add_dynamic_network(&subnet_owner_coldkey, &subnet_owner_hotkey); - register_ok_neuron(netuid, old_hotkey, coldkey, 1234); - register_ok_neuron(netuid, new_hotkey, coldkey, 1234); - - // Add balance to coldkeys - add_balance_to_coldkey_account(&coldkey, 10_000_000_000_u64.into()); - add_balance_to_coldkey_account(&staker1, 10_000_000_000_u64.into()); - add_balance_to_coldkey_account(&staker2, 10_000_000_000_u64.into()); - - // Stake with staker1 coldkey on old_hotkey - assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(staker1), - old_hotkey, - netuid, - staked_tao_1.into() - )); - - // Stake with staker2 coldkey on old_hotkey - assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(staker2), - old_hotkey, - netuid, - staked_tao_2.into() - )); - - // Stake with staker1 coldkey on new_hotkey - assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(staker1), - new_hotkey, - netuid, - staked_tao_3.into() - )); - - // Stake with staker2 coldkey on new_hotkey - assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(staker2), - new_hotkey, - netuid, - staked_tao_4.into() - )); - - // Emulate effect of emission into alpha pool - makes numerators and denominators not equal to alpha - let emission = AlphaBalance::from(1_000_000_000); - SubtensorModule::increase_stake_for_hotkey_on_subnet(&old_hotkey, netuid, emission); - SubtensorModule::increase_stake_for_hotkey_on_subnet(&new_hotkey, netuid, emission); - - // Hotkey new_hotkey gets deregistered, stake stays - IsNetworkMember::::remove(new_hotkey, netuid); - - let hk1_stake_1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &old_hotkey, - &staker1, - netuid, - ); - let hk2_stake_1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &new_hotkey, - &staker1, - netuid, - ); - let hk1_stake_2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &old_hotkey, - &staker2, - netuid, - ); - let hk2_stake_2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &new_hotkey, - &staker2, - netuid, - ); - - assert!(!hk1_stake_1.is_zero()); - assert!(!hk2_stake_1.is_zero()); - assert!(!hk1_stake_2.is_zero()); - assert!(!hk2_stake_2.is_zero()); - - let total_hk1_stake = SubtensorModule::get_total_stake_for_hotkey(&old_hotkey); - let total_hk2_stake = SubtensorModule::get_total_stake_for_hotkey(&new_hotkey); - assert!(!total_hk1_stake.is_zero()); - assert!(!total_hk2_stake.is_zero()); - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - )); - - // Check correctness of stake transfer - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &old_hotkey, - &staker1, - netuid - ), - 0.into() - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &old_hotkey, - &staker2, - netuid - ), - 0.into() - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &new_hotkey, - &staker1, - netuid - ), - hk2_stake_1 + hk1_stake_1 - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &new_hotkey, - &staker2, - netuid - ), - hk2_stake_2 + hk1_stake_2 - ); - - // Check total stake transfer - assert_abs_diff_eq!( - SubtensorModule::get_total_stake_for_hotkey(&old_hotkey), - 0.into(), - epsilon = 1.into() - ); - assert_abs_diff_eq!( - SubtensorModule::get_total_stake_for_hotkey(&new_hotkey), - total_hk1_stake + total_hk2_stake, - epsilon = 1.into() - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_revert_hotkey_swap_with_revert_stake_the_same --exact --nocapture -#[test] -fn test_revert_hotkey_swap_with_revert_stake_the_same() { - new_test_ext(1).execute_with(|| { - let netuid_1 = NetUid::from(1); - let netuid_2 = NetUid::from(2); - let tempo: u16 = 13; - let hk1 = U256::from(1); - let new_hotkey = U256::from(2); - let random_hotkey = U256::from(3); - let coldkey = U256::from(3); - let coldkey_2 = U256::from(4); - let coldkey_3 = U256::from(5); - let coldkey_4 = U256::from(6); - let random_coldkey = U256::from(7); - let initial_balance = 10_000_000_000u64 * 2; - let stake1 = 500_000_000u64; - let stake2 = 1_000_000_000u64; - let stake_ck2 = 1_500_000_000u64; - let stake_ck3 = 300_000_000u64; - let stake_ck4 = 900_000_000u64; - - assert_ok!(SubtensorModule::try_associate_hotkey( - <::RuntimeOrigin>::signed(random_coldkey), - random_hotkey - )); - - // Setup - super::mock::setup_reserves(netuid_1, (stake_ck4 * 100).into(), (stake_ck4 * 100).into()); - super::mock::setup_reserves(netuid_2, (stake_ck4 * 100).into(), (stake_ck4 * 100).into()); - - add_network(netuid_1, tempo, 0); - add_network(netuid_2, tempo, 0); - - SubnetMechanism::::insert(netuid_1, 1); - SubnetMechanism::::insert(netuid_2, 1); - - register_ok_neuron(netuid_1, hk1, coldkey, 0); - register_ok_neuron(netuid_2, hk1, coldkey, 0); - - add_balance_to_coldkey_account(&coldkey, initial_balance.into()); - add_balance_to_coldkey_account(&coldkey_4, initial_balance.into()); - add_balance_to_coldkey_account(&random_coldkey, initial_balance.into()); - step_block(20); // Waiting interval to be able to swap later - - // Checking stake for hk1 on both networks - let hk1_stake_before_increase_sn_1 = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey, netuid_1); - assert!( - hk1_stake_before_increase_sn_1 == 0.into(), - "hk1 should have empty stake" - ); - - let hk1_stake_before_increase_sn_2 = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey, netuid_2); - assert!( - hk1_stake_before_increase_sn_2 == 0.into(), - "hk1 should have empty stake" - ); - - // Adding stake to hk1 on both networks - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hk1, - &coldkey, - netuid_1, - stake1.into(), - ); - // Adding another stake for different coldkey - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hk1, - &coldkey_2, - netuid_1, - stake_ck2.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hk1, - &coldkey_3, - netuid_1, - stake_ck3.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hk1, - &coldkey, - netuid_2, - stake2.into(), - ); - - // The stake for validator - let hk1_stake_before_swap_sn_1 = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey, netuid_1); - assert!( - hk1_stake_before_swap_sn_1 == stake1.into(), - "hk1 should have stake before swap on sn_1" - ); - - // Let's check individual stake - let hk1_stake_before_swap_sn_1 = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey_2, netuid_1); - assert_eq!( - hk1_stake_before_swap_sn_1, - (stake_ck2).into(), - "stake for ck2 should be only his stake" - ); - - let hk1_stake_before_swap_sn_2 = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey, netuid_2); - assert!( - hk1_stake_before_swap_sn_2 == stake2.into(), - "hk1 should have stake before swap on sn_2" - ); - - assert_ok!(SubtensorModule::do_swap_hotkey( - <::RuntimeOrigin>::signed(coldkey), - &hk1, - &new_hotkey, - Some(netuid_1), - false - )); - - assert_eq!(Owner::::get(hk1), coldkey); - - SubtensorModule::do_add_stake( - RawOrigin::Signed(random_coldkey).into(), - hk1, - netuid_1, - stake_ck4.into(), - ) - .unwrap(); - - // Check stake moved to new hotkey on subnet1 - let new_hotkey_stake_after_swap_ck = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &new_hotkey, - &coldkey, - netuid_1, - ); - assert_eq!(new_hotkey_stake_after_swap_ck, stake1.into()); - - // Check stake moved for ck2 - let new_hotkey_stake_after_swap_ck_1 = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &new_hotkey, - &coldkey_2, - netuid_1, - ); - assert_eq!(new_hotkey_stake_after_swap_ck_1, stake_ck2.into()); - - // Check stake moved for ck3 - let new_hotkey_stake_after_swap_ck_3 = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &new_hotkey, - &coldkey_3, - netuid_1, - ); - assert_eq!(new_hotkey_stake_after_swap_ck_3, stake_ck3.into()); - - step_block(20); - - // Let's check individual stakes; they changed because of emissions - let new_hotkey_stake_before_revert_ck = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &new_hotkey, - &coldkey, - netuid_1, - ); - assert!(new_hotkey_stake_before_revert_ck > stake1.into()); - - let new_hotkey_stake_before_revert_ck_2 = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &new_hotkey, - &coldkey_2, - netuid_1, - ); - assert!(new_hotkey_stake_before_revert_ck_2 > stake_ck2.into()); - - let new_hotkey_stake_before_revert_ck_3 = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &new_hotkey, - &coldkey_3, - netuid_1, - ); - assert!(new_hotkey_stake_before_revert_ck_3 > stake_ck3.into()); - - // Reverting back: hk2 -> hk1 - assert_ok!(SubtensorModule::do_swap_hotkey( - <::RuntimeOrigin>::signed(coldkey), - &new_hotkey, - &hk1, - Some(netuid_1), - false - )); - - // Let's check individual stakes; they changed because of emissions - let old_hotkey_stake_after_revert_ck = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey, netuid_1); - assert_eq!( - old_hotkey_stake_after_revert_ck, - new_hotkey_stake_before_revert_ck - ); - - let old_hotkey_stake_after_revert_ck_2 = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey_2, netuid_1); - assert_eq!( - old_hotkey_stake_after_revert_ck_2, - new_hotkey_stake_before_revert_ck_2 - ); - - let old_hotkey_stake_after_revert_ck_3 = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey_3, netuid_1); - assert_eq!( - old_hotkey_stake_after_revert_ck_3, - new_hotkey_stake_before_revert_ck_3 - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_hotkey_root_claims_unchanged_if_not_root --exact --nocapture -#[test] -fn test_swap_hotkey_root_claims_unchanged_if_not_root() { - new_test_ext(1).execute_with(|| { - let owner_coldkey = U256::from(1001); - let neuron_hotkey = U256::from(1002); - let staker_coldkey = U256::from(1003); - let netuid = add_dynamic_network(&neuron_hotkey, &owner_coldkey); - let new_hotkey = U256::from(10030); - - add_balance_to_coldkey_account(&owner_coldkey, 20_000_000_000_000_000_u64.into()); - SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 - - let root_stake = 2_000_000_000u64; - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &neuron_hotkey, - &staker_coldkey, - NetUid::ROOT, - root_stake.into(), - ); - - let initial_total_hotkey_alpha = 10_000_000_000u64; - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &neuron_hotkey, - &staker_coldkey, - netuid, - initial_total_hotkey_alpha.into(), - ); - - let validator_stake = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &neuron_hotkey, - &staker_coldkey, - netuid, - ); - assert_eq!(validator_stake, initial_total_hotkey_alpha.into()); - - // Distribute pending root alpha - let pending_root_alpha = 1_000_000_000u64; - SubtensorModule::distribute_emission( - netuid, - AlphaBalance::ZERO, - AlphaBalance::ZERO, - pending_root_alpha.into(), - AlphaBalance::ZERO, - ); - - assert_ok!(SubtensorModule::claim_root( - RuntimeOrigin::signed(staker_coldkey), - BTreeSet::from([netuid]) - )); - - let claimable = RootClaimable::::get(neuron_hotkey) - .get(&netuid) - .copied(); - - assert!(claimable.is_some()); - let claimable = claimable.unwrap_or_default(); - - assert!(claimable > 0); - - assert!(RootClaimed::::get((netuid, &neuron_hotkey, &staker_coldkey,)) > 0u128); - - step_block(20); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(owner_coldkey), - &neuron_hotkey, - &new_hotkey, - Some(netuid), - false - )); - - // Claimable and claimed should stay on old hotkey - assert_eq!( - RootClaimable::::get(neuron_hotkey) - .get(&netuid) - .copied(), - Some(claimable) - ); - assert!(RootClaimed::::get((netuid, &neuron_hotkey, &staker_coldkey,)) > 0u128); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_hotkey_root_claims_changed_if_root --exact --nocapture -#[test] -fn test_swap_hotkey_root_claims_changed_if_root() { - new_test_ext(1).execute_with(|| { - let owner_coldkey = U256::from(1001); - - let neuron_hotkey = U256::from(1004); - let neuron_hotkey_new = U256::from(1005); - - let staker_coldkey = U256::from(1006); - - NetworksAdded::::insert(NetUid::ROOT, true); - - // Use neuron_hotkey as subnet creator so it receives root dividends - let netuid_1 = add_dynamic_network(&neuron_hotkey, &owner_coldkey); - - add_balance_to_coldkey_account(&owner_coldkey, 20_000_000_000_000_000_u64.into()); - SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 - - let root_stake = 2_000_000_000u64; - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &neuron_hotkey, - &staker_coldkey, - NetUid::ROOT, - root_stake.into(), - ); - - let initial_total_hotkey_alpha = 10_000_000_000u64; - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &neuron_hotkey, - &owner_coldkey, - netuid_1, - initial_total_hotkey_alpha.into(), - ); - - // Distribute pending root alpha - let pending_root_alpha = 1_000_000_000u64; - SubtensorModule::distribute_emission( - netuid_1, - AlphaBalance::ZERO, - AlphaBalance::ZERO, - pending_root_alpha.into(), - AlphaBalance::ZERO, - ); - - assert_ok!(SubtensorModule::set_root_claim_type( - RuntimeOrigin::signed(staker_coldkey), - RootClaimTypeEnum::Keep - )); - assert_ok!(SubtensorModule::claim_root( - RuntimeOrigin::signed(staker_coldkey), - BTreeSet::from([netuid_1]) - )); - - let claimable = RootClaimable::::get(neuron_hotkey) - .get(&netuid_1) - .copied(); - assert!(claimable.is_some()); - let claimable = claimable.unwrap_or_default(); - - assert!(claimable > 0); - - let claimed = RootClaimed::::get((netuid_1, &neuron_hotkey, &staker_coldkey)); - assert!(claimed > 0u128); - - step_block(20); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(owner_coldkey), - &neuron_hotkey, - &neuron_hotkey_new, - Some(NetUid::ROOT), - false - )); - - // Claimable and claimed should be transferred to new hotkey - assert_eq!( - RootClaimable::::get(neuron_hotkey_new) - .get(&netuid_1) - .copied(), - Some(claimable) - ); - assert_eq!( - RootClaimed::::get((netuid_1, &neuron_hotkey_new, &staker_coldkey,)), - claimed - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_hotkey_root_claims_changed_if_all_subnets --exact --nocapture -#[test] -fn test_swap_hotkey_root_claims_changed_if_all_subnets() { - new_test_ext(1).execute_with(|| { - let owner_coldkey = U256::from(1001); - let neuron_hotkey = U256::from(1004); - let neuron_hotkey_new = U256::from(1005); - - let staker_coldkey = U256::from(1006); - - // Ensure ROOT network is registered for all-subnets swap - SubtokenEnabled::::insert(NetUid::ROOT, true); - NetworksAdded::::insert(NetUid::ROOT, true); - - // Use neuron_hotkey as subnet creator so it receives root dividends - let netuid_1 = add_dynamic_network(&neuron_hotkey, &owner_coldkey); - - add_balance_to_coldkey_account(&owner_coldkey, 20_000_000_000_000_000_u64.into()); - SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 - - let root_stake = 2_000_000_000u64; - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &neuron_hotkey, - &staker_coldkey, - NetUid::ROOT, - root_stake.into(), - ); - - let initial_total_hotkey_alpha = 10_000_000_000u64; - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &neuron_hotkey, - &owner_coldkey, - netuid_1, - initial_total_hotkey_alpha.into(), - ); - - // Distribute pending root alpha - let pending_root_alpha = 1_000_000_000u64; - SubtensorModule::distribute_emission( - netuid_1, - AlphaBalance::ZERO, - AlphaBalance::ZERO, - pending_root_alpha.into(), - AlphaBalance::ZERO, - ); - - assert_ok!(SubtensorModule::set_root_claim_type( - RuntimeOrigin::signed(staker_coldkey), - RootClaimTypeEnum::Keep - )); - assert_ok!(SubtensorModule::claim_root( - RuntimeOrigin::signed(staker_coldkey), - BTreeSet::from([netuid_1]) - )); - - let claimable = RootClaimable::::get(neuron_hotkey) - .get(&netuid_1) - .copied(); - assert!(claimable.is_some()); - let claimable = claimable.unwrap_or_default(); - - assert!(claimable > 0); - - let claimed = RootClaimed::::get((netuid_1, &neuron_hotkey, &staker_coldkey)); - assert!(claimed > 0u128); - - step_block(20); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(owner_coldkey), - &neuron_hotkey, - &neuron_hotkey_new, - None, - false - )); - - // Claimable and claimed should be transferred to new hotkey - assert_eq!( - RootClaimable::::get(neuron_hotkey_new) - .get(&netuid_1) - .copied(), - Some(claimable) - ); - assert_eq!( - RootClaimed::::get((netuid_1, &neuron_hotkey_new, &staker_coldkey,)), - claimed - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_hotkey_auto_parent_delegation_transferred_on_root --exact --nocapture -#[test] -fn test_swap_hotkey_auto_parent_delegation_transferred_on_root() { - new_test_ext(1).execute_with(|| { - let owner_coldkey = U256::from(1001); - let old_hotkey = U256::from(1004); - let new_hotkey = U256::from(1005); - - let _ = add_dynamic_network(&old_hotkey, &owner_coldkey); - NetworksAdded::::insert(NetUid::ROOT, true); - add_balance_to_coldkey_account(&owner_coldkey, 20_000_000_000_000_000_u64.into()); - - // Opt out of auto parent delegation on the old hotkey. - AutoParentDelegationEnabled::::insert(old_hotkey, false); - assert!(AutoParentDelegationEnabled::::contains_key( - old_hotkey - )); - assert!(!AutoParentDelegationEnabled::::get(old_hotkey)); - - step_block(20); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(owner_coldkey), - &old_hotkey, - &new_hotkey, - Some(NetUid::ROOT), - false - )); - - // Flag is moved to the new hotkey, cleared from the old one. - assert!(!AutoParentDelegationEnabled::::contains_key( - old_hotkey - )); - assert!(AutoParentDelegationEnabled::::contains_key( - new_hotkey - )); - assert!(!AutoParentDelegationEnabled::::get(new_hotkey)); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_hotkey_auto_parent_delegation_transferred_on_all_subnets --exact --nocapture -#[test] -fn test_swap_hotkey_auto_parent_delegation_transferred_on_all_subnets() { - new_test_ext(1).execute_with(|| { - let owner_coldkey = U256::from(1001); - let old_hotkey = U256::from(1004); - let new_hotkey = U256::from(1005); - - SubtokenEnabled::::insert(NetUid::ROOT, true); - NetworksAdded::::insert(NetUid::ROOT, true); - - let _ = add_dynamic_network(&old_hotkey, &owner_coldkey); - add_balance_to_coldkey_account(&owner_coldkey, 20_000_000_000_000_000_u64.into()); - - AutoParentDelegationEnabled::::insert(old_hotkey, false); - - step_block(20); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(owner_coldkey), - &old_hotkey, - &new_hotkey, - None, - false - )); - - assert!(!AutoParentDelegationEnabled::::contains_key( - old_hotkey - )); - assert!(AutoParentDelegationEnabled::::contains_key( - new_hotkey - )); - assert!(!AutoParentDelegationEnabled::::get(new_hotkey)); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_hotkey_auto_parent_delegation_not_transferred_on_non_root --exact --nocapture -#[test] -fn test_swap_hotkey_auto_parent_delegation_not_transferred_on_non_root() { - new_test_ext(1).execute_with(|| { - let owner_coldkey = U256::from(1001); - let old_hotkey = U256::from(1004); - let new_hotkey = U256::from(1005); - - let netuid = add_dynamic_network(&old_hotkey, &owner_coldkey); - add_balance_to_coldkey_account(&owner_coldkey, 20_000_000_000_000_000_u64.into()); - - AutoParentDelegationEnabled::::insert(old_hotkey, false); - - System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); - assert_ok!(SubtensorModule::do_swap_hotkey( - RuntimeOrigin::signed(owner_coldkey), - &old_hotkey, - &new_hotkey, - Some(netuid), - false - )); - - // Non-root subnet swap must not move the flag. - assert!(AutoParentDelegationEnabled::::contains_key( - old_hotkey - )); - assert!(!AutoParentDelegationEnabled::::get(old_hotkey)); - assert!(!AutoParentDelegationEnabled::::contains_key( - new_hotkey - )); - }); -} diff --git a/pallets/subtensor/src/tests/swap_hotkey_with_subnet/membership_serve.rs b/pallets/subtensor/src/tests/swap_hotkey_with_subnet/membership_serve.rs new file mode 100644 index 0000000000..c55e2d59cf --- /dev/null +++ b/pallets/subtensor/src/tests/swap_hotkey_with_subnet/membership_serve.rs @@ -0,0 +1,255 @@ +#![allow(unused, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used)] + +use approx::assert_abs_diff_eq; +use codec::Encode; +use frame_support::weights::Weight; +use frame_support::{assert_err, assert_noop, assert_ok}; +use frame_system::{Config, RawOrigin}; +use subtensor_runtime_common::{AlphaBalance, NetUidStorageIndex, TaoBalance, Token}; + +use super::super::mock::*; +use crate::*; +use share_pool::SafeFloat; +use sp_core::{Get, H160, H256, U256}; +use sp_runtime::{PerU16, SaturatedConversion}; +use sp_std::collections::vec_deque::VecDeque; +use std::collections::BTreeSet; +use substrate_fixed::types::{I96F32, U64F64}; + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_subnet_membership --exact --nocapture +#[test] +fn test_swap_subnet_membership() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + IsNetworkMember::::insert(old_hotkey, netuid, true); + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + )); + + assert!(!IsNetworkMember::::contains_key(old_hotkey, netuid)); + assert!(IsNetworkMember::::get(new_hotkey, netuid)); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_uids_and_keys --exact --nocapture +#[test] +fn test_swap_uids_and_keys() { + new_test_ext(1).execute_with(|| { + let uid = 5u16; + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + IsNetworkMember::::insert(old_hotkey, netuid, true); + Uids::::insert(netuid, old_hotkey, uid); + Keys::::insert(netuid, uid, old_hotkey); + + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + )); + + assert_eq!(Uids::::get(netuid, old_hotkey), None); + assert_eq!(Uids::::get(netuid, new_hotkey), Some(uid)); + assert_eq!(Keys::::get(netuid, uid), new_hotkey); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_prometheus --exact --nocapture +#[test] +fn test_swap_prometheus() { + new_test_ext(1).execute_with(|| { + let uid = 5u16; + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + + let prometheus_info = PrometheusInfo::default(); + + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + IsNetworkMember::::insert(old_hotkey, netuid, true); + Prometheus::::insert(netuid, old_hotkey, prometheus_info.clone()); + + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + )); + + assert!(!Prometheus::::contains_key(netuid, old_hotkey)); + assert_eq!( + Prometheus::::get(netuid, new_hotkey), + Some(prometheus_info) + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_axons --exact --nocapture +#[test] +fn test_swap_axons() { + new_test_ext(1).execute_with(|| { + let uid = 5u16; + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + + let axon_info = AxonInfo::default(); + + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + IsNetworkMember::::insert(old_hotkey, netuid, true); + Axons::::insert(netuid, old_hotkey, axon_info.clone()); + + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + )); + + assert!(!Axons::::contains_key(netuid, old_hotkey)); + assert_eq!(Axons::::get(netuid, new_hotkey), Some(axon_info)); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_certificates --exact --nocapture +#[test] +fn test_swap_certificates() { + new_test_ext(1).execute_with(|| { + let uid = 5u16; + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + + let certificate = NeuronCertificate::try_from(vec![1, 2, 3]).unwrap(); + + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + IsNetworkMember::::insert(old_hotkey, netuid, true); + NeuronCertificates::::insert(netuid, old_hotkey, certificate.clone()); + + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + )); + + assert!(!NeuronCertificates::::contains_key( + netuid, old_hotkey + )); + assert_eq!( + NeuronCertificates::::get(netuid, new_hotkey), + Some(certificate) + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_weight_commits --exact --nocapture +#[test] +fn test_swap_weight_commits() { + new_test_ext(1).execute_with(|| { + let uid = 5u16; + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + + let mut weight_commits: VecDeque<(H256, u64, u64, u64)> = VecDeque::new(); + weight_commits.push_back((H256::from_low_u64_be(100), 200, 1, 1)); + + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + IsNetworkMember::::insert(old_hotkey, netuid, true); + WeightCommits::::insert( + NetUidStorageIndex::from(netuid), + old_hotkey, + weight_commits.clone(), + ); + + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + )); + + assert!(!WeightCommits::::contains_key( + NetUidStorageIndex::from(netuid), + old_hotkey + )); + assert_eq!( + WeightCommits::::get(NetUidStorageIndex::from(netuid), new_hotkey), + Some(weight_commits) + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_loaded_emission --exact --nocapture +#[test] +fn test_swap_loaded_emission() { + new_test_ext(1).execute_with(|| { + let uid = 5u16; + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + + let server_emission = 1000u64; + let validator_emission = 1000u64; + + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + IsNetworkMember::::insert(old_hotkey, netuid, true); + LoadedEmission::::insert( + netuid, + vec![(old_hotkey, server_emission, validator_emission)], + ); + + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + )); + + let new_loaded_emission = LoadedEmission::::get(netuid); + assert_eq!( + new_loaded_emission, + Some(vec![(new_hotkey, server_emission, validator_emission)]) + ); + }); +} diff --git a/pallets/subtensor/src/tests/swap_hotkey_with_subnet/mod.rs b/pallets/subtensor/src/tests/swap_hotkey_with_subnet/mod.rs new file mode 100644 index 0000000000..35507219f1 --- /dev/null +++ b/pallets/subtensor/src/tests/swap_hotkey_with_subnet/mod.rs @@ -0,0 +1,26 @@ +#![allow(unused, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used)] +//! Integration tests for subnet-scoped hotkey swap ([`crate::swap::swap_hotkey`]). +//! +//! Layout mirrors `swap/swap_hotkey.rs` concepts: ownership, membership/serve +//! metadata, stake transfer, parent/child maps, rate limits, revert paths, and +//! root claims. +//! +//! ## Search anchors +//! +//! | Module | Owns | +//! |--------|------| +//! | [`owner_identity`] | Owner / OwnedHotkeys / Delegates / ownership errors / subnet-owner hotkey | +//! | [`membership_serve`] | membership, UIDs/keys, Prometheus, axons, certificates, weight commits, loaded emission | +//! | [`stake_transfer`] | total stake, staking-hotkey indexes, V1/V2 alpha, keep_stake, multi coldkey/subnet | +//! | [`parent_child_maps`] | ChildKeys / ParentKeys maps and auto parent-delegation | +//! | [`rate_limits`] | `HotkeySwapOnSubnetInterval` / `LastHotkeySwapOnNetuid` | +//! | [`revert_swap`] | swap-back / revert preserves stake, maps, dividends, voting power, claims | +//! | [`root_claims`] | root claim rows transfer on root / all-subnet vs non-root | + +mod membership_serve; +mod owner_identity; +mod parent_child_maps; +mod rate_limits; +mod revert_swap; +mod root_claims; +mod stake_transfer; diff --git a/pallets/subtensor/src/tests/swap_hotkey_with_subnet/owner_identity.rs b/pallets/subtensor/src/tests/swap_hotkey_with_subnet/owner_identity.rs new file mode 100644 index 0000000000..031f1df6e0 --- /dev/null +++ b/pallets/subtensor/src/tests/swap_hotkey_with_subnet/owner_identity.rs @@ -0,0 +1,258 @@ +#![allow(unused, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used)] + +use approx::assert_abs_diff_eq; +use codec::Encode; +use frame_support::weights::Weight; +use frame_support::{assert_err, assert_noop, assert_ok}; +use frame_system::{Config, RawOrigin}; +use subtensor_runtime_common::{AlphaBalance, NetUidStorageIndex, TaoBalance, Token}; + +use super::super::mock::*; +use crate::*; +use share_pool::SafeFloat; +use sp_core::{Get, H160, H256, U256}; +use sp_runtime::{PerU16, SaturatedConversion}; +use std::collections::BTreeSet; +use substrate_fixed::types::{I96F32, U64F64}; + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_owner --exact --nocapture +#[test] +fn test_swap_owner() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + Owner::::insert(old_hotkey, coldkey); + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false, + )); + + assert_eq!(Owner::::get(old_hotkey), coldkey); + assert_eq!(Owner::::get(new_hotkey), coldkey); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_owned_hotkeys --exact --nocapture +#[test] +fn test_swap_owned_hotkeys() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + OwnedHotkeys::::insert(coldkey, vec![old_hotkey]); + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + )); + + let hotkeys = OwnedHotkeys::::get(coldkey); + assert!(hotkeys.contains(&old_hotkey)); + assert!(hotkeys.contains(&new_hotkey)); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_delegates --exact --nocapture +#[test] +fn test_swap_delegates() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + Delegates::::insert(old_hotkey, PerU16::from_parts(100)); + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + )); + + assert!(Delegates::::contains_key(old_hotkey)); + assert!(!Delegates::::contains_key(new_hotkey)); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_do_swap_hotkey_err_not_owner --exact --nocapture +#[test] +fn test_do_swap_hotkey_err_not_owner() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let tempo: u16 = 13; + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let not_owner_coldkey = U256::from(4); + let swap_cost = TaoBalance::from(1_000_000_000u64); + + // Setup initial state + add_network(netuid, tempo, 0); + register_ok_neuron(netuid, old_hotkey, coldkey, 0); + add_balance_to_coldkey_account(¬_owner_coldkey, swap_cost); + + // Attempt the swap with a non-owner coldkey + assert_err!( + SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(not_owner_coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + ), + Error::::NonAssociatedColdKey + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_owner_old_hotkey_not_exist --exact --nocapture +#[test] +fn test_swap_owner_old_hotkey_not_exist() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + + let netuid = add_dynamic_network(&new_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + // Ensure old_hotkey does not exist + assert!(!Owner::::contains_key(old_hotkey)); + + // Perform the swap + assert_err!( + SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + ), + Error::::NonAssociatedColdKey + ); + + // Verify the swap + assert_eq!(Owner::::get(new_hotkey), coldkey); + assert!(!Owner::::contains_key(old_hotkey)); + }); +} + +// SKIP_WASM_BUILD=1 cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_owner_new_hotkey_owned_by_another_coldkey --exact --nocapture +#[test] +fn test_swap_owner_new_hotkey_owned_by_another_coldkey() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let another_coldkey = U256::from(4); + + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + // new_hotkey already exists globally and is owned by a foreign coldkey, + // so the new-hotkey ownership check must reject the swap. + Owner::::insert(new_hotkey, another_coldkey); + + // Perform the swap + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_err!( + SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + ), + Error::::NonAssociatedColdKey + ); + + // Verify the swap + assert_eq!(Owner::::get(old_hotkey), coldkey); + assert!(Owner::::contains_key(old_hotkey)); + }); +} + +// SKIP_WASM_BUILD=1 cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_owner_new_hotkey_already_exists --exact --nocapture +#[test] +fn test_swap_owner_new_hotkey_already_exists() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + + let netuid = add_dynamic_network(&new_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + // old_hotkey is owned by coldkey; new_hotkey was already registered on `netuid` + // by add_dynamic_network (the condition under test). Do NOT reassign new_hotkey to + // a foreign coldkey — the new_hotkey-ownership check (NonAssociatedColdKey) would + // then fire before the already-registered-in-subnet check this test targets. + Owner::::insert(old_hotkey, coldkey); + + // Perform the swap + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_err!( + SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + ), + Error::::HotKeyAlreadyRegisteredInSubNet + ); + + // Verify the swap + assert_eq!(Owner::::get(old_hotkey), coldkey); + assert!(Owner::::contains_key(old_hotkey)); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_hotkey_is_sn_owner_hotkey --exact --nocapture +#[test] +fn test_swap_hotkey_is_sn_owner_hotkey() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + + // Create dynamic network + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + // Check for SubnetOwnerHotkey + assert_eq!(SubnetOwnerHotkey::::get(netuid), old_hotkey); + + // Perform the swap + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + ),); + + // Check for SubnetOwnerHotkey + assert_eq!(SubnetOwnerHotkey::::get(netuid), new_hotkey); + }); +} diff --git a/pallets/subtensor/src/tests/swap_hotkey_with_subnet/parent_child_maps.rs b/pallets/subtensor/src/tests/swap_hotkey_with_subnet/parent_child_maps.rs new file mode 100644 index 0000000000..a7e297dacb --- /dev/null +++ b/pallets/subtensor/src/tests/swap_hotkey_with_subnet/parent_child_maps.rs @@ -0,0 +1,469 @@ +#![allow(unused, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used)] + +use approx::assert_abs_diff_eq; +use codec::Encode; +use frame_support::weights::Weight; +use frame_support::{assert_err, assert_noop, assert_ok}; +use frame_system::{Config, RawOrigin}; +use subtensor_runtime_common::{AlphaBalance, NetUidStorageIndex, TaoBalance, Token}; + +use super::super::mock::*; +use crate::*; +use share_pool::SafeFloat; +use sp_core::{Get, H160, H256, U256}; +use sp_runtime::{PerU16, SaturatedConversion}; +use std::collections::BTreeSet; +use substrate_fixed::types::{I96F32, U64F64}; + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_child_keys --exact --nocapture +#[test] +fn test_swap_child_keys() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + let children = vec![(100u64, U256::from(4)), (200u64, U256::from(5))]; + + // Initialize ChildKeys for old_hotkey + ChildKeys::::insert(old_hotkey, netuid, children.clone()); + + // Perform the swap + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + ),); + + // Verify the swap + assert_eq!(ChildKeys::::get(new_hotkey, netuid), children); + assert!(ChildKeys::::get(old_hotkey, netuid).is_empty()); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_child_keys_self_loop --exact --show-output +#[test] +#[allow(deprecated)] +fn test_swap_child_keys_self_loop() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + let amount = AlphaBalance::from(12345); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + // Only for checking + TotalHotkeyAlpha::::insert(old_hotkey, netuid, AlphaBalance::from(amount)); + + let children = vec![(200u64, new_hotkey)]; + + // Initialize ChildKeys for old_hotkey + ChildKeys::::insert(old_hotkey, netuid, children.clone()); + + // Perform the swap extrinsic + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_err!( + SubtensorModule::swap_hotkey( + RuntimeOrigin::signed(coldkey), + old_hotkey, + new_hotkey, + Some(netuid), + ), + Error::::InvalidChild + ); + + // Verify the swap didn't happen + assert_eq!(ChildKeys::::get(old_hotkey, netuid), children); + assert!(ChildKeys::::get(new_hotkey, netuid).is_empty()); + assert_eq!(TotalHotkeyAlpha::::get(old_hotkey, netuid), amount); + assert_eq!( + TotalHotkeyAlpha::::get(new_hotkey, netuid), + AlphaBalance::from(0) + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_parent_keys --exact --nocapture +#[test] +fn test_swap_parent_keys() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + let parents = vec![(100u64, U256::from(4)), (200u64, U256::from(5))]; + + // Initialize ParentKeys for old_hotkey + ParentKeys::::insert(old_hotkey, netuid, parents.clone()); + + // Initialize ChildKeys for parent + ChildKeys::::insert(U256::from(4), netuid, vec![(100u64, old_hotkey)]); + ChildKeys::::insert(U256::from(5), netuid, vec![(200u64, old_hotkey)]); + + // Perform the swap + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + ),); + + // Verify ParentKeys swap + assert_eq!(ParentKeys::::get(new_hotkey, netuid), parents); + assert!(ParentKeys::::get(old_hotkey, netuid).is_empty()); + + // Verify ChildKeys update for parents + assert_eq!( + ChildKeys::::get(U256::from(4), netuid), + vec![(100u64, new_hotkey)] + ); + assert_eq!( + ChildKeys::::get(U256::from(5), netuid), + vec![(200u64, new_hotkey)] + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_multiple_subnets --exact --nocapture +#[test] +fn test_swap_multiple_subnets() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let new_hotkey_2 = U256::from(3); + let coldkey = U256::from(4); + let netuid1 = add_dynamic_network(&old_hotkey, &coldkey); + let netuid2 = add_dynamic_network(&old_hotkey, &coldkey); + + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + let children1 = vec![(100u64, U256::from(4)), (200u64, U256::from(5))]; + let children2 = vec![(300u64, U256::from(6))]; + + // Initialize ChildKeys for old_hotkey in multiple subnets + ChildKeys::::insert(old_hotkey, netuid1, children1.clone()); + ChildKeys::::insert(old_hotkey, netuid2, children2.clone()); + + // Perform the swap + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid1), + false + ),); + + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey_2, + Some(netuid2), + false + ),); + + // Verify the swap for both subnets + assert_eq!(ChildKeys::::get(new_hotkey, netuid1), children1); + assert_eq!(ChildKeys::::get(new_hotkey_2, netuid2), children2); + assert!(ChildKeys::::get(old_hotkey, netuid1).is_empty()); + assert!(ChildKeys::::get(old_hotkey, netuid2).is_empty()); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_complex_parent_child_structure --exact --nocapture +#[test] +fn test_swap_complex_parent_child_structure() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + let parent1 = U256::from(4); + let parent2 = U256::from(5); + let child1 = U256::from(6); + let child2 = U256::from(7); + + // Set up complex parent-child structure + ParentKeys::::insert( + old_hotkey, + netuid, + vec![(100u64, parent1), (200u64, parent2)], + ); + ChildKeys::::insert(old_hotkey, netuid, vec![(300u64, child1), (400u64, child2)]); + ChildKeys::::insert( + parent1, + netuid, + vec![(100u64, old_hotkey), (500u64, U256::from(8))], + ); + ChildKeys::::insert( + parent2, + netuid, + vec![(200u64, old_hotkey), (600u64, U256::from(9))], + ); + + // Perform the swap + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + ),); + + // Verify ParentKeys swap + assert_eq!( + ParentKeys::::get(new_hotkey, netuid), + vec![(100u64, parent1), (200u64, parent2)] + ); + assert!(ParentKeys::::get(old_hotkey, netuid).is_empty()); + + // Verify ChildKeys swap + assert_eq!( + ChildKeys::::get(new_hotkey, netuid), + vec![(300u64, child1), (400u64, child2)] + ); + assert!(ChildKeys::::get(old_hotkey, netuid).is_empty()); + + // Verify parent's ChildKeys update + assert!(ChildKeys::::get(parent1, netuid).contains(&(500u64, U256::from(8))),); + assert!(ChildKeys::::get(parent1, netuid).contains(&(100u64, new_hotkey)),); + assert!(ChildKeys::::get(parent2, netuid).contains(&(600u64, U256::from(9))),); + assert!(ChildKeys::::get(parent2, netuid).contains(&(200u64, new_hotkey)),); + }); +} + +#[test] +fn test_swap_parent_hotkey_childkey_maps() { + new_test_ext(1).execute_with(|| { + let parent_old = U256::from(1); + let coldkey = U256::from(2); + let child = U256::from(3); + let child_other = U256::from(4); + let parent_new = U256::from(5); + + let netuid = add_dynamic_network(&parent_old, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + SubtensorModule::create_account_if_non_existent(&coldkey, &parent_old); + + // Set child and verify state maps + mock_set_children(&coldkey, &parent_old, netuid, &[(u64::MAX, child)]); + // Wait rate limit + step_rate_limit(&TransactionType::SetChildren, netuid); + // Schedule some pending child keys. + mock_schedule_children(&coldkey, &parent_old, netuid, &[(u64::MAX, child_other)]); + + assert_eq!( + ParentKeys::::get(child, netuid), + vec![(u64::MAX, parent_old)] + ); + assert_eq!( + ChildKeys::::get(parent_old, netuid), + vec![(u64::MAX, child)] + ); + let existing_pending_child_keys = PendingChildKeys::::get(netuid, parent_old); + assert_eq!(existing_pending_child_keys.0, vec![(u64::MAX, child_other)]); + + // Swap + + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &parent_old, + &parent_new, + Some(netuid), + false + ),); + + // Verify parent and child keys updates + assert_eq!( + ParentKeys::::get(child, netuid), + vec![(u64::MAX, parent_new)] + ); + assert_eq!( + ChildKeys::::get(parent_new, netuid), + vec![(u64::MAX, child)] + ); + assert_eq!( + PendingChildKeys::::get(netuid, parent_new), + existing_pending_child_keys // Entry under new hotkey. + ); + }) +} + +#[test] +fn test_swap_child_hotkey_childkey_maps() { + new_test_ext(1).execute_with(|| { + let parent = U256::from(1); + let coldkey = U256::from(2); + let child_old = U256::from(3); + let child_new = U256::from(4); + let netuid = add_dynamic_network(&child_old, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + SubtensorModule::create_account_if_non_existent(&coldkey, &child_old); + SubtensorModule::create_account_if_non_existent(&coldkey, &parent); + + // Set child and verify state maps + mock_set_children(&coldkey, &parent, netuid, &[(u64::MAX, child_old)]); + // Wait rate limit + step_rate_limit(&TransactionType::SetChildren, netuid); + // Schedule some pending child keys. + mock_schedule_children(&coldkey, &parent, netuid, &[(u64::MAX, child_old)]); + + assert_eq!( + ParentKeys::::get(child_old, netuid), + vec![(u64::MAX, parent)] + ); + assert_eq!( + ChildKeys::::get(parent, netuid), + vec![(u64::MAX, child_old)] + ); + let existing_pending_child_keys = PendingChildKeys::::get(netuid, parent); + assert_eq!(existing_pending_child_keys.0, vec![(u64::MAX, child_old)]); + + // Swap + + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &child_old, + &child_new, + Some(netuid), + false + ),); + + // Verify parent and child keys updates + assert_eq!( + ParentKeys::::get(child_new, netuid), + vec![(u64::MAX, parent)] + ); + assert_eq!( + ChildKeys::::get(parent, netuid), + vec![(u64::MAX, child_new)] + ); + assert_eq!( + PendingChildKeys::::get(netuid, parent), + (vec![(u64::MAX, child_new)], existing_pending_child_keys.1) // Same cooldown block. + ); + }) +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_hotkey_auto_parent_delegation_transferred_on_root --exact --nocapture +#[test] +fn test_swap_hotkey_auto_parent_delegation_transferred_on_root() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(1001); + let old_hotkey = U256::from(1004); + let new_hotkey = U256::from(1005); + + let _ = add_dynamic_network(&old_hotkey, &owner_coldkey); + NetworksAdded::::insert(NetUid::ROOT, true); + add_balance_to_coldkey_account(&owner_coldkey, 20_000_000_000_000_000_u64.into()); + + // Opt out of auto parent delegation on the old hotkey. + AutoParentDelegationEnabled::::insert(old_hotkey, false); + assert!(AutoParentDelegationEnabled::::contains_key( + old_hotkey + )); + assert!(!AutoParentDelegationEnabled::::get(old_hotkey)); + + step_block(20); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(owner_coldkey), + &old_hotkey, + &new_hotkey, + Some(NetUid::ROOT), + false + )); + + // Flag is moved to the new hotkey, cleared from the old one. + assert!(!AutoParentDelegationEnabled::::contains_key( + old_hotkey + )); + assert!(AutoParentDelegationEnabled::::contains_key( + new_hotkey + )); + assert!(!AutoParentDelegationEnabled::::get(new_hotkey)); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_hotkey_auto_parent_delegation_transferred_on_all_subnets --exact --nocapture +#[test] +fn test_swap_hotkey_auto_parent_delegation_transferred_on_all_subnets() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(1001); + let old_hotkey = U256::from(1004); + let new_hotkey = U256::from(1005); + + SubtokenEnabled::::insert(NetUid::ROOT, true); + NetworksAdded::::insert(NetUid::ROOT, true); + + let _ = add_dynamic_network(&old_hotkey, &owner_coldkey); + add_balance_to_coldkey_account(&owner_coldkey, 20_000_000_000_000_000_u64.into()); + + AutoParentDelegationEnabled::::insert(old_hotkey, false); + + step_block(20); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(owner_coldkey), + &old_hotkey, + &new_hotkey, + None, + false + )); + + assert!(!AutoParentDelegationEnabled::::contains_key( + old_hotkey + )); + assert!(AutoParentDelegationEnabled::::contains_key( + new_hotkey + )); + assert!(!AutoParentDelegationEnabled::::get(new_hotkey)); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_hotkey_auto_parent_delegation_not_transferred_on_non_root --exact --nocapture +#[test] +fn test_swap_hotkey_auto_parent_delegation_not_transferred_on_non_root() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(1001); + let old_hotkey = U256::from(1004); + let new_hotkey = U256::from(1005); + + let netuid = add_dynamic_network(&old_hotkey, &owner_coldkey); + add_balance_to_coldkey_account(&owner_coldkey, 20_000_000_000_000_000_u64.into()); + + AutoParentDelegationEnabled::::insert(old_hotkey, false); + + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(owner_coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + )); + + // Non-root subnet swap must not move the flag. + assert!(AutoParentDelegationEnabled::::contains_key( + old_hotkey + )); + assert!(!AutoParentDelegationEnabled::::get(old_hotkey)); + assert!(!AutoParentDelegationEnabled::::contains_key( + new_hotkey + )); + }); +} diff --git a/pallets/subtensor/src/tests/swap_hotkey_with_subnet/rate_limits.rs b/pallets/subtensor/src/tests/swap_hotkey_with_subnet/rate_limits.rs new file mode 100644 index 0000000000..6d9c8081bb --- /dev/null +++ b/pallets/subtensor/src/tests/swap_hotkey_with_subnet/rate_limits.rs @@ -0,0 +1,139 @@ +#![allow(unused, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used)] + +use approx::assert_abs_diff_eq; +use codec::Encode; +use frame_support::weights::Weight; +use frame_support::{assert_err, assert_noop, assert_ok}; +use frame_system::{Config, RawOrigin}; +use subtensor_runtime_common::{AlphaBalance, NetUidStorageIndex, TaoBalance, Token}; + +use super::super::mock::*; +use crate::*; +use share_pool::SafeFloat; +use sp_core::{Get, H160, H256, U256}; +use sp_runtime::{PerU16, SaturatedConversion}; +use std::collections::BTreeSet; +use substrate_fixed::types::{I96F32, U64F64}; + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_hotkey_swap_rate_limits --exact --nocapture +#[test] +fn test_swap_hotkey_swap_rate_limits() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + + let delegate_take_block = 4567; + let child_key_take_block = 8910; + + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + // Set the last delegate take block for the old hotkey + SubtensorModule::set_last_tx_block_delegate_take(&old_hotkey, delegate_take_block); + // Set last childkey take block for the old hotkey + SubtensorModule::set_last_tx_block_childkey(&old_hotkey, child_key_take_block); + + // Perform the swap + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + ),); + + // Check for new hotkey (LastTxBlock is no longer transferred: the generic tx rate + // limit was removed. + assert_eq!( + SubtensorModule::get_last_tx_block_delegate_take(&new_hotkey), + delegate_take_block + ); + assert_eq!( + SubtensorModule::get_last_tx_block_childkey_take(&new_hotkey), + child_key_take_block + ); + }); +} + +#[test] +fn test_swap_owner_failed_interval_not_passed() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + Owner::::insert(old_hotkey, coldkey); + assert_err!( + SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + ), + Error::::HotKeySwapOnSubnetIntervalNotPassed, + ); + }); +} + +#[test] +fn test_swap_owner_check_swap_block_set() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + Owner::::insert(old_hotkey, coldkey); + let new_block_number = System::block_number() + HotkeySwapOnSubnetInterval::get(); + System::set_block_number(new_block_number); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + )); + + assert_eq!( + LastHotkeySwapOnNetuid::::get(netuid, coldkey), + new_block_number + ); + }); +} + +#[test] +fn test_swap_owner_check_swap_record_clean_up() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + Owner::::insert(old_hotkey, coldkey); + let new_block_number = System::block_number() + HotkeySwapOnSubnetInterval::get(); + System::set_block_number(new_block_number); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + )); + + assert_eq!( + LastHotkeySwapOnNetuid::::get(netuid, coldkey), + new_block_number + ); + + step_block((HotkeySwapOnSubnetInterval::get() as u16 + u16::from(netuid)) * 2); + assert!(!LastHotkeySwapOnNetuid::::contains_key( + netuid, coldkey + )); + }); +} diff --git a/pallets/subtensor/src/tests/swap_hotkey_with_subnet/revert_swap.rs b/pallets/subtensor/src/tests/swap_hotkey_with_subnet/revert_swap.rs new file mode 100644 index 0000000000..b321cc0c98 --- /dev/null +++ b/pallets/subtensor/src/tests/swap_hotkey_with_subnet/revert_swap.rs @@ -0,0 +1,959 @@ +#![allow(unused, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used)] + +use approx::assert_abs_diff_eq; +use codec::Encode; +use frame_support::weights::Weight; +use frame_support::{assert_err, assert_noop, assert_ok}; +use frame_system::{Config, RawOrigin}; +use subtensor_runtime_common::{AlphaBalance, NetUidStorageIndex, TaoBalance, Token}; + +use super::super::mock::*; +use crate::*; +use share_pool::SafeFloat; +use sp_core::{Get, H160, H256, U256}; +use sp_runtime::{PerU16, SaturatedConversion}; +use std::collections::BTreeSet; +use substrate_fixed::types::{I96F32, U64F64}; + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_revert_hotkey_swap_stake_is_not_lost --exact --nocapture +#[test] +fn test_revert_hotkey_swap_stake_is_not_lost() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let netuid2 = NetUid::from(2); + let tempo: u16 = 13; + let hk1 = U256::from(1); + let hk2 = U256::from(2); + let coldkey = U256::from(3); + let swap_cost = 1_000_000_000u64 * 2; + let stake2 = 1_000_000_000u64; + + // Setup + add_network(netuid, tempo, 0); + add_network(netuid2, tempo, 0); + register_ok_neuron(netuid, hk1, coldkey, 0); + register_ok_neuron(netuid2, hk1, coldkey, 0); + add_balance_to_coldkey_account(&coldkey, swap_cost.into()); + + let hk1_stake_before_increase = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey, netuid); + assert!( + hk1_stake_before_increase == 0.into(), + "hk1 should have empty stake" + ); + + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hk1, + &coldkey, + netuid, + 1_000_000_000u64.into(), + ); + + let hk1_stake_before_swap = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey, netuid); + assert!( + hk1_stake_before_swap == 1_000_000_000.into(), + "hk1 should have stake before swap" + ); + + step_block(20); + + assert_ok!(SubtensorModule::perform_hotkey_swap( + <::RuntimeOrigin>::signed(coldkey), + &hk1, + &hk2, + Some(netuid), + false + )); + + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hk1, + &coldkey, + netuid, + stake2.into(), + ); + + step_block(20); + + let hk2_stake_before_revert = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk2, &coldkey, netuid); + let hk1_stake_before_revert = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey, netuid); + + assert_eq!(hk1_stake_before_revert, stake2.into()); + + // Revert: hk2 -> hk1 + assert_ok!(SubtensorModule::perform_hotkey_swap( + <::RuntimeOrigin>::signed(coldkey), + &hk2, + &hk1, + Some(netuid), + false + )); + + let hk1_stake_after_revert = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey, netuid); + let hk2_stake_after_revert = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk2, &coldkey, netuid); + + assert_eq!( + hk1_stake_after_revert, + hk2_stake_before_revert + stake2.into(), + ); + + // hk2 should be empty + assert_eq!( + hk2_stake_after_revert, + 0.into(), + "hk2 should have no stake after revert" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_revert_hotkey_swap --exact --nocapture +// This test confirms, that the old hotkey can be reverted after the hotkey swap +#[test] +fn test_revert_hotkey_swap() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let netuid2 = NetUid::from(2); + let tempo: u16 = 13; + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let swap_cost = 1_000_000_000u64 * 2; + + // Setup initial state + add_network(netuid, tempo, 0); + add_network(netuid2, tempo, 0); + register_ok_neuron(netuid, old_hotkey, coldkey, 0); + register_ok_neuron(netuid2, old_hotkey, coldkey, 0); + add_balance_to_coldkey_account(&coldkey, swap_cost.into()); + step_block(20); + + // Perform the first swap (only on netuid) + assert_ok!(SubtensorModule::perform_hotkey_swap( + <::RuntimeOrigin>::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + )); + + assert!(SubtensorModule::is_hotkey_registered_on_any_network( + &old_hotkey + )); + + step_block(20); + + assert_ok!(SubtensorModule::perform_hotkey_swap( + <::RuntimeOrigin>::signed(coldkey), + &new_hotkey, + &old_hotkey, + Some(netuid), + false + )); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_revert_hotkey_swap_parent_hotkey_childkey_maps --exact --nocapture +#[test] +fn test_revert_hotkey_swap_parent_hotkey_childkey_maps() { + new_test_ext(1).execute_with(|| { + let hk1 = U256::from(1); + let coldkey = U256::from(2); + let child = U256::from(3); + let child_other = U256::from(4); + let hk2 = U256::from(5); + + let netuid = add_dynamic_network(&hk1, &coldkey); + let netuid2 = add_dynamic_network(&hk1, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + SubtensorModule::create_account_if_non_existent(&coldkey, &hk1); + + mock_set_children(&coldkey, &hk1, netuid, &[(u64::MAX, child)]); + step_rate_limit(&TransactionType::SetChildren, netuid); + mock_schedule_children(&coldkey, &hk1, netuid, &[(u64::MAX, child_other)]); + + assert_eq!( + ParentKeys::::get(child, netuid), + vec![(u64::MAX, hk1)] + ); + assert_eq!(ChildKeys::::get(hk1, netuid), vec![(u64::MAX, child)]); + let existing_pending_child_keys = PendingChildKeys::::get(netuid, hk1); + assert_eq!(existing_pending_child_keys.0, vec![(u64::MAX, child_other)]); + + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &hk1, + &hk2, + Some(netuid), + false + )); + + assert_eq!( + ParentKeys::::get(child, netuid), + vec![(u64::MAX, hk2)] + ); + assert_eq!(ChildKeys::::get(hk2, netuid), vec![(u64::MAX, child)]); + assert_eq!( + PendingChildKeys::::get(netuid, hk2), + existing_pending_child_keys + ); + assert!(ChildKeys::::get(hk1, netuid).is_empty()); + assert!(PendingChildKeys::::get(netuid, hk1).0.is_empty()); + + // Revert: hk2 -> hk1 + step_block(20); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &hk2, + &hk1, + Some(netuid), + false + )); + + assert_eq!( + ParentKeys::::get(child, netuid), + vec![(u64::MAX, hk1)], + "ParentKeys must point back to hk1 after revert" + ); + assert_eq!( + ChildKeys::::get(hk1, netuid), + vec![(u64::MAX, child)], + "ChildKeys must be restored to hk1 after revert" + ); + assert_eq!( + PendingChildKeys::::get(netuid, hk1), + existing_pending_child_keys, + "PendingChildKeys must be restored to hk1 after revert" + ); + + assert!( + ChildKeys::::get(hk2, netuid).is_empty(), + "hk2 must have no ChildKeys after revert" + ); + assert!( + PendingChildKeys::::get(netuid, hk2).0.is_empty(), + "hk2 must have no PendingChildKeys after revert" + ); + }) +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_revert_hotkey_swap_uids_and_keys --exact --nocapture +#[test] +fn test_revert_hotkey_swap_uids_and_keys() { + new_test_ext(1).execute_with(|| { + let uid = 5u16; + let hk1 = U256::from(1); + let hk2 = U256::from(2); + let coldkey = U256::from(3); + + let netuid = add_dynamic_network(&hk1, &coldkey); + let netuid2 = add_dynamic_network(&hk1, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + IsNetworkMember::::insert(hk1, netuid, true); + Uids::::insert(netuid, hk1, uid); + Keys::::insert(netuid, uid, hk1); + + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &hk1, + &hk2, + Some(netuid), + false + )); + + assert_eq!(Uids::::get(netuid, hk1), None); + assert_eq!(Uids::::get(netuid, hk2), Some(uid)); + assert_eq!(Keys::::get(netuid, uid), hk2); + + // Revert: hk2 -> hk1 + step_block(20); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &hk2, + &hk1, + Some(netuid), + false + )); + + assert_eq!( + Uids::::get(netuid, hk2), + None, + "hk2 must have no uid after revert" + ); + assert_eq!( + Uids::::get(netuid, hk1), + Some(uid), + "hk1 must have its uid restored after revert" + ); + assert_eq!( + Keys::::get(netuid, uid), + hk1, + "Keys must point back to hk1 after revert" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_revert_hotkey_swap_auto_stake_destination --exact --nocapture +#[test] +fn test_revert_hotkey_swap_auto_stake_destination() { + new_test_ext(1).execute_with(|| { + let hk1 = U256::from(1); + let hk2 = U256::from(2); + let coldkey = U256::from(3); + let netuid = NetUid::from(2u16); + let netuid2 = NetUid::from(3u16); + let staker1 = U256::from(4); + let staker2 = U256::from(5); + let coldkeys = vec![staker1, staker2, coldkey]; + + add_network(netuid, 1, 0); + add_network(netuid2, 1, 0); + register_ok_neuron(netuid, hk1, coldkey, 0); + register_ok_neuron(netuid2, hk1, coldkey, 0); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + AutoStakeDestinationColdkeys::::insert(hk1, netuid, coldkeys.clone()); + AutoStakeDestination::::insert(coldkey, netuid, hk1); + AutoStakeDestination::::insert(staker1, netuid, hk1); + AutoStakeDestination::::insert(staker2, netuid, hk1); + + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &hk1, + &hk2, + Some(netuid), + false + )); + + assert_eq!( + AutoStakeDestinationColdkeys::::get(hk2, netuid), + coldkeys + ); + assert!(AutoStakeDestinationColdkeys::::get(hk1, netuid).is_empty()); + assert_eq!( + AutoStakeDestination::::get(coldkey, netuid), + Some(hk2) + ); + assert_eq!( + AutoStakeDestination::::get(staker1, netuid), + Some(hk2) + ); + assert_eq!( + AutoStakeDestination::::get(staker2, netuid), + Some(hk2) + ); + + // Revert: hk2 -> hk1 + step_block(20); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &hk2, + &hk1, + Some(netuid), + false + )); + + assert_eq!( + AutoStakeDestinationColdkeys::::get(hk1, netuid), + coldkeys, + "AutoStakeDestinationColdkeys must be restored to hk1 after revert" + ); + assert!( + AutoStakeDestinationColdkeys::::get(hk2, netuid).is_empty(), + "hk2 must have no AutoStakeDestinationColdkeys after revert" + ); + assert_eq!( + AutoStakeDestination::::get(coldkey, netuid), + Some(hk1), + "coldkey AutoStakeDestination must point back to hk1 after revert" + ); + assert_eq!( + AutoStakeDestination::::get(staker1, netuid), + Some(hk1), + "staker1 AutoStakeDestination must point back to hk1 after revert" + ); + assert_eq!( + AutoStakeDestination::::get(staker2, netuid), + Some(hk1), + "staker2 AutoStakeDestination must point back to hk1 after revert" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_revert_hotkey_swap_subnet_owner --exact --nocapture +#[test] +fn test_revert_hotkey_swap_subnet_owner() { + new_test_ext(1).execute_with(|| { + let hk1 = U256::from(1); + let hk2 = U256::from(2); + let coldkey = U256::from(3); + + let netuid = add_dynamic_network(&hk1, &coldkey); + let netuid2 = add_dynamic_network(&hk1, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + assert_eq!(SubnetOwnerHotkey::::get(netuid), hk1); + + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &hk1, + &hk2, + Some(netuid), + false + )); + + assert_eq!( + SubnetOwnerHotkey::::get(netuid), + hk2, + "hk2 must be subnet owner after swap" + ); + + // Revert: hk2 -> hk1 + step_block(20); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &hk2, + &hk1, + Some(netuid), + false + )); + + assert_eq!( + SubnetOwnerHotkey::::get(netuid), + hk1, + "hk1 must be restored as subnet owner after revert" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_revert_hotkey_swap_dividends --exact --nocapture +#[test] +fn test_revert_hotkey_swap_dividends() { + new_test_ext(1).execute_with(|| { + let hk1 = U256::from(1); + let hk2 = U256::from(2); + let coldkey = U256::from(3); + + let netuid = add_dynamic_network(&hk1, &coldkey); + remove_owner_registration_stake(netuid); + let netuid2 = add_dynamic_network(&hk1, &coldkey); + remove_owner_registration_stake(netuid2); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + let amount = 10_000; + let shares = U64F64::from_num(10_000); + + TotalHotkeyAlpha::::insert(hk1, netuid, AlphaBalance::from(amount)); + TotalHotkeyAlphaLastEpoch::::insert(hk1, netuid, AlphaBalance::from(amount * 2)); + TotalHotkeyShares::::insert(hk1, netuid, U64F64::from_num(shares)); + Alpha::::insert((hk1, coldkey, netuid), U64F64::from_num(amount)); + AlphaDividendsPerSubnet::::insert(netuid, hk1, AlphaBalance::from(amount)); + + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &hk1, + &hk2, + Some(netuid), + false + )); + + assert_eq!( + TotalHotkeyAlpha::::get(hk1, netuid), + AlphaBalance::ZERO + ); + assert_eq!( + TotalHotkeyAlpha::::get(hk2, netuid), + AlphaBalance::from(amount) + ); + assert_eq!( + TotalHotkeyAlphaLastEpoch::::get(hk1, netuid), + AlphaBalance::ZERO + ); + assert_eq!( + TotalHotkeyAlphaLastEpoch::::get(hk2, netuid), + AlphaBalance::from(amount * 2) + ); + assert_eq!( + TotalHotkeyShares::::get(hk1, netuid), + U64F64::from_num(0) + ); + assert_eq!( + TotalHotkeyShares::::get(hk2, netuid), + U64F64::from_num(0) + ); + assert_eq!(TotalHotkeySharesV2::::get(hk2, netuid), shares.into()); + assert_eq!( + Alpha::::get((hk1, coldkey, netuid)), + U64F64::from_num(0) + ); + assert_eq!( + Alpha::::get((hk2, coldkey, netuid)), + U64F64::from_num(0) + ); + assert_eq!(AlphaV2::::get((hk2, coldkey, netuid)), amount.into()); + assert_eq!( + AlphaDividendsPerSubnet::::get(netuid, hk1), + AlphaBalance::ZERO + ); + assert_eq!( + AlphaDividendsPerSubnet::::get(netuid, hk2), + AlphaBalance::from(amount) + ); + + // Revert: hk2 -> hk1 + step_block(20); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &hk2, + &hk1, + Some(netuid), + false + )); + + assert_eq!( + TotalHotkeyAlpha::::get(hk2, netuid), + AlphaBalance::ZERO, + "hk2 TotalHotkeyAlpha must be zero after revert" + ); + assert_eq!( + TotalHotkeyAlpha::::get(hk1, netuid), + AlphaBalance::from(amount), + "hk1 TotalHotkeyAlpha must be restored after revert" + ); + assert_eq!( + TotalHotkeyAlphaLastEpoch::::get(hk2, netuid), + AlphaBalance::ZERO, + "hk2 TotalHotkeyAlphaLastEpoch must be zero after revert" + ); + assert_eq!( + TotalHotkeyAlphaLastEpoch::::get(hk1, netuid), + AlphaBalance::from(amount * 2), + "hk1 TotalHotkeyAlphaLastEpoch must be restored after revert" + ); + assert_eq!( + TotalHotkeyShares::::get(hk2, netuid), + U64F64::from_num(0), + "hk2 TotalHotkeyShares must be zero after revert" + ); + assert_eq!( + TotalHotkeyShares::::get(hk1, netuid), + U64F64::from_num(0), + "hk1 TotalHotkeyShares must be migrated to v2" + ); + assert_eq!( + TotalHotkeySharesV2::::get(hk1, netuid), + shares.into(), + "hk1 TotalHotkeyShares must be restored to v2 after revert" + ); + assert_eq!( + Alpha::::get((hk2, coldkey, netuid)), + U64F64::from_num(0), + "hk2 Alpha must be zero after revert" + ); + assert_eq!( + Alpha::::get((hk1, coldkey, netuid)), + U64F64::from_num(0), + "hk1 Alpha must be migrated to v2" + ); + assert_eq!( + AlphaV2::::get((hk1, coldkey, netuid)), + amount.into(), + "hk1 Alpha must be restored to v2 after revert" + ); + assert_eq!( + AlphaDividendsPerSubnet::::get(netuid, hk2), + AlphaBalance::ZERO, + "hk2 AlphaDividendsPerSubnet must be zero after revert" + ); + assert_eq!( + AlphaDividendsPerSubnet::::get(netuid, hk1), + AlphaBalance::from(amount), + "hk1 AlphaDividendsPerSubnet must be restored after revert" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_revert_voting_power_transfers_on_hotkey_swap --exact --nocapture +#[test] +fn test_revert_voting_power_transfers_on_hotkey_swap() { + new_test_ext(1).execute_with(|| { + let hk1 = U256::from(1); + let hk2 = U256::from(99); + let coldkey = U256::from(2); + let netuid = add_dynamic_network(&hk1, &coldkey); + let voting_power_value = 5_000_000_000_000_u64; + + VotingPower::::insert(netuid, hk1, voting_power_value); + assert_eq!( + SubtensorModule::get_voting_power(netuid, &hk1), + voting_power_value + ); + assert_eq!(SubtensorModule::get_voting_power(netuid, &hk2), 0); + + SubtensorModule::swap_voting_power_for_hotkey(&hk1, &hk2, netuid); + + assert_eq!(SubtensorModule::get_voting_power(netuid, &hk1), 0); + assert_eq!( + SubtensorModule::get_voting_power(netuid, &hk2), + voting_power_value + ); + + // Revert: hk2 -> hk1 + SubtensorModule::swap_voting_power_for_hotkey(&hk2, &hk1, netuid); + + assert_eq!( + SubtensorModule::get_voting_power(netuid, &hk1), + voting_power_value, + "hk1 voting power must be fully restored after revert" + ); + assert_eq!( + SubtensorModule::get_voting_power(netuid, &hk2), + 0, + "hk2 must have no voting power after revert" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_revert_claim_root_with_swap_hotkey --exact --nocapture +#[test] +fn test_revert_claim_root_with_swap_hotkey() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(1001); + let hk1 = U256::from(1002); + let hk2 = U256::from(1003); + let coldkey = U256::from(1004); + + let netuid = add_dynamic_network(&hk1, &owner_coldkey); + let netuid2 = add_dynamic_network(&hk1, &owner_coldkey); + + add_balance_to_coldkey_account(&owner_coldkey, 1_000_000_000_000_u64.into()); + SubtensorModule::set_tao_weight(u64::MAX); + + let root_stake = 2_000_000u64; + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hk1, + &coldkey, + NetUid::ROOT, + root_stake.into(), + ); + + let initial_total_hotkey_alpha = 10_000_000u64; + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hk1, + &owner_coldkey, + netuid, + initial_total_hotkey_alpha.into(), + ); + + let pending_root_alpha = 1_000_000u64; + SubtensorModule::distribute_emission( + netuid, + AlphaBalance::ZERO, + AlphaBalance::ZERO, + pending_root_alpha.into(), + AlphaBalance::ZERO, + ); + + assert_ok!(SubtensorModule::set_root_claim_type( + RuntimeOrigin::signed(coldkey), + RootClaimTypeEnum::Keep + )); + assert_ok!(SubtensorModule::claim_root( + RuntimeOrigin::signed(coldkey), + BTreeSet::from([netuid]) + )); + + let stake_after_claim: u64 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey, netuid) + .into(); + + let hk1_root_claimed = RootClaimed::::get((netuid, &hk1, &coldkey)); + let hk1_claimable = *RootClaimable::::get(hk1).get(&netuid).unwrap(); + + assert_eq!(u128::from(stake_after_claim), hk1_root_claimed); + assert!(!RootClaimable::::get(hk2).contains_key(&netuid)); + + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(owner_coldkey), + &hk1, + &hk2, + Some(netuid), + false + )); + + assert_eq!( + RootClaimed::::get((netuid, &hk2, &coldkey)), + 0u128, + "hk2 RootClaimed must be zero after swap" + ); + assert_eq!( + RootClaimed::::get((netuid, &hk1, &coldkey)), + hk1_root_claimed, + "hk2 must have hk1's RootClaimed after swap" + ); + assert!(RootClaimable::::get(hk1).contains_key(&netuid)); + assert!(!RootClaimable::::get(hk2).contains_key(&netuid)); + + // Revert: hk2 -> hk1 + step_block(20); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(owner_coldkey), + &hk2, + &hk1, + Some(netuid), + false + )); + + assert_eq!( + RootClaimed::::get((netuid, &hk2, &coldkey)), + 0u128, + "hk2 RootClaimed must be zero after revert" + ); + assert_eq!( + RootClaimed::::get((netuid, &hk1, &coldkey)), + hk1_root_claimed, + "hk1 RootClaimed must be restored after revert" + ); + + assert!(!RootClaimable::::get(hk2).contains_key(&netuid)); + assert_eq!( + *RootClaimable::::get(hk1).get(&netuid).unwrap(), + hk1_claimable, + "hk1 RootClaimable must be restored after revert" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_revert_hotkey_swap_with_revert_stake_the_same --exact --nocapture +#[test] +fn test_revert_hotkey_swap_with_revert_stake_the_same() { + new_test_ext(1).execute_with(|| { + let netuid_1 = NetUid::from(1); + let netuid_2 = NetUid::from(2); + let tempo: u16 = 13; + let hk1 = U256::from(1); + let new_hotkey = U256::from(2); + let random_hotkey = U256::from(3); + let coldkey = U256::from(3); + let coldkey_2 = U256::from(4); + let coldkey_3 = U256::from(5); + let coldkey_4 = U256::from(6); + let random_coldkey = U256::from(7); + let initial_balance = 10_000_000_000u64 * 2; + let stake1 = 500_000_000u64; + let stake2 = 1_000_000_000u64; + let stake_ck2 = 1_500_000_000u64; + let stake_ck3 = 300_000_000u64; + let stake_ck4 = 900_000_000u64; + + assert_ok!(SubtensorModule::try_associate_hotkey( + <::RuntimeOrigin>::signed(random_coldkey), + random_hotkey + )); + + // Setup + super::super::mock::setup_reserves( + netuid_1, + (stake_ck4 * 100).into(), + (stake_ck4 * 100).into(), + ); + super::super::mock::setup_reserves( + netuid_2, + (stake_ck4 * 100).into(), + (stake_ck4 * 100).into(), + ); + + add_network(netuid_1, tempo, 0); + add_network(netuid_2, tempo, 0); + + SubnetMechanism::::insert(netuid_1, 1); + SubnetMechanism::::insert(netuid_2, 1); + + register_ok_neuron(netuid_1, hk1, coldkey, 0); + register_ok_neuron(netuid_2, hk1, coldkey, 0); + + add_balance_to_coldkey_account(&coldkey, initial_balance.into()); + add_balance_to_coldkey_account(&coldkey_4, initial_balance.into()); + add_balance_to_coldkey_account(&random_coldkey, initial_balance.into()); + step_block(20); // Waiting interval to be able to swap later + + // Checking stake for hk1 on both networks + let hk1_stake_before_increase_sn_1 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey, netuid_1); + assert!( + hk1_stake_before_increase_sn_1 == 0.into(), + "hk1 should have empty stake" + ); + + let hk1_stake_before_increase_sn_2 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey, netuid_2); + assert!( + hk1_stake_before_increase_sn_2 == 0.into(), + "hk1 should have empty stake" + ); + + // Adding stake to hk1 on both networks + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hk1, + &coldkey, + netuid_1, + stake1.into(), + ); + // Adding another stake for different coldkey + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hk1, + &coldkey_2, + netuid_1, + stake_ck2.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hk1, + &coldkey_3, + netuid_1, + stake_ck3.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hk1, + &coldkey, + netuid_2, + stake2.into(), + ); + + // The stake for validator + let hk1_stake_before_swap_sn_1 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey, netuid_1); + assert!( + hk1_stake_before_swap_sn_1 == stake1.into(), + "hk1 should have stake before swap on sn_1" + ); + + // Let's check individual stake + let hk1_stake_before_swap_sn_1 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey_2, netuid_1); + assert_eq!( + hk1_stake_before_swap_sn_1, + (stake_ck2).into(), + "stake for ck2 should be only his stake" + ); + + let hk1_stake_before_swap_sn_2 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey, netuid_2); + assert!( + hk1_stake_before_swap_sn_2 == stake2.into(), + "hk1 should have stake before swap on sn_2" + ); + + assert_ok!(SubtensorModule::perform_hotkey_swap( + <::RuntimeOrigin>::signed(coldkey), + &hk1, + &new_hotkey, + Some(netuid_1), + false + )); + + assert_eq!(Owner::::get(hk1), coldkey); + + SubtensorModule::do_add_stake( + RawOrigin::Signed(random_coldkey).into(), + hk1, + netuid_1, + stake_ck4.into(), + ) + .unwrap(); + + // Check stake moved to new hotkey on subnet1 + let new_hotkey_stake_after_swap_ck = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &new_hotkey, + &coldkey, + netuid_1, + ); + assert_eq!(new_hotkey_stake_after_swap_ck, stake1.into()); + + // Check stake moved for ck2 + let new_hotkey_stake_after_swap_ck_1 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &new_hotkey, + &coldkey_2, + netuid_1, + ); + assert_eq!(new_hotkey_stake_after_swap_ck_1, stake_ck2.into()); + + // Check stake moved for ck3 + let new_hotkey_stake_after_swap_ck_3 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &new_hotkey, + &coldkey_3, + netuid_1, + ); + assert_eq!(new_hotkey_stake_after_swap_ck_3, stake_ck3.into()); + + step_block(20); + + // Let's check individual stakes; they changed because of emissions + let new_hotkey_stake_before_revert_ck = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &new_hotkey, + &coldkey, + netuid_1, + ); + assert!(new_hotkey_stake_before_revert_ck > stake1.into()); + + let new_hotkey_stake_before_revert_ck_2 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &new_hotkey, + &coldkey_2, + netuid_1, + ); + assert!(new_hotkey_stake_before_revert_ck_2 > stake_ck2.into()); + + let new_hotkey_stake_before_revert_ck_3 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &new_hotkey, + &coldkey_3, + netuid_1, + ); + assert!(new_hotkey_stake_before_revert_ck_3 > stake_ck3.into()); + + // Reverting back: hk2 -> hk1 + assert_ok!(SubtensorModule::perform_hotkey_swap( + <::RuntimeOrigin>::signed(coldkey), + &new_hotkey, + &hk1, + Some(netuid_1), + false + )); + + // Let's check individual stakes; they changed because of emissions + let old_hotkey_stake_after_revert_ck = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey, netuid_1); + assert_eq!( + old_hotkey_stake_after_revert_ck, + new_hotkey_stake_before_revert_ck + ); + + let old_hotkey_stake_after_revert_ck_2 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey_2, netuid_1); + assert_eq!( + old_hotkey_stake_after_revert_ck_2, + new_hotkey_stake_before_revert_ck_2 + ); + + let old_hotkey_stake_after_revert_ck_3 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hk1, &coldkey_3, netuid_1); + assert_eq!( + old_hotkey_stake_after_revert_ck_3, + new_hotkey_stake_before_revert_ck_3 + ); + }); +} diff --git a/pallets/subtensor/src/tests/swap_hotkey_with_subnet/root_claims.rs b/pallets/subtensor/src/tests/swap_hotkey_with_subnet/root_claims.rs new file mode 100644 index 0000000000..6ecee2a0d9 --- /dev/null +++ b/pallets/subtensor/src/tests/swap_hotkey_with_subnet/root_claims.rs @@ -0,0 +1,275 @@ +#![allow(unused, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used)] + +use approx::assert_abs_diff_eq; +use codec::Encode; +use frame_support::weights::Weight; +use frame_support::{assert_err, assert_noop, assert_ok}; +use frame_system::{Config, RawOrigin}; +use subtensor_runtime_common::{AlphaBalance, NetUidStorageIndex, TaoBalance, Token}; + +use super::super::mock::*; +use crate::*; +use share_pool::SafeFloat; +use sp_core::{Get, H160, H256, U256}; +use sp_runtime::{PerU16, SaturatedConversion}; +use std::collections::BTreeSet; +use substrate_fixed::types::{I96F32, U64F64}; + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_hotkey_root_claims_unchanged_if_not_root --exact --nocapture +#[test] +fn test_swap_hotkey_root_claims_unchanged_if_not_root() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(1001); + let neuron_hotkey = U256::from(1002); + let staker_coldkey = U256::from(1003); + let netuid = add_dynamic_network(&neuron_hotkey, &owner_coldkey); + let new_hotkey = U256::from(10030); + + add_balance_to_coldkey_account(&owner_coldkey, 20_000_000_000_000_000_u64.into()); + SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 + + let root_stake = 2_000_000_000u64; + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &neuron_hotkey, + &staker_coldkey, + NetUid::ROOT, + root_stake.into(), + ); + + let initial_total_hotkey_alpha = 10_000_000_000u64; + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &neuron_hotkey, + &staker_coldkey, + netuid, + initial_total_hotkey_alpha.into(), + ); + + let validator_stake = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &neuron_hotkey, + &staker_coldkey, + netuid, + ); + assert_eq!(validator_stake, initial_total_hotkey_alpha.into()); + + // Distribute pending root alpha + let pending_root_alpha = 1_000_000_000u64; + SubtensorModule::distribute_emission( + netuid, + AlphaBalance::ZERO, + AlphaBalance::ZERO, + pending_root_alpha.into(), + AlphaBalance::ZERO, + ); + + assert_ok!(SubtensorModule::claim_root( + RuntimeOrigin::signed(staker_coldkey), + BTreeSet::from([netuid]) + )); + + let claimable = RootClaimable::::get(neuron_hotkey) + .get(&netuid) + .copied(); + + assert!(claimable.is_some()); + let claimable = claimable.unwrap_or_default(); + + assert!(claimable > 0); + + assert!(RootClaimed::::get((netuid, &neuron_hotkey, &staker_coldkey,)) > 0u128); + + step_block(20); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(owner_coldkey), + &neuron_hotkey, + &new_hotkey, + Some(netuid), + false + )); + + // Claimable and claimed should stay on old hotkey + assert_eq!( + RootClaimable::::get(neuron_hotkey) + .get(&netuid) + .copied(), + Some(claimable) + ); + assert!(RootClaimed::::get((netuid, &neuron_hotkey, &staker_coldkey,)) > 0u128); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_hotkey_root_claims_changed_if_root --exact --nocapture +#[test] +fn test_swap_hotkey_root_claims_changed_if_root() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(1001); + + let neuron_hotkey = U256::from(1004); + let neuron_hotkey_new = U256::from(1005); + + let staker_coldkey = U256::from(1006); + + NetworksAdded::::insert(NetUid::ROOT, true); + + // Use neuron_hotkey as subnet creator so it receives root dividends + let netuid_1 = add_dynamic_network(&neuron_hotkey, &owner_coldkey); + + add_balance_to_coldkey_account(&owner_coldkey, 20_000_000_000_000_000_u64.into()); + SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 + + let root_stake = 2_000_000_000u64; + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &neuron_hotkey, + &staker_coldkey, + NetUid::ROOT, + root_stake.into(), + ); + + let initial_total_hotkey_alpha = 10_000_000_000u64; + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &neuron_hotkey, + &owner_coldkey, + netuid_1, + initial_total_hotkey_alpha.into(), + ); + + // Distribute pending root alpha + let pending_root_alpha = 1_000_000_000u64; + SubtensorModule::distribute_emission( + netuid_1, + AlphaBalance::ZERO, + AlphaBalance::ZERO, + pending_root_alpha.into(), + AlphaBalance::ZERO, + ); + + assert_ok!(SubtensorModule::set_root_claim_type( + RuntimeOrigin::signed(staker_coldkey), + RootClaimTypeEnum::Keep + )); + assert_ok!(SubtensorModule::claim_root( + RuntimeOrigin::signed(staker_coldkey), + BTreeSet::from([netuid_1]) + )); + + let claimable = RootClaimable::::get(neuron_hotkey) + .get(&netuid_1) + .copied(); + assert!(claimable.is_some()); + let claimable = claimable.unwrap_or_default(); + + assert!(claimable > 0); + + let claimed = RootClaimed::::get((netuid_1, &neuron_hotkey, &staker_coldkey)); + assert!(claimed > 0u128); + + step_block(20); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(owner_coldkey), + &neuron_hotkey, + &neuron_hotkey_new, + Some(NetUid::ROOT), + false + )); + + // Claimable and claimed should be transferred to new hotkey + assert_eq!( + RootClaimable::::get(neuron_hotkey_new) + .get(&netuid_1) + .copied(), + Some(claimable) + ); + assert_eq!( + RootClaimed::::get((netuid_1, &neuron_hotkey_new, &staker_coldkey,)), + claimed + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_hotkey_root_claims_changed_if_all_subnets --exact --nocapture +#[test] +fn test_swap_hotkey_root_claims_changed_if_all_subnets() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(1001); + let neuron_hotkey = U256::from(1004); + let neuron_hotkey_new = U256::from(1005); + + let staker_coldkey = U256::from(1006); + + // Ensure ROOT network is registered for all-subnets swap + SubtokenEnabled::::insert(NetUid::ROOT, true); + NetworksAdded::::insert(NetUid::ROOT, true); + + // Use neuron_hotkey as subnet creator so it receives root dividends + let netuid_1 = add_dynamic_network(&neuron_hotkey, &owner_coldkey); + + add_balance_to_coldkey_account(&owner_coldkey, 20_000_000_000_000_000_u64.into()); + SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 + + let root_stake = 2_000_000_000u64; + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &neuron_hotkey, + &staker_coldkey, + NetUid::ROOT, + root_stake.into(), + ); + + let initial_total_hotkey_alpha = 10_000_000_000u64; + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &neuron_hotkey, + &owner_coldkey, + netuid_1, + initial_total_hotkey_alpha.into(), + ); + + // Distribute pending root alpha + let pending_root_alpha = 1_000_000_000u64; + SubtensorModule::distribute_emission( + netuid_1, + AlphaBalance::ZERO, + AlphaBalance::ZERO, + pending_root_alpha.into(), + AlphaBalance::ZERO, + ); + + assert_ok!(SubtensorModule::set_root_claim_type( + RuntimeOrigin::signed(staker_coldkey), + RootClaimTypeEnum::Keep + )); + assert_ok!(SubtensorModule::claim_root( + RuntimeOrigin::signed(staker_coldkey), + BTreeSet::from([netuid_1]) + )); + + let claimable = RootClaimable::::get(neuron_hotkey) + .get(&netuid_1) + .copied(); + assert!(claimable.is_some()); + let claimable = claimable.unwrap_or_default(); + + assert!(claimable > 0); + + let claimed = RootClaimed::::get((netuid_1, &neuron_hotkey, &staker_coldkey)); + assert!(claimed > 0u128); + + step_block(20); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(owner_coldkey), + &neuron_hotkey, + &neuron_hotkey_new, + None, + false + )); + + // Claimable and claimed should be transferred to new hotkey + assert_eq!( + RootClaimable::::get(neuron_hotkey_new) + .get(&netuid_1) + .copied(), + Some(claimable) + ); + assert_eq!( + RootClaimed::::get((netuid_1, &neuron_hotkey_new, &staker_coldkey,)), + claimed + ); + }); +} diff --git a/pallets/subtensor/src/tests/swap_hotkey_with_subnet/stake_transfer.rs b/pallets/subtensor/src/tests/swap_hotkey_with_subnet/stake_transfer.rs new file mode 100644 index 0000000000..c242f1c2b0 --- /dev/null +++ b/pallets/subtensor/src/tests/swap_hotkey_with_subnet/stake_transfer.rs @@ -0,0 +1,1003 @@ +#![allow(unused, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used)] + +use approx::assert_abs_diff_eq; +use codec::Encode; +use frame_support::weights::Weight; +use frame_support::{assert_err, assert_noop, assert_ok}; +use frame_system::{Config, RawOrigin}; +use subtensor_runtime_common::{AlphaBalance, NetUidStorageIndex, TaoBalance, Token}; + +use super::super::mock::*; +use crate::*; +use share_pool::SafeFloat; +use sp_core::{Get, H160, H256, U256}; +use sp_runtime::{PerU16, SaturatedConversion}; +use std::collections::BTreeSet; +use substrate_fixed::types::{I96F32, U64F64}; + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_total_hotkey_stake --exact --nocapture +#[test] +fn test_swap_total_hotkey_stake() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let amount = DefaultMinStake::::get().to_u64() * 10; + + let fee = (amount as f64 * 0.003) as u64; + + //add network + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + remove_owner_registration_stake(netuid); + + // Give it some $$$ in his coldkey balance + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + // Add stake + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + old_hotkey, + netuid, + amount.into() + )); + + // Check if stake has increased + assert_abs_diff_eq!( + SubtensorModule::get_total_stake_for_hotkey(&old_hotkey), + (amount - fee).into(), + epsilon = TaoBalance::from(amount / 100), + ); + assert_abs_diff_eq!( + SubtensorModule::get_total_stake_for_hotkey(&new_hotkey), + TaoBalance::ZERO, + epsilon = 1.into(), + ); + + // Swap hotkey + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + )); + + // Verify that total hotkey stake swapped + assert_abs_diff_eq!( + SubtensorModule::get_total_stake_for_hotkey(&old_hotkey), + TaoBalance::ZERO, + epsilon = 1.into(), + ); + assert_abs_diff_eq!( + SubtensorModule::get_total_stake_for_hotkey(&new_hotkey), + TaoBalance::from(amount - fee), + epsilon = TaoBalance::from(amount / 100), + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_staking_hotkeys --exact --nocapture +#[test] +fn test_swap_staking_hotkeys() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + StakingHotkeys::::insert(coldkey, vec![old_hotkey]); + Alpha::::insert((old_hotkey, coldkey, netuid), U64F64::from_num(100)); + + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + )); + + let staking_hotkeys = StakingHotkeys::::get(coldkey); + assert!(staking_hotkeys.contains(&old_hotkey)); + assert!(staking_hotkeys.contains(&new_hotkey)); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey::test_swap_hotkey_with_multiple_coldkeys --exact --show-output --nocapture +#[test] +fn test_swap_hotkey_with_multiple_coldkeys() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey1 = U256::from(3); + let coldkey2 = U256::from(4); + + let stake = 1_000_000_000; + + StakingHotkeys::::insert(coldkey1, vec![old_hotkey]); + StakingHotkeys::::insert(coldkey2, vec![old_hotkey]); + SubtensorModule::create_account_if_non_existent(&coldkey1, &old_hotkey); + add_balance_to_coldkey_account(&coldkey1, 1_000_000_000_000_u64.into()); + add_balance_to_coldkey_account(&coldkey2, 1_000_000_000_000_u64.into()); + + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey1), + old_hotkey, + netuid, + stake.into() + )); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey2), + old_hotkey, + netuid, + TaoBalance::from(stake / 2) + )); + let stake1_before = SubtensorModule::get_total_stake_for_coldkey(&coldkey1); + let stake2_before = SubtensorModule::get_total_stake_for_coldkey(&coldkey2); + + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey1), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + )); + + assert_eq!( + SubtensorModule::get_total_stake_for_coldkey(&coldkey1), + SubtensorModule::get_total_stake_for_coldkey(&coldkey1), + ); + assert_eq!( + SubtensorModule::get_total_stake_for_coldkey(&coldkey2), + SubtensorModule::get_total_stake_for_coldkey(&coldkey2), + ); + + assert_eq!( + SubtensorModule::get_total_stake_for_coldkey(&coldkey1), + stake1_before + ); + assert_eq!( + SubtensorModule::get_total_stake_for_coldkey(&coldkey2), + stake2_before + ); + + assert!(StakingHotkeys::::get(coldkey1).contains(&new_hotkey)); + assert!(StakingHotkeys::::get(coldkey2).contains(&new_hotkey)); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_hotkey_with_multiple_subnets --exact --nocapture +#[test] +fn test_swap_hotkey_with_multiple_subnets() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let new_hotkey_2 = U256::from(3); + let coldkey = U256::from(4); + + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + let netuid1 = add_dynamic_network(&old_hotkey, &coldkey); + let netuid2 = add_dynamic_network(&old_hotkey, &coldkey); + + IsNetworkMember::::insert(old_hotkey, netuid1, true); + IsNetworkMember::::insert(old_hotkey, netuid2, true); + + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid1), + false + )); + + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey_2, + Some(netuid2), + false + )); + + assert!(IsNetworkMember::::get(new_hotkey, netuid1)); + assert!(IsNetworkMember::::get(new_hotkey_2, netuid2)); + assert!(!IsNetworkMember::::get(old_hotkey, netuid1)); + assert!(!IsNetworkMember::::get(old_hotkey, netuid2)); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_staking_hotkeys_multiple_coldkeys --exact --nocapture +#[test] +fn test_swap_staking_hotkeys_multiple_coldkeys() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey1 = U256::from(3); + let coldkey2 = U256::from(4); + let staker5 = U256::from(5); + + let stake = 1_000_000_000; + add_balance_to_coldkey_account(&coldkey1, 1_000_000_000_000_u64.into()); + add_balance_to_coldkey_account(&coldkey2, 1_000_000_000_000_u64.into()); + + // Set up initial state + StakingHotkeys::::insert(coldkey1, vec![old_hotkey]); + StakingHotkeys::::insert(coldkey2, vec![old_hotkey, staker5]); + + SubtensorModule::create_account_if_non_existent(&coldkey1, &old_hotkey); + + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey1), + old_hotkey, + netuid, + stake.into() + )); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey2), + old_hotkey, + netuid, + stake.into() + )); + + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey1), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + )); + + // Check if new_hotkey replaced old_hotkey in StakingHotkeys + assert!(StakingHotkeys::::get(coldkey1).contains(&new_hotkey)); + assert!(StakingHotkeys::::get(coldkey1).contains(&old_hotkey)); + + // Check if new_hotkey replaced old_hotkey for coldkey2 as well + assert!(StakingHotkeys::::get(coldkey2).contains(&new_hotkey)); + assert!(StakingHotkeys::::get(coldkey2).contains(&old_hotkey)); + assert!(StakingHotkeys::::get(coldkey2).contains(&staker5)); + // Other hotkeys should remain + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_hotkey_with_no_stake --exact --nocapture +#[test] +fn test_swap_hotkey_with_no_stake() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + // Set up initial state with no stake + Owner::::insert(old_hotkey, coldkey); + + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + )); + + // Check if ownership transferred + assert!(Owner::::contains_key(old_hotkey)); + assert_eq!(Owner::::get(new_hotkey), coldkey); + + // Ensure no unexpected changes in Stake + assert!(!Alpha::::contains_key((old_hotkey, coldkey, netuid))); + assert!(!Alpha::::contains_key((new_hotkey, coldkey, netuid))); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey::test_swap_hotkey_with_multiple_coldkeys_and_subnets --exact --show-output +#[test] +fn test_swap_hotkey_with_multiple_coldkeys_and_subnets() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let new_hotkey_2 = U256::from(3); + let coldkey1 = U256::from(4); + let coldkey2 = U256::from(5); + let netuid1 = NetUid::from(1); + let netuid2 = NetUid::from(2); + let stake = DefaultMinStake::::get().to_u64() * 10; + + // Set up initial state + add_network(netuid1, 1, 1); + add_network(netuid2, 1, 1); + register_ok_neuron(netuid1, old_hotkey, coldkey1, 1234); + register_ok_neuron(netuid2, old_hotkey, coldkey1, 1234); + + // Add balance to both coldkeys + add_balance_to_coldkey_account(&coldkey1, 1_000_000_000_000_u64.into()); + add_balance_to_coldkey_account(&coldkey2, 1_000_000_000_000_u64.into()); + + // Stake with coldkey1 + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(coldkey1), + old_hotkey, + netuid1, + stake.into() + )); + + // Stake with coldkey2 also + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(coldkey2), + old_hotkey, + netuid2, + stake.into() + )); + + let ck1_stake = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &old_hotkey, + &coldkey1, + netuid1, + ); + let ck2_stake = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &old_hotkey, + &coldkey2, + netuid2, + ); + assert!(!ck1_stake.is_zero()); + assert!(!ck2_stake.is_zero()); + let total_hk_stake = SubtensorModule::get_total_stake_for_hotkey(&old_hotkey); + assert!(!total_hk_stake.is_zero()); + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey1), + &old_hotkey, + &new_hotkey, + Some(netuid1), + false + )); + + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey1), + &old_hotkey, + &new_hotkey_2, + Some(netuid2), + false + )); + + // Check ownership transfer + assert_eq!( + SubtensorModule::get_owning_coldkey_for_hotkey(&new_hotkey), + coldkey1 + ); + assert!(!SubtensorModule::get_owned_hotkeys(&coldkey2).contains(&new_hotkey)); + assert_eq!( + SubtensorModule::get_owning_coldkey_for_hotkey(&new_hotkey_2), + coldkey1 + ); + assert!(!SubtensorModule::get_owned_hotkeys(&coldkey2).contains(&new_hotkey_2)); + + // Check stake transfer + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &new_hotkey, + &coldkey1, + netuid1 + ), + ck1_stake + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &new_hotkey_2, + &coldkey2, + netuid2 + ), + ck2_stake + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &old_hotkey, + &coldkey1, + netuid1 + ), + AlphaBalance::ZERO + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &old_hotkey, + &coldkey2, + netuid2 + ), + AlphaBalance::ZERO + ); + + // Check subnet membership transfer + assert!(SubtensorModule::is_hotkey_registered_on_network( + netuid1, + &new_hotkey + )); + assert!(SubtensorModule::is_hotkey_registered_on_network( + netuid2, + &new_hotkey_2 + )); + assert!(!SubtensorModule::is_hotkey_registered_on_network( + netuid1, + &old_hotkey + )); + assert!(!SubtensorModule::is_hotkey_registered_on_network( + netuid2, + &old_hotkey + )); + + // Check total stake transfer + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&new_hotkey) + + SubtensorModule::get_total_stake_for_hotkey(&new_hotkey_2), + total_hk_stake + ); + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&old_hotkey), + TaoBalance::ZERO + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_stake_success --exact --nocapture +#[test] +fn test_swap_stake_success() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + remove_owner_registration_stake(netuid); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + let amount = 10_000; + let shares = U64F64::from_num(10_000); + + // Initialize staking variables for old_hotkey + TotalHotkeyAlpha::::insert(old_hotkey, netuid, AlphaBalance::from(amount)); + TotalHotkeyAlphaLastEpoch::::insert( + old_hotkey, + netuid, + AlphaBalance::from(amount * 2), + ); + TotalHotkeyShares::::insert(old_hotkey, netuid, U64F64::from_num(shares)); + Alpha::::insert((old_hotkey, coldkey, netuid), U64F64::from_num(amount)); + AlphaDividendsPerSubnet::::insert(netuid, old_hotkey, AlphaBalance::from(amount)); + + // Perform the swap + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + ),); + + // Verify the swap + assert_eq!( + TotalHotkeyAlpha::::get(old_hotkey, netuid), + AlphaBalance::ZERO + ); + assert_eq!( + TotalHotkeyAlpha::::get(new_hotkey, netuid), + AlphaBalance::from(amount) + ); + assert_eq!( + TotalHotkeyAlphaLastEpoch::::get(old_hotkey, netuid), + AlphaBalance::ZERO + ); + assert_eq!( + TotalHotkeyAlphaLastEpoch::::get(new_hotkey, netuid), + AlphaBalance::from(amount * 2) + ); + assert_eq!( + TotalHotkeyShares::::get(old_hotkey, netuid), + U64F64::from_num(0) + ); + assert_eq!( + TotalHotkeyShares::::get(new_hotkey, netuid), + U64F64::from_num(0) + ); + assert_abs_diff_eq!( + f64::from(TotalHotkeySharesV2::::get(new_hotkey, netuid)), + shares.to_num::(), + epsilon = 0.0000000001 + ); + assert_eq!( + Alpha::::get((old_hotkey, coldkey, netuid)), + U64F64::from_num(0) + ); + assert_eq!( + Alpha::::get((new_hotkey, coldkey, netuid)), + U64F64::from_num(0) + ); + assert_eq!( + f64::from(AlphaV2::::get((new_hotkey, coldkey, netuid))), + amount as f64 + ); + assert_eq!( + AlphaDividendsPerSubnet::::get(netuid, old_hotkey), + AlphaBalance::ZERO + ); + assert_eq!( + AlphaDividendsPerSubnet::::get(netuid, new_hotkey), + AlphaBalance::from(amount) + ); + }); +} + +#[test] +fn test_swap_stake_v2_success() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + let amount = 10_000; + let shares = U64F64::from_num(10_000); + + // Initialize staking variables for old_hotkey + TotalHotkeyAlpha::::insert(old_hotkey, netuid, AlphaBalance::from(amount)); + TotalHotkeyAlphaLastEpoch::::insert( + old_hotkey, + netuid, + AlphaBalance::from(amount * 2), + ); + TotalHotkeySharesV2::::insert(old_hotkey, netuid, SafeFloat::from(shares)); + AlphaV2::::insert( + (old_hotkey, coldkey, netuid), + SafeFloat::from(U64F64::from_num(amount)), + ); + AlphaDividendsPerSubnet::::insert(netuid, old_hotkey, AlphaBalance::from(amount)); + + // Perform the swap + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false, + ),); + + // Verify the swap + assert_eq!( + TotalHotkeyAlpha::::get(old_hotkey, netuid), + AlphaBalance::ZERO + ); + assert_eq!( + TotalHotkeyAlpha::::get(new_hotkey, netuid), + AlphaBalance::from(amount) + ); + assert_eq!( + TotalHotkeyAlphaLastEpoch::::get(old_hotkey, netuid), + AlphaBalance::ZERO + ); + assert_eq!( + TotalHotkeyAlphaLastEpoch::::get(new_hotkey, netuid), + AlphaBalance::from(amount * 2) + ); + assert_eq!( + f64::from(TotalHotkeySharesV2::::get(old_hotkey, netuid)), + 0_f64 + ); + assert_abs_diff_eq!( + f64::from(TotalHotkeySharesV2::::get(new_hotkey, netuid)), + shares.to_num::(), + epsilon = 0.0000000001 + ); + assert_eq!( + f64::from(AlphaV2::::get((old_hotkey, coldkey, netuid))), + 0_f64 + ); + assert_eq!( + f64::from(AlphaV2::::get((new_hotkey, coldkey, netuid))), + amount as f64 + ); + assert_eq!( + AlphaDividendsPerSubnet::::get(netuid, old_hotkey), + AlphaBalance::ZERO + ); + assert_eq!( + AlphaDividendsPerSubnet::::get(netuid, new_hotkey), + AlphaBalance::from(amount) + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_hotkey_error_cases --exact --nocapture +#[test] +fn test_swap_hotkey_error_cases() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(3); + let wrong_coldkey = U256::from(4); + let netuid = add_dynamic_network(&old_hotkey, &coldkey); + + // Set up initial state + Owner::::insert(old_hotkey, coldkey); + TotalNetworks::::put(1); + SubtensorModule::set_last_tx_block(&coldkey, 0); + + // Test not enough balance + let swap_cost = SubtensorModule::get_key_swap_cost(); + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_err!( + SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + ), + Error::::NotEnoughBalanceToPaySwapHotKey + ); + + let initial_balance = SubtensorModule::get_key_swap_cost() + 1000.into(); + add_balance_to_coldkey_account(&coldkey, initial_balance); + + // Test new hotkey same as old + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_noop!( + SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &old_hotkey, + Some(netuid), + false + ), + Error::::NewHotKeyIsSameWithOld + ); + + // Test new hotkey already registered + IsNetworkMember::::insert(new_hotkey, netuid, true); + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_noop!( + SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + ), + Error::::HotKeyAlreadyRegisteredInSubNet + ); + IsNetworkMember::::remove(new_hotkey, netuid); + + // Test non-associated coldkey + assert_noop!( + SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(wrong_coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + ), + Error::::NonAssociatedColdKey + ); + + // Run the successful swap + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + ),); + }); +} + +// Check swap hotkey with keep_stake doesn't affect stake and related storage maps +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_hotkey_swap_keep_stake --exact --nocapture +#[test] +fn test_hotkey_swap_keep_stake() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let tempo: u16 = 13; + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let child_key = U256::from(4); + let coldkey = U256::from(3); + let swap_cost = 1_000_000_000u64 * 2; + let stake_amount = 1_000_000_000u64; + let voting_power_value = 5_000_000_000_000_u64; + + // Setup + add_network(netuid, tempo, 0); + register_ok_neuron(netuid, old_hotkey, coldkey, 0); + add_balance_to_coldkey_account(&coldkey, swap_cost.into()); + + VotingPower::::insert(netuid, old_hotkey, voting_power_value); + assert_eq!( + SubtensorModule::get_voting_power(netuid, &old_hotkey), + voting_power_value + ); + + ChildKeys::::insert(old_hotkey, netuid, vec![(u64::MAX, child_key)]); + ParentKeys::::insert(child_key, netuid, vec![(u64::MAX, old_hotkey)]); + + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &old_hotkey, + &coldkey, + netuid, + stake_amount.into(), + ); + + assert!(SubtensorModule::is_hotkey_registered_on_network( + netuid, + &old_hotkey + )); + + step_block(20); + + let old_hotkey_stake_before_swap = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &old_hotkey, + &coldkey, + netuid, + ); + + assert_ok!(SubtensorModule::perform_hotkey_swap( + <::RuntimeOrigin>::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + true + )); + + let old_hotkey_stake_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &old_hotkey, + &coldkey, + netuid, + ); + assert_eq!( + old_hotkey_stake_after, old_hotkey_stake_before_swap, + "old_hotkey stake must NOT change during keep_stake swap" + ); + + let new_hotkey_stake_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &new_hotkey, + &coldkey, + netuid, + ); + assert_eq!( + new_hotkey_stake_after, + 0.into(), + "new_hotkey should have no stake" + ); + + assert!( + SubtensorModule::is_hotkey_registered_on_network(netuid, &new_hotkey), + "new_hotkey should be registered on netuid" + ); + + assert!( + !SubtensorModule::is_hotkey_registered_on_network(netuid, &old_hotkey), + "old_hotkey should NOT be registered on netuid after swap" + ); + + let root_total_alpha = TotalHotkeyAlpha::::get(old_hotkey, netuid); + let child_total_alpha = TotalHotkeyAlpha::::get(new_hotkey, netuid); + assert!( + root_total_alpha > 0.into(), + "old_hotkey should retain TotalHotkeyAlpha" + ); + assert_eq!( + child_total_alpha, + 0.into(), + "new_hotkey should have zero TotalHotkeyAlpha" + ); + + let root_voting_power = VotingPower::::get(netuid, old_hotkey); + let child_voting_power = VotingPower::::get(netuid, new_hotkey); + assert!( + root_voting_power > 0, + "old_hotkey should retain VotingPower" + ); + assert_eq!( + child_voting_power, 0, + "new_hotkey should have zero VotingPower" + ); + + let old_hotkey_children = ChildKeys::::get(old_hotkey, netuid); + assert!( + !old_hotkey_children.iter().any(|(_, c)| *c == child_key), + "old_hotkey should NOT retain ChildKeys after swap" + ); + let new_hotkey_children = ChildKeys::::get(new_hotkey, netuid); + assert!( + new_hotkey_children.iter().any(|(_, c)| *c == child_key), + "new_hotkey should inherit ChildKeys from old_hotkey" + ); + + let child_key_parents = ParentKeys::::get(child_key, netuid); + assert!( + child_key_parents.iter().any(|(_, p)| *p == new_hotkey), + "child_key should have new_hotkey as parent after swap" + ); + assert!( + !child_key_parents.iter().any(|(_, p)| *p == old_hotkey), + "child_key should NOT have old_hotkey as parent after swap" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_hotkey_with_existing_stake --exact --show-output +#[test] +fn test_swap_hotkey_with_existing_stake() { + new_test_ext(1).execute_with(|| { + let old_hotkey = U256::from(1); + let new_hotkey = U256::from(2); + let coldkey = U256::from(4); + let staker1 = U256::from(5); + let staker2 = U256::from(6); + let subnet_owner_coldkey = U256::from(1000); + let subnet_owner_hotkey = U256::from(1001); + let staked_tao_1 = 100_000_000; + let staked_tao_2 = 200_000_000; + let staked_tao_3 = 300_000_000; + let staked_tao_4 = 500_000_000; + + // Set up initial state + let netuid = add_dynamic_network(&subnet_owner_coldkey, &subnet_owner_hotkey); + register_ok_neuron(netuid, old_hotkey, coldkey, 1234); + register_ok_neuron(netuid, new_hotkey, coldkey, 1234); + + // Add balance to coldkeys + add_balance_to_coldkey_account(&coldkey, 10_000_000_000_u64.into()); + add_balance_to_coldkey_account(&staker1, 10_000_000_000_u64.into()); + add_balance_to_coldkey_account(&staker2, 10_000_000_000_u64.into()); + + // Stake with staker1 coldkey on old_hotkey + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(staker1), + old_hotkey, + netuid, + staked_tao_1.into() + )); + + // Stake with staker2 coldkey on old_hotkey + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(staker2), + old_hotkey, + netuid, + staked_tao_2.into() + )); + + // Stake with staker1 coldkey on new_hotkey + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(staker1), + new_hotkey, + netuid, + staked_tao_3.into() + )); + + // Stake with staker2 coldkey on new_hotkey + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(staker2), + new_hotkey, + netuid, + staked_tao_4.into() + )); + + // Emulate effect of emission into alpha pool - makes numerators and denominators not equal to alpha + let emission = AlphaBalance::from(1_000_000_000); + SubtensorModule::increase_stake_for_hotkey_on_subnet(&old_hotkey, netuid, emission); + SubtensorModule::increase_stake_for_hotkey_on_subnet(&new_hotkey, netuid, emission); + + // Hotkey new_hotkey gets deregistered, stake stays + IsNetworkMember::::remove(new_hotkey, netuid); + + let hk1_stake_1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &old_hotkey, + &staker1, + netuid, + ); + let hk2_stake_1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &new_hotkey, + &staker1, + netuid, + ); + let hk1_stake_2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &old_hotkey, + &staker2, + netuid, + ); + let hk2_stake_2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &new_hotkey, + &staker2, + netuid, + ); + + assert!(!hk1_stake_1.is_zero()); + assert!(!hk2_stake_1.is_zero()); + assert!(!hk1_stake_2.is_zero()); + assert!(!hk2_stake_2.is_zero()); + + let total_hk1_stake = SubtensorModule::get_total_stake_for_hotkey(&old_hotkey); + let total_hk2_stake = SubtensorModule::get_total_stake_for_hotkey(&new_hotkey); + assert!(!total_hk1_stake.is_zero()); + assert!(!total_hk2_stake.is_zero()); + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + + assert_ok!(SubtensorModule::perform_hotkey_swap( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + )); + + // Check correctness of stake transfer + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &old_hotkey, + &staker1, + netuid + ), + 0.into() + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &old_hotkey, + &staker2, + netuid + ), + 0.into() + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &new_hotkey, + &staker1, + netuid + ), + hk2_stake_1 + hk1_stake_1 + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &new_hotkey, + &staker2, + netuid + ), + hk2_stake_2 + hk1_stake_2 + ); + + // Check total stake transfer + assert_abs_diff_eq!( + SubtensorModule::get_total_stake_for_hotkey(&old_hotkey), + 0.into(), + epsilon = 1.into() + ); + assert_abs_diff_eq!( + SubtensorModule::get_total_stake_for_hotkey(&new_hotkey), + total_hk1_stake + total_hk2_stake, + epsilon = 1.into() + ); + }); +} diff --git a/pallets/subtensor/src/tests/tao.rs b/pallets/subtensor/src/tests/tao.rs index 7d4be6c963..885794c42e 100644 --- a/pallets/subtensor/src/tests/tao.rs +++ b/pallets/subtensor/src/tests/tao.rs @@ -1,3 +1,7 @@ +//! TAO issuance / ED-sensitive balance tests using [`crate::tests::mock_high_ed`]. +//! +//! Covers max issuance, dust, and fungible inspect edge cases. + #![allow( unused, clippy::indexing_slicing, diff --git a/pallets/subtensor/src/tests/tempo_control.rs b/pallets/subtensor/src/tests/tempo_control.rs index 6921bf7c75..2abefa8de7 100644 --- a/pallets/subtensor/src/tests/tempo_control.rs +++ b/pallets/subtensor/src/tests/tempo_control.rs @@ -1,5 +1,10 @@ +//! Tests for tempo / trigger-epoch / activity-cutoff ([`crate::coinbase::tempo_control`]). +//! +//! Also freezes deprecated `set_tempo` call indices for metadata compatibility. + #![allow(clippy::expect_used)] #![allow(deprecated)] + use codec::Encode; use frame_support::{ assert_noop, assert_ok, diff --git a/pallets/subtensor/src/tests/uids.rs b/pallets/subtensor/src/tests/uids.rs index edf41ebb01..63766de9ae 100644 --- a/pallets/subtensor/src/tests/uids.rs +++ b/pallets/subtensor/src/tests/uids.rs @@ -1,3 +1,7 @@ +//! Tests for neuron uid maps ([`crate::subnets::uids`]). +//! +//! Covers `replace_neuron`, uid↔hotkey indexes, and prune interactions. + #![allow(clippy::expect_used, clippy::unwrap_used, clippy::indexing_slicing)] use super::mock::*; @@ -43,16 +47,16 @@ fn test_replace_neuron() { // set non-default values Emission::::mutate(netuid, |v| { - SubtensorModule::set_element_at(v, neuron_uid as usize, 5.into()) + SubtensorModule::set_vec_element_at(v, neuron_uid as usize, 5.into()) }); Consensus::::mutate(netuid, |v| { - SubtensorModule::set_element_at(v, neuron_uid as usize, PerU16::from_parts(5)) + SubtensorModule::set_vec_element_at(v, neuron_uid as usize, PerU16::from_parts(5)) }); Incentive::::mutate(NetUidStorageIndex::from(netuid), |v| { - SubtensorModule::set_element_at(v, neuron_uid as usize, PerU16::from_parts(5)) + SubtensorModule::set_vec_element_at(v, neuron_uid as usize, PerU16::from_parts(5)) }); Dividends::::mutate(netuid, |v| { - SubtensorModule::set_element_at(v, neuron_uid as usize, PerU16::from_parts(5)) + SubtensorModule::set_vec_element_at(v, neuron_uid as usize, PerU16::from_parts(5)) }); Bonds::::insert(NetUidStorageIndex::from(netuid), neuron_uid, vec![(0, 1)]); diff --git a/pallets/subtensor/src/tests/voting_power.rs b/pallets/subtensor/src/tests/voting_power.rs index 9af3639b99..70a274b9c9 100644 --- a/pallets/subtensor/src/tests/voting_power.rs +++ b/pallets/subtensor/src/tests/voting_power.rs @@ -1,3 +1,7 @@ +//! Tests for voting-power EMA tracking ([`crate::utils::voting_power`]). +//! +//! Covers enable/disable grace, threshold dips, hotkey-swap transfer, and deregister clear. + #![allow(unused, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used)] use alloc::collections::BTreeMap; @@ -22,7 +26,7 @@ use crate::*; const DEFAULT_STAKE_AMOUNT: u64 = 1_000_000_000_000; // 1 million RAO /// Build epoch output from current state for testing voting power updates. -fn build_mock_epoch_output(netuid: NetUid) -> BTreeMap { +fn build_voting_power_epoch_output(netuid: NetUid) -> BTreeMap { let n = SubtensorModule::get_subnetwork_n(netuid); let validator_permits = ValidatorPermit::::get(netuid); @@ -106,7 +110,7 @@ impl VotingPowerTestFixture { /// Run voting power update for N epochs fn run_epochs(&self, n: u32) { for _ in 0..n { - let epoch_output = build_mock_epoch_output(self.netuid); + let epoch_output = build_voting_power_epoch_output(self.netuid); SubtensorModule::update_voting_power_for_subnet(self.netuid, &epoch_output); } } @@ -430,7 +434,7 @@ fn test_only_validators_get_voting_power() { ValidatorPermit::::insert(netuid, vec![true, false]); // Run epoch - let epoch_output = build_mock_epoch_output(netuid); + let epoch_output = build_voting_power_epoch_output(netuid); SubtensorModule::update_voting_power_for_subnet(netuid, &epoch_output); // Only validator should have voting power @@ -581,7 +585,7 @@ fn test_voting_power_not_removed_with_small_dip_below_threshold() { VotingPower::::insert(f.netuid, f.hotkey, above_threshold); // Build epoch output with stake that will produce EMA around 95% of threshold - let mut epoch_output = build_mock_epoch_output(f.netuid); + let mut epoch_output = build_voting_power_epoch_output(f.netuid); if let Some(terms) = epoch_output.get_mut(&f.hotkey) { terms.stake = small_dip.into(); // Stake drops but stays in buffer zone } diff --git a/pallets/subtensor/src/tests/weights.rs b/pallets/subtensor/src/tests/weights.rs deleted file mode 100644 index 23318eca4a..0000000000 --- a/pallets/subtensor/src/tests/weights.rs +++ /dev/null @@ -1,6248 +0,0 @@ -#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] - -use ark_serialize::CanonicalDeserialize; -use ark_serialize::CanonicalSerialize; -use codec::Compact; -use frame_support::{ - assert_err, assert_ok, - dispatch::{DispatchClass, DispatchResult, GetDispatchInfo, Pays}, -}; -use pallet_drand::types::Pulse; -use rand_chacha::{ChaCha20Rng, rand_core::SeedableRng}; -use scale_info::prelude::collections::HashMap; -use sha2::Digest; -use sp_core::Encode; -use sp_core::{H256, U256}; -use sp_runtime::{ - BoundedVec, DispatchError, - traits::{BlakeTwo256, ConstU32, Hash}, -}; -use sp_std::collections::vec_deque::VecDeque; -use substrate_fixed::types::I32F32; -use subtensor_runtime_common::NetUidStorageIndex; -use tle::{ - curves::drand::TinyBLS381, - ibe::fullident::Identity, - stream_ciphers::AESGCMStreamCipherProvider, - tlock::{tld, tle}, -}; -use w3f_bls::EngineBLS; - -use super::mock::*; -use crate::coinbase::reveal_commits::{LegacyWeightsTlockPayload, WeightsTlockPayload}; -use crate::*; -/*************************** - pub fn set_weights() tests -*****************************/ - -// Test the call passes through the subtensor module. -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_set_weights_dispatch_info_ok --exact --show-output --nocapture -#[test] -fn test_set_weights_dispatch_info_ok() { - new_test_ext(0).execute_with(|| { - let dests = vec![1, 1]; - let weights = vec![1, 1]; - let netuid = NetUid::from(1); - let version_key: u64 = 0; - let call = RuntimeCall::SubtensorModule(SubtensorCall::set_weights { - netuid, - dests, - weights, - version_key, - }); - let dispatch_info = call.get_dispatch_info(); - - assert_eq!(dispatch_info.class, DispatchClass::Normal); - assert_eq!(dispatch_info.pays_fee, Pays::No); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_commit_weights_dispatch_info_ok --exact --show-output --nocapture -#[test] -fn test_commit_weights_dispatch_info_ok() { - new_test_ext(0).execute_with(|| { - let dests = vec![1, 1]; - let weights = vec![1, 1]; - let netuid = NetUid::from(1); - let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; - let version_key: u64 = 0; - let hotkey: U256 = U256::from(1); - - let commit_hash: H256 = - BlakeTwo256::hash_of(&(hotkey, netuid, dests, weights, salt, version_key)); - - let call = RuntimeCall::SubtensorModule(SubtensorCall::commit_weights { - netuid, - commit_hash, - }); - let dispatch_info = call.get_dispatch_info(); - - assert_eq!(dispatch_info.class, DispatchClass::Normal); - assert_eq!(dispatch_info.pays_fee, Pays::No); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_weights_dispatch_info_ok --exact --show-output --nocapture -#[test] -fn test_reveal_weights_dispatch_info_ok() { - new_test_ext(0).execute_with(|| { - let dests = vec![1, 1]; - let weights = vec![1, 1]; - let netuid = NetUid::from(1); - let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; - let version_key: u64 = 0; - - let call = RuntimeCall::SubtensorModule(SubtensorCall::reveal_weights { - netuid, - uids: dests, - values: weights, - salt, - version_key, - }); - let dispatch_info = call.get_dispatch_info(); - - assert_eq!(dispatch_info.class, DispatchClass::Normal); - assert_eq!(dispatch_info.pays_fee, Pays::No); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_set_weights_is_root_error --exact --show-output --nocapture -#[test] -fn test_set_weights_is_root_error() { - new_test_ext(0).execute_with(|| { - let uids = vec![0]; - let weights = vec![1]; - let version_key: u64 = 0; - let hotkey = U256::from(1); - SubtensorModule::set_commit_reveal_weights_enabled(NetUid::ROOT, false); - - assert_err!( - SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - NetUid::ROOT, - uids.clone(), - weights.clone(), - version_key, - ), - Error::::CanNotSetRootNetworkWeights - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_weights_err_no_validator_permit --exact --show-output --nocapture -// Test ensures that uid has validator permit to set non-self weights. -#[test] -fn test_weights_err_no_validator_permit() { - new_test_ext(0).execute_with(|| { - let hotkey_account_id = U256::from(55); - let netuid = NetUid::from(1); - let tempo: u16 = 13; - add_network_disable_commit_reveal(netuid, tempo, 0); - SubtensorModule::set_min_allowed_weights(netuid, 0); - SubtensorModule::set_max_allowed_uids(netuid, 3); - register_ok_neuron(netuid, hotkey_account_id, U256::from(66), 0); - register_ok_neuron(netuid, U256::from(1), U256::from(1), 65555); - register_ok_neuron(netuid, U256::from(2), U256::from(2), 75555); - - let weights_keys: Vec = vec![1, 2]; - let weight_values: Vec = vec![1, 2]; - - let result = SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey_account_id), - netuid, - weights_keys, - weight_values, - 0, - ); - assert_eq!(result, Err(Error::::NeuronNoValidatorPermit.into())); - - let weights_keys: Vec = vec![1, 2]; - let weight_values: Vec = vec![1, 2]; - let neuron_uid: u16 = - SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey_account_id) - .expect("Not registered."); - SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid, true); - let result = SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey_account_id), - netuid, - weights_keys, - weight_values, - 0, - ); - assert_ok!(result); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_set_stake_threshold_failed --exact --show-output --nocapture -#[test] -fn test_set_stake_threshold_failed() { - new_test_ext(0).execute_with(|| { - let dests = vec![0]; - let weights = vec![1]; - let netuid = NetUid::from(1); - let version_key: u64 = 0; - let hotkey = U256::from(0); - let coldkey = U256::from(0); - - add_network_disable_commit_reveal(netuid, 1, 0); - register_ok_neuron(netuid, hotkey, coldkey, 2143124); - SubtensorModule::set_stake_threshold(20_000_000_000_000); - add_balance_to_coldkey_account(&hotkey, 20_000_000_000_000_000_u64.into()); - - // Check the signed extension function. - assert_eq!(SubtensorModule::get_stake_threshold(), 20_000_000_000_000); - assert!(!SubtensorModule::check_weights_min_stake(&hotkey, netuid)); - assert_ok!(SubtensorModule::do_add_stake( - RuntimeOrigin::signed(hotkey), - hotkey, - netuid, - 19_000_000_000_000_u64.into() - )); - assert!(!SubtensorModule::check_weights_min_stake(&hotkey, netuid)); - assert_ok!(SubtensorModule::do_add_stake( - RuntimeOrigin::signed(hotkey), - hotkey, - netuid, - 20_000_000_000_000_u64.into() - )); - assert!(SubtensorModule::check_weights_min_stake(&hotkey, netuid)); - - // Check that it fails at the pallet level. - SubtensorModule::set_stake_threshold(100_000_000_000_000); - assert_eq!( - SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid, - dests.clone(), - weights.clone(), - version_key, - ), - Err(Error::::NotEnoughStakeToSetWeights.into()) - ); - // Now passes - assert_ok!(SubtensorModule::do_add_stake( - RuntimeOrigin::signed(hotkey), - hotkey, - netuid, - 100_000_000_000_000_u64.into() - )); - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid, - dests.clone(), - weights.clone(), - version_key - )); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_weights_version_key --exact --show-output --nocapture -// Test ensures that a uid can only set weights if it has the valid weights set version key. -#[test] -fn test_weights_version_key() { - new_test_ext(0).execute_with(|| { - let hotkey = U256::from(55); - let coldkey = U256::from(66); - let netuid0 = NetUid::from(1); - let netuid1 = NetUid::from(2); - - add_network_disable_commit_reveal(netuid0, 1, 0); - add_network_disable_commit_reveal(netuid1, 1, 0); - register_ok_neuron(netuid0, hotkey, coldkey, 2143124); - register_ok_neuron(netuid1, hotkey, coldkey, 3124124); - - let weights_keys: Vec = vec![0]; - let weight_values: Vec = vec![1]; - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid0, - weights_keys.clone(), - weight_values.clone(), - 0 - )); - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid1, - weights_keys.clone(), - weight_values.clone(), - 0 - )); - - // Set version keys. - let key0: u64 = 12312; - let key1: u64 = 20313; - SubtensorModule::set_weights_version_key(netuid0, key0); - SubtensorModule::set_weights_version_key(netuid1, key1); - - // Setting works with version key. - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid0, - weights_keys.clone(), - weight_values.clone(), - key0 - )); - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid1, - weights_keys.clone(), - weight_values.clone(), - key1 - )); - - // validator:20313 >= network:12312 (accepted: validator newer) - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid0, - weights_keys.clone(), - weight_values.clone(), - key1 - )); - - // Setting fails with incorrect keys. - // validator:12312 < network:20313 (rejected: validator not updated) - assert_eq!( - SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid1, - weights_keys.clone(), - weight_values.clone(), - key0 - ), - Err(Error::::IncorrectWeightVersionKey.into()) - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_weights_err_setting_weights_too_fast --exact --show-output --nocapture -// Test ensures that uid has validator permit to set non-self weights. -#[test] -fn test_weights_err_setting_weights_too_fast() { - new_test_ext(0).execute_with(|| { - let hotkey_account_id = U256::from(55); - let netuid = NetUid::from(1); - let tempo: u16 = 13; - add_network_disable_commit_reveal(netuid, tempo, 0); - SubtensorModule::set_min_allowed_weights(netuid, 0); - SubtensorModule::set_max_allowed_uids(netuid, 3); - register_ok_neuron(netuid, hotkey_account_id, U256::from(66), 0); - register_ok_neuron(netuid, U256::from(1), U256::from(1), 65555); - register_ok_neuron(netuid, U256::from(2), U256::from(2), 75555); - - let neuron_uid: u16 = - SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey_account_id) - .expect("Not registered."); - SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid, true); - add_balance_to_coldkey_account(&U256::from(66), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &(U256::from(66)), - netuid, - 1.into(), - ); - SubtensorModule::set_weights_set_rate_limit(netuid, 10); - assert_eq!(SubtensorModule::get_weights_set_rate_limit(netuid), 10); - - let weights_keys: Vec = vec![1, 2]; - let weight_values: Vec = vec![1, 2]; - - // Note that LastUpdate has default 0 for new uids, but if they have actually set weights on block 0 - // then they are allowed to set weights again once more without a wait restriction, to accommodate the default. - let result = SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey_account_id), - netuid, - weights_keys.clone(), - weight_values.clone(), - 0, - ); - assert_ok!(result); - run_to_block(1); - - for i in 1..100 { - let result = SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey_account_id), - netuid, - weights_keys.clone(), - weight_values.clone(), - 0, - ); - if i % 10 == 1 { - assert_ok!(result); - } else { - assert_eq!(result, Err(Error::::SettingWeightsTooFast.into())); - } - run_to_block(i + 1); - } - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_weights_err_weights_vec_not_equal_size --exact --show-output --nocapture -// Test ensures that uids -- weights must have the same size. -#[test] -fn test_weights_err_weights_vec_not_equal_size() { - new_test_ext(0).execute_with(|| { - let hotkey_account_id = U256::from(55); - let netuid = NetUid::from(1); - let tempo: u16 = 13; - let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; - add_network(netuid, tempo, 0); - register_ok_neuron(netuid, hotkey_account_id, U256::from(66), 0); - let neuron_uid: u16 = - SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey_account_id) - .expect("Not registered."); - SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid, true); - let weights_keys: Vec = vec![1, 2, 3, 4, 5, 6]; - let weight_values: Vec = vec![1, 2, 3, 4, 5]; // Uneven sizes - let result = commit_reveal_set_weights( - hotkey_account_id, - 1.into(), - weights_keys.clone(), - weight_values.clone(), - salt.clone(), - 0, - ); - assert_eq!(result, Err(Error::::WeightVecNotEqualSize.into())); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_weights_err_has_duplicate_ids --exact --show-output --nocapture -// Test ensures that uids can have not duplicates -#[test] -fn test_weights_err_has_duplicate_ids() { - new_test_ext(0).execute_with(|| { - let hotkey_account_id = U256::from(666); - let netuid = NetUid::from(1); - let tempo: u16 = 13; - let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; - add_network(netuid, tempo, 0); - - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_max_allowed_uids(netuid, 100); // Allow many registrations per block. - SubtensorModule::set_max_registrations_per_block(netuid, 100); // Allow many registrations per block. - SubtensorModule::set_target_registrations_per_interval(netuid, 100); // Allow many registrations per block. - // uid 0 - register_ok_neuron(netuid, hotkey_account_id, U256::from(77), 0); - let neuron_uid: u16 = - SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey_account_id) - .expect("Not registered."); - SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid, true); - add_balance_to_coldkey_account(&U256::from(77), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &(U256::from(77)), - netuid, - 1.into(), - ); - - // uid 1 - register_ok_neuron(netuid, U256::from(1), U256::from(1), 100_000); - SubtensorModule::get_uid_for_net_and_hotkey(netuid, &U256::from(1)) - .expect("Not registered."); - - // uid 2 - register_ok_neuron(netuid, U256::from(2), U256::from(1), 200_000); - SubtensorModule::get_uid_for_net_and_hotkey(netuid, &U256::from(2)) - .expect("Not registered."); - - // uid 3 - register_ok_neuron(netuid, U256::from(3), U256::from(1), 300_000); - SubtensorModule::get_uid_for_net_and_hotkey(netuid, &U256::from(3)) - .expect("Not registered."); - - assert_eq!(SubtensorModule::get_subnetwork_n(netuid), 4); - - let weights_keys: Vec = vec![1, 1, 1]; // Contains duplicates - let weight_values: Vec = vec![1, 2, 3]; - let result = commit_reveal_set_weights( - hotkey_account_id, - netuid, - weights_keys.clone(), - weight_values.clone(), - salt.clone(), - 0, - ); - assert_eq!(result, Err(Error::::DuplicateUids.into())); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_weights_err_max_weight_limit --exact --show-output --nocapture -// Test ensures weights cannot exceed max weight limit. -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_no_signature --exact --show-output --nocapture -// Tests the call requires a valid origin. -#[test] -fn test_no_signature() { - new_test_ext(0).execute_with(|| { - let uids: Vec = vec![]; - let values: Vec = vec![]; - SubtensorModule::set_commit_reveal_weights_enabled(1.into(), false); - let result = SubtensorModule::set_weights(RuntimeOrigin::none(), 1.into(), uids, values, 0); - assert_eq!(result, Err(DispatchError::BadOrigin)); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_set_weights_err_not_active --exact --show-output --nocapture -// Tests that weights cannot be set BY non-registered hotkeys. -#[test] -fn test_set_weights_err_not_active() { - new_test_ext(0).execute_with(|| { - let netuid = NetUid::from(1); - let tempo: u16 = 13; - let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; - add_network(netuid, tempo, 0); - - // Register one neuron. Should have uid 0 - register_ok_neuron(netuid, U256::from(666), U256::from(2), 100000); - SubtensorModule::get_uid_for_net_and_hotkey(netuid, &U256::from(666)) - .expect("Not registered."); - - let weights_keys: Vec = vec![0]; // Uid 0 is valid. - let weight_values: Vec = vec![1]; - // This hotkey is NOT registered. - let result = commit_reveal_set_weights( - U256::from(1), - 1.into(), - weights_keys, - weight_values, - salt, - 0, - ); - assert_eq!( - result, - Err(Error::::HotKeyNotRegisteredInSubNet.into()) - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_set_weights_err_invalid_uid --exact --show-output --nocapture -// Tests that set weights fails if you pass invalid uids. -#[test] -fn test_set_weights_err_invalid_uid() { - new_test_ext(0).execute_with(|| { - let hotkey_account_id = U256::from(55); - let netuid = NetUid::from(1); - let tempo: u16 = 13; - let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; - add_network(netuid, tempo, 0); - register_ok_neuron(netuid, hotkey_account_id, U256::from(66), 0); - let neuron_uid: u16 = - SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey_account_id) - .expect("Not registered."); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid, true); - add_balance_to_coldkey_account(&U256::from(66), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey_account_id, - &(U256::from(66)), - netuid, - 1.into(), - ); - let weight_keys: Vec = vec![9999]; // Does not exist - let weight_values: Vec = vec![88]; // random value - let result = commit_reveal_set_weights( - hotkey_account_id, - netuid, - weight_keys, - weight_values, - salt, - 0, - ); - assert_eq!(result, Err(Error::::UidVecContainInvalidOne.into())); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_set_weight_not_enough_values --exact --show-output --nocapture -// Tests that set weights fails if you don't pass enough values. -#[test] -fn test_set_weight_not_enough_values() { - new_test_ext(0).execute_with(|| { - let netuid = NetUid::from(1); - let tempo: u16 = 13; - let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; - let account_id = U256::from(1); - add_network_disable_commit_reveal(netuid, tempo, 0); - - register_ok_neuron(netuid, account_id, U256::from(2), 100000); - let neuron_uid: u16 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &U256::from(1)) - .expect("Not registered."); - SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid, true); - add_balance_to_coldkey_account(&U256::from(2), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &account_id, - &(U256::from(2)), - netuid, - 1.into(), - ); - - register_ok_neuron(netuid, U256::from(3), U256::from(4), 300000); - SubtensorModule::set_min_allowed_weights(netuid, 2); - - // Should fail because we are only setting a single value and its not the self weight. - let weight_keys: Vec = vec![1]; // not weight. - let weight_values: Vec = vec![88]; // random value. - let result = SubtensorModule::set_weights( - RuntimeOrigin::signed(account_id), - 1.into(), - weight_keys, - weight_values, - 0, - ); - assert_eq!(result, Err(Error::::WeightVecLengthIsLow.into())); - - // Shouldnt fail because we setting a single value but it is the self weight. - let weight_keys: Vec = vec![0]; // self weight. - let weight_values: Vec = vec![88]; // random value. - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(account_id), - 1.into(), - weight_keys, - weight_values, - 0 - )); - - // Should pass because we are setting enough values. - let weight_keys: Vec = vec![0, 1]; // self weight. - let weight_values: Vec = vec![10, 10]; // random value. - SubtensorModule::set_min_allowed_weights(netuid, 1); - assert_ok!(commit_reveal_set_weights( - account_id, - 1.into(), - weight_keys, - weight_values, - salt, - 0 - )); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_set_weight_too_many_uids --exact --show-output --nocapture -// Tests that the weights set fails if you pass too many uids for the subnet -#[test] -fn test_set_weight_too_many_uids() { - new_test_ext(0).execute_with(|| { - let netuid = NetUid::from(1); - let tempo: u16 = 13; - add_network_disable_commit_reveal(netuid, tempo, 0); - - register_ok_neuron(1.into(), U256::from(1), U256::from(2), 100_000); - let neuron_uid: u16 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &U256::from(1)) - .expect("Not registered."); - SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid, true); - - register_ok_neuron(1.into(), U256::from(3), U256::from(4), 300_000); - SubtensorModule::set_min_allowed_weights(1.into(), 2); - // Should fail because we are setting more weights than there are neurons. - let weight_keys: Vec = vec![0, 1, 2, 3, 4]; // more uids than neurons in subnet. - let weight_values: Vec = vec![88, 102, 303, 1212, 11]; // random value. - let result = SubtensorModule::set_weights( - RuntimeOrigin::signed(U256::from(1)), - 1.into(), - weight_keys, - weight_values, - 0, - ); - assert_eq!( - result, - Err(Error::::UidsLengthExceedUidsInSubNet.into()) - ); - - // Shouldnt fail because we are setting less weights than there are neurons. - let weight_keys: Vec = vec![0, 1]; // Only on neurons that exist. - let weight_values: Vec = vec![10, 10]; // random value. - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(U256::from(1)), - 1.into(), - weight_keys, - weight_values, - 0 - )); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_set_weights_sum_larger_than_u16_max --exact --show-output --nocapture -// Tests that the weights set doesn't panic if you pass weights that sum to larger than u16 max. -#[test] -fn test_set_weights_sum_larger_than_u16_max() { - new_test_ext(0).execute_with(|| { - let netuid = NetUid::from(1); - let tempo: u16 = 13; - let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; - add_network(netuid, tempo, 0); - - register_ok_neuron(1.into(), U256::from(1), U256::from(2), 100_000); - let neuron_uid: u16 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &U256::from(1)) - .expect("Not registered."); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid, true); - add_balance_to_coldkey_account(&U256::from(2), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(1)), - &(U256::from(2)), - netuid, - 1.into(), - ); - - register_ok_neuron(1.into(), U256::from(3), U256::from(4), 300_000); - SubtensorModule::set_min_allowed_weights(1.into(), 2); - - // Shouldn't fail because we are setting the right number of weights. - let weight_keys: Vec = vec![0, 1]; - let weight_values: Vec = vec![u16::MAX, u16::MAX]; - // sum of weights is larger than u16 max. - assert!(weight_values.iter().map(|x| *x as u64).sum::() > (u16::MAX as u64)); - - let result = - commit_reveal_set_weights(U256::from(1), 1.into(), weight_keys, weight_values, salt, 0); - assert_ok!(result); - - // Get max-upscaled unnormalized weights. - let all_weights: Vec> = SubtensorModule::get_weights(netuid.into()); - let weights_set: &[I32F32] = &all_weights[neuron_uid as usize]; - assert_eq!(weights_set[0], I32F32::from_num(u16::MAX)); - assert_eq!(weights_set[1], I32F32::from_num(u16::MAX)); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_do_commit_crv3_weights_disabled --exact --show-output --nocapture -/// Check _truthy_ path for self weight -#[test] -fn test_check_length_allows_singleton() { - new_test_ext(0).execute_with(|| { - let netuid = NetUid::from(1); - - let max_allowed: u16 = 1; - let min_allowed_weights = max_allowed; - - SubtensorModule::set_min_allowed_weights(netuid, min_allowed_weights); - - let uids: Vec = Vec::from_iter((0..max_allowed).map(|id| id + 1)); - let uid: u16 = uids[0]; - let weights: Vec = Vec::from_iter((0..max_allowed).map(|id| id + 1)); - - let expected = true; - let result = SubtensorModule::check_length(netuid, uid, &uids, &weights); - - assert_eq!(expected, result, "Failed get expected result"); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_check_length_weights_length_exceeds_min_allowed --exact --show-output --nocapture -/// Check _truthy_ path for weights within allowed range -#[test] -fn test_check_length_weights_length_exceeds_min_allowed() { - new_test_ext(0).execute_with(|| { - let netuid = NetUid::from(1); - - let max_allowed: u16 = 3; - let min_allowed_weights = max_allowed; - - SubtensorModule::set_min_allowed_weights(netuid, min_allowed_weights); - - let uids: Vec = Vec::from_iter((0..max_allowed).map(|id| id + 1)); - let uid: u16 = uids[0]; - let weights: Vec = Vec::from_iter((0..max_allowed).map(|id| id + 1)); - - let expected = true; - let result = SubtensorModule::check_length(netuid, uid, &uids, &weights); - - assert_eq!(expected, result, "Failed get expected result"); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_check_length_to_few_weights --exact --show-output --nocapture -/// Check _falsey_ path for weights outside allowed range -#[test] -fn test_check_length_to_few_weights() { - new_test_ext(0).execute_with(|| { - let netuid = NetUid::from(1); - - let min_allowed_weights = 3; - - add_network(netuid, 1, 0); - SubtensorModule::set_target_registrations_per_interval(netuid, 100); - SubtensorModule::set_max_registrations_per_block(netuid, 100); - // register morw than min allowed - register_ok_neuron(1.into(), U256::from(1), U256::from(1), 300_000); - register_ok_neuron(1.into(), U256::from(2), U256::from(2), 300_001); - register_ok_neuron(1.into(), U256::from(3), U256::from(3), 300_002); - register_ok_neuron(1.into(), U256::from(4), U256::from(4), 300_003); - register_ok_neuron(1.into(), U256::from(5), U256::from(5), 300_004); - register_ok_neuron(1.into(), U256::from(6), U256::from(6), 300_005); - register_ok_neuron(1.into(), U256::from(7), U256::from(7), 300_006); - SubtensorModule::set_min_allowed_weights(netuid, min_allowed_weights); - - let uids: Vec = Vec::from_iter((0..2).map(|id| id + 1)); - let weights: Vec = Vec::from_iter((0..2).map(|id| id + 1)); - let uid: u16 = uids[0]; - - let expected = false; - let result = SubtensorModule::check_length(netuid, uid, &uids, &weights); - - assert_eq!(expected, result, "Failed get expected result"); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_normalize_weights_does_not_mutate_when_sum_is_zero --exact --show-output --nocapture -/// Check do nothing path -#[test] -fn test_normalize_weights_does_not_mutate_when_sum_is_zero() { - new_test_ext(0).execute_with(|| { - let max_allowed: u16 = 3; - - let weights: Vec = Vec::from_iter((0..max_allowed).map(|_| 0)); - - let expected = weights.clone(); - let result = SubtensorModule::normalize_weights(weights); - - assert_eq!( - expected, result, - "Failed get expected result when everything _should_ be fine" - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_normalize_weights_does_not_mutate_when_sum_not_zero --exact --show-output --nocapture -/// Check do something path -#[test] -fn test_normalize_weights_does_not_mutate_when_sum_not_zero() { - new_test_ext(0).execute_with(|| { - let max_allowed: u16 = 3; - - let weights: Vec = Vec::from_iter(0..max_allowed); - - let expected = weights.clone(); - let result = SubtensorModule::normalize_weights(weights); - - assert_eq!(expected.len(), result.len(), "Length of weights changed?!"); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_max_weight_limited_allow_self_weights_to_exceed_max_weight_limit --exact --show-output --nocapture -/// Check _truthy_ path for weights length -#[test] -fn test_max_weight_limited_allow_self_weights_to_exceed_max_weight_limit() { - new_test_ext(0).execute_with(|| { - let max_allowed: u16 = 1; - - let netuid = NetUid::from(1); - let uids: Vec = Vec::from_iter((0..max_allowed).map(|id| id + 1)); - let uid: u16 = uids[0]; - let weights: Vec = vec![0]; - - let expected = true; - let result = SubtensorModule::max_weight_limited(netuid, uid, &uids, &weights); - - assert_eq!( - expected, result, - "Failed get expected result when everything _should_ be fine" - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_max_weight_limited_when_weight_limit_is_u16_max --exact --show-output --nocapture -/// Check _truthy_ path for max weight limit -#[test] -fn test_max_weight_limited_when_weight_limit_is_u16_max() { - new_test_ext(0).execute_with(|| { - let max_allowed: u16 = 3; - - let netuid = NetUid::from(1); - let uids: Vec = Vec::from_iter((0..max_allowed).map(|id| id + 1)); - let uid: u16 = uids[0]; - let weights: Vec = Vec::from_iter((0..max_allowed).map(|_id| u16::MAX)); - - let expected = true; - let result = SubtensorModule::max_weight_limited(netuid, uid, &uids, &weights); - - assert_eq!( - expected, result, - "Failed get expected result when everything _should_ be fine" - ); - }); -} - -#[test] -fn test_get_max_weight_limit_is_constant() { - new_test_ext(0).execute_with(|| { - assert_eq!( - SubtensorModule::get_max_weight_limit(NetUid::from(1)), - u16::MAX - ); - assert_eq!( - SubtensorModule::get_max_weight_limit(NetUid::ROOT), - u16::MAX - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_is_self_weight_weights_length_not_one --exact --show-output --nocapture -/// Check _falsey_ path for weights length -#[test] -fn test_is_self_weight_weights_length_not_one() { - new_test_ext(0).execute_with(|| { - let max_allowed: u16 = 3; - - let uids: Vec = Vec::from_iter((0..max_allowed).map(|id| id + 1)); - let uid: u16 = uids[0]; - let weights: Vec = Vec::from_iter((0..max_allowed).map(|id| id + 1)); - - let expected = false; - let result = SubtensorModule::is_self_weight(uid, &uids, &weights); - - assert_eq!( - expected, result, - "Failed get expected result when `weights.len() != 1`" - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_is_self_weight_uid_not_in_uids --exact --show-output --nocapture -/// Check _falsey_ path for uid vs uids[0] -#[test] -fn test_is_self_weight_uid_not_in_uids() { - new_test_ext(0).execute_with(|| { - let max_allowed: u16 = 3; - - let uids: Vec = Vec::from_iter((0..max_allowed).map(|id| id + 1)); - let uid: u16 = uids[1]; - let weights: Vec = vec![0]; - - let expected = false; - let result = SubtensorModule::is_self_weight(uid, &uids, &weights); - - assert_eq!( - expected, result, - "Failed get expected result when `uid != uids[0]`" - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_is_self_weight_uid_in_uids --exact --show-output --nocapture -/// Check _truthy_ path -/// @TODO: double-check if this really be desired behavior -#[test] -fn test_is_self_weight_uid_in_uids() { - new_test_ext(0).execute_with(|| { - let max_allowed: u16 = 1; - - let uids: Vec = Vec::from_iter((0..max_allowed).map(|id| id + 1)); - let uid: u16 = uids[0]; - let weights: Vec = vec![0]; - - let expected = true; - let result = SubtensorModule::is_self_weight(uid, &uids, &weights); - - assert_eq!( - expected, result, - "Failed get expected result when everything _should_ be fine" - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_check_len_uids_within_allowed_within_network_pool --exact --show-output --nocapture -/// Check _truthy_ path -#[test] -fn test_check_len_uids_within_allowed_within_network_pool() { - new_test_ext(0).execute_with(|| { - let netuid = NetUid::from(1); - - let tempo: u16 = 13; - let modality: u16 = 0; - - let max_registrations_per_block: u16 = 100; - - add_network(netuid, tempo, modality); - - /* @TODO: use a loop maybe */ - register_ok_neuron(netuid, U256::from(1), U256::from(1), 0); - register_ok_neuron(netuid, U256::from(3), U256::from(3), 65555); - register_ok_neuron(netuid, U256::from(5), U256::from(5), 75555); - let max_allowed: u16 = SubtensorModule::get_subnetwork_n(netuid); - - SubtensorModule::set_max_allowed_uids(netuid, max_allowed); - SubtensorModule::set_max_registrations_per_block(netuid, max_registrations_per_block); - - let uids: Vec = Vec::from_iter(0..max_allowed); - - let expected = true; - let result = SubtensorModule::check_len_uids_within_allowed(netuid, &uids); - assert_eq!( - expected, result, - "netuid network length and uids length incompatible" - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_check_len_uids_within_allowed_not_within_network_pool --exact --show-output --nocapture -#[test] -fn test_check_len_uids_within_allowed_not_within_network_pool() { - new_test_ext(0).execute_with(|| { - let netuid = NetUid::from(1); - - let tempo: u16 = 13; - let modality: u16 = 0; - - let max_registrations_per_block: u16 = 100; - - add_network(netuid, tempo, modality); - - /* @TODO: use a loop maybe */ - register_ok_neuron(netuid, U256::from(1), U256::from(1), 0); - register_ok_neuron(netuid, U256::from(3), U256::from(3), 65555); - register_ok_neuron(netuid, U256::from(5), U256::from(5), 75555); - let max_allowed: u16 = SubtensorModule::get_subnetwork_n(netuid); - - SubtensorModule::set_max_allowed_uids(netuid, max_allowed); - SubtensorModule::set_max_registrations_per_block(netuid, max_registrations_per_block); - - let uids: Vec = Vec::from_iter(0..(max_allowed + 1)); - - let expected = false; - let result = SubtensorModule::check_len_uids_within_allowed(netuid, &uids); - assert_eq!( - expected, result, - "Failed to detect incompatible uids for network" - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_set_weights_commit_reveal_enabled_error --exact --show-output --nocapture -#[test] -fn test_set_weights_commit_reveal_enabled_error() { - new_test_ext(0).execute_with(|| { - let netuid = NetUid::from(1); - add_network(netuid, 1, 0); - register_ok_neuron(netuid, U256::from(1), U256::from(2), 10); - - let uids = vec![0]; - let weights = vec![1]; - let version_key: u64 = 0; - let hotkey = U256::from(1); - - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - - assert_err!( - SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weights.clone(), - version_key - ), - Error::::CommitRevealEnabled - ); - - SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); - - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids, - weights, - version_key - )); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_weights_when_commit_reveal_disabled --exact --show-output --nocapture -#[test] -fn test_reveal_weights_when_commit_reveal_disabled() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let uids: Vec = vec![0, 1]; - let weight_values: Vec = vec![10, 10]; - let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; - let version_key: u64 = 0; - let hotkey: U256 = U256::from(1); - - let commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - )); - - System::set_block_number(0); - - let tempo: u16 = 5; - add_network(netuid, tempo, 0); - - // Register neurons and set up configurations - register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); - register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); - - // Enable commit-reveal and commit - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - - step_epochs(1, netuid); - - // Disable commit-reveal before reveal - SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); - - // Attempt to reveal, should fail with CommitRevealDisabled - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids, - weight_values, - salt, - version_key, - ), - Error::::CommitRevealDisabled - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_commit_reveal_weights_ok --exact --show-output --nocapture -#[test] -fn test_commit_reveal_weights_ok() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let uids: Vec = vec![0, 1]; - let weight_values: Vec = vec![10, 10]; - let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; - let version_key: u64 = 0; - let hotkey: U256 = U256::from(1); - - let commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - )); - - System::set_block_number(0); - - let tempo: u16 = 5; - add_network(netuid, tempo, 0); - - // Register neurons and set up configurations - register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); - register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - add_balance_to_coldkey_account(&U256::from(0), 1.into()); - add_balance_to_coldkey_account(&U256::from(1), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(0)), - &(U256::from(0)), - netuid, - 1.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(1)), - &(U256::from(1)), - netuid, - 1.into(), - ); - - // Commit at block 0 - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - - step_epochs(1, netuid); - - // Reveal in the next epoch - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids, - weight_values, - salt, - version_key, - )); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_commit_reveal_tempo_interval --exact --show-output --nocapture -#[test] -fn test_commit_reveal_tempo_interval() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let uids: Vec = vec![0, 1]; - let weight_values: Vec = vec![10, 10]; - let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; - let version_key: u64 = 0; - let hotkey: U256 = U256::from(1); - - let commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - )); - - System::set_block_number(0); - - let tempo: u16 = 100; - add_network(netuid, tempo, 0); - - register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); - register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - add_balance_to_coldkey_account(&U256::from(0), 1.into()); - add_balance_to_coldkey_account(&U256::from(1), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(0)), - &(U256::from(0)), - netuid, - 1.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(1)), - &(U256::from(1)), - netuid, - 1.into(), - ); - - // Commit at block 0 - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - - // Attempt to reveal in the same epoch, should fail - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - ), - Error::::RevealTooEarly - ); - - step_epochs(1, netuid); - - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - )); - - step_block(6); - - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - ), - Error::::NoWeightsCommitFound - ); - - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - - // step two epochs - step_epochs(2, netuid); - - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - ), - Error::::ExpiredWeightCommit - ); - - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - - step_block(50); - - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - ), - Error::::RevealTooEarly - ); - - step_epochs(1, netuid); - - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids, - weight_values, - salt, - version_key, - )); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_commit_reveal_hash --exact --show-output --nocapture -#[test] -fn test_commit_reveal_hash() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let uids: Vec = vec![0, 1]; - let weight_values: Vec = vec![10, 10]; - let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; - let bad_salt: Vec = vec![0, 2, 3, 4, 5, 6, 7, 8]; - let version_key: u64 = 0; - let hotkey: U256 = U256::from(1); - - add_network(netuid, 5, 0); - System::set_block_number(0); - - register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); - register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); - add_balance_to_coldkey_account(&U256::from(0), 1.into()); - add_balance_to_coldkey_account(&U256::from(1), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(0)), - &(U256::from(0)), - netuid, - 1.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(1)), - &(U256::from(1)), - netuid, - 1.into(), - ); - - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - - let commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - )); - - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - - step_epochs(1, netuid); - - // Attempt to reveal with incorrect data, should fail - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - vec![0, 2], - weight_values.clone(), - salt.clone(), - version_key - ), - Error::::InvalidRevealCommitHashNotMatch - ); - - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - bad_salt.clone(), - version_key, - ), - Error::::InvalidRevealCommitHashNotMatch - ); - - // Correct reveal, should succeed - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids, - weight_values, - salt, - version_key, - )); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_commit_reveal_disabled_or_enabled --exact --show-output --nocapture -#[test] -fn test_commit_reveal_disabled_or_enabled() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let uids: Vec = vec![0, 1]; - let weight_values: Vec = vec![10, 10]; - let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; - let version_key: u64 = 0; - let hotkey: U256 = U256::from(1); - - let commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - )); - - add_network(netuid, 5, 0); - System::set_block_number(0); - - register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); - register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); - add_balance_to_coldkey_account(&U256::from(0), 1.into()); - add_balance_to_coldkey_account(&U256::from(1), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(0)), - &(U256::from(0)), - netuid, - 1.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(1)), - &(U256::from(1)), - netuid, - 1.into(), - ); - - // Disable commit/reveal - SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); - - // Attempt to commit, should fail - assert_err!( - SubtensorModule::commit_weights(RuntimeOrigin::signed(hotkey), netuid, commit_hash), - Error::::CommitRevealDisabled - ); - - // Enable commit/reveal - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - - // Commit should now succeed - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - - step_epochs(1, netuid); - - // Reveal should succeed - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids, - weight_values, - salt, - version_key, - )); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_toggle_commit_reveal_weights_and_set_weights --exact --show-output --nocapture -#[test] -fn test_toggle_commit_reveal_weights_and_set_weights() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let uids: Vec = vec![0, 1]; - let weight_values: Vec = vec![10, 10]; - let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; - let version_key: u64 = 0; - let hotkey: U256 = U256::from(1); - - let commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - )); - - add_network(netuid, 5, 0); - System::set_block_number(0); - - register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); - register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); - add_balance_to_coldkey_account(&U256::from(0), 1.into()); - add_balance_to_coldkey_account(&U256::from(1), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(0)), - &(U256::from(0)), - netuid, - 1.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(1)), - &(U256::from(1)), - netuid, - 1.into(), - ); - - // Enable commit/reveal - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - - // Commit at block 0 - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - - step_epochs(1, netuid); - - // Reveal in the next epoch - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - )); - - // Disable commit/reveal - SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); - - // Advance to allow setting weights (due to rate limit) - step_block(5); - - // Set weights directly - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids, - weight_values, - version_key, - )); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_tempo_change_during_commit_reveal_process --exact --show-output --nocapture -#[test] -fn test_tempo_change_during_commit_reveal_process() { - new_test_ext(0).execute_with(|| { - let netuid = NetUid::from(1); - let uids: Vec = vec![0, 1]; - let weight_values: Vec = vec![10, 10]; - let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; - let version_key: u64 = 0; - let hotkey: U256 = U256::from(1); - - let commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - )); - - System::set_block_number(0); - - let tempo: u16 = 100; - add_network(netuid, tempo, 0); - - register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); - register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - add_balance_to_coldkey_account(&U256::from(0), 1.into()); - add_balance_to_coldkey_account(&U256::from(1), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(0)), - &(U256::from(0)), - netuid, - 1.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(1)), - &(U256::from(1)), - netuid, - 1.into(), - ); - - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - log::info!( - "Commit successful at block {}", - SubtensorModule::get_current_block_as_u64() - ); - - step_block(9); - log::info!( - "Advanced to block {}", - SubtensorModule::get_current_block_as_u64() - ); - - let tempo_before_next_reveal: u16 = 200; - log::info!("Changing tempo to {tempo_before_next_reveal}"); - SubtensorModule::set_tempo_unchecked(netuid, tempo_before_next_reveal); - - step_epochs(1, netuid); - log::info!( - "Advanced to block {}", - SubtensorModule::get_current_block_as_u64() - ); - - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - )); - log::info!( - "Revealed at block {}", - SubtensorModule::get_current_block_as_u64() - ); - - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - log::info!( - "Commit successful at block {}", - SubtensorModule::get_current_block_as_u64() - ); - - let tempo: u16 = 150; - log::info!("Changing tempo to {tempo}"); - SubtensorModule::set_tempo_unchecked(netuid, tempo); - - step_epochs(1, netuid); - log::info!( - "Advanced to block {}", - SubtensorModule::get_current_block_as_u64() - ); - - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - )); - log::info!( - "Revealed at block {}", - SubtensorModule::get_current_block_as_u64() - ); - - let tempo: u16 = 1050; - log::info!("Changing tempo to {tempo}"); - SubtensorModule::set_tempo_unchecked(netuid, tempo); - - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - log::info!( - "Commit successful at block {}", - SubtensorModule::get_current_block_as_u64() - ); - - let tempo: u16 = 805; - log::info!("Changing tempo to {tempo}"); - SubtensorModule::set_tempo_unchecked(netuid, tempo); - - step_epochs(1, netuid); - log::info!( - "Advanced to block {}", - SubtensorModule::get_current_block_as_u64() - ); - - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - )); - log::info!( - "Revealed at block {}", - SubtensorModule::get_current_block_as_u64() - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_commit_reveal_multiple_commits --exact --show-output --nocapture -#[test] -fn test_commit_reveal_multiple_commits() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let uids: Vec = vec![0, 1]; - let weight_values: Vec = vec![10, 10]; - let version_key: u64 = 0; - let hotkey: U256 = U256::from(1); - - System::set_block_number(0); - - let tempo: u16 = 7200; - add_network(netuid, tempo, 0); - - // Setup the network and neurons - register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); - register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - add_balance_to_coldkey_account(&U256::from(0), 1.into()); - add_balance_to_coldkey_account(&U256::from(1), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(0)), - &(U256::from(0)), - netuid, - 1.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(1)), - &(U256::from(1)), - netuid, - 1.into(), - ); - - // 1. Commit 10 times successfully - let mut commit_info = Vec::new(); - for i in 0..10 { - let salt_i: Vec = vec![i; 8]; // Unique salt for each commit - let commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt_i.clone(), - version_key, - )); - commit_info.push((commit_hash, salt_i)); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - } - - // 2. Attempt to commit an 11th time, should fail - let salt_11: Vec = vec![11; 8]; - let commit_hash_11: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt_11.clone(), - version_key, - )); - assert_err!( - SubtensorModule::commit_weights(RuntimeOrigin::signed(hotkey), netuid, commit_hash_11), - Error::::TooManyUnrevealedCommits - ); - - // 3. Attempt to reveal out of order (reveal the second commit first) - // Advance to the next epoch for reveals to be valid - step_epochs(1, netuid); - - // Try to reveal the second commit first - let (_commit_hash_2, salt_2) = &commit_info[1]; - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt_2.clone(), - version_key, - )); - - // Check that commits before the revealed one are removed - let remaining_commits = - crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey) - .expect("expected 8 remaining commits"); - assert_eq!(remaining_commits.len(), 8); // 10 commits - 2 removed (index 0 and 1) - - // 4. Reveal the last commit next - let (_commit_hash_10, salt_10) = &commit_info[9]; - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt_10.clone(), - version_key, - )); - - // Remaining commits should have removed up to index 9 - let remaining_commits = - crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey); - assert!(remaining_commits.is_none()); // All commits removed - - // After revealing all commits, attempt to commit again should now succeed - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash_11 - )); - - // 5. Test expired commits are removed and do not block reveals - // Commit again and let the commit expire - let salt_12: Vec = vec![12; 8]; - let commit_hash_12: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt_12.clone(), - version_key, - )); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash_12 - )); - - // Advance two epochs so the commit expires - step_epochs(2, netuid); - - // Attempt to reveal the expired commit, should fail - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt_12.clone(), - version_key, - ), - Error::::ExpiredWeightCommit - ); - - // Commit again and reveal after advancing to next epoch - let salt_13: Vec = vec![13; 8]; - let commit_hash_13: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt_13.clone(), - version_key, - )); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash_13 - )); - - step_epochs(1, netuid); - - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt_13.clone(), - version_key, - )); - - // 6. Ensure that attempting to reveal after the valid reveal period fails - // Commit again - let salt_14: Vec = vec![14; 8]; - let commit_hash_14: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt_14.clone(), - version_key, - )); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash_14 - )); - - // Advance beyond the valid reveal period (more than one epoch) - step_epochs(2, netuid); - - // Attempt to reveal, should fail - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt_14.clone(), - version_key, - ), - Error::::ExpiredWeightCommit - ); - - // 7. Attempt to reveal a commit that is not ready yet (before the reveal period) - // Commit again - let salt_15: Vec = vec![15; 8]; - let commit_hash_15: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt_15.clone(), - version_key, - )); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash_15 - )); - - // Attempt to reveal immediately, should fail - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt_15.clone(), - version_key, - ), - Error::::RevealTooEarly - ); - - step_epochs(1, netuid); - - // Now reveal should succeed - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt_15.clone(), - version_key, - )); - - // 8. Test that revealing with incorrect data (salt) fails - // Commit again - let salt_16: Vec = vec![16; 8]; - let commit_hash_16: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt_16.clone(), - version_key, - )); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash_16 - )); - - step_epochs(1, netuid); - - // Attempt to reveal with incorrect salt - let wrong_salt: Vec = vec![99; 8]; - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - wrong_salt.clone(), - version_key, - ), - Error::::InvalidRevealCommitHashNotMatch - ); - - // Reveal with correct data should succeed - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt_16.clone(), - version_key, - )); - - // 9. Test that attempting to reveal when there are no commits fails - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt_16.clone(), - version_key, - ), - Error::::NoWeightsCommitFound - ); - - // 10. Commit twice and attempt to reveal out of sequence (which is now allowed) - let salt_a: Vec = vec![21; 8]; - let commit_hash_a: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt_a.clone(), - version_key, - )); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash_a - )); - - let salt_b: Vec = vec![22; 8]; - let commit_hash_b: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt_b.clone(), - version_key, - )); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash_b - )); - - step_epochs(1, netuid); - - // Reveal the second commit first, should now succeed - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt_b.clone(), - version_key, - )); - - // Check that the first commit has been removed - let remaining_commits = - crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey); - assert!(remaining_commits.is_none()); - - // Attempting to reveal the first commit should fail as it was removed - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids, - weight_values, - salt_a, - version_key, - ), - Error::::NoWeightsCommitFound - ); - }); -} - -fn commit_reveal_set_weights( - hotkey: U256, - netuid: NetUid, - uids: Vec, - weights: Vec, - salt: Vec, - version_key: u64, -) -> DispatchResult { - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - - let commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weights.clone(), - salt.clone(), - version_key, - )); - - SubtensorModule::commit_weights(RuntimeOrigin::signed(hotkey), netuid, commit_hash)?; - - step_epochs(1, netuid); - - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids, - weights, - salt, - version_key, - )?; - - Ok(()) -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_expired_commits_handling_in_commit_and_reveal --exact --show-output --nocapture -#[test] -fn test_expired_commits_handling_in_commit_and_reveal() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey: ::AccountId = U256::from(1); - let version_key: u64 = 0; - let uids: Vec = vec![0, 1]; - let weight_values: Vec = vec![10, 10]; - let tempo: u16 = 100; - - System::set_block_number(0); - add_network(netuid, tempo, 0); - - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - // Register neurons - register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); - register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); - add_balance_to_coldkey_account(&U256::from(0), 1.into()); - add_balance_to_coldkey_account(&U256::from(1), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(0)), - &(U256::from(0)), - netuid, - 1.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(1)), - &(U256::from(1)), - netuid, - 1.into(), - ); - - // 1. Commit 5 times in epoch 0 - let mut commit_info = Vec::new(); - for i in 0..5 { - let salt: Vec = vec![i; 8]; - let commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - )); - commit_info.push((commit_hash, salt)); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - } - - // Advance to epoch 1 - step_epochs(1, netuid); - - // 2. Commit another 5 times in epoch 1 - for i in 5..10 { - let salt: Vec = vec![i; 8]; - let commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - )); - commit_info.push((commit_hash, salt)); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - } - - // 3. Attempt to commit an 11th time, should fail with TooManyUnrevealedCommits - let salt_11: Vec = vec![11; 8]; - let commit_hash_11: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt_11.clone(), - version_key, - )); - assert_err!( - SubtensorModule::commit_weights(RuntimeOrigin::signed(hotkey), netuid, commit_hash_11), - Error::::TooManyUnrevealedCommits - ); - - // 4. Advance to epoch 2 to expire the commits from epoch 0 - step_epochs(1, netuid); // Now at epoch 2 - - // 5. Attempt to commit again; should succeed after expired commits are removed - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash_11 - )); - - // 6. Verify that the number of unrevealed, non-expired commits is now 6 - let commits: VecDeque<(H256, u64, u64, u64)> = - crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey) - .expect("Expected a commit"); - assert_eq!(commits.len(), 6); // 5 non-expired commits from epoch 1 + new commit - - // 7. Attempt to reveal an expired commit (from epoch 0) - // Previous commit removed expired commits - let (_, expired_salt) = &commit_info[0]; - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - expired_salt.clone(), - version_key, - ), - Error::::InvalidRevealCommitHashNotMatch - ); - - // 8. Reveal commits from epoch 1 at current_epoch = 2 - for (_, salt) in commit_info.iter().skip(5).take(5) { - let salt = salt.clone(); - - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - )); - } - - // 9. Advance to epoch 3 to reveal the new commit - step_epochs(1, netuid); - - // 10. Reveal the new commit from epoch 2 - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt_11.clone(), - version_key, - )); - - // 10. Verify that all commits have been revealed and the queue is empty - let commits = crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey); - assert!(commits.is_none()); - - // 11. Attempt to reveal again, should fail with NoWeightsCommitFound - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt_11.clone(), - version_key, - ), - Error::::NoWeightsCommitFound - ); - - // 12. Commit again to ensure we can continue after previous commits - let salt_12: Vec = vec![12; 8]; - let commit_hash_12: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt_12.clone(), - version_key, - )); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash_12 - )); - - // Advance to next epoch (epoch 4) and reveal - step_epochs(1, netuid); - - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids, - weight_values, - salt_12, - version_key, - )); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_at_exact_epoch --exact --show-output --nocapture -#[test] -fn test_reveal_at_exact_epoch() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey: ::AccountId = U256::from(1); - let version_key: u64 = 0; - let uids: Vec = vec![0, 1]; - let weight_values: Vec = vec![10, 10]; - let tempo: u16 = 100; - - System::set_block_number(0); - add_network(netuid, tempo, 0); - - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); - register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); - add_balance_to_coldkey_account(&U256::from(0), 1.into()); - add_balance_to_coldkey_account(&U256::from(1), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(0)), - &(U256::from(0)), - netuid, - 1.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(1)), - &(U256::from(1)), - netuid, - 1.into(), - ); - - let reveal_periods: Vec = vec![1, 2, 7, 40, 86, 100]; - - for &reveal_period in &reveal_periods { - assert_ok!(SubtensorModule::set_reveal_period(netuid, reveal_period)); - - let salt: Vec = vec![42; 8]; - let commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - )); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - - // Retrieve commit information - let commit_block = SubtensorModule::get_current_block_as_u64(); - let commit_epoch = SubtensorModule::get_epoch_index(netuid, commit_block); - let reveal_epoch = commit_epoch.saturating_add(reveal_period); - - // Attempt to reveal before the allowed epoch - if reveal_period > 0 { - // Advance to epoch before the reveal epoch - if reveal_period >= 1 { - step_epochs((reveal_period - 1) as u16, netuid); - } - - // Attempt to reveal too early - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - ), - Error::::RevealTooEarly - ); - } - - // Advance to the exact reveal epoch - let current_epoch = SubtensorModule::get_epoch_index( - netuid, - SubtensorModule::get_current_block_as_u64(), - ); - if current_epoch < reveal_epoch { - step_epochs((reveal_epoch - current_epoch) as u16, netuid); - } - - // Reveal at the exact allowed epoch - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - )); - - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - ), - Error::::NoWeightsCommitFound - ); - - let new_salt: Vec = vec![43; 8]; - let new_commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - new_salt.clone(), - version_key, - )); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - new_commit_hash - )); - - // Advance past the reveal epoch to ensure commit expiration - step_epochs((reveal_period + 1) as u16, netuid); - - // Attempt to reveal after the allowed epoch - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - new_salt.clone(), - version_key, - ), - Error::::ExpiredWeightCommit - ); - - crate::WeightCommits::::remove(NetUidStorageIndex::from(netuid), hotkey); - } - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_tempo_and_reveal_period_change_during_commit_reveal_process --exact --show-output --nocapture -#[test] -fn test_tempo_and_reveal_period_change_during_commit_reveal_process() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let uids: Vec = vec![0, 1]; - let weight_values: Vec = vec![10, 10]; - let salt: Vec = vec![42; 8]; - let version_key: u64 = 0; - let hotkey: ::AccountId = U256::from(1); - - // Compute initial commit hash - let commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - )); - - System::set_block_number(0); - - let initial_tempo: u16 = 100; - let initial_reveal_period: u64 = 1; - add_network(netuid, initial_tempo, 0); - assert_ok!(SubtensorModule::set_reveal_period(netuid, initial_reveal_period)); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); - register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); - add_balance_to_coldkey_account(&U256::from(0), 1.into()); - add_balance_to_coldkey_account(&U256::from(1), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(0)), - &(U256::from(0)), - netuid, - 1.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(1)), - &(U256::from(1)), - netuid, - 1.into(), - ); - - // Step 1: Commit weights - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - log::info!( - "Commit successful at block {}", - SubtensorModule::get_current_block_as_u64() - ); - - // Retrieve commit block and epoch - let commit_block = SubtensorModule::get_current_block_as_u64(); - let commit_epoch = SubtensorModule::get_epoch_index(netuid, commit_block); - - // Step 2: Change tempo and reveal period after commit - let new_tempo: u16 = 50; - let new_reveal_period: u64 = 2; - SubtensorModule::set_tempo_unchecked(netuid, new_tempo); - assert_ok!(SubtensorModule::set_reveal_period(netuid, new_reveal_period)); - log::info!( - "Changed tempo to {new_tempo} and reveal period to {new_reveal_period}" - ); - - // Step 3: Advance blocks to reach the reveal epoch according to new tempo and reveal period - let current_block = SubtensorModule::get_current_block_as_u64(); - let current_epoch = SubtensorModule::get_epoch_index(netuid, current_block); - let reveal_epoch = commit_epoch.saturating_add(new_reveal_period); - - // Advance to one epoch before reveal epoch - if current_epoch < reveal_epoch { - let epochs_to_advance = reveal_epoch - current_epoch - 1; - step_epochs(epochs_to_advance as u16, netuid); - } - - // Attempt to reveal too early - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key - ), - Error::::RevealTooEarly - ); - log::info!( - "Attempted to reveal too early at block {}", - SubtensorModule::get_current_block_as_u64() - ); - - // Advance to reveal epoch - step_epochs(1, netuid); - - // Attempt to reveal at the correct epoch - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key - )); - log::info!( - "Revealed weights at block {}", - SubtensorModule::get_current_block_as_u64() - ); - - // Step 4: Change tempo and reveal period again after reveal - let new_tempo_after_reveal: u16 = 200; - let new_reveal_period_after_reveal: u64 = 1; - SubtensorModule::set_tempo_unchecked(netuid, new_tempo_after_reveal); - assert_ok!(SubtensorModule::set_reveal_period( - netuid, - new_reveal_period_after_reveal - )); - log::info!("Changed tempo to {new_tempo_after_reveal} and reveal period to {new_reveal_period_after_reveal} after reveal"); - - // Step 5: Commit again - let new_salt: Vec = vec![43; 8]; - let new_commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - new_salt.clone(), - version_key, - )); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - new_commit_hash - )); - log::info!( - "Commit successful at block {}", - SubtensorModule::get_current_block_as_u64() - ); - - // Retrieve new commit block and epoch - let new_commit_block = SubtensorModule::get_current_block_as_u64(); - let new_commit_epoch = SubtensorModule::get_epoch_index(netuid, new_commit_block); - let new_reveal_epoch = new_commit_epoch.saturating_add(new_reveal_period_after_reveal); - - // Advance to reveal epoch - let current_block = SubtensorModule::get_current_block_as_u64(); - let current_epoch = SubtensorModule::get_epoch_index(netuid, current_block); - if current_epoch < new_reveal_epoch { - let epochs_to_advance = new_reveal_epoch - current_epoch; - step_epochs(epochs_to_advance as u16, netuid); - } - - // Attempt to reveal at the correct epoch - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - new_salt.clone(), - version_key - )); - log::info!( - "Revealed weights at block {}", - SubtensorModule::get_current_block_as_u64() - ); - - // Step 6: Attempt to reveal after the allowed epoch (commit expires) - // Advance past the reveal epoch - let expiration_epochs = 1; - step_epochs(expiration_epochs as u16, netuid); - - // Attempt to reveal again (should fail due to expired commit) - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - new_salt.clone(), - version_key - ), - Error::::NoWeightsCommitFound - ); - log::info!( - "Attempted to reveal after expiration at block {}", - SubtensorModule::get_current_block_as_u64() - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_commit_reveal_order_enforcement --exact --show-output --nocapture -#[test] -fn test_commit_reveal_order_enforcement() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey: ::AccountId = U256::from(1); - let version_key: u64 = 0; - let uids: Vec = vec![0, 1]; - let weight_values: Vec = vec![10, 10]; - let tempo: u16 = 100; - - System::set_block_number(0); - add_network(netuid, tempo, 0); - - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); - register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); - add_balance_to_coldkey_account(&U256::from(0), 1.into()); - add_balance_to_coldkey_account(&U256::from(1), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(0)), - &(U256::from(0)), - netuid, - 1.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(1)), - &(U256::from(1)), - netuid, - 1.into(), - ); - - // Commit three times: A, B, C - let mut commit_info = Vec::new(); - for i in 0..3 { - let salt: Vec = vec![i; 8]; - let commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - )); - commit_info.push((commit_hash, salt)); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - } - - step_epochs(1, netuid); - - // Attempt to reveal B first (index 1), should now succeed - let (_commit_hash_b, salt_b) = &commit_info[1]; - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt_b.clone(), - version_key, - )); - - // Check that commits A and B are removed - let remaining_commits = - crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey) - .expect("expected 1 remaining commit"); - assert_eq!(remaining_commits.len(), 1); // Only commit C should remain - - // Attempt to reveal C (index 2), should succeed - let (_commit_hash_c, salt_c) = &commit_info[2]; - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt_c.clone(), - version_key, - )); - - // Attempting to reveal A (index 0) should fail as it's been removed - let (_commit_hash_a, salt_a) = &commit_info[0]; - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids, - weight_values, - salt_a.clone(), - version_key, - ), - Error::::NoWeightsCommitFound - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_at_exact_block --exact --show-output --nocapture -#[test] -fn test_reveal_at_exact_block() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey: ::AccountId = U256::from(1); - let version_key: u64 = 0; - let uids: Vec = vec![0, 1]; - let weight_values: Vec = vec![10, 10]; - let tempo: u16 = 360; - - System::set_block_number(0); - add_network_disable_commit_reveal(netuid, tempo, 0); - - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); - register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); - - let reveal_periods: Vec = vec![1, 2, 5, 19, 21, 30, 77]; - - for &reveal_period in &reveal_periods { - assert_ok!(SubtensorModule::set_reveal_period(netuid, reveal_period)); - - // Step 1: Commit weights - let salt: Vec = vec![42 + (reveal_period % 100) as u16; 8]; - let commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - )); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - - // Epoch the commit was tagged with (counter is the canonical index). - let commit_epoch = - crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey) - .and_then(|q| q.back().map(|(_, e, _, _)| *e)) - .expect("commit stored"); - - // Attempt to reveal before the reveal epoch — too early. - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key - ), - Error::::RevealTooEarly - ); - - // Advance the epoch counter into the reveal epoch; pin the scheduler. - SubnetEpochIndex::::insert(netuid, commit_epoch + reveal_period); - LastEpochBlock::::insert(netuid, SubtensorModule::get_current_block_as_u64()); - PendingEpochAt::::insert(netuid, 0); - - // Reveal at the exact allowed epoch - assert_ok!(SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key - )); - - // Attempt to reveal again; should fail with NoWeightsCommitFound - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key - ), - Error::::NoWeightsCommitFound - ); - - // Commit again with new salt - let new_salt: Vec = vec![43 + (reveal_period % 100) as u16; 8]; - let new_commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - new_salt.clone(), - version_key, - )); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - new_commit_hash - )); - - // Advance the epoch counter past the reveal epoch — commit expired. - let new_commit_epoch = - crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey) - .and_then(|q| q.back().map(|(_, e, _, _)| *e)) - .expect("commit stored"); - SubnetEpochIndex::::insert(netuid, new_commit_epoch + reveal_period + 1); - LastEpochBlock::::insert(netuid, SubtensorModule::get_current_block_as_u64()); - - // Attempt to reveal after the commit has expired - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - new_salt.clone(), - version_key - ), - Error::::ExpiredWeightCommit - ); - - // Clean up for next iteration - crate::WeightCommits::::remove(NetUidStorageIndex::from(netuid), hotkey); - } - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_successful_batch_reveal --exact --show-output --nocapture -#[test] -fn test_successful_batch_reveal() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey = U256::from(1); - let version_keys: Vec = vec![0, 0, 0]; - let uids_list: Vec> = vec![vec![0, 1], vec![1, 0], vec![0, 1]]; - let weight_values_list: Vec> = vec![vec![10, 20], vec![30, 40], vec![50, 60]]; - let tempo: u16 = 100; - - System::set_block_number(0); - add_network(netuid, tempo, 0); - - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); - register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); - add_balance_to_coldkey_account(&U256::from(0), 1.into()); - add_balance_to_coldkey_account(&U256::from(1), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(0)), - &(U256::from(0)), - netuid, - 1.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(1)), - &(U256::from(1)), - netuid, - 1.into(), - ); - - // 1. Commit multiple times - let mut commit_info = Vec::new(); - for i in 0..3 { - let salt: Vec = vec![i as u16; 8]; - let commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids_list[i].clone(), - weight_values_list[i].clone(), - salt.clone(), - version_keys[i], - )); - commit_info.push((commit_hash, salt)); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - } - - step_epochs(1, netuid); - - // 2. Prepare batch reveal parameters - let salts_list: Vec> = commit_info.iter().map(|(_, salt)| salt.clone()).collect(); - - // 3. Perform batch reveal - assert_ok!(SubtensorModule::do_batch_reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids_list.clone(), - weight_values_list.clone(), - salts_list.clone(), - version_keys.clone(), - )); - - // 4. Ensure all commits are removed - let commits = crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey); - assert!(commits.is_none()); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_batch_reveal_with_expired_commits --exact --show-output --nocapture -#[test] -fn test_batch_reveal_with_expired_commits() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey = U256::from(1); - let version_keys: Vec = vec![0, 0, 0]; - let uids_list: Vec> = vec![vec![0, 1], vec![1, 0], vec![0, 1]]; - let weight_values_list: Vec> = vec![vec![10, 20], vec![30, 40], vec![50, 60]]; - let tempo: u16 = 100; - - System::set_block_number(0); - add_network(netuid, tempo, 0); - - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); - register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); - add_balance_to_coldkey_account(&U256::from(0), 1.into()); - add_balance_to_coldkey_account(&U256::from(1), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(0)), - &(U256::from(0)), - netuid, - 1.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(1)), - &(U256::from(1)), - netuid, - 1.into(), - ); - - let mut commit_info = Vec::new(); - - // 1. Commit the first weight in epoch 0 - let salt0: Vec = vec![0u16; 8]; - let commit_hash0: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids_list[0].clone(), - weight_values_list[0].clone(), - salt0.clone(), - version_keys[0], - )); - commit_info.push((commit_hash0, salt0)); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash0 - )); - - // Advance to epoch 1 - step_epochs(1, netuid); - - // 2. Commit the next two weights in epoch 1 - for i in 1..3 { - let salt: Vec = vec![i as u16; 8]; - let commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids_list[i].clone(), - weight_values_list[i].clone(), - salt.clone(), - version_keys[i], - )); - commit_info.push((commit_hash, salt)); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - } - - // Advance to epoch 2 (after reveal period for first commit) - step_epochs(1, netuid); - - // 3. Prepare batch reveal parameters - let salts_list: Vec> = commit_info.iter().map(|(_, salt)| salt.clone()).collect(); - - // 4. Perform batch reveal - let result = SubtensorModule::do_batch_reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids_list.clone(), - weight_values_list.clone(), - salts_list.clone(), - version_keys.clone(), - ); - assert_err!(result, Error::::ExpiredWeightCommit); - - // 5. Expired commit is not removed until a successful call - let commits = crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey) - .expect("Expected remaining commits"); - assert_eq!(commits.len(), 3); - - // 6. Try revealing the remaining commits - let valid_uids_list = uids_list[1..].to_vec(); - let valid_weight_values_list = weight_values_list[1..].to_vec(); - let valid_salts_list = salts_list[1..].to_vec(); - let valid_version_keys = version_keys[1..].to_vec(); - - assert_ok!(SubtensorModule::do_batch_reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - valid_uids_list, - valid_weight_values_list, - valid_salts_list, - valid_version_keys, - )); - - // 7. Ensure all commits are removed - let commits = crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey); - assert!(commits.is_none()); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_batch_reveal_with_invalid_input_lengths --exact --show-output --nocapture -#[test] -fn test_batch_reveal_with_invalid_input_lengths() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey = U256::from(1); - let tempo: u16 = 100; - - System::set_block_number(0); - add_network(netuid, tempo, 0); - - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - - // Base data for valid inputs - let uids_list: Vec> = vec![vec![0, 1], vec![1, 0]]; - let weight_values_list: Vec> = vec![vec![10, 20], vec![30, 40]]; - let salts_list: Vec> = vec![vec![0u16; 8], vec![1u16; 8]]; - let version_keys: Vec = vec![0, 0]; - - // Test cases with mismatched input lengths - - // Case 1: uids_list has an extra element - let uids_list_case = vec![vec![0, 1], vec![1, 0], vec![2, 3]]; - let result = SubtensorModule::do_batch_reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids_list_case.clone(), - weight_values_list.clone(), - salts_list.clone(), - version_keys.clone(), - ); - assert_err!(result, Error::::InputLengthsUnequal); - - // Case 2: weight_values_list has an extra element - let weight_values_list_case = vec![vec![10, 20], vec![30, 40], vec![50, 60]]; - let result = SubtensorModule::do_batch_reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids_list.clone(), - weight_values_list_case.clone(), - salts_list.clone(), - version_keys.clone(), - ); - assert_err!(result, Error::::InputLengthsUnequal); - - // Case 3: salts_list has an extra element - let salts_list_case = vec![vec![0u16; 8], vec![1u16; 8], vec![2u16; 8]]; - let result = SubtensorModule::do_batch_reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids_list.clone(), - weight_values_list.clone(), - salts_list_case.clone(), - version_keys.clone(), - ); - assert_err!(result, Error::::InputLengthsUnequal); - - // Case 4: version_keys has an extra element - let version_keys_case = vec![0, 0, 0]; - let result = SubtensorModule::do_batch_reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids_list.clone(), - weight_values_list.clone(), - salts_list.clone(), - version_keys_case.clone(), - ); - assert_err!(result, Error::::InputLengthsUnequal); - - // Case 5: All input vectors have mismatched lengths - let uids_list_case = vec![vec![0, 1]]; - let weight_values_list_case = vec![vec![10, 20], vec![30, 40]]; - let salts_list_case = vec![vec![0u16; 8]]; - let version_keys_case = vec![0, 0, 0]; - let result = SubtensorModule::do_batch_reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids_list_case, - weight_values_list_case, - salts_list_case, - version_keys_case, - ); - assert_err!(result, Error::::InputLengthsUnequal); - - // Case 6: Valid input lengths (should not return an error) - let result = SubtensorModule::do_batch_reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids_list.clone(), - weight_values_list.clone(), - salts_list.clone(), - version_keys.clone(), - ); - // We expect an error because no commits have been made, but it should not be InputLengthsUnequal - assert_err!(result, Error::::NoWeightsCommitFound); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_batch_reveal_with_no_commits --exact --show-output --nocapture -#[test] -fn test_batch_reveal_with_no_commits() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey = U256::from(1); - let version_keys: Vec = vec![0]; - let uids_list: Vec> = vec![vec![0, 1]]; - let weight_values_list: Vec> = vec![vec![10, 20]]; - let salts_list: Vec> = vec![vec![0u16; 8]]; - let tempo: u16 = 100; - - System::set_block_number(0); - add_network(netuid, tempo, 0); - - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - - // 1. Attempt to perform batch reveal without any commits - let result = SubtensorModule::do_batch_reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids_list, - weight_values_list, - salts_list, - version_keys, - ); - assert_err!(result, Error::::NoWeightsCommitFound); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_batch_reveal_before_reveal_period --exact --show-output --nocapture -#[test] -fn test_batch_reveal_before_reveal_period() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey = U256::from(1); - let version_keys: Vec = vec![0, 0]; - let uids_list: Vec> = vec![vec![0, 1], vec![1, 0]]; - let weight_values_list: Vec> = vec![vec![10, 20], vec![30, 40]]; - let tempo: u16 = 100; - - System::set_block_number(0); - add_network(netuid, tempo, 0); - - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - - register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); - register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); - - // 1. Commit multiple times in the same epoch - let mut commit_info = Vec::new(); - for i in 0..2 { - let salt: Vec = vec![i as u16; 8]; - let commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids_list[i].clone(), - weight_values_list[i].clone(), - salt.clone(), - version_keys[i], - )); - commit_info.push((commit_hash, salt)); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - } - - // 2. Prepare batch reveal parameters - let salts_list: Vec> = commit_info.iter().map(|(_, salt)| salt.clone()).collect(); - - // 3. Attempt to reveal before reveal period - let result = SubtensorModule::do_batch_reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids_list.clone(), - weight_values_list.clone(), - salts_list.clone(), - version_keys.clone(), - ); - assert_err!(result, Error::::RevealTooEarly); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_batch_reveal_after_commits_expired --exact --show-output --nocapture -#[test] -fn test_batch_reveal_after_commits_expired() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey = U256::from(1); - let version_keys: Vec = vec![0, 0]; - let uids_list: Vec> = vec![vec![0, 1], vec![1, 0]]; - let weight_values_list: Vec> = vec![vec![10, 20], vec![30, 40]]; - let tempo: u16 = 100; - - System::set_block_number(0); - add_network(netuid, tempo, 0); - - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - - register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); - register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); - - let mut commit_info = Vec::new(); - - // 1. Commit the first weight in epoch 0 - let salt0: Vec = vec![0u16; 8]; - let commit_hash0: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids_list[0].clone(), - weight_values_list[0].clone(), - salt0.clone(), - version_keys[0], - )); - commit_info.push((commit_hash0, salt0)); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash0 - )); - - // Advance to epoch 1 - step_epochs(1, netuid); - - // 2. Commit the second weight in epoch 1 - let salt1: Vec = vec![1u16; 8]; - let commit_hash1: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids_list[1].clone(), - weight_values_list[1].clone(), - salt1.clone(), - version_keys[1], - )); - commit_info.push((commit_hash1, salt1)); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash1 - )); - - // Advance to epoch 4 to ensure both commits have expired (assuming reveal_period is 1) - step_epochs(3, netuid); - - // 3. Prepare batch reveal parameters - let salts_list: Vec> = commit_info.iter().map(|(_, salt)| salt.clone()).collect(); - - // 4. Attempt to reveal after commits have expired - let result = SubtensorModule::do_batch_reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids_list.clone(), - weight_values_list.clone(), - salts_list, - version_keys.clone(), - ); - assert_err!(result, Error::::ExpiredWeightCommit); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_batch_reveal_when_commit_reveal_disabled --exact --show-output --nocapture -#[test] -fn test_batch_reveal_when_commit_reveal_disabled() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey = U256::from(1); - let version_keys: Vec = vec![0]; - let uids_list: Vec> = vec![vec![0, 1]]; - let weight_values_list: Vec> = vec![vec![10, 20]]; - let salts_list: Vec> = vec![vec![0u16; 8]]; - let tempo: u16 = 100; - - System::set_block_number(0); - add_network(netuid, tempo, 0); - - SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); - - // 1. Attempt to perform batch reveal when commit-reveal is disabled - let result = SubtensorModule::do_batch_reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids_list, - weight_values_list, - salts_list, - version_keys, - ); - assert_err!(result, Error::::CommitRevealDisabled); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_batch_reveal_with_out_of_order_commits --exact --show-output --nocapture -#[test] -fn test_batch_reveal_with_out_of_order_commits() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey = U256::from(1); - let version_keys: Vec = vec![0, 0, 0]; - let uids_list: Vec> = vec![vec![0, 1], vec![1, 0], vec![0, 1]]; - let weight_values_list: Vec> = vec![vec![10, 20], vec![30, 40], vec![50, 60]]; - let tempo: u16 = 100; - - System::set_block_number(0); - add_network(netuid, tempo, 0); - - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); - register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); - add_balance_to_coldkey_account(&U256::from(0), 1.into()); - add_balance_to_coldkey_account(&U256::from(1), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(0)), - &(U256::from(0)), - netuid, - 1.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(1)), - &(U256::from(1)), - netuid, - 1.into(), - ); - - // 1. Commit multiple times (A, B, C) - let mut commit_info = Vec::new(); - for i in 0..3 { - let salt: Vec = vec![i as u16; 8]; - let commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids_list[i].clone(), - weight_values_list[i].clone(), - salt.clone(), - version_keys[i], - )); - commit_info.push((commit_hash, salt)); - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - } - - step_epochs(1, netuid); - - // 2. Prepare batch reveal parameters for commits A and C (out of order) - let salts_list: Vec> = vec![ - commit_info[2].1.clone(), // Third commit (C) - commit_info[0].1.clone(), // First commit (A) - ]; - let uids_list_out_of_order = vec![ - uids_list[2].clone(), // C - uids_list[0].clone(), // A - ]; - let weight_values_list_out_of_order = vec![ - weight_values_list[2].clone(), // C - weight_values_list[0].clone(), // A - ]; - let version_keys_out_of_order = vec![ - version_keys[2], // C - version_keys[0], // A - ]; - - // 3. Attempt batch reveal of A and C out of order - let result = SubtensorModule::do_batch_reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids_list_out_of_order, - weight_values_list_out_of_order, - salts_list, - version_keys_out_of_order, - ); - - // 4. Ensure the batch reveal succeeds - assert_ok!(result); - - // 5. Prepare and reveal the remaining commit (B) - let remaining_salt = commit_info[1].1.clone(); - let remaining_uids = uids_list[1].clone(); - let remaining_weights = weight_values_list[1].clone(); - let remaining_version_key = version_keys[1]; - - assert_ok!(SubtensorModule::do_batch_reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - vec![remaining_uids], - vec![remaining_weights], - vec![remaining_salt], - vec![remaining_version_key], - )); - - // 6. Ensure all commits are removed - let commits = crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey); - assert!(commits.is_none()); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_highly_concurrent_commits_and_reveals_with_multiple_hotkeys --exact --show-output --nocapture -#[test] -fn test_highly_concurrent_commits_and_reveals_with_multiple_hotkeys() { - new_test_ext(1).execute_with(|| { - // ==== Test Configuration ==== - let netuid = NetUid::from(1); - let num_hotkeys: usize = 10; - let max_unrevealed_commits: usize = 10; - let commits_per_hotkey: usize = 20; - let initial_reveal_period: u64 = 5; - let initial_tempo: u16 = 100; - - // ==== Setup Network ==== - add_network(netuid, initial_tempo, 0); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - assert_ok!(SubtensorModule::set_reveal_period(netuid, initial_reveal_period)); - SubtensorModule::set_max_registrations_per_block(netuid, u16::MAX); - SubtensorModule::set_target_registrations_per_interval(netuid, u16::MAX); - - // ==== Register Validators ==== - for uid in 0..5 { - let validator_id = U256::from(100 + uid as u64); - register_ok_neuron(netuid, validator_id, U256::from(200 + uid as u64), 300_000); - SubtensorModule::set_validator_permit_for_uid(netuid, uid, true); - } - - // ==== Register Hotkeys ==== - let mut hotkeys: Vec<::AccountId> = Vec::new(); - for i in 0..num_hotkeys { - let hotkey_id = U256::from(1000 + i as u64); - register_ok_neuron(netuid, hotkey_id, U256::from(2000 + i as u64), 100_000); - hotkeys.push(hotkey_id); - } - - // ==== Initialize Commit Information ==== - let mut commit_info_map: HashMap< - ::AccountId, - Vec<(H256, Vec, Vec, Vec, u64)>, - > = HashMap::new(); - - // Initialize the map - for hotkey in &hotkeys { - commit_info_map.insert(*hotkey, Vec::new()); - } - - // ==== Function to Generate Unique Data ==== - fn generate_unique_data(index: usize) -> (Vec, Vec, Vec, u64) { - let uids = vec![index as u16, (index + 1) as u16]; - let values = vec![(index * 10) as u16, ((index + 1) * 10) as u16]; - let salt = vec![(index % 100) as u16; 8]; - let version_key = index as u64; - (uids, values, salt, version_key) - } - - // ==== Simulate Concurrent Commits and Reveals ==== - for i in 0..commits_per_hotkey { - for hotkey in &hotkeys { - - let current_commits = crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey) - .unwrap_or_default(); - if current_commits.len() >= max_unrevealed_commits { - continue; - } - - let (uids, values, salt, version_key) = generate_unique_data(i); - let commit_hash: H256 = BlakeTwo256::hash_of(&( - *hotkey, - netuid, - uids.clone(), - values.clone(), - salt.clone(), - version_key, - )); - - if let Some(commits) = commit_info_map.get_mut(hotkey) { - commits.push((commit_hash, salt.clone(), uids.clone(), values.clone(), version_key)); - } - - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(*hotkey), - netuid, - commit_hash - )); - } - - // ==== Reveal Phase ==== - for hotkey in &hotkeys { - if let Some(commits) = commit_info_map.get_mut(hotkey) { - if commits.is_empty() { - continue; // No commits to reveal - } - - let (_commit_hash, salt, uids, values, version_key) = commits.first().expect("expected a value"); - - let reveal_result = SubtensorModule::reveal_weights( - RuntimeOrigin::signed(*hotkey), - netuid, - uids.clone(), - values.clone(), - salt.clone(), - *version_key, - ); - - match reveal_result { - Ok(_) => { - commits.remove(0); - } - Err(e) => { - if e == Error::::RevealTooEarly.into() - || e == Error::::ExpiredWeightCommit.into() - || e == Error::::InvalidRevealCommitHashNotMatch.into() - { - log::info!("Expected error during reveal after epoch advancement: {e:?}"); - } else { - panic!( - "Unexpected error during reveal: {e:?}, expected RevealTooEarly, ExpiredWeightCommit, or InvalidRevealCommitHashNotMatch" - ); - } - } - } - } - } - } - - // ==== Modify Network Parameters During Commits ==== - SubtensorModule::set_tempo_unchecked(netuid, 150); - assert_ok!(SubtensorModule::set_reveal_period(netuid, 7)); - log::info!("Changed tempo to 150 and reveal_period to 7 during commits."); - - step_epochs(3, netuid); - - // ==== Continue Reveals After Epoch Advancement ==== - for hotkey in &hotkeys { - if let Some(commits) = commit_info_map.get_mut(hotkey) { - while !commits.is_empty() { - let (_commit_hash, salt, uids, values, version_key) = &commits[0]; - - // Attempt to reveal - let reveal_result = SubtensorModule::reveal_weights( - RuntimeOrigin::signed(*hotkey), - netuid, - uids.clone(), - values.clone(), - salt.clone(), - *version_key, - ); - - match reveal_result { - Ok(_) => { - commits.remove(0); - } - Err(e) => { - // Check if the error is due to reveal being too early or commit expired - if e == Error::::RevealTooEarly.into() - || e == Error::::ExpiredWeightCommit.into() - || e == Error::::InvalidRevealCommitHashNotMatch.into() - { - log::info!("Expected error during reveal after epoch advancement: {e:?}"); - break; - } else { - panic!( - "Unexpected error during reveal after epoch advancement: {e:?}, expected RevealTooEarly, ExpiredWeightCommit, or InvalidRevealCommitHashNotMatch" - ); - } - } - } - } - } - } - - // ==== Change Network Parameters Again ==== - SubtensorModule::set_tempo_unchecked(netuid, 200); - assert_ok!(SubtensorModule::set_reveal_period(netuid, 10)); - log::info!("Changed tempo to 200 and reveal_period to 10 after initial reveals."); - - step_epochs(10, netuid); - - // ==== Final Reveal Attempts ==== - for (hotkey, commits) in commit_info_map.iter_mut() { - for (_commit_hash, salt, uids, values, version_key) in commits.iter() { - let reveal_result = SubtensorModule::reveal_weights( - RuntimeOrigin::signed(*hotkey), - netuid, - uids.clone(), - values.clone(), - salt.clone(), - *version_key, - ); - - assert_eq!( - reveal_result, - Err(Error::::ExpiredWeightCommit.into()), - "Expected ExpiredWeightCommit error, got {reveal_result:?}" - ); - } -} - - for hotkey in &hotkeys { - commit_info_map.insert(*hotkey, Vec::new()); - - for i in 0..max_unrevealed_commits { - let (uids, values, salt, version_key) = generate_unique_data(i + commits_per_hotkey); - let commit_hash: H256 = BlakeTwo256::hash_of(&( - *hotkey, - netuid, - uids.clone(), - values.clone(), - salt.clone(), - version_key, - )); - - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(*hotkey), - netuid, - commit_hash - )); - } - - let (uids, values, salt, version_key) = generate_unique_data(max_unrevealed_commits + commits_per_hotkey); - let commit_hash: H256 = BlakeTwo256::hash_of(&( - *hotkey, - netuid, - uids.clone(), - values.clone(), - salt.clone(), - version_key, - )); - - assert_err!( - SubtensorModule::commit_weights( - RuntimeOrigin::signed(*hotkey), - netuid, - commit_hash - ), - Error::::TooManyUnrevealedCommits - ); - } - - // Attempt unauthorized reveal - let unauthorized_hotkey = hotkeys[0]; - let target_hotkey = hotkeys[1]; - if let Some(commits) = commit_info_map.get(&target_hotkey) - && let Some((_commit_hash, salt, uids, values, version_key)) = commits.first() { - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(unauthorized_hotkey), - netuid, - uids.clone(), - values.clone(), - salt.clone(), - *version_key, - ), - Error::::InvalidRevealCommitHashNotMatch - ); - } - - let non_committing_hotkey: ::AccountId = U256::from(9999); - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(non_committing_hotkey), - netuid, - vec![0, 1], - vec![10, 20], - vec![0; 8], - 0, - ), - Error::::NoWeightsCommitFound - ); - - assert_eq!(SubtensorModule::get_reveal_period(netuid), 10); - assert_eq!(SubtensorModule::get_tempo(netuid), 200); - }) -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_commit_weights_rate_limit --exact --show-output --nocapture -#[test] -fn test_commit_weights_rate_limit() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let uids: Vec = vec![0, 1]; - let weight_values: Vec = vec![10, 10]; - let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; - let version_key: u64 = 0; - let hotkey: U256 = U256::from(1); - - let commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - )); - System::set_block_number(11); - - let tempo: u16 = 5; - add_network(netuid, tempo, 0); - - register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); - register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 10); // Rate limit is 10 blocks - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - add_balance_to_coldkey_account(&U256::from(0), 1.into()); - add_balance_to_coldkey_account(&U256::from(1), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(0)), - &(U256::from(0)), - netuid, - 1.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(1)), - &(U256::from(1)), - netuid, - 1.into(), - ); - - let neuron_uid = - SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey).expect("expected uid"); - SubtensorModule::set_last_update_for_uid(NetUidStorageIndex::from(netuid), neuron_uid, 0); - - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - - let new_salt: Vec = vec![9; 8]; - let new_commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - new_salt.clone(), - version_key, - )); - assert_err!( - SubtensorModule::commit_weights(RuntimeOrigin::signed(hotkey), netuid, new_commit_hash), - Error::::CommittingWeightsTooFast - ); - - step_block(5); - assert_err!( - SubtensorModule::commit_weights(RuntimeOrigin::signed(hotkey), netuid, new_commit_hash), - Error::::CommittingWeightsTooFast - ); - - step_block(5); // Current block is now 21 - - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - new_commit_hash - )); - - SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); - let weights_keys: Vec = vec![0]; - let weight_values: Vec = vec![1]; - - assert_err!( - SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid, - weights_keys.clone(), - weight_values.clone(), - 0 - ), - Error::::SettingWeightsTooFast - ); - - step_block(10); - - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid, - weights_keys.clone(), - weight_values.clone(), - 0 - )); - - assert_err!( - SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid, - weights_keys.clone(), - weight_values.clone(), - 0 - ), - Error::::SettingWeightsTooFast - ); - - step_block(5); - - assert_err!( - SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid, - weights_keys.clone(), - weight_values.clone(), - 0 - ), - Error::::SettingWeightsTooFast - ); - - step_block(5); - - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(hotkey), - netuid, - weights_keys.clone(), - weight_values.clone(), - 0 - )); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::tlock_encrypt_decrypt_drand_quicknet_works --exact --show-output --nocapture -#[test] -pub fn tlock_encrypt_decrypt_drand_quicknet_works() { - // using a pulse from drand's QuickNet - // https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/1000 - // the beacon public key - let pk_bytes = - b"83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a" - ; // a round number that we know a signature for - let round: u64 = 1000; - // the signature produced in that round - let signature = - b"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" - ; - - // Convert hex string to bytes - let pub_key_bytes = hex::decode(pk_bytes).expect("Failed to decode public key bytes"); - // Deserialize to G1Affine - let pub_key = - ::PublicKeyGroup::deserialize_compressed(&*pub_key_bytes) - .expect("Failed to deserialize public key"); - - // then we tlock a message for the pubkey - let plaintext = b"this is a test".as_slice(); - let esk = [2; 32]; - - let sig_bytes = hex::decode(signature).expect("Failed to decode signature bytes"); - let sig = ::SignatureGroup::deserialize_compressed(&*sig_bytes) - .expect("Failed to deserialize signature"); - - let message = { - let mut hasher = sha2::Sha256::new(); - hasher.update(round.to_be_bytes()); - hasher.finalize().to_vec() - }; - - let identity = Identity::new(b"", vec![message]); - - let rng = ChaCha20Rng::seed_from_u64(0); - let ct = tle::( - pub_key, esk, plaintext, identity, rng, - ) - .expect("Encryption failed"); - - // then we can decrypt the ciphertext using the signature - let result = tld::(ct, sig).expect("Decryption failed"); - assert!(result == plaintext); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_success --exact --show-output --nocapture - -#[test] -fn test_reveal_crv3_commits_success() { - new_test_ext(100).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey1: AccountId = U256::from(1); - let hotkey2: AccountId = U256::from(2); - let reveal_round: u64 = 1000; - - add_network(netuid, 5, 0); - register_ok_neuron(netuid, hotkey1, U256::from(3), 100_000); - register_ok_neuron(netuid, hotkey2, U256::from(4), 100_000); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); - - let neuron_uid1 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey1) - .expect("Failed to get neuron UID for hotkey1"); - let neuron_uid2 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey2) - .expect("Failed to get neuron UID for hotkey2"); - - SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid1, true); - SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid2, true); - add_balance_to_coldkey_account(&U256::from(3), 1.into()); - add_balance_to_coldkey_account(&U256::from(4), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey1, - &(U256::from(3)), - netuid, - 1.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey2, - &(U256::from(4)), - netuid, - 1.into(), - ); - - let version_key = SubtensorModule::get_weights_version_key(netuid); - - let payload = WeightsTlockPayload { - hotkey: hotkey1.encode(), - values: vec![10, 20], - uids: vec![neuron_uid1, neuron_uid2], - version_key, - }; - - let serialized_payload = payload.encode(); - - let esk = [2; 32]; - let rng = ChaCha20Rng::seed_from_u64(0); - - let pk_bytes = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a") - .expect("Failed to decode public key bytes"); - let pub_key = ::PublicKeyGroup::deserialize_compressed(&*pk_bytes) - .expect("Failed to deserialize public key"); - - let message = { - let mut hasher = sha2::Sha256::new(); - hasher.update(reveal_round.to_be_bytes()); - hasher.finalize().to_vec() - }; - let identity = Identity::new(b"", vec![message]); - - let ct = tle::( - pub_key, - esk, - &serialized_payload, - identity, - rng, - ) - .expect("Encryption failed"); - - let mut commit_bytes = Vec::new(); - ct.serialize_compressed(&mut commit_bytes) - .expect("Failed to serialize commit"); - - assert!( - !commit_bytes.is_empty(), - "commit_bytes is empty after serialization" - ); - - log::debug!( - "Commit bytes now contain {commit_bytes:#?}" - ); - - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey1), - netuid, - commit_bytes.clone().try_into().expect("Failed to convert commit bytes into bounded vector"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - - let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") - .expect("Failed to decode signature bytes"); - - pallet_drand::Pulses::::insert( - reveal_round, - Pulse { - round: reveal_round, - randomness: vec![0; 32].try_into().expect("Failed to convert randomness vector"), - signature: sig_bytes.try_into().expect("Failed to convert signature bytes"), - }, - ); - - // Step epochs to run the epoch via the blockstep - step_epochs(3, netuid); - - let weights_sparse = SubtensorModule::get_weights_sparse(netuid.into()); - let weights = weights_sparse.get(neuron_uid1 as usize).cloned().unwrap_or_default(); - - assert!( - !weights.is_empty(), - "Weights for neuron_uid1 are empty, expected weights to be set." - ); - - let expected_weights: Vec<(u16, I32F32)> = payload - .uids - .iter() - .zip(payload.values.iter()) - .map(|(&uid, &value)| (uid, I32F32::from_num(value))) - .collect(); - - let total_weight: I32F32 = weights.iter().map(|(_, w)| *w).sum(); - - let normalized_weights: Vec<(u16, I32F32)> = weights - .iter() - .map(|&(uid, w)| (uid, w * I32F32::from_num(30) / total_weight)) - .collect(); - - for ((uid_a, w_a), (uid_b, w_b)) in normalized_weights.iter().zip(expected_weights.iter()) { - assert_eq!(uid_a, uid_b); - - let actual_weight_f64: f64 = w_a.to_num::(); - let rounded_actual_weight = actual_weight_f64.round() as i64; - - assert!( - rounded_actual_weight != 0, - "Actual weight for uid {uid_a} is zero" - ); - - let expected_weight = w_b.to_num::(); - - assert_eq!( - rounded_actual_weight, expected_weight, - "Weight mismatch for uid {uid_a}: expected {expected_weight}, got {rounded_actual_weight}" - ); - } - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_cannot_reveal_after_reveal_epoch --exact --show-output --nocapture -#[test] -fn test_reveal_crv3_commits_cannot_reveal_after_reveal_epoch() { - new_test_ext(100).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey1: AccountId = U256::from(1); - let hotkey2: AccountId = U256::from(2); - let reveal_round: u64 = 1000; - - add_network(netuid, 5, 0); - register_ok_neuron(netuid, hotkey1, U256::from(3), 100_000); - register_ok_neuron(netuid, hotkey2, U256::from(4), 100_000); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); - - let neuron_uid1 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey1) - .expect("Failed to get neuron UID for hotkey1"); - let neuron_uid2 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey2) - .expect("Failed to get neuron UID for hotkey2"); - - SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid1, true); - SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid2, true); - - let version_key = SubtensorModule::get_weights_version_key(netuid); - - let payload = WeightsTlockPayload { - hotkey: hotkey1.encode(), - values: vec![10, 20], - uids: vec![neuron_uid1, neuron_uid2], - version_key, - }; - - let serialized_payload = payload.encode(); - - let esk = [2; 32]; - let rng = ChaCha20Rng::seed_from_u64(0); - - let pk_bytes = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a") - .expect("Failed to decode public key bytes"); - let pub_key = ::PublicKeyGroup::deserialize_compressed(&*pk_bytes) - .expect("Failed to deserialize public key"); - - let message = { - let mut hasher = sha2::Sha256::new(); - hasher.update(reveal_round.to_be_bytes()); - hasher.finalize().to_vec() - }; - let identity = Identity::new(b"", vec![message]); - - let ct = tle::( - pub_key, - esk, - &serialized_payload, - identity, - rng, - ) - .expect("Encryption failed"); - - let mut commit_bytes = Vec::new(); - ct.serialize_compressed(&mut commit_bytes) - .expect("Failed to serialize commit"); - - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey1), - netuid, - commit_bytes - .clone() - .try_into() - .expect("Failed to convert commit bytes into bounded vector"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - - // Do NOT insert the pulse at this time; this simulates the missing pulse during the reveal epoch - // Advance epochs to reach the reveal epoch (3 epochs as reveal_period is 3) - step_epochs(3, netuid); - - // Verify that weights are not set - let weights_sparse = SubtensorModule::get_weights_sparse(netuid.into()); - let weights = weights_sparse - .get(neuron_uid1 as usize) - .cloned() - .unwrap_or_default(); - - assert!( - weights.is_empty(), - "Weights for neuron_uid1 should be empty as the commit cannot be revealed without the pulse." - ); - - // Now, after the reveal epoch has passed, insert the pulse - let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") - .expect("Failed to decode signature bytes"); - - pallet_drand::Pulses::::insert( - reveal_round, - Pulse { - round: reveal_round, - randomness: vec![0; 32] - .try_into() - .expect("Failed to convert randomness vector"), - signature: sig_bytes - .try_into() - .expect("Failed to convert signature bytes"), - }, - ); - - // Advance one more epoch to be after the reveal epoch - step_epochs(1, netuid); - - // Attempt to reveal commits after the reveal epoch has passed - assert_ok!(SubtensorModule::reveal_crv3_commits_for_subnet(netuid)); - - // Verify that the weights for the neuron have not been set - let weights_sparse = SubtensorModule::get_weights_sparse(netuid.into()); - let weights = weights_sparse - .get(neuron_uid1 as usize) - .cloned() - .unwrap_or_default(); - - assert!( - weights.is_empty(), - "Weights for neuron_uid1 should be empty as the commit cannot be revealed after the reveal epoch." - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_do_commit_crv3_weights_success --exact --show-output --nocapture -#[test] -fn test_do_commit_crv3_weights_success() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey: AccountId = U256::from(1); - let commit_data: Vec = vec![1, 2, 3, 4, 5]; - let reveal_round: u64 = 1000; - - add_network(netuid, 5, 0); - register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_data - .clone() - .try_into() - .expect("Failed to convert commit data into bounded vector"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - - let cur_epoch = - SubtensorModule::get_epoch_index(netuid, SubtensorModule::get_current_block_as_u64()); - let commits = - TimelockedWeightCommits::::get(NetUidStorageIndex::from(netuid), cur_epoch); - assert_eq!(commits.len(), 1); - assert_eq!(commits[0].0, hotkey); - assert_eq!(commits[0].2, commit_data); - assert_eq!(commits[0].3, reveal_round); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_do_commit_crv3_weights_disabled --exact --show-output --nocapture -#[test] -fn test_do_commit_crv3_weights_disabled() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey: AccountId = U256::from(1); - let commit_data: Vec = vec![1, 2, 3, 4, 5]; - let reveal_round: u64 = 1000; - - add_network(netuid, 5, 0); - register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); - - SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); - assert_err!( - SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_data - .try_into() - .expect("Failed to convert commit data into bounded vector"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - ), - Error::::CommitRevealDisabled - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_do_commit_crv3_weights_hotkey_not_registered --exact --show-output --nocapture -#[test] -fn test_do_commit_crv3_weights_hotkey_not_registered() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let unregistered_hotkey: AccountId = U256::from(99); - let commit_data: Vec = vec![1, 2, 3, 4, 5]; - let reveal_round: u64 = 1000; - let hotkey: AccountId = U256::from(1); - - add_network(netuid, 5, 0); - register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - - assert_err!( - SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(unregistered_hotkey), - netuid, - commit_data - .try_into() - .expect("Failed to convert commit data into bounded vector"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - ), - Error::::HotKeyNotRegisteredInSubNet - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_do_commit_crv3_weights_committing_too_fast --exact --show-output --nocapture -#[test] -fn test_do_commit_crv3_weights_committing_too_fast() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey: AccountId = U256::from(1); - let commit_data_1: Vec = vec![1, 2, 3]; - let commit_data_2: Vec = vec![4, 5, 6]; - let reveal_round: u64 = 1000; - - add_network(netuid, 5, 0); - register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - let neuron_uid = - SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey).expect("Expected uid"); - SubtensorModule::set_last_update_for_uid(NetUidStorageIndex::from(netuid), neuron_uid, 0); - - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_data_1 - .clone() - .try_into() - .expect("Failed to convert commit data into bounded vector"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - - assert_err!( - SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_data_2 - .clone() - .try_into() - .expect("Failed to convert commit data into bounded vector"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - ), - Error::::CommittingWeightsTooFast - ); - - step_block(2); - - assert_err!( - SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_data_2 - .clone() - .try_into() - .expect("Failed to convert commit data into bounded vector"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - ), - Error::::CommittingWeightsTooFast - ); - - step_block(3); - - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_data_2 - .try_into() - .expect("Failed to convert commit data into bounded vector"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_do_commit_crv3_weights_too_many_unrevealed_commits --exact --show-output --nocapture -#[test] -fn test_do_commit_crv3_weights_too_many_unrevealed_commits() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey1: AccountId = U256::from(1); - let hotkey2: AccountId = U256::from(2); - let reveal_round: u64 = 1000; - - add_network(netuid, 5, 0); - register_ok_neuron(netuid, hotkey1, U256::from(2), 100_000); - register_ok_neuron(netuid, hotkey2, U256::from(3), 100_000); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - // Hotkey1 submits 10 commits successfully - for i in 0..10 { - let commit_data: Vec = vec![i as u8; 5]; - let bounded_commit_data = commit_data - .try_into() - .expect("Failed to convert commit data into bounded vector"); - - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey1), - netuid, - bounded_commit_data, - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - } - - // Hotkey1 attempts to commit an 11th time, should fail with TooManyUnrevealedCommits - let new_commit_data: Vec = vec![11; 5]; - let bounded_new_commit_data = new_commit_data - .try_into() - .expect("Failed to convert new commit data into bounded vector"); - - assert_err!( - SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey1), - netuid, - bounded_new_commit_data, - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - ), - Error::::TooManyUnrevealedCommits - ); - - // Hotkey2 can still submit commits independently - let commit_data_hotkey2: Vec = vec![0; 5]; - let bounded_commit_data_hotkey2 = commit_data_hotkey2 - .try_into() - .expect("Failed to convert commit data into bounded vector"); - - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey2), - netuid, - bounded_commit_data_hotkey2, - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - - // Hotkey2 can submit up to 10 commits - for i in 1..10 { - let commit_data: Vec = vec![i as u8; 5]; - let bounded_commit_data = commit_data - .try_into() - .expect("Failed to convert commit data into bounded vector"); - - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey2), - netuid, - bounded_commit_data, - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - } - - // Hotkey2 attempts to commit an 11th time, should fail - let new_commit_data: Vec = vec![11; 5]; - let bounded_new_commit_data = new_commit_data - .try_into() - .expect("Failed to convert new commit data into bounded vector"); - - assert_err!( - SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey2), - netuid, - bounded_new_commit_data, - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - ), - Error::::TooManyUnrevealedCommits - ); - - step_epochs(10, netuid); - - let new_commit_data: Vec = vec![11; 5]; - let bounded_new_commit_data = new_commit_data - .try_into() - .expect("Failed to convert new commit data into bounded vector"); - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey1), - netuid, - bounded_new_commit_data, - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_decryption_failure --exact --show-output --nocapture -#[test] -fn test_reveal_crv3_commits_decryption_failure() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey: AccountId = U256::from(1); - let reveal_round: u64 = 1000; - - add_network(netuid, 5, 0); - register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - - let commit_bytes: Vec = vec![0xff; 100]; - let bounded_commit_bytes = commit_bytes - .clone() - .try_into() - .expect("Failed to convert commit bytes into bounded vector"); - - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey), - netuid, - bounded_commit_bytes, - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - - step_epochs(1, netuid); - - pallet_drand::Pulses::::insert( - reveal_round, - Pulse { - round: reveal_round, - randomness: vec![0; 32] - .try_into() - .expect("Failed to convert randomness vector"), - signature: vec![0; 128] - .try_into() - .expect("Failed to convert signature vector"), - }, - ); - - assert_ok!(SubtensorModule::reveal_crv3_commits_for_subnet(netuid)); - - let neuron_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey) - .expect("Failed to get neuron UID for hotkey") as usize; - let weights_matrix = SubtensorModule::get_weights(netuid.into()); - let weights = weights_matrix.get(neuron_uid).cloned().unwrap_or_default(); - assert!(weights.iter().all(|&w| w == I32F32::from_num(0))); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_multiple_commits_some_fail_some_succeed --exact --show-output --nocapture -#[test] -fn test_reveal_crv3_commits_multiple_commits_some_fail_some_succeed() { - new_test_ext(100).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey1: AccountId = U256::from(1); - let hotkey2: AccountId = U256::from(2); - let reveal_round: u64 = 1000; - - add_network(netuid, 5, 0); - register_ok_neuron(netuid, hotkey1, U256::from(3), 100_000); - register_ok_neuron(netuid, hotkey2, U256::from(4), 100_000); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - assert_ok!(SubtensorModule::set_reveal_period(netuid, 1)); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - // Prepare a valid payload for hotkey1 - let neuron_uid1 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey1) - .expect("Failed to get neuron UID for hotkey1"); - let version_key = SubtensorModule::get_weights_version_key(netuid); - let valid_payload = WeightsTlockPayload { - hotkey: hotkey1.encode(), - values: vec![10], - uids: vec![neuron_uid1], - version_key, - }; - let serialized_valid_payload = valid_payload.encode(); - - let esk = [2; 32]; - let rng = ChaCha20Rng::seed_from_u64(0); - - let pk_bytes = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a") - .expect("Failed to decode public key bytes"); - let pub_key = ::PublicKeyGroup::deserialize_compressed(&*pk_bytes) - .expect("Failed to deserialize public key"); - - let message = { - let mut hasher = sha2::Sha256::new(); - hasher.update(reveal_round.to_be_bytes()); - hasher.finalize().to_vec() - }; - let identity = Identity::new(b"", vec![message]); - - let ct_valid = tle::( - pub_key, - esk, - &serialized_valid_payload, - identity.clone(), - rng.clone(), - ) - .expect("Encryption failed"); - - let mut commit_bytes_valid = Vec::new(); - ct_valid - .serialize_compressed(&mut commit_bytes_valid) - .expect("Failed to serialize valid commit"); - - // Prepare an invalid payload for hotkey2 - let invalid_payload = vec![0u8; 10]; // Invalid payload - let ct_invalid = tle::( - pub_key, - esk, - &invalid_payload, - identity, - rng, - ) - .expect("Encryption failed"); - - let mut commit_bytes_invalid = Vec::new(); - ct_invalid - .serialize_compressed(&mut commit_bytes_invalid) - .expect("Failed to serialize invalid commit"); - - // Insert both commits - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey1), - netuid, - commit_bytes_valid.try_into().expect("Failed to convert valid commit data"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey2), - netuid, - commit_bytes_invalid.try_into().expect("Failed to convert invalid commit data"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - - // Insert the pulse - let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") - .expect("Failed to decode signature bytes"); - - pallet_drand::Pulses::::insert( - reveal_round, - Pulse { - round: reveal_round, - randomness: vec![0; 32].try_into().expect("Failed to convert randomness vector"), - signature: sig_bytes.try_into().expect("Failed to convert signature bytes"), - }, - ); - - step_epochs(1, netuid); - - // Verify that weights are set for hotkey1 - let neuron_uid1 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey1) - .expect("Failed to get neuron UID for hotkey1") as usize; - let weights_sparse = SubtensorModule::get_weights_sparse(netuid.into()); - let weights1 = weights_sparse.get(neuron_uid1).cloned().unwrap_or_default(); - assert!( - !weights1.is_empty(), - "Weights for neuron_uid1 should be set" - ); - - // Verify that weights are not set for hotkey2 - let neuron_uid2 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey2) - .expect("Failed to get neuron UID for hotkey2") as usize; - let weights2 = weights_sparse.get(neuron_uid2).cloned().unwrap_or_default(); - assert!( - weights2.is_empty(), - "Weights for neuron_uid2 should be empty as commit could not be revealed" - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_do_set_weights_failure --exact --show-output --nocapture -#[test] -fn test_reveal_crv3_commits_do_set_weights_failure() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey: AccountId = U256::from(1); - let reveal_round: u64 = 1000; - - add_network(netuid, 5, 0); - register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - // Prepare payload with mismatched uids and values lengths - let version_key = SubtensorModule::get_weights_version_key(netuid); - let payload = WeightsTlockPayload { - hotkey: hotkey.encode(), - values: vec![10, 20], // Length 2 - uids: vec![0], // Length 1 - version_key, - }; - let serialized_payload = payload.encode(); - - let esk = [2; 32]; - let rng = ChaCha20Rng::seed_from_u64(0); - - let pk_bytes = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a") - .expect("Failed to decode public key bytes"); - let pub_key = ::PublicKeyGroup::deserialize_compressed(&*pk_bytes) - .expect("Failed to deserialize public key"); - - let message = { - let mut hasher = sha2::Sha256::new(); - hasher.update(reveal_round.to_be_bytes()); - hasher.finalize().to_vec() - }; - let identity = Identity::new(b"", vec![message]); - - let ct = tle::( - pub_key, - esk, - &serialized_payload, - identity, - rng, - ) - .expect("Encryption failed"); - - let mut commit_bytes = Vec::new(); - ct.serialize_compressed(&mut commit_bytes) - .expect("Failed to serialize commit"); - - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_bytes.try_into().expect("Failed to convert commit data into bounded vector"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - - let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") - .expect("Failed to decode signature bytes"); - - pallet_drand::Pulses::::insert( - reveal_round, - Pulse { - round: reveal_round, - randomness: vec![0; 32].try_into().expect("Failed to convert randomness vector"), - signature: sig_bytes.try_into().expect("Failed to convert signature bytes"), - }, - ); - - step_epochs(3, netuid); - - // Verify that weights are not set due to `do_set_weights` failure - let neuron_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey) - .expect("Failed to get neuron UID for hotkey") as usize; - let weights_sparse = SubtensorModule::get_weights_sparse(netuid.into()); - let weights = weights_sparse.get(neuron_uid).cloned().unwrap_or_default(); - assert!( - weights.is_empty(), - "Weights for neuron_uid should be empty as do_set_weights should have failed" - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_payload_decoding_failure --exact --show-output --nocapture -#[test] -fn test_reveal_crv3_commits_payload_decoding_failure() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey: AccountId = U256::from(1); - let reveal_round: u64 = 1000; - - add_network(netuid, 5, 0); - register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - let invalid_payload = vec![0u8; 10]; // Not a valid encoding of WeightsTlockPayload - - let esk = [2; 32]; - let rng = ChaCha20Rng::seed_from_u64(0); - - let pk_bytes = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a") - .expect("Failed to decode public key bytes"); - let pub_key = ::PublicKeyGroup::deserialize_compressed(&*pk_bytes) - .expect("Failed to deserialize public key"); - - let message = { - let mut hasher = sha2::Sha256::new(); - hasher.update(reveal_round.to_be_bytes()); - hasher.finalize().to_vec() - }; - let identity = Identity::new(b"", vec![message]); - - let ct = tle::( - pub_key, - esk, - &invalid_payload, - identity, - rng, - ) - .expect("Encryption failed"); - - let mut commit_bytes = Vec::new(); - ct.serialize_compressed(&mut commit_bytes) - .expect("Failed to serialize commit"); - - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_bytes.try_into().expect("Failed to convert commit data into bounded vector"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - - let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") - .expect("Failed to decode signature bytes"); - - pallet_drand::Pulses::::insert( - reveal_round, - Pulse { - round: reveal_round, - randomness: vec![0; 32].try_into().expect("Failed to convert randomness vector"), - signature: sig_bytes.try_into().expect("Failed to convert signature bytes"), - }, - ); - - step_epochs(3, netuid); - - // Verify that weights are not set - let neuron_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey) - .expect("Failed to get neuron UID for hotkey") as usize; - let weights_sparse = SubtensorModule::get_weights_sparse(netuid.into()); - let weights = weights_sparse.get(neuron_uid).cloned().unwrap_or_default(); - assert!( - weights.is_empty(), - "Weights for neuron_uid should be empty as the payload could not be decoded" - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_signature_deserialization_failure --exact --show-output --nocapture -#[test] -fn test_reveal_crv3_commits_signature_deserialization_failure() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey: AccountId = U256::from(1); - let reveal_round: u64 = 1000; - - add_network(netuid, 5, 0); - register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - let version_key = SubtensorModule::get_weights_version_key(netuid); - let payload = WeightsTlockPayload { - hotkey: hotkey.encode(), - values: vec![10, 20], - uids: vec![0, 1], - version_key, - }; - let serialized_payload = payload.encode(); - - let esk = [2; 32]; - let rng = ChaCha20Rng::seed_from_u64(0); - - let pk_bytes = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a") - .expect("Failed to decode public key bytes"); - let pub_key = ::PublicKeyGroup::deserialize_compressed(&*pk_bytes) - .expect("Failed to deserialize public key"); - - let message = { - let mut hasher = sha2::Sha256::new(); - hasher.update(reveal_round.to_be_bytes()); - hasher.finalize().to_vec() - }; - let identity = Identity::new(b"", vec![message]); - - let ct = tle::( - pub_key, - esk, - &serialized_payload, - identity, - rng, - ) - .expect("Encryption failed"); - - let mut commit_bytes = Vec::new(); - ct.serialize_compressed(&mut commit_bytes) - .expect("Failed to serialize commit"); - - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_bytes.try_into().expect("Failed to convert commit data into bounded vector"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - - pallet_drand::Pulses::::insert( - reveal_round, - Pulse { - round: reveal_round, - randomness: vec![0; 32].try_into().expect("Failed to convert randomness vector"), - signature: vec![0; 10].try_into().expect("Failed to create invalid signature"), // Invalid signature length - }, - ); - - step_epochs(3, netuid); - - // Verify that weights are not set - let neuron_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey) - .expect("Failed to get neuron UID for hotkey") as usize; - let weights_sparse = SubtensorModule::get_weights_sparse(netuid.into()); - let weights = weights_sparse.get(neuron_uid).cloned().unwrap_or_default(); - assert!( - weights.is_empty(), - "Weights for neuron_uid should be empty as the signature could not be deserialized" - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_do_commit_crv3_weights_commit_size_exceeds_limit --exact --show-output --nocapture -#[test] -fn test_do_commit_crv3_weights_commit_size_exceeds_limit() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey: AccountId = U256::from(1); - let reveal_round: u64 = 1000; - - add_network(netuid, 5, 0); - register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - let max_commit_size = MAX_CRV3_COMMIT_SIZE_BYTES as usize; - let commit_data_exceeding: Vec = vec![0u8; max_commit_size + 1]; // Exceeds max size - - // Attempt to create a BoundedVec; this should fail - let bounded_commit_data_result = - BoundedVec::>::try_from( - commit_data_exceeding.clone(), - ); - - assert!( - bounded_commit_data_result.is_err(), - "Expected error when converting commit data exceeding max size into BoundedVec" - ); - - let commit_data_max_size: Vec = vec![0u8; max_commit_size]; // Exactly at max size - let bounded_commit_data = BoundedVec::>::try_from( - commit_data_max_size.clone(), - ) - .expect("Failed to create BoundedVec with data at max size"); - - // Now call the function with valid data at max size - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey), - netuid, - bounded_commit_data, - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_with_empty_commit_queue --exact --show-output --nocapture -#[test] -fn test_reveal_crv3_commits_with_empty_commit_queue() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - - add_network(netuid, 5, 0); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - step_epochs(2, netuid); - - let weights_sparse = SubtensorModule::get_weights_sparse(netuid.into()); - assert!( - weights_sparse.is_empty(), - "Weights should be empty as there were no commits to reveal" - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_with_incorrect_identity_message --exact --show-output --nocapture -#[test] -fn test_reveal_crv3_commits_with_incorrect_identity_message() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey: AccountId = U256::from(1); - let reveal_round: u64 = 1000; - - add_network(netuid, 5, 0); - register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - assert_ok!(SubtensorModule::set_reveal_period(netuid, 1)); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - // Prepare a valid payload but use incorrect identity message during encryption - let neuron_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey) - .expect("Failed to get neuron UID for hotkey"); - let version_key = SubtensorModule::get_weights_version_key(netuid); - let payload = WeightsTlockPayload { - hotkey: hotkey.encode(), - values: vec![10], - uids: vec![neuron_uid], - version_key, - }; - let serialized_payload = payload.encode(); - - let esk = [2; 32]; - let rng = ChaCha20Rng::seed_from_u64(0); - - let pk_bytes = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a") - .expect("Failed to decode public key bytes"); - let pub_key = ::PublicKeyGroup::deserialize_compressed(&*pk_bytes) - .expect("Failed to deserialize public key"); - - // Use incorrect message for identity (e.g., reveal_round + 1) - let incorrect_message = { - let mut hasher = sha2::Sha256::new(); - hasher.update((reveal_round + 1).to_be_bytes()); - hasher.finalize().to_vec() - }; - let identity = Identity::new(b"", vec![incorrect_message]); - - let ct = tle::( - pub_key, - esk, - &serialized_payload, - identity, - rng, - ) - .expect("Encryption failed"); - - let mut commit_bytes = Vec::new(); - ct.serialize_compressed(&mut commit_bytes) - .expect("Failed to serialize commit"); - - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_bytes.try_into().expect("Failed to convert commit data into bounded vector"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - - let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") - .expect("Failed to decode signature bytes"); - - pallet_drand::Pulses::::insert( - reveal_round, - Pulse { - round: reveal_round, - randomness: vec![0; 32].try_into().expect("Failed to convert randomness vector"), - signature: sig_bytes.try_into().expect("Failed to convert signature bytes"), - }, - ); - - step_epochs(1, netuid); - - // Verify that weights are not set due to decryption failure - let neuron_uid = neuron_uid as usize; - let weights_sparse = SubtensorModule::get_weights_sparse(netuid.into()); - let weights = weights_sparse.get(neuron_uid).cloned().unwrap_or_default(); - assert!( - weights.is_empty(), - "Weights for neuron_uid should be empty due to incorrect identity message" - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_multiple_commits_by_same_hotkey_within_limit --exact --show-output --nocapture -#[test] -fn test_multiple_commits_by_same_hotkey_within_limit() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey: AccountId = U256::from(1); - let reveal_round: u64 = 1000; - - add_network(netuid, 5, 0); - register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - assert_ok!(SubtensorModule::set_reveal_period(netuid, 1)); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - for i in 0..10 { - let commit_data: Vec = vec![i; 5]; - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_data - .try_into() - .expect("Failed to convert commit data into bounded vector"), - reveal_round + i as u64, - SubtensorModule::get_commit_reveal_weights_version() - )); - } - - let cur_epoch = - SubtensorModule::get_epoch_index(netuid, SubtensorModule::get_current_block_as_u64()); - let commits = - TimelockedWeightCommits::::get(NetUidStorageIndex::from(netuid), cur_epoch); - assert_eq!( - commits.len(), - 10, - "Expected 10 commits stored for the hotkey" - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_removes_past_epoch_commits --exact --show-output --nocapture -#[test] -fn test_reveal_crv3_commits_removes_past_epoch_commits() { - new_test_ext(100).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey: AccountId = U256::from(1); - let reveal_round: u64 = 1_000; - - add_network(netuid, /*tempo*/ 5, 0); - register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - assert_ok!(SubtensorModule::set_reveal_period(netuid, 1)); // reveal_period = 1 epoch - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - // --------------------------------------------------------------------- - // Put dummy commits into the two epochs immediately *before* current. - // --------------------------------------------------------------------- - // Establish a non-zero epoch counter and pin the scheduler so the reveal - // pass sees exactly this epoch (no look-ahead increment). - let cur_epoch: u64 = 10; - SubnetEpochIndex::::insert(netuid, cur_epoch); - LastEpochBlock::::insert(netuid, SubtensorModule::get_current_block_as_u64()); - PendingEpochAt::::insert(netuid, 0); - let cur_block = SubtensorModule::get_current_block_as_u64(); - let past_epoch = cur_epoch.saturating_sub(2); // definitely < reveal_epoch - let reveal_epoch = cur_epoch.saturating_sub(1); // == cur_epoch - reveal_period - - for &epoch in &[past_epoch, reveal_epoch] { - let bounded_commit = vec![epoch as u8; 5].try_into().expect("bounded vec"); - - assert_ok!(TimelockedWeightCommits::::try_mutate( - NetUidStorageIndex::from(netuid), - epoch, - |q| -> DispatchResult { - q.push_back((hotkey, cur_block, bounded_commit, reveal_round)); - Ok(()) - } - )); - } - - // Sanity – both epochs presently hold a commit. - assert!( - !TimelockedWeightCommits::::get(NetUidStorageIndex::from(netuid), past_epoch) - .is_empty() - ); - assert!( - !TimelockedWeightCommits::::get(NetUidStorageIndex::from(netuid), reveal_epoch) - .is_empty() - ); - - // --------------------------------------------------------------------- - // Run the reveal pass WITHOUT a pulse – only expiry housekeeping runs. - // --------------------------------------------------------------------- - assert_ok!(SubtensorModule::reveal_crv3_commits_for_subnet(netuid)); - - // past_epoch (< reveal_epoch) must be gone - assert!( - TimelockedWeightCommits::::get(NetUidStorageIndex::from(netuid), past_epoch) - .is_empty(), - "expired epoch {past_epoch} should be cleared" - ); - - // reveal_epoch queue is *kept* because its commit could still be revealed later. - assert!( - !TimelockedWeightCommits::::get(NetUidStorageIndex::from(netuid), reveal_epoch) - .is_empty(), - "reveal-epoch {reveal_epoch} must be retained until commit can be revealed" - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_multiple_valid_commits_all_processed --exact --show-output --nocapture -#[test] -fn test_reveal_crv3_commits_multiple_valid_commits_all_processed() { - new_test_ext(100).execute_with(|| { - let netuid = NetUid::from(1); - let reveal_round: u64 = 1_000; - - // ───── network parameters ─────────────────────────────────────────── - add_network(netuid, 5, 0); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - assert_ok!(SubtensorModule::set_reveal_period(netuid, 1)); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_max_registrations_per_block(netuid, 100); - SubtensorModule::set_target_registrations_per_interval(netuid, 100); - - // Insert the pulse - let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") - .expect("Failed to decode signature bytes"); - - // pulse for round 1000 - // let sig_bytes = hex::decode( - // "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e\ - // 342b73a8dd2bacbe47e4b6b63ed5e39", - // ) - // .unwrap(); - pallet_drand::Pulses::::insert( - reveal_round, - Pulse { - round: reveal_round, - randomness: vec![0; 32].try_into().unwrap(), - signature: sig_bytes.try_into().unwrap(), - }, - ); - - // ───── five neurons (hotkeys 1‑5) ─────────────────────────────────── - let hotkeys: Vec<_> = (1..=5).map(U256::from).collect(); - for (i, hk) in hotkeys.iter().enumerate() { - let cold: AccountId = U256::from(i + 100); - - register_ok_neuron(netuid, *hk, cold, 100_000); - SubtensorModule::set_validator_permit_for_uid(netuid, i as u16, true); - - // add minimal stake so `do_set_weights` will succeed - add_balance_to_coldkey_account(&cold, 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - hk, - &cold, - netuid, - 1.into(), - ); - - step_block(1); // avoids TooManyRegistrationsThisBlock - } - - - // ───── create & submit commits for each hotkey ────────────────────── - let esk = [2u8; 32]; - let pk_bytes = hex::decode( - "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c\ - 8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb\ - 5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a", - ) - .unwrap(); - let pk = - ::PublicKeyGroup::deserialize_compressed(&*pk_bytes).unwrap(); - - for (i, hk) in hotkeys.iter().enumerate() { - let payload = WeightsTlockPayload { - hotkey: hk.encode(), - values: vec![10, 20, 30, 40, 50], - uids: (0..5).map(|u| u as u16).collect(), - version_key: SubtensorModule::get_weights_version_key(netuid), - }; - - let id_msg = { - let mut h = sha2::Sha256::new(); - h.update(reveal_round.to_be_bytes()); - h.finalize().to_vec() - }; - let ct = tle::( - pk, - esk, - &payload.encode(), - Identity::new(b"", vec![id_msg]), - ChaCha20Rng::seed_from_u64(i as u64), - ) - .unwrap(); - - let mut commit_bytes = Vec::new(); - ct.serialize_compressed(&mut commit_bytes).unwrap(); - - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(*hk), - netuid, - commit_bytes.try_into().unwrap(), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - } - - // advance reveal_period + 1 epochs → 2 epochs - step_epochs(2, netuid); - - // ───── assertions ─────────────────────────────────────────────────── - let w_sparse = SubtensorModule::get_weights_sparse(netuid.into()); - for hk in hotkeys { - let uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hk).unwrap() as usize; - assert!( - !w_sparse.get(uid).unwrap_or(&Vec::new()).is_empty(), - "weights for uid {uid} should be set" - ); - } - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_max_neurons --exact --show-output --nocapture -#[test] -fn test_reveal_crv3_commits_max_neurons() { - new_test_ext(100).execute_with(|| { - let netuid = NetUid::from(1); - let reveal_round: u64 = 1_000; - - // ───── network parameters ─────────────────────────────────────────── - add_network(netuid, 5, 0); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - assert_ok!(SubtensorModule::set_reveal_period(netuid, 1)); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_max_registrations_per_block(netuid, 10_000); - SubtensorModule::set_target_registrations_per_interval(netuid, 10_000); - SubtensorModule::set_max_allowed_uids(netuid, 10_024); - - // ───── register 1 024 neurons ─────────────────────────────────────── - for i in 0..1_024u16 { - let hk: AccountId = U256::from(i as u64 + 1); - let cold: AccountId = U256::from(i as u64 + 10_000); - - register_ok_neuron(netuid, hk, cold, 100_000); - SubtensorModule::set_validator_permit_for_uid(netuid, i, true); - - // give each neuron a nominal stake (safe even if not needed) - add_balance_to_coldkey_account(&cold, 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hk, - &cold, - netuid, - 1.into(), - ); - - step_block(1); // avoid registration‑limit panic - } - - // ───── pulse for round 1000 ───────────────────────────────────────── - let sig_bytes = hex::decode( - "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e\ - 342b73a8dd2bacbe47e4b6b63ed5e39", - ) - .unwrap(); - pallet_drand::Pulses::::insert( - reveal_round, - Pulse { - round: reveal_round, - randomness: vec![0; 32].try_into().unwrap(), - signature: sig_bytes.try_into().unwrap(), - }, - ); - - // ───── three committing hotkeys ───────────────────────────────────── - let esk = [2u8; 32]; - let pk_bytes = hex::decode( - "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c\ - 8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb\ - 5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a", - ) - .unwrap(); - let pk = - ::PublicKeyGroup::deserialize_compressed(&*pk_bytes).unwrap(); - let committing_hotkeys = [U256::from(1), U256::from(2), U256::from(3)]; - let mut commits = Vec::new(); - for (i, hk) in committing_hotkeys.iter().enumerate() { - let payload = WeightsTlockPayload { - hotkey: hk.encode(), - values: vec![10u16; 1_024], - uids: (0..1_024).collect(), - version_key: SubtensorModule::get_weights_version_key(netuid), - }; - let id_msg = { - let mut h = sha2::Sha256::new(); - h.update(reveal_round.to_be_bytes()); - h.finalize().to_vec() - }; - let ct = tle::( - pk, - esk, - &payload.encode(), - Identity::new(b"", vec![id_msg]), - ChaCha20Rng::seed_from_u64(i as u64), - ) - .unwrap(); - let mut commit_bytes = Vec::new(); - ct.serialize_compressed(&mut commit_bytes).unwrap(); - // Submit the commit - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(*hk), - netuid, - commit_bytes - .try_into() - .expect("Failed to convert commit data"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - - // Store the expected weights for later comparison - commits.push((hk, payload)); - } - // ───── advance reveal_period + 1 epochs ───────────────────────────── - step_epochs(2, netuid); - - // ───── verify weights ─────────────────────────────────────────────── - let w_sparse = SubtensorModule::get_weights_sparse(netuid.into()); - for hk in &committing_hotkeys { - let uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, hk).unwrap() as usize; - assert!( - !w_sparse.get(uid).unwrap_or(&Vec::new()).is_empty(), - "weights for uid {uid} should be set" - ); - } - }); -} - -// `get_first_block_of_epoch` is a legacy modulo helper — NOT used by live -// commit-reveal logic -#[test] -fn test_get_first_block_of_epoch_epoch_zero() { - new_test_ext(1).execute_with(|| { - let netuid: NetUid = NetUid::from(1); - add_network(netuid, 10, 0); - - // 0 * 11 - 2, saturating at 0. - assert_eq!(SubtensorModule::get_first_block_of_epoch(netuid, 0), 0); - }); -} - -#[test] -fn test_get_first_block_of_epoch_small_epoch() { - new_test_ext(1).execute_with(|| { - let netuid: NetUid = NetUid::from(0); - add_network(netuid, 1, 0); - - // 1 * 2 - 1 = 1. - assert_eq!(SubtensorModule::get_first_block_of_epoch(netuid, 1), 1); - }); -} - -#[test] -fn test_get_first_block_of_epoch_with_offset() { - new_test_ext(1).execute_with(|| { - let netuid: NetUid = NetUid::from(1); - add_network(netuid, 10, 0); - - // 1 * 11 - 2 = 9. - assert_eq!(SubtensorModule::get_first_block_of_epoch(netuid, 1), 9); - }); -} - -#[test] -fn test_get_first_block_of_epoch_large_epoch() { - new_test_ext(1).execute_with(|| { - let netuid: NetUid = NetUid::from(0); - add_network(netuid, 100, 0); - - let epoch: u64 = 1000; - // 1000 * 101 - 1. - assert_eq!( - SubtensorModule::get_first_block_of_epoch(netuid, epoch), - epoch * 101 - 1 - ); - }); -} - -#[test] -fn test_reveal_crv3_commits_hotkey_check() { - new_test_ext(100).execute_with(|| { - // Failure case: hotkey mismatch - let netuid = NetUid::from(1); - let hotkey1: AccountId = U256::from(1); - let hotkey2: AccountId = U256::from(2); - let reveal_round: u64 = 1000; - - add_network(netuid, 5, 0); - register_ok_neuron(netuid, hotkey1, U256::from(3), 100_000); - register_ok_neuron(netuid, hotkey2, U256::from(4), 100_000); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); - - let neuron_uid1 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey1) - .expect("Failed to get neuron UID for hotkey1"); - let neuron_uid2 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey2) - .expect("Failed to get neuron UID for hotkey2"); - - SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid1, true); - SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid2, true); - add_balance_to_coldkey_account(&U256::from(3), 1.into()); - add_balance_to_coldkey_account(&U256::from(4), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey1, - &(U256::from(3)), - netuid, - 1.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey2, - &(U256::from(4)), - netuid, - 1.into(), - ); - - let version_key = SubtensorModule::get_weights_version_key(netuid); - - let payload = WeightsTlockPayload { - hotkey: hotkey2.encode(), // Mismatch: using hotkey2 instead of hotkey1 - values: vec![10, 20], - uids: vec![neuron_uid1, neuron_uid2], - version_key, - }; - - let serialized_payload = payload.encode(); - - let esk = [2; 32]; - let rng = ChaCha20Rng::seed_from_u64(0); - - let pk_bytes = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a") - .expect("Failed to decode public key bytes"); - let pub_key = ::PublicKeyGroup::deserialize_compressed(&*pk_bytes) - .expect("Failed to deserialize public key"); - - let message = { - let mut hasher = sha2::Sha256::new(); - hasher.update(reveal_round.to_be_bytes()); - hasher.finalize().to_vec() - }; - let identity = Identity::new(b"", vec![message]); - - let ct = tle::( - pub_key, - esk, - &serialized_payload, - identity, - rng, - ) - .expect("Encryption failed"); - - let mut commit_bytes = Vec::new(); - ct.serialize_compressed(&mut commit_bytes) - .expect("Failed to serialize commit"); - - assert!( - !commit_bytes.is_empty(), - "commit_bytes is empty after serialization" - ); - - log::debug!( - "Commit bytes now contain {commit_bytes:#?}" - ); - - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey1), - netuid, - commit_bytes.clone().try_into().expect("Failed to convert commit bytes into bounded vector"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - - let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") - .expect("Failed to decode signature bytes"); - - pallet_drand::Pulses::::insert( - reveal_round, - Pulse { - round: reveal_round, - randomness: vec![0; 32].try_into().expect("Failed to convert randomness vector"), - signature: sig_bytes.try_into().expect("Failed to convert signature bytes"), - }, - ); - - // Step epochs to run the epoch via the blockstep - step_epochs(3, netuid); - - let weights_sparse = SubtensorModule::get_weights_sparse(netuid.into()); - let weights = weights_sparse.get(neuron_uid1 as usize).cloned().unwrap_or_default(); - - assert!( - weights.is_empty(), - "Weights for neuron_uid1 should be empty due to hotkey mismatch." - ); - }); - - new_test_ext(100).execute_with(|| { - // Success case: hotkey match - let netuid = NetUid::from(1); - let hotkey1: AccountId = U256::from(1); - let hotkey2: AccountId = U256::from(2); - let reveal_round: u64 = 1000; - - add_network(netuid, 5, 0); - register_ok_neuron(netuid, hotkey1, U256::from(3), 100_000); - register_ok_neuron(netuid, hotkey2, U256::from(4), 100_000); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); - - let neuron_uid1 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey1) - .expect("Failed to get neuron UID for hotkey1"); - let neuron_uid2 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey2) - .expect("Failed to get neuron UID for hotkey2"); - - SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid1, true); - SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid2, true); - add_balance_to_coldkey_account(&U256::from(3), 1.into()); - add_balance_to_coldkey_account(&U256::from(4), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey1, - &(U256::from(3)), - netuid, - 1.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey2, - &(U256::from(4)), - netuid, - 1.into(), - ); - - let version_key = SubtensorModule::get_weights_version_key(netuid); - - let payload = WeightsTlockPayload { - hotkey: hotkey1.encode(), // Match: using hotkey1 - values: vec![10, 20], - uids: vec![neuron_uid1, neuron_uid2], - version_key, - }; - - let serialized_payload = payload.encode(); - - let esk = [2; 32]; - let rng = ChaCha20Rng::seed_from_u64(0); - - let pk_bytes = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a") - .expect("Failed to decode public key bytes"); - let pub_key = ::PublicKeyGroup::deserialize_compressed(&*pk_bytes) - .expect("Failed to deserialize public key"); - - let message = { - let mut hasher = sha2::Sha256::new(); - hasher.update(reveal_round.to_be_bytes()); - hasher.finalize().to_vec() - }; - let identity = Identity::new(b"", vec![message]); - - let ct = tle::( - pub_key, - esk, - &serialized_payload, - identity, - rng, - ) - .expect("Encryption failed"); - - let mut commit_bytes = Vec::new(); - ct.serialize_compressed(&mut commit_bytes) - .expect("Failed to serialize commit"); - - assert!( - !commit_bytes.is_empty(), - "commit_bytes is empty after serialization" - ); - - log::debug!( - "Commit bytes now contain {commit_bytes:#?}" - ); - - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey1), - netuid, - commit_bytes.clone().try_into().expect("Failed to convert commit bytes into bounded vector"), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - - let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") - .expect("Failed to decode signature bytes"); - - pallet_drand::Pulses::::insert( - reveal_round, - Pulse { - round: reveal_round, - randomness: vec![0; 32].try_into().expect("Failed to convert randomness vector"), - signature: sig_bytes.try_into().expect("Failed to convert signature bytes"), - }, - ); - - // Step epochs to run the epoch via the blockstep - step_epochs(3, netuid); - - let weights_sparse = SubtensorModule::get_weights_sparse(netuid.into()); - let weights = weights_sparse.get(neuron_uid1 as usize).cloned().unwrap_or_default(); - - assert!( - !weights.is_empty(), - "Weights for neuron_uid1 are empty, expected weights to be set." - ); - - let expected_weights: Vec<(u16, I32F32)> = payload - .uids - .iter() - .zip(payload.values.iter()) - .map(|(&uid, &value)| (uid, I32F32::from_num(value))) - .collect(); - - let total_weight: I32F32 = weights.iter().map(|(_, w)| *w).sum(); - - let normalized_weights: Vec<(u16, I32F32)> = weights - .iter() - .map(|&(uid, w)| (uid, w * I32F32::from_num(30) / total_weight)) - .collect(); - - for ((uid_a, w_a), (uid_b, w_b)) in normalized_weights.iter().zip(expected_weights.iter()) { - assert_eq!(uid_a, uid_b); - - let actual_weight_f64: f64 = w_a.to_num::(); - let rounded_actual_weight = actual_weight_f64.round() as i64; - - assert!( - rounded_actual_weight != 0, - "Actual weight for uid {uid_a} is zero" - ); - - let expected_weight = w_b.to_num::(); - - assert_eq!( - rounded_actual_weight, expected_weight, - "Weight mismatch for uid {uid_a}: expected {expected_weight}, got {rounded_actual_weight}" - ); - } - }); -} - -#[test] -fn test_reveal_crv3_commits_retry_on_missing_pulse() { - new_test_ext(100).execute_with(|| { - let netuid = NetUid::from(1); - let hotkey: AccountId = U256::from(1); - let reveal_round: u64 = 1_000; - - // ─── network & neuron ─────────────────────────────────────────────── - add_network(netuid, 5, 0); - register_ok_neuron(netuid, hotkey, U256::from(3), 100_000); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - SubtensorModule::set_stake_threshold(0); - - let uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey).unwrap(); - SubtensorModule::set_validator_permit_for_uid(netuid, uid, true); - - // ─── craft commit ─────────────────────────────────────────────────── - let payload = WeightsTlockPayload { - hotkey: hotkey.encode(), - values: vec![10], - uids: vec![uid], - version_key: SubtensorModule::get_weights_version_key(netuid), - }; - let esk = [2u8; 32]; - let pk_bytes = hex::decode( - "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c\ - 8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb\ - 5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a", - ) - .unwrap(); - let pk = - ::PublicKeyGroup::deserialize_compressed(&*pk_bytes).unwrap(); - let id_msg = { - let mut h = sha2::Sha256::new(); - h.update(reveal_round.to_be_bytes()); - h.finalize().to_vec() - }; - let ct = tle::( - pk, - esk, - &payload.encode(), - Identity::new(b"", vec![id_msg]), - ChaCha20Rng::seed_from_u64(0), - ) - .unwrap(); - let mut commit_bytes = Vec::new(); - ct.serialize_compressed(&mut commit_bytes).unwrap(); - - // submit commit - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_bytes.clone().try_into().unwrap(), - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - - // epoch in which commit was stored - let stored_epoch = - TimelockedWeightCommits::::iter_prefix(NetUidStorageIndex::from(netuid)) - .next() - .map(|(e, _)| e) - .expect("commit stored"); - - // Place the subnet's epoch counter at the commit's reveal epoch - // (`commit_epoch + reveal_period`). The counter is the canonical epoch - // index; pin `LastEpochBlock`/`PendingEpochAt` so `should_run_epoch` stays - // false and the look-ahead does not skip past the reveal epoch. - let reveal_epoch = stored_epoch + SubtensorModule::get_reveal_period(netuid); - SubnetEpochIndex::::insert(netuid, reveal_epoch); - LastEpochBlock::::insert(netuid, SubtensorModule::get_current_block_as_u64()); - PendingEpochAt::::insert(netuid, 0); - - // run *one* block inside reveal epoch without pulse → commit should stay queued - step_block(1); - assert!( - !TimelockedWeightCommits::::get(NetUidStorageIndex::from(netuid), stored_epoch) - .is_empty(), - "commit must remain queued when pulse is missing" - ); - - // ─── insert pulse & step one more block ───────────────────────────── - let sig_bytes = hex::decode( - "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e\ - 342b73a8dd2bacbe47e4b6b63ed5e39", - ) - .unwrap(); - pallet_drand::Pulses::::insert( - reveal_round, - Pulse { - round: reveal_round, - randomness: vec![0; 32].try_into().unwrap(), - signature: sig_bytes.try_into().unwrap(), - }, - ); - - step_block(1); // automatic reveal runs here - - let weights = SubtensorModule::get_weights_sparse(netuid.into()) - .get(uid as usize) - .cloned() - .unwrap_or_default(); - assert!(!weights.is_empty(), "weights must be set after pulse"); - - assert!( - TimelockedWeightCommits::::get(NetUidStorageIndex::from(netuid), stored_epoch) - .is_empty(), - "queue should be empty after successful reveal" - ); - }); -} - -#[test] -fn test_reveal_crv3_commits_legacy_payload_success() { - new_test_ext(100).execute_with(|| { - // ───────────────────────────────────── - // 1 ▸ network + neurons - // ───────────────────────────────────── - let netuid = NetUid::from(1); - let hotkey1: AccountId = U256::from(1); - let hotkey2: AccountId = U256::from(2); - let reveal_round: u64 = 1_000; - - add_network(netuid, /*tempo*/ 5, /*modality*/ 0); - register_ok_neuron(netuid, hotkey1, U256::from(3), 100_000); - register_ok_neuron(netuid, hotkey2, U256::from(4), 100_000); - - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); - - let uid1 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey1).unwrap(); - let uid2 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey2).unwrap(); - - SubtensorModule::set_validator_permit_for_uid(netuid, uid1, true); - SubtensorModule::set_validator_permit_for_uid(netuid, uid2, true); - - add_balance_to_coldkey_account(&U256::from(3), 1.into()); - add_balance_to_coldkey_account(&U256::from(4), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey1, - &U256::from(3), - netuid, - 1.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey2, - &U256::from(4), - netuid, - 1.into(), - ); - - // ───────────────────────────────────── - // 2 ▸ craft legacy payload (NO hotkey) - // ───────────────────────────────────── - let legacy_payload = LegacyWeightsTlockPayload { - uids: vec![uid1, uid2], - values: vec![10, 20], - version_key: SubtensorModule::get_weights_version_key(netuid), - }; - let serialized_payload = legacy_payload.encode(); - - // encrypt with TLE - let esk = [2u8; 32]; - let rng = ChaCha20Rng::seed_from_u64(0); - - let pk_bytes = hex::decode( - "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c\ - 8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb\ - 5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a", - ) - .unwrap(); - let pk = - ::PublicKeyGroup::deserialize_compressed(&*pk_bytes).unwrap(); - - let msg_hash = { - let mut h = sha2::Sha256::new(); - h.update(reveal_round.to_be_bytes()); - h.finalize().to_vec() - }; - let identity = Identity::new(b"", vec![msg_hash]); - - let ct = tle::( - pk, - esk, - &serialized_payload, - identity, - rng, - ) - .expect("encryption must succeed"); - - let mut commit_bytes = Vec::new(); - ct.serialize_compressed(&mut commit_bytes).unwrap(); - let bounded_commit: BoundedVec<_, ConstU32> = - commit_bytes.clone().try_into().unwrap(); - - // ───────────────────────────────────── - // 3 ▸ put commit on‑chain - // ───────────────────────────────────── - assert_ok!(SubtensorModule::do_commit_timelocked_weights( - RuntimeOrigin::signed(hotkey1), - netuid, - bounded_commit, - reveal_round, - SubtensorModule::get_commit_reveal_weights_version() - )); - - // insert pulse so reveal can succeed the first time - let sig_bytes = hex::decode( - "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e3\ - 42b73a8dd2bacbe47e4b6b63ed5e39", - ) - .unwrap(); - pallet_drand::Pulses::::insert( - reveal_round, - Pulse { - round: reveal_round, - randomness: vec![0; 32].try_into().unwrap(), - signature: sig_bytes.try_into().unwrap(), - }, - ); - - let commit_block = SubtensorModule::get_current_block_as_u64(); - let commit_epoch = SubtensorModule::get_epoch_index(netuid, commit_block); - - // ───────────────────────────────────── - // 4 ▸ advance epochs to trigger reveal - // ───────────────────────────────────── - step_epochs(3, netuid); - - // ───────────────────────────────────── - // 5 ▸ assertions - // ───────────────────────────────────── - let weights_sparse = SubtensorModule::get_weights_sparse(netuid.into()); - let w1 = weights_sparse - .get(uid1 as usize) - .cloned() - .unwrap_or_default(); - assert!(!w1.is_empty(), "weights must be set for uid1"); - - // find raw values for uid1 & uid2 - let w_map: std::collections::HashMap<_, _> = w1.into_iter().collect(); - let v1 = *w_map.get(&uid1).expect("uid1 weight"); - let v2 = *w_map.get(&uid2).expect("uid2 weight"); - assert!(v2 > v1, "uid2 weight should be greater than uid1 (20 > 10)"); - - // commit should be gone - assert!( - TimelockedWeightCommits::::get(NetUidStorageIndex::from(netuid), commit_epoch) - .is_empty(), - "commit storage should be cleaned after reveal" - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_subnet_owner_can_validate_without_stake_or_manual_permit --exact --show-output --nocapture -#[test] -fn test_subnet_owner_can_validate_without_stake_or_manual_permit() { - new_test_ext(0).execute_with(|| { - let owner_hotkey = U256::from(10); - let owner_coldkey = U256::from(11); - let other_hotkey = U256::from(20); - let other_coldkey = U256::from(21); - - // Create a real dynamic subnet whose owner hotkey is `owner_hotkey`. - let netuid = add_dynamic_network_disable_commit_reveal(&owner_hotkey, &owner_coldkey); - remove_owner_registration_stake(netuid); - - // Add one non-owner neuron with deterministic subnet stake. - register_ok_neuron(netuid, other_hotkey, other_coldkey, 0); - add_balance_to_coldkey_account(&other_coldkey, 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &other_hotkey, - &other_coldkey, - netuid, - 1.into(), - ); - - let owner_uid = - SubtensorModule::get_owner_uid(netuid).expect("subnet owner should resolve to a uid"); - let registered_owner_uid = - SubtensorModule::get_uid_for_net_and_hotkey(netuid, &owner_hotkey) - .expect("owner hotkey should be registered on the subnet"); - assert_eq!(registered_owner_uid, owner_uid); - - let other_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &other_hotkey) - .expect("other hotkey should be registered on the subnet"); - - let (owner_weight_stake, _, _) = - SubtensorModule::get_stake_weights_for_hotkey_on_subnet(&owner_hotkey, netuid); - let (other_weight_stake, _, _) = - SubtensorModule::get_stake_weights_for_hotkey_on_subnet(&other_hotkey, netuid); - assert!(owner_weight_stake < other_weight_stake); - - // Make the non-owner stake-qualified while the owner remains below threshold. - SubtensorModule::set_stake_threshold(1_u64); - assert!(SubtensorModule::check_weights_min_stake( - &other_hotkey, - netuid - )); - - // Clear all explicit permits. The owner should not rely on manual permit state. - SubtensorModule::set_validator_permit_for_uid(netuid, owner_uid, false); - SubtensorModule::set_validator_permit_for_uid(netuid, other_uid, false); - assert!(!SubtensorModule::get_validator_permit_for_uid( - netuid, owner_uid - )); - assert!(!SubtensorModule::get_validator_permit_for_uid( - netuid, other_uid - )); - - // Sanity check: a non-owner without a permit still cannot set non-self weights. - assert!(!SubtensorModule::check_validator_permit( - netuid, - other_uid, - &[owner_uid], - &[1u16], - )); - assert_eq!( - SubtensorModule::set_weights( - RuntimeOrigin::signed(other_hotkey), - netuid, - vec![owner_uid], - vec![1u16], - 0, - ), - Err(Error::::NeuronNoValidatorPermit.into()) - ); - - // The subnet owner bypasses both the stake gate and the validator-permit gate. - assert!(SubtensorModule::check_weights_min_stake( - &owner_hotkey, - netuid - )); - assert!(SubtensorModule::check_validator_permit( - netuid, - owner_uid, - &[other_uid], - &[1u16], - )); - - assert_ok!(SubtensorModule::set_weights( - RuntimeOrigin::signed(owner_hotkey), - netuid, - vec![other_uid], - vec![1u16], - 0, - )); - - // After an epoch, the owner is still validator-eligible even though only the - step_epochs(1, netuid); - assert!(SubtensorModule::get_validator_permit_for_uid( - netuid, owner_uid - )); - - // The original top-k result is preserved; the owner is added on top. - assert!(SubtensorModule::get_validator_permit_for_uid( - netuid, other_uid - )); - }); -} - -// Regression: when a batch of weight commits has per-item failures, each -// emitted BatchWeightItemFailed event must carry the netuid of the failing -// item so downstream consumers (indexers, validator monitors) can correlate -// failure → subnet without re-deriving from iteration order. -// -// Both netuids in this test fail (commit-reveal disabled on both) — what we -// assert is the *positional propagation*: the per-item events carry the -// distinct netuids that produced them, in iteration order. -#[test] -fn test_batch_commit_weights_item_failure_event_includes_netuid() { - new_test_ext(1).execute_with(|| { - let netuid_a = NetUid::from(1); - let netuid_b = NetUid::from(2); - add_network(netuid_a, 1, 0); - add_network(netuid_b, 1, 0); - SubtensorModule::set_commit_reveal_weights_enabled(netuid_a, false); - SubtensorModule::set_commit_reveal_weights_enabled(netuid_b, false); - - let hotkey = U256::from(1); - let netuids: Vec> = vec![netuid_a.into(), netuid_b.into()]; - let hashes: Vec = vec![H256::repeat_byte(0xAA), H256::repeat_byte(0xBB)]; - - assert_ok!(SubtensorModule::do_batch_commit_weights( - RuntimeOrigin::signed(hotkey), - netuids, - hashes, - )); - - let failures: Vec = System::events() - .iter() - .filter_map(|e| match &e.event { - RuntimeEvent::SubtensorModule(Event::BatchWeightItemFailed(netuid, _err)) => { - Some(*netuid) - } - _ => None, - }) - .collect(); - - assert_eq!( - failures, - vec![netuid_a, netuid_b], - "BatchWeightItemFailed events should carry each failing netuid in batch order" - ); - }); -} - -// Regression: same shape as the commit-path test, but for the set-path -// (`do_batch_set_weights`). Each failing item must emit a -// BatchWeightItemFailed carrying its netuid. -#[test] -fn test_batch_set_weights_item_failure_event_includes_netuid() { - new_test_ext(1).execute_with(|| { - let netuid_a = NetUid::from(3); - let netuid_b = NetUid::from(4); - add_network(netuid_a, 1, 0); - add_network(netuid_b, 1, 0); - // do_set_weights fails iff commit-reveal is ENABLED on the netuid. - SubtensorModule::set_commit_reveal_weights_enabled(netuid_a, true); - SubtensorModule::set_commit_reveal_weights_enabled(netuid_b, true); - - let hotkey = U256::from(11); - let netuids: Vec> = vec![netuid_a.into(), netuid_b.into()]; - let weights: Vec, Compact)>> = vec![vec![], vec![]]; - let version_keys: Vec> = vec![0u64.into(), 0u64.into()]; - - assert_ok!(SubtensorModule::do_batch_set_weights( - RuntimeOrigin::signed(hotkey), - netuids, - weights, - version_keys, - )); - - let failures: Vec = System::events() - .iter() - .filter_map(|e| match &e.event { - RuntimeEvent::SubtensorModule(Event::BatchWeightItemFailed(netuid, _err)) => { - Some(*netuid) - } - _ => None, - }) - .collect(); - - assert_eq!( - failures, - vec![netuid_a, netuid_b], - "BatchWeightItemFailed events should carry each failing netuid in batch order" - ); - }); -} diff --git a/pallets/subtensor/src/tests/weights/batch_reveal.rs b/pallets/subtensor/src/tests/weights/batch_reveal.rs new file mode 100644 index 0000000000..189d058498 --- /dev/null +++ b/pallets/subtensor/src/tests/weights/batch_reveal.rs @@ -0,0 +1,1120 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! `batch_reveal_weights` and related commit rate-limit coverage. + +use codec::Compact; +use frame_support::{assert_err, assert_ok}; +use scale_info::prelude::collections::HashMap; +use sp_core::{H256, U256}; +use sp_runtime::traits::{BlakeTwo256, Hash}; +use subtensor_runtime_common::NetUidStorageIndex; + +use crate::tests::mock::*; +use crate::*; + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_successful_batch_reveal --exact --show-output --nocapture +#[test] +fn test_successful_batch_reveal() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey = U256::from(1); + let version_keys: Vec = vec![0, 0, 0]; + let uids_list: Vec> = vec![vec![0, 1], vec![1, 0], vec![0, 1]]; + let weight_values_list: Vec> = vec![vec![10, 20], vec![30, 40], vec![50, 60]]; + let tempo: u16 = 100; + + System::set_block_number(0); + add_network(netuid, tempo, 0); + + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); + register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); + SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); + add_balance_to_coldkey_account(&U256::from(0), 1.into()); + add_balance_to_coldkey_account(&U256::from(1), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(0)), + &(U256::from(0)), + netuid, + 1.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(1)), + &(U256::from(1)), + netuid, + 1.into(), + ); + + // 1. Commit multiple times + let mut commit_info = Vec::new(); + for i in 0..3 { + let salt: Vec = vec![i as u16; 8]; + let commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids_list[i].clone(), + weight_values_list[i].clone(), + salt.clone(), + version_keys[i], + )); + commit_info.push((commit_hash, salt)); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash + )); + } + + step_epochs(1, netuid); + + // 2. Prepare batch reveal parameters + let salts_list: Vec> = commit_info.iter().map(|(_, salt)| salt.clone()).collect(); + + // 3. Perform batch reveal + assert_ok!(SubtensorModule::do_batch_reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids_list.clone(), + weight_values_list.clone(), + salts_list.clone(), + version_keys.clone(), + )); + + // 4. Ensure all commits are removed + let commits = crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey); + assert!(commits.is_none()); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_batch_reveal_with_expired_commits --exact --show-output --nocapture +#[test] +fn test_batch_reveal_with_expired_commits() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey = U256::from(1); + let version_keys: Vec = vec![0, 0, 0]; + let uids_list: Vec> = vec![vec![0, 1], vec![1, 0], vec![0, 1]]; + let weight_values_list: Vec> = vec![vec![10, 20], vec![30, 40], vec![50, 60]]; + let tempo: u16 = 100; + + System::set_block_number(0); + add_network(netuid, tempo, 0); + + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); + register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); + SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); + add_balance_to_coldkey_account(&U256::from(0), 1.into()); + add_balance_to_coldkey_account(&U256::from(1), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(0)), + &(U256::from(0)), + netuid, + 1.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(1)), + &(U256::from(1)), + netuid, + 1.into(), + ); + + let mut commit_info = Vec::new(); + + // 1. Commit the first weight in epoch 0 + let salt0: Vec = vec![0u16; 8]; + let commit_hash0: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids_list[0].clone(), + weight_values_list[0].clone(), + salt0.clone(), + version_keys[0], + )); + commit_info.push((commit_hash0, salt0)); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash0 + )); + + // Advance to epoch 1 + step_epochs(1, netuid); + + // 2. Commit the next two weights in epoch 1 + for i in 1..3 { + let salt: Vec = vec![i as u16; 8]; + let commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids_list[i].clone(), + weight_values_list[i].clone(), + salt.clone(), + version_keys[i], + )); + commit_info.push((commit_hash, salt)); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash + )); + } + + // Advance to epoch 2 (after reveal period for first commit) + step_epochs(1, netuid); + + // 3. Prepare batch reveal parameters + let salts_list: Vec> = commit_info.iter().map(|(_, salt)| salt.clone()).collect(); + + // 4. Perform batch reveal + let result = SubtensorModule::do_batch_reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids_list.clone(), + weight_values_list.clone(), + salts_list.clone(), + version_keys.clone(), + ); + assert_err!(result, Error::::ExpiredWeightCommit); + + // 5. Expired commit is not removed until a successful call + let commits = crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey) + .expect("Expected remaining commits"); + assert_eq!(commits.len(), 3); + + // 6. Try revealing the remaining commits + let valid_uids_list = uids_list[1..].to_vec(); + let valid_weight_values_list = weight_values_list[1..].to_vec(); + let valid_salts_list = salts_list[1..].to_vec(); + let valid_version_keys = version_keys[1..].to_vec(); + + assert_ok!(SubtensorModule::do_batch_reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + valid_uids_list, + valid_weight_values_list, + valid_salts_list, + valid_version_keys, + )); + + // 7. Ensure all commits are removed + let commits = crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey); + assert!(commits.is_none()); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_batch_reveal_with_invalid_input_lengths --exact --show-output --nocapture +#[test] +fn test_batch_reveal_with_invalid_input_lengths() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey = U256::from(1); + let tempo: u16 = 100; + + System::set_block_number(0); + add_network(netuid, tempo, 0); + + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + + // Base data for valid inputs + let uids_list: Vec> = vec![vec![0, 1], vec![1, 0]]; + let weight_values_list: Vec> = vec![vec![10, 20], vec![30, 40]]; + let salts_list: Vec> = vec![vec![0u16; 8], vec![1u16; 8]]; + let version_keys: Vec = vec![0, 0]; + + // Test cases with mismatched input lengths + + // Case 1: uids_list has an extra element + let uids_list_case = vec![vec![0, 1], vec![1, 0], vec![2, 3]]; + let result = SubtensorModule::do_batch_reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids_list_case.clone(), + weight_values_list.clone(), + salts_list.clone(), + version_keys.clone(), + ); + assert_err!(result, Error::::InputLengthsUnequal); + + // Case 2: weight_values_list has an extra element + let weight_values_list_case = vec![vec![10, 20], vec![30, 40], vec![50, 60]]; + let result = SubtensorModule::do_batch_reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids_list.clone(), + weight_values_list_case.clone(), + salts_list.clone(), + version_keys.clone(), + ); + assert_err!(result, Error::::InputLengthsUnequal); + + // Case 3: salts_list has an extra element + let salts_list_case = vec![vec![0u16; 8], vec![1u16; 8], vec![2u16; 8]]; + let result = SubtensorModule::do_batch_reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids_list.clone(), + weight_values_list.clone(), + salts_list_case.clone(), + version_keys.clone(), + ); + assert_err!(result, Error::::InputLengthsUnequal); + + // Case 4: version_keys has an extra element + let version_keys_case = vec![0, 0, 0]; + let result = SubtensorModule::do_batch_reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids_list.clone(), + weight_values_list.clone(), + salts_list.clone(), + version_keys_case.clone(), + ); + assert_err!(result, Error::::InputLengthsUnequal); + + // Case 5: All input vectors have mismatched lengths + let uids_list_case = vec![vec![0, 1]]; + let weight_values_list_case = vec![vec![10, 20], vec![30, 40]]; + let salts_list_case = vec![vec![0u16; 8]]; + let version_keys_case = vec![0, 0, 0]; + let result = SubtensorModule::do_batch_reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids_list_case, + weight_values_list_case, + salts_list_case, + version_keys_case, + ); + assert_err!(result, Error::::InputLengthsUnequal); + + // Case 6: Valid input lengths (should not return an error) + let result = SubtensorModule::do_batch_reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids_list.clone(), + weight_values_list.clone(), + salts_list.clone(), + version_keys.clone(), + ); + // We expect an error because no commits have been made, but it should not be InputLengthsUnequal + assert_err!(result, Error::::NoWeightsCommitFound); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_batch_reveal_with_no_commits --exact --show-output --nocapture +#[test] +fn test_batch_reveal_with_no_commits() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey = U256::from(1); + let version_keys: Vec = vec![0]; + let uids_list: Vec> = vec![vec![0, 1]]; + let weight_values_list: Vec> = vec![vec![10, 20]]; + let salts_list: Vec> = vec![vec![0u16; 8]]; + let tempo: u16 = 100; + + System::set_block_number(0); + add_network(netuid, tempo, 0); + + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + + // 1. Attempt to perform batch reveal without any commits + let result = SubtensorModule::do_batch_reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids_list, + weight_values_list, + salts_list, + version_keys, + ); + assert_err!(result, Error::::NoWeightsCommitFound); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_batch_reveal_before_reveal_period --exact --show-output --nocapture +#[test] +fn test_batch_reveal_before_reveal_period() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey = U256::from(1); + let version_keys: Vec = vec![0, 0]; + let uids_list: Vec> = vec![vec![0, 1], vec![1, 0]]; + let weight_values_list: Vec> = vec![vec![10, 20], vec![30, 40]]; + let tempo: u16 = 100; + + System::set_block_number(0); + add_network(netuid, tempo, 0); + + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + + register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); + register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); + SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); + + // 1. Commit multiple times in the same epoch + let mut commit_info = Vec::new(); + for i in 0..2 { + let salt: Vec = vec![i as u16; 8]; + let commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids_list[i].clone(), + weight_values_list[i].clone(), + salt.clone(), + version_keys[i], + )); + commit_info.push((commit_hash, salt)); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash + )); + } + + // 2. Prepare batch reveal parameters + let salts_list: Vec> = commit_info.iter().map(|(_, salt)| salt.clone()).collect(); + + // 3. Attempt to reveal before reveal period + let result = SubtensorModule::do_batch_reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids_list.clone(), + weight_values_list.clone(), + salts_list.clone(), + version_keys.clone(), + ); + assert_err!(result, Error::::RevealTooEarly); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_batch_reveal_after_commits_expired --exact --show-output --nocapture +#[test] +fn test_batch_reveal_after_commits_expired() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey = U256::from(1); + let version_keys: Vec = vec![0, 0]; + let uids_list: Vec> = vec![vec![0, 1], vec![1, 0]]; + let weight_values_list: Vec> = vec![vec![10, 20], vec![30, 40]]; + let tempo: u16 = 100; + + System::set_block_number(0); + add_network(netuid, tempo, 0); + + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + + register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); + register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); + SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); + + let mut commit_info = Vec::new(); + + // 1. Commit the first weight in epoch 0 + let salt0: Vec = vec![0u16; 8]; + let commit_hash0: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids_list[0].clone(), + weight_values_list[0].clone(), + salt0.clone(), + version_keys[0], + )); + commit_info.push((commit_hash0, salt0)); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash0 + )); + + // Advance to epoch 1 + step_epochs(1, netuid); + + // 2. Commit the second weight in epoch 1 + let salt1: Vec = vec![1u16; 8]; + let commit_hash1: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids_list[1].clone(), + weight_values_list[1].clone(), + salt1.clone(), + version_keys[1], + )); + commit_info.push((commit_hash1, salt1)); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash1 + )); + + // Advance to epoch 4 to ensure both commits have expired (assuming reveal_period is 1) + step_epochs(3, netuid); + + // 3. Prepare batch reveal parameters + let salts_list: Vec> = commit_info.iter().map(|(_, salt)| salt.clone()).collect(); + + // 4. Attempt to reveal after commits have expired + let result = SubtensorModule::do_batch_reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids_list.clone(), + weight_values_list.clone(), + salts_list, + version_keys.clone(), + ); + assert_err!(result, Error::::ExpiredWeightCommit); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_batch_reveal_when_commit_reveal_disabled --exact --show-output --nocapture +#[test] +fn test_batch_reveal_when_commit_reveal_disabled() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey = U256::from(1); + let version_keys: Vec = vec![0]; + let uids_list: Vec> = vec![vec![0, 1]]; + let weight_values_list: Vec> = vec![vec![10, 20]]; + let salts_list: Vec> = vec![vec![0u16; 8]]; + let tempo: u16 = 100; + + System::set_block_number(0); + add_network(netuid, tempo, 0); + + SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); + + // 1. Attempt to perform batch reveal when commit-reveal is disabled + let result = SubtensorModule::do_batch_reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids_list, + weight_values_list, + salts_list, + version_keys, + ); + assert_err!(result, Error::::CommitRevealDisabled); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_batch_reveal_with_out_of_order_commits --exact --show-output --nocapture +#[test] +fn test_batch_reveal_with_out_of_order_commits() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey = U256::from(1); + let version_keys: Vec = vec![0, 0, 0]; + let uids_list: Vec> = vec![vec![0, 1], vec![1, 0], vec![0, 1]]; + let weight_values_list: Vec> = vec![vec![10, 20], vec![30, 40], vec![50, 60]]; + let tempo: u16 = 100; + + System::set_block_number(0); + add_network(netuid, tempo, 0); + + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); + register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); + SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); + add_balance_to_coldkey_account(&U256::from(0), 1.into()); + add_balance_to_coldkey_account(&U256::from(1), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(0)), + &(U256::from(0)), + netuid, + 1.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(1)), + &(U256::from(1)), + netuid, + 1.into(), + ); + + // 1. Commit multiple times (A, B, C) + let mut commit_info = Vec::new(); + for i in 0..3 { + let salt: Vec = vec![i as u16; 8]; + let commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids_list[i].clone(), + weight_values_list[i].clone(), + salt.clone(), + version_keys[i], + )); + commit_info.push((commit_hash, salt)); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash + )); + } + + step_epochs(1, netuid); + + // 2. Prepare batch reveal parameters for commits A and C (out of order) + let salts_list: Vec> = vec![ + commit_info[2].1.clone(), // Third commit (C) + commit_info[0].1.clone(), // First commit (A) + ]; + let uids_list_out_of_order = vec![ + uids_list[2].clone(), // C + uids_list[0].clone(), // A + ]; + let weight_values_list_out_of_order = vec![ + weight_values_list[2].clone(), // C + weight_values_list[0].clone(), // A + ]; + let version_keys_out_of_order = vec![ + version_keys[2], // C + version_keys[0], // A + ]; + + // 3. Attempt batch reveal of A and C out of order + let result = SubtensorModule::do_batch_reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids_list_out_of_order, + weight_values_list_out_of_order, + salts_list, + version_keys_out_of_order, + ); + + // 4. Ensure the batch reveal succeeds + assert_ok!(result); + + // 5. Prepare and reveal the remaining commit (B) + let remaining_salt = commit_info[1].1.clone(); + let remaining_uids = uids_list[1].clone(); + let remaining_weights = weight_values_list[1].clone(); + let remaining_version_key = version_keys[1]; + + assert_ok!(SubtensorModule::do_batch_reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + vec![remaining_uids], + vec![remaining_weights], + vec![remaining_salt], + vec![remaining_version_key], + )); + + // 6. Ensure all commits are removed + let commits = crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey); + assert!(commits.is_none()); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_highly_concurrent_commits_and_reveals_with_multiple_hotkeys --exact --show-output --nocapture +#[test] +fn test_highly_concurrent_commits_and_reveals_with_multiple_hotkeys() { + new_test_ext(1).execute_with(|| { + // ==== Test Configuration ==== + let netuid = NetUid::from(1); + let num_hotkeys: usize = 10; + let max_unrevealed_commits: usize = 10; + let commits_per_hotkey: usize = 20; + let initial_reveal_period: u64 = 5; + let initial_tempo: u16 = 100; + + // ==== Setup Network ==== + add_network(netuid, initial_tempo, 0); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + assert_ok!(SubtensorModule::set_reveal_period(netuid, initial_reveal_period)); + SubtensorModule::set_max_registrations_per_block(netuid, u16::MAX); + SubtensorModule::set_target_registrations_per_interval(netuid, u16::MAX); + + // ==== Register Validators ==== + for uid in 0..5 { + let validator_id = U256::from(100 + uid as u64); + register_ok_neuron(netuid, validator_id, U256::from(200 + uid as u64), 300_000); + SubtensorModule::set_validator_permit_for_uid(netuid, uid, true); + } + + // ==== Register Hotkeys ==== + let mut hotkeys: Vec<::AccountId> = Vec::new(); + for i in 0..num_hotkeys { + let hotkey_id = U256::from(1000 + i as u64); + register_ok_neuron(netuid, hotkey_id, U256::from(2000 + i as u64), 100_000); + hotkeys.push(hotkey_id); + } + + // ==== Initialize Commit Information ==== + let mut commit_info_map: HashMap< + ::AccountId, + Vec<(H256, Vec, Vec, Vec, u64)>, + > = HashMap::new(); + + // Initialize the map + for hotkey in &hotkeys { + commit_info_map.insert(*hotkey, Vec::new()); + } + + // ==== Function to Generate Unique Data ==== + fn generate_unique_data(index: usize) -> (Vec, Vec, Vec, u64) { + let uids = vec![index as u16, (index + 1) as u16]; + let values = vec![(index * 10) as u16, ((index + 1) * 10) as u16]; + let salt = vec![(index % 100) as u16; 8]; + let version_key = index as u64; + (uids, values, salt, version_key) + } + + // ==== Simulate Concurrent Commits and Reveals ==== + for i in 0..commits_per_hotkey { + for hotkey in &hotkeys { + + let current_commits = crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey) + .unwrap_or_default(); + if current_commits.len() >= max_unrevealed_commits { + continue; + } + + let (uids, values, salt, version_key) = generate_unique_data(i); + let commit_hash: H256 = BlakeTwo256::hash_of(&( + *hotkey, + netuid, + uids.clone(), + values.clone(), + salt.clone(), + version_key, + )); + + if let Some(commits) = commit_info_map.get_mut(hotkey) { + commits.push((commit_hash, salt.clone(), uids.clone(), values.clone(), version_key)); + } + + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(*hotkey), + netuid, + commit_hash + )); + } + + // ==== Reveal Phase ==== + for hotkey in &hotkeys { + if let Some(commits) = commit_info_map.get_mut(hotkey) { + if commits.is_empty() { + continue; // No commits to reveal + } + + let (_commit_hash, salt, uids, values, version_key) = commits.first().expect("expected a value"); + + let reveal_result = SubtensorModule::reveal_weights( + RuntimeOrigin::signed(*hotkey), + netuid, + uids.clone(), + values.clone(), + salt.clone(), + *version_key, + ); + + match reveal_result { + Ok(_) => { + commits.remove(0); + } + Err(e) => { + if e == Error::::RevealTooEarly.into() + || e == Error::::ExpiredWeightCommit.into() + || e == Error::::InvalidRevealCommitHashNotMatch.into() + { + log::info!("Expected error during reveal after epoch advancement: {e:?}"); + } else { + panic!( + "Unexpected error during reveal: {e:?}, expected RevealTooEarly, ExpiredWeightCommit, or InvalidRevealCommitHashNotMatch" + ); + } + } + } + } + } + } + + // ==== Modify Network Parameters During Commits ==== + SubtensorModule::set_tempo_unchecked(netuid, 150); + assert_ok!(SubtensorModule::set_reveal_period(netuid, 7)); + log::info!("Changed tempo to 150 and reveal_period to 7 during commits."); + + step_epochs(3, netuid); + + // ==== Continue Reveals After Epoch Advancement ==== + for hotkey in &hotkeys { + if let Some(commits) = commit_info_map.get_mut(hotkey) { + while !commits.is_empty() { + let (_commit_hash, salt, uids, values, version_key) = &commits[0]; + + // Attempt to reveal + let reveal_result = SubtensorModule::reveal_weights( + RuntimeOrigin::signed(*hotkey), + netuid, + uids.clone(), + values.clone(), + salt.clone(), + *version_key, + ); + + match reveal_result { + Ok(_) => { + commits.remove(0); + } + Err(e) => { + // Check if the error is due to reveal being too early or commit expired + if e == Error::::RevealTooEarly.into() + || e == Error::::ExpiredWeightCommit.into() + || e == Error::::InvalidRevealCommitHashNotMatch.into() + { + log::info!("Expected error during reveal after epoch advancement: {e:?}"); + break; + } else { + panic!( + "Unexpected error during reveal after epoch advancement: {e:?}, expected RevealTooEarly, ExpiredWeightCommit, or InvalidRevealCommitHashNotMatch" + ); + } + } + } + } + } + } + + // ==== Change Network Parameters Again ==== + SubtensorModule::set_tempo_unchecked(netuid, 200); + assert_ok!(SubtensorModule::set_reveal_period(netuid, 10)); + log::info!("Changed tempo to 200 and reveal_period to 10 after initial reveals."); + + step_epochs(10, netuid); + + // ==== Final Reveal Attempts ==== + for (hotkey, commits) in commit_info_map.iter_mut() { + for (_commit_hash, salt, uids, values, version_key) in commits.iter() { + let reveal_result = SubtensorModule::reveal_weights( + RuntimeOrigin::signed(*hotkey), + netuid, + uids.clone(), + values.clone(), + salt.clone(), + *version_key, + ); + + assert_eq!( + reveal_result, + Err(Error::::ExpiredWeightCommit.into()), + "Expected ExpiredWeightCommit error, got {reveal_result:?}" + ); + } +} + + for hotkey in &hotkeys { + commit_info_map.insert(*hotkey, Vec::new()); + + for i in 0..max_unrevealed_commits { + let (uids, values, salt, version_key) = generate_unique_data(i + commits_per_hotkey); + let commit_hash: H256 = BlakeTwo256::hash_of(&( + *hotkey, + netuid, + uids.clone(), + values.clone(), + salt.clone(), + version_key, + )); + + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(*hotkey), + netuid, + commit_hash + )); + } + + let (uids, values, salt, version_key) = generate_unique_data(max_unrevealed_commits + commits_per_hotkey); + let commit_hash: H256 = BlakeTwo256::hash_of(&( + *hotkey, + netuid, + uids.clone(), + values.clone(), + salt.clone(), + version_key, + )); + + assert_err!( + SubtensorModule::commit_weights( + RuntimeOrigin::signed(*hotkey), + netuid, + commit_hash + ), + Error::::TooManyUnrevealedCommits + ); + } + + // Attempt unauthorized reveal + let unauthorized_hotkey = hotkeys[0]; + let target_hotkey = hotkeys[1]; + if let Some(commits) = commit_info_map.get(&target_hotkey) + && let Some((_commit_hash, salt, uids, values, version_key)) = commits.first() { + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(unauthorized_hotkey), + netuid, + uids.clone(), + values.clone(), + salt.clone(), + *version_key, + ), + Error::::InvalidRevealCommitHashNotMatch + ); + } + + let non_committing_hotkey: ::AccountId = U256::from(9999); + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(non_committing_hotkey), + netuid, + vec![0, 1], + vec![10, 20], + vec![0; 8], + 0, + ), + Error::::NoWeightsCommitFound + ); + + assert_eq!(SubtensorModule::get_reveal_period(netuid), 10); + assert_eq!(SubtensorModule::get_tempo(netuid), 200); + }) +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_commit_weights_rate_limit --exact --show-output --nocapture +#[test] +fn test_commit_weights_rate_limit() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let uids: Vec = vec![0, 1]; + let weight_values: Vec = vec![10, 10]; + let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; + let version_key: u64 = 0; + let hotkey: U256 = U256::from(1); + + let commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + )); + System::set_block_number(11); + + let tempo: u16 = 5; + add_network(netuid, tempo, 0); + + register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); + register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_weights_set_rate_limit(netuid, 10); // Rate limit is 10 blocks + SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); + SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + add_balance_to_coldkey_account(&U256::from(0), 1.into()); + add_balance_to_coldkey_account(&U256::from(1), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(0)), + &(U256::from(0)), + netuid, + 1.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(1)), + &(U256::from(1)), + netuid, + 1.into(), + ); + + let neuron_uid = + SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey).expect("expected uid"); + SubtensorModule::set_last_update_for_uid(NetUidStorageIndex::from(netuid), neuron_uid, 0); + + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash + )); + + let new_salt: Vec = vec![9; 8]; + let new_commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + new_salt.clone(), + version_key, + )); + assert_err!( + SubtensorModule::commit_weights(RuntimeOrigin::signed(hotkey), netuid, new_commit_hash), + Error::::CommittingWeightsTooFast + ); + + step_block(5); + assert_err!( + SubtensorModule::commit_weights(RuntimeOrigin::signed(hotkey), netuid, new_commit_hash), + Error::::CommittingWeightsTooFast + ); + + step_block(5); // Current block is now 21 + + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + new_commit_hash + )); + + SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); + let weights_keys: Vec = vec![0]; + let weight_values: Vec = vec![1]; + + assert_err!( + SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid, + weights_keys.clone(), + weight_values.clone(), + 0 + ), + Error::::SettingWeightsTooFast + ); + + step_block(10); + + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid, + weights_keys.clone(), + weight_values.clone(), + 0 + )); + + assert_err!( + SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid, + weights_keys.clone(), + weight_values.clone(), + 0 + ), + Error::::SettingWeightsTooFast + ); + + step_block(5); + + assert_err!( + SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid, + weights_keys.clone(), + weight_values.clone(), + 0 + ), + Error::::SettingWeightsTooFast + ); + + step_block(5); + + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid, + weights_keys.clone(), + weight_values.clone(), + 0 + )); + }); +} + +#[test] +fn test_batch_commit_weights_item_failure_event_includes_netuid() { + new_test_ext(1).execute_with(|| { + let netuid_a = NetUid::from(1); + let netuid_b = NetUid::from(2); + add_network(netuid_a, 1, 0); + add_network(netuid_b, 1, 0); + SubtensorModule::set_commit_reveal_weights_enabled(netuid_a, false); + SubtensorModule::set_commit_reveal_weights_enabled(netuid_b, false); + + let hotkey = U256::from(1); + let netuids: Vec> = vec![netuid_a.into(), netuid_b.into()]; + let hashes: Vec = vec![H256::repeat_byte(0xAA), H256::repeat_byte(0xBB)]; + + assert_ok!(SubtensorModule::do_batch_commit_weights( + RuntimeOrigin::signed(hotkey), + netuids, + hashes, + )); + + let failures: Vec = System::events() + .iter() + .filter_map(|e| match &e.event { + RuntimeEvent::SubtensorModule(Event::BatchWeightItemFailed(netuid, _err)) => { + Some(*netuid) + } + _ => None, + }) + .collect(); + + assert_eq!( + failures, + vec![netuid_a, netuid_b], + "BatchWeightItemFailed events should carry each failing netuid in batch order" + ); + }); +} + +// Regression: same shape as the commit-path test, but for the set-path +// (`do_batch_set_weights`). Each failing item must emit a +// BatchWeightItemFailed carrying its netuid. +#[test] +fn test_batch_set_weights_item_failure_event_includes_netuid() { + new_test_ext(1).execute_with(|| { + let netuid_a = NetUid::from(3); + let netuid_b = NetUid::from(4); + add_network(netuid_a, 1, 0); + add_network(netuid_b, 1, 0); + // do_set_weights fails iff commit-reveal is ENABLED on the netuid. + SubtensorModule::set_commit_reveal_weights_enabled(netuid_a, true); + SubtensorModule::set_commit_reveal_weights_enabled(netuid_b, true); + + let hotkey = U256::from(11); + let netuids: Vec> = vec![netuid_a.into(), netuid_b.into()]; + let weights: Vec, Compact)>> = vec![vec![], vec![]]; + let version_keys: Vec> = vec![0u64.into(), 0u64.into()]; + + assert_ok!(SubtensorModule::do_batch_set_weights( + RuntimeOrigin::signed(hotkey), + netuids, + weights, + version_keys, + )); + + let failures: Vec = System::events() + .iter() + .filter_map(|e| match &e.event { + RuntimeEvent::SubtensorModule(Event::BatchWeightItemFailed(netuid, _err)) => { + Some(*netuid) + } + _ => None, + }) + .collect(); + + assert_eq!( + failures, + vec![netuid_a, netuid_b], + "BatchWeightItemFailed events should carry each failing netuid in batch order" + ); + }); +} diff --git a/pallets/subtensor/src/tests/weights/commit_reveal.rs b/pallets/subtensor/src/tests/weights/commit_reveal.rs new file mode 100644 index 0000000000..7d0963e390 --- /dev/null +++ b/pallets/subtensor/src/tests/weights/commit_reveal.rs @@ -0,0 +1,1103 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! Hash-based commit–reveal weights (`commit_weights` / `reveal_weights`). + +use frame_support::{assert_err, assert_ok}; +use sp_core::{H256, U256}; +use sp_runtime::traits::{BlakeTwo256, Hash}; +use subtensor_runtime_common::NetUidStorageIndex; + +use crate::tests::mock::*; +use crate::*; + +#[test] +fn test_set_weights_commit_reveal_enabled_error() { + new_test_ext(0).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + register_ok_neuron(netuid, U256::from(1), U256::from(2), 10); + + let uids = vec![0]; + let weights = vec![1]; + let version_key: u64 = 0; + let hotkey = U256::from(1); + + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + + assert_err!( + SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weights.clone(), + version_key + ), + Error::::CommitRevealEnabled + ); + + SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); + + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids, + weights, + version_key + )); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_weights_when_commit_reveal_disabled --exact --show-output --nocapture +#[test] +fn test_reveal_weights_when_commit_reveal_disabled() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let uids: Vec = vec![0, 1]; + let weight_values: Vec = vec![10, 10]; + let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; + let version_key: u64 = 0; + let hotkey: U256 = U256::from(1); + + let commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + )); + + System::set_block_number(0); + + let tempo: u16 = 5; + add_network(netuid, tempo, 0); + + // Register neurons and set up configurations + register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); + register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); + SubtensorModule::set_weights_set_rate_limit(netuid, 5); + SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); + SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); + + // Enable commit-reveal and commit + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash + )); + + step_epochs(1, netuid); + + // Disable commit-reveal before reveal + SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); + + // Attempt to reveal, should fail with CommitRevealDisabled + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids, + weight_values, + salt, + version_key, + ), + Error::::CommitRevealDisabled + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_commit_reveal_weights_ok --exact --show-output --nocapture +#[test] +fn test_commit_reveal_weights_ok() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let uids: Vec = vec![0, 1]; + let weight_values: Vec = vec![10, 10]; + let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; + let version_key: u64 = 0; + let hotkey: U256 = U256::from(1); + + let commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + )); + + System::set_block_number(0); + + let tempo: u16 = 5; + add_network(netuid, tempo, 0); + + // Register neurons and set up configurations + register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); + register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_weights_set_rate_limit(netuid, 5); + SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); + SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + add_balance_to_coldkey_account(&U256::from(0), 1.into()); + add_balance_to_coldkey_account(&U256::from(1), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(0)), + &(U256::from(0)), + netuid, + 1.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(1)), + &(U256::from(1)), + netuid, + 1.into(), + ); + + // Commit at block 0 + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash + )); + + step_epochs(1, netuid); + + // Reveal in the next epoch + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids, + weight_values, + salt, + version_key, + )); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_commit_reveal_tempo_interval --exact --show-output --nocapture +#[test] +fn test_commit_reveal_tempo_interval() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let uids: Vec = vec![0, 1]; + let weight_values: Vec = vec![10, 10]; + let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; + let version_key: u64 = 0; + let hotkey: U256 = U256::from(1); + + let commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + )); + + System::set_block_number(0); + + let tempo: u16 = 100; + add_network(netuid, tempo, 0); + + register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); + register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_weights_set_rate_limit(netuid, 5); + SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); + SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + add_balance_to_coldkey_account(&U256::from(0), 1.into()); + add_balance_to_coldkey_account(&U256::from(1), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(0)), + &(U256::from(0)), + netuid, + 1.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(1)), + &(U256::from(1)), + netuid, + 1.into(), + ); + + // Commit at block 0 + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash + )); + + // Attempt to reveal in the same epoch, should fail + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + ), + Error::::RevealTooEarly + ); + + step_epochs(1, netuid); + + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + )); + + step_block(6); + + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + ), + Error::::NoWeightsCommitFound + ); + + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash + )); + + // step two epochs + step_epochs(2, netuid); + + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + ), + Error::::ExpiredWeightCommit + ); + + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash + )); + + step_block(50); + + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + ), + Error::::RevealTooEarly + ); + + step_epochs(1, netuid); + + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids, + weight_values, + salt, + version_key, + )); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_commit_reveal_hash --exact --show-output --nocapture +#[test] +fn test_commit_reveal_hash() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let uids: Vec = vec![0, 1]; + let weight_values: Vec = vec![10, 10]; + let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; + let bad_salt: Vec = vec![0, 2, 3, 4, 5, 6, 7, 8]; + let version_key: u64 = 0; + let hotkey: U256 = U256::from(1); + + add_network(netuid, 5, 0); + System::set_block_number(0); + + register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); + register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_weights_set_rate_limit(netuid, 5); + SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); + SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); + add_balance_to_coldkey_account(&U256::from(0), 1.into()); + add_balance_to_coldkey_account(&U256::from(1), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(0)), + &(U256::from(0)), + netuid, + 1.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(1)), + &(U256::from(1)), + netuid, + 1.into(), + ); + + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + + let commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + )); + + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash + )); + + step_epochs(1, netuid); + + // Attempt to reveal with incorrect data, should fail + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + vec![0, 2], + weight_values.clone(), + salt.clone(), + version_key + ), + Error::::InvalidRevealCommitHashNotMatch + ); + + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + bad_salt.clone(), + version_key, + ), + Error::::InvalidRevealCommitHashNotMatch + ); + + // Correct reveal, should succeed + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids, + weight_values, + salt, + version_key, + )); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_commit_reveal_disabled_or_enabled --exact --show-output --nocapture +#[test] +fn test_commit_reveal_disabled_or_enabled() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let uids: Vec = vec![0, 1]; + let weight_values: Vec = vec![10, 10]; + let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; + let version_key: u64 = 0; + let hotkey: U256 = U256::from(1); + + let commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + )); + + add_network(netuid, 5, 0); + System::set_block_number(0); + + register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); + register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_weights_set_rate_limit(netuid, 5); + SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); + SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); + add_balance_to_coldkey_account(&U256::from(0), 1.into()); + add_balance_to_coldkey_account(&U256::from(1), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(0)), + &(U256::from(0)), + netuid, + 1.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(1)), + &(U256::from(1)), + netuid, + 1.into(), + ); + + // Disable commit/reveal + SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); + + // Attempt to commit, should fail + assert_err!( + SubtensorModule::commit_weights(RuntimeOrigin::signed(hotkey), netuid, commit_hash), + Error::::CommitRevealDisabled + ); + + // Enable commit/reveal + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + + // Commit should now succeed + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash + )); + + step_epochs(1, netuid); + + // Reveal should succeed + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids, + weight_values, + salt, + version_key, + )); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_toggle_commit_reveal_weights_and_set_weights --exact --show-output --nocapture +#[test] +fn test_toggle_commit_reveal_weights_and_set_weights() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let uids: Vec = vec![0, 1]; + let weight_values: Vec = vec![10, 10]; + let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; + let version_key: u64 = 0; + let hotkey: U256 = U256::from(1); + + let commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + )); + + add_network(netuid, 5, 0); + System::set_block_number(0); + + register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); + register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); + SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); + SubtensorModule::set_weights_set_rate_limit(netuid, 5); + add_balance_to_coldkey_account(&U256::from(0), 1.into()); + add_balance_to_coldkey_account(&U256::from(1), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(0)), + &(U256::from(0)), + netuid, + 1.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(1)), + &(U256::from(1)), + netuid, + 1.into(), + ); + + // Enable commit/reveal + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + + // Commit at block 0 + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash + )); + + step_epochs(1, netuid); + + // Reveal in the next epoch + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + )); + + // Disable commit/reveal + SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); + + // Advance to allow setting weights (due to rate limit) + step_block(5); + + // Set weights directly + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids, + weight_values, + version_key, + )); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_tempo_change_during_commit_reveal_process --exact --show-output --nocapture +#[test] +fn test_tempo_change_during_commit_reveal_process() { + new_test_ext(0).execute_with(|| { + let netuid = NetUid::from(1); + let uids: Vec = vec![0, 1]; + let weight_values: Vec = vec![10, 10]; + let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; + let version_key: u64 = 0; + let hotkey: U256 = U256::from(1); + + let commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + )); + + System::set_block_number(0); + + let tempo: u16 = 100; + add_network(netuid, tempo, 0); + + register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); + register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_weights_set_rate_limit(netuid, 5); + SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); + SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + add_balance_to_coldkey_account(&U256::from(0), 1.into()); + add_balance_to_coldkey_account(&U256::from(1), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(0)), + &(U256::from(0)), + netuid, + 1.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(1)), + &(U256::from(1)), + netuid, + 1.into(), + ); + + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash + )); + log::info!( + "Commit successful at block {}", + SubtensorModule::get_current_block_as_u64() + ); + + step_block(9); + log::info!( + "Advanced to block {}", + SubtensorModule::get_current_block_as_u64() + ); + + let tempo_before_next_reveal: u16 = 200; + log::info!("Changing tempo to {tempo_before_next_reveal}"); + SubtensorModule::set_tempo_unchecked(netuid, tempo_before_next_reveal); + + step_epochs(1, netuid); + log::info!( + "Advanced to block {}", + SubtensorModule::get_current_block_as_u64() + ); + + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + )); + log::info!( + "Revealed at block {}", + SubtensorModule::get_current_block_as_u64() + ); + + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash + )); + log::info!( + "Commit successful at block {}", + SubtensorModule::get_current_block_as_u64() + ); + + let tempo: u16 = 150; + log::info!("Changing tempo to {tempo}"); + SubtensorModule::set_tempo_unchecked(netuid, tempo); + + step_epochs(1, netuid); + log::info!( + "Advanced to block {}", + SubtensorModule::get_current_block_as_u64() + ); + + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + )); + log::info!( + "Revealed at block {}", + SubtensorModule::get_current_block_as_u64() + ); + + let tempo: u16 = 1050; + log::info!("Changing tempo to {tempo}"); + SubtensorModule::set_tempo_unchecked(netuid, tempo); + + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash + )); + log::info!( + "Commit successful at block {}", + SubtensorModule::get_current_block_as_u64() + ); + + let tempo: u16 = 805; + log::info!("Changing tempo to {tempo}"); + SubtensorModule::set_tempo_unchecked(netuid, tempo); + + step_epochs(1, netuid); + log::info!( + "Advanced to block {}", + SubtensorModule::get_current_block_as_u64() + ); + + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + )); + log::info!( + "Revealed at block {}", + SubtensorModule::get_current_block_as_u64() + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_commit_reveal_multiple_commits --exact --show-output --nocapture +#[test] +fn test_commit_reveal_multiple_commits() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let uids: Vec = vec![0, 1]; + let weight_values: Vec = vec![10, 10]; + let version_key: u64 = 0; + let hotkey: U256 = U256::from(1); + + System::set_block_number(0); + + let tempo: u16 = 7200; + add_network(netuid, tempo, 0); + + // Setup the network and neurons + register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); + register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); + SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + add_balance_to_coldkey_account(&U256::from(0), 1.into()); + add_balance_to_coldkey_account(&U256::from(1), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(0)), + &(U256::from(0)), + netuid, + 1.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(1)), + &(U256::from(1)), + netuid, + 1.into(), + ); + + // 1. Commit 10 times successfully + let mut commit_info = Vec::new(); + for i in 0..10 { + let salt_i: Vec = vec![i; 8]; // Unique salt for each commit + let commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt_i.clone(), + version_key, + )); + commit_info.push((commit_hash, salt_i)); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash + )); + } + + // 2. Attempt to commit an 11th time, should fail + let salt_11: Vec = vec![11; 8]; + let commit_hash_11: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt_11.clone(), + version_key, + )); + assert_err!( + SubtensorModule::commit_weights(RuntimeOrigin::signed(hotkey), netuid, commit_hash_11), + Error::::TooManyUnrevealedCommits + ); + + // 3. Attempt to reveal out of order (reveal the second commit first) + // Advance to the next epoch for reveals to be valid + step_epochs(1, netuid); + + // Try to reveal the second commit first + let (_commit_hash_2, salt_2) = &commit_info[1]; + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt_2.clone(), + version_key, + )); + + // Check that commits before the revealed one are removed + let remaining_commits = + crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey) + .expect("expected 8 remaining commits"); + assert_eq!(remaining_commits.len(), 8); // 10 commits - 2 removed (index 0 and 1) + + // 4. Reveal the last commit next + let (_commit_hash_10, salt_10) = &commit_info[9]; + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt_10.clone(), + version_key, + )); + + // Remaining commits should have removed up to index 9 + let remaining_commits = + crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey); + assert!(remaining_commits.is_none()); // All commits removed + + // After revealing all commits, attempt to commit again should now succeed + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash_11 + )); + + // 5. Test expired commits are removed and do not block reveals + // Commit again and let the commit expire + let salt_12: Vec = vec![12; 8]; + let commit_hash_12: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt_12.clone(), + version_key, + )); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash_12 + )); + + // Advance two epochs so the commit expires + step_epochs(2, netuid); + + // Attempt to reveal the expired commit, should fail + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt_12.clone(), + version_key, + ), + Error::::ExpiredWeightCommit + ); + + // Commit again and reveal after advancing to next epoch + let salt_13: Vec = vec![13; 8]; + let commit_hash_13: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt_13.clone(), + version_key, + )); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash_13 + )); + + step_epochs(1, netuid); + + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt_13.clone(), + version_key, + )); + + // 6. Ensure that attempting to reveal after the valid reveal period fails + // Commit again + let salt_14: Vec = vec![14; 8]; + let commit_hash_14: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt_14.clone(), + version_key, + )); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash_14 + )); + + // Advance beyond the valid reveal period (more than one epoch) + step_epochs(2, netuid); + + // Attempt to reveal, should fail + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt_14.clone(), + version_key, + ), + Error::::ExpiredWeightCommit + ); + + // 7. Attempt to reveal a commit that is not ready yet (before the reveal period) + // Commit again + let salt_15: Vec = vec![15; 8]; + let commit_hash_15: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt_15.clone(), + version_key, + )); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash_15 + )); + + // Attempt to reveal immediately, should fail + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt_15.clone(), + version_key, + ), + Error::::RevealTooEarly + ); + + step_epochs(1, netuid); + + // Now reveal should succeed + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt_15.clone(), + version_key, + )); + + // 8. Test that revealing with incorrect data (salt) fails + // Commit again + let salt_16: Vec = vec![16; 8]; + let commit_hash_16: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt_16.clone(), + version_key, + )); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash_16 + )); + + step_epochs(1, netuid); + + // Attempt to reveal with incorrect salt + let wrong_salt: Vec = vec![99; 8]; + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + wrong_salt.clone(), + version_key, + ), + Error::::InvalidRevealCommitHashNotMatch + ); + + // Reveal with correct data should succeed + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt_16.clone(), + version_key, + )); + + // 9. Test that attempting to reveal when there are no commits fails + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt_16.clone(), + version_key, + ), + Error::::NoWeightsCommitFound + ); + + // 10. Commit twice and attempt to reveal out of sequence (which is now allowed) + let salt_a: Vec = vec![21; 8]; + let commit_hash_a: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt_a.clone(), + version_key, + )); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash_a + )); + + let salt_b: Vec = vec![22; 8]; + let commit_hash_b: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt_b.clone(), + version_key, + )); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash_b + )); + + step_epochs(1, netuid); + + // Reveal the second commit first, should now succeed + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt_b.clone(), + version_key, + )); + + // Check that the first commit has been removed + let remaining_commits = + crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey); + assert!(remaining_commits.is_none()); + + // Attempting to reveal the first commit should fail as it was removed + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids, + weight_values, + salt_a, + version_key, + ), + Error::::NoWeightsCommitFound + ); + }); +} diff --git a/pallets/subtensor/src/tests/weights/commit_reveal_timing.rs b/pallets/subtensor/src/tests/weights/commit_reveal_timing.rs new file mode 100644 index 0000000000..7cc07b37e8 --- /dev/null +++ b/pallets/subtensor/src/tests/weights/commit_reveal_timing.rs @@ -0,0 +1,798 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! Commit–reveal timing: expiry, exact epoch/block, tempo & reveal-period changes. + +use frame_support::{assert_err, assert_ok}; +use sp_core::{H256, U256}; +use sp_runtime::traits::{BlakeTwo256, Hash}; +use sp_std::collections::vec_deque::VecDeque; +use subtensor_runtime_common::NetUidStorageIndex; + +use crate::tests::mock::*; +use crate::*; + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_expired_commits_handling_in_commit_and_reveal --exact --show-output --nocapture +#[test] +fn test_expired_commits_handling_in_commit_and_reveal() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey: ::AccountId = U256::from(1); + let version_key: u64 = 0; + let uids: Vec = vec![0, 1]; + let weight_values: Vec = vec![10, 10]; + let tempo: u16 = 100; + + System::set_block_number(0); + add_network(netuid, tempo, 0); + + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + // Register neurons + register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); + register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); + SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); + add_balance_to_coldkey_account(&U256::from(0), 1.into()); + add_balance_to_coldkey_account(&U256::from(1), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(0)), + &(U256::from(0)), + netuid, + 1.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(1)), + &(U256::from(1)), + netuid, + 1.into(), + ); + + // 1. Commit 5 times in epoch 0 + let mut commit_info = Vec::new(); + for i in 0..5 { + let salt: Vec = vec![i; 8]; + let commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + )); + commit_info.push((commit_hash, salt)); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash + )); + } + + // Advance to epoch 1 + step_epochs(1, netuid); + + // 2. Commit another 5 times in epoch 1 + for i in 5..10 { + let salt: Vec = vec![i; 8]; + let commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + )); + commit_info.push((commit_hash, salt)); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash + )); + } + + // 3. Attempt to commit an 11th time, should fail with TooManyUnrevealedCommits + let salt_11: Vec = vec![11; 8]; + let commit_hash_11: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt_11.clone(), + version_key, + )); + assert_err!( + SubtensorModule::commit_weights(RuntimeOrigin::signed(hotkey), netuid, commit_hash_11), + Error::::TooManyUnrevealedCommits + ); + + // 4. Advance to epoch 2 to expire the commits from epoch 0 + step_epochs(1, netuid); // Now at epoch 2 + + // 5. Attempt to commit again; should succeed after expired commits are removed + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash_11 + )); + + // 6. Verify that the number of unrevealed, non-expired commits is now 6 + let commits: VecDeque<(H256, u64, u64, u64)> = + crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey) + .expect("Expected a commit"); + assert_eq!(commits.len(), 6); // 5 non-expired commits from epoch 1 + new commit + + // 7. Attempt to reveal an expired commit (from epoch 0) + // Previous commit removed expired commits + let (_, expired_salt) = &commit_info[0]; + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + expired_salt.clone(), + version_key, + ), + Error::::InvalidRevealCommitHashNotMatch + ); + + // 8. Reveal commits from epoch 1 at current_epoch = 2 + for (_, salt) in commit_info.iter().skip(5).take(5) { + let salt = salt.clone(); + + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + )); + } + + // 9. Advance to epoch 3 to reveal the new commit + step_epochs(1, netuid); + + // 10. Reveal the new commit from epoch 2 + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt_11.clone(), + version_key, + )); + + // 10. Verify that all commits have been revealed and the queue is empty + let commits = crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey); + assert!(commits.is_none()); + + // 11. Attempt to reveal again, should fail with NoWeightsCommitFound + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt_11.clone(), + version_key, + ), + Error::::NoWeightsCommitFound + ); + + // 12. Commit again to ensure we can continue after previous commits + let salt_12: Vec = vec![12; 8]; + let commit_hash_12: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt_12.clone(), + version_key, + )); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash_12 + )); + + // Advance to next epoch (epoch 4) and reveal + step_epochs(1, netuid); + + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids, + weight_values, + salt_12, + version_key, + )); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_at_exact_epoch --exact --show-output --nocapture +#[test] +fn test_reveal_at_exact_epoch() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey: ::AccountId = U256::from(1); + let version_key: u64 = 0; + let uids: Vec = vec![0, 1]; + let weight_values: Vec = vec![10, 10]; + let tempo: u16 = 100; + + System::set_block_number(0); + add_network(netuid, tempo, 0); + + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); + register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); + SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); + add_balance_to_coldkey_account(&U256::from(0), 1.into()); + add_balance_to_coldkey_account(&U256::from(1), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(0)), + &(U256::from(0)), + netuid, + 1.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(1)), + &(U256::from(1)), + netuid, + 1.into(), + ); + + let reveal_periods: Vec = vec![1, 2, 7, 40, 86, 100]; + + for &reveal_period in &reveal_periods { + assert_ok!(SubtensorModule::set_reveal_period(netuid, reveal_period)); + + let salt: Vec = vec![42; 8]; + let commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + )); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash + )); + + // Retrieve commit information + let commit_block = SubtensorModule::get_current_block_as_u64(); + let commit_epoch = SubtensorModule::get_epoch_index(netuid, commit_block); + let reveal_epoch = commit_epoch.saturating_add(reveal_period); + + // Attempt to reveal before the allowed epoch + if reveal_period > 0 { + // Advance to epoch before the reveal epoch + if reveal_period >= 1 { + step_epochs((reveal_period - 1) as u16, netuid); + } + + // Attempt to reveal too early + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + ), + Error::::RevealTooEarly + ); + } + + // Advance to the exact reveal epoch + let current_epoch = SubtensorModule::get_epoch_index( + netuid, + SubtensorModule::get_current_block_as_u64(), + ); + if current_epoch < reveal_epoch { + step_epochs((reveal_epoch - current_epoch) as u16, netuid); + } + + // Reveal at the exact allowed epoch + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + )); + + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + ), + Error::::NoWeightsCommitFound + ); + + let new_salt: Vec = vec![43; 8]; + let new_commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + new_salt.clone(), + version_key, + )); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + new_commit_hash + )); + + // Advance past the reveal epoch to ensure commit expiration + step_epochs((reveal_period + 1) as u16, netuid); + + // Attempt to reveal after the allowed epoch + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + new_salt.clone(), + version_key, + ), + Error::::ExpiredWeightCommit + ); + + crate::WeightCommits::::remove(NetUidStorageIndex::from(netuid), hotkey); + } + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_tempo_and_reveal_period_change_during_commit_reveal_process --exact --show-output --nocapture +#[test] +fn test_tempo_and_reveal_period_change_during_commit_reveal_process() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let uids: Vec = vec![0, 1]; + let weight_values: Vec = vec![10, 10]; + let salt: Vec = vec![42; 8]; + let version_key: u64 = 0; + let hotkey: ::AccountId = U256::from(1); + + // Compute initial commit hash + let commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + )); + + System::set_block_number(0); + + let initial_tempo: u16 = 100; + let initial_reveal_period: u64 = 1; + add_network(netuid, initial_tempo, 0); + assert_ok!(SubtensorModule::set_reveal_period(netuid, initial_reveal_period)); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); + register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); + SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); + add_balance_to_coldkey_account(&U256::from(0), 1.into()); + add_balance_to_coldkey_account(&U256::from(1), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(0)), + &(U256::from(0)), + netuid, + 1.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(1)), + &(U256::from(1)), + netuid, + 1.into(), + ); + + // Step 1: Commit weights + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash + )); + log::info!( + "Commit successful at block {}", + SubtensorModule::get_current_block_as_u64() + ); + + // Retrieve commit block and epoch + let commit_block = SubtensorModule::get_current_block_as_u64(); + let commit_epoch = SubtensorModule::get_epoch_index(netuid, commit_block); + + // Step 2: Change tempo and reveal period after commit + let new_tempo: u16 = 50; + let new_reveal_period: u64 = 2; + SubtensorModule::set_tempo_unchecked(netuid, new_tempo); + assert_ok!(SubtensorModule::set_reveal_period(netuid, new_reveal_period)); + log::info!( + "Changed tempo to {new_tempo} and reveal period to {new_reveal_period}" + ); + + // Step 3: Advance blocks to reach the reveal epoch according to new tempo and reveal period + let current_block = SubtensorModule::get_current_block_as_u64(); + let current_epoch = SubtensorModule::get_epoch_index(netuid, current_block); + let reveal_epoch = commit_epoch.saturating_add(new_reveal_period); + + // Advance to one epoch before reveal epoch + if current_epoch < reveal_epoch { + let epochs_to_advance = reveal_epoch - current_epoch - 1; + step_epochs(epochs_to_advance as u16, netuid); + } + + // Attempt to reveal too early + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key + ), + Error::::RevealTooEarly + ); + log::info!( + "Attempted to reveal too early at block {}", + SubtensorModule::get_current_block_as_u64() + ); + + // Advance to reveal epoch + step_epochs(1, netuid); + + // Attempt to reveal at the correct epoch + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key + )); + log::info!( + "Revealed weights at block {}", + SubtensorModule::get_current_block_as_u64() + ); + + // Step 4: Change tempo and reveal period again after reveal + let new_tempo_after_reveal: u16 = 200; + let new_reveal_period_after_reveal: u64 = 1; + SubtensorModule::set_tempo_unchecked(netuid, new_tempo_after_reveal); + assert_ok!(SubtensorModule::set_reveal_period( + netuid, + new_reveal_period_after_reveal + )); + log::info!("Changed tempo to {new_tempo_after_reveal} and reveal period to {new_reveal_period_after_reveal} after reveal"); + + // Step 5: Commit again + let new_salt: Vec = vec![43; 8]; + let new_commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + new_salt.clone(), + version_key, + )); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + new_commit_hash + )); + log::info!( + "Commit successful at block {}", + SubtensorModule::get_current_block_as_u64() + ); + + // Retrieve new commit block and epoch + let new_commit_block = SubtensorModule::get_current_block_as_u64(); + let new_commit_epoch = SubtensorModule::get_epoch_index(netuid, new_commit_block); + let new_reveal_epoch = new_commit_epoch.saturating_add(new_reveal_period_after_reveal); + + // Advance to reveal epoch + let current_block = SubtensorModule::get_current_block_as_u64(); + let current_epoch = SubtensorModule::get_epoch_index(netuid, current_block); + if current_epoch < new_reveal_epoch { + let epochs_to_advance = new_reveal_epoch - current_epoch; + step_epochs(epochs_to_advance as u16, netuid); + } + + // Attempt to reveal at the correct epoch + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + new_salt.clone(), + version_key + )); + log::info!( + "Revealed weights at block {}", + SubtensorModule::get_current_block_as_u64() + ); + + // Step 6: Attempt to reveal after the allowed epoch (commit expires) + // Advance past the reveal epoch + let expiration_epochs = 1; + step_epochs(expiration_epochs as u16, netuid); + + // Attempt to reveal again (should fail due to expired commit) + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + new_salt.clone(), + version_key + ), + Error::::NoWeightsCommitFound + ); + log::info!( + "Attempted to reveal after expiration at block {}", + SubtensorModule::get_current_block_as_u64() + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_commit_reveal_order_enforcement --exact --show-output --nocapture +#[test] +fn test_commit_reveal_order_enforcement() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey: ::AccountId = U256::from(1); + let version_key: u64 = 0; + let uids: Vec = vec![0, 1]; + let weight_values: Vec = vec![10, 10]; + let tempo: u16 = 100; + + System::set_block_number(0); + add_network(netuid, tempo, 0); + + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); + register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); + SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); + add_balance_to_coldkey_account(&U256::from(0), 1.into()); + add_balance_to_coldkey_account(&U256::from(1), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(0)), + &(U256::from(0)), + netuid, + 1.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(1)), + &(U256::from(1)), + netuid, + 1.into(), + ); + + // Commit three times: A, B, C + let mut commit_info = Vec::new(); + for i in 0..3 { + let salt: Vec = vec![i; 8]; + let commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + )); + commit_info.push((commit_hash, salt)); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash + )); + } + + step_epochs(1, netuid); + + // Attempt to reveal B first (index 1), should now succeed + let (_commit_hash_b, salt_b) = &commit_info[1]; + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt_b.clone(), + version_key, + )); + + // Check that commits A and B are removed + let remaining_commits = + crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey) + .expect("expected 1 remaining commit"); + assert_eq!(remaining_commits.len(), 1); // Only commit C should remain + + // Attempt to reveal C (index 2), should succeed + let (_commit_hash_c, salt_c) = &commit_info[2]; + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt_c.clone(), + version_key, + )); + + // Attempting to reveal A (index 0) should fail as it's been removed + let (_commit_hash_a, salt_a) = &commit_info[0]; + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids, + weight_values, + salt_a.clone(), + version_key, + ), + Error::::NoWeightsCommitFound + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_at_exact_block --exact --show-output --nocapture +#[test] +fn test_reveal_at_exact_block() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey: ::AccountId = U256::from(1); + let version_key: u64 = 0; + let uids: Vec = vec![0, 1]; + let weight_values: Vec = vec![10, 10]; + let tempo: u16 = 360; + + System::set_block_number(0); + add_network_disable_commit_reveal(netuid, tempo, 0); + + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); + register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); + SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); + SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); + + let reveal_periods: Vec = vec![1, 2, 5, 19, 21, 30, 77]; + + for &reveal_period in &reveal_periods { + assert_ok!(SubtensorModule::set_reveal_period(netuid, reveal_period)); + + // Step 1: Commit weights + let salt: Vec = vec![42 + (reveal_period % 100) as u16; 8]; + let commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key, + )); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_hash + )); + + // Epoch the commit was tagged with (counter is the canonical index). + let commit_epoch = + crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey) + .and_then(|q| q.back().map(|(_, e, _, _)| *e)) + .expect("commit stored"); + + // Attempt to reveal before the reveal epoch — too early. + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key + ), + Error::::RevealTooEarly + ); + + // Advance the epoch counter into the reveal epoch; pin the scheduler. + SubnetEpochIndex::::insert(netuid, commit_epoch + reveal_period); + LastEpochBlock::::insert(netuid, SubtensorModule::get_current_block_as_u64()); + PendingEpochAt::::insert(netuid, 0); + + // Reveal at the exact allowed epoch + assert_ok!(SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key + )); + + // Attempt to reveal again; should fail with NoWeightsCommitFound + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key + ), + Error::::NoWeightsCommitFound + ); + + // Commit again with new salt + let new_salt: Vec = vec![43 + (reveal_period % 100) as u16; 8]; + let new_commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weight_values.clone(), + new_salt.clone(), + version_key, + )); + assert_ok!(SubtensorModule::commit_weights( + RuntimeOrigin::signed(hotkey), + netuid, + new_commit_hash + )); + + // Advance the epoch counter past the reveal epoch — commit expired. + let new_commit_epoch = + crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey) + .and_then(|q| q.back().map(|(_, e, _, _)| *e)) + .expect("commit stored"); + SubnetEpochIndex::::insert(netuid, new_commit_epoch + reveal_period + 1); + LastEpochBlock::::insert(netuid, SubtensorModule::get_current_block_as_u64()); + + // Attempt to reveal after the commit has expired + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + new_salt.clone(), + version_key + ), + Error::::ExpiredWeightCommit + ); + + // Clean up for next iteration + crate::WeightCommits::::remove(NetUidStorageIndex::from(netuid), hotkey); + } + }); +} diff --git a/pallets/subtensor/src/tests/weights/helpers.rs b/pallets/subtensor/src/tests/weights/helpers.rs new file mode 100644 index 0000000000..910e0e3dd8 --- /dev/null +++ b/pallets/subtensor/src/tests/weights/helpers.rs @@ -0,0 +1,45 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! Shared helpers for weights extrinsic tests. + +use frame_support::dispatch::DispatchResult; +use sp_core::H256; +use sp_core::U256; +use sp_runtime::traits::{BlakeTwo256, Hash}; + +use crate::tests::mock::*; +use crate::*; + +pub(super) fn commit_reveal_set_weights( + hotkey: U256, + netuid: NetUid, + uids: Vec, + weights: Vec, + salt: Vec, + version_key: u64, +) -> DispatchResult { + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + + let commit_hash: H256 = BlakeTwo256::hash_of(&( + hotkey, + netuid, + uids.clone(), + weights.clone(), + salt.clone(), + version_key, + )); + + SubtensorModule::commit_weights(RuntimeOrigin::signed(hotkey), netuid, commit_hash)?; + + step_epochs(1, netuid); + + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids, + weights, + salt, + version_key, + )?; + + Ok(()) +} diff --git a/pallets/subtensor/src/tests/weights/mod.rs b/pallets/subtensor/src/tests/weights/mod.rs new file mode 100644 index 0000000000..4b77afb9aa --- /dev/null +++ b/pallets/subtensor/src/tests/weights/mod.rs @@ -0,0 +1,28 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! Tests for subnet weight extrinsics and helpers in [`crate::subnets::weights`]. +//! +//! ## Search anchors +//! +//! | Module | Owns | +//! |--------|------| +//! | [`helpers`] | `commit_reveal_set_weights` fixture | +//! | [`set_weights`] | `set_weights` dispatch, stake/permit/version guards | +//! | [`weight_checks`] | `check_length`, normalize, max-weight, self-weight, epoch block helper | +//! | [`commit_reveal`] | hash commit–reveal happy path & toggles | +//! | [`commit_reveal_timing`] | expiry, exact epoch/block, tempo changes | +//! | [`batch_reveal`] | `batch_reveal_weights` + batch event netuid fields | +//! | [`timelocked_commit`] | CRv3 / timelocked commits + tlock smoke | +//! | [`timelocked_reveal`] | CRv3 reveal failure modes and multi-commit processing | +//! | [`timelocked_reveal_hotkey`] | CRv3 hotkey check, missing-pulse retry, legacy payload | +//! | [`owner_permit`] | subnet-owner validate without stake/permit | + +mod batch_reveal; +mod commit_reveal; +mod commit_reveal_timing; +mod helpers; +mod owner_permit; +mod set_weights; +mod timelocked_commit; +mod timelocked_reveal; +mod timelocked_reveal_hotkey; +mod weight_checks; diff --git a/pallets/subtensor/src/tests/weights/owner_permit.rs b/pallets/subtensor/src/tests/weights/owner_permit.rs new file mode 100644 index 0000000000..2b2c42b4a8 --- /dev/null +++ b/pallets/subtensor/src/tests/weights/owner_permit.rs @@ -0,0 +1,123 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! Subnet owner may validate/set weights without stake or manual permit. + +use frame_support::assert_ok; +use sp_core::U256; + +use crate::tests::mock::*; +use crate::*; + +#[test] +fn test_subnet_owner_can_validate_without_stake_or_manual_permit() { + new_test_ext(0).execute_with(|| { + let owner_hotkey = U256::from(10); + let owner_coldkey = U256::from(11); + let other_hotkey = U256::from(20); + let other_coldkey = U256::from(21); + + // Create a real dynamic subnet whose owner hotkey is `owner_hotkey`. + let netuid = add_dynamic_network_disable_commit_reveal(&owner_hotkey, &owner_coldkey); + remove_owner_registration_stake(netuid); + + // Add one non-owner neuron with deterministic subnet stake. + register_ok_neuron(netuid, other_hotkey, other_coldkey, 0); + add_balance_to_coldkey_account(&other_coldkey, 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &other_hotkey, + &other_coldkey, + netuid, + 1.into(), + ); + + let owner_uid = + SubtensorModule::get_owner_uid(netuid).expect("subnet owner should resolve to a uid"); + let registered_owner_uid = + SubtensorModule::get_uid_for_net_and_hotkey(netuid, &owner_hotkey) + .expect("owner hotkey should be registered on the subnet"); + assert_eq!(registered_owner_uid, owner_uid); + + let other_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &other_hotkey) + .expect("other hotkey should be registered on the subnet"); + + let (owner_weight_stake, _, _) = + SubtensorModule::get_stake_weights_for_hotkey_on_subnet(&owner_hotkey, netuid); + let (other_weight_stake, _, _) = + SubtensorModule::get_stake_weights_for_hotkey_on_subnet(&other_hotkey, netuid); + assert!(owner_weight_stake < other_weight_stake); + + // Make the non-owner stake-qualified while the owner remains below threshold. + SubtensorModule::set_stake_threshold(1_u64); + assert!(SubtensorModule::check_weights_min_stake( + &other_hotkey, + netuid + )); + + // Clear all explicit permits. The owner should not rely on manual permit state. + SubtensorModule::set_validator_permit_for_uid(netuid, owner_uid, false); + SubtensorModule::set_validator_permit_for_uid(netuid, other_uid, false); + assert!(!SubtensorModule::get_validator_permit_for_uid( + netuid, owner_uid + )); + assert!(!SubtensorModule::get_validator_permit_for_uid( + netuid, other_uid + )); + + // Sanity check: a non-owner without a permit still cannot set non-self weights. + assert!(!SubtensorModule::check_validator_permit( + netuid, + other_uid, + &[owner_uid], + &[1u16], + )); + assert_eq!( + SubtensorModule::set_weights( + RuntimeOrigin::signed(other_hotkey), + netuid, + vec![owner_uid], + vec![1u16], + 0, + ), + Err(Error::::NeuronNoValidatorPermit.into()) + ); + + // The subnet owner bypasses both the stake gate and the validator-permit gate. + assert!(SubtensorModule::check_weights_min_stake( + &owner_hotkey, + netuid + )); + assert!(SubtensorModule::check_validator_permit( + netuid, + owner_uid, + &[other_uid], + &[1u16], + )); + + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(owner_hotkey), + netuid, + vec![other_uid], + vec![1u16], + 0, + )); + + // After an epoch, the owner is still validator-eligible even though only the + step_epochs(1, netuid); + assert!(SubtensorModule::get_validator_permit_for_uid( + netuid, owner_uid + )); + + // The original top-k result is preserved; the owner is added on top. + assert!(SubtensorModule::get_validator_permit_for_uid( + netuid, other_uid + )); + }); +} + +// Regression: when a batch of weight commits has per-item failures, each +// emitted BatchWeightItemFailed event must carry the netuid of the failing +// item so downstream consumers (indexers, validator monitors) can correlate +// failure → subnet without re-deriving from iteration order. +// +// Both netuids in this test fail (commit-reveal disabled on both) — what we +// assert is the *positional propagation*: the per-item events carry the +// distinct netuids that produced them, in iteration order. diff --git a/pallets/subtensor/src/tests/weights/set_weights.rs b/pallets/subtensor/src/tests/weights/set_weights.rs new file mode 100644 index 0000000000..f0295fdbc8 --- /dev/null +++ b/pallets/subtensor/src/tests/weights/set_weights.rs @@ -0,0 +1,690 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! Tests for `set_weights` / dispatch info / stake & permit guards. + +use frame_support::{ + assert_err, assert_ok, + dispatch::{DispatchClass, GetDispatchInfo, Pays}, +}; +use sp_core::{H256, U256}; +use sp_runtime::{ + DispatchError, + traits::{BlakeTwo256, Hash}, +}; +use substrate_fixed::types::I32F32; + +use super::helpers::commit_reveal_set_weights; +use crate::tests::mock::*; +use crate::*; + +/*************************** + pub fn set_weights() tests +*****************************/ + +// Test the call passes through the subtensor module. +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_set_weights_dispatch_info_ok --exact --show-output --nocapture +#[test] +fn test_set_weights_dispatch_info_ok() { + new_test_ext(0).execute_with(|| { + let dests = vec![1, 1]; + let weights = vec![1, 1]; + let netuid = NetUid::from(1); + let version_key: u64 = 0; + let call = RuntimeCall::SubtensorModule(SubtensorCall::set_weights { + netuid, + dests, + weights, + version_key, + }); + let dispatch_info = call.get_dispatch_info(); + + assert_eq!(dispatch_info.class, DispatchClass::Normal); + assert_eq!(dispatch_info.pays_fee, Pays::No); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_commit_weights_dispatch_info_ok --exact --show-output --nocapture +#[test] +fn test_commit_weights_dispatch_info_ok() { + new_test_ext(0).execute_with(|| { + let dests = vec![1, 1]; + let weights = vec![1, 1]; + let netuid = NetUid::from(1); + let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; + let version_key: u64 = 0; + let hotkey: U256 = U256::from(1); + + let commit_hash: H256 = + BlakeTwo256::hash_of(&(hotkey, netuid, dests, weights, salt, version_key)); + + let call = RuntimeCall::SubtensorModule(SubtensorCall::commit_weights { + netuid, + commit_hash, + }); + let dispatch_info = call.get_dispatch_info(); + + assert_eq!(dispatch_info.class, DispatchClass::Normal); + assert_eq!(dispatch_info.pays_fee, Pays::No); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_weights_dispatch_info_ok --exact --show-output --nocapture +#[test] +fn test_reveal_weights_dispatch_info_ok() { + new_test_ext(0).execute_with(|| { + let dests = vec![1, 1]; + let weights = vec![1, 1]; + let netuid = NetUid::from(1); + let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; + let version_key: u64 = 0; + + let call = RuntimeCall::SubtensorModule(SubtensorCall::reveal_weights { + netuid, + uids: dests, + values: weights, + salt, + version_key, + }); + let dispatch_info = call.get_dispatch_info(); + + assert_eq!(dispatch_info.class, DispatchClass::Normal); + assert_eq!(dispatch_info.pays_fee, Pays::No); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_set_weights_is_root_error --exact --show-output --nocapture +#[test] +fn test_set_weights_is_root_error() { + new_test_ext(0).execute_with(|| { + let uids = vec![0]; + let weights = vec![1]; + let version_key: u64 = 0; + let hotkey = U256::from(1); + SubtensorModule::set_commit_reveal_weights_enabled(NetUid::ROOT, false); + + assert_err!( + SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + NetUid::ROOT, + uids.clone(), + weights.clone(), + version_key, + ), + Error::::CanNotSetRootNetworkWeights + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_weights_err_no_validator_permit --exact --show-output --nocapture +// Test ensures that uid has validator permit to set non-self weights. +#[test] +fn test_weights_err_no_validator_permit() { + new_test_ext(0).execute_with(|| { + let hotkey_account_id = U256::from(55); + let netuid = NetUid::from(1); + let tempo: u16 = 13; + add_network_disable_commit_reveal(netuid, tempo, 0); + SubtensorModule::set_min_allowed_weights(netuid, 0); + SubtensorModule::set_max_allowed_uids(netuid, 3); + register_ok_neuron(netuid, hotkey_account_id, U256::from(66), 0); + register_ok_neuron(netuid, U256::from(1), U256::from(1), 65555); + register_ok_neuron(netuid, U256::from(2), U256::from(2), 75555); + + let weights_keys: Vec = vec![1, 2]; + let weight_values: Vec = vec![1, 2]; + + let result = SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey_account_id), + netuid, + weights_keys, + weight_values, + 0, + ); + assert_eq!(result, Err(Error::::NeuronNoValidatorPermit.into())); + + let weights_keys: Vec = vec![1, 2]; + let weight_values: Vec = vec![1, 2]; + let neuron_uid: u16 = + SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey_account_id) + .expect("Not registered."); + SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid, true); + let result = SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey_account_id), + netuid, + weights_keys, + weight_values, + 0, + ); + assert_ok!(result); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_set_stake_threshold_failed --exact --show-output --nocapture +#[test] +fn test_set_stake_threshold_failed() { + new_test_ext(0).execute_with(|| { + let dests = vec![0]; + let weights = vec![1]; + let netuid = NetUid::from(1); + let version_key: u64 = 0; + let hotkey = U256::from(0); + let coldkey = U256::from(0); + + add_network_disable_commit_reveal(netuid, 1, 0); + register_ok_neuron(netuid, hotkey, coldkey, 2143124); + SubtensorModule::set_stake_threshold(20_000_000_000_000); + add_balance_to_coldkey_account(&hotkey, 20_000_000_000_000_000_u64.into()); + + // Check the signed extension function. + assert_eq!(SubtensorModule::get_stake_threshold(), 20_000_000_000_000); + assert!(!SubtensorModule::check_weights_min_stake(&hotkey, netuid)); + assert_ok!(SubtensorModule::do_add_stake( + RuntimeOrigin::signed(hotkey), + hotkey, + netuid, + 19_000_000_000_000_u64.into() + )); + assert!(!SubtensorModule::check_weights_min_stake(&hotkey, netuid)); + assert_ok!(SubtensorModule::do_add_stake( + RuntimeOrigin::signed(hotkey), + hotkey, + netuid, + 20_000_000_000_000_u64.into() + )); + assert!(SubtensorModule::check_weights_min_stake(&hotkey, netuid)); + + // Check that it fails at the pallet level. + SubtensorModule::set_stake_threshold(100_000_000_000_000); + assert_eq!( + SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid, + dests.clone(), + weights.clone(), + version_key, + ), + Err(Error::::NotEnoughStakeToSetWeights.into()) + ); + // Now passes + assert_ok!(SubtensorModule::do_add_stake( + RuntimeOrigin::signed(hotkey), + hotkey, + netuid, + 100_000_000_000_000_u64.into() + )); + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid, + dests.clone(), + weights.clone(), + version_key + )); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_weights_version_key --exact --show-output --nocapture +// Test ensures that a uid can only set weights if it has the valid weights set version key. +#[test] +fn test_weights_version_key() { + new_test_ext(0).execute_with(|| { + let hotkey = U256::from(55); + let coldkey = U256::from(66); + let netuid0 = NetUid::from(1); + let netuid1 = NetUid::from(2); + + add_network_disable_commit_reveal(netuid0, 1, 0); + add_network_disable_commit_reveal(netuid1, 1, 0); + register_ok_neuron(netuid0, hotkey, coldkey, 2143124); + register_ok_neuron(netuid1, hotkey, coldkey, 3124124); + + let weights_keys: Vec = vec![0]; + let weight_values: Vec = vec![1]; + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid0, + weights_keys.clone(), + weight_values.clone(), + 0 + )); + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid1, + weights_keys.clone(), + weight_values.clone(), + 0 + )); + + // Set version keys. + let key0: u64 = 12312; + let key1: u64 = 20313; + SubtensorModule::set_weights_version_key(netuid0, key0); + SubtensorModule::set_weights_version_key(netuid1, key1); + + // Setting works with version key. + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid0, + weights_keys.clone(), + weight_values.clone(), + key0 + )); + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid1, + weights_keys.clone(), + weight_values.clone(), + key1 + )); + + // validator:20313 >= network:12312 (accepted: validator newer) + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid0, + weights_keys.clone(), + weight_values.clone(), + key1 + )); + + // Setting fails with incorrect keys. + // validator:12312 < network:20313 (rejected: validator not updated) + assert_eq!( + SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey), + netuid1, + weights_keys.clone(), + weight_values.clone(), + key0 + ), + Err(Error::::IncorrectWeightVersionKey.into()) + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_weights_err_setting_weights_too_fast --exact --show-output --nocapture +// Test ensures that uid has validator permit to set non-self weights. +#[test] +fn test_weights_err_setting_weights_too_fast() { + new_test_ext(0).execute_with(|| { + let hotkey_account_id = U256::from(55); + let netuid = NetUid::from(1); + let tempo: u16 = 13; + add_network_disable_commit_reveal(netuid, tempo, 0); + SubtensorModule::set_min_allowed_weights(netuid, 0); + SubtensorModule::set_max_allowed_uids(netuid, 3); + register_ok_neuron(netuid, hotkey_account_id, U256::from(66), 0); + register_ok_neuron(netuid, U256::from(1), U256::from(1), 65555); + register_ok_neuron(netuid, U256::from(2), U256::from(2), 75555); + + let neuron_uid: u16 = + SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey_account_id) + .expect("Not registered."); + SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid, true); + add_balance_to_coldkey_account(&U256::from(66), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &(U256::from(66)), + netuid, + 1.into(), + ); + SubtensorModule::set_weights_set_rate_limit(netuid, 10); + assert_eq!(SubtensorModule::get_weights_set_rate_limit(netuid), 10); + + let weights_keys: Vec = vec![1, 2]; + let weight_values: Vec = vec![1, 2]; + + // Note that LastUpdate has default 0 for new uids, but if they have actually set weights on block 0 + // then they are allowed to set weights again once more without a wait restriction, to accommodate the default. + let result = SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey_account_id), + netuid, + weights_keys.clone(), + weight_values.clone(), + 0, + ); + assert_ok!(result); + run_to_block(1); + + for i in 1..100 { + let result = SubtensorModule::set_weights( + RuntimeOrigin::signed(hotkey_account_id), + netuid, + weights_keys.clone(), + weight_values.clone(), + 0, + ); + if i % 10 == 1 { + assert_ok!(result); + } else { + assert_eq!(result, Err(Error::::SettingWeightsTooFast.into())); + } + run_to_block(i + 1); + } + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_weights_err_weights_vec_not_equal_size --exact --show-output --nocapture +// Test ensures that uids -- weights must have the same size. +#[test] +fn test_weights_err_weights_vec_not_equal_size() { + new_test_ext(0).execute_with(|| { + let hotkey_account_id = U256::from(55); + let netuid = NetUid::from(1); + let tempo: u16 = 13; + let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; + add_network(netuid, tempo, 0); + register_ok_neuron(netuid, hotkey_account_id, U256::from(66), 0); + let neuron_uid: u16 = + SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey_account_id) + .expect("Not registered."); + SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid, true); + let weights_keys: Vec = vec![1, 2, 3, 4, 5, 6]; + let weight_values: Vec = vec![1, 2, 3, 4, 5]; // Uneven sizes + let result = commit_reveal_set_weights( + hotkey_account_id, + 1.into(), + weights_keys.clone(), + weight_values.clone(), + salt.clone(), + 0, + ); + assert_eq!(result, Err(Error::::WeightVecNotEqualSize.into())); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_weights_err_has_duplicate_ids --exact --show-output --nocapture +// Test ensures that uids can have not duplicates +#[test] +fn test_weights_err_has_duplicate_ids() { + new_test_ext(0).execute_with(|| { + let hotkey_account_id = U256::from(666); + let netuid = NetUid::from(1); + let tempo: u16 = 13; + let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; + add_network(netuid, tempo, 0); + + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_max_allowed_uids(netuid, 100); // Allow many registrations per block. + SubtensorModule::set_max_registrations_per_block(netuid, 100); // Allow many registrations per block. + SubtensorModule::set_target_registrations_per_interval(netuid, 100); // Allow many registrations per block. + // uid 0 + register_ok_neuron(netuid, hotkey_account_id, U256::from(77), 0); + let neuron_uid: u16 = + SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey_account_id) + .expect("Not registered."); + SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid, true); + add_balance_to_coldkey_account(&U256::from(77), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &(U256::from(77)), + netuid, + 1.into(), + ); + + // uid 1 + register_ok_neuron(netuid, U256::from(1), U256::from(1), 100_000); + SubtensorModule::get_uid_for_net_and_hotkey(netuid, &U256::from(1)) + .expect("Not registered."); + + // uid 2 + register_ok_neuron(netuid, U256::from(2), U256::from(1), 200_000); + SubtensorModule::get_uid_for_net_and_hotkey(netuid, &U256::from(2)) + .expect("Not registered."); + + // uid 3 + register_ok_neuron(netuid, U256::from(3), U256::from(1), 300_000); + SubtensorModule::get_uid_for_net_and_hotkey(netuid, &U256::from(3)) + .expect("Not registered."); + + assert_eq!(SubtensorModule::get_subnetwork_n(netuid), 4); + + let weights_keys: Vec = vec![1, 1, 1]; // Contains duplicates + let weight_values: Vec = vec![1, 2, 3]; + let result = commit_reveal_set_weights( + hotkey_account_id, + netuid, + weights_keys.clone(), + weight_values.clone(), + salt.clone(), + 0, + ); + assert_eq!(result, Err(Error::::DuplicateUids.into())); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_weights_err_max_weight_limit --exact --show-output --nocapture +// Test ensures weights cannot exceed max weight limit. +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_no_signature --exact --show-output --nocapture +// Tests the call requires a valid origin. +#[test] +fn test_no_signature() { + new_test_ext(0).execute_with(|| { + let uids: Vec = vec![]; + let values: Vec = vec![]; + SubtensorModule::set_commit_reveal_weights_enabled(1.into(), false); + let result = SubtensorModule::set_weights(RuntimeOrigin::none(), 1.into(), uids, values, 0); + assert_eq!(result, Err(DispatchError::BadOrigin)); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_set_weights_err_not_active --exact --show-output --nocapture +// Tests that weights cannot be set BY non-registered hotkeys. +#[test] +fn test_set_weights_err_not_active() { + new_test_ext(0).execute_with(|| { + let netuid = NetUid::from(1); + let tempo: u16 = 13; + let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; + add_network(netuid, tempo, 0); + + // Register one neuron. Should have uid 0 + register_ok_neuron(netuid, U256::from(666), U256::from(2), 100000); + SubtensorModule::get_uid_for_net_and_hotkey(netuid, &U256::from(666)) + .expect("Not registered."); + + let weights_keys: Vec = vec![0]; // Uid 0 is valid. + let weight_values: Vec = vec![1]; + // This hotkey is NOT registered. + let result = commit_reveal_set_weights( + U256::from(1), + 1.into(), + weights_keys, + weight_values, + salt, + 0, + ); + assert_eq!( + result, + Err(Error::::HotKeyNotRegisteredInSubNet.into()) + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_set_weights_err_invalid_uid --exact --show-output --nocapture +// Tests that set weights fails if you pass invalid uids. +#[test] +fn test_set_weights_err_invalid_uid() { + new_test_ext(0).execute_with(|| { + let hotkey_account_id = U256::from(55); + let netuid = NetUid::from(1); + let tempo: u16 = 13; + let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; + add_network(netuid, tempo, 0); + register_ok_neuron(netuid, hotkey_account_id, U256::from(66), 0); + let neuron_uid: u16 = + SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey_account_id) + .expect("Not registered."); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid, true); + add_balance_to_coldkey_account(&U256::from(66), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &(U256::from(66)), + netuid, + 1.into(), + ); + let weight_keys: Vec = vec![9999]; // Does not exist + let weight_values: Vec = vec![88]; // random value + let result = commit_reveal_set_weights( + hotkey_account_id, + netuid, + weight_keys, + weight_values, + salt, + 0, + ); + assert_eq!(result, Err(Error::::UidVecContainInvalidOne.into())); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_set_weight_not_enough_values --exact --show-output --nocapture +// Tests that set weights fails if you don't pass enough values. +#[test] +fn test_set_weight_not_enough_values() { + new_test_ext(0).execute_with(|| { + let netuid = NetUid::from(1); + let tempo: u16 = 13; + let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; + let account_id = U256::from(1); + add_network_disable_commit_reveal(netuid, tempo, 0); + + register_ok_neuron(netuid, account_id, U256::from(2), 100000); + let neuron_uid: u16 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &U256::from(1)) + .expect("Not registered."); + SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid, true); + add_balance_to_coldkey_account(&U256::from(2), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &account_id, + &(U256::from(2)), + netuid, + 1.into(), + ); + + register_ok_neuron(netuid, U256::from(3), U256::from(4), 300000); + SubtensorModule::set_min_allowed_weights(netuid, 2); + + // Should fail because we are only setting a single value and its not the self weight. + let weight_keys: Vec = vec![1]; // not weight. + let weight_values: Vec = vec![88]; // random value. + let result = SubtensorModule::set_weights( + RuntimeOrigin::signed(account_id), + 1.into(), + weight_keys, + weight_values, + 0, + ); + assert_eq!(result, Err(Error::::WeightVecLengthIsLow.into())); + + // Shouldnt fail because we setting a single value but it is the self weight. + let weight_keys: Vec = vec![0]; // self weight. + let weight_values: Vec = vec![88]; // random value. + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(account_id), + 1.into(), + weight_keys, + weight_values, + 0 + )); + + // Should pass because we are setting enough values. + let weight_keys: Vec = vec![0, 1]; // self weight. + let weight_values: Vec = vec![10, 10]; // random value. + SubtensorModule::set_min_allowed_weights(netuid, 1); + assert_ok!(commit_reveal_set_weights( + account_id, + 1.into(), + weight_keys, + weight_values, + salt, + 0 + )); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_set_weight_too_many_uids --exact --show-output --nocapture +// Tests that the weights set fails if you pass too many uids for the subnet +#[test] +fn test_set_weight_too_many_uids() { + new_test_ext(0).execute_with(|| { + let netuid = NetUid::from(1); + let tempo: u16 = 13; + add_network_disable_commit_reveal(netuid, tempo, 0); + + register_ok_neuron(1.into(), U256::from(1), U256::from(2), 100_000); + let neuron_uid: u16 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &U256::from(1)) + .expect("Not registered."); + SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid, true); + + register_ok_neuron(1.into(), U256::from(3), U256::from(4), 300_000); + SubtensorModule::set_min_allowed_weights(1.into(), 2); + // Should fail because we are setting more weights than there are neurons. + let weight_keys: Vec = vec![0, 1, 2, 3, 4]; // more uids than neurons in subnet. + let weight_values: Vec = vec![88, 102, 303, 1212, 11]; // random value. + let result = SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(1)), + 1.into(), + weight_keys, + weight_values, + 0, + ); + assert_eq!( + result, + Err(Error::::UidsLengthExceedUidsInSubNet.into()) + ); + + // Shouldnt fail because we are setting less weights than there are neurons. + let weight_keys: Vec = vec![0, 1]; // Only on neurons that exist. + let weight_values: Vec = vec![10, 10]; // random value. + assert_ok!(SubtensorModule::set_weights( + RuntimeOrigin::signed(U256::from(1)), + 1.into(), + weight_keys, + weight_values, + 0 + )); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_set_weights_sum_larger_than_u16_max --exact --show-output --nocapture +// Tests that the weights set doesn't panic if you pass weights that sum to larger than u16 max. +#[test] +fn test_set_weights_sum_larger_than_u16_max() { + new_test_ext(0).execute_with(|| { + let netuid = NetUid::from(1); + let tempo: u16 = 13; + let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; + add_network(netuid, tempo, 0); + + register_ok_neuron(1.into(), U256::from(1), U256::from(2), 100_000); + let neuron_uid: u16 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &U256::from(1)) + .expect("Not registered."); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid, true); + add_balance_to_coldkey_account(&U256::from(2), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &(U256::from(1)), + &(U256::from(2)), + netuid, + 1.into(), + ); + + register_ok_neuron(1.into(), U256::from(3), U256::from(4), 300_000); + SubtensorModule::set_min_allowed_weights(1.into(), 2); + + // Shouldn't fail because we are setting the right number of weights. + let weight_keys: Vec = vec![0, 1]; + let weight_values: Vec = vec![u16::MAX, u16::MAX]; + // sum of weights is larger than u16 max. + assert!(weight_values.iter().map(|x| *x as u64).sum::() > (u16::MAX as u64)); + + let result = + commit_reveal_set_weights(U256::from(1), 1.into(), weight_keys, weight_values, salt, 0); + assert_ok!(result); + + // Get max-upscaled unnormalized weights. + let all_weights: Vec> = SubtensorModule::get_weights(netuid.into()); + let weights_set: &[I32F32] = &all_weights[neuron_uid as usize]; + assert_eq!(weights_set[0], I32F32::from_num(u16::MAX)); + assert_eq!(weights_set[1], I32F32::from_num(u16::MAX)); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_do_commit_crv3_weights_disabled --exact --show-output --nocapture diff --git a/pallets/subtensor/src/tests/weights/timelocked_commit.rs b/pallets/subtensor/src/tests/weights/timelocked_commit.rs new file mode 100644 index 0000000000..38e7c39873 --- /dev/null +++ b/pallets/subtensor/src/tests/weights/timelocked_commit.rs @@ -0,0 +1,633 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! Timelocked (CRv3) weight commits and tlock encrypt/decrypt smoke test. + +use ark_serialize::CanonicalDeserialize; +use ark_serialize::CanonicalSerialize; +use frame_support::{assert_err, assert_ok}; +use pallet_drand::types::Pulse; +use rand_chacha::{ChaCha20Rng, rand_core::SeedableRng}; +use sha2::Digest; +use sp_core::Encode; +use sp_core::U256; +use substrate_fixed::types::I32F32; +use subtensor_runtime_common::NetUidStorageIndex; +use tle::{ + curves::drand::TinyBLS381, + ibe::fullident::Identity, + stream_ciphers::AESGCMStreamCipherProvider, + tlock::{tld, tle}, +}; +use w3f_bls::EngineBLS; + +use crate::coinbase::reveal_commits::WeightsTlockPayload; +use crate::tests::mock::*; +use crate::*; + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::tlock_encrypt_decrypt_drand_quicknet_works --exact --show-output --nocapture +#[test] +pub fn tlock_encrypt_decrypt_drand_quicknet_works() { + // using a pulse from drand's QuickNet + // https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/1000 + // the beacon public key + let pk_bytes = + b"83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a" + ; // a round number that we know a signature for + let round: u64 = 1000; + // the signature produced in that round + let signature = + b"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" + ; + + // Convert hex string to bytes + let pub_key_bytes = hex::decode(pk_bytes).expect("Failed to decode public key bytes"); + // Deserialize to G1Affine + let pub_key = + ::PublicKeyGroup::deserialize_compressed(&*pub_key_bytes) + .expect("Failed to deserialize public key"); + + // then we tlock a message for the pubkey + let plaintext = b"this is a test".as_slice(); + let esk = [2; 32]; + + let sig_bytes = hex::decode(signature).expect("Failed to decode signature bytes"); + let sig = ::SignatureGroup::deserialize_compressed(&*sig_bytes) + .expect("Failed to deserialize signature"); + + let message = { + let mut hasher = sha2::Sha256::new(); + hasher.update(round.to_be_bytes()); + hasher.finalize().to_vec() + }; + + let identity = Identity::new(b"", vec![message]); + + let rng = ChaCha20Rng::seed_from_u64(0); + let ct = tle::( + pub_key, esk, plaintext, identity, rng, + ) + .expect("Encryption failed"); + + // then we can decrypt the ciphertext using the signature + let result = tld::(ct, sig).expect("Decryption failed"); + assert!(result == plaintext); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_success --exact --show-output --nocapture + +#[test] +fn test_reveal_crv3_commits_success() { + new_test_ext(100).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey1: AccountId = U256::from(1); + let hotkey2: AccountId = U256::from(2); + let reveal_round: u64 = 1000; + + add_network(netuid, 5, 0); + register_ok_neuron(netuid, hotkey1, U256::from(3), 100_000); + register_ok_neuron(netuid, hotkey2, U256::from(4), 100_000); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); + + let neuron_uid1 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey1) + .expect("Failed to get neuron UID for hotkey1"); + let neuron_uid2 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey2) + .expect("Failed to get neuron UID for hotkey2"); + + SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid1, true); + SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid2, true); + add_balance_to_coldkey_account(&U256::from(3), 1.into()); + add_balance_to_coldkey_account(&U256::from(4), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &(U256::from(3)), + netuid, + 1.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey2, + &(U256::from(4)), + netuid, + 1.into(), + ); + + let version_key = SubtensorModule::get_weights_version_key(netuid); + + let payload = WeightsTlockPayload { + hotkey: hotkey1.encode(), + values: vec![10, 20], + uids: vec![neuron_uid1, neuron_uid2], + version_key, + }; + + let serialized_payload = payload.encode(); + + let esk = [2; 32]; + let rng = ChaCha20Rng::seed_from_u64(0); + + let pk_bytes = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a") + .expect("Failed to decode public key bytes"); + let pub_key = ::PublicKeyGroup::deserialize_compressed(&*pk_bytes) + .expect("Failed to deserialize public key"); + + let message = { + let mut hasher = sha2::Sha256::new(); + hasher.update(reveal_round.to_be_bytes()); + hasher.finalize().to_vec() + }; + let identity = Identity::new(b"", vec![message]); + + let ct = tle::( + pub_key, + esk, + &serialized_payload, + identity, + rng, + ) + .expect("Encryption failed"); + + let mut commit_bytes = Vec::new(); + ct.serialize_compressed(&mut commit_bytes) + .expect("Failed to serialize commit"); + + assert!( + !commit_bytes.is_empty(), + "commit_bytes is empty after serialization" + ); + + log::debug!( + "Commit bytes now contain {commit_bytes:#?}" + ); + + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey1), + netuid, + commit_bytes.clone().try_into().expect("Failed to convert commit bytes into bounded vector"), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + + let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") + .expect("Failed to decode signature bytes"); + + pallet_drand::Pulses::::insert( + reveal_round, + Pulse { + round: reveal_round, + randomness: vec![0; 32].try_into().expect("Failed to convert randomness vector"), + signature: sig_bytes.try_into().expect("Failed to convert signature bytes"), + }, + ); + + // Step epochs to run the epoch via the blockstep + step_epochs(3, netuid); + + let weights_sparse = SubtensorModule::unnormalized_weights_sparse(netuid.into()); + let weights = weights_sparse.get(neuron_uid1 as usize).cloned().unwrap_or_default(); + + assert!( + !weights.is_empty(), + "Weights for neuron_uid1 are empty, expected weights to be set." + ); + + let expected_weights: Vec<(u16, I32F32)> = payload + .uids + .iter() + .zip(payload.values.iter()) + .map(|(&uid, &value)| (uid, I32F32::from_num(value))) + .collect(); + + let total_weight: I32F32 = weights.iter().map(|(_, w)| *w).sum(); + + let normalized_weights: Vec<(u16, I32F32)> = weights + .iter() + .map(|&(uid, w)| (uid, w * I32F32::from_num(30) / total_weight)) + .collect(); + + for ((uid_a, w_a), (uid_b, w_b)) in normalized_weights.iter().zip(expected_weights.iter()) { + assert_eq!(uid_a, uid_b); + + let actual_weight_f64: f64 = w_a.to_num::(); + let rounded_actual_weight = actual_weight_f64.round() as i64; + + assert!( + rounded_actual_weight != 0, + "Actual weight for uid {uid_a} is zero" + ); + + let expected_weight = w_b.to_num::(); + + assert_eq!( + rounded_actual_weight, expected_weight, + "Weight mismatch for uid {uid_a}: expected {expected_weight}, got {rounded_actual_weight}" + ); + } + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_cannot_reveal_after_reveal_epoch --exact --show-output --nocapture +#[test] +fn test_reveal_crv3_commits_cannot_reveal_after_reveal_epoch() { + new_test_ext(100).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey1: AccountId = U256::from(1); + let hotkey2: AccountId = U256::from(2); + let reveal_round: u64 = 1000; + + add_network(netuid, 5, 0); + register_ok_neuron(netuid, hotkey1, U256::from(3), 100_000); + register_ok_neuron(netuid, hotkey2, U256::from(4), 100_000); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); + + let neuron_uid1 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey1) + .expect("Failed to get neuron UID for hotkey1"); + let neuron_uid2 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey2) + .expect("Failed to get neuron UID for hotkey2"); + + SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid1, true); + SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid2, true); + + let version_key = SubtensorModule::get_weights_version_key(netuid); + + let payload = WeightsTlockPayload { + hotkey: hotkey1.encode(), + values: vec![10, 20], + uids: vec![neuron_uid1, neuron_uid2], + version_key, + }; + + let serialized_payload = payload.encode(); + + let esk = [2; 32]; + let rng = ChaCha20Rng::seed_from_u64(0); + + let pk_bytes = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a") + .expect("Failed to decode public key bytes"); + let pub_key = ::PublicKeyGroup::deserialize_compressed(&*pk_bytes) + .expect("Failed to deserialize public key"); + + let message = { + let mut hasher = sha2::Sha256::new(); + hasher.update(reveal_round.to_be_bytes()); + hasher.finalize().to_vec() + }; + let identity = Identity::new(b"", vec![message]); + + let ct = tle::( + pub_key, + esk, + &serialized_payload, + identity, + rng, + ) + .expect("Encryption failed"); + + let mut commit_bytes = Vec::new(); + ct.serialize_compressed(&mut commit_bytes) + .expect("Failed to serialize commit"); + + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey1), + netuid, + commit_bytes + .clone() + .try_into() + .expect("Failed to convert commit bytes into bounded vector"), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + + // Do NOT insert the pulse at this time; this simulates the missing pulse during the reveal epoch + // Advance epochs to reach the reveal epoch (3 epochs as reveal_period is 3) + step_epochs(3, netuid); + + // Verify that weights are not set + let weights_sparse = SubtensorModule::unnormalized_weights_sparse(netuid.into()); + let weights = weights_sparse + .get(neuron_uid1 as usize) + .cloned() + .unwrap_or_default(); + + assert!( + weights.is_empty(), + "Weights for neuron_uid1 should be empty as the commit cannot be revealed without the pulse." + ); + + // Now, after the reveal epoch has passed, insert the pulse + let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") + .expect("Failed to decode signature bytes"); + + pallet_drand::Pulses::::insert( + reveal_round, + Pulse { + round: reveal_round, + randomness: vec![0; 32] + .try_into() + .expect("Failed to convert randomness vector"), + signature: sig_bytes + .try_into() + .expect("Failed to convert signature bytes"), + }, + ); + + // Advance one more epoch to be after the reveal epoch + step_epochs(1, netuid); + + // Attempt to reveal commits after the reveal epoch has passed + assert_ok!(SubtensorModule::reveal_crv3_commits_for_subnet(netuid)); + + // Verify that the weights for the neuron have not been set + let weights_sparse = SubtensorModule::unnormalized_weights_sparse(netuid.into()); + let weights = weights_sparse + .get(neuron_uid1 as usize) + .cloned() + .unwrap_or_default(); + + assert!( + weights.is_empty(), + "Weights for neuron_uid1 should be empty as the commit cannot be revealed after the reveal epoch." + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_do_commit_crv3_weights_success --exact --show-output --nocapture +#[test] +fn test_do_commit_crv3_weights_success() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey: AccountId = U256::from(1); + let commit_data: Vec = vec![1, 2, 3, 4, 5]; + let reveal_round: u64 = 1000; + + add_network(netuid, 5, 0); + register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_data + .clone() + .try_into() + .expect("Failed to convert commit data into bounded vector"), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + + let cur_epoch = + SubtensorModule::get_epoch_index(netuid, SubtensorModule::get_current_block_as_u64()); + let commits = + TimelockedWeightCommits::::get(NetUidStorageIndex::from(netuid), cur_epoch); + assert_eq!(commits.len(), 1); + assert_eq!(commits[0].0, hotkey); + assert_eq!(commits[0].2, commit_data); + assert_eq!(commits[0].3, reveal_round); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_do_commit_crv3_weights_disabled --exact --show-output --nocapture +#[test] +fn test_do_commit_crv3_weights_disabled() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey: AccountId = U256::from(1); + let commit_data: Vec = vec![1, 2, 3, 4, 5]; + let reveal_round: u64 = 1000; + + add_network(netuid, 5, 0); + register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); + SubtensorModule::set_weights_set_rate_limit(netuid, 5); + + SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); + assert_err!( + SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_data + .try_into() + .expect("Failed to convert commit data into bounded vector"), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + ), + Error::::CommitRevealDisabled + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_do_commit_crv3_weights_hotkey_not_registered --exact --show-output --nocapture +#[test] +fn test_do_commit_crv3_weights_hotkey_not_registered() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let unregistered_hotkey: AccountId = U256::from(99); + let commit_data: Vec = vec![1, 2, 3, 4, 5]; + let reveal_round: u64 = 1000; + let hotkey: AccountId = U256::from(1); + + add_network(netuid, 5, 0); + register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); + SubtensorModule::set_weights_set_rate_limit(netuid, 5); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + + assert_err!( + SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(unregistered_hotkey), + netuid, + commit_data + .try_into() + .expect("Failed to convert commit data into bounded vector"), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + ), + Error::::HotKeyNotRegisteredInSubNet + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_do_commit_crv3_weights_committing_too_fast --exact --show-output --nocapture +#[test] +fn test_do_commit_crv3_weights_committing_too_fast() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey: AccountId = U256::from(1); + let commit_data_1: Vec = vec![1, 2, 3]; + let commit_data_2: Vec = vec![4, 5, 6]; + let reveal_round: u64 = 1000; + + add_network(netuid, 5, 0); + register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); + SubtensorModule::set_weights_set_rate_limit(netuid, 5); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + let neuron_uid = + SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey).expect("Expected uid"); + SubtensorModule::set_last_update_for_uid(NetUidStorageIndex::from(netuid), neuron_uid, 0); + + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_data_1 + .clone() + .try_into() + .expect("Failed to convert commit data into bounded vector"), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + + assert_err!( + SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_data_2 + .clone() + .try_into() + .expect("Failed to convert commit data into bounded vector"), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + ), + Error::::CommittingWeightsTooFast + ); + + step_block(2); + + assert_err!( + SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_data_2 + .clone() + .try_into() + .expect("Failed to convert commit data into bounded vector"), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + ), + Error::::CommittingWeightsTooFast + ); + + step_block(3); + + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_data_2 + .try_into() + .expect("Failed to convert commit data into bounded vector"), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_do_commit_crv3_weights_too_many_unrevealed_commits --exact --show-output --nocapture +#[test] +fn test_do_commit_crv3_weights_too_many_unrevealed_commits() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey1: AccountId = U256::from(1); + let hotkey2: AccountId = U256::from(2); + let reveal_round: u64 = 1000; + + add_network(netuid, 5, 0); + register_ok_neuron(netuid, hotkey1, U256::from(2), 100_000); + register_ok_neuron(netuid, hotkey2, U256::from(3), 100_000); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + // Hotkey1 submits 10 commits successfully + for i in 0..10 { + let commit_data: Vec = vec![i as u8; 5]; + let bounded_commit_data = commit_data + .try_into() + .expect("Failed to convert commit data into bounded vector"); + + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey1), + netuid, + bounded_commit_data, + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + } + + // Hotkey1 attempts to commit an 11th time, should fail with TooManyUnrevealedCommits + let new_commit_data: Vec = vec![11; 5]; + let bounded_new_commit_data = new_commit_data + .try_into() + .expect("Failed to convert new commit data into bounded vector"); + + assert_err!( + SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey1), + netuid, + bounded_new_commit_data, + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + ), + Error::::TooManyUnrevealedCommits + ); + + // Hotkey2 can still submit commits independently + let commit_data_hotkey2: Vec = vec![0; 5]; + let bounded_commit_data_hotkey2 = commit_data_hotkey2 + .try_into() + .expect("Failed to convert commit data into bounded vector"); + + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey2), + netuid, + bounded_commit_data_hotkey2, + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + + // Hotkey2 can submit up to 10 commits + for i in 1..10 { + let commit_data: Vec = vec![i as u8; 5]; + let bounded_commit_data = commit_data + .try_into() + .expect("Failed to convert commit data into bounded vector"); + + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey2), + netuid, + bounded_commit_data, + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + } + + // Hotkey2 attempts to commit an 11th time, should fail + let new_commit_data: Vec = vec![11; 5]; + let bounded_new_commit_data = new_commit_data + .try_into() + .expect("Failed to convert new commit data into bounded vector"); + + assert_err!( + SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey2), + netuid, + bounded_new_commit_data, + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + ), + Error::::TooManyUnrevealedCommits + ); + + step_epochs(10, netuid); + + let new_commit_data: Vec = vec![11; 5]; + let bounded_new_commit_data = new_commit_data + .try_into() + .expect("Failed to convert new commit data into bounded vector"); + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey1), + netuid, + bounded_new_commit_data, + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + }); +} diff --git a/pallets/subtensor/src/tests/weights/timelocked_reveal.rs b/pallets/subtensor/src/tests/weights/timelocked_reveal.rs new file mode 100644 index 0000000000..495a269bc3 --- /dev/null +++ b/pallets/subtensor/src/tests/weights/timelocked_reveal.rs @@ -0,0 +1,938 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! Timelocked (CRv3) reveal failure modes and multi-commit processing. + +use ark_serialize::CanonicalDeserialize; +use ark_serialize::CanonicalSerialize; +use frame_support::{assert_ok, dispatch::DispatchResult}; +use pallet_drand::types::Pulse; +use rand_chacha::{ChaCha20Rng, rand_core::SeedableRng}; +use sha2::Digest; +use sp_core::Encode; +use sp_core::U256; +use sp_runtime::{BoundedVec, traits::ConstU32}; +use substrate_fixed::types::I32F32; +use subtensor_runtime_common::NetUidStorageIndex; +use tle::{ + curves::drand::TinyBLS381, ibe::fullident::Identity, + stream_ciphers::AESGCMStreamCipherProvider, tlock::tle, +}; +use w3f_bls::EngineBLS; + +use crate::coinbase::reveal_commits::WeightsTlockPayload; +use crate::tests::mock::*; +use crate::*; + +#[test] +fn test_reveal_crv3_commits_decryption_failure() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey: AccountId = U256::from(1); + let reveal_round: u64 = 1000; + + add_network(netuid, 5, 0); + register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + + let commit_bytes: Vec = vec![0xff; 100]; + let bounded_commit_bytes = commit_bytes + .clone() + .try_into() + .expect("Failed to convert commit bytes into bounded vector"); + + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey), + netuid, + bounded_commit_bytes, + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + + step_epochs(1, netuid); + + pallet_drand::Pulses::::insert( + reveal_round, + Pulse { + round: reveal_round, + randomness: vec![0; 32] + .try_into() + .expect("Failed to convert randomness vector"), + signature: vec![0; 128] + .try_into() + .expect("Failed to convert signature vector"), + }, + ); + + assert_ok!(SubtensorModule::reveal_crv3_commits_for_subnet(netuid)); + + let neuron_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey) + .expect("Failed to get neuron UID for hotkey") as usize; + let weights_matrix = SubtensorModule::get_weights(netuid.into()); + let weights = weights_matrix.get(neuron_uid).cloned().unwrap_or_default(); + assert!(weights.iter().all(|&w| w == I32F32::from_num(0))); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_multiple_commits_some_fail_some_succeed --exact --show-output --nocapture +#[test] +fn test_reveal_crv3_commits_multiple_commits_some_fail_some_succeed() { + new_test_ext(100).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey1: AccountId = U256::from(1); + let hotkey2: AccountId = U256::from(2); + let reveal_round: u64 = 1000; + + add_network(netuid, 5, 0); + register_ok_neuron(netuid, hotkey1, U256::from(3), 100_000); + register_ok_neuron(netuid, hotkey2, U256::from(4), 100_000); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + assert_ok!(SubtensorModule::set_reveal_period(netuid, 1)); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + // Prepare a valid payload for hotkey1 + let neuron_uid1 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey1) + .expect("Failed to get neuron UID for hotkey1"); + let version_key = SubtensorModule::get_weights_version_key(netuid); + let valid_payload = WeightsTlockPayload { + hotkey: hotkey1.encode(), + values: vec![10], + uids: vec![neuron_uid1], + version_key, + }; + let serialized_valid_payload = valid_payload.encode(); + + let esk = [2; 32]; + let rng = ChaCha20Rng::seed_from_u64(0); + + let pk_bytes = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a") + .expect("Failed to decode public key bytes"); + let pub_key = ::PublicKeyGroup::deserialize_compressed(&*pk_bytes) + .expect("Failed to deserialize public key"); + + let message = { + let mut hasher = sha2::Sha256::new(); + hasher.update(reveal_round.to_be_bytes()); + hasher.finalize().to_vec() + }; + let identity = Identity::new(b"", vec![message]); + + let ct_valid = tle::( + pub_key, + esk, + &serialized_valid_payload, + identity.clone(), + rng.clone(), + ) + .expect("Encryption failed"); + + let mut commit_bytes_valid = Vec::new(); + ct_valid + .serialize_compressed(&mut commit_bytes_valid) + .expect("Failed to serialize valid commit"); + + // Prepare an invalid payload for hotkey2 + let invalid_payload = vec![0u8; 10]; // Invalid payload + let ct_invalid = tle::( + pub_key, + esk, + &invalid_payload, + identity, + rng, + ) + .expect("Encryption failed"); + + let mut commit_bytes_invalid = Vec::new(); + ct_invalid + .serialize_compressed(&mut commit_bytes_invalid) + .expect("Failed to serialize invalid commit"); + + // Insert both commits + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey1), + netuid, + commit_bytes_valid.try_into().expect("Failed to convert valid commit data"), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey2), + netuid, + commit_bytes_invalid.try_into().expect("Failed to convert invalid commit data"), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + + // Insert the pulse + let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") + .expect("Failed to decode signature bytes"); + + pallet_drand::Pulses::::insert( + reveal_round, + Pulse { + round: reveal_round, + randomness: vec![0; 32].try_into().expect("Failed to convert randomness vector"), + signature: sig_bytes.try_into().expect("Failed to convert signature bytes"), + }, + ); + + step_epochs(1, netuid); + + // Verify that weights are set for hotkey1 + let neuron_uid1 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey1) + .expect("Failed to get neuron UID for hotkey1") as usize; + let weights_sparse = SubtensorModule::unnormalized_weights_sparse(netuid.into()); + let weights1 = weights_sparse.get(neuron_uid1).cloned().unwrap_or_default(); + assert!( + !weights1.is_empty(), + "Weights for neuron_uid1 should be set" + ); + + // Verify that weights are not set for hotkey2 + let neuron_uid2 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey2) + .expect("Failed to get neuron UID for hotkey2") as usize; + let weights2 = weights_sparse.get(neuron_uid2).cloned().unwrap_or_default(); + assert!( + weights2.is_empty(), + "Weights for neuron_uid2 should be empty as commit could not be revealed" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_do_set_weights_failure --exact --show-output --nocapture +#[test] +fn test_reveal_crv3_commits_do_set_weights_failure() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey: AccountId = U256::from(1); + let reveal_round: u64 = 1000; + + add_network(netuid, 5, 0); + register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + // Prepare payload with mismatched uids and values lengths + let version_key = SubtensorModule::get_weights_version_key(netuid); + let payload = WeightsTlockPayload { + hotkey: hotkey.encode(), + values: vec![10, 20], // Length 2 + uids: vec![0], // Length 1 + version_key, + }; + let serialized_payload = payload.encode(); + + let esk = [2; 32]; + let rng = ChaCha20Rng::seed_from_u64(0); + + let pk_bytes = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a") + .expect("Failed to decode public key bytes"); + let pub_key = ::PublicKeyGroup::deserialize_compressed(&*pk_bytes) + .expect("Failed to deserialize public key"); + + let message = { + let mut hasher = sha2::Sha256::new(); + hasher.update(reveal_round.to_be_bytes()); + hasher.finalize().to_vec() + }; + let identity = Identity::new(b"", vec![message]); + + let ct = tle::( + pub_key, + esk, + &serialized_payload, + identity, + rng, + ) + .expect("Encryption failed"); + + let mut commit_bytes = Vec::new(); + ct.serialize_compressed(&mut commit_bytes) + .expect("Failed to serialize commit"); + + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_bytes.try_into().expect("Failed to convert commit data into bounded vector"), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + + let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") + .expect("Failed to decode signature bytes"); + + pallet_drand::Pulses::::insert( + reveal_round, + Pulse { + round: reveal_round, + randomness: vec![0; 32].try_into().expect("Failed to convert randomness vector"), + signature: sig_bytes.try_into().expect("Failed to convert signature bytes"), + }, + ); + + step_epochs(3, netuid); + + // Verify that weights are not set due to `do_set_weights` failure + let neuron_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey) + .expect("Failed to get neuron UID for hotkey") as usize; + let weights_sparse = SubtensorModule::unnormalized_weights_sparse(netuid.into()); + let weights = weights_sparse.get(neuron_uid).cloned().unwrap_or_default(); + assert!( + weights.is_empty(), + "Weights for neuron_uid should be empty as do_set_weights should have failed" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_payload_decoding_failure --exact --show-output --nocapture +#[test] +fn test_reveal_crv3_commits_payload_decoding_failure() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey: AccountId = U256::from(1); + let reveal_round: u64 = 1000; + + add_network(netuid, 5, 0); + register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + let invalid_payload = vec![0u8; 10]; // Not a valid encoding of WeightsTlockPayload + + let esk = [2; 32]; + let rng = ChaCha20Rng::seed_from_u64(0); + + let pk_bytes = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a") + .expect("Failed to decode public key bytes"); + let pub_key = ::PublicKeyGroup::deserialize_compressed(&*pk_bytes) + .expect("Failed to deserialize public key"); + + let message = { + let mut hasher = sha2::Sha256::new(); + hasher.update(reveal_round.to_be_bytes()); + hasher.finalize().to_vec() + }; + let identity = Identity::new(b"", vec![message]); + + let ct = tle::( + pub_key, + esk, + &invalid_payload, + identity, + rng, + ) + .expect("Encryption failed"); + + let mut commit_bytes = Vec::new(); + ct.serialize_compressed(&mut commit_bytes) + .expect("Failed to serialize commit"); + + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_bytes.try_into().expect("Failed to convert commit data into bounded vector"), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + + let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") + .expect("Failed to decode signature bytes"); + + pallet_drand::Pulses::::insert( + reveal_round, + Pulse { + round: reveal_round, + randomness: vec![0; 32].try_into().expect("Failed to convert randomness vector"), + signature: sig_bytes.try_into().expect("Failed to convert signature bytes"), + }, + ); + + step_epochs(3, netuid); + + // Verify that weights are not set + let neuron_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey) + .expect("Failed to get neuron UID for hotkey") as usize; + let weights_sparse = SubtensorModule::unnormalized_weights_sparse(netuid.into()); + let weights = weights_sparse.get(neuron_uid).cloned().unwrap_or_default(); + assert!( + weights.is_empty(), + "Weights for neuron_uid should be empty as the payload could not be decoded" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_signature_deserialization_failure --exact --show-output --nocapture +#[test] +fn test_reveal_crv3_commits_signature_deserialization_failure() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey: AccountId = U256::from(1); + let reveal_round: u64 = 1000; + + add_network(netuid, 5, 0); + register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + let version_key = SubtensorModule::get_weights_version_key(netuid); + let payload = WeightsTlockPayload { + hotkey: hotkey.encode(), + values: vec![10, 20], + uids: vec![0, 1], + version_key, + }; + let serialized_payload = payload.encode(); + + let esk = [2; 32]; + let rng = ChaCha20Rng::seed_from_u64(0); + + let pk_bytes = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a") + .expect("Failed to decode public key bytes"); + let pub_key = ::PublicKeyGroup::deserialize_compressed(&*pk_bytes) + .expect("Failed to deserialize public key"); + + let message = { + let mut hasher = sha2::Sha256::new(); + hasher.update(reveal_round.to_be_bytes()); + hasher.finalize().to_vec() + }; + let identity = Identity::new(b"", vec![message]); + + let ct = tle::( + pub_key, + esk, + &serialized_payload, + identity, + rng, + ) + .expect("Encryption failed"); + + let mut commit_bytes = Vec::new(); + ct.serialize_compressed(&mut commit_bytes) + .expect("Failed to serialize commit"); + + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_bytes.try_into().expect("Failed to convert commit data into bounded vector"), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + + pallet_drand::Pulses::::insert( + reveal_round, + Pulse { + round: reveal_round, + randomness: vec![0; 32].try_into().expect("Failed to convert randomness vector"), + signature: vec![0; 10].try_into().expect("Failed to create invalid signature"), // Invalid signature length + }, + ); + + step_epochs(3, netuid); + + // Verify that weights are not set + let neuron_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey) + .expect("Failed to get neuron UID for hotkey") as usize; + let weights_sparse = SubtensorModule::unnormalized_weights_sparse(netuid.into()); + let weights = weights_sparse.get(neuron_uid).cloned().unwrap_or_default(); + assert!( + weights.is_empty(), + "Weights for neuron_uid should be empty as the signature could not be deserialized" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_do_commit_crv3_weights_commit_size_exceeds_limit --exact --show-output --nocapture +#[test] +fn test_do_commit_crv3_weights_commit_size_exceeds_limit() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey: AccountId = U256::from(1); + let reveal_round: u64 = 1000; + + add_network(netuid, 5, 0); + register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + let max_commit_size = MAX_CRV3_COMMIT_SIZE_BYTES as usize; + let commit_data_exceeding: Vec = vec![0u8; max_commit_size + 1]; // Exceeds max size + + // Attempt to create a BoundedVec; this should fail + let bounded_commit_data_result = + BoundedVec::>::try_from( + commit_data_exceeding.clone(), + ); + + assert!( + bounded_commit_data_result.is_err(), + "Expected error when converting commit data exceeding max size into BoundedVec" + ); + + let commit_data_max_size: Vec = vec![0u8; max_commit_size]; // Exactly at max size + let bounded_commit_data = BoundedVec::>::try_from( + commit_data_max_size.clone(), + ) + .expect("Failed to create BoundedVec with data at max size"); + + // Now call the function with valid data at max size + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey), + netuid, + bounded_commit_data, + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_with_empty_commit_queue --exact --show-output --nocapture +#[test] +fn test_reveal_crv3_commits_with_empty_commit_queue() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + + add_network(netuid, 5, 0); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + step_epochs(2, netuid); + + let weights_sparse = SubtensorModule::unnormalized_weights_sparse(netuid.into()); + assert!( + weights_sparse.is_empty(), + "Weights should be empty as there were no commits to reveal" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_with_incorrect_identity_message --exact --show-output --nocapture +#[test] +fn test_reveal_crv3_commits_with_incorrect_identity_message() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey: AccountId = U256::from(1); + let reveal_round: u64 = 1000; + + add_network(netuid, 5, 0); + register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + assert_ok!(SubtensorModule::set_reveal_period(netuid, 1)); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + // Prepare a valid payload but use incorrect identity message during encryption + let neuron_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey) + .expect("Failed to get neuron UID for hotkey"); + let version_key = SubtensorModule::get_weights_version_key(netuid); + let payload = WeightsTlockPayload { + hotkey: hotkey.encode(), + values: vec![10], + uids: vec![neuron_uid], + version_key, + }; + let serialized_payload = payload.encode(); + + let esk = [2; 32]; + let rng = ChaCha20Rng::seed_from_u64(0); + + let pk_bytes = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a") + .expect("Failed to decode public key bytes"); + let pub_key = ::PublicKeyGroup::deserialize_compressed(&*pk_bytes) + .expect("Failed to deserialize public key"); + + // Use incorrect message for identity (e.g., reveal_round + 1) + let incorrect_message = { + let mut hasher = sha2::Sha256::new(); + hasher.update((reveal_round + 1).to_be_bytes()); + hasher.finalize().to_vec() + }; + let identity = Identity::new(b"", vec![incorrect_message]); + + let ct = tle::( + pub_key, + esk, + &serialized_payload, + identity, + rng, + ) + .expect("Encryption failed"); + + let mut commit_bytes = Vec::new(); + ct.serialize_compressed(&mut commit_bytes) + .expect("Failed to serialize commit"); + + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_bytes.try_into().expect("Failed to convert commit data into bounded vector"), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + + let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") + .expect("Failed to decode signature bytes"); + + pallet_drand::Pulses::::insert( + reveal_round, + Pulse { + round: reveal_round, + randomness: vec![0; 32].try_into().expect("Failed to convert randomness vector"), + signature: sig_bytes.try_into().expect("Failed to convert signature bytes"), + }, + ); + + step_epochs(1, netuid); + + // Verify that weights are not set due to decryption failure + let neuron_uid = neuron_uid as usize; + let weights_sparse = SubtensorModule::unnormalized_weights_sparse(netuid.into()); + let weights = weights_sparse.get(neuron_uid).cloned().unwrap_or_default(); + assert!( + weights.is_empty(), + "Weights for neuron_uid should be empty due to incorrect identity message" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_multiple_commits_by_same_hotkey_within_limit --exact --show-output --nocapture +#[test] +fn test_multiple_commits_by_same_hotkey_within_limit() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey: AccountId = U256::from(1); + let reveal_round: u64 = 1000; + + add_network(netuid, 5, 0); + register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + assert_ok!(SubtensorModule::set_reveal_period(netuid, 1)); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + for i in 0..10 { + let commit_data: Vec = vec![i; 5]; + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_data + .try_into() + .expect("Failed to convert commit data into bounded vector"), + reveal_round + i as u64, + SubtensorModule::get_commit_reveal_weights_version() + )); + } + + let cur_epoch = + SubtensorModule::get_epoch_index(netuid, SubtensorModule::get_current_block_as_u64()); + let commits = + TimelockedWeightCommits::::get(NetUidStorageIndex::from(netuid), cur_epoch); + assert_eq!( + commits.len(), + 10, + "Expected 10 commits stored for the hotkey" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_removes_past_epoch_commits --exact --show-output --nocapture +#[test] +fn test_reveal_crv3_commits_removes_past_epoch_commits() { + new_test_ext(100).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey: AccountId = U256::from(1); + let reveal_round: u64 = 1_000; + + add_network(netuid, /*tempo*/ 5, 0); + register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + assert_ok!(SubtensorModule::set_reveal_period(netuid, 1)); // reveal_period = 1 epoch + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + // --------------------------------------------------------------------- + // Put dummy commits into the two epochs immediately *before* current. + // --------------------------------------------------------------------- + // Establish a non-zero epoch counter and pin the scheduler so the reveal + // pass sees exactly this epoch (no look-ahead increment). + let cur_epoch: u64 = 10; + SubnetEpochIndex::::insert(netuid, cur_epoch); + LastEpochBlock::::insert(netuid, SubtensorModule::get_current_block_as_u64()); + PendingEpochAt::::insert(netuid, 0); + let cur_block = SubtensorModule::get_current_block_as_u64(); + let past_epoch = cur_epoch.saturating_sub(2); // definitely < reveal_epoch + let reveal_epoch = cur_epoch.saturating_sub(1); // == cur_epoch - reveal_period + + for &epoch in &[past_epoch, reveal_epoch] { + let bounded_commit = vec![epoch as u8; 5].try_into().expect("bounded vec"); + + assert_ok!(TimelockedWeightCommits::::try_mutate( + NetUidStorageIndex::from(netuid), + epoch, + |q| -> DispatchResult { + q.push_back((hotkey, cur_block, bounded_commit, reveal_round)); + Ok(()) + } + )); + } + + // Sanity – both epochs presently hold a commit. + assert!( + !TimelockedWeightCommits::::get(NetUidStorageIndex::from(netuid), past_epoch) + .is_empty() + ); + assert!( + !TimelockedWeightCommits::::get(NetUidStorageIndex::from(netuid), reveal_epoch) + .is_empty() + ); + + // --------------------------------------------------------------------- + // Run the reveal pass WITHOUT a pulse – only expiry housekeeping runs. + // --------------------------------------------------------------------- + assert_ok!(SubtensorModule::reveal_crv3_commits_for_subnet(netuid)); + + // past_epoch (< reveal_epoch) must be gone + assert!( + TimelockedWeightCommits::::get(NetUidStorageIndex::from(netuid), past_epoch) + .is_empty(), + "expired epoch {past_epoch} should be cleared" + ); + + // reveal_epoch queue is *kept* because its commit could still be revealed later. + assert!( + !TimelockedWeightCommits::::get(NetUidStorageIndex::from(netuid), reveal_epoch) + .is_empty(), + "reveal-epoch {reveal_epoch} must be retained until commit can be revealed" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_multiple_valid_commits_all_processed --exact --show-output --nocapture +#[test] +fn test_reveal_crv3_commits_multiple_valid_commits_all_processed() { + new_test_ext(100).execute_with(|| { + let netuid = NetUid::from(1); + let reveal_round: u64 = 1_000; + + // ───── network parameters ─────────────────────────────────────────── + add_network(netuid, 5, 0); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + assert_ok!(SubtensorModule::set_reveal_period(netuid, 1)); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_max_registrations_per_block(netuid, 100); + SubtensorModule::set_target_registrations_per_interval(netuid, 100); + + // Insert the pulse + let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") + .expect("Failed to decode signature bytes"); + + // pulse for round 1000 + // let sig_bytes = hex::decode( + // "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e\ + // 342b73a8dd2bacbe47e4b6b63ed5e39", + // ) + // .unwrap(); + pallet_drand::Pulses::::insert( + reveal_round, + Pulse { + round: reveal_round, + randomness: vec![0; 32].try_into().unwrap(), + signature: sig_bytes.try_into().unwrap(), + }, + ); + + // ───── five neurons (hotkeys 1‑5) ─────────────────────────────────── + let hotkeys: Vec<_> = (1..=5).map(U256::from).collect(); + for (i, hk) in hotkeys.iter().enumerate() { + let cold: AccountId = U256::from(i + 100); + + register_ok_neuron(netuid, *hk, cold, 100_000); + SubtensorModule::set_validator_permit_for_uid(netuid, i as u16, true); + + // add minimal stake so `do_set_weights` will succeed + add_balance_to_coldkey_account(&cold, 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + hk, + &cold, + netuid, + 1.into(), + ); + + step_block(1); // avoids TooManyRegistrationsThisBlock + } + + + // ───── create & submit commits for each hotkey ────────────────────── + let esk = [2u8; 32]; + let pk_bytes = hex::decode( + "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c\ + 8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb\ + 5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a", + ) + .unwrap(); + let pk = + ::PublicKeyGroup::deserialize_compressed(&*pk_bytes).unwrap(); + + for (i, hk) in hotkeys.iter().enumerate() { + let payload = WeightsTlockPayload { + hotkey: hk.encode(), + values: vec![10, 20, 30, 40, 50], + uids: (0..5).map(|u| u as u16).collect(), + version_key: SubtensorModule::get_weights_version_key(netuid), + }; + + let id_msg = { + let mut h = sha2::Sha256::new(); + h.update(reveal_round.to_be_bytes()); + h.finalize().to_vec() + }; + let ct = tle::( + pk, + esk, + &payload.encode(), + Identity::new(b"", vec![id_msg]), + ChaCha20Rng::seed_from_u64(i as u64), + ) + .unwrap(); + + let mut commit_bytes = Vec::new(); + ct.serialize_compressed(&mut commit_bytes).unwrap(); + + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(*hk), + netuid, + commit_bytes.try_into().unwrap(), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + } + + // advance reveal_period + 1 epochs → 2 epochs + step_epochs(2, netuid); + + // ───── assertions ─────────────────────────────────────────────────── + let w_sparse = SubtensorModule::unnormalized_weights_sparse(netuid.into()); + for hk in hotkeys { + let uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hk).unwrap() as usize; + assert!( + !w_sparse.get(uid).unwrap_or(&Vec::new()).is_empty(), + "weights for uid {uid} should be set" + ); + } + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_max_neurons --exact --show-output --nocapture +#[test] +fn test_reveal_crv3_commits_max_neurons() { + new_test_ext(100).execute_with(|| { + let netuid = NetUid::from(1); + let reveal_round: u64 = 1_000; + + // ───── network parameters ─────────────────────────────────────────── + add_network(netuid, 5, 0); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + assert_ok!(SubtensorModule::set_reveal_period(netuid, 1)); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_max_registrations_per_block(netuid, 10_000); + SubtensorModule::set_target_registrations_per_interval(netuid, 10_000); + SubtensorModule::set_max_allowed_uids(netuid, 10_024); + + // ───── register 1 024 neurons ─────────────────────────────────────── + for i in 0..1_024u16 { + let hk: AccountId = U256::from(i as u64 + 1); + let cold: AccountId = U256::from(i as u64 + 10_000); + + register_ok_neuron(netuid, hk, cold, 100_000); + SubtensorModule::set_validator_permit_for_uid(netuid, i, true); + + // give each neuron a nominal stake (safe even if not needed) + add_balance_to_coldkey_account(&cold, 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hk, + &cold, + netuid, + 1.into(), + ); + + step_block(1); // avoid registration‑limit panic + } + + // ───── pulse for round 1000 ───────────────────────────────────────── + let sig_bytes = hex::decode( + "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e\ + 342b73a8dd2bacbe47e4b6b63ed5e39", + ) + .unwrap(); + pallet_drand::Pulses::::insert( + reveal_round, + Pulse { + round: reveal_round, + randomness: vec![0; 32].try_into().unwrap(), + signature: sig_bytes.try_into().unwrap(), + }, + ); + + // ───── three committing hotkeys ───────────────────────────────────── + let esk = [2u8; 32]; + let pk_bytes = hex::decode( + "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c\ + 8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb\ + 5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a", + ) + .unwrap(); + let pk = + ::PublicKeyGroup::deserialize_compressed(&*pk_bytes).unwrap(); + let committing_hotkeys = [U256::from(1), U256::from(2), U256::from(3)]; + let mut commits = Vec::new(); + for (i, hk) in committing_hotkeys.iter().enumerate() { + let payload = WeightsTlockPayload { + hotkey: hk.encode(), + values: vec![10u16; 1_024], + uids: (0..1_024).collect(), + version_key: SubtensorModule::get_weights_version_key(netuid), + }; + let id_msg = { + let mut h = sha2::Sha256::new(); + h.update(reveal_round.to_be_bytes()); + h.finalize().to_vec() + }; + let ct = tle::( + pk, + esk, + &payload.encode(), + Identity::new(b"", vec![id_msg]), + ChaCha20Rng::seed_from_u64(i as u64), + ) + .unwrap(); + let mut commit_bytes = Vec::new(); + ct.serialize_compressed(&mut commit_bytes).unwrap(); + // Submit the commit + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(*hk), + netuid, + commit_bytes + .try_into() + .expect("Failed to convert commit data"), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + + // Store the expected weights for later comparison + commits.push((hk, payload)); + } + // ───── advance reveal_period + 1 epochs ───────────────────────────── + step_epochs(2, netuid); + + // ───── verify weights ─────────────────────────────────────────────── + let w_sparse = SubtensorModule::unnormalized_weights_sparse(netuid.into()); + for hk in &committing_hotkeys { + let uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, hk).unwrap() as usize; + assert!( + !w_sparse.get(uid).unwrap_or(&Vec::new()).is_empty(), + "weights for uid {uid} should be set" + ); + } + }); +} diff --git a/pallets/subtensor/src/tests/weights/timelocked_reveal_hotkey.rs b/pallets/subtensor/src/tests/weights/timelocked_reveal_hotkey.rs new file mode 100644 index 0000000000..a56387d577 --- /dev/null +++ b/pallets/subtensor/src/tests/weights/timelocked_reveal_hotkey.rs @@ -0,0 +1,553 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! Timelocked (CRv3) reveal: hotkey checks, missing pulse retry, legacy payload. + +use ark_serialize::CanonicalDeserialize; +use ark_serialize::CanonicalSerialize; +use frame_support::assert_ok; +use pallet_drand::types::Pulse; +use rand_chacha::{ChaCha20Rng, rand_core::SeedableRng}; +use sha2::Digest; +use sp_core::Encode; +use sp_core::U256; +use sp_runtime::{BoundedVec, traits::ConstU32}; +use substrate_fixed::types::I32F32; +use subtensor_runtime_common::NetUidStorageIndex; +use tle::{ + curves::drand::TinyBLS381, ibe::fullident::Identity, + stream_ciphers::AESGCMStreamCipherProvider, tlock::tle, +}; +use w3f_bls::EngineBLS; + +use crate::coinbase::reveal_commits::{LegacyWeightsTlockPayload, WeightsTlockPayload}; +use crate::tests::mock::*; +use crate::*; + +#[test] +fn test_reveal_crv3_commits_hotkey_check() { + new_test_ext(100).execute_with(|| { + // Failure case: hotkey mismatch + let netuid = NetUid::from(1); + let hotkey1: AccountId = U256::from(1); + let hotkey2: AccountId = U256::from(2); + let reveal_round: u64 = 1000; + + add_network(netuid, 5, 0); + register_ok_neuron(netuid, hotkey1, U256::from(3), 100_000); + register_ok_neuron(netuid, hotkey2, U256::from(4), 100_000); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); + + let neuron_uid1 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey1) + .expect("Failed to get neuron UID for hotkey1"); + let neuron_uid2 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey2) + .expect("Failed to get neuron UID for hotkey2"); + + SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid1, true); + SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid2, true); + add_balance_to_coldkey_account(&U256::from(3), 1.into()); + add_balance_to_coldkey_account(&U256::from(4), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &(U256::from(3)), + netuid, + 1.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey2, + &(U256::from(4)), + netuid, + 1.into(), + ); + + let version_key = SubtensorModule::get_weights_version_key(netuid); + + let payload = WeightsTlockPayload { + hotkey: hotkey2.encode(), // Mismatch: using hotkey2 instead of hotkey1 + values: vec![10, 20], + uids: vec![neuron_uid1, neuron_uid2], + version_key, + }; + + let serialized_payload = payload.encode(); + + let esk = [2; 32]; + let rng = ChaCha20Rng::seed_from_u64(0); + + let pk_bytes = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a") + .expect("Failed to decode public key bytes"); + let pub_key = ::PublicKeyGroup::deserialize_compressed(&*pk_bytes) + .expect("Failed to deserialize public key"); + + let message = { + let mut hasher = sha2::Sha256::new(); + hasher.update(reveal_round.to_be_bytes()); + hasher.finalize().to_vec() + }; + let identity = Identity::new(b"", vec![message]); + + let ct = tle::( + pub_key, + esk, + &serialized_payload, + identity, + rng, + ) + .expect("Encryption failed"); + + let mut commit_bytes = Vec::new(); + ct.serialize_compressed(&mut commit_bytes) + .expect("Failed to serialize commit"); + + assert!( + !commit_bytes.is_empty(), + "commit_bytes is empty after serialization" + ); + + log::debug!( + "Commit bytes now contain {commit_bytes:#?}" + ); + + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey1), + netuid, + commit_bytes.clone().try_into().expect("Failed to convert commit bytes into bounded vector"), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + + let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") + .expect("Failed to decode signature bytes"); + + pallet_drand::Pulses::::insert( + reveal_round, + Pulse { + round: reveal_round, + randomness: vec![0; 32].try_into().expect("Failed to convert randomness vector"), + signature: sig_bytes.try_into().expect("Failed to convert signature bytes"), + }, + ); + + // Step epochs to run the epoch via the blockstep + step_epochs(3, netuid); + + let weights_sparse = SubtensorModule::unnormalized_weights_sparse(netuid.into()); + let weights = weights_sparse.get(neuron_uid1 as usize).cloned().unwrap_or_default(); + + assert!( + weights.is_empty(), + "Weights for neuron_uid1 should be empty due to hotkey mismatch." + ); + }); + + new_test_ext(100).execute_with(|| { + // Success case: hotkey match + let netuid = NetUid::from(1); + let hotkey1: AccountId = U256::from(1); + let hotkey2: AccountId = U256::from(2); + let reveal_round: u64 = 1000; + + add_network(netuid, 5, 0); + register_ok_neuron(netuid, hotkey1, U256::from(3), 100_000); + register_ok_neuron(netuid, hotkey2, U256::from(4), 100_000); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); + + let neuron_uid1 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey1) + .expect("Failed to get neuron UID for hotkey1"); + let neuron_uid2 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey2) + .expect("Failed to get neuron UID for hotkey2"); + + SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid1, true); + SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid2, true); + add_balance_to_coldkey_account(&U256::from(3), 1.into()); + add_balance_to_coldkey_account(&U256::from(4), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &(U256::from(3)), + netuid, + 1.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey2, + &(U256::from(4)), + netuid, + 1.into(), + ); + + let version_key = SubtensorModule::get_weights_version_key(netuid); + + let payload = WeightsTlockPayload { + hotkey: hotkey1.encode(), // Match: using hotkey1 + values: vec![10, 20], + uids: vec![neuron_uid1, neuron_uid2], + version_key, + }; + + let serialized_payload = payload.encode(); + + let esk = [2; 32]; + let rng = ChaCha20Rng::seed_from_u64(0); + + let pk_bytes = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a") + .expect("Failed to decode public key bytes"); + let pub_key = ::PublicKeyGroup::deserialize_compressed(&*pk_bytes) + .expect("Failed to deserialize public key"); + + let message = { + let mut hasher = sha2::Sha256::new(); + hasher.update(reveal_round.to_be_bytes()); + hasher.finalize().to_vec() + }; + let identity = Identity::new(b"", vec![message]); + + let ct = tle::( + pub_key, + esk, + &serialized_payload, + identity, + rng, + ) + .expect("Encryption failed"); + + let mut commit_bytes = Vec::new(); + ct.serialize_compressed(&mut commit_bytes) + .expect("Failed to serialize commit"); + + assert!( + !commit_bytes.is_empty(), + "commit_bytes is empty after serialization" + ); + + log::debug!( + "Commit bytes now contain {commit_bytes:#?}" + ); + + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey1), + netuid, + commit_bytes.clone().try_into().expect("Failed to convert commit bytes into bounded vector"), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + + let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") + .expect("Failed to decode signature bytes"); + + pallet_drand::Pulses::::insert( + reveal_round, + Pulse { + round: reveal_round, + randomness: vec![0; 32].try_into().expect("Failed to convert randomness vector"), + signature: sig_bytes.try_into().expect("Failed to convert signature bytes"), + }, + ); + + // Step epochs to run the epoch via the blockstep + step_epochs(3, netuid); + + let weights_sparse = SubtensorModule::unnormalized_weights_sparse(netuid.into()); + let weights = weights_sparse.get(neuron_uid1 as usize).cloned().unwrap_or_default(); + + assert!( + !weights.is_empty(), + "Weights for neuron_uid1 are empty, expected weights to be set." + ); + + let expected_weights: Vec<(u16, I32F32)> = payload + .uids + .iter() + .zip(payload.values.iter()) + .map(|(&uid, &value)| (uid, I32F32::from_num(value))) + .collect(); + + let total_weight: I32F32 = weights.iter().map(|(_, w)| *w).sum(); + + let normalized_weights: Vec<(u16, I32F32)> = weights + .iter() + .map(|&(uid, w)| (uid, w * I32F32::from_num(30) / total_weight)) + .collect(); + + for ((uid_a, w_a), (uid_b, w_b)) in normalized_weights.iter().zip(expected_weights.iter()) { + assert_eq!(uid_a, uid_b); + + let actual_weight_f64: f64 = w_a.to_num::(); + let rounded_actual_weight = actual_weight_f64.round() as i64; + + assert!( + rounded_actual_weight != 0, + "Actual weight for uid {uid_a} is zero" + ); + + let expected_weight = w_b.to_num::(); + + assert_eq!( + rounded_actual_weight, expected_weight, + "Weight mismatch for uid {uid_a}: expected {expected_weight}, got {rounded_actual_weight}" + ); + } + }); +} + +#[test] +fn test_reveal_crv3_commits_retry_on_missing_pulse() { + new_test_ext(100).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey: AccountId = U256::from(1); + let reveal_round: u64 = 1_000; + + // ─── network & neuron ─────────────────────────────────────────────── + add_network(netuid, 5, 0); + register_ok_neuron(netuid, hotkey, U256::from(3), 100_000); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_stake_threshold(0); + + let uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey).unwrap(); + SubtensorModule::set_validator_permit_for_uid(netuid, uid, true); + + // ─── craft commit ─────────────────────────────────────────────────── + let payload = WeightsTlockPayload { + hotkey: hotkey.encode(), + values: vec![10], + uids: vec![uid], + version_key: SubtensorModule::get_weights_version_key(netuid), + }; + let esk = [2u8; 32]; + let pk_bytes = hex::decode( + "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c\ + 8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb\ + 5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a", + ) + .unwrap(); + let pk = + ::PublicKeyGroup::deserialize_compressed(&*pk_bytes).unwrap(); + let id_msg = { + let mut h = sha2::Sha256::new(); + h.update(reveal_round.to_be_bytes()); + h.finalize().to_vec() + }; + let ct = tle::( + pk, + esk, + &payload.encode(), + Identity::new(b"", vec![id_msg]), + ChaCha20Rng::seed_from_u64(0), + ) + .unwrap(); + let mut commit_bytes = Vec::new(); + ct.serialize_compressed(&mut commit_bytes).unwrap(); + + // submit commit + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_bytes.clone().try_into().unwrap(), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + + // epoch in which commit was stored + let stored_epoch = + TimelockedWeightCommits::::iter_prefix(NetUidStorageIndex::from(netuid)) + .next() + .map(|(e, _)| e) + .expect("commit stored"); + + // Place the subnet's epoch counter at the commit's reveal epoch + // (`commit_epoch + reveal_period`). The counter is the canonical epoch + // index; pin `LastEpochBlock`/`PendingEpochAt` so `should_run_epoch` stays + // false and the look-ahead does not skip past the reveal epoch. + let reveal_epoch = stored_epoch + SubtensorModule::get_reveal_period(netuid); + SubnetEpochIndex::::insert(netuid, reveal_epoch); + LastEpochBlock::::insert(netuid, SubtensorModule::get_current_block_as_u64()); + PendingEpochAt::::insert(netuid, 0); + + // run *one* block inside reveal epoch without pulse → commit should stay queued + step_block(1); + assert!( + !TimelockedWeightCommits::::get(NetUidStorageIndex::from(netuid), stored_epoch) + .is_empty(), + "commit must remain queued when pulse is missing" + ); + + // ─── insert pulse & step one more block ───────────────────────────── + let sig_bytes = hex::decode( + "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e\ + 342b73a8dd2bacbe47e4b6b63ed5e39", + ) + .unwrap(); + pallet_drand::Pulses::::insert( + reveal_round, + Pulse { + round: reveal_round, + randomness: vec![0; 32].try_into().unwrap(), + signature: sig_bytes.try_into().unwrap(), + }, + ); + + step_block(1); // automatic reveal runs here + + let weights = SubtensorModule::unnormalized_weights_sparse(netuid.into()) + .get(uid as usize) + .cloned() + .unwrap_or_default(); + assert!(!weights.is_empty(), "weights must be set after pulse"); + + assert!( + TimelockedWeightCommits::::get(NetUidStorageIndex::from(netuid), stored_epoch) + .is_empty(), + "queue should be empty after successful reveal" + ); + }); +} + +#[test] +fn test_reveal_crv3_commits_legacy_payload_success() { + new_test_ext(100).execute_with(|| { + // ───────────────────────────────────── + // 1 ▸ network + neurons + // ───────────────────────────────────── + let netuid = NetUid::from(1); + let hotkey1: AccountId = U256::from(1); + let hotkey2: AccountId = U256::from(2); + let reveal_round: u64 = 1_000; + + add_network(netuid, /*tempo*/ 5, /*modality*/ 0); + register_ok_neuron(netuid, hotkey1, U256::from(3), 100_000); + register_ok_neuron(netuid, hotkey2, U256::from(4), 100_000); + + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + assert_ok!(SubtensorModule::set_reveal_period(netuid, 3)); + + let uid1 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey1).unwrap(); + let uid2 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey2).unwrap(); + + SubtensorModule::set_validator_permit_for_uid(netuid, uid1, true); + SubtensorModule::set_validator_permit_for_uid(netuid, uid2, true); + + add_balance_to_coldkey_account(&U256::from(3), 1.into()); + add_balance_to_coldkey_account(&U256::from(4), 1.into()); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &U256::from(3), + netuid, + 1.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey2, + &U256::from(4), + netuid, + 1.into(), + ); + + // ───────────────────────────────────── + // 2 ▸ craft legacy payload (NO hotkey) + // ───────────────────────────────────── + let legacy_payload = LegacyWeightsTlockPayload { + uids: vec![uid1, uid2], + values: vec![10, 20], + version_key: SubtensorModule::get_weights_version_key(netuid), + }; + let serialized_payload = legacy_payload.encode(); + + // encrypt with TLE + let esk = [2u8; 32]; + let rng = ChaCha20Rng::seed_from_u64(0); + + let pk_bytes = hex::decode( + "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c\ + 8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb\ + 5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a", + ) + .unwrap(); + let pk = + ::PublicKeyGroup::deserialize_compressed(&*pk_bytes).unwrap(); + + let msg_hash = { + let mut h = sha2::Sha256::new(); + h.update(reveal_round.to_be_bytes()); + h.finalize().to_vec() + }; + let identity = Identity::new(b"", vec![msg_hash]); + + let ct = tle::( + pk, + esk, + &serialized_payload, + identity, + rng, + ) + .expect("encryption must succeed"); + + let mut commit_bytes = Vec::new(); + ct.serialize_compressed(&mut commit_bytes).unwrap(); + let bounded_commit: BoundedVec<_, ConstU32> = + commit_bytes.clone().try_into().unwrap(); + + // ───────────────────────────────────── + // 3 ▸ put commit on‑chain + // ───────────────────────────────────── + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey1), + netuid, + bounded_commit, + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + + // insert pulse so reveal can succeed the first time + let sig_bytes = hex::decode( + "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e3\ + 42b73a8dd2bacbe47e4b6b63ed5e39", + ) + .unwrap(); + pallet_drand::Pulses::::insert( + reveal_round, + Pulse { + round: reveal_round, + randomness: vec![0; 32].try_into().unwrap(), + signature: sig_bytes.try_into().unwrap(), + }, + ); + + let commit_block = SubtensorModule::get_current_block_as_u64(); + let commit_epoch = SubtensorModule::get_epoch_index(netuid, commit_block); + + // ───────────────────────────────────── + // 4 ▸ advance epochs to trigger reveal + // ───────────────────────────────────── + step_epochs(3, netuid); + + // ───────────────────────────────────── + // 5 ▸ assertions + // ───────────────────────────────────── + let weights_sparse = SubtensorModule::unnormalized_weights_sparse(netuid.into()); + let w1 = weights_sparse + .get(uid1 as usize) + .cloned() + .unwrap_or_default(); + assert!(!w1.is_empty(), "weights must be set for uid1"); + + // find raw values for uid1 & uid2 + let w_map: std::collections::HashMap<_, _> = w1.into_iter().collect(); + let v1 = *w_map.get(&uid1).expect("uid1 weight"); + let v2 = *w_map.get(&uid2).expect("uid2 weight"); + assert!(v2 > v1, "uid2 weight should be greater than uid1 (20 > 10)"); + + // commit should be gone + assert!( + TimelockedWeightCommits::::get(NetUidStorageIndex::from(netuid), commit_epoch) + .is_empty(), + "commit storage should be cleaned after reveal" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_subnet_owner_can_validate_without_stake_or_manual_permit --exact --show-output --nocapture diff --git a/pallets/subtensor/src/tests/weights/weight_checks.rs b/pallets/subtensor/src/tests/weights/weight_checks.rs new file mode 100644 index 0000000000..ff5226934e --- /dev/null +++ b/pallets/subtensor/src/tests/weights/weight_checks.rs @@ -0,0 +1,358 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +//! Unit tests for weight validation helpers (`check_length`, `normalize_weights`, …). + +use sp_core::U256; + +use crate::tests::mock::*; +use crate::*; + +#[test] +fn test_check_length_allows_singleton() { + new_test_ext(0).execute_with(|| { + let netuid = NetUid::from(1); + + let max_allowed: u16 = 1; + let min_allowed_weights = max_allowed; + + SubtensorModule::set_min_allowed_weights(netuid, min_allowed_weights); + + let uids: Vec = Vec::from_iter((0..max_allowed).map(|id| id + 1)); + let uid: u16 = uids[0]; + let weights: Vec = Vec::from_iter((0..max_allowed).map(|id| id + 1)); + + let expected = true; + let result = SubtensorModule::check_length(netuid, uid, &uids, &weights); + + assert_eq!(expected, result, "Failed get expected result"); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_check_length_weights_length_exceeds_min_allowed --exact --show-output --nocapture +/// Check _truthy_ path for weights within allowed range +#[test] +fn test_check_length_weights_length_exceeds_min_allowed() { + new_test_ext(0).execute_with(|| { + let netuid = NetUid::from(1); + + let max_allowed: u16 = 3; + let min_allowed_weights = max_allowed; + + SubtensorModule::set_min_allowed_weights(netuid, min_allowed_weights); + + let uids: Vec = Vec::from_iter((0..max_allowed).map(|id| id + 1)); + let uid: u16 = uids[0]; + let weights: Vec = Vec::from_iter((0..max_allowed).map(|id| id + 1)); + + let expected = true; + let result = SubtensorModule::check_length(netuid, uid, &uids, &weights); + + assert_eq!(expected, result, "Failed get expected result"); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_check_length_to_few_weights --exact --show-output --nocapture +/// Check _falsey_ path for weights outside allowed range +#[test] +fn test_check_length_to_few_weights() { + new_test_ext(0).execute_with(|| { + let netuid = NetUid::from(1); + + let min_allowed_weights = 3; + + add_network(netuid, 1, 0); + SubtensorModule::set_target_registrations_per_interval(netuid, 100); + SubtensorModule::set_max_registrations_per_block(netuid, 100); + // register morw than min allowed + register_ok_neuron(1.into(), U256::from(1), U256::from(1), 300_000); + register_ok_neuron(1.into(), U256::from(2), U256::from(2), 300_001); + register_ok_neuron(1.into(), U256::from(3), U256::from(3), 300_002); + register_ok_neuron(1.into(), U256::from(4), U256::from(4), 300_003); + register_ok_neuron(1.into(), U256::from(5), U256::from(5), 300_004); + register_ok_neuron(1.into(), U256::from(6), U256::from(6), 300_005); + register_ok_neuron(1.into(), U256::from(7), U256::from(7), 300_006); + SubtensorModule::set_min_allowed_weights(netuid, min_allowed_weights); + + let uids: Vec = Vec::from_iter((0..2).map(|id| id + 1)); + let weights: Vec = Vec::from_iter((0..2).map(|id| id + 1)); + let uid: u16 = uids[0]; + + let expected = false; + let result = SubtensorModule::check_length(netuid, uid, &uids, &weights); + + assert_eq!(expected, result, "Failed get expected result"); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_normalize_weights_does_not_mutate_when_sum_is_zero --exact --show-output --nocapture +/// Check do nothing path +#[test] +fn test_normalize_weights_does_not_mutate_when_sum_is_zero() { + new_test_ext(0).execute_with(|| { + let max_allowed: u16 = 3; + + let weights: Vec = Vec::from_iter((0..max_allowed).map(|_| 0)); + + let expected = weights.clone(); + let result = SubtensorModule::normalize_weights(weights); + + assert_eq!( + expected, result, + "Failed get expected result when everything _should_ be fine" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_normalize_weights_does_not_mutate_when_sum_not_zero --exact --show-output --nocapture +/// Check do something path +#[test] +fn test_normalize_weights_does_not_mutate_when_sum_not_zero() { + new_test_ext(0).execute_with(|| { + let max_allowed: u16 = 3; + + let weights: Vec = Vec::from_iter(0..max_allowed); + + let expected = weights.clone(); + let result = SubtensorModule::normalize_weights(weights); + + assert_eq!(expected.len(), result.len(), "Length of weights changed?!"); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_max_weight_limited_allow_self_weights_to_exceed_max_weight_limit --exact --show-output --nocapture +/// Check _truthy_ path for weights length +#[test] +fn test_max_weight_limited_allow_self_weights_to_exceed_max_weight_limit() { + new_test_ext(0).execute_with(|| { + let max_allowed: u16 = 1; + + let netuid = NetUid::from(1); + let uids: Vec = Vec::from_iter((0..max_allowed).map(|id| id + 1)); + let uid: u16 = uids[0]; + let weights: Vec = vec![0]; + + let expected = true; + let result = SubtensorModule::max_weight_limited(netuid, uid, &uids, &weights); + + assert_eq!( + expected, result, + "Failed get expected result when everything _should_ be fine" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_max_weight_limited_when_weight_limit_is_u16_max --exact --show-output --nocapture +/// Check _truthy_ path for max weight limit +#[test] +fn test_max_weight_limited_when_weight_limit_is_u16_max() { + new_test_ext(0).execute_with(|| { + let max_allowed: u16 = 3; + + let netuid = NetUid::from(1); + let uids: Vec = Vec::from_iter((0..max_allowed).map(|id| id + 1)); + let uid: u16 = uids[0]; + let weights: Vec = Vec::from_iter((0..max_allowed).map(|_id| u16::MAX)); + + let expected = true; + let result = SubtensorModule::max_weight_limited(netuid, uid, &uids, &weights); + + assert_eq!( + expected, result, + "Failed get expected result when everything _should_ be fine" + ); + }); +} + +#[test] +fn test_get_max_weight_limit_is_constant() { + new_test_ext(0).execute_with(|| { + assert_eq!( + SubtensorModule::get_max_weight_limit(NetUid::from(1)), + u16::MAX + ); + assert_eq!( + SubtensorModule::get_max_weight_limit(NetUid::ROOT), + u16::MAX + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_is_self_weight_weights_length_not_one --exact --show-output --nocapture +/// Check _falsey_ path for weights length +#[test] +fn test_is_self_weight_weights_length_not_one() { + new_test_ext(0).execute_with(|| { + let max_allowed: u16 = 3; + + let uids: Vec = Vec::from_iter((0..max_allowed).map(|id| id + 1)); + let uid: u16 = uids[0]; + let weights: Vec = Vec::from_iter((0..max_allowed).map(|id| id + 1)); + + let expected = false; + let result = SubtensorModule::is_self_weight(uid, &uids, &weights); + + assert_eq!( + expected, result, + "Failed get expected result when `weights.len() != 1`" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_is_self_weight_uid_not_in_uids --exact --show-output --nocapture +/// Check _falsey_ path for uid vs uids[0] +#[test] +fn test_is_self_weight_uid_not_in_uids() { + new_test_ext(0).execute_with(|| { + let max_allowed: u16 = 3; + + let uids: Vec = Vec::from_iter((0..max_allowed).map(|id| id + 1)); + let uid: u16 = uids[1]; + let weights: Vec = vec![0]; + + let expected = false; + let result = SubtensorModule::is_self_weight(uid, &uids, &weights); + + assert_eq!( + expected, result, + "Failed get expected result when `uid != uids[0]`" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_is_self_weight_uid_in_uids --exact --show-output --nocapture +/// Check _truthy_ path +/// @TODO: double-check if this really be desired behavior +#[test] +fn test_is_self_weight_uid_in_uids() { + new_test_ext(0).execute_with(|| { + let max_allowed: u16 = 1; + + let uids: Vec = Vec::from_iter((0..max_allowed).map(|id| id + 1)); + let uid: u16 = uids[0]; + let weights: Vec = vec![0]; + + let expected = true; + let result = SubtensorModule::is_self_weight(uid, &uids, &weights); + + assert_eq!( + expected, result, + "Failed get expected result when everything _should_ be fine" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_check_len_uids_within_allowed_within_network_pool --exact --show-output --nocapture +/// Check _truthy_ path +#[test] +fn test_check_len_uids_within_allowed_within_network_pool() { + new_test_ext(0).execute_with(|| { + let netuid = NetUid::from(1); + + let tempo: u16 = 13; + let modality: u16 = 0; + + let max_registrations_per_block: u16 = 100; + + add_network(netuid, tempo, modality); + + /* @TODO: use a loop maybe */ + register_ok_neuron(netuid, U256::from(1), U256::from(1), 0); + register_ok_neuron(netuid, U256::from(3), U256::from(3), 65555); + register_ok_neuron(netuid, U256::from(5), U256::from(5), 75555); + let max_allowed: u16 = SubtensorModule::get_subnetwork_n(netuid); + + SubtensorModule::set_max_allowed_uids(netuid, max_allowed); + SubtensorModule::set_max_registrations_per_block(netuid, max_registrations_per_block); + + let uids: Vec = Vec::from_iter(0..max_allowed); + + let expected = true; + let result = SubtensorModule::check_len_uids_within_allowed(netuid, &uids); + assert_eq!( + expected, result, + "netuid network length and uids length incompatible" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_check_len_uids_within_allowed_not_within_network_pool --exact --show-output --nocapture +#[test] +fn test_check_len_uids_within_allowed_not_within_network_pool() { + new_test_ext(0).execute_with(|| { + let netuid = NetUid::from(1); + + let tempo: u16 = 13; + let modality: u16 = 0; + + let max_registrations_per_block: u16 = 100; + + add_network(netuid, tempo, modality); + + /* @TODO: use a loop maybe */ + register_ok_neuron(netuid, U256::from(1), U256::from(1), 0); + register_ok_neuron(netuid, U256::from(3), U256::from(3), 65555); + register_ok_neuron(netuid, U256::from(5), U256::from(5), 75555); + let max_allowed: u16 = SubtensorModule::get_subnetwork_n(netuid); + + SubtensorModule::set_max_allowed_uids(netuid, max_allowed); + SubtensorModule::set_max_registrations_per_block(netuid, max_registrations_per_block); + + let uids: Vec = Vec::from_iter(0..(max_allowed + 1)); + + let expected = false; + let result = SubtensorModule::check_len_uids_within_allowed(netuid, &uids); + assert_eq!( + expected, result, + "Failed to detect incompatible uids for network" + ); + }); +} + +// `get_first_block_of_epoch` is a legacy modulo helper — NOT used by live +// commit-reveal logic +#[test] +fn test_get_first_block_of_epoch_epoch_zero() { + new_test_ext(1).execute_with(|| { + let netuid: NetUid = NetUid::from(1); + add_network(netuid, 10, 0); + + // 0 * 11 - 2, saturating at 0. + assert_eq!(SubtensorModule::get_first_block_of_epoch(netuid, 0), 0); + }); +} + +#[test] +fn test_get_first_block_of_epoch_small_epoch() { + new_test_ext(1).execute_with(|| { + let netuid: NetUid = NetUid::from(0); + add_network(netuid, 1, 0); + + // 1 * 2 - 1 = 1. + assert_eq!(SubtensorModule::get_first_block_of_epoch(netuid, 1), 1); + }); +} + +#[test] +fn test_get_first_block_of_epoch_with_offset() { + new_test_ext(1).execute_with(|| { + let netuid: NetUid = NetUid::from(1); + add_network(netuid, 10, 0); + + // 1 * 11 - 2 = 9. + assert_eq!(SubtensorModule::get_first_block_of_epoch(netuid, 1), 9); + }); +} + +#[test] +fn test_get_first_block_of_epoch_large_epoch() { + new_test_ext(1).execute_with(|| { + let netuid: NetUid = NetUid::from(0); + add_network(netuid, 100, 0); + + let epoch: u64 = 1000; + // 1000 * 101 - 1. + assert_eq!( + SubtensorModule::get_first_block_of_epoch(netuid, epoch), + epoch * 101 - 1 + ); + }); +} diff --git a/pallets/subtensor/src/utils/cleanup.rs b/pallets/subtensor/src/utils/cleanup.rs index f8421010f9..5290982eee 100644 --- a/pallets/subtensor/src/utils/cleanup.rs +++ b/pallets/subtensor/src/utils/cleanup.rs @@ -1,6 +1,17 @@ +//! Weight-metered deletion of storage entries keyed by `netuid`. +//! +//! Used by subnet dissolution and stake-cleanup paths that must stop when the +//! remaining weight budget cannot cover another read or write. + use super::*; impl Pallet { + /// Scan `iter`, collect keys for items matching `netuid`, then delete them. + /// + /// Removals are deferred until after the scan so mutating storage while + /// iterating the same prefix is safe. Returns `(read_all, last_item)` where + /// `read_all` is `false` if the weight meter ran out mid-scan (caller should + /// resume later from `last_item`). pub fn remove_storage_entries_for_netuid( weight_meter: &mut WeightMeter, iter: I, @@ -13,32 +24,32 @@ impl Pallet { I: Iterator, I::Item: Clone, { - let r = T::DbWeight::get().reads(1); - let w = T::DbWeight::get().writes(writes_per_match); + let read_weight = T::DbWeight::get().reads(1); + let write_weight = T::DbWeight::get().writes(writes_per_match); let mut read_all = true; - let mut to_rm: sp_std::vec::Vec = sp_std::vec::Vec::new(); + let mut keys_to_remove: sp_std::vec::Vec = sp_std::vec::Vec::new(); let mut last_item = None; for item in iter { - if !weight_meter.can_consume(r) { + if !weight_meter.can_consume(read_weight) { read_all = false; break; } - weight_meter.consume(r); + weight_meter.consume(read_weight); if matches_netuid(&item) { - if !weight_meter.can_consume(w) { + if !weight_meter.can_consume(write_weight) { read_all = false; break; } - weight_meter.consume(w); + weight_meter.consume(write_weight); - to_rm.push(key_from_item(item.clone())); + keys_to_remove.push(key_from_item(item.clone())); } last_item = Some(item); } - for hot in to_rm { - ops_based_on_key(&hot); + for key in keys_to_remove { + ops_based_on_key(&key); } (read_all, last_item) diff --git a/pallets/subtensor/src/utils/evm.rs b/pallets/subtensor/src/utils/evm.rs index d01654a044..3fd72b6520 100644 --- a/pallets/subtensor/src/utils/evm.rs +++ b/pallets/subtensor/src/utils/evm.rs @@ -1,3 +1,12 @@ +//! Hotkey ↔ EVM address association: EIP-191 recover, forward map, reverse index. +//! +//! Storage: +//! - [`AssociatedEvmAddress`] — `(netuid, uid) → (H160, block)` +//! - [`AssociatedUidsByEvmAddress`] — `(netuid, H160) → [(uid, block), …]` (capped) +//! +//! Agents often land here via `do_associate_evm_key`, `uid_lookup`, or the EVM +//! `UidLookup` precompile (which wraps [`Self::uid_lookup`]). + use super::*; use alloc::string::ToString; use frame_support::ensure; @@ -8,14 +17,16 @@ use sp_std::collections::btree_map::BTreeMap; use sp_std::vec::Vec; use subtensor_runtime_common::NetUid; -const MESSAGE_PREFIX: &str = "\x19Ethereum Signed Message:\n"; +/// Ethereum personal_sign / EIP-191 prefix (`"\x19Ethereum Signed Message:\n"`). +const EIP191_MESSAGE_PREFIX: &str = "\x19Ethereum Signed Message:\n"; impl Pallet { + /// Keccak-256 of the EIP-191 personal_sign wrapper around `message`. pub(crate) fn hash_message_eip191>(message: M) -> [u8; 32] { let msg_len = message.as_ref().len().to_string(); keccak_256( &[ - MESSAGE_PREFIX.as_bytes(), + EIP191_MESSAGE_PREFIX.as_bytes(), msg_len.as_bytes(), message.as_ref(), ] @@ -51,7 +62,7 @@ impl Pallet { mut signature: Signature, ) -> dispatch::DispatchResult { let hotkey = ensure_signed(origin)?; - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); ensure!( Self::get_owning_coldkey_for_hotkey(&hotkey) != DefaultAccount::::get(), Error::::NonAssociatedColdKey @@ -121,6 +132,10 @@ impl Pallet { Ok(()) } + /// Write forward map + reverse index for `(netuid, uid) ↔ evm_key` at `block_associated`. + /// + /// If the UID already pointed at a different EVM key, that UID is removed from the old + /// reverse-index bucket before the new association is upserted. pub fn set_associated_evm_address( netuid: NetUid, uid: u16, @@ -137,12 +152,14 @@ impl Pallet { AssociatedEvmAddress::::insert(netuid, uid, (evm_key, block_associated)); } + /// Remove the association for `(netuid, uid)` from both forward and reverse maps. pub fn remove_associated_evm_address(netuid: NetUid, uid: u16) { if let Some((evm_key, _)) = AssociatedEvmAddress::::take(netuid, uid) { Self::remove_uid_from_evm_address_index(netuid, evm_key, uid); } } + /// Wipe all EVM associations for `netuid` (subnet dissolve / full reset). pub fn clear_associated_evm_addresses(netuid: NetUid) { let _ = AssociatedEvmAddress::::clear_prefix(netuid, u32::MAX, None); let _ = AssociatedUidsByEvmAddress::::clear_prefix(netuid, u32::MAX, None); @@ -220,6 +237,9 @@ impl Pallet { }); } + /// UIDs associated with `evm_key` on `netuid`, oldest association block first, capped at `limit`. + /// + /// Backing storage is [`AssociatedUidsByEvmAddress`]. Wrapped by the `UidLookup` precompile. pub fn uid_lookup(netuid: NetUid, evm_key: H160, limit: u16) -> Vec<(u16, u64)> { let mut ret_val = AssociatedUidsByEvmAddress::::get(netuid, evm_key) .into_iter() @@ -229,6 +249,7 @@ impl Pallet { ret_val } + /// Reject re-association of `uid` on `netuid` until [`T::EvmKeyAssociateRateLimit`] blocks elapse. pub fn ensure_evm_key_associate_rate_limit(netuid: NetUid, uid: u16) -> DispatchResult { let now = Self::get_current_block_as_u64(); let block_associated = match AssociatedEvmAddress::::get(netuid, uid) { diff --git a/pallets/subtensor/src/utils/identity.rs b/pallets/subtensor/src/utils/identity.rs index f41e3480af..55622a875d 100644 --- a/pallets/subtensor/src/utils/identity.rs +++ b/pallets/subtensor/src/utils/identity.rs @@ -1,3 +1,8 @@ +//! Coldkey ([`IdentitiesV2`]) and subnet ([`SubnetIdentitiesV3`]) identity writes. +//! +//! Extrinsic bodies live in `lib.rs`; these `do_*` helpers perform ownership checks, +//! field-length validation, and storage + event emission. + use super::*; use frame_support::ensure; use frame_system::ensure_signed; @@ -5,25 +10,10 @@ use sp_std::vec::Vec; use subtensor_runtime_common::NetUid; impl Pallet { - /// Sets the identity for a coldkey. - /// - /// This function allows a user to set or update their identity information associated with their coldkey. - /// It checks if the caller has at least one registered hotkey, validates the provided identity information, - /// and then stores it in the blockchain state. - /// - /// # Arguments - /// - /// * `origin`: The origin of the call, which should be a signed extrinsic. - /// * `name`: The name to be associated with the identity. - /// * `url`: A URL associated with the identity. - /// * `image`: An image URL or identifier for the identity. - /// * `discord`: Discord information for the identity. - /// * `description`: A description of the identity. - /// * `additional`: Any additional information for the identity. - /// - /// # Returns + /// Set or replace the caller's coldkey identity in [`IdentitiesV2`]. /// - /// Returns `Ok(())` if the identity is successfully set, otherwise returns an error. + /// Requires at least one owned hotkey registered on any subnet. Field bytes are + /// validated by [`Self::is_valid_identity`] before insert; emits [`Event::ChainIdentitySet`]. pub fn do_set_identity( origin: OriginFor, name: Vec, @@ -78,23 +68,10 @@ impl Pallet { Ok(()) } - /// Sets the identity for a subnet. - /// - /// This function allows the owner of a subnet to set or update the identity information associated with the subnet. - /// It verifies that the caller is the owner of the specified subnet, validates the provided identity information, - /// and then stores it in the blockchain state. - /// - /// # Arguments - /// - /// * `origin`: The origin of the call, which should be a signed extrinsic. - /// * `netuid`: The unique identifier for the subnet. - /// * `subnet_name`: The name of the subnet to be associated with the identity. - /// * `github_repo`: The GitHub repository URL associated with the subnet identity. - /// * `subnet_contact`: Contact information for the subnet. - /// - /// # Returns + /// Set or replace a subnet's identity in [`SubnetIdentitiesV3`]. /// - /// Returns `Ok(())` if the subnet identity is successfully set, otherwise returns an error. + /// Caller must be [`SubnetOwner`] for `netuid`. Validated by + /// [`Self::is_valid_subnet_identity`]; emits [`Event::SubnetIdentitySet`]. pub fn do_set_subnet_identity( origin: OriginFor, netuid: NetUid, @@ -109,7 +86,7 @@ impl Pallet { ) -> dispatch::DispatchResult { // Ensure the call is signed and get the signer's (coldkey) account let coldkey = ensure_signed(origin)?; - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); // Ensure that the coldkey owns the subnet ensure!( @@ -148,19 +125,11 @@ impl Pallet { Ok(()) } - /// Validates the given ChainIdentityOf struct. + /// Per-field and aggregate byte limits for [`ChainIdentityOfV2`]. /// - /// This function checks if the total length of all fields in the ChainIdentityOf struct - /// is less than or equal to 512 bytes, and if each individual field is also - /// less than or equal to 512 bytes. - /// - /// # Arguments - /// - /// * `identity`: A reference to the ChainIdentityOf struct to be validated. - /// - /// # Returns - /// - /// * `bool`: Returns true if the Identity is valid, false otherwise. + /// Individual caps: name/url/github_repo/discord ≤ 256; image/description/additional ≤ 1024. + /// The aggregate check sums name+url+image+discord+description+additional against 4096 and + /// deliberately omits `github_repo` from that sum (still enforced by its per-field cap). pub fn is_valid_identity(identity: &ChainIdentityOfV2) -> bool { let total_length = identity .name @@ -189,19 +158,11 @@ impl Pallet { && identity.additional.len() <= 1024 } - /// Validates the given SubnetIdentityOfV3 struct. - /// - /// This function checks if the total length of all fields in the SubnetIdentityOfV3 struct - /// is less than or equal to 2304 bytes, and if each individual field is also - /// within its respective maximum byte limit. - /// - /// # Arguments - /// - /// * `identity`: A reference to the SubnetIdentityOfV3 struct to be validated. - /// - /// # Returns + /// Per-field and aggregate byte limits for [`SubnetIdentityOfV3`]. /// - /// * `bool`: Returns true if the SubnetIdentityV3 is valid, false otherwise. + /// Individual caps: subnet_name/discord ≤ 256; remaining string fields ≤ 1024. + /// Aggregate check sums only subnet_name+github_repo+subnet_contact against 5632; + /// other fields are enforced solely by their per-field caps. pub fn is_valid_subnet_identity(identity: &SubnetIdentityOfV3) -> bool { let total_length = identity .subnet_name diff --git a/pallets/subtensor/src/utils/misc/consensus_params.rs b/pallets/subtensor/src/utils/misc/consensus_params.rs new file mode 100644 index 0000000000..0d394f3692 --- /dev/null +++ b/pallets/subtensor/src/utils/misc/consensus_params.rs @@ -0,0 +1,203 @@ +//! Yuma-consensus vector getters/setters and per-UID consensus fields. +use super::*; +use sp_core::U256; +use sp_runtime::PerU16; +use subtensor_runtime_common::{AlphaBalance, NetUid, NetUidStorageIndex}; + +impl Pallet { + // ============================== + // ==== YumaConsensus params ==== + // ============================== + /// Deprecated: Rank is no longer computed during epoch. Always returns empty. + pub fn get_rank(_netuid: NetUid) -> Vec { + Vec::new() + } + /// Deprecated: Trust is no longer computed during epoch. Always returns empty. + pub fn get_trust(_netuid: NetUid) -> Vec { + Vec::new() + } + pub fn get_active(netuid: NetUid) -> Vec { + Active::::get(netuid) + } + pub fn get_emission(netuid: NetUid) -> Vec { + Emission::::get(netuid) + } + pub fn get_consensus(netuid: NetUid) -> Vec { + Consensus::::get(netuid) + .into_iter() + .map(PerU16::deconstruct) + .collect() + } + pub fn get_incentive(netuid: NetUidStorageIndex) -> Vec { + Incentive::::get(netuid) + .into_iter() + .map(PerU16::deconstruct) + .collect() + } + pub fn get_dividends(netuid: NetUid) -> Vec { + Dividends::::get(netuid) + .into_iter() + .map(PerU16::deconstruct) + .collect() + } + /// Fetch LastUpdate for `netuid` and ensure its length is at least `get_subnetwork_n(netuid)`, + /// padding with zeros if needed. Returns the (possibly padded) vector. + pub fn get_last_update(netuid_index: NetUidStorageIndex) -> Vec { + let netuid = Self::netuid_from_mechanism_storage_index(netuid_index); + let target_len = Self::get_subnetwork_n(netuid) as usize; + let mut v = LastUpdate::::get(netuid_index); + if v.len() < target_len { + v.resize(target_len, 0); + } + v + } + /// Deprecated: PruningScores is no longer computed during epoch. Always returns empty. + pub fn get_pruning_score(_netuid: NetUid) -> Vec { + Vec::new() + } + pub fn get_validator_trust(netuid: NetUid) -> Vec { + ValidatorTrust::::get(netuid) + .into_iter() + .map(PerU16::deconstruct) + .collect() + } + pub fn get_validator_permit(netuid: NetUid) -> Vec { + ValidatorPermit::::get(netuid) + } + + // ================================== + // ==== YumaConsensus UID params ==== + // ================================== + pub fn set_last_update_for_uid(netuid: NetUidStorageIndex, uid: u16, last_update: u64) { + let mut updated_last_update_vec = Self::get_last_update(netuid); + let Some(updated_last_update) = updated_last_update_vec.get_mut(uid as usize) else { + return; + }; + *updated_last_update = last_update; + LastUpdate::::insert(netuid, updated_last_update_vec); + } + pub fn set_active_for_uid(netuid: NetUid, uid: u16, active: bool) { + let mut updated_active_vec = Self::get_active(netuid); + let Some(updated_active) = updated_active_vec.get_mut(uid as usize) else { + return; + }; + *updated_active = active; + Active::::insert(netuid, updated_active_vec); + } + pub fn set_validator_permit_for_uid(netuid: NetUid, uid: u16, validator_permit: bool) { + let mut updated_validator_permits = Self::get_validator_permit(netuid); + let Some(updated_validator_permit) = updated_validator_permits.get_mut(uid as usize) else { + return; + }; + *updated_validator_permit = validator_permit; + ValidatorPermit::::insert(netuid, updated_validator_permits); + } + pub fn set_stake_threshold(min_stake: u64) { + StakeThreshold::::put(min_stake); + Self::deposit_event(Event::StakeThresholdSet(min_stake)); + } + + /// Deprecated: Rank is no longer computed. Always returns 0. + pub fn get_rank_for_uid(_netuid: NetUid, _uid: u16) -> u16 { + 0 + } + /// Deprecated: Trust is no longer computed. Always returns 0. + pub fn get_trust_for_uid(_netuid: NetUid, _uid: u16) -> u16 { + 0 + } + pub fn get_emission_for_uid(netuid: NetUid, uid: u16) -> AlphaBalance { + let vec = Emission::::get(netuid); + vec.get(uid as usize).copied().unwrap_or_default() + } + pub fn get_active_for_uid(netuid: NetUid, uid: u16) -> bool { + let vec = Active::::get(netuid); + vec.get(uid as usize).copied().unwrap_or(false) + } + pub fn get_consensus_for_uid(netuid: NetUid, uid: u16) -> u16 { + let vec = Consensus::::get(netuid); + vec.get(uid as usize) + .copied() + .unwrap_or_default() + .deconstruct() + } + pub fn get_incentive_for_uid(netuid: NetUidStorageIndex, uid: u16) -> u16 { + let vec = Incentive::::get(netuid); + vec.get(uid as usize) + .copied() + .unwrap_or_default() + .deconstruct() + } + pub fn get_dividends_for_uid(netuid: NetUid, uid: u16) -> u16 { + let vec = Dividends::::get(netuid); + vec.get(uid as usize) + .copied() + .unwrap_or_default() + .deconstruct() + } + pub fn get_last_update_for_uid(netuid: NetUidStorageIndex, uid: u16) -> u64 { + let vec = LastUpdate::::get(netuid); + vec.get(uid as usize).copied().unwrap_or(0) + } + /// Deprecated: PruningScores is no longer computed. Always returns u16::MAX. + pub fn get_pruning_score_for_uid(_netuid: NetUid, _uid: u16) -> u16 { + u16::MAX + } + pub fn get_validator_trust_for_uid(netuid: NetUid, uid: u16) -> u16 { + let vec = ValidatorTrust::::get(netuid); + vec.get(uid as usize) + .copied() + .unwrap_or_default() + .deconstruct() + } + pub fn get_validator_permit_for_uid(netuid: NetUid, uid: u16) -> bool { + let vec = ValidatorPermit::::get(netuid); + vec.get(uid as usize).copied().unwrap_or(false) + } + pub fn get_stake_threshold() -> u64 { + StakeThreshold::::get() + } + + // ============================ + // ==== Subnetwork Getters ==== + // ============================ + pub fn get_tempo(netuid: NetUid) -> u16 { + Tempo::::get(netuid) + } + pub fn get_last_adjustment_block(netuid: NetUid) -> u64 { + LastAdjustmentBlock::::get(netuid) + } + pub fn get_blocks_since_last_step(netuid: NetUid) -> u64 { + BlocksSinceLastStep::::get(netuid) + } + pub fn get_difficulty(netuid: NetUid) -> U256 { + U256::from(Self::get_difficulty_as_u64(netuid)) + } + pub fn get_registrations_this_block(netuid: NetUid) -> u16 { + RegistrationsThisBlock::::get(netuid) + } + pub fn get_last_mechanism_step_block(netuid: NetUid) -> u64 { + LastMechansimStepBlock::::get(netuid) + } + pub fn get_registrations_this_interval(netuid: NetUid) -> u16 { + RegistrationsThisInterval::::get(netuid) + } + pub fn get_pow_registrations_this_interval(netuid: NetUid) -> u16 { + POWRegistrationsThisInterval::::get(netuid) + } + pub fn get_burn_registrations_this_interval(netuid: NetUid) -> u16 { + BurnRegistrationsThisInterval::::get(netuid) + } + pub fn get_neuron_block_at_registration(netuid: NetUid, neuron_uid: u16) -> u64 { + BlockAtRegistration::::get(netuid, neuron_uid) + } + /// Returns the minimum number of non-immortal & non-immune UIDs that must remain in a subnet. + pub fn get_min_non_immune_uids(netuid: NetUid) -> u16 { + MinNonImmuneUids::::get(netuid) + } + + /// Sets the minimum number of non-immortal & non-immune UIDs that must remain in a subnet. + pub fn set_min_non_immune_uids(netuid: NetUid, min: u16) { + MinNonImmuneUids::::insert(netuid, min); + Self::deposit_event(Event::MinNonImmuneUidsSet(netuid, min)); + } +} diff --git a/pallets/subtensor/src/utils/misc/mod.rs b/pallets/subtensor/src/utils/misc/mod.rs new file mode 100644 index 0000000000..fd9c5c050f --- /dev/null +++ b/pallets/subtensor/src/utils/misc/mod.rs @@ -0,0 +1,18 @@ +//! Grab-bag of pallet helpers shared across staking, epoch, admin-utils, and extrinsics. +//! +//! Prefer searching the concept modules below rather than this directory name: +//! - [`origin_and_admin`] — owner/root origins, admin freeze window, owner RL recording +//! - [`tempo_and_counters`] — tempo, registration counters, [`Pallet::get_current_block_as_u64`] +//! - [`consensus_params`] — emission/consensus/incentive vector accessors +//! - [`take_and_locks`] — take ownership checks, subnet locked TAO +//! - [`subnet_hyperparams`] — burn/difficulty/weights/owner-cut/… getters & setters +//! - [`q32_math`] — Q32 fixed-point multiply / pow / half-life decay + +use super::*; + +pub mod consensus_params; +pub mod origin_and_admin; +pub mod q32_math; +pub mod subnet_hyperparams; +pub mod take_and_locks; +pub mod tempo_and_counters; diff --git a/pallets/subtensor/src/utils/misc/origin_and_admin.rs b/pallets/subtensor/src/utils/misc/origin_and_admin.rs new file mode 100644 index 0000000000..982b468cc9 --- /dev/null +++ b/pallets/subtensor/src/utils/misc/origin_and_admin.rs @@ -0,0 +1,111 @@ +//! Subnet-owner / root origin checks, admin freeze window, and owner rate-limit recording. +use super::*; +use crate::Error; +use crate::system::{ensure_signed, ensure_signed_or_root}; +use subtensor_runtime_common::NetUid; + +impl Pallet { + /// Allow root (`None`) or the [`SubnetOwner`] coldkey (`Some`) for `netuid`. + pub fn ensure_subnet_owner_or_root( + origin: OriginFor, + netuid: NetUid, + ) -> Result, DispatchError> { + let coldkey = ensure_signed_or_root(origin); + match coldkey { + Ok(Some(who)) if SubnetOwner::::get(netuid) == who => Ok(Some(who)), + Ok(Some(_)) => Err(DispatchError::BadOrigin), + Ok(None) => Ok(None), + Err(x) => Err(x.into()), + } + } + + /// Require the signed origin to be [`SubnetOwner`] for `netuid`. + pub fn ensure_subnet_owner( + origin: OriginFor, + netuid: NetUid, + ) -> Result { + let coldkey = ensure_signed(origin); + match coldkey { + Ok(who) if SubnetOwner::::get(netuid) == who => Ok(who), + Ok(_) => Err(DispatchError::BadOrigin), + Err(x) => Err(x.into()), + } + } + + /// Owner-or-root gate that also enforces each [`TransactionType`] rate limit for the owner. + /// + /// Root bypasses rate checks. Prefer renaming to `ensure_subnet_owner_or_root_with_limits` + /// (see `refactor/rename-proposals.md`). + pub fn ensure_subnet_owner_or_root_with_limits( + origin: OriginFor, + netuid: NetUid, + limits: &[crate::utils::rate_limiting::TransactionType], + ) -> Result, DispatchError> { + let maybe_who = Self::ensure_subnet_owner_or_root(origin, netuid)?; + if let Some(who) = maybe_who.as_ref() { + for tx in limits.iter() { + ensure!( + tx.passes_rate_limit_on_subnet::(who, netuid), + Error::::TxRateLimitExceeded + ); + } + } + Ok(maybe_who) + } + + /// Returns true if the current block is within the terminal freeze window of the tempo for the + /// given subnet. During this window, admin ops are prohibited to avoid interference with + /// validator weight submissions. Engages immediately on a pending manual trigger (so the trigger + /// arms the freeze for the entire countdown to `PendingEpochAt`). + pub fn is_in_admin_freeze_window(netuid: NetUid, current_block: u64) -> bool { + let tempo = Self::get_tempo(netuid); + if tempo == 0 { + return false; + } + let pending = PendingEpochAt::::get(netuid); + if pending > 0 && pending > current_block { + return true; + } + let remaining = Self::blocks_until_next_auto_epoch(netuid, tempo, current_block); + let window = AdminFreezeWindow::::get() as u64; + remaining < window + } + + /// Ensures the admin freeze window is not currently active for the given subnet. + pub fn ensure_admin_window_open(netuid: NetUid) -> Result<(), DispatchError> { + let now = Self::get_current_block_as_u64(); + ensure!( + !Self::is_in_admin_freeze_window(netuid, now), + Error::::AdminActionProhibitedDuringWeightsWindow + ); + Ok(()) + } + + /// Set the global admin-freeze window length in blocks (weights-submission quiet period). + pub fn set_admin_freeze_window(window: u16) { + AdminFreezeWindow::::set(window); + Self::deposit_event(Event::AdminFreezeWindowSet(window)); + } + + /// Set how many tempos an owner must wait between hyperparameter updates. + pub fn set_owner_hyperparam_rate_limit(epochs: u16) { + OwnerHyperparamRateLimit::::set(epochs); + Self::deposit_event(Event::OwnerHyperparamRateLimitSet(epochs)); + } + + /// If `maybe_owner` is `Some`, stamp `txs` last-block markers on `netuid` at the current block. + /// + /// Prefer renaming to `record_owner_rate_limits` (see `refactor/rename-proposals.md`). + pub fn record_owner_rate_limits( + maybe_owner: Option<::AccountId>, + netuid: NetUid, + txs: &[TransactionType], + ) { + if let Some(who) = maybe_owner { + let now = Self::get_current_block_as_u64(); + for tx in txs { + tx.set_last_block_on_subnet::(&who, netuid, now); + } + } + } +} diff --git a/pallets/subtensor/src/utils/misc/q32_math.rs b/pallets/subtensor/src/utils/misc/q32_math.rs new file mode 100644 index 0000000000..383cceca54 --- /dev/null +++ b/pallets/subtensor/src/utils/misc/q32_math.rs @@ -0,0 +1,72 @@ +//! Saturating Q32 fixed-point multiply, power, and half-life decay helpers. +use super::*; + +impl Pallet { + /// Multiply an integer `value` by a Q32 fixed-point factor. + /// + /// Q32 means: + /// 1.0 == 1 << 32 + /// 0.5 == 1 << 31 + /// + /// Safe / non-panicking: + /// * uses saturating u128 multiplication + /// * clamps back into u64 range + pub fn mul_by_q32(value: u64, factor_q32: u64) -> u64 { + let product: u128 = (value as u128).saturating_mul(factor_q32 as u128); + let shifted: u128 = product >> 32; + core::cmp::min(shifted, u64::MAX as u128) as u64 + } + + /// Exponentiation-by-squaring for Q32 values. + /// + /// Returns `base_q32 ^ exp` in Q32. + /// + /// Safe / non-panicking: + /// * uses `mul_by_q32`, which is saturating/clamped + pub fn pow_q32(base_q32: u64, exp: u16) -> u64 { + let mut result: u64 = 1u64 << 32; // 1.0 in Q32 + let mut factor: u64 = base_q32; + let mut power: u32 = u32::from(exp); + + while power > 0 { + if (power & 1) == 1 { + result = Self::mul_by_q32(result, factor); + } + + power >>= 1; + + if power > 0 { + factor = Self::mul_by_q32(factor, factor); + } + } + + result + } + + /// Returns the per-block decay factor `f` in Q32 + pub fn decay_factor_q32(half_life: u16) -> u64 { + if half_life == 0 { + return 1u64 << 32; // 1.0 + } + + let one_q32: u64 = 1u64 << 32; + let half_q32: u64 = 1u64 << 31; // 0.5 + + let mut lo: u64 = 0; + let mut hi: u64 = one_q32; + + while lo.saturating_add(1) < hi { + let span: u64 = hi.saturating_sub(lo); + let mid: u64 = lo.saturating_add(span >> 1); + let mid_pow: u64 = Self::pow_q32(mid, half_life); + + if mid_pow > half_q32 { + hi = mid; + } else { + lo = mid; + } + } + + lo + } +} diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc/subnet_hyperparams.rs similarity index 57% rename from pallets/subtensor/src/utils/misc.rs rename to pallets/subtensor/src/utils/misc/subnet_hyperparams.rs index 4300f1a806..afac26956b 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc/subnet_hyperparams.rs @@ -1,402 +1,14 @@ +//! Subnet and global hyperparameter getters/setters used by admin-utils and extrinsics. use super::*; use crate::Error; -use crate::system::{ensure_signed, ensure_signed_or_root, pallet_prelude::BlockNumberFor}; +use crate::system::pallet_prelude::BlockNumberFor; use safe_math::*; use sp_core::Get; -use sp_core::U256; -use sp_runtime::{PerU16, Saturating}; +use sp_runtime::PerU16; use substrate_fixed::types::{I32F32, I64F64, U64F64, U96F32}; -use subtensor_runtime_common::{AlphaBalance, NetUid, NetUidStorageIndex, TaoBalance}; +use subtensor_runtime_common::{NetUid, TaoBalance}; impl Pallet { - pub fn ensure_subnet_owner_or_root( - o: OriginFor, - netuid: NetUid, - ) -> Result, DispatchError> { - let coldkey = ensure_signed_or_root(o); - match coldkey { - Ok(Some(who)) if SubnetOwner::::get(netuid) == who => Ok(Some(who)), - Ok(Some(_)) => Err(DispatchError::BadOrigin), - Ok(None) => Ok(None), - Err(x) => Err(x.into()), - } - } - - pub fn ensure_subnet_owner( - o: OriginFor, - netuid: NetUid, - ) -> Result { - let coldkey = ensure_signed(o); - match coldkey { - Ok(who) if SubnetOwner::::get(netuid) == who => Ok(who), - Ok(_) => Err(DispatchError::BadOrigin), - Err(x) => Err(x.into()), - } - } - - /// Ensure owner-or-root with a set of TransactionType rate checks (owner only). - pub fn ensure_sn_owner_or_root_with_limits( - o: OriginFor, - netuid: NetUid, - limits: &[crate::utils::rate_limiting::TransactionType], - ) -> Result, DispatchError> { - let maybe_who = Self::ensure_subnet_owner_or_root(o, netuid)?; - if let Some(who) = maybe_who.as_ref() { - for tx in limits.iter() { - ensure!( - tx.passes_rate_limit_on_subnet::(who, netuid), - Error::::TxRateLimitExceeded - ); - } - } - Ok(maybe_who) - } - - /// Returns true if the current block is within the terminal freeze window of the tempo for the - /// given subnet. During this window, admin ops are prohibited to avoid interference with - /// validator weight submissions. Engages immediately on a pending manual trigger (so the trigger - /// arms the freeze for the entire countdown to `PendingEpochAt`). - pub fn is_in_admin_freeze_window(netuid: NetUid, current_block: u64) -> bool { - let tempo = Self::get_tempo(netuid); - if tempo == 0 { - return false; - } - let pending = PendingEpochAt::::get(netuid); - if pending > 0 && pending > current_block { - return true; - } - let remaining = Self::blocks_until_next_auto_epoch(netuid, tempo, current_block); - let window = AdminFreezeWindow::::get() as u64; - remaining < window - } - - /// Ensures the admin freeze window is not currently active for the given subnet. - pub fn ensure_admin_window_open(netuid: NetUid) -> Result<(), DispatchError> { - let now = Self::get_current_block_as_u64(); - ensure!( - !Self::is_in_admin_freeze_window(netuid, now), - Error::::AdminActionProhibitedDuringWeightsWindow - ); - Ok(()) - } - - pub fn set_admin_freeze_window(window: u16) { - AdminFreezeWindow::::set(window); - Self::deposit_event(Event::AdminFreezeWindowSet(window)); - } - - pub fn set_owner_hyperparam_rate_limit(epochs: u16) { - OwnerHyperparamRateLimit::::set(epochs); - Self::deposit_event(Event::OwnerHyperparamRateLimitSet(epochs)); - } - - /// If owner is `Some`, record last-blocks for the provided `TransactionType`s. - pub fn record_owner_rl( - maybe_owner: Option<::AccountId>, - netuid: NetUid, - txs: &[TransactionType], - ) { - if let Some(who) = maybe_owner { - let now = Self::get_current_block_as_u64(); - for tx in txs { - tx.set_last_block_on_subnet::(&who, netuid, now); - } - } - } - - // ======================== - // ==== Global Setters ==== - // ======================== - /// Unchecked tempo write used by tests, precompiles, and internal helpers. - /// Does NOT reset `LastEpochBlock` — that is the responsibility of - /// `AdminUtils::sudo_set_tempo` (owner-or-root), which performs the cycle - /// reset explicitly via `apply_tempo_with_cycle_reset`. - pub fn set_tempo_unchecked(netuid: NetUid, tempo: u16) { - Tempo::::insert(netuid, tempo); - Self::deposit_event(Event::TempoSet(netuid, tempo)); - } - - /// Sets `Tempo` and resets the state-based scheduler anchor `LastEpochBlock` - /// to the current block - pub fn apply_tempo_with_cycle_reset(netuid: NetUid, tempo: u16) { - Self::set_tempo_unchecked(netuid, tempo); - let now = Self::get_current_block_as_u64(); - LastEpochBlock::::insert(netuid, now); - } - - pub fn set_last_adjustment_block(netuid: NetUid, last_adjustment_block: u64) { - LastAdjustmentBlock::::insert(netuid, last_adjustment_block); - } - pub fn set_blocks_since_last_step(netuid: NetUid, blocks_since_last_step: u64) { - BlocksSinceLastStep::::insert(netuid, blocks_since_last_step); - } - pub fn set_registrations_this_block(netuid: NetUid, registrations_this_block: u16) { - RegistrationsThisBlock::::insert(netuid, registrations_this_block); - } - pub fn set_last_mechanism_step_block(netuid: NetUid, last_mechanism_step_block: u64) { - LastMechansimStepBlock::::insert(netuid, last_mechanism_step_block); - } - pub fn set_registrations_this_interval(netuid: NetUid, registrations_this_interval: u16) { - RegistrationsThisInterval::::insert(netuid, registrations_this_interval); - } - pub fn set_pow_registrations_this_interval( - netuid: NetUid, - pow_registrations_this_interval: u16, - ) { - POWRegistrationsThisInterval::::insert(netuid, pow_registrations_this_interval); - } - pub fn set_burn_registrations_this_interval( - netuid: NetUid, - burn_registrations_this_interval: u16, - ) { - BurnRegistrationsThisInterval::::insert(netuid, burn_registrations_this_interval); - } - - // ======================== - // ==== Global Getters ==== - // ======================== - pub fn get_current_block_as_u64() -> u64 { - TryInto::try_into(>::block_number()) - .ok() - .expect("blockchain will not exceed 2^64 blocks; QED.") - } - - // ============================== - // ==== YumaConsensus params ==== - // ============================== - /// Deprecated: Rank is no longer computed during epoch. Always returns empty. - pub fn get_rank(_netuid: NetUid) -> Vec { - Vec::new() - } - /// Deprecated: Trust is no longer computed during epoch. Always returns empty. - pub fn get_trust(_netuid: NetUid) -> Vec { - Vec::new() - } - pub fn get_active(netuid: NetUid) -> Vec { - Active::::get(netuid) - } - pub fn get_emission(netuid: NetUid) -> Vec { - Emission::::get(netuid) - } - pub fn get_consensus(netuid: NetUid) -> Vec { - Consensus::::get(netuid) - .into_iter() - .map(PerU16::deconstruct) - .collect() - } - pub fn get_incentive(netuid: NetUidStorageIndex) -> Vec { - Incentive::::get(netuid) - .into_iter() - .map(PerU16::deconstruct) - .collect() - } - pub fn get_dividends(netuid: NetUid) -> Vec { - Dividends::::get(netuid) - .into_iter() - .map(PerU16::deconstruct) - .collect() - } - /// Fetch LastUpdate for `netuid` and ensure its length is at least `get_subnetwork_n(netuid)`, - /// padding with zeros if needed. Returns the (possibly padded) vector. - pub fn get_last_update(netuid_index: NetUidStorageIndex) -> Vec { - let netuid = Self::get_netuid(netuid_index); - let target_len = Self::get_subnetwork_n(netuid) as usize; - let mut v = LastUpdate::::get(netuid_index); - if v.len() < target_len { - v.resize(target_len, 0); - } - v - } - /// Deprecated: PruningScores is no longer computed during epoch. Always returns empty. - pub fn get_pruning_score(_netuid: NetUid) -> Vec { - Vec::new() - } - pub fn get_validator_trust(netuid: NetUid) -> Vec { - ValidatorTrust::::get(netuid) - .into_iter() - .map(PerU16::deconstruct) - .collect() - } - pub fn get_validator_permit(netuid: NetUid) -> Vec { - ValidatorPermit::::get(netuid) - } - - // ================================== - // ==== YumaConsensus UID params ==== - // ================================== - pub fn set_last_update_for_uid(netuid: NetUidStorageIndex, uid: u16, last_update: u64) { - let mut updated_last_update_vec = Self::get_last_update(netuid); - let Some(updated_last_update) = updated_last_update_vec.get_mut(uid as usize) else { - return; - }; - *updated_last_update = last_update; - LastUpdate::::insert(netuid, updated_last_update_vec); - } - pub fn set_active_for_uid(netuid: NetUid, uid: u16, active: bool) { - let mut updated_active_vec = Self::get_active(netuid); - let Some(updated_active) = updated_active_vec.get_mut(uid as usize) else { - return; - }; - *updated_active = active; - Active::::insert(netuid, updated_active_vec); - } - pub fn set_validator_permit_for_uid(netuid: NetUid, uid: u16, validator_permit: bool) { - let mut updated_validator_permits = Self::get_validator_permit(netuid); - let Some(updated_validator_permit) = updated_validator_permits.get_mut(uid as usize) else { - return; - }; - *updated_validator_permit = validator_permit; - ValidatorPermit::::insert(netuid, updated_validator_permits); - } - pub fn set_stake_threshold(min_stake: u64) { - StakeThreshold::::put(min_stake); - Self::deposit_event(Event::StakeThresholdSet(min_stake)); - } - - /// Deprecated: Rank is no longer computed. Always returns 0. - pub fn get_rank_for_uid(_netuid: NetUid, _uid: u16) -> u16 { - 0 - } - /// Deprecated: Trust is no longer computed. Always returns 0. - pub fn get_trust_for_uid(_netuid: NetUid, _uid: u16) -> u16 { - 0 - } - pub fn get_emission_for_uid(netuid: NetUid, uid: u16) -> AlphaBalance { - let vec = Emission::::get(netuid); - vec.get(uid as usize).copied().unwrap_or_default() - } - pub fn get_active_for_uid(netuid: NetUid, uid: u16) -> bool { - let vec = Active::::get(netuid); - vec.get(uid as usize).copied().unwrap_or(false) - } - pub fn get_consensus_for_uid(netuid: NetUid, uid: u16) -> u16 { - let vec = Consensus::::get(netuid); - vec.get(uid as usize) - .copied() - .unwrap_or_default() - .deconstruct() - } - pub fn get_incentive_for_uid(netuid: NetUidStorageIndex, uid: u16) -> u16 { - let vec = Incentive::::get(netuid); - vec.get(uid as usize) - .copied() - .unwrap_or_default() - .deconstruct() - } - pub fn get_dividends_for_uid(netuid: NetUid, uid: u16) -> u16 { - let vec = Dividends::::get(netuid); - vec.get(uid as usize) - .copied() - .unwrap_or_default() - .deconstruct() - } - pub fn get_last_update_for_uid(netuid: NetUidStorageIndex, uid: u16) -> u64 { - let vec = LastUpdate::::get(netuid); - vec.get(uid as usize).copied().unwrap_or(0) - } - /// Deprecated: PruningScores is no longer computed. Always returns u16::MAX. - pub fn get_pruning_score_for_uid(_netuid: NetUid, _uid: u16) -> u16 { - u16::MAX - } - pub fn get_validator_trust_for_uid(netuid: NetUid, uid: u16) -> u16 { - let vec = ValidatorTrust::::get(netuid); - vec.get(uid as usize) - .copied() - .unwrap_or_default() - .deconstruct() - } - pub fn get_validator_permit_for_uid(netuid: NetUid, uid: u16) -> bool { - let vec = ValidatorPermit::::get(netuid); - vec.get(uid as usize).copied().unwrap_or(false) - } - pub fn get_stake_threshold() -> u64 { - StakeThreshold::::get() - } - - // ============================ - // ==== Subnetwork Getters ==== - // ============================ - pub fn get_tempo(netuid: NetUid) -> u16 { - Tempo::::get(netuid) - } - pub fn get_last_adjustment_block(netuid: NetUid) -> u64 { - LastAdjustmentBlock::::get(netuid) - } - pub fn get_blocks_since_last_step(netuid: NetUid) -> u64 { - BlocksSinceLastStep::::get(netuid) - } - pub fn get_difficulty(netuid: NetUid) -> U256 { - U256::from(Self::get_difficulty_as_u64(netuid)) - } - pub fn get_registrations_this_block(netuid: NetUid) -> u16 { - RegistrationsThisBlock::::get(netuid) - } - pub fn get_last_mechanism_step_block(netuid: NetUid) -> u64 { - LastMechansimStepBlock::::get(netuid) - } - pub fn get_registrations_this_interval(netuid: NetUid) -> u16 { - RegistrationsThisInterval::::get(netuid) - } - pub fn get_pow_registrations_this_interval(netuid: NetUid) -> u16 { - POWRegistrationsThisInterval::::get(netuid) - } - pub fn get_burn_registrations_this_interval(netuid: NetUid) -> u16 { - BurnRegistrationsThisInterval::::get(netuid) - } - pub fn get_neuron_block_at_registration(netuid: NetUid, neuron_uid: u16) -> u64 { - BlockAtRegistration::::get(netuid, neuron_uid) - } - /// Returns the minimum number of non-immortal & non-immune UIDs that must remain in a subnet. - pub fn get_min_non_immune_uids(netuid: NetUid) -> u16 { - MinNonImmuneUids::::get(netuid) - } - - /// Sets the minimum number of non-immortal & non-immune UIDs that must remain in a subnet. - pub fn set_min_non_immune_uids(netuid: NetUid, min: u16) { - MinNonImmuneUids::::insert(netuid, min); - Self::deposit_event(Event::MinNonImmuneUidsSet(netuid, min)); - } - - // ======================== - // ===== Take checks ====== - // ======================== - pub fn do_take_checks(coldkey: &T::AccountId, hotkey: &T::AccountId) -> Result<(), Error> { - // Ensure we are delegating a known key. - ensure!( - Self::hotkey_account_exists(hotkey), - Error::::HotKeyAccountNotExists - ); - - // Ensure that the coldkey is the owner. - ensure!( - Self::coldkey_owns_hotkey(coldkey, hotkey), - Error::::NonAssociatedColdKey - ); - - Ok(()) - } - - // ======================== - // === Token Management === - // ======================== - pub fn set_subnet_locked_balance(netuid: NetUid, amount: TaoBalance) { - SubnetLocked::::insert(netuid, amount); - } - pub fn get_subnet_locked_balance(netuid: NetUid) -> TaoBalance { - SubnetLocked::::get(netuid) - } - pub fn get_total_subnet_locked() -> TaoBalance { - let mut total_subnet_locked: u64 = 0; - for (_, locked) in SubnetLocked::::iter() { - total_subnet_locked.saturating_accrue(locked.into()); - } - total_subnet_locked.into() - } - - pub fn set_recycle_or_burn(netuid: NetUid, recycle_or_burn: RecycleOrBurnEnum) { - RecycleOrBurn::::insert(netuid, recycle_or_burn); - } - // ======================== // ========= Sudo ========= // ======================== @@ -921,7 +533,7 @@ impl Pallet { pub fn set_subnet_owner_hotkey(netuid: NetUid, hotkey: &T::AccountId) -> DispatchResult { // Ensure that hotkey is not a special account ensure!( - Self::is_subnet_account_id(hotkey).is_none(), + Self::netuid_for_subnet_account(hotkey).is_none(), Error::::CannotUseSystemAccount ); @@ -997,72 +609,4 @@ impl Pallet { pub fn set_net_tao_flow_enabled(enabled: bool) { NetTaoFlowEnabled::::set(enabled); } - - /// Multiply an integer `value` by a Q32 fixed-point factor. - /// - /// Q32 means: - /// 1.0 == 1 << 32 - /// 0.5 == 1 << 31 - /// - /// Safe / non-panicking: - /// * uses saturating u128 multiplication - /// * clamps back into u64 range - pub fn mul_by_q32(value: u64, factor_q32: u64) -> u64 { - let product: u128 = (value as u128).saturating_mul(factor_q32 as u128); - let shifted: u128 = product >> 32; - core::cmp::min(shifted, u64::MAX as u128) as u64 - } - - /// Exponentiation-by-squaring for Q32 values. - /// - /// Returns `base_q32 ^ exp` in Q32. - /// - /// Safe / non-panicking: - /// * uses `mul_by_q32`, which is saturating/clamped - pub fn pow_q32(base_q32: u64, exp: u16) -> u64 { - let mut result: u64 = 1u64 << 32; // 1.0 in Q32 - let mut factor: u64 = base_q32; - let mut power: u32 = u32::from(exp); - - while power > 0 { - if (power & 1) == 1 { - result = Self::mul_by_q32(result, factor); - } - - power >>= 1; - - if power > 0 { - factor = Self::mul_by_q32(factor, factor); - } - } - - result - } - - /// Returns the per-block decay factor `f` in Q32 - pub fn decay_factor_q32(half_life: u16) -> u64 { - if half_life == 0 { - return 1u64 << 32; // 1.0 - } - - let one_q32: u64 = 1u64 << 32; - let half_q32: u64 = 1u64 << 31; // 0.5 - - let mut lo: u64 = 0; - let mut hi: u64 = one_q32; - - while lo.saturating_add(1) < hi { - let span: u64 = hi.saturating_sub(lo); - let mid: u64 = lo.saturating_add(span >> 1); - let mid_pow: u64 = Self::pow_q32(mid, half_life); - - if mid_pow > half_q32 { - hi = mid; - } else { - lo = mid; - } - } - - lo - } } diff --git a/pallets/subtensor/src/utils/misc/take_and_locks.rs b/pallets/subtensor/src/utils/misc/take_and_locks.rs new file mode 100644 index 0000000000..1cd53e2547 --- /dev/null +++ b/pallets/subtensor/src/utils/misc/take_and_locks.rs @@ -0,0 +1,47 @@ +//! Delegate-take ownership checks and subnet locked-balance / recycle-or-burn helpers. +use super::*; +use crate::Error; +use sp_runtime::Saturating; +use subtensor_runtime_common::{NetUid, TaoBalance}; + +impl Pallet { + // ======================== + // ===== Take checks ====== + // ======================== + pub fn do_take_checks(coldkey: &T::AccountId, hotkey: &T::AccountId) -> Result<(), Error> { + // Ensure we are delegating a known key. + ensure!( + Self::hotkey_account_exists(hotkey), + Error::::HotKeyAccountNotExists + ); + + // Ensure that the coldkey is the owner. + ensure!( + Self::coldkey_owns_hotkey(coldkey, hotkey), + Error::::NonAssociatedColdKey + ); + + Ok(()) + } + + // ======================== + // === Token Management === + // ======================== + pub fn set_subnet_locked_balance(netuid: NetUid, amount: TaoBalance) { + SubnetLocked::::insert(netuid, amount); + } + pub fn get_subnet_locked_balance(netuid: NetUid) -> TaoBalance { + SubnetLocked::::get(netuid) + } + pub fn get_total_subnet_locked() -> TaoBalance { + let mut total_subnet_locked: u64 = 0; + for (_, locked) in SubnetLocked::::iter() { + total_subnet_locked.saturating_accrue(locked.into()); + } + total_subnet_locked.into() + } + + pub fn set_recycle_or_burn(netuid: NetUid, recycle_or_burn: RecycleOrBurnEnum) { + RecycleOrBurn::::insert(netuid, recycle_or_burn); + } +} diff --git a/pallets/subtensor/src/utils/misc/tempo_and_counters.rs b/pallets/subtensor/src/utils/misc/tempo_and_counters.rs new file mode 100644 index 0000000000..614eae0935 --- /dev/null +++ b/pallets/subtensor/src/utils/misc/tempo_and_counters.rs @@ -0,0 +1,62 @@ +//! Tempo writes, registration/adjustment counters, and current-block helper. +use super::*; +use subtensor_runtime_common::NetUid; + +impl Pallet { + // ======================== + // ==== Global Setters ==== + // ======================== + /// Unchecked tempo write used by tests, precompiles, and internal helpers. + /// Does NOT reset `LastEpochBlock` — that is the responsibility of + /// `AdminUtils::sudo_set_tempo` (owner-or-root), which performs the cycle + /// reset explicitly via `apply_tempo_with_cycle_reset`. + pub fn set_tempo_unchecked(netuid: NetUid, tempo: u16) { + Tempo::::insert(netuid, tempo); + Self::deposit_event(Event::TempoSet(netuid, tempo)); + } + + /// Sets `Tempo` and resets the state-based scheduler anchor `LastEpochBlock` + /// to the current block + pub fn apply_tempo_with_cycle_reset(netuid: NetUid, tempo: u16) { + Self::set_tempo_unchecked(netuid, tempo); + let now = Self::get_current_block_as_u64(); + LastEpochBlock::::insert(netuid, now); + } + + pub fn set_last_adjustment_block(netuid: NetUid, last_adjustment_block: u64) { + LastAdjustmentBlock::::insert(netuid, last_adjustment_block); + } + pub fn set_blocks_since_last_step(netuid: NetUid, blocks_since_last_step: u64) { + BlocksSinceLastStep::::insert(netuid, blocks_since_last_step); + } + pub fn set_registrations_this_block(netuid: NetUid, registrations_this_block: u16) { + RegistrationsThisBlock::::insert(netuid, registrations_this_block); + } + pub fn set_last_mechanism_step_block(netuid: NetUid, last_mechanism_step_block: u64) { + LastMechansimStepBlock::::insert(netuid, last_mechanism_step_block); + } + pub fn set_registrations_this_interval(netuid: NetUid, registrations_this_interval: u16) { + RegistrationsThisInterval::::insert(netuid, registrations_this_interval); + } + pub fn set_pow_registrations_this_interval( + netuid: NetUid, + pow_registrations_this_interval: u16, + ) { + POWRegistrationsThisInterval::::insert(netuid, pow_registrations_this_interval); + } + pub fn set_burn_registrations_this_interval( + netuid: NetUid, + burn_registrations_this_interval: u16, + ) { + BurnRegistrationsThisInterval::::insert(netuid, burn_registrations_this_interval); + } + + // ======================== + // ==== Global Getters ==== + // ======================== + pub fn get_current_block_as_u64() -> u64 { + TryInto::try_into(>::block_number()) + .ok() + .expect("blockchain will not exceed 2^64 blocks; QED.") + } +} diff --git a/pallets/subtensor/src/utils/mod.rs b/pallets/subtensor/src/utils/mod.rs index bb7127f007..13a5968f04 100644 --- a/pallets/subtensor/src/utils/mod.rs +++ b/pallets/subtensor/src/utils/mod.rs @@ -1,3 +1,14 @@ +//! Shared pallet helpers that do not belong to a single feature module. +//! +//! Search anchors: +//! - [`cleanup`] — weight-metered storage cleanup during subnet dissolve / stake wipe +//! - [`evm`] — hotkey↔EVM address association (EIP-191 recover + reverse index) +//! - [`identity`] — coldkey and subnet identity validation / storage writes +//! - [`misc`] — origin guards, admin freeze window, hyperparam getters/setters, Q32 math +//! - [`rate_limiting`] — [`TransactionType`] / [`Hyperparameter`] rate-limit keys +//! - [`voting_power`] — per-subnet validator voting-power EMA tracking +//! - [`try_state`] — try-runtime stake invariants (`try-runtime` feature only) + use super::*; pub mod cleanup; pub mod evm; diff --git a/pallets/subtensor/src/utils/rate_limiting.rs b/pallets/subtensor/src/utils/rate_limiting.rs index 983e8ded46..a4813af3e3 100644 --- a/pallets/subtensor/src/utils/rate_limiting.rs +++ b/pallets/subtensor/src/utils/rate_limiting.rs @@ -1,8 +1,19 @@ +//! Transaction and hyperparameter rate-limit keys for Subtensor extrinsics. +//! +//! [`TransactionType`] maps to a `u16` stored in [`TransactionKeyLastBlock`] (or special-cased +//! storage for network register / SN owner hotkey / owner hyperparams). Those `u16` codes are +//! frozen wire/storage discriminants — append new variants at the end; do not renumber. +//! +//! [`Hyperparameter`] discriminants are likewise frozen (used inside +//! [`RateLimitKey::OwnerHyperparamUpdate`]). + use subtensor_runtime_common::NetUid; use super::*; -/// Enum representing different types of transactions +/// Extrinsic / admin action categories that share the rate-limit storage path. +/// +/// `Into` values are persisted; see module docs before reordering variants. #[derive(Copy, Clone)] #[non_exhaustive] pub enum TransactionType { @@ -21,7 +32,7 @@ pub enum TransactionType { } impl TransactionType { - /// Get the rate limit for a specific transaction type + /// Global (non-subnet) rate limit in blocks for this transaction type. pub fn rate_limit(&self) -> u64 { match self { Self::SetChildren => 150, // 30 minutes @@ -35,6 +46,7 @@ impl TransactionType { } } + /// Subnet-scoped rate limit in blocks (tempo-multiplied for owner hyperparams / weights key). pub fn rate_limit_on_subnet(&self, netuid: NetUid) -> u64 { #[allow(clippy::match_single_binding)] match self { @@ -53,6 +65,7 @@ impl TransactionType { } } + /// Whether `key` may submit this global transaction type at the current block. pub fn passes_rate_limit(&self, key: &T::AccountId) -> bool { let block = Pallet::::get_current_block_as_u64(); let limit = self.rate_limit::(); @@ -61,12 +74,13 @@ impl TransactionType { Self::check_passes_rate_limit(limit, block, last_block) } + /// `true` when `last_block == 0` (never used) or `block - last_block >= limit`. pub fn check_passes_rate_limit(limit: u64, block: u64, last_block: u64) -> bool { // Allow the first transaction (when last_block is 0) or if the rate limit has passed last_block == 0 || block.saturating_sub(last_block) >= limit } - /// Check if a transaction should be rate limited on a specific subnet + /// Whether `hotkey` may submit this transaction type on `netuid` at the current block. pub fn passes_rate_limit_on_subnet( &self, hotkey: &T::AccountId, @@ -79,7 +93,7 @@ impl TransactionType { Self::check_passes_rate_limit(limit, block, last_block) } - /// Get the block number of the last transaction for a specific key, and transaction type + /// Block of the last global transaction for `key` and this type. pub fn last_block(&self, key: &T::AccountId) -> u64 { match self { Self::RegisterNetwork => Pallet::::get_network_last_lock_block(), @@ -87,8 +101,7 @@ impl TransactionType { } } - /// Get the block number of the last transaction for a specific hotkey, network, and transaction - /// type + /// Block of the last subnet-scoped transaction for `hotkey` / `netuid` / this type. pub fn last_block_on_subnet(&self, hotkey: &T::AccountId, netuid: NetUid) -> u64 { match self { Self::RegisterNetwork => Pallet::::get_network_last_lock_block(), @@ -105,8 +118,7 @@ impl TransactionType { } } - /// Set the block number of the last transaction for a specific hotkey, network, and transaction - /// type + /// Record `block` as the last submission time for this type on `netuid`. pub fn set_last_block_on_subnet( &self, key: &T::AccountId, @@ -131,7 +143,7 @@ impl TransactionType { } } -/// Implement conversion from TransactionType to u16 +/// Frozen `u16` codes persisted in [`TransactionKeyLastBlock`] — do not renumber. impl From for u16 { fn from(tx_type: TransactionType) -> Self { match tx_type { @@ -151,7 +163,7 @@ impl From for u16 { } } -/// Implement conversion from u16 to TransactionType +/// Inverse of [`From for u16`]; unknown codes map to [`TransactionType::Unknown`]. impl From for TransactionType { fn from(value: u16) -> Self { match value { @@ -177,6 +189,10 @@ impl From for TransactionType { } } +/// Owner-settable subnet hyperparameters used as rate-limit sub-keys. +/// +/// Explicit discriminants are stored on-chain via [`RateLimitKey::OwnerHyperparamUpdate`] — +/// append only; do not renumber existing variants. #[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, Debug, TypeInfo)] #[non_exhaustive] pub enum Hyperparameter { @@ -221,40 +237,50 @@ impl Pallet { // ==== Rate Limiting ===== // ======================== + /// Clear the last generic tx-block marker for `key`. pub fn remove_last_tx_block(key: &T::AccountId) { Self::remove_rate_limited_last_block(&RateLimitKey::LastTxBlock(key.clone())) } + /// Record the last generic tx-block for `key`. pub fn set_last_tx_block(key: &T::AccountId, block: u64) { Self::set_rate_limited_last_block(&RateLimitKey::LastTxBlock(key.clone()), block); } + /// Last block at which `key` submitted a generic rate-limited tx. pub fn get_last_tx_block(key: &T::AccountId) -> u64 { Self::get_rate_limited_last_block(&RateLimitKey::LastTxBlock(key.clone())) } + /// Clear the last delegate-take tx-block marker for `key`. pub fn remove_last_tx_block_delegate_take(key: &T::AccountId) { Self::remove_rate_limited_last_block(&RateLimitKey::LastTxBlockDelegateTake(key.clone())) } + /// Record the last delegate-take tx-block for `key`. pub fn set_last_tx_block_delegate_take(key: &T::AccountId, block: u64) { Self::set_rate_limited_last_block( &RateLimitKey::LastTxBlockDelegateTake(key.clone()), block, ); } + /// Last block at which `key` updated delegate take. pub fn get_last_tx_block_delegate_take(key: &T::AccountId) -> u64 { Self::get_rate_limited_last_block(&RateLimitKey::LastTxBlockDelegateTake(key.clone())) } + /// Last block at which `key` updated childkey take. pub fn get_last_tx_block_childkey_take(key: &T::AccountId) -> u64 { Self::get_rate_limited_last_block(&RateLimitKey::LastTxBlockChildKeyTake(key.clone())) } + /// Clear the last childkey-take tx-block marker for `key`. pub fn remove_last_tx_block_childkey(key: &T::AccountId) { Self::remove_rate_limited_last_block(&RateLimitKey::LastTxBlockChildKeyTake(key.clone())) } + /// Record the last childkey-take tx-block for `key`. pub fn set_last_tx_block_childkey(key: &T::AccountId, block: u64) { Self::set_rate_limited_last_block( &RateLimitKey::LastTxBlockChildKeyTake(key.clone()), block, ); } + /// `true` if `current_block - prev_tx_block` is still within the global tx rate limit. pub fn exceeds_tx_rate_limit(prev_tx_block: u64, current_block: u64) -> bool { let rate_limit: u64 = Self::get_tx_rate_limit(); if rate_limit == 0 || prev_tx_block == 0 { @@ -263,6 +289,7 @@ impl Pallet { current_block.saturating_sub(prev_tx_block) <= rate_limit } + /// `true` if `current_block - prev_tx_block` is still within the delegate-take rate limit. pub fn exceeds_tx_delegate_take_rate_limit(prev_tx_block: u64, current_block: u64) -> bool { let rate_limit: u64 = Self::get_tx_delegate_take_rate_limit(); if rate_limit == 0 || prev_tx_block == 0 { diff --git a/pallets/subtensor/src/utils/try_state.rs b/pallets/subtensor/src/utils/try_state.rs index 605548ea6e..58f7d7a06f 100644 --- a/pallets/subtensor/src/utils/try_state.rs +++ b/pallets/subtensor/src/utils/try_state.rs @@ -1,7 +1,9 @@ +//! Try-runtime invariants for stake accounting (`try-runtime` feature only). + use super::*; impl Pallet { - /// Checks the sum of all stakes matches the [`TotalStake`]. + /// Checks that Σ `SubnetTAO` (minus non-root network min-lock) equals [`TotalStake`]. #[allow(dead_code)] pub(crate) fn check_total_stake() -> Result<(), sp_runtime::TryRuntimeError> { // Calculate the total staked amount diff --git a/pallets/subtensor/src/utils/voting_power.rs b/pallets/subtensor/src/utils/voting_power.rs index 11d8880b97..b1259991ca 100644 --- a/pallets/subtensor/src/utils/voting_power.rs +++ b/pallets/subtensor/src/utils/voting_power.rs @@ -1,13 +1,20 @@ +//! Per-subnet validator voting-power EMA tracking. +//! +//! When enabled (`VotingPowerTrackingEnabled`), epoch processing updates [`VotingPower`] +//! for validators with a permit using stake + [`VotingPowerEmaAlpha`]. Disabling is +//! scheduled with a [`VOTING_POWER_DISABLE_GRACE_PERIOD_BLOCKS`] delay before entries clear. + use super::*; use crate::epoch::run_epoch::EpochTerms; use alloc::collections::BTreeMap; use subtensor_runtime_common::{AlphaBalance, NetUid}; -/// 14 days in blocks (assuming ~12 second blocks) -/// 14 * 24 * 60 * 60 / 12 = 100800 blocks +/// Grace period after a disable request before tracking stops and [`VotingPower`] is cleared. +/// +/// 14 days at ~12s blocks: `14 * 24 * 60 * 60 / 12 = 100800`. pub const VOTING_POWER_DISABLE_GRACE_PERIOD_BLOCKS: u64 = 100800; -/// Maximum alpha value (1.0 represented as u64 with 18 decimals) +/// EMA alpha of 1.0 in 18-decimal fixed point (`10^18`). pub const MAX_VOTING_POWER_EMA_ALPHA: u64 = 1_000_000_000_000_000_000; impl Pallet { @@ -43,7 +50,7 @@ impl Pallet { /// Enable voting power tracking for a subnet. pub fn do_enable_voting_power_tracking(netuid: NetUid) -> DispatchResult { - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); // Enable tracking VotingPowerTrackingEnabled::::insert(netuid, true); @@ -61,7 +68,7 @@ impl Pallet { /// Schedule disabling of voting power tracking for a subnet. /// Tracking will continue for 14 days, then automatically disable. pub fn do_disable_voting_power_tracking(netuid: NetUid) -> DispatchResult { - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); // Check if tracking is enabled ensure!( Self::get_voting_power_tracking_enabled(netuid), @@ -91,7 +98,7 @@ impl Pallet { /// Set the EMA alpha value for voting power calculation on a subnet. pub fn do_set_voting_power_ema_alpha(netuid: NetUid, alpha: u64) -> DispatchResult { - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!(Self::subnet_exists(netuid), Error::::SubnetNotExists); // Validate alpha (must be <= 1.0, represented as 10^18) ensure!( alpha <= MAX_VOTING_POWER_EMA_ALPHA, diff --git a/pallets/swap/rpc/src/lib.rs b/pallets/swap/rpc/src/lib.rs index fa072c29ae..66aaa3801d 100644 --- a/pallets/swap/rpc/src/lib.rs +++ b/pallets/swap/rpc/src/lib.rs @@ -1,4 +1,7 @@ -//! RPC interface for the Swap pallet +//! JSON-RPC surface for TAO↔alpha price quotes and simulated swaps. +//! +//! Method name strings (`swap_currentAlphaPrice`, …) are frozen wire IDs — do not rename. +//! Handlers forward to [`SwapRuntimeApi`]; `sim_swap_*` returns SCALE-encoded [`SimSwapResult`]. use codec::Encode; use std::sync::Arc; @@ -13,14 +16,20 @@ use sp_blockchain::HeaderBackend; use sp_runtime::traits::Block as BlockT; use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance}; -pub use pallet_subtensor_swap_runtime_api::{SubnetPrice, SwapRuntimeApi}; +pub use pallet_subtensor_swap_runtime_api::{SimSwapResult, SubnetPrice, SwapRuntimeApi}; +/// Client/server RPC trait for the swap pallet. +/// +/// `#[method(name = "…")]` strings are part of the public node API. #[rpc(client, server)] pub trait SwapRpcApi { + /// Alpha price for `netuid` at `at` (or best block), scaled by `1e9`. #[method(name = "swap_currentAlphaPrice")] fn current_alpha_price(&self, netuid: NetUid, at: Option) -> RpcResult; + /// Alpha prices for all dynamic subnets at `at` (or best block). #[method(name = "swap_currentAlphaPriceAll")] fn current_alpha_price_all(&self, at: Option) -> RpcResult>; + /// Dry-run TAO→alpha swap; returns SCALE-encoded [`SimSwapResult`] bytes. #[method(name = "swap_simSwapTaoForAlpha")] fn sim_swap_tao_for_alpha( &self, @@ -28,6 +37,7 @@ pub trait SwapRpcApi { tao: TaoBalance, at: Option, ) -> RpcResult>; + /// Dry-run alpha→TAO swap; returns SCALE-encoded [`SimSwapResult`] bytes. #[method(name = "swap_simSwapAlphaForTao")] fn sim_swap_alpha_for_tao( &self, diff --git a/pallets/swap/runtime-api/src/lib.rs b/pallets/swap/runtime-api/src/lib.rs index 0f9803f162..6d07ce0344 100644 --- a/pallets/swap/runtime-api/src/lib.rs +++ b/pallets/swap/runtime-api/src/lib.rs @@ -1,3 +1,8 @@ +//! Runtime API for off-chain TAO↔alpha price and swap simulation queries. +//! +//! Implemented by the node runtime; consumed by the swap RPC crate. +//! Method names and [`SimSwapResult`] / [`SubnetPrice`] layouts are wire-stable. + #![cfg_attr(not(feature = "std"), no_std)] use frame_support::pallet_prelude::*; @@ -25,10 +30,18 @@ pub struct SubnetPrice { } sp_api::decl_runtime_apis! { + /// Runtime API for swap price quotes and dry-run swaps. + /// + /// RPC method strings (`swap_currentAlphaPrice`, etc.) must stay in sync with + /// `pallets/swap/rpc` — do not rename these trait methods. pub trait SwapRuntimeApi { + /// Alpha price for `netuid`, scaled by `1e9`. fn current_alpha_price(netuid: NetUid) -> u64; + /// Alpha prices for every subnet that has a dynamic mechanism. fn current_alpha_price_all() -> Vec; + /// Dry-run buy: pay `tao` rao, receive alpha (fees included in result). fn sim_swap_tao_for_alpha(netuid: NetUid, tao: TaoBalance) -> SimSwapResult; + /// Dry-run sell: pay `alpha`, receive TAO rao (fees included in result). fn sim_swap_alpha_for_tao(netuid: NetUid, alpha: AlphaBalance) -> SimSwapResult; } } diff --git a/pallets/swap/src/benchmarking.rs b/pallets/swap/src/benchmarking.rs index 1751ee1a97..8f5ae23556 100644 --- a/pallets/swap/src/benchmarking.rs +++ b/pallets/swap/src/benchmarking.rs @@ -1,4 +1,4 @@ -//! Benchmarking setup for pallet-subtensor-swap +//! Extrinsic benchmarks for `pallet-subtensor-swap` (active + deprecated LP stubs). #![allow(clippy::unwrap_used)] #![allow(clippy::multiple_bound_locations)] #![allow(deprecated)] diff --git a/pallets/swap/src/lib.rs b/pallets/swap/src/lib.rs index b51c3351dc..afc0ab2105 100644 --- a/pallets/swap/src/lib.rs +++ b/pallets/swap/src/lib.rs @@ -1,3 +1,15 @@ +//! # Subtensor Swap Pallet +//! +//! Weighted-balancer AMM for TAO↔alpha swaps on dynamic subnets (`mechanism == 1`). +//! +//! Core surfaces agents search for: +//! - [`Pallet::do_swap`] / [`SwapHandler`] — execute or simulate a swap +//! - [`Pallet::adjust_protocol_liquidity`] — inject protocol TAO/alpha without moving price +//! - [`Balancer`] — pool math (weights, price, reserve deltas) +//! - [`FeeRate`] / [`SwapBalancer`] — per-`netuid` fee and weight state +//! +//! User LP extrinsics (`add_liquidity`, etc.) are deprecated stubs; liquidity is protocol-owned. + #![cfg_attr(not(feature = "std"), no_std)] pub mod pallet; diff --git a/pallets/swap/src/mock.rs b/pallets/swap/src/mock.rs index 1b0ada87be..23c3f41f8b 100644 --- a/pallets/swap/src/mock.rs +++ b/pallets/swap/src/mock.rs @@ -1,3 +1,5 @@ +//! Test runtime and mocked TAO/alpha reserves for `pallet-subtensor-swap` unit tests. + #![allow(clippy::unwrap_used)] use core::num::NonZeroU64; diff --git a/pallets/swap/src/pallet/balancer.rs b/pallets/swap/src/pallet/balancer.rs deleted file mode 100644 index 244e4ff9eb..0000000000 --- a/pallets/swap/src/pallet/balancer.rs +++ /dev/null @@ -1,1351 +0,0 @@ -// Balancer swap -// -// Unlike uniswap v2 or v3, it allows adding liquidity disproportionally to price. This is -// achieved by introducing the weights w1 and w2 so that w1 + w2 = 1. In these formulas x -// means base currency (alpha) and y means quote currency (tao). The w1 weight in the code -// below is referred as weight_base, and w2 as weight_quote. Because of the w1 + w2 = 1 -// constraint, only weight_quote is stored, and weight_base is always calculated. -// -// The formulas used for pool operation are following: -// -// Price: p = (w1*y) / (w2*x) -// -// Reserve deltas / (or -1 * payouts) in swaps are computed by: -// -// if ∆x is given (sell) ∆y = y * ((x / (x+∆x))^(w1/w2) - 1) -// if ∆y is given (buy) ∆x = x * ((y / (y+∆y))^(w2/w1) - 1) -// -// When swaps are executing the orders with slippage control, we need to know what amount -// we can swap before the price reaches the limit value of p': -// -// If p' < p (sell): ∆x = x * ((p / p')^w2 - 1) -// If p' < p (buy): ∆y = y * ((p' / p)^w1 - 1) -// -// In order to initialize weights with existing reserve values and price: -// -// w1 = px / (px + y) -// w2 = y / (px + y) -// -// Weights are adjusted when some amounts are added to the reserves. This prevents price -// from changing. -// -// new_w1 = p * (x + ∆x) / (p * (x + ∆x) + y + ∆y) -// new_w2 = (y + ∆y) / (p * (x + ∆x) + y + ∆y) -// -// Weights are limited to stay within [0.1, 0.9] range to avoid precision issues in exponentiation. -// Practically, these limitations will not be achieved, but if they are, the swap will not allow injection -// that will push the weights out of this interval because we prefer chain and swap stability over success -// of a single injection. Currently, we only allow the protocol to inject disproportionally to price, and -// the amount of disproportion will not cause weigths to get far from 0.5. -// - -use codec::{Decode, Encode, MaxEncodedLen}; -use frame_support::pallet_prelude::*; -use safe_bigmath::*; -use safe_math::*; -use sp_arithmetic::Perquintill; -use sp_core::U256; -use sp_runtime::Saturating; -use sp_std::ops::Neg; -use substrate_fixed::types::U64F64; -use subtensor_macros::freeze_struct; - -/// Balancer implements all high complexity math for swap operations such as: -/// - Swapping x for y, which includes limit orders -/// - Adding and removing liquidity (including unbalanced) -/// -/// Notation used in this file: -/// - x: Base reserve (alplha reserve) -/// - y: Quote reserve (tao reserve) -/// - ∆x: Alpha paid in/out -/// - ∆y: Tao paid in/out -/// - w1: Base weight (a.k.a weight_base) -/// - w2: Quote weight (a.k.a weight_quote) -#[freeze_struct("33a4fb0774da77c7")] -#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] -pub struct Balancer { - quote: Perquintill, -} - -/// Accuracy matches to 18 decimal digits used to represent weights -pub const ACCURACY: u64 = 1_000_000_000_000_000_000_u64; -/// Lower imit of weights is 0.01 -pub const MIN_WEIGHT: Perquintill = Perquintill::from_parts(ACCURACY / 100); -/// 1.0 in Perquintill -pub const ONE: Perquintill = Perquintill::from_parts(ACCURACY); - -#[derive(Debug)] -pub enum BalancerError { - /// The provided weight value is out of range - InvalidValue, -} - -impl Default for Balancer { - /// The default value of weights is 0.5 for pool initialization - fn default() -> Self { - Self { - quote: Perquintill::from_rational(1u128, 2u128), - } - } -} - -impl Balancer { - /// Creates a new instance of balancer with a given quote weight - pub fn new(quote: Perquintill) -> Result { - if Self::check_constraints(quote) { - Ok(Balancer { quote }) - } else { - Err(BalancerError::InvalidValue) - } - } - - /// Constraints limit balancer weights within certain range of values: - /// - Both weights are above minimum - /// - Sum of weights is equal to 1.0 - fn check_constraints(quote: Perquintill) -> bool { - let base = ONE.saturating_sub(quote); - (base >= MIN_WEIGHT) && (quote >= MIN_WEIGHT) - } - - /// We store quote weight as Perquintill - pub fn get_quote_weight(&self) -> Perquintill { - self.quote - } - - /// Base weight is calculated as 1.0 - quote_weight - pub fn get_base_weight(&self) -> Perquintill { - ONE.saturating_sub(self.quote) - } - - /// Sets quote currency weight in the balancer. - /// Because sum of weights is always 1.0, there is no need to - /// store base currency weight - pub fn set_quote_weight(&mut self, new_value: Perquintill) -> Result<(), BalancerError> { - if Self::check_constraints(new_value) { - self.quote = new_value; - Ok(()) - } else { - Err(BalancerError::InvalidValue) - } - } - - /// If base_quote is true, calculate (x / (x + ∆x))^(weight_base / weight_quote), - /// otherwise, calculate (x / (x + ∆x))^(weight_quote / weight_base) - /// - /// Here we use SafeInt from bigmath crate for high-precision exponentiation, - /// which exposes the function pow_ratio_scaled. - /// - /// Note: ∆x may be negative - fn exp_scaled(&self, x: u64, dx: i128, base_quote: bool) -> U64F64 { - let x_plus_dx = if dx >= 0 { - x.saturating_add(dx as u64) - } else { - x.saturating_sub(dx.neg() as u64) - }; - - if x_plus_dx == 0 { - return U64F64::saturating_from_num(0); - } - let w1: u128 = self.get_base_weight().deconstruct() as u128; - let w2: u128 = self.get_quote_weight().deconstruct() as u128; - - let precision = 256; - let x_safe = SafeInt::from(x); - let w1_safe = SafeInt::from(w1); - let w2_safe = SafeInt::from(w2); - let perquintill_scale = SafeInt::from(ACCURACY as u128); - let denominator = SafeInt::from(x_plus_dx); - log::debug!("x = {:?}", x); - log::debug!("dx = {:?}", dx); - log::debug!("x_safe = {:?}", x_safe); - log::debug!("denominator = {:?}", denominator); - log::debug!("w1_safe = {:?}", w1_safe); - log::debug!("w2_safe = {:?}", w2_safe); - log::debug!("precision = {:?}", precision); - log::debug!("perquintill_scale = {:?}", perquintill_scale); - - let maybe_result_safe_int = if base_quote { - SafeInt::pow_ratio_scaled( - &x_safe, - &denominator, - &w1_safe, - &w2_safe, - precision, - &perquintill_scale, - ) - } else { - SafeInt::pow_ratio_scaled( - &x_safe, - &denominator, - &w2_safe, - &w1_safe, - precision, - &perquintill_scale, - ) - }; - - if let Some(result_safe_int) = maybe_result_safe_int - && let Some(result_u64) = result_safe_int.to_u64() - { - let result = U64F64::saturating_from_num(result_u64) - .safe_div(U64F64::saturating_from_num(ACCURACY)); - return if dx >= 0 { - result.min(U64F64::from_num(1)) - } else { - result - }; - } - U64F64::saturating_from_num(0) - } - - /// Calculates exponent of (x / (x + ∆x)) ^ (w_base/w_quote) - /// This method is used in sell swaps - /// (∆x is given by user, ∆y is paid out by the pool) - pub fn exp_base_quote(&self, x: u64, dx: u64) -> U64F64 { - self.exp_scaled(x, dx as i128, true) - } - - /// Calculates exponent of (y / (y + ∆y)) ^ (w_quote/w_base) - /// This method is used in buy swaps - /// (∆y is given by user, ∆x is paid out by the pool) - pub fn exp_quote_base(&self, y: u64, dy: u64) -> U64F64 { - self.exp_scaled(y, dy as i128, false) - } - - /// Calculates price as (w1/w2) * (y/x), where - /// - w1 is base weight - /// - w2 is quote weight - /// - x is base reserve - /// - y is quote reserve - pub fn calculate_price(&self, x: u64, y: u64) -> U64F64 { - let w2_fixed = U64F64::saturating_from_num(self.get_quote_weight().deconstruct()); - let w1_fixed = U64F64::saturating_from_num(self.get_base_weight().deconstruct()); - let x_fixed = U64F64::saturating_from_num(x); - let y_fixed = U64F64::saturating_from_num(y); - w1_fixed - .safe_div(w2_fixed) - .saturating_mul(y_fixed.safe_div(x_fixed)) - } - - /// Multiply a u128 value by a Perquintill with u128 result rounded to the - /// nearest integer - fn mul_perquintill_round(p: Perquintill, value: u128) -> u128 { - let parts = p.deconstruct() as u128; - let acc = ACCURACY as u128; - - let num = U256::from(value).saturating_mul(U256::from(parts)); - let den = U256::from(acc); - - // Add 0.5 before integer division to achieve rounding to the nearest - // integer - let zero = U256::from(0); - let res = num - .saturating_add(den.checked_div(U256::from(2u8)).unwrap_or(zero)) - .checked_div(den) - .unwrap_or(zero); - res.min(U256::from(u128::MAX)) - .try_into() - .unwrap_or_default() - } - - /// When liquidity is added to balancer swap, it may be added with arbitrary proportion, - /// not necessarily in the proportion of price, like with uniswap v2 or v3. In order to - /// stay within balancer pool invariant, the weights need to be updated. Invariant: - /// - /// L = x ^ weight_base * y ^ weight_quote - /// - /// Note that weights must remain within the proper range (both be above MIN_WEIGHT), - /// so only reasonably small disproportions of updates are appropriate. - pub fn update_weights_for_added_liquidity( - &mut self, - tao_reserve: u64, - alpha_reserve: u64, - tao_delta: u64, - alpha_delta: u64, - ) -> Result<(), BalancerError> { - // Calculate new to-be reserves (do not update here) - let tao_reserve_u128 = u64::from(tao_reserve) as u128; - let alpha_reserve_u128 = u64::from(alpha_reserve) as u128; - let tao_delta_u128 = u64::from(tao_delta) as u128; - let alpha_delta_u128 = u64::from(alpha_delta) as u128; - let new_tao_reserve_u128 = tao_reserve_u128.saturating_add(tao_delta_u128); - let new_alpha_reserve_u128 = alpha_reserve_u128.saturating_add(alpha_delta_u128); - - // Calculate new weights - let quantity_1: u128 = Self::mul_perquintill_round( - self.get_base_weight(), - tao_reserve_u128.saturating_mul(new_alpha_reserve_u128), - ); - let quantity_2: u128 = Self::mul_perquintill_round( - self.get_quote_weight(), - alpha_reserve_u128.saturating_mul(new_tao_reserve_u128), - ); - let q_sum = quantity_1.saturating_add(quantity_2); - - // Calculate new reserve weights - let new_reserve_weight = if q_sum != 0 { - // Both TAO and Alpha are non-zero, normal case - Perquintill::from_rational(quantity_2, q_sum) - } else { - // Either TAO or Alpha reserve were and/or remain zero => Initialize weights to 0.5 - Perquintill::from_rational(1u128, 2u128) - }; - - self.set_quote_weight(new_reserve_weight) - } - - /// Calculates quote delta needed to reach the price up when byuing - /// This method is needed for limit orders. - /// - /// Formula is: - /// ∆y = y * ((price_new / price)^weight_base - 1) - /// price_new >= price - pub fn calculate_quote_delta_in( - &self, - current_price: U64F64, - target_price: U64F64, - reserve: u64, - ) -> u64 { - let base_numerator: u128 = target_price.to_bits(); - let base_denominator: u128 = current_price.to_bits(); - let w1_fixed: u128 = self.get_base_weight().deconstruct() as u128; - let scale: u128 = 10u128.pow(18); - - let maybe_exp_result = SafeInt::pow_ratio_scaled( - &SafeInt::from(base_numerator), - &SafeInt::from(base_denominator), - &SafeInt::from(w1_fixed), - &SafeInt::from(ACCURACY), - 1024, - &SafeInt::from(scale), - ); - - if let Some(exp_result_safe_int) = maybe_exp_result { - let reserve_fixed = U64F64::saturating_from_num(reserve); - let one = U64F64::saturating_from_num(1); - let scale_fixed = U64F64::saturating_from_num(scale); - let exp_result_fixed = if let Some(exp_result_u64) = exp_result_safe_int.to_u64() { - U64F64::saturating_from_num(exp_result_u64) - } else if u64::MAX < exp_result_safe_int { - U64F64::saturating_from_num(u64::MAX) - } else { - U64F64::saturating_from_num(0) - }; - reserve_fixed - .saturating_mul(exp_result_fixed.safe_div(scale_fixed).saturating_sub(one)) - .saturating_to_num::() - } else { - 0u64 - } - } - - /// Calculates base delta needed to reach the price down when selling - /// This method is needed for limit orders. - /// - /// Formula is: - /// ∆x = x * ((price / price_new)^weight_quote - 1) - /// price_new <= price - pub fn calculate_base_delta_in( - &self, - current_price: U64F64, - target_price: U64F64, - reserve: u64, - ) -> u64 { - let base_numerator: u128 = current_price.to_bits(); - let base_denominator: u128 = target_price.to_bits(); - let w2_fixed: u128 = self.get_quote_weight().deconstruct() as u128; - let scale: u128 = 10u128.pow(18); - - let maybe_exp_result = SafeInt::pow_ratio_scaled( - &SafeInt::from(base_numerator), - &SafeInt::from(base_denominator), - &SafeInt::from(w2_fixed), - &SafeInt::from(ACCURACY), - 1024, - &SafeInt::from(scale), - ); - - if let Some(exp_result_safe_int) = maybe_exp_result { - let one = U64F64::saturating_from_num(1); - let scale_fixed = U64F64::saturating_from_num(scale); - let reserve_fixed = U64F64::saturating_from_num(reserve); - let exp_result_fixed = if let Some(exp_result_u64) = exp_result_safe_int.to_u64() { - U64F64::saturating_from_num(exp_result_u64) - } else if u64::MAX < exp_result_safe_int { - U64F64::saturating_from_num(u64::MAX) - } else { - U64F64::saturating_from_num(0) - }; - reserve_fixed - .saturating_mul(exp_result_fixed.safe_div(scale_fixed).saturating_sub(one)) - .saturating_to_num::() - } else { - 0u64 - } - } - - /// Calculates amount of Alpha that needs to be sold to get a given amount of TAO - pub fn get_base_needed_for_quote( - &self, - tao_reserve: u64, - alpha_reserve: u64, - delta_tao: u64, - ) -> u64 { - let e = self.exp_scaled(tao_reserve, (delta_tao as i128).neg(), false); - let one = U64F64::from_num(1); - let alpha_reserve_fixed = U64F64::from_num(alpha_reserve); - // e > 1 in this case - alpha_reserve_fixed - .saturating_mul(e.saturating_sub(one)) - .saturating_to_num::() - } -} - -// cargo test --package pallet-subtensor-swap --lib -- pallet::balancer::tests --nocapture -#[cfg(test)] -#[allow(clippy::expect_used, clippy::unwrap_used)] -#[cfg(feature = "std")] -mod tests { - use crate::pallet::Balancer; - use crate::pallet::balancer::*; - use approx::assert_abs_diff_eq; - use sp_arithmetic::Perquintill; - use std::panic::{AssertUnwindSafe, catch_unwind}; - - // Helper: convert Perquintill to f64 for comparison - fn perquintill_to_f64(p: Perquintill) -> f64 { - let parts = p.deconstruct() as f64; - parts / ACCURACY as f64 - } - - // Helper: convert U64F64 to f64 for comparison - fn f(v: U64F64) -> f64 { - v.to_num::() - } - - fn assert_no_panic(label: &str, f: F) -> R - where - F: FnOnce() -> R, - { - catch_unwind(AssertUnwindSafe(f)).unwrap_or_else(|_| panic!("{label} panicked")) - } - - #[test] - fn test_balancer_rejects_invalid_boundary_weights_without_panicking() { - [ - Perquintill::zero(), - Perquintill::from_parts(1), - MIN_WEIGHT.saturating_sub(Perquintill::from_parts(1)), - ONE.saturating_sub(MIN_WEIGHT) - .saturating_add(Perquintill::from_parts(1)), - ONE, - ] - .into_iter() - .for_each(|quote| { - assert_no_panic("Balancer::new invalid boundary weight", || { - assert!(Balancer::new(quote).is_err()); - }); - }); - - let mut balancer = Balancer::default(); - assert_no_panic("Balancer::set_quote_weight invalid boundary weight", || { - assert!(balancer.set_quote_weight(Perquintill::zero()).is_err()); - }); - assert_eq!( - balancer.get_quote_weight(), - Perquintill::from_rational(1u128, 2u128) - ); - } - - #[test] - fn test_balancer_extreme_exp_inputs_do_not_panic() { - let weights = [ - MIN_WEIGHT, - Perquintill::from_rational(1u128, 2u128), - ONE.saturating_sub(MIN_WEIGHT), - ]; - let inputs = [ - (0u64, 0u64), - (0u64, 1u64), - (1u64, 0u64), - (1u64, 1u64), - (1u64, u64::MAX), - (u64::MAX, 0u64), - (u64::MAX, 1u64), - (u64::MAX, u64::MAX), - ]; - - for quote in weights { - let balancer = Balancer::new(quote).unwrap(); - for (reserve, delta) in inputs { - assert_no_panic("exp_base_quote extreme input", || { - let _ = balancer.exp_base_quote(reserve, delta); - }); - assert_no_panic("exp_quote_base extreme input", || { - let _ = balancer.exp_quote_base(reserve, delta); - }); - assert_no_panic("exp_scaled negative extreme input", || { - let _ = balancer.exp_scaled(reserve, -(delta as i128), true); - let _ = balancer.exp_scaled(reserve, -(delta as i128), false); - }); - } - } - } - - #[test] - fn test_balancer_price_and_limit_delta_corner_cases_do_not_panic() { - let balancer = Balancer::new(MIN_WEIGHT).unwrap(); - let prices = [ - U64F64::from_num(0), - U64F64::from_num(1), - U64F64::from_num(u64::MAX), - ]; - let reserves = [0u64, 1u64, u64::MAX]; - - for x in reserves { - for y in reserves { - assert_no_panic("calculate_price corner reserves", || { - let _ = balancer.calculate_price(x, y); - }); - } - } - - for current_price in prices { - for target_price in prices { - for reserve in reserves { - assert_no_panic("calculate_quote_delta_in corner input", || { - let _ = - balancer.calculate_quote_delta_in(current_price, target_price, reserve); - }); - assert_no_panic("calculate_base_delta_in corner input", || { - let _ = - balancer.calculate_base_delta_in(current_price, target_price, reserve); - }); - } - } - } - } - - #[test] - fn test_balancer_liquidity_weight_update_extremes_do_not_panic() { - let inputs = [ - (0u64, 0u64, 0u64, 0u64), - (0u64, 0u64, u64::MAX, u64::MAX), - (0u64, u64::MAX, u64::MAX, 0u64), - (u64::MAX, 0u64, 0u64, u64::MAX), - (u64::MAX, u64::MAX, u64::MAX, u64::MAX), - (1u64, u64::MAX, u64::MAX, 1u64), - (u64::MAX, 1u64, 1u64, u64::MAX), - ]; - - for (tao_reserve, alpha_reserve, tao_delta, alpha_delta) in inputs { - let mut balancer = Balancer::default(); - assert_no_panic("update_weights_for_added_liquidity extreme input", || { - let _ = balancer.update_weights_for_added_liquidity( - tao_reserve, - alpha_reserve, - tao_delta, - alpha_delta, - ); - }); - } - } - - #[test] - fn test_balancer_base_needed_for_quote_extremes_do_not_panic() { - let balancer = Balancer::new(ONE.saturating_sub(MIN_WEIGHT)).unwrap(); - let inputs = [ - (0u64, 0u64, 0u64), - (0u64, 1u64, 1u64), - (1u64, 0u64, 1u64), - (1u64, 1u64, 0u64), - (1u64, 1u64, 1u64), - (1u64, 1u64, u64::MAX), - (u64::MAX, u64::MAX, 0u64), - (u64::MAX, u64::MAX, u64::MAX), - ]; - - for (tao_reserve, alpha_reserve, delta_tao) in inputs { - assert_no_panic("get_base_needed_for_quote extreme input", || { - let _ = balancer.get_base_needed_for_quote(tao_reserve, alpha_reserve, delta_tao); - }); - } - } - - #[test] - fn test_safe_bigmath_pow_ratio_internal_paths_do_not_panic() { - let base_num = SafeInt::from(999_999_937u64); - let base_den = SafeInt::from(1_000_000_003u64); - let scale = SafeInt::from(1_000_000u64); - let cases = [ - // Exact integer/root path with exponent values at the safe-bigmath threshold. - ( - SafeInt::from(1024u32), - SafeInt::one(), - "exact max numerator", - ), - ( - SafeInt::from(999u32), - SafeInt::from(1024u32), - "exact root denominator", - ), - // One step over the threshold forces the fixed-point ln/exp fallback path. - (SafeInt::from(1025u32), SafeInt::one(), "fallback numerator"), - ( - SafeInt::from(999u32), - SafeInt::from(1025u32), - "fallback denominator", - ), - // GCD reduction should route this back to the exact path. - ( - SafeInt::from(2048u32), - SafeInt::from(4096u32), - "gcd reduced", - ), - ]; - - for (exp_num, exp_den, label) in cases { - let result = assert_no_panic(label, || { - SafeInt::pow_ratio_scaled(&base_num, &base_den, &exp_num, &exp_den, 64, &scale) - }); - assert!(result.is_some(), "{label} should produce a result"); - } - } - - #[test] - fn test_balancer_near_equal_weights_with_tiny_delta_do_not_panic() { - let weights = [ - Perquintill::from_parts(500_000_000_500_000_000), - Perquintill::from_parts(499_999_999_500_000_000), - Perquintill::from_parts(500_000_000_000_500_000), - Perquintill::from_parts(499_999_999_999_500_000), - ]; - let reserve = 21_000_000_000_000_000u64; - let tiny_deltas = [1u64, 100u64, 100_000u64]; - - for quote in weights { - let balancer = Balancer::new(quote).unwrap(); - for delta in tiny_deltas { - assert_no_panic("near-equal exp_base_quote tiny delta", || { - let e = balancer.exp_base_quote(reserve, delta); - assert!(e <= U64F64::from_num(1)); - assert!(e > U64F64::from_num(0)); - }); - assert_no_panic("near-equal exp_quote_base tiny delta", || { - let e = balancer.exp_quote_base(reserve, delta); - assert!(e <= U64F64::from_num(1)); - assert!(e > U64F64::from_num(0)); - }); - } - } - } - - #[test] - fn test_balancer_log_normalization_reserve_shapes_do_not_panic() { - let balancer = Balancer::new(Perquintill::from_parts(500_000_000_500_000_000)).unwrap(); - let reserves = [ - (1u64 << 42) - 1, - 1u64 << 42, - (1u64 << 42) + 1, - ((1u64 << 42) + (1u64 << 41)) - 1, - (1u64 << 42) + (1u64 << 41), - ((1u64 << 42) + (1u64 << 41)) + 1, - ]; - - for reserve in reserves { - for delta in [1u64, reserve / 1_000, reserve / 2] { - assert_no_panic("log-normalization exp_base_quote", || { - let e = balancer.exp_base_quote(reserve, delta); - assert!(e <= U64F64::from_num(1)); - }); - assert_no_panic("log-normalization exp_quote_base", || { - let e = balancer.exp_quote_base(reserve, delta); - assert!(e <= U64F64::from_num(1)); - }); - } - } - } - - #[test] - fn test_perquintill_power() { - const PRECISION: u32 = 4096; - const PERQUINTILL: u128 = ACCURACY as u128; - - let x = SafeInt::from(21_000_000_000_000_000u64); - let delta = SafeInt::from(7_000_000_000_000_000u64); - let w1 = SafeInt::from(600_000_000_000_000_000u128); - let w2 = SafeInt::from(400_000_000_000_000_000u128); - let denominator = &x + δ - assert_eq!(w1.clone() + w2.clone(), SafeInt::from(PERQUINTILL)); - - let perquintill_result = SafeInt::pow_ratio_scaled( - &x, - &denominator, - &w1, - &w2, - PRECISION, - &SafeInt::from(PERQUINTILL), - ) - .expect("perquintill integer result"); - - assert_eq!( - perquintill_result, - SafeInt::from(649_519_052_838_328_985u128) - ); - let readable = safe_bigmath::SafeDec::<18>::from_raw(perquintill_result); - assert_eq!(format!("{}", readable), "0.649519052838328985"); - } - - /// Validate realistic values that can be calculated with f64 precision - #[test] - fn test_exp_base_quote_happy_path() { - // Outer test cases: w_quote - [ - Perquintill::from_rational(500_000_000_000_u128, 1_000_000_000_000_u128), - Perquintill::from_rational(500_000_000_001_u128, 1_000_000_000_000_u128), - Perquintill::from_rational(499_999_999_999_u128, 1_000_000_000_000_u128), - Perquintill::from_rational(500_000_000_100_u128, 1_000_000_000_000_u128), - Perquintill::from_rational(500_000_001_000_u128, 1_000_000_000_000_u128), - Perquintill::from_rational(500_000_010_000_u128, 1_000_000_000_000_u128), - Perquintill::from_rational(500_000_100_000_u128, 1_000_000_000_000_u128), - Perquintill::from_rational(500_001_000_000_u128, 1_000_000_000_000_u128), - Perquintill::from_rational(500_010_000_000_u128, 1_000_000_000_000_u128), - Perquintill::from_rational(500_100_000_000_u128, 1_000_000_000_000_u128), - Perquintill::from_rational(501_000_000_000_u128, 1_000_000_000_000_u128), - Perquintill::from_rational(510_000_000_000_u128, 1_000_000_000_000_u128), - Perquintill::from_rational(100_000_000_000_u128, 1_000_000_000_000_u128), - Perquintill::from_rational(100_000_000_001_u128, 1_000_000_000_000_u128), - Perquintill::from_rational(200_000_000_000_u128, 1_000_000_000_000_u128), - Perquintill::from_rational(300_000_000_000_u128, 1_000_000_000_000_u128), - Perquintill::from_rational(400_000_000_000_u128, 1_000_000_000_000_u128), - Perquintill::from_rational(600_000_000_000_u128, 1_000_000_000_000_u128), - Perquintill::from_rational(700_000_000_000_u128, 1_000_000_000_000_u128), - Perquintill::from_rational(800_000_000_000_u128, 1_000_000_000_000_u128), - Perquintill::from_rational(899_999_999_999_u128, 1_000_000_000_000_u128), - Perquintill::from_rational(900_000_000_000_u128, 1_000_000_000_000_u128), - Perquintill::from_rational( - 102_337_248_363_782_924_u128, - 1_000_000_000_000_000_000_u128, - ), - ] - .into_iter() - .for_each(|w_quote| { - // Inner test cases: y, x, ∆x - [ - (1_000_u64, 1_000_u64, 0_u64), - (1_000_u64, 1_000_u64, 1_u64), - (1_500_u64, 1_000_u64, 1_u64), - ( - 1_000_000_000_000_u64, - 100_000_000_000_000_u64, - 100_000_000_u64, - ), - ( - 1_000_000_000_000_u64, - 100_000_000_000_000_u64, - 100_000_000_u64, - ), - ( - 100_000_000_000_u64, - 100_000_000_000_000_u64, - 100_000_000_u64, - ), - (100_000_000_000_u64, 100_000_000_000_000_u64, 1_000_000_u64), - ( - 100_000_000_000_u64, - 100_000_000_000_000_u64, - 1_000_000_000_000_u64, - ), - ( - 1_000_000_000_u64, - 100_000_000_000_000_u64, - 1_000_000_000_000_u64, - ), - ( - 1_000_000_u64, - 100_000_000_000_000_u64, - 1_000_000_000_000_u64, - ), - (1_000_u64, 100_000_000_000_000_u64, 1_000_000_000_000_u64), - (1_000_u64, 100_000_000_000_000_u64, 1_000_000_000_u64), - (1_000_u64, 100_000_000_000_000_u64, 1_000_000_u64), - (1_000_u64, 100_000_000_000_000_u64, 1_000_u64), - (1_000_u64, 100_000_000_000_000_u64, 100_000_000_000_000_u64), - (10_u64, 100_000_000_000_000_u64, 100_000_000_000_000_u64), - // Extreme values of ∆x for small x - (1_000_000_000_u64, 4_000_000_000_u64, 1_000_000_000_000_u64), - (1_000_000_000_000_u64, 1_000_u64, 1_000_000_000_000_u64), - ( - 5_628_038_062_729_553_u64, - 400_775_553_u64, - 14_446_633_907_665_582_u64, - ), - ( - 5_600_000_000_000_000_u64, - 400_000_000_u64, - 14_000_000_000_000_000_u64, - ), - ] - .into_iter() - .for_each(|(y, x, dx)| { - let bal = Balancer::new(w_quote).unwrap(); - let e1 = bal.exp_base_quote(x, dx); - let e2 = bal.exp_quote_base(x, dx); - let one = U64F64::from_num(1); - let y_fixed = U64F64::from_num(y); - let dy1 = y_fixed * (one - e1); - let dy2 = y_fixed * (one - e2); - - if dx > x.saturating_mul(1_000) { - assert!(e1 <= one); - assert!(e2 <= one); - return; - } - - let w1 = perquintill_to_f64(bal.get_base_weight()); - let w2 = perquintill_to_f64(bal.get_quote_weight()); - let e1_expected = (x as f64 / (x as f64 + dx as f64)).powf(w1 / w2); - let dy1_expected = y as f64 * (1. - e1_expected); - let e2_expected = (x as f64 / (x as f64 + dx as f64)).powf(w2 / w1); - let dy2_expected = y as f64 * (1. - e2_expected); - - // Start tolerance with 0.001 rao - let mut eps1 = 0.001; - let mut eps2 = 0.001; - - // If swapping more than 100k tao/alpha, relax tolerance to 1.0 rao - if dy1_expected > 100_000_000_000_000_f64 { - eps1 = 1.0; - } - if dy2_expected > 100_000_000_000_000_f64 { - eps2 = 1.0; - } - assert_abs_diff_eq!(f(dy1), dy1_expected, epsilon = eps1); - assert_abs_diff_eq!(f(dy2), dy2_expected, epsilon = eps2); - }) - }); - } - - /// This test exercises practical application edge cases of exp_base_quote - /// The practical formula where this function is used: - /// ∆y = y * (exp_base_quote(x, ∆x) - 1) - /// - /// The test validates that two different sets of parameters produce (sensibly) - /// different results - /// - #[test] - fn test_exp_base_quote_dy_precision() { - // Test cases: y, x1, ∆x1, w_quote1, x2, ∆x2, w_quote2 - // Realized dy1 should be greater than dy2 - [ - ( - 1_000_000_000_u64, - 21_000_000_000_000_000_u64, - 21_000_000_000_u64, - Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), - 21_000_000_000_000_000_u64, - 21_000_000_000_u64, - Perquintill::from_rational(1_000_000_000_001_u128, 2_000_000_000_000_u128), - ), - ( - 1_000_000_000_u64, - 21_000_000_000_000_000_u64, - 21_000_000_000_u64, - Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_001_u128), - 21_000_000_000_000_000_u64, - 21_000_000_000_u64, - Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), - ), - ( - 1_000_000_000_u64, - 21_000_000_000_000_000_u64, - 2_u64, - Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), - 21_000_000_000_000_000_u64, - 1_u64, - Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), - ), - ( - 1_000_000_000_u64, - 21_000_000_000_000_000_u64, - 1_u64, - Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), - 21_000_000_000_000_000_u64, - 1_u64, - Perquintill::from_rational(1_010_000_000_000_u128, 2_000_000_000_000_u128), - ), - ( - 1_000_000_000_u64, - 21_000_000_000_000_000_u64, - 1_u64, - Perquintill::from_rational(1_000_000_000_000_u128, 2_010_000_000_000_u128), - 21_000_000_000_000_000_u64, - 1_u64, - Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), - ), - ] - .into_iter() - .for_each(|(y, x1, dx1, w_quote1, x2, dx2, w_quote2)| { - let bal1 = Balancer::new(w_quote1).unwrap(); - let bal2 = Balancer::new(w_quote2).unwrap(); - - let exp1 = bal1.exp_base_quote(x1, dx1); - let exp2 = bal2.exp_base_quote(x2, dx2); - - let one = U64F64::from_num(1); - let y_fixed = U64F64::from_num(y); - let dy1 = y_fixed * (one - exp1); - let dy2 = y_fixed * (one - exp2); - - assert!(dy1 > dy2); - - let zero = U64F64::from_num(0); - assert!(dy1 != zero); - assert!(dy2 != zero); - }) - } - - /// Test the broad range of w_quote values, usually should be ignored - #[ignore] - #[test] - fn test_exp_quote_broad_range() { - let y = 1_000_000_000_000_u64; - let x = 100_000_000_000_000_u64; - let dx = 10_000_000_u64; - - let mut prev = U64F64::from_num(1_000_000_000); - let mut last_progress = 0.; - let start = 100_000_000_000_u128; - let stop = 900_000_000_000_u128; - for num in (start..=stop).step_by(1000_usize) { - let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); - let bal = Balancer::new(w_quote).unwrap(); - let e = bal.exp_base_quote(x, dx); - - let one = U64F64::from_num(1); - let dy = U64F64::from_num(y) * (one - e); - - let progress = (num as f64 - start as f64) / (stop as f64 - start as f64); - if progress - last_progress >= 0.0001 { - // Replace with println for real-time progress - log::debug!("progress = {:?}%", progress * 100.); - log::debug!("dy = {:?}", dy); - last_progress = progress; - } - - assert!(dy != U64F64::from_num(0)); - assert!(dy <= prev); - prev = dy; - } - } - - // cargo test --package pallet-subtensor-swap --lib -- pallet::balancer::tests::test_exp_quote_fuzzy --include-ignored --exact --nocapture - #[ignore] - #[test] - fn test_exp_quote_fuzzy() { - use rand::rngs::StdRng; - use rand::{Rng, SeedableRng}; - use rayon::prelude::*; - use std::sync::Arc; - use std::sync::atomic::{AtomicUsize, Ordering}; - - const ITERATIONS: usize = 1_000_000_000; - let counter = Arc::new(AtomicUsize::new(0)); - - (0..ITERATIONS) - .into_par_iter() - .for_each(|i| { - // Each iteration gets its own deterministic RNG. - // Seed depends on i, so runs are reproducible. - let mut rng = StdRng::seed_from_u64(42 + i as u64); - let max_supply: u64 = 21_000_000_000_000_000; - let full_range = true; - - let x: u64 = rng.gen_range(1_000..=max_supply); // Alpha reserve - let y: u64 = if full_range { - // TAO reserve (allow huge prices) - rng.gen_range(1_000..=max_supply) - } else { - // TAO reserve (limit prices with 0-1000) - rng.gen_range(1_000..x.saturating_mul(1000).min(max_supply)) - }; - let dx: u64 = if full_range { - // Alhpa sold (allow huge values) - rng.gen_range(1_000..=21_000_000_000_000_000) - } else { - // Alhpa sold (do not sell more than 100% of what's in alpha reserve) - rng.gen_range(1_000..=x) - }; - let w_numerator: u64 = rng.gen_range(ACCURACY / 10..=ACCURACY / 10 * 9); - let w_quote = Perquintill::from_rational(w_numerator, ACCURACY); - - let bal = Balancer::new(w_quote).unwrap(); - let e = bal.exp_base_quote(x, dx); - - let one = U64F64::from_num(1); - let dy = U64F64::from_num(y) * (one - e); - - // Calculate expected in f64 and approx-assert - let w1 = perquintill_to_f64(bal.get_base_weight()); - let w2 = perquintill_to_f64(bal.get_quote_weight()); - let e_expected = (x as f64 / (x as f64 + dx as f64)).powf(w1 / w2); - let dy_expected = y as f64 * (1. - e_expected); - - let actual = dy.to_num::(); - let eps = (dy_expected / 1_000_000.).clamp(1.0, 1000.0); - - assert!( - (actual - dy_expected).abs() <= eps, - "dy mismatch:\n actual: {}\n expected: {}\n eps: {}\nParameters:\n x: {}\n y: {}\n dx: {}\n w_numerator: {}\n", - actual, dy_expected, eps, x, y, dx, w_numerator, - ); - - // Assert that we aren't giving out more than reserve y - assert!(dy <= y, "dy = {},\ny = {}", dy, y,); - - // Print progress - let done = counter.fetch_add(1, Ordering::Relaxed) + 1; - if done % 10_000_000 == 0 { - let progress = done as f64 / ITERATIONS as f64 * 100.0; - // Replace with println for real-time progress - log::debug!("progress = {progress:.4}%"); - } - }); - } - - #[test] - fn test_calculate_quote_delta_in() { - let num = 250_000_000_000_u128; // w1 = 0.75 - let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); - let bal = Balancer::new(w_quote).unwrap(); - - let current_price: U64F64 = U64F64::from_num(0.1); - let target_price: U64F64 = U64F64::from_num(0.2); - let tao_reserve: u64 = 1_000_000_000; - - let dy = bal.calculate_quote_delta_in(current_price, target_price, tao_reserve); - - // ∆y = y•[(p'/p)^w1 - 1] - let dy_expected = tao_reserve as f64 - * ((target_price.to_num::() / current_price.to_num::()).powf(0.75) - 1.0); - - assert_eq!(dy, dy_expected as u64,); - } - - #[test] - fn test_calculate_base_delta_in() { - let num = 250_000_000_000_u128; // w2 = 0.25 - let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); - let bal = Balancer::new(w_quote).unwrap(); - - let current_price: U64F64 = U64F64::from_num(0.2); - let target_price: U64F64 = U64F64::from_num(0.1); - let alpha_reserve: u64 = 1_000_000_000; - - let dx = bal.calculate_base_delta_in(current_price, target_price, alpha_reserve); - - // ∆x = x•[(p/p')^w2 - 1] - let dx_expected = alpha_reserve as f64 - * ((current_price.to_num::() / target_price.to_num::()).powf(0.25) - 1.0); - - assert_eq!(dx, dx_expected as u64,); - } - - #[test] - fn test_calculate_quote_delta_in_impossible() { - let num = 250_000_000_000_u128; // w1 = 0.75 - let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); - let bal = Balancer::new(w_quote).unwrap(); - - // Impossible price (lower) - let current_price: U64F64 = U64F64::from_num(0.1); - let target_price: U64F64 = U64F64::from_num(0.05); - let tao_reserve: u64 = 1_000_000_000; - - let dy = bal.calculate_quote_delta_in(current_price, target_price, tao_reserve); - let dy_expected = 0u64; - - assert_eq!(dy, dy_expected); - } - - #[test] - fn test_calculate_base_delta_in_impossible() { - let num = 250_000_000_000_u128; // w2 = 0.25 - let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); - let bal = Balancer::new(w_quote).unwrap(); - - // Impossible price (higher) - let current_price: U64F64 = U64F64::from_num(0.1); - let target_price: U64F64 = U64F64::from_num(0.2); - let alpha_reserve: u64 = 1_000_000_000; - - let dx = bal.calculate_base_delta_in(current_price, target_price, alpha_reserve); - let dx_expected = 0u64; - - assert_eq!(dx, dx_expected); - } - - #[test] - fn test_calculate_delta_in_reverse_swap() { - let num = 500_000_000_000_u128; - let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); - let bal = Balancer::new(w_quote).unwrap(); - - let current_price: U64F64 = U64F64::from_num(0.1); - let target_price: U64F64 = U64F64::from_num(0.2); - let tao_reserve: u64 = 1_000_000_000; - - // Here is the simple case of w1 = w2 = 0.5, so alpha = tao / price - let alpha_reserve: u64 = (tao_reserve as f64 / current_price.to_num::()) as u64; - - let dy = bal.calculate_quote_delta_in(current_price, target_price, tao_reserve); - let dx = alpha_reserve as f64 - * (1.0 - - (tao_reserve as f64 / (tao_reserve as f64 + dy as f64)) - .powf(num as f64 / (1_000_000_000_000 - num) as f64)); - - // Verify that buying with dy will in fact bring the price to target_price - let actual_price = bal.calculate_price(alpha_reserve - dx as u64, tao_reserve + dy); - assert_abs_diff_eq!( - actual_price.to_num::(), - target_price.to_num::(), - epsilon = target_price.to_num::() / 1_000_000_000. - ); - } - - #[test] - fn test_mul_round_zero_and_one() { - let v = 1_000_000u128; - - // p = 0 -> always 0 - assert_eq!(Balancer::mul_perquintill_round(Perquintill::zero(), v), 0); - - // p = 1 -> identity - assert_eq!(Balancer::mul_perquintill_round(Perquintill::one(), v), v); - } - - #[test] - fn test_mul_round_half_behaviour() { - // p = 1/2 - let p = Perquintill::from_rational(1u128, 2u128); - - // Check rounding around .5 boundaries - // value * 1/2, rounded to nearest - assert_eq!(Balancer::mul_perquintill_round(p, 0), 0); // 0.0 -> 0 - assert_eq!(Balancer::mul_perquintill_round(p, 1), 1); // 0.5 -> 1 (round up) - assert_eq!(Balancer::mul_perquintill_round(p, 2), 1); // 1.0 -> 1 - assert_eq!(Balancer::mul_perquintill_round(p, 3), 2); // 1.5 -> 2 - assert_eq!(Balancer::mul_perquintill_round(p, 4), 2); // 2.0 -> 2 - assert_eq!(Balancer::mul_perquintill_round(p, 5), 3); // 2.5 -> 3 - assert_eq!(Balancer::mul_perquintill_round(p, 1023), 512); // 511.5 -> 512 - assert_eq!(Balancer::mul_perquintill_round(p, 1025), 513); // 512.5 -> 513 - } - - #[test] - fn test_mul_round_third_behaviour() { - // p = 1/3 - let p = Perquintill::from_rational(1u128, 3u128); - - // value * 1/3, rounded to nearest - assert_eq!(Balancer::mul_perquintill_round(p, 3), 1); // 1.0 -> 1 - assert_eq!(Balancer::mul_perquintill_round(p, 4), 1); // 1.333... -> 1 - assert_eq!(Balancer::mul_perquintill_round(p, 5), 2); // 1.666... -> 2 - assert_eq!(Balancer::mul_perquintill_round(p, 6), 2); // 2.0 -> 2 - } - - #[test] - fn test_mul_round_large_values_simple_rational() { - // p = 7/10 (exact in perquintill: 0.7) - let p = Perquintill::from_rational(7u128, 10u128); - let v: u128 = 1_000_000_000_000_000_000; - - let res = Balancer::mul_perquintill_round(p, v); - - // Expected = round(0.7 * v) with pure integer math: - // round(v * 7 / 10) = (v*7 + 10/2) / 10 - let expected = (v.saturating_mul(7) + 10 / 2) / 10; - - assert_eq!(res, expected); - } - - #[test] - fn test_mul_round_max_value_with_one() { - let v = u128::MAX; - let p = ONE; - - // For p = 1, result must be exactly value, and must not overflow - let res = Balancer::mul_perquintill_round(p, v); - assert_eq!(res, v); - } - - #[test] - fn test_price_with_equal_weights_is_y_over_x() { - // quote = 0.5, base = 0.5 -> w1 / w2 = 1, so price = y/x - let quote = Perquintill::from_rational(1u128, 2u128); - let bal = Balancer::new(quote).unwrap(); - - let x = 2u64; - let y = 5u64; - - let price = bal.calculate_price(x, y); - let price_f = f(price); - - let expected_f = (y as f64) / (x as f64); - assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-12); - } - - #[test] - fn test_price_scales_with_weight_ratio_two_to_one() { - // Assume base = 1 - quote. - // quote = 1/3 -> base = 2/3, so w1 / w2 = 2. - // Then price = 2 * (y/x). - let quote = Perquintill::from_rational(1u128, 3u128); - let bal = Balancer::new(quote).unwrap(); - - let x = 4u64; - let y = 10u64; - - let price_f = f(bal.calculate_price(x, y)); - let expected_f = 2.0 * (y as f64 / x as f64); - - assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-10); - } - - #[test] - fn test_price_is_zero_when_y_is_zero() { - // If y = 0, y/x = 0 so price must be 0 regardless of weights (for x > 0). - let quote = Perquintill::from_rational(3u128, 10u128); // 0.3 - let bal = Balancer::new(quote).unwrap(); - - let x = 10u64; - let y = 0u64; - - let price_f = f(bal.calculate_price(x, y)); - assert_abs_diff_eq!(price_f, 0.0, epsilon = 0.0); - } - - #[test] - fn test_price_invariant_when_scaling_x_and_y_with_equal_weights() { - // For equal weights, price(x, y) == price(kx, ky). - let quote = Perquintill::from_rational(1u128, 2u128); // 0.5 - let bal = Balancer::new(quote).unwrap(); - - let x1 = 3u64; - let y1 = 7u64; - let k = 10u64; - let x2 = x1 * k; - let y2 = y1 * k; - - let p1 = f(bal.calculate_price(x1, y1)); - let p2 = f(bal.calculate_price(x2, y2)); - - assert_abs_diff_eq!(p1, p2, epsilon = 1e-12); - } - - #[test] - fn test_price_matches_formula_for_general_quote() { - // General check: price = (w1 / w2) * (y/x), - // where w1 = base_weight, w2 = quote_weight. - // Here we assume get_base_weight = 1 - quote. - let quote = Perquintill::from_rational(2u128, 5u128); // 0.4 - let bal = Balancer::new(quote).unwrap(); - - let x = 9u64; - let y = 25u64; - - let price_f = f(bal.calculate_price(x, y)); - - let base = Perquintill::one() - quote; - let w1 = base.deconstruct() as f64; - let w2 = quote.deconstruct() as f64; - - let expected_f = (w1 / w2) * (y as f64 / x as f64); - assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-9); - } - - #[test] - fn test_price_high_values_non_equal_weights() { - // Non-equal weights, high x and y (up to 21e15) - let quote = Perquintill::from_rational(3u128, 10u128); // 0.3 - let bal = Balancer::new(quote).unwrap(); - - let x: u64 = 21_000_000_000_000_000; - let y: u64 = 15_000_000_000_000_000; - - let price = bal.calculate_price(x, y); - let price_f = f(price); - - // Expected: (w1 / w2) * (y / x), using Balancer's actual weights - let w1 = bal.get_base_weight().deconstruct() as f64; - let w2 = bal.get_quote_weight().deconstruct() as f64; - let expected_f = (w1 / w2) * (y as f64 / x as f64); - - assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-9); - } - - // cargo test --package pallet-subtensor-swap --lib -- pallet::balancer::tests::test_exp_scaled --exact --nocapture - #[test] - fn test_exp_scaled() { - [ - // base_weight_numerator, base_weight_denominator, reserve, d_reserve, base_quote - (5_u64, 10_u64, 100000_u64, 100_u64, true, 0.999000999000999), - (1_u64, 4_u64, 500000_u64, 5000_u64, true, 0.970590147927644), - (3_u64, 4_u64, 200000_u64, 2000_u64, false, 0.970590147927644), - ( - 9_u64, - 10_u64, - 13513642_u64, - 1673_u64, - false, - 0.998886481979889, - ), - ( - 773_u64, - 1000_u64, - 7_000_000_000_u64, - 10_000_u64, - true, - 0.999999580484586, - ), - ] - .into_iter() - .map(|v| { - ( - Perquintill::from_rational(v.0, v.1), - v.2, - v.3, - v.4, - U64F64::from_num(v.5), - ) - }) - .for_each(|(quote_weight, reserve, d_reserve, base_quote, expected)| { - let balancer = Balancer::new(quote_weight).unwrap(); - let result = balancer.exp_scaled(reserve, d_reserve as i128, base_quote); - assert_abs_diff_eq!( - result.to_num::(), - expected.to_num::(), - epsilon = 0.000000001 - ); - }); - } - - // cargo test --package pallet-subtensor-swap --lib -- pallet::balancer::tests::test_base_needed_for_quote --exact --nocapture - #[test] - fn test_base_needed_for_quote() { - let num = 250_000_000_000_u128; // w1 = 0.75 - let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); - let bal = Balancer::new(w_quote).unwrap(); - - let tao_reserve: u64 = 1_000_000_000; - let alpha_reserve: u64 = 1_000_000_000; - let tao_delta: u64 = 1_123_432; // typical fee range - - let dx = bal.get_base_needed_for_quote(tao_reserve, alpha_reserve, tao_delta); - - // ∆x = x•[(y/(y+∆y))^(w2/w1) - 1] - let dx_expected = tao_reserve as f64 - * ((tao_reserve as f64 / ((tao_reserve - tao_delta) as f64)).powf(0.25 / 0.75) - 1.0); - - assert_eq!(dx, dx_expected as u64,); - } -} diff --git a/pallets/swap/src/pallet/balancer/mod.rs b/pallets/swap/src/pallet/balancer/mod.rs new file mode 100644 index 0000000000..41b58c33f5 --- /dev/null +++ b/pallets/swap/src/pallet/balancer/mod.rs @@ -0,0 +1,383 @@ +//! Weighted-balancer AMM math for TAO (quote) ↔ alpha (base) swaps. +//! +//! Unlike Uniswap v2/v3, liquidity may be added off-price via weights `w1 + w2 = 1` +//! (`w1` = base/alpha, `w2` = quote/TAO). Only quote weight is stored; base = `1 - quote`. +//! +//! Formulas: +//! - Price: `p = (w1*y) / (w2*x)` +//! - Sell (`∆x` given): `∆y = y * ((x / (x+∆x))^(w1/w2) - 1)` +//! - Buy (`∆y` given): `∆x = x * ((y / (y+∆y))^(w2/w1) - 1)` +//! - Limit sell to `p' < p`: `∆x = x * ((p / p')^w2 - 1)` +//! - Limit buy to `p' > p`: `∆y = y * ((p' / p)^w1 - 1)` +//! - Init from reserves + price: `w1 = px / (px + y)`, `w2 = y / (px + y)` +//! - Reweight after injection (price-preserving): +//! `new_w2 = (y + ∆y) / (p * (x + ∆x) + y + ∆y)` +//! +//! Weights are clamped to stay away from `{0,1}` so exponentiation stays stable; failed +//! injections go to per-subnet reservoirs instead of moving price. + +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::pallet_prelude::*; +use safe_bigmath::*; +use safe_math::*; +use sp_arithmetic::Perquintill; +use sp_core::U256; +use sp_runtime::Saturating; +use sp_std::ops::Neg; +use substrate_fixed::types::U64F64; +use subtensor_macros::freeze_struct; + +/// Balancer implements all high complexity math for swap operations such as: +/// - Swapping x for y, which includes limit orders +/// - Adding and removing liquidity (including unbalanced) +/// +/// Notation used in this file: +/// - x: Base reserve (alpha reserve) +/// - y: Quote reserve (tao reserve) +/// - ∆x: Alpha paid in/out +/// - ∆y: Tao paid in/out +/// - w1: Base weight (a.k.a weight_base) +/// - w2: Quote weight (a.k.a weight_quote) +#[freeze_struct("33a4fb0774da77c7")] +#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct Balancer { + quote: Perquintill, +} + +/// Accuracy matches to 18 decimal digits used to represent weights +pub const ACCURACY: u64 = 1_000_000_000_000_000_000_u64; +/// Lower limit of weights is 0.01 +pub const MIN_WEIGHT: Perquintill = Perquintill::from_parts(ACCURACY / 100); +/// 1.0 in Perquintill +pub const ONE: Perquintill = Perquintill::from_parts(ACCURACY); + +#[derive(Debug)] +pub enum BalancerError { + /// The provided weight value is out of range + InvalidValue, +} + +impl Default for Balancer { + /// The default value of weights is 0.5 for pool initialization + fn default() -> Self { + Self { + quote: Perquintill::from_rational(1u128, 2u128), + } + } +} + +impl Balancer { + /// Creates a new instance of balancer with a given quote weight + pub fn new(quote: Perquintill) -> Result { + if Self::check_constraints(quote) { + Ok(Balancer { quote }) + } else { + Err(BalancerError::InvalidValue) + } + } + + /// Constraints limit balancer weights within certain range of values: + /// - Both weights are above minimum + /// - Sum of weights is equal to 1.0 + fn check_constraints(quote: Perquintill) -> bool { + let base = ONE.saturating_sub(quote); + (base >= MIN_WEIGHT) && (quote >= MIN_WEIGHT) + } + + /// We store quote weight as Perquintill + pub fn get_quote_weight(&self) -> Perquintill { + self.quote + } + + /// Base weight is calculated as 1.0 - quote_weight + pub fn get_base_weight(&self) -> Perquintill { + ONE.saturating_sub(self.quote) + } + + /// Sets quote currency weight in the balancer. + /// Because sum of weights is always 1.0, there is no need to + /// store base currency weight + pub fn set_quote_weight(&mut self, new_value: Perquintill) -> Result<(), BalancerError> { + if Self::check_constraints(new_value) { + self.quote = new_value; + Ok(()) + } else { + Err(BalancerError::InvalidValue) + } + } + + /// If base_quote is true, calculate (x / (x + ∆x))^(weight_base / weight_quote), + /// otherwise, calculate (x / (x + ∆x))^(weight_quote / weight_base) + /// + /// Here we use SafeInt from bigmath crate for high-precision exponentiation, + /// which exposes the function pow_ratio_scaled. + /// + /// Note: ∆x may be negative + fn exp_scaled(&self, x: u64, dx: i128, base_quote: bool) -> U64F64 { + let x_plus_dx = if dx >= 0 { + x.saturating_add(dx as u64) + } else { + x.saturating_sub(dx.neg() as u64) + }; + + if x_plus_dx == 0 { + return U64F64::saturating_from_num(0); + } + let w1: u128 = self.get_base_weight().deconstruct() as u128; + let w2: u128 = self.get_quote_weight().deconstruct() as u128; + + let precision = 256; + let x_safe = SafeInt::from(x); + let w1_safe = SafeInt::from(w1); + let w2_safe = SafeInt::from(w2); + let perquintill_scale = SafeInt::from(ACCURACY as u128); + let denominator = SafeInt::from(x_plus_dx); + log::debug!("x = {:?}", x); + log::debug!("dx = {:?}", dx); + log::debug!("x_safe = {:?}", x_safe); + log::debug!("denominator = {:?}", denominator); + log::debug!("w1_safe = {:?}", w1_safe); + log::debug!("w2_safe = {:?}", w2_safe); + log::debug!("precision = {:?}", precision); + log::debug!("perquintill_scale = {:?}", perquintill_scale); + + let maybe_result_safe_int = if base_quote { + SafeInt::pow_ratio_scaled( + &x_safe, + &denominator, + &w1_safe, + &w2_safe, + precision, + &perquintill_scale, + ) + } else { + SafeInt::pow_ratio_scaled( + &x_safe, + &denominator, + &w2_safe, + &w1_safe, + precision, + &perquintill_scale, + ) + }; + + if let Some(result_safe_int) = maybe_result_safe_int + && let Some(result_u64) = result_safe_int.to_u64() + { + let result = U64F64::saturating_from_num(result_u64) + .safe_div(U64F64::saturating_from_num(ACCURACY)); + return if dx >= 0 { + result.min(U64F64::from_num(1)) + } else { + result + }; + } + U64F64::saturating_from_num(0) + } + + /// Calculates exponent of (x / (x + ∆x)) ^ (w_base/w_quote) + /// This method is used in sell swaps + /// (∆x is given by user, ∆y is paid out by the pool) + pub fn exp_base_quote(&self, x: u64, dx: u64) -> U64F64 { + self.exp_scaled(x, dx as i128, true) + } + + /// Calculates exponent of (y / (y + ∆y)) ^ (w_quote/w_base) + /// This method is used in buy swaps + /// (∆y is given by user, ∆x is paid out by the pool) + pub fn exp_quote_base(&self, y: u64, dy: u64) -> U64F64 { + self.exp_scaled(y, dy as i128, false) + } + + /// Calculates price as (w1/w2) * (y/x), where + /// - w1 is base weight + /// - w2 is quote weight + /// - x is base reserve + /// - y is quote reserve + pub fn calculate_price(&self, x: u64, y: u64) -> U64F64 { + let w2_fixed = U64F64::saturating_from_num(self.get_quote_weight().deconstruct()); + let w1_fixed = U64F64::saturating_from_num(self.get_base_weight().deconstruct()); + let x_fixed = U64F64::saturating_from_num(x); + let y_fixed = U64F64::saturating_from_num(y); + w1_fixed + .safe_div(w2_fixed) + .saturating_mul(y_fixed.safe_div(x_fixed)) + } + + /// Multiply a u128 value by a Perquintill with u128 result rounded to the + /// nearest integer + fn mul_perquintill_round(p: Perquintill, value: u128) -> u128 { + let parts = p.deconstruct() as u128; + let acc = ACCURACY as u128; + + let num = U256::from(value).saturating_mul(U256::from(parts)); + let den = U256::from(acc); + + // Add 0.5 before integer division to achieve rounding to the nearest + // integer + let zero = U256::from(0); + let res = num + .saturating_add(den.checked_div(U256::from(2u8)).unwrap_or(zero)) + .checked_div(den) + .unwrap_or(zero); + res.min(U256::from(u128::MAX)) + .try_into() + .unwrap_or_default() + } + + /// When liquidity is added to balancer swap, it may be added with arbitrary proportion, + /// not necessarily in the proportion of price, like with uniswap v2 or v3. In order to + /// stay within balancer pool invariant, the weights need to be updated. Invariant: + /// + /// L = x ^ weight_base * y ^ weight_quote + /// + /// Note that weights must remain within the proper range (both be above MIN_WEIGHT), + /// so only reasonably small disproportions of updates are appropriate. + pub fn update_weights_for_added_liquidity( + &mut self, + tao_reserve: u64, + alpha_reserve: u64, + tao_delta: u64, + alpha_delta: u64, + ) -> Result<(), BalancerError> { + // Calculate new to-be reserves (do not update here) + let tao_reserve_u128 = u64::from(tao_reserve) as u128; + let alpha_reserve_u128 = u64::from(alpha_reserve) as u128; + let tao_delta_u128 = u64::from(tao_delta) as u128; + let alpha_delta_u128 = u64::from(alpha_delta) as u128; + let new_tao_reserve_u128 = tao_reserve_u128.saturating_add(tao_delta_u128); + let new_alpha_reserve_u128 = alpha_reserve_u128.saturating_add(alpha_delta_u128); + + // Calculate new weights + let quantity_1: u128 = Self::mul_perquintill_round( + self.get_base_weight(), + tao_reserve_u128.saturating_mul(new_alpha_reserve_u128), + ); + let quantity_2: u128 = Self::mul_perquintill_round( + self.get_quote_weight(), + alpha_reserve_u128.saturating_mul(new_tao_reserve_u128), + ); + let q_sum = quantity_1.saturating_add(quantity_2); + + // Calculate new reserve weights + let new_reserve_weight = if q_sum != 0 { + // Both TAO and Alpha are non-zero, normal case + Perquintill::from_rational(quantity_2, q_sum) + } else { + // Either TAO or Alpha reserve were and/or remain zero => Initialize weights to 0.5 + Perquintill::from_rational(1u128, 2u128) + }; + + self.set_quote_weight(new_reserve_weight) + } + + /// Calculates quote delta needed to reach the price up when byuing + /// This method is needed for limit orders. + /// + /// Formula is: + /// ∆y = y * ((price_new / price)^weight_base - 1) + /// price_new >= price + pub fn calculate_quote_delta_in( + &self, + current_price: U64F64, + target_price: U64F64, + reserve: u64, + ) -> u64 { + let base_numerator: u128 = target_price.to_bits(); + let base_denominator: u128 = current_price.to_bits(); + let w1_fixed: u128 = self.get_base_weight().deconstruct() as u128; + let scale: u128 = 10u128.pow(18); + + let maybe_exp_result = SafeInt::pow_ratio_scaled( + &SafeInt::from(base_numerator), + &SafeInt::from(base_denominator), + &SafeInt::from(w1_fixed), + &SafeInt::from(ACCURACY), + 1024, + &SafeInt::from(scale), + ); + + if let Some(exp_result_safe_int) = maybe_exp_result { + let reserve_fixed = U64F64::saturating_from_num(reserve); + let one = U64F64::saturating_from_num(1); + let scale_fixed = U64F64::saturating_from_num(scale); + let exp_result_fixed = if let Some(exp_result_u64) = exp_result_safe_int.to_u64() { + U64F64::saturating_from_num(exp_result_u64) + } else if u64::MAX < exp_result_safe_int { + U64F64::saturating_from_num(u64::MAX) + } else { + U64F64::saturating_from_num(0) + }; + reserve_fixed + .saturating_mul(exp_result_fixed.safe_div(scale_fixed).saturating_sub(one)) + .saturating_to_num::() + } else { + 0u64 + } + } + + /// Calculates base delta needed to reach the price down when selling + /// This method is needed for limit orders. + /// + /// Formula is: + /// ∆x = x * ((price / price_new)^weight_quote - 1) + /// price_new <= price + pub fn calculate_base_delta_in( + &self, + current_price: U64F64, + target_price: U64F64, + reserve: u64, + ) -> u64 { + let base_numerator: u128 = current_price.to_bits(); + let base_denominator: u128 = target_price.to_bits(); + let w2_fixed: u128 = self.get_quote_weight().deconstruct() as u128; + let scale: u128 = 10u128.pow(18); + + let maybe_exp_result = SafeInt::pow_ratio_scaled( + &SafeInt::from(base_numerator), + &SafeInt::from(base_denominator), + &SafeInt::from(w2_fixed), + &SafeInt::from(ACCURACY), + 1024, + &SafeInt::from(scale), + ); + + if let Some(exp_result_safe_int) = maybe_exp_result { + let one = U64F64::saturating_from_num(1); + let scale_fixed = U64F64::saturating_from_num(scale); + let reserve_fixed = U64F64::saturating_from_num(reserve); + let exp_result_fixed = if let Some(exp_result_u64) = exp_result_safe_int.to_u64() { + U64F64::saturating_from_num(exp_result_u64) + } else if u64::MAX < exp_result_safe_int { + U64F64::saturating_from_num(u64::MAX) + } else { + U64F64::saturating_from_num(0) + }; + reserve_fixed + .saturating_mul(exp_result_fixed.safe_div(scale_fixed).saturating_sub(one)) + .saturating_to_num::() + } else { + 0u64 + } + } + + /// Calculates amount of Alpha that needs to be sold to get a given amount of TAO + pub fn get_base_needed_for_quote( + &self, + tao_reserve: u64, + alpha_reserve: u64, + delta_tao: u64, + ) -> u64 { + let e = self.exp_scaled(tao_reserve, (delta_tao as i128).neg(), false); + let one = U64F64::from_num(1); + let alpha_reserve_fixed = U64F64::from_num(alpha_reserve); + // e > 1 in this case + alpha_reserve_fixed + .saturating_mul(e.saturating_sub(one)) + .saturating_to_num::() + } +} + +#[cfg(all(test, feature = "std"))] +#[allow(clippy::expect_used, clippy::unwrap_used)] +mod tests; diff --git a/pallets/swap/src/pallet/balancer/tests.rs b/pallets/swap/src/pallet/balancer/tests.rs new file mode 100644 index 0000000000..2c2a4d9451 --- /dev/null +++ b/pallets/swap/src/pallet/balancer/tests.rs @@ -0,0 +1,942 @@ +//! Unit tests for weighted-balancer AMM math (`Balancer`). +//! +//! Run: `cargo test -p pallet-subtensor-swap --lib -- pallet::balancer::tests --nocapture` + +use crate::pallet::Balancer; +use crate::pallet::balancer::*; +use approx::assert_abs_diff_eq; +use sp_arithmetic::Perquintill; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use substrate_fixed::types::U64F64; + +// Helper: convert Perquintill to f64 for comparison +fn perquintill_to_f64(p: Perquintill) -> f64 { + let parts = p.deconstruct() as f64; + parts / ACCURACY as f64 +} + +// Helper: convert U64F64 to f64 for comparison +fn f(v: U64F64) -> f64 { + v.to_num::() +} + +fn assert_no_panic(label: &str, f: F) -> R +where + F: FnOnce() -> R, +{ + catch_unwind(AssertUnwindSafe(f)).unwrap_or_else(|_| panic!("{label} panicked")) +} + +#[test] +fn test_balancer_rejects_invalid_boundary_weights_without_panicking() { + [ + Perquintill::zero(), + Perquintill::from_parts(1), + MIN_WEIGHT.saturating_sub(Perquintill::from_parts(1)), + ONE.saturating_sub(MIN_WEIGHT) + .saturating_add(Perquintill::from_parts(1)), + ONE, + ] + .into_iter() + .for_each(|quote| { + assert_no_panic("Balancer::new invalid boundary weight", || { + assert!(Balancer::new(quote).is_err()); + }); + }); + + let mut balancer = Balancer::default(); + assert_no_panic("Balancer::set_quote_weight invalid boundary weight", || { + assert!(balancer.set_quote_weight(Perquintill::zero()).is_err()); + }); + assert_eq!( + balancer.get_quote_weight(), + Perquintill::from_rational(1u128, 2u128) + ); +} + +#[test] +fn test_balancer_extreme_exp_inputs_do_not_panic() { + let weights = [ + MIN_WEIGHT, + Perquintill::from_rational(1u128, 2u128), + ONE.saturating_sub(MIN_WEIGHT), + ]; + let inputs = [ + (0u64, 0u64), + (0u64, 1u64), + (1u64, 0u64), + (1u64, 1u64), + (1u64, u64::MAX), + (u64::MAX, 0u64), + (u64::MAX, 1u64), + (u64::MAX, u64::MAX), + ]; + + for quote in weights { + let balancer = Balancer::new(quote).unwrap(); + for (reserve, delta) in inputs { + assert_no_panic("exp_base_quote extreme input", || { + let _ = balancer.exp_base_quote(reserve, delta); + }); + assert_no_panic("exp_quote_base extreme input", || { + let _ = balancer.exp_quote_base(reserve, delta); + }); + assert_no_panic("exp_scaled negative extreme input", || { + let _ = balancer.exp_scaled(reserve, -(delta as i128), true); + let _ = balancer.exp_scaled(reserve, -(delta as i128), false); + }); + } + } +} + +#[test] +fn test_balancer_price_and_limit_delta_corner_cases_do_not_panic() { + let balancer = Balancer::new(MIN_WEIGHT).unwrap(); + let prices = [ + U64F64::from_num(0), + U64F64::from_num(1), + U64F64::from_num(u64::MAX), + ]; + let reserves = [0u64, 1u64, u64::MAX]; + + for x in reserves { + for y in reserves { + assert_no_panic("calculate_price corner reserves", || { + let _ = balancer.calculate_price(x, y); + }); + } + } + + for current_price in prices { + for target_price in prices { + for reserve in reserves { + assert_no_panic("calculate_quote_delta_in corner input", || { + let _ = balancer.calculate_quote_delta_in(current_price, target_price, reserve); + }); + assert_no_panic("calculate_base_delta_in corner input", || { + let _ = balancer.calculate_base_delta_in(current_price, target_price, reserve); + }); + } + } + } +} + +#[test] +fn test_balancer_liquidity_weight_update_extremes_do_not_panic() { + let inputs = [ + (0u64, 0u64, 0u64, 0u64), + (0u64, 0u64, u64::MAX, u64::MAX), + (0u64, u64::MAX, u64::MAX, 0u64), + (u64::MAX, 0u64, 0u64, u64::MAX), + (u64::MAX, u64::MAX, u64::MAX, u64::MAX), + (1u64, u64::MAX, u64::MAX, 1u64), + (u64::MAX, 1u64, 1u64, u64::MAX), + ]; + + for (tao_reserve, alpha_reserve, tao_delta, alpha_delta) in inputs { + let mut balancer = Balancer::default(); + assert_no_panic("update_weights_for_added_liquidity extreme input", || { + let _ = balancer.update_weights_for_added_liquidity( + tao_reserve, + alpha_reserve, + tao_delta, + alpha_delta, + ); + }); + } +} + +#[test] +fn test_balancer_base_needed_for_quote_extremes_do_not_panic() { + let balancer = Balancer::new(ONE.saturating_sub(MIN_WEIGHT)).unwrap(); + let inputs = [ + (0u64, 0u64, 0u64), + (0u64, 1u64, 1u64), + (1u64, 0u64, 1u64), + (1u64, 1u64, 0u64), + (1u64, 1u64, 1u64), + (1u64, 1u64, u64::MAX), + (u64::MAX, u64::MAX, 0u64), + (u64::MAX, u64::MAX, u64::MAX), + ]; + + for (tao_reserve, alpha_reserve, delta_tao) in inputs { + assert_no_panic("get_base_needed_for_quote extreme input", || { + let _ = balancer.get_base_needed_for_quote(tao_reserve, alpha_reserve, delta_tao); + }); + } +} + +#[test] +fn test_safe_bigmath_pow_ratio_internal_paths_do_not_panic() { + let base_num = SafeInt::from(999_999_937u64); + let base_den = SafeInt::from(1_000_000_003u64); + let scale = SafeInt::from(1_000_000u64); + let cases = [ + // Exact integer/root path with exponent values at the safe-bigmath threshold. + ( + SafeInt::from(1024u32), + SafeInt::one(), + "exact max numerator", + ), + ( + SafeInt::from(999u32), + SafeInt::from(1024u32), + "exact root denominator", + ), + // One step over the threshold forces the fixed-point ln/exp fallback path. + (SafeInt::from(1025u32), SafeInt::one(), "fallback numerator"), + ( + SafeInt::from(999u32), + SafeInt::from(1025u32), + "fallback denominator", + ), + // GCD reduction should route this back to the exact path. + ( + SafeInt::from(2048u32), + SafeInt::from(4096u32), + "gcd reduced", + ), + ]; + + for (exp_num, exp_den, label) in cases { + let result = assert_no_panic(label, || { + SafeInt::pow_ratio_scaled(&base_num, &base_den, &exp_num, &exp_den, 64, &scale) + }); + assert!(result.is_some(), "{label} should produce a result"); + } +} + +#[test] +fn test_balancer_near_equal_weights_with_tiny_delta_do_not_panic() { + let weights = [ + Perquintill::from_parts(500_000_000_500_000_000), + Perquintill::from_parts(499_999_999_500_000_000), + Perquintill::from_parts(500_000_000_000_500_000), + Perquintill::from_parts(499_999_999_999_500_000), + ]; + let reserve = 21_000_000_000_000_000u64; + let tiny_deltas = [1u64, 100u64, 100_000u64]; + + for quote in weights { + let balancer = Balancer::new(quote).unwrap(); + for delta in tiny_deltas { + assert_no_panic("near-equal exp_base_quote tiny delta", || { + let e = balancer.exp_base_quote(reserve, delta); + assert!(e <= U64F64::from_num(1)); + assert!(e > U64F64::from_num(0)); + }); + assert_no_panic("near-equal exp_quote_base tiny delta", || { + let e = balancer.exp_quote_base(reserve, delta); + assert!(e <= U64F64::from_num(1)); + assert!(e > U64F64::from_num(0)); + }); + } + } +} + +#[test] +fn test_balancer_log_normalization_reserve_shapes_do_not_panic() { + let balancer = Balancer::new(Perquintill::from_parts(500_000_000_500_000_000)).unwrap(); + let reserves = [ + (1u64 << 42) - 1, + 1u64 << 42, + (1u64 << 42) + 1, + ((1u64 << 42) + (1u64 << 41)) - 1, + (1u64 << 42) + (1u64 << 41), + ((1u64 << 42) + (1u64 << 41)) + 1, + ]; + + for reserve in reserves { + for delta in [1u64, reserve / 1_000, reserve / 2] { + assert_no_panic("log-normalization exp_base_quote", || { + let e = balancer.exp_base_quote(reserve, delta); + assert!(e <= U64F64::from_num(1)); + }); + assert_no_panic("log-normalization exp_quote_base", || { + let e = balancer.exp_quote_base(reserve, delta); + assert!(e <= U64F64::from_num(1)); + }); + } + } +} + +#[test] +fn test_perquintill_power() { + const PRECISION: u32 = 4096; + const PERQUINTILL: u128 = ACCURACY as u128; + + let x = SafeInt::from(21_000_000_000_000_000u64); + let delta = SafeInt::from(7_000_000_000_000_000u64); + let w1 = SafeInt::from(600_000_000_000_000_000u128); + let w2 = SafeInt::from(400_000_000_000_000_000u128); + let denominator = &x + δ + assert_eq!(w1.clone() + w2.clone(), SafeInt::from(PERQUINTILL)); + + let perquintill_result = SafeInt::pow_ratio_scaled( + &x, + &denominator, + &w1, + &w2, + PRECISION, + &SafeInt::from(PERQUINTILL), + ) + .expect("perquintill integer result"); + + assert_eq!( + perquintill_result, + SafeInt::from(649_519_052_838_328_985u128) + ); + let readable = safe_bigmath::SafeDec::<18>::from_raw(perquintill_result); + assert_eq!(format!("{}", readable), "0.649519052838328985"); +} + +/// Validate realistic values that can be calculated with f64 precision +#[test] +fn test_exp_base_quote_happy_path() { + // Outer test cases: w_quote + [ + Perquintill::from_rational(500_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_000_000_001_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(499_999_999_999_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_000_000_100_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_000_001_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_000_010_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_000_100_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_001_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_010_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_100_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(501_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(510_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(100_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(100_000_000_001_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(200_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(300_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(400_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(600_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(700_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(800_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(899_999_999_999_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(900_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(102_337_248_363_782_924_u128, 1_000_000_000_000_000_000_u128), + ] + .into_iter() + .for_each(|w_quote| { + // Inner test cases: y, x, ∆x + [ + (1_000_u64, 1_000_u64, 0_u64), + (1_000_u64, 1_000_u64, 1_u64), + (1_500_u64, 1_000_u64, 1_u64), + ( + 1_000_000_000_000_u64, + 100_000_000_000_000_u64, + 100_000_000_u64, + ), + ( + 1_000_000_000_000_u64, + 100_000_000_000_000_u64, + 100_000_000_u64, + ), + ( + 100_000_000_000_u64, + 100_000_000_000_000_u64, + 100_000_000_u64, + ), + (100_000_000_000_u64, 100_000_000_000_000_u64, 1_000_000_u64), + ( + 100_000_000_000_u64, + 100_000_000_000_000_u64, + 1_000_000_000_000_u64, + ), + ( + 1_000_000_000_u64, + 100_000_000_000_000_u64, + 1_000_000_000_000_u64, + ), + ( + 1_000_000_u64, + 100_000_000_000_000_u64, + 1_000_000_000_000_u64, + ), + (1_000_u64, 100_000_000_000_000_u64, 1_000_000_000_000_u64), + (1_000_u64, 100_000_000_000_000_u64, 1_000_000_000_u64), + (1_000_u64, 100_000_000_000_000_u64, 1_000_000_u64), + (1_000_u64, 100_000_000_000_000_u64, 1_000_u64), + (1_000_u64, 100_000_000_000_000_u64, 100_000_000_000_000_u64), + (10_u64, 100_000_000_000_000_u64, 100_000_000_000_000_u64), + // Extreme values of ∆x for small x + (1_000_000_000_u64, 4_000_000_000_u64, 1_000_000_000_000_u64), + (1_000_000_000_000_u64, 1_000_u64, 1_000_000_000_000_u64), + ( + 5_628_038_062_729_553_u64, + 400_775_553_u64, + 14_446_633_907_665_582_u64, + ), + ( + 5_600_000_000_000_000_u64, + 400_000_000_u64, + 14_000_000_000_000_000_u64, + ), + ] + .into_iter() + .for_each(|(y, x, dx)| { + let bal = Balancer::new(w_quote).unwrap(); + let e1 = bal.exp_base_quote(x, dx); + let e2 = bal.exp_quote_base(x, dx); + let one = U64F64::from_num(1); + let y_fixed = U64F64::from_num(y); + let dy1 = y_fixed * (one - e1); + let dy2 = y_fixed * (one - e2); + + if dx > x.saturating_mul(1_000) { + assert!(e1 <= one); + assert!(e2 <= one); + return; + } + + let w1 = perquintill_to_f64(bal.get_base_weight()); + let w2 = perquintill_to_f64(bal.get_quote_weight()); + let e1_expected = (x as f64 / (x as f64 + dx as f64)).powf(w1 / w2); + let dy1_expected = y as f64 * (1. - e1_expected); + let e2_expected = (x as f64 / (x as f64 + dx as f64)).powf(w2 / w1); + let dy2_expected = y as f64 * (1. - e2_expected); + + // Start tolerance with 0.001 rao + let mut eps1 = 0.001; + let mut eps2 = 0.001; + + // If swapping more than 100k tao/alpha, relax tolerance to 1.0 rao + if dy1_expected > 100_000_000_000_000_f64 { + eps1 = 1.0; + } + if dy2_expected > 100_000_000_000_000_f64 { + eps2 = 1.0; + } + assert_abs_diff_eq!(f(dy1), dy1_expected, epsilon = eps1); + assert_abs_diff_eq!(f(dy2), dy2_expected, epsilon = eps2); + }) + }); +} + +/// This test exercises practical application edge cases of exp_base_quote +/// The practical formula where this function is used: +/// ∆y = y * (exp_base_quote(x, ∆x) - 1) +/// +/// The test validates that two different sets of parameters produce (sensibly) +/// different results +/// +#[test] +fn test_exp_base_quote_dy_precision() { + // Test cases: y, x1, ∆x1, w_quote1, x2, ∆x2, w_quote2 + // Realized dy1 should be greater than dy2 + [ + ( + 1_000_000_000_u64, + 21_000_000_000_000_000_u64, + 21_000_000_000_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + 21_000_000_000_000_000_u64, + 21_000_000_000_u64, + Perquintill::from_rational(1_000_000_000_001_u128, 2_000_000_000_000_u128), + ), + ( + 1_000_000_000_u64, + 21_000_000_000_000_000_u64, + 21_000_000_000_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_001_u128), + 21_000_000_000_000_000_u64, + 21_000_000_000_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + ), + ( + 1_000_000_000_u64, + 21_000_000_000_000_000_u64, + 2_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + 21_000_000_000_000_000_u64, + 1_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + ), + ( + 1_000_000_000_u64, + 21_000_000_000_000_000_u64, + 1_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + 21_000_000_000_000_000_u64, + 1_u64, + Perquintill::from_rational(1_010_000_000_000_u128, 2_000_000_000_000_u128), + ), + ( + 1_000_000_000_u64, + 21_000_000_000_000_000_u64, + 1_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_010_000_000_000_u128), + 21_000_000_000_000_000_u64, + 1_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + ), + ] + .into_iter() + .for_each(|(y, x1, dx1, w_quote1, x2, dx2, w_quote2)| { + let bal1 = Balancer::new(w_quote1).unwrap(); + let bal2 = Balancer::new(w_quote2).unwrap(); + + let exp1 = bal1.exp_base_quote(x1, dx1); + let exp2 = bal2.exp_base_quote(x2, dx2); + + let one = U64F64::from_num(1); + let y_fixed = U64F64::from_num(y); + let dy1 = y_fixed * (one - exp1); + let dy2 = y_fixed * (one - exp2); + + assert!(dy1 > dy2); + + let zero = U64F64::from_num(0); + assert!(dy1 != zero); + assert!(dy2 != zero); + }) +} + +/// Test the broad range of w_quote values, usually should be ignored +#[ignore] +#[test] +fn test_exp_quote_broad_range() { + let y = 1_000_000_000_000_u64; + let x = 100_000_000_000_000_u64; + let dx = 10_000_000_u64; + + let mut prev = U64F64::from_num(1_000_000_000); + let mut last_progress = 0.; + let start = 100_000_000_000_u128; + let stop = 900_000_000_000_u128; + for num in (start..=stop).step_by(1000_usize) { + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + let e = bal.exp_base_quote(x, dx); + + let one = U64F64::from_num(1); + let dy = U64F64::from_num(y) * (one - e); + + let progress = (num as f64 - start as f64) / (stop as f64 - start as f64); + if progress - last_progress >= 0.0001 { + // Replace with println for real-time progress + log::debug!("progress = {:?}%", progress * 100.); + log::debug!("dy = {:?}", dy); + last_progress = progress; + } + + assert!(dy != U64F64::from_num(0)); + assert!(dy <= prev); + prev = dy; + } +} + +// cargo test --package pallet-subtensor-swap --lib -- pallet::balancer::tests::test_exp_quote_fuzzy --include-ignored --exact --nocapture +#[ignore] +#[test] +fn test_exp_quote_fuzzy() { + use rand::rngs::StdRng; + use rand::{Rng, SeedableRng}; + use rayon::prelude::*; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + + const ITERATIONS: usize = 1_000_000_000; + let counter = Arc::new(AtomicUsize::new(0)); + + (0..ITERATIONS) + .into_par_iter() + .for_each(|i| { + // Each iteration gets its own deterministic RNG. + // Seed depends on i, so runs are reproducible. + let mut rng = StdRng::seed_from_u64(42 + i as u64); + let max_supply: u64 = 21_000_000_000_000_000; + let full_range = true; + + let x: u64 = rng.gen_range(1_000..=max_supply); // Alpha reserve + let y: u64 = if full_range { + // TAO reserve (allow huge prices) + rng.gen_range(1_000..=max_supply) + } else { + // TAO reserve (limit prices with 0-1000) + rng.gen_range(1_000..x.saturating_mul(1000).min(max_supply)) + }; + let dx: u64 = if full_range { + // Alhpa sold (allow huge values) + rng.gen_range(1_000..=21_000_000_000_000_000) + } else { + // Alhpa sold (do not sell more than 100% of what's in alpha reserve) + rng.gen_range(1_000..=x) + }; + let w_numerator: u64 = rng.gen_range(ACCURACY / 10..=ACCURACY / 10 * 9); + let w_quote = Perquintill::from_rational(w_numerator, ACCURACY); + + let bal = Balancer::new(w_quote).unwrap(); + let e = bal.exp_base_quote(x, dx); + + let one = U64F64::from_num(1); + let dy = U64F64::from_num(y) * (one - e); + + // Calculate expected in f64 and approx-assert + let w1 = perquintill_to_f64(bal.get_base_weight()); + let w2 = perquintill_to_f64(bal.get_quote_weight()); + let e_expected = (x as f64 / (x as f64 + dx as f64)).powf(w1 / w2); + let dy_expected = y as f64 * (1. - e_expected); + + let actual = dy.to_num::(); + let eps = (dy_expected / 1_000_000.).clamp(1.0, 1000.0); + + assert!( + (actual - dy_expected).abs() <= eps, + "dy mismatch:\n actual: {}\n expected: {}\n eps: {}\nParameters:\n x: {}\n y: {}\n dx: {}\n w_numerator: {}\n", + actual, dy_expected, eps, x, y, dx, w_numerator, + ); + + // Assert that we aren't giving out more than reserve y + assert!(dy <= y, "dy = {},\ny = {}", dy, y,); + + // Print progress + let done = counter.fetch_add(1, Ordering::Relaxed) + 1; + if done % 10_000_000 == 0 { + let progress = done as f64 / ITERATIONS as f64 * 100.0; + // Replace with println for real-time progress + log::debug!("progress = {progress:.4}%"); + } + }); +} + +#[test] +fn test_calculate_quote_delta_in() { + let num = 250_000_000_000_u128; // w1 = 0.75 + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + let current_price: U64F64 = U64F64::from_num(0.1); + let target_price: U64F64 = U64F64::from_num(0.2); + let tao_reserve: u64 = 1_000_000_000; + + let dy = bal.calculate_quote_delta_in(current_price, target_price, tao_reserve); + + // ∆y = y•[(p'/p)^w1 - 1] + let dy_expected = tao_reserve as f64 + * ((target_price.to_num::() / current_price.to_num::()).powf(0.75) - 1.0); + + assert_eq!(dy, dy_expected as u64,); +} + +#[test] +fn test_calculate_base_delta_in() { + let num = 250_000_000_000_u128; // w2 = 0.25 + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + let current_price: U64F64 = U64F64::from_num(0.2); + let target_price: U64F64 = U64F64::from_num(0.1); + let alpha_reserve: u64 = 1_000_000_000; + + let dx = bal.calculate_base_delta_in(current_price, target_price, alpha_reserve); + + // ∆x = x•[(p/p')^w2 - 1] + let dx_expected = alpha_reserve as f64 + * ((current_price.to_num::() / target_price.to_num::()).powf(0.25) - 1.0); + + assert_eq!(dx, dx_expected as u64,); +} + +#[test] +fn test_calculate_quote_delta_in_impossible() { + let num = 250_000_000_000_u128; // w1 = 0.75 + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + // Impossible price (lower) + let current_price: U64F64 = U64F64::from_num(0.1); + let target_price: U64F64 = U64F64::from_num(0.05); + let tao_reserve: u64 = 1_000_000_000; + + let dy = bal.calculate_quote_delta_in(current_price, target_price, tao_reserve); + let dy_expected = 0u64; + + assert_eq!(dy, dy_expected); +} + +#[test] +fn test_calculate_base_delta_in_impossible() { + let num = 250_000_000_000_u128; // w2 = 0.25 + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + // Impossible price (higher) + let current_price: U64F64 = U64F64::from_num(0.1); + let target_price: U64F64 = U64F64::from_num(0.2); + let alpha_reserve: u64 = 1_000_000_000; + + let dx = bal.calculate_base_delta_in(current_price, target_price, alpha_reserve); + let dx_expected = 0u64; + + assert_eq!(dx, dx_expected); +} + +#[test] +fn test_calculate_delta_in_reverse_swap() { + let num = 500_000_000_000_u128; + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + let current_price: U64F64 = U64F64::from_num(0.1); + let target_price: U64F64 = U64F64::from_num(0.2); + let tao_reserve: u64 = 1_000_000_000; + + // Here is the simple case of w1 = w2 = 0.5, so alpha = tao / price + let alpha_reserve: u64 = (tao_reserve as f64 / current_price.to_num::()) as u64; + + let dy = bal.calculate_quote_delta_in(current_price, target_price, tao_reserve); + let dx = alpha_reserve as f64 + * (1.0 + - (tao_reserve as f64 / (tao_reserve as f64 + dy as f64)) + .powf(num as f64 / (1_000_000_000_000 - num) as f64)); + + // Verify that buying with dy will in fact bring the price to target_price + let actual_price = bal.calculate_price(alpha_reserve - dx as u64, tao_reserve + dy); + assert_abs_diff_eq!( + actual_price.to_num::(), + target_price.to_num::(), + epsilon = target_price.to_num::() / 1_000_000_000. + ); +} + +#[test] +fn test_mul_round_zero_and_one() { + let v = 1_000_000u128; + + // p = 0 -> always 0 + assert_eq!(Balancer::mul_perquintill_round(Perquintill::zero(), v), 0); + + // p = 1 -> identity + assert_eq!(Balancer::mul_perquintill_round(Perquintill::one(), v), v); +} + +#[test] +fn test_mul_round_half_behaviour() { + // p = 1/2 + let p = Perquintill::from_rational(1u128, 2u128); + + // Check rounding around .5 boundaries + // value * 1/2, rounded to nearest + assert_eq!(Balancer::mul_perquintill_round(p, 0), 0); // 0.0 -> 0 + assert_eq!(Balancer::mul_perquintill_round(p, 1), 1); // 0.5 -> 1 (round up) + assert_eq!(Balancer::mul_perquintill_round(p, 2), 1); // 1.0 -> 1 + assert_eq!(Balancer::mul_perquintill_round(p, 3), 2); // 1.5 -> 2 + assert_eq!(Balancer::mul_perquintill_round(p, 4), 2); // 2.0 -> 2 + assert_eq!(Balancer::mul_perquintill_round(p, 5), 3); // 2.5 -> 3 + assert_eq!(Balancer::mul_perquintill_round(p, 1023), 512); // 511.5 -> 512 + assert_eq!(Balancer::mul_perquintill_round(p, 1025), 513); // 512.5 -> 513 +} + +#[test] +fn test_mul_round_third_behaviour() { + // p = 1/3 + let p = Perquintill::from_rational(1u128, 3u128); + + // value * 1/3, rounded to nearest + assert_eq!(Balancer::mul_perquintill_round(p, 3), 1); // 1.0 -> 1 + assert_eq!(Balancer::mul_perquintill_round(p, 4), 1); // 1.333... -> 1 + assert_eq!(Balancer::mul_perquintill_round(p, 5), 2); // 1.666... -> 2 + assert_eq!(Balancer::mul_perquintill_round(p, 6), 2); // 2.0 -> 2 +} + +#[test] +fn test_mul_round_large_values_simple_rational() { + // p = 7/10 (exact in perquintill: 0.7) + let p = Perquintill::from_rational(7u128, 10u128); + let v: u128 = 1_000_000_000_000_000_000; + + let res = Balancer::mul_perquintill_round(p, v); + + // Expected = round(0.7 * v) with pure integer math: + // round(v * 7 / 10) = (v*7 + 10/2) / 10 + let expected = (v.saturating_mul(7) + 10 / 2) / 10; + + assert_eq!(res, expected); +} + +#[test] +fn test_mul_round_max_value_with_one() { + let v = u128::MAX; + let p = ONE; + + // For p = 1, result must be exactly value, and must not overflow + let res = Balancer::mul_perquintill_round(p, v); + assert_eq!(res, v); +} + +#[test] +fn test_price_with_equal_weights_is_y_over_x() { + // quote = 0.5, base = 0.5 -> w1 / w2 = 1, so price = y/x + let quote = Perquintill::from_rational(1u128, 2u128); + let bal = Balancer::new(quote).unwrap(); + + let x = 2u64; + let y = 5u64; + + let price = bal.calculate_price(x, y); + let price_f = f(price); + + let expected_f = (y as f64) / (x as f64); + assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-12); +} + +#[test] +fn test_price_scales_with_weight_ratio_two_to_one() { + // Assume base = 1 - quote. + // quote = 1/3 -> base = 2/3, so w1 / w2 = 2. + // Then price = 2 * (y/x). + let quote = Perquintill::from_rational(1u128, 3u128); + let bal = Balancer::new(quote).unwrap(); + + let x = 4u64; + let y = 10u64; + + let price_f = f(bal.calculate_price(x, y)); + let expected_f = 2.0 * (y as f64 / x as f64); + + assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-10); +} + +#[test] +fn test_price_is_zero_when_y_is_zero() { + // If y = 0, y/x = 0 so price must be 0 regardless of weights (for x > 0). + let quote = Perquintill::from_rational(3u128, 10u128); // 0.3 + let bal = Balancer::new(quote).unwrap(); + + let x = 10u64; + let y = 0u64; + + let price_f = f(bal.calculate_price(x, y)); + assert_abs_diff_eq!(price_f, 0.0, epsilon = 0.0); +} + +#[test] +fn test_price_invariant_when_scaling_x_and_y_with_equal_weights() { + // For equal weights, price(x, y) == price(kx, ky). + let quote = Perquintill::from_rational(1u128, 2u128); // 0.5 + let bal = Balancer::new(quote).unwrap(); + + let x1 = 3u64; + let y1 = 7u64; + let k = 10u64; + let x2 = x1 * k; + let y2 = y1 * k; + + let p1 = f(bal.calculate_price(x1, y1)); + let p2 = f(bal.calculate_price(x2, y2)); + + assert_abs_diff_eq!(p1, p2, epsilon = 1e-12); +} + +#[test] +fn test_price_matches_formula_for_general_quote() { + // General check: price = (w1 / w2) * (y/x), + // where w1 = base_weight, w2 = quote_weight. + // Here we assume get_base_weight = 1 - quote. + let quote = Perquintill::from_rational(2u128, 5u128); // 0.4 + let bal = Balancer::new(quote).unwrap(); + + let x = 9u64; + let y = 25u64; + + let price_f = f(bal.calculate_price(x, y)); + + let base = Perquintill::one() - quote; + let w1 = base.deconstruct() as f64; + let w2 = quote.deconstruct() as f64; + + let expected_f = (w1 / w2) * (y as f64 / x as f64); + assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-9); +} + +#[test] +fn test_price_high_values_non_equal_weights() { + // Non-equal weights, high x and y (up to 21e15) + let quote = Perquintill::from_rational(3u128, 10u128); // 0.3 + let bal = Balancer::new(quote).unwrap(); + + let x: u64 = 21_000_000_000_000_000; + let y: u64 = 15_000_000_000_000_000; + + let price = bal.calculate_price(x, y); + let price_f = f(price); + + // Expected: (w1 / w2) * (y / x), using Balancer's actual weights + let w1 = bal.get_base_weight().deconstruct() as f64; + let w2 = bal.get_quote_weight().deconstruct() as f64; + let expected_f = (w1 / w2) * (y as f64 / x as f64); + + assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-9); +} + +// cargo test --package pallet-subtensor-swap --lib -- pallet::balancer::tests::test_exp_scaled --exact --nocapture +#[test] +fn test_exp_scaled() { + [ + // base_weight_numerator, base_weight_denominator, reserve, d_reserve, base_quote + (5_u64, 10_u64, 100000_u64, 100_u64, true, 0.999000999000999), + (1_u64, 4_u64, 500000_u64, 5000_u64, true, 0.970590147927644), + (3_u64, 4_u64, 200000_u64, 2000_u64, false, 0.970590147927644), + ( + 9_u64, + 10_u64, + 13513642_u64, + 1673_u64, + false, + 0.998886481979889, + ), + ( + 773_u64, + 1000_u64, + 7_000_000_000_u64, + 10_000_u64, + true, + 0.999999580484586, + ), + ] + .into_iter() + .map(|v| { + ( + Perquintill::from_rational(v.0, v.1), + v.2, + v.3, + v.4, + U64F64::from_num(v.5), + ) + }) + .for_each(|(quote_weight, reserve, d_reserve, base_quote, expected)| { + let balancer = Balancer::new(quote_weight).unwrap(); + let result = balancer.exp_scaled(reserve, d_reserve as i128, base_quote); + assert_abs_diff_eq!( + result.to_num::(), + expected.to_num::(), + epsilon = 0.000000001 + ); + }); +} + +// cargo test --package pallet-subtensor-swap --lib -- pallet::balancer::tests::test_base_needed_for_quote --exact --nocapture +#[test] +fn test_base_needed_for_quote() { + let num = 250_000_000_000_u128; // w1 = 0.75 + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + let tao_reserve: u64 = 1_000_000_000; + let alpha_reserve: u64 = 1_000_000_000; + let tao_delta: u64 = 1_123_432; // typical fee range + + let dx = bal.get_base_needed_for_quote(tao_reserve, alpha_reserve, tao_delta); + + // ∆x = x•[(y/(y+∆y))^(w2/w1) - 1] + let dx_expected = tao_reserve as f64 + * ((tao_reserve as f64 / ((tao_reserve - tao_delta) as f64)).powf(0.25 / 0.75) - 1.0); + + assert_eq!(dx, dx_expected as u64,); +} diff --git a/pallets/swap/src/pallet/hooks.rs b/pallets/swap/src/pallet/hooks.rs index 90989d5f52..a42bd9a04e 100644 --- a/pallets/swap/src/pallet/hooks.rs +++ b/pallets/swap/src/pallet/hooks.rs @@ -1,24 +1,24 @@ +//! Block hooks: currently only `on_runtime_upgrade` (Uniswap-v3 → balancer migration). + use frame_support::pallet_macros::pallet_section; #[pallet_section] mod hooks { #[pallet::hooks] impl Hooks> for Pallet { - fn on_initialize(block_number: BlockNumberFor) -> Weight { + fn on_initialize(_block_number: BlockNumberFor) -> Weight { Weight::from_parts(0, 0) } fn on_finalize(_block_number: BlockNumberFor) {} + /// Runs storage migrations (v3 tick/position maps → weighted balancer). fn on_runtime_upgrade() -> Weight { - // --- Migrate storage let mut weight = Weight::from_parts(0, 0); - weight = weight - // Cleanup uniswap v3 and migrate to balancer - .saturating_add( - migrations::migrate_swapv3_to_balancer::migrate_swapv3_to_balancer::(), - ); + weight = weight.saturating_add( + migrations::migrate_swapv3_to_balancer::migrate_swapv3_to_balancer::(), + ); weight } diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index 3b841169ed..db7b9428c2 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -1,3 +1,5 @@ +//! Pallet impls: price, swap execution, protocol liquidity, and [`SwapHandler`] wiring. + use frame_support::storage::{TransactionOutcome, transactional}; use frame_support::{ ensure, @@ -19,6 +21,9 @@ use super::swap_step::{BasicSwapStep, MAX_SWAP_INPUT_RESERVE_MULTIPLIER, SwapSte use crate::{pallet::Balancer, pallet::balancer::BalancerError}; impl Pallet { + /// Current alpha price for `netuid` from balancer weights × reserves (`mechanism == 1`). + /// + /// Static / unknown mechanisms return `1`; zero alpha reserve returns `0`. pub fn current_price(netuid: NetUid) -> U64F64 { match T::SubnetInfo::mechanism(netuid.into()) { 1 => { @@ -35,7 +40,10 @@ impl Pallet { } } - // initializes pal-swap (balancer) for a subnet if needed + /// Lazily initialize [`SwapBalancer`] / [`PalSwapInitialized`] for `netuid` if unset. + /// + /// With `Some(price)`, derives quote weight from reserves + price; with `None`, uses 0.5/0.5. + /// Fails with [`Error::ReservesOutOfBalance`] when price-init is impossible (e.g. empty pool). pub fn maybe_initialize_palswap( netuid: NetUid, maybe_price: Option, @@ -166,6 +174,7 @@ impl Pallet { (TaoBalance::ZERO, AlphaBalance::ZERO) } + /// Try cloning `balancer` and applying a price-preserving weight update; `None` if out of range. fn try_update_balancer( balancer: &Balancer, tao_reserve: TaoBalance, @@ -251,6 +260,7 @@ impl Pallet { }) } + /// Reject swaps whose post-fee input exceeds `MAX_SWAP_INPUT_RESERVE_MULTIPLIER` × input reserve. fn ensure_swap_input_within_reserve_limit( netuid: NetUid, amount: Order::PaidIn, @@ -268,6 +278,7 @@ impl Pallet { Ok(()) } + /// Core swap path: reserve checks, lazy init, limit-price check, then [`BasicSwapStep::execute`]. fn swap_inner( netuid: NetUid, order: Order, @@ -346,10 +357,12 @@ impl Pallet { T::ProtocolId::get().into_account_truncating() } + /// Minimum allowable alpha price used as a default sell-side limit (raw token units). pub(crate) fn min_price_inner() -> C { u64::from(1_000_u64).into() } + /// Maximum allowable alpha price used as a default buy-side limit (raw token units). pub(crate) fn max_price_inner() -> C { u64::from(1_000_000_000_000_000_u64).into() } diff --git a/pallets/swap/src/pallet/migrations/migrate_swapv3_to_balancer.rs b/pallets/swap/src/pallet/migrations/migrate_swapv3_to_balancer.rs index 2f06d88a00..124123ebc1 100644 --- a/pallets/swap/src/pallet/migrations/migrate_swapv3_to_balancer.rs +++ b/pallets/swap/src/pallet/migrations/migrate_swapv3_to_balancer.rs @@ -1,9 +1,14 @@ +//! One-shot migration: Uniswap-v3 tick/position maps → weighted [`Balancer`] pools. +//! +//! Idempotent via [`HasMigrationRun`] key `"migrate_swapv3_to_balancer"` (do not rename). + use super::*; use crate::HasMigrationRun; use frame_support::{storage_alias, traits::Get, weights::Weight}; use scale_info::prelude::string::String; use substrate_fixed::types::U64F64; +/// Storage aliases for maps removed by this migration (read-before-delete only). pub mod deprecated_swap_maps { use super::*; @@ -22,6 +27,7 @@ pub mod deprecated_swap_maps { StorageMap, Twox64Concat, NetUid, AlphaBalance, ValueQuery>; } +/// Initialize balancers from V3 sqrt prices, then clear obsolete V3 storage prefixes. pub fn migrate_swapv3_to_balancer() -> Weight { let migration_name = BoundedVec::truncate_from(b"migrate_swapv3_to_balancer".to_vec()); let mut weight = T::DbWeight::get().reads(1); @@ -60,20 +66,20 @@ pub fn migrate_swapv3_to_balancer() -> Weight { // ------------------------------ // Step 2: Clear Map entries // ------------------------------ - remove_prefix::("Swap", "AlphaSqrtPrice", &mut weight); - remove_prefix::("Swap", "CurrentTick", &mut weight); - remove_prefix::("Swap", "EnabledUserLiquidity", &mut weight); - remove_prefix::("Swap", "FeeGlobalTao", &mut weight); - remove_prefix::("Swap", "FeeGlobalAlpha", &mut weight); - remove_prefix::("Swap", "LastPositionId", &mut weight); + clear_twox_map_prefix::("Swap", "AlphaSqrtPrice", &mut weight); + clear_twox_map_prefix::("Swap", "CurrentTick", &mut weight); + clear_twox_map_prefix::("Swap", "EnabledUserLiquidity", &mut weight); + clear_twox_map_prefix::("Swap", "FeeGlobalTao", &mut weight); + clear_twox_map_prefix::("Swap", "FeeGlobalAlpha", &mut weight); + clear_twox_map_prefix::("Swap", "LastPositionId", &mut weight); // Scrap reservoirs can be just cleaned because they are already included in reserves - remove_prefix::("Swap", "ScrapReservoirTao", &mut weight); - remove_prefix::("Swap", "ScrapReservoirAlpha", &mut weight); - remove_prefix::("Swap", "Ticks", &mut weight); - remove_prefix::("Swap", "TickIndexBitmapWords", &mut weight); - remove_prefix::("Swap", "SwapV3Initialized", &mut weight); - remove_prefix::("Swap", "CurrentLiquidity", &mut weight); - remove_prefix::("Swap", "Positions", &mut weight); + clear_twox_map_prefix::("Swap", "ScrapReservoirTao", &mut weight); + clear_twox_map_prefix::("Swap", "ScrapReservoirAlpha", &mut weight); + clear_twox_map_prefix::("Swap", "Ticks", &mut weight); + clear_twox_map_prefix::("Swap", "TickIndexBitmapWords", &mut weight); + clear_twox_map_prefix::("Swap", "SwapV3Initialized", &mut weight); + clear_twox_map_prefix::("Swap", "CurrentLiquidity", &mut weight); + clear_twox_map_prefix::("Swap", "Positions", &mut weight); // ------------------------------ // Step 3: Mark Migration as Completed diff --git a/pallets/swap/src/pallet/migrations/mod.rs b/pallets/swap/src/pallet/migrations/mod.rs index d34626f05e..0db1f15351 100644 --- a/pallets/swap/src/pallet/migrations/mod.rs +++ b/pallets/swap/src/pallet/migrations/mod.rs @@ -1,3 +1,5 @@ +//! Storage migrations for `pallet-subtensor-swap`. + use super::*; use frame_support::pallet_prelude::Weight; use sp_io::KillStorageResult; @@ -7,7 +9,8 @@ use sp_std::vec::Vec; pub mod migrate_swapv3_to_balancer; -pub(crate) fn remove_prefix(module: &str, old_map: &str, weight: &mut Weight) { +/// Clear all keys under `Twox128(module) ++ Twox128(old_map)` and charge write weight. +pub(crate) fn clear_twox_map_prefix(module: &str, old_map: &str, weight: &mut Weight) { let mut prefix = Vec::new(); prefix.extend_from_slice(&twox_128(module.as_bytes())); prefix.extend_from_slice(&twox_128(old_map.as_bytes())); diff --git a/pallets/swap/src/pallet/mod.rs b/pallets/swap/src/pallet/mod.rs index f23fc97c73..9a9f439c56 100644 --- a/pallets/swap/src/pallet/mod.rs +++ b/pallets/swap/src/pallet/mod.rs @@ -1,3 +1,8 @@ +//! FRAME pallet definition for the TAO↔alpha weighted-balancer AMM. +//! +//! Storage names, call indices, and event/error variant order are frozen wire surfaces. +//! Swap execution lives in `impls` / `swap_step`; pool math in `balancer`. + use core::num::NonZeroU64; use frame_support::{PalletId, pallet_prelude::*, traits::Get}; @@ -18,7 +23,7 @@ mod swap_step; #[cfg(test)] mod tests; -// Define a maximum length for the migration key +/// Max length of a `HasMigrationRun` key (`BoundedVec`). type MigrationKeyMaxLen = ConstU32<128>; #[allow(clippy::module_inception)] @@ -31,43 +36,41 @@ mod pallet { #[pallet::pallet] pub struct Pallet(_); - /// Configure the pallet by specifying the parameters and types on which it depends. + /// Runtime configuration for the swap pallet (reserves, fee bounds, protocol account). #[pallet::config] pub trait Config: frame_system::Config { - /// Implementor of - /// [`SubnetInfo`](subtensor_swap_interface::SubnetInfo). + /// Subnet existence / mechanism queries (`mechanism == 1` ⇒ dynamic AMM). type SubnetInfo: SubnetInfo; - /// Tao reserves info. + /// Price-active TAO reserve provider (`SubnetTAO` in the full runtime). type TaoReserve: TokenReserve; - /// Alpha reserves info. + /// Price-active alpha reserve provider (`SubnetAlphaIn` in the full runtime). type AlphaReserve: TokenReserve; - /// Implementor of - /// [`BalanceOps`](subtensor_swap_interface::BalanceOps). + /// Coldkey/hotkey balance ops used by deprecated LP paths and fee sinks. type BalanceOps: BalanceOps; - /// This type is used to derive protocol accoun ID. + /// PalletId used to derive the protocol-owned account for this swap pallet. #[pallet::constant] type ProtocolId: Get; - /// The maximum fee rate that can be set + /// Upper bound for [`FeeRate`] (u16-normalized); root cannot set above this. #[pallet::constant] type MaxFeeRate: Get; - /// Minimum liquidity that is safe for rounding and integer math. + /// Minimum liquidity considered safe for rounding / integer math. #[pallet::constant] type MinimumLiquidity: Get; - /// Minimum reserve for tao and alpha + /// Floor for TAO and alpha reserves before a swap may execute. #[pallet::constant] type MinimumReserve: Get; - /// Weight information for extrinsics in this pallet. + /// Extrinsic weight functions (generated / benchmarked). type WeightInfo: WeightInfo; - /// Helper for setting up cross-pallet state needed by benchmarks. + /// Cross-pallet subnet/hotkey setup for runtime benchmarks. #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper: BenchmarkHelper; } @@ -85,49 +88,49 @@ mod pallet { fn register_hotkey(_hotkey: &AccountId, _coldkey: &AccountId) {} } - /// Default fee rate if not set + /// Default [`FeeRate`]: `33 / u16::MAX` ≈ 0.05%. #[pallet::type_value] pub fn DefaultFeeRate() -> u16 { 33 // ~0.05 % } - /// The fee rate applied to swaps per subnet, normalized value between 0 and u16::MAX + /// Per-subnet swap fee rate, u16-normalized (`rate / u16::MAX` of the input). #[pallet::storage] pub type FeeRate = StorageMap<_, Twox64Concat, NetUid, u16, ValueQuery, DefaultFeeRate>; //////////////////////////////////////////////////// // Balancer (PalSwap) maps and variables - /// Default reserve weight + /// Default [`Balancer`]: equal 0.5 / 0.5 base/quote weights. #[pallet::type_value] pub fn DefaultBalancer() -> Balancer { Balancer::default() } - /// u64-normalized reserve weight + /// Per-subnet weighted-balancer state (stores quote weight; base = 1 − quote). #[pallet::storage] pub type SwapBalancer = StorageMap<_, Twox64Concat, NetUid, Balancer, ValueQuery, DefaultBalancer>; - /// Storage to determine whether balancer swap was initialized for a specific subnet. + /// Whether the balancer pool for `netuid` has been initialized (lazy via swaps / init). #[pallet::storage] pub type PalSwapInitialized = StorageMap<_, Twox64Concat, NetUid, bool, ValueQuery>; - /// TAO protocol liquidity that could not be injected without exceeding balancer weight bounds. + /// Materialized TAO that could not become price-active without violating weight bounds. #[pallet::storage] pub type BalancerTaoReservoir = StorageMap<_, Twox64Concat, NetUid, TaoBalance, ValueQuery>; - /// Alpha protocol liquidity that could not be injected without exceeding balancer weight bounds. + /// Materialized alpha that could not become price-active without violating weight bounds. #[pallet::storage] pub type BalancerAlphaReservoir = StorageMap<_, Twox64Concat, NetUid, AlphaBalance, ValueQuery>; - /// --- Storage for migration run status + /// Idempotency flags for on-runtime-upgrade migrations (keyed by migration name bytes). #[pallet::storage] pub type HasMigrationRun = StorageMap<_, Identity, BoundedVec, bool, ValueQuery>; - /// Alpha reservoir for scraps of protocol claimed fees. + /// Leftover alpha scraps from protocol fee claims (legacy; largely unused post-v3 migration). #[pallet::storage] pub type ScrapReservoirAlpha = StorageMap<_, Twox64Concat, NetUid, AlphaBalance, ValueQuery>; @@ -185,10 +188,9 @@ mod pallet { impl Pallet { #![deny(clippy::expect_used)] - /// Set the fee rate for swaps on a specific subnet (normalized value). - /// For example, 0.3% is approximately 196. + /// Set the per-subnet swap [`FeeRate`] (u16-normalized; e.g. ~196 ≈ 0.3%). /// - /// Only callable by the admin origin + /// Root-only. Requires the subnet to exist and `rate <= MaxFeeRate`. #[pallet::call_index(0)] #[pallet::weight(::WeightInfo::set_fee_rate())] pub fn set_fee_rate(origin: OriginFor, netuid: NetUid, rate: u16) -> DispatchResult { @@ -270,7 +272,7 @@ mod pallet { } } -/// Struct representing a tick index, DEPRECATED +/// Deprecated Uniswap-v3 tick index retained only for call/SCALE compatibility of LP stubs. #[freeze_struct("7c280c2b3bbbb33e")] #[derive( Debug, @@ -290,7 +292,7 @@ mod pallet { )] pub struct TickIndex(i32); -/// Struct representing a liquidity position ID, DEPRECATED +/// Deprecated Uniswap-v3 LP position id retained only for call/SCALE compatibility of LP stubs. #[freeze_struct("e695cd6455c3f0cb")] #[derive( Clone, diff --git a/pallets/swap/src/pallet/swap_step.rs b/pallets/swap/src/pallet/swap_step.rs index 3d4d516d1f..ed047df122 100644 --- a/pallets/swap/src/pallet/swap_step.rs +++ b/pallets/swap/src/pallet/swap_step.rs @@ -1,3 +1,5 @@ +//! Single-step swap execution against the weighted balancer (buy/sell specializations). + use core::marker::PhantomData; use frame_support::ensure; @@ -7,9 +9,10 @@ use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token, TokenRes use super::pallet::*; +/// Cap on post-fee swap input as a multiple of the input-side reserve (anti-drain). pub(crate) const MAX_SWAP_INPUT_RESERVE_MULTIPLIER: u64 = 1_000; -/// A struct representing a single swap step with all its parameters and state +/// One atomic swap step: fee, limit-price clamp, and reserve delta conversion. pub(crate) struct BasicSwapStep where T: Config, @@ -224,6 +227,7 @@ impl SwapStep } } +/// Direction-specific swap math (TAO→alpha vs alpha→TAO) used by [`BasicSwapStep`]. pub(crate) trait SwapStep where T: Config, @@ -248,6 +252,7 @@ where fn convert_deltas(netuid: NetUid, delta_in: PaidIn) -> PaidOut; } +/// Result of one [`BasicSwapStep::execute`]: fees and in/out deltas. #[derive(Debug, PartialEq)] pub(crate) struct SwapStepResult where diff --git a/pallets/swap/src/pallet/tests.rs b/pallets/swap/src/pallet/tests.rs deleted file mode 100644 index dd41c84269..0000000000 --- a/pallets/swap/src/pallet/tests.rs +++ /dev/null @@ -1,1071 +0,0 @@ -#![allow( - clippy::arithmetic_side_effects, - clippy::expect_used, - clippy::indexing_slicing, - clippy::unwrap_used -)] - -use approx::assert_abs_diff_eq; -use frame_support::weights::WeightMeter; -use frame_support::{assert_noop, assert_ok}; -use sp_arithmetic::Perquintill; -use sp_runtime::DispatchError; -use substrate_fixed::types::U64F64; -use subtensor_runtime_common::{NetUid, Token}; -use subtensor_swap_interface::{Order as OrderT, SwapHandler}; - -use super::*; -use crate::mock::*; -use crate::pallet::swap_step::*; - -// Run all tests: -// cargo test --package pallet-subtensor-swap --lib -- pallet::tests --nocapture - -#[allow(dead_code)] -fn get_min_price() -> U64F64 { - U64F64::from_num(Pallet::::min_price_inner::()) - / U64F64::from_num(1_000_000_000) -} - -#[allow(dead_code)] -fn get_max_price() -> U64F64 { - U64F64::from_num(Pallet::::max_price_inner::()) - / U64F64::from_num(1_000_000_000) -} - -mod dispatchables { - use super::*; - - #[test] - fn test_set_fee_rate() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let fee_rate = 500; // 0.76% fee - - assert_noop!( - Swap::set_fee_rate(RuntimeOrigin::signed(666), netuid, fee_rate), - DispatchError::BadOrigin - ); - - assert_ok!(Swap::set_fee_rate(RuntimeOrigin::root(), netuid, fee_rate)); - - // Check that fee rate was set correctly - assert_eq!(FeeRate::::get(netuid), fee_rate); - - // Verify fee rate validation - should fail if too high - let too_high_fee = MaxFeeRate::get() + 1; - assert_noop!( - Swap::set_fee_rate(RuntimeOrigin::root(), netuid, too_high_fee), - Error::::FeeRateTooHigh - ); - }); - } - - fn perquintill_to_f64(p: Perquintill) -> f64 { - let parts = p.deconstruct() as f64; - parts / 1_000_000_000_000_000_000_f64 - } - - /// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::dispatchables::test_adjust_protocol_liquidity_happy --exact --nocapture - #[test] - fn test_adjust_protocol_liquidity_happy() { - // test case: tao_delta, alpha_delta - [ - (0_u64, 0_u64), - (0_u64, 1_u64), - (1_u64, 0_u64), - (1_u64, 1_u64), - (0_u64, 10_u64), - (10_u64, 0_u64), - (10_u64, 10_u64), - (0_u64, 100_u64), - (100_u64, 0_u64), - (100_u64, 100_u64), - (0_u64, 1_000_u64), - (1_000_u64, 0_u64), - (1_000_u64, 1_000_u64), - (1_000_000_u64, 0_u64), - (0_u64, 1_000_000_u64), - (1_000_000_u64, 1_000_000_u64), - (1_000_000_000_u64, 0_u64), - (0_u64, 1_000_000_000_u64), - (1_000_000_000_u64, 1_000_000_000_u64), - (1_000_000_000_000_u64, 0_u64), - (0_u64, 1_000_000_000_000_u64), - (1_000_000_000_000_u64, 1_000_000_000_000_u64), - (1_u64, 2_u64), - (2_u64, 1_u64), - (10_u64, 20_u64), - (20_u64, 10_u64), - (100_u64, 200_u64), - (200_u64, 100_u64), - (1_000_u64, 2_000_u64), - (2_000_u64, 1_000_u64), - (1_000_000_u64, 2_000_000_u64), - (2_000_000_u64, 1_000_000_u64), - (1_000_000_000_u64, 2_000_000_000_u64), - (2_000_000_000_u64, 1_000_000_000_u64), - (1_000_000_000_000_u64, 2_000_000_000_000_u64), - (2_000_000_000_000_u64, 1_000_000_000_000_u64), - (1_234_567_u64, 2_432_765_u64), - (1_234_567_u64, 2_432_765_890_u64), - ] - .into_iter() - .for_each(|(tao_delta, alpha_delta)| { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let tao_delta = TaoBalance::from(tao_delta); - let alpha_delta = AlphaBalance::from(alpha_delta); - - // Initialize reserves and price - let tao = TaoBalance::from(1_000_000_000_000_u64); - let alpha = AlphaBalance::from(4_000_000_000_000_u64); - TaoReserve::set_mock_reserve(netuid, tao); - AlphaReserve::set_mock_reserve(netuid, alpha); - let price_before = Swap::current_price(netuid); - - // Adjust reserves - Swap::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta); - TaoReserve::set_mock_reserve(netuid, tao + tao_delta); - AlphaReserve::set_mock_reserve(netuid, alpha + alpha_delta); - - // Check that price didn't change - let price_after = Swap::current_price(netuid); - assert_abs_diff_eq!( - price_before.to_num::(), - price_after.to_num::(), - epsilon = price_before.to_num::() / 1_000_000_000_000. - ); - - // Check that reserve weight was properly updated - let new_tao = u64::from(tao + tao_delta) as f64; - let new_alpha = u64::from(alpha + alpha_delta) as f64; - let expected_quote_weight = - new_tao / (new_alpha * price_before.to_num::() + new_tao); - let expected_quote_weight_delta = expected_quote_weight - 0.5; - let res_weights = SwapBalancer::::get(netuid); - let actual_quote_weight_delta = - perquintill_to_f64(res_weights.get_quote_weight()) - 0.5; - let eps = expected_quote_weight / 1_000_000_000_000.; - assert_abs_diff_eq!( - expected_quote_weight_delta, - actual_quote_weight_delta, - epsilon = eps - ); - }); - }); - } - - #[test] - fn test_adjust_protocol_liquidity_materializes_tao_when_reservoiring_tao() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - let tao = TaoBalance::from(1_000_u64); - let alpha = AlphaBalance::from(1_000_u64); - TaoReserve::set_mock_reserve(netuid, tao); - AlphaReserve::set_mock_reserve(netuid, alpha); - - let (price_active_tao, price_active_alpha) = Swap::adjust_protocol_liquidity( - netuid, - TaoBalance::from(200_000_u64), - AlphaBalance::from(1_000_u64), - ); - - assert_eq!(price_active_tao, TaoBalance::ZERO); - assert_eq!(price_active_alpha, AlphaBalance::from(1_000_u64)); - assert_eq!( - BalancerTaoReservoir::::get(netuid), - TaoBalance::from(200_000_u64) - ); - assert_eq!( - BalancerAlphaReservoir::::get(netuid), - AlphaBalance::ZERO - ); - }); - } - - #[test] - fn test_adjust_protocol_liquidity_materializes_alpha_when_reservoiring_alpha() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - let tao = TaoBalance::from(1_000_u64); - let alpha = AlphaBalance::from(1_000_u64); - TaoReserve::set_mock_reserve(netuid, tao); - AlphaReserve::set_mock_reserve(netuid, alpha); - - let (price_active_tao, price_active_alpha) = Swap::adjust_protocol_liquidity( - netuid, - TaoBalance::from(1_000_u64), - AlphaBalance::from(200_000_u64), - ); - - assert_eq!(price_active_tao, TaoBalance::from(1_000_u64)); - assert_eq!(price_active_alpha, AlphaBalance::ZERO); - assert_eq!(BalancerTaoReservoir::::get(netuid), TaoBalance::ZERO); - assert_eq!( - BalancerAlphaReservoir::::get(netuid), - AlphaBalance::from(200_000_u64) - ); - }); - } - - #[test] - fn test_adjust_protocol_liquidity_retries_reservoir_with_new_injection() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - let mut tao = TaoBalance::from(1_000_u64); - let mut alpha = AlphaBalance::from(1_000_u64); - TaoReserve::set_mock_reserve(netuid, tao); - AlphaReserve::set_mock_reserve(netuid, alpha); - - let (price_active_tao, price_active_alpha) = Swap::adjust_protocol_liquidity( - netuid, - TaoBalance::from(200_000_u64), - AlphaBalance::from(1_000_u64), - ); - assert_eq!(price_active_tao, TaoBalance::ZERO); - assert_eq!(price_active_alpha, AlphaBalance::from(1_000_u64)); - tao += price_active_tao; - alpha += price_active_alpha; - TaoReserve::set_mock_reserve(netuid, tao); - AlphaReserve::set_mock_reserve(netuid, alpha); - - let (price_active_tao, price_active_alpha) = Swap::adjust_protocol_liquidity( - netuid, - TaoBalance::from(1_000_u64), - AlphaBalance::from(200_000_u64), - ); - - assert!(price_active_tao >= TaoBalance::from(1_000_u64)); - assert!(price_active_alpha >= AlphaBalance::from(200_000_u64)); - assert_eq!(BalancerTaoReservoir::::get(netuid), TaoBalance::ZERO); - assert_eq!( - BalancerAlphaReservoir::::get(netuid), - AlphaBalance::ZERO - ); - }); - } - - #[test] - fn test_adjust_protocol_liquidity_activates_reservoir_amounts() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - TaoReserve::set_mock_reserve(netuid, TaoBalance::from(1_000_000_u64)); - AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(1_000_000_u64)); - BalancerTaoReservoir::::insert(netuid, TaoBalance::from(10_000_u64)); - BalancerAlphaReservoir::::insert(netuid, AlphaBalance::from(20_000_u64)); - - let tao_delta = TaoBalance::from(300_u64); - let alpha_delta = AlphaBalance::from(400_u64); - let (price_active_tao, price_active_alpha) = - Swap::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta); - - assert_eq!(price_active_tao, TaoBalance::from(10_300_u64)); - assert_eq!(price_active_alpha, AlphaBalance::from(20_400_u64)); - assert_eq!(BalancerTaoReservoir::::get(netuid), TaoBalance::ZERO); - assert_eq!( - BalancerAlphaReservoir::::get(netuid), - AlphaBalance::ZERO - ); - }); - } - - /// This test case verifies that small gradual injections (like emissions in every block) - /// in the worst case - /// - Do not cause price to change - /// - Result in the same weight change as one large injection - /// - /// This is a long test that only tests validity of weights math. Run again if changing - /// Balancer::update_weights_for_added_liquidity - /// - /// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::dispatchables::test_adjust_protocol_liquidity_deltas --exact --nocapture - #[ignore] - #[test] - fn test_adjust_protocol_liquidity_deltas() { - // The number of times (blocks) over which gradual injections will be made - // One year price drift due to precision is under 1e-6 - const ITERATIONS: u64 = 2_700_000; - const PRICE_PRECISION: f64 = 0.000_001; - const PREC_LARGE_DELTA: f64 = 0.001; - const WEIGHT_PRECISION: f64 = 0.000_000_000_000_000_001; - - let initial_tao_reserve = TaoBalance::from(1_000_000_000_000_000_u64); - let initial_alpha_reserve = AlphaBalance::from(10_000_000_000_000_000_u64); - - // test case: tao_delta, alpha_delta, price_precision - [ - (0_u64, 0_u64, PRICE_PRECISION), - (0_u64, 1_u64, PRICE_PRECISION), - (1_u64, 0_u64, PRICE_PRECISION), - (1_u64, 1_u64, PRICE_PRECISION), - (0_u64, 10_u64, PRICE_PRECISION), - (10_u64, 0_u64, PRICE_PRECISION), - (10_u64, 10_u64, PRICE_PRECISION), - (0_u64, 100_u64, PRICE_PRECISION), - (100_u64, 0_u64, PRICE_PRECISION), - (100_u64, 100_u64, PRICE_PRECISION), - (0_u64, 987_u64, PRICE_PRECISION), - (987_u64, 0_u64, PRICE_PRECISION), - (876_u64, 987_u64, PRICE_PRECISION), - (0_u64, 1_000_u64, PRICE_PRECISION), - (1_000_u64, 0_u64, PRICE_PRECISION), - (1_000_u64, 1_000_u64, PRICE_PRECISION), - (0_u64, 1_234_u64, PRICE_PRECISION), - (1_234_u64, 0_u64, PRICE_PRECISION), - (1_234_u64, 4_321_u64, PRICE_PRECISION), - (1_234_000_u64, 4_321_000_u64, PREC_LARGE_DELTA), - (1_234_u64, 4_321_000_u64, PREC_LARGE_DELTA), - ] - .into_iter() - .for_each(|(tao_delta, alpha_delta, price_precision)| { - new_test_ext().execute_with(|| { - let netuid1 = NetUid::from(1); - - let tao_delta = TaoBalance::from(tao_delta); - let alpha_delta = AlphaBalance::from(alpha_delta); - - // Initialize realistically large reserves - let mut tao = initial_tao_reserve; - let mut alpha = initial_alpha_reserve; - TaoReserve::set_mock_reserve(netuid1, tao); - AlphaReserve::set_mock_reserve(netuid1, alpha); - let price_before = Swap::current_price(netuid1); - - // Adjust reserves gradually - for _ in 0..ITERATIONS { - Swap::adjust_protocol_liquidity(netuid1, tao_delta, alpha_delta); - tao += tao_delta; - alpha += alpha_delta; - TaoReserve::set_mock_reserve(netuid1, tao); - AlphaReserve::set_mock_reserve(netuid1, alpha); - } - // Check that price didn't change - let price_after = Swap::current_price(netuid1); - assert_abs_diff_eq!( - price_before.to_num::(), - price_after.to_num::(), - epsilon = price_precision - ); - - ///////////////////////// - // Now do one-time big injection with another netuid and compare weights - let netuid2 = NetUid::from(2); - - // Initialize same large reserves - TaoReserve::set_mock_reserve(netuid2, initial_tao_reserve); - AlphaReserve::set_mock_reserve(netuid2, initial_alpha_reserve); - - // Adjust reserves by one large amount at once - let tao_delta_once = TaoBalance::from(ITERATIONS * u64::from(tao_delta)); - let alpha_delta_once = AlphaBalance::from(ITERATIONS * u64::from(alpha_delta)); - Swap::adjust_protocol_liquidity(netuid2, tao_delta_once, alpha_delta_once); - TaoReserve::set_mock_reserve(netuid2, initial_tao_reserve + tao_delta_once); - AlphaReserve::set_mock_reserve(netuid2, initial_alpha_reserve + alpha_delta_once); - - // Compare reserve weights for netuid 1 and 2 - let res_weights1 = SwapBalancer::::get(netuid1); - let res_weights2 = SwapBalancer::::get(netuid2); - let actual_quote_weight1 = perquintill_to_f64(res_weights1.get_quote_weight()); - let actual_quote_weight2 = perquintill_to_f64(res_weights2.get_quote_weight()); - assert_abs_diff_eq!( - actual_quote_weight1, - actual_quote_weight2, - epsilon = WEIGHT_PRECISION - ); - }); - }); - } - - /// Should work ok when initial alpha is zero - /// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::dispatchables::test_adjust_protocol_liquidity_zero_alpha --exact --nocapture - #[test] - fn test_adjust_protocol_liquidity_zero_alpha() { - // test case: tao_delta, alpha_delta - [ - (0_u64, 0_u64), - (0_u64, 1_u64), - (1_u64, 0_u64), - (1_u64, 1_u64), - (0_u64, 10_u64), - (10_u64, 0_u64), - (10_u64, 10_u64), - (0_u64, 100_u64), - (100_u64, 0_u64), - (100_u64, 100_u64), - (0_u64, 1_000_u64), - (1_000_u64, 0_u64), - (1_000_u64, 1_000_u64), - (1_000_000_u64, 0_u64), - (0_u64, 1_000_000_u64), - (1_000_000_u64, 1_000_000_u64), - (1_000_000_000_u64, 0_u64), - (0_u64, 1_000_000_000_u64), - (1_000_000_000_u64, 1_000_000_000_u64), - (1_000_000_000_000_u64, 0_u64), - (0_u64, 1_000_000_000_000_u64), - (1_000_000_000_000_u64, 1_000_000_000_000_u64), - (1_u64, 2_u64), - (2_u64, 1_u64), - (10_u64, 20_u64), - (20_u64, 10_u64), - (100_u64, 200_u64), - (200_u64, 100_u64), - (1_000_u64, 2_000_u64), - (2_000_u64, 1_000_u64), - (1_000_000_u64, 2_000_000_u64), - (2_000_000_u64, 1_000_000_u64), - (1_000_000_000_u64, 2_000_000_000_u64), - (2_000_000_000_u64, 1_000_000_000_u64), - (1_000_000_000_000_u64, 2_000_000_000_000_u64), - (2_000_000_000_000_u64, 1_000_000_000_000_u64), - (1_234_567_u64, 2_432_765_u64), - (1_234_567_u64, 2_432_765_890_u64), - ] - .into_iter() - .for_each(|(tao_delta, alpha_delta)| { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - let tao_delta = TaoBalance::from(tao_delta); - let alpha_delta = AlphaBalance::from(alpha_delta); - - // Initialize reserves and price - // broken state: Zero price because of zero alpha reserve - let tao = TaoBalance::from(1_000_000_000_000_u64); - let alpha = AlphaBalance::from(0_u64); - TaoReserve::set_mock_reserve(netuid, tao); - AlphaReserve::set_mock_reserve(netuid, alpha); - let price_before = Swap::current_price(netuid); - assert_eq!(price_before, U64F64::from_num(0)); - let new_tao = u64::from(tao + tao_delta) as f64; - let new_alpha = u64::from(alpha + alpha_delta) as f64; - - // Adjust reserves - Swap::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta); - TaoReserve::set_mock_reserve(netuid, tao + tao_delta); - AlphaReserve::set_mock_reserve(netuid, alpha + alpha_delta); - - let res_weights = SwapBalancer::::get(netuid); - let actual_quote_weight = perquintill_to_f64(res_weights.get_quote_weight()); - - // Check that price didn't change - let price_after = Swap::current_price(netuid); - if new_alpha == 0. { - // If the pool state is still broken (∆x = 0), no change - assert_eq!(actual_quote_weight, 0.5); - assert_eq!(price_after, U64F64::from_num(0)); - } else { - // Price got fixed - let expected_price = new_tao / new_alpha; - assert_abs_diff_eq!( - expected_price, - price_after.to_num::(), - epsilon = price_before.to_num::() / 1_000_000_000_000. - ); - assert_eq!(actual_quote_weight, 0.5); - } - }); - }); - } -} - -#[test] -fn test_swap_initialization() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - // Setup reserves - let tao = TaoBalance::from(1_000_000_000u64); - let alpha = AlphaBalance::from(4_000_000_000u64); - TaoReserve::set_mock_reserve(netuid, tao); - AlphaReserve::set_mock_reserve(netuid, alpha); - - assert_ok!(Pallet::::maybe_initialize_palswap(netuid, None)); - assert!(PalSwapInitialized::::get(netuid)); - - // Verify current price is set - let price = Pallet::::current_price(netuid); - let expected_price = U64F64::from_num(0.25_f64); - assert_abs_diff_eq!( - price.to_num::(), - expected_price.to_num::(), - epsilon = 0.000000001 - ); - - // Verify that swap reserve weight is initialized - let reserve_weight = SwapBalancer::::get(netuid); - assert_eq!( - reserve_weight.get_quote_weight(), - Perquintill::from_rational(1_u64, 2_u64), - ); - }); -} - -#[test] -fn test_swap_initialization_with_price() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - // Setup reserves, tao / alpha = 0.25 - let tao = TaoBalance::from(1_000_000_000u64); - let alpha = AlphaBalance::from(4_000_000_000u64); - TaoReserve::set_mock_reserve(netuid, tao); - AlphaReserve::set_mock_reserve(netuid, alpha); - - // Initialize with 0.2 price - assert_ok!(Pallet::::maybe_initialize_palswap( - netuid, - Some(U64F64::from(1u16) / U64F64::from(5u16)) - )); - assert!(PalSwapInitialized::::get(netuid)); - - // Verify current price is set to 0.2 - let price = Pallet::::current_price(netuid); - let expected_price = U64F64::from_num(0.2_f64); - assert_abs_diff_eq!( - price.to_num::(), - expected_price.to_num::(), - epsilon = 0.000000001 - ); - }); -} - -// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_swap_basic --exact --nocapture -#[test] -fn test_swap_basic() { - new_test_ext().execute_with(|| { - fn perform_test( - netuid: NetUid, - order: Order, - limit_price: f64, - price_should_grow: bool, - ) where - Order: OrderT, - BasicSwapStep: - SwapStep, - { - let swap_amount = order.amount().to_u64(); - - // Setup swap - // Price is 0.25 - let initial_tao_reserve = TaoBalance::from(1_000_000_000_u64); - let initial_alpha_reserve = AlphaBalance::from(4_000_000_000_u64); - TaoReserve::set_mock_reserve(netuid, initial_tao_reserve); - AlphaReserve::set_mock_reserve(netuid, initial_alpha_reserve); - assert_ok!(Pallet::::maybe_initialize_palswap(netuid, None)); - - // Get current price - let current_price_before = Pallet::::current_price(netuid); - - // Get reserves - let tao_reserve = TaoReserve::reserve(netuid.into()).to_u64(); - let alpha_reserve = AlphaReserve::reserve(netuid.into()).to_u64(); - - // Expected fee amount - let fee_rate = FeeRate::::get(netuid) as f64 / u16::MAX as f64; - let expected_fee = (swap_amount as f64 * fee_rate) as u64; - - // Calculate expected output amount using f64 math - // This is a simple case when w1 = w2 = 0.5, so there's no - // exponentiation needed - let x = alpha_reserve as f64; - let y = tao_reserve as f64; - let expected_output_amount = if price_should_grow { - x * (1.0 - y / (y + (swap_amount - expected_fee) as f64)) - } else { - y * (1.0 - x / (x + (swap_amount - expected_fee) as f64)) - }; - - // Swap - let limit_price_fixed = U64F64::from_num(limit_price); - let swap_result = - Pallet::::do_swap(netuid, order.clone(), limit_price_fixed, false, false) - .unwrap(); - assert_abs_diff_eq!( - swap_result.amount_paid_out.to_u64(), - expected_output_amount as u64, - epsilon = 1 - ); - - assert_abs_diff_eq!( - swap_result.paid_in_reserve_delta() as u64, - (swap_amount - expected_fee), - epsilon = 1 - ); - assert_abs_diff_eq!( - swap_result.paid_out_reserve_delta() as i64, - -(expected_output_amount as i64), - epsilon = 1 - ); - - // Update reserves (because it happens outside of do_swap in stake_utils) - if price_should_grow { - TaoReserve::set_mock_reserve( - netuid, - TaoBalance::from( - (u64::from(initial_tao_reserve) as i128 - + swap_result.paid_in_reserve_delta()) as u64, - ), - ); - AlphaReserve::set_mock_reserve( - netuid, - AlphaBalance::from( - (u64::from(initial_alpha_reserve) as i128 - + swap_result.paid_out_reserve_delta()) as u64, - ), - ); - } else { - TaoReserve::set_mock_reserve( - netuid, - TaoBalance::from( - (u64::from(initial_tao_reserve) as i128 - + swap_result.paid_out_reserve_delta()) as u64, - ), - ); - AlphaReserve::set_mock_reserve( - netuid, - AlphaBalance::from( - (u64::from(initial_alpha_reserve) as i128 - + swap_result.paid_in_reserve_delta()) as u64, - ), - ); - } - - // Assert that price movement is in correct direction - let current_price_after = Pallet::::current_price(netuid); - assert_eq!( - current_price_after >= current_price_before, - price_should_grow - ); - } - - // Current price is 0.25 - // Test case is (order_type, liquidity, limit_price, output_amount) - perform_test(1.into(), GetAlphaForTao::with_amount(1_000), 1000.0, true); - perform_test(1.into(), GetAlphaForTao::with_amount(2_000), 1000.0, true); - perform_test(1.into(), GetAlphaForTao::with_amount(123_456), 1000.0, true); - perform_test(2.into(), GetTaoForAlpha::with_amount(1_000), 0.0001, false); - perform_test(2.into(), GetTaoForAlpha::with_amount(2_000), 0.0001, false); - perform_test( - 2.into(), - GetTaoForAlpha::with_amount(123_456), - 0.0001, - false, - ); - perform_test( - 3.into(), - GetAlphaForTao::with_amount(1_000_000_000), - 1000.0, - true, - ); - perform_test( - 3.into(), - GetAlphaForTao::with_amount(10_000_000_000_u64), - 1000.0, - true, - ); - }); -} - -// cargo test --package pallet-subtensor-swap --lib -- pallet::impls::tests::test_swap_precision_edge_case --exact --show-output -#[test] -fn test_swap_precision_edge_case() { - // Test case: tao_reserve, alpha_reserve, swap_amount - [ - (1_000_u64, 1_000_u64, 999_500_u64), - (1_000_000_u64, 1_000_000_u64, 999_500_000_u64), - ] - .into_iter() - .for_each(|(tao_reserve, alpha_reserve, swap_amount)| { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let order = GetTaoForAlpha::with_amount(swap_amount); - - // Very low reserves - TaoReserve::set_mock_reserve(netuid, TaoBalance::from(tao_reserve)); - AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(alpha_reserve)); - - // Minimum possible limit price - let limit_price: U64F64 = get_min_price(); - println!("limit_price = {:?}", limit_price); - - // Swap - let swap_result = - Pallet::::do_swap(netuid, order, limit_price, false, true).unwrap(); - - assert!(swap_result.amount_paid_out > TaoBalance::ZERO); - }); - }); -} - -#[test] -fn test_convert_deltas() { - new_test_ext().execute_with(|| { - for (tao, alpha, w_quote, delta_in) in [ - (1500, 1000, 0.5, 1), - (1500, 1000, 0.5, 10000), - (1500, 1000, 0.5, 1000000), - (1500, 1000, 0.5, u64::MAX), - (1, 1000000, 0.5, 1), - (1, 1000000, 0.5, 10000), - (1, 1000000, 0.5, 1000000), - (1, 1000000, 0.5, u64::MAX), - (1000000, 1, 0.5, 1), - (1000000, 1, 0.5, 10000), - (1000000, 1, 0.5, 1000000), - (1000000, 1, 0.5, u64::MAX), - (1500, 1000, 0.50000001, 1), - (1500, 1000, 0.50000001, 10000), - (1500, 1000, 0.50000001, 1000000), - (1500, 1000, 0.50000001, u64::MAX), - (1, 1000000, 0.50000001, 1), - (1, 1000000, 0.50000001, 10000), - (1, 1000000, 0.50000001, 1000000), - (1, 1000000, 0.50000001, u64::MAX), - (1000000, 1, 0.50000001, 1), - (1000000, 1, 0.50000001, 10000), - (1000000, 1, 0.50000001, 1000000), - (1000000, 1, 0.50000001, u64::MAX), - (1500, 1000, 0.49999999, 1), - (1500, 1000, 0.49999999, 10000), - (1500, 1000, 0.49999999, 1000000), - (1500, 1000, 0.49999999, u64::MAX), - (1, 1000000, 0.49999999, 1), - (1, 1000000, 0.49999999, 10000), - (1, 1000000, 0.49999999, 1000000), - (1, 1000000, 0.49999999, u64::MAX), - (1000000, 1, 0.49999999, 1), - (1000000, 1, 0.49999999, 10000), - (1000000, 1, 0.49999999, 1000000), - (1000000, 1, 0.49999999, u64::MAX), - // Low quote weight - (1500, 1000, 0.1, 1), - (1500, 1000, 0.1, 10000), - (1500, 1000, 0.1, 1000000), - (1500, 1000, 0.1, u64::MAX), - (1, 1000000, 0.1, 1), - (1, 1000000, 0.1, 10000), - (1, 1000000, 0.1, 1000000), - (1, 1000000, 0.1, u64::MAX), - (1000000, 1, 0.1, 1), - (1000000, 1, 0.1, 10000), - (1000000, 1, 0.1, 1000000), - (1000000, 1, 0.1, u64::MAX), - // High quote weight - (1500, 1000, 0.9, 1), - (1500, 1000, 0.9, 10000), - (1500, 1000, 0.9, 1000000), - (1500, 1000, 0.9, u64::MAX), - (1, 1000000, 0.9, 1), - (1, 1000000, 0.9, 10000), - (1, 1000000, 0.9, 1000000), - (1, 1000000, 0.9, u64::MAX), - (1000000, 1, 0.9, 1), - (1000000, 1, 0.9, 10000), - (1000000, 1, 0.9, 1000000), - (1000000, 1, 0.9, u64::MAX), - ] { - // Initialize reserves and weights - let netuid = NetUid::from(1); - TaoReserve::set_mock_reserve(netuid, TaoBalance::from(tao)); - AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(alpha)); - assert_ok!(Pallet::::maybe_initialize_palswap(netuid, None)); - - let w_accuracy = 1_000_000_000_f64; - let w_quote_pt = - Perquintill::from_rational((w_quote * w_accuracy) as u128, w_accuracy as u128); - let bal = Balancer::new(w_quote_pt).unwrap(); - SwapBalancer::::insert(netuid, bal); - - // Calculate expected swap results (buy and sell) using f64 math - let y = tao as f64; - let x = alpha as f64; - let d = delta_in as f64; - let w1_div_w2 = (1. - w_quote) / w_quote; - let w2_div_w1 = w_quote / (1. - w_quote); - let expected_sell = y * (1. - (x / (x + d)).powf(w1_div_w2)); - let expected_buy = x * (1. - (y / (y + d)).powf(w2_div_w1)); - - assert_abs_diff_eq!( - u64::from( - BasicSwapStep::::convert_deltas( - netuid, - delta_in.into() - ) - ), - expected_sell as u64, - epsilon = 2u64 - ); - assert_abs_diff_eq!( - u64::from( - BasicSwapStep::::convert_deltas( - netuid, - delta_in.into() - ) - ), - expected_buy as u64, - epsilon = 2u64 - ); - } - }); -} - -#[test] -fn test_rollback_works() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - assert_eq!( - Pallet::::do_swap( - netuid, - GetAlphaForTao::with_amount(1_000_000), - u64::MAX.into(), - false, - true - ) - .unwrap(), - Pallet::::do_swap( - netuid, - GetAlphaForTao::with_amount(1_000_000), - u64::MAX.into(), - false, - false - ) - .unwrap() - ); - }) -} - -#[test] -fn test_swap_rejects_input_over_1000x_input_reserve() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - TaoReserve::set_mock_reserve(netuid, TaoBalance::from(1_000)); - AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(1_000)); - - assert_noop!( - Pallet::::do_swap( - netuid, - GetTaoForAlpha::with_amount(1_000_001), - get_min_price(), - true, - false, - ), - Error::::SwapInputTooLarge - ); - assert_noop!( - Pallet::::do_swap( - netuid, - GetAlphaForTao::with_amount(1_000_001), - get_max_price(), - true, - false, - ), - Error::::SwapInputTooLarge - ); - }); -} - -#[test] -fn test_sim_swap_rejects_input_over_1000x_input_reserve() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - TaoReserve::set_mock_reserve(netuid, TaoBalance::from(1_000)); - AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(1_000)); - - assert_noop!( - Pallet::::sim_swap(netuid, GetTaoForAlpha::with_amount(1_001_000)), - Error::::SwapInputTooLarge - ); - assert_noop!( - Pallet::::sim_swap(netuid, GetAlphaForTao::with_amount(1_001_000)), - Error::::SwapInputTooLarge - ); - }); -} - -#[test] -fn test_swap_allows_input_at_1000x_input_reserve() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - TaoReserve::set_mock_reserve(netuid, TaoBalance::from(1_000)); - AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(1_000)); - - assert_ok!(Pallet::::do_swap( - netuid, - GetTaoForAlpha::with_amount(1_000_000), - get_min_price(), - true, - true, - )); - assert_ok!(Pallet::::do_swap( - netuid, - GetAlphaForTao::with_amount(1_000_000), - get_max_price(), - true, - true, - )); - }); -} - -#[allow(dead_code)] -fn bbox(t: U64F64, a: U64F64, b: U64F64) -> U64F64 { - if t < a { - a - } else if t > b { - b - } else { - t - } -} - -#[allow(dead_code)] -fn print_current_price(netuid: NetUid) { - let current_price = Pallet::::current_price(netuid); - log::trace!("Current price: {current_price:.6}"); -} - -/// Reservoir liquidity is already materialized but not price-active; direct -/// cleanup materializes it into the reserve abstraction before clearing. -#[test] -fn test_clear_protocol_liquidity_clears_nonzero_reservoirs() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(202); - - // Insert map values - FeeRate::::insert(netuid, 1_000); - PalSwapInitialized::::insert(netuid, true); - BalancerTaoReservoir::::insert(netuid, TaoBalance::from(12_345_u64)); - BalancerAlphaReservoir::::insert(netuid, AlphaBalance::from(67_890_u64)); - let w_quote_pt = Perquintill::from_rational(1u128, 2u128); - let bal = Balancer::new(w_quote_pt).unwrap(); - SwapBalancer::::insert(netuid, bal); - - // Sanity: PalSwap is not initialized - assert!(PalSwapInitialized::::get(netuid)); - - // ACT - assert!(Pallet::::do_clear_protocol_liquidity( - netuid, - &mut WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)) - )); - - assert!(!FeeRate::::contains_key(netuid)); - assert!(!PalSwapInitialized::::contains_key(netuid)); - assert!(!SwapBalancer::::contains_key(netuid)); - assert!(!BalancerTaoReservoir::::contains_key(netuid)); - assert!(!BalancerAlphaReservoir::::contains_key(netuid)); - }); -} - -#[test] -fn test_clear_protocol_liquidity_green_path() { - new_test_ext().execute_with(|| { - // --- Arrange --- - let netuid = NetUid::from(1); - - // Initialize swap state - assert_ok!(Pallet::::maybe_initialize_palswap(netuid, None)); - assert!( - PalSwapInitialized::::get(netuid), - "Swap must be initialized" - ); - - // --- Act --- - // Green path: just clear protocol liquidity and wipe all V3 state. - assert!(Pallet::::do_clear_protocol_liquidity( - netuid, - &mut WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)) - )); - - // Flags - assert!(!PalSwapInitialized::::contains_key(netuid)); - - // Knobs removed - assert!(!FeeRate::::contains_key(netuid)); - - // --- And it's idempotent --- - assert!(Pallet::::do_clear_protocol_liquidity( - netuid, - &mut WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)) - )); - assert!(!PalSwapInitialized::::contains_key(netuid)); - }); -} - -// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_migrate_swapv3_to_balancer --exact --nocapture -#[test] -fn test_migrate_swapv3_to_balancer() { - use crate::migrations::migrate_swapv3_to_balancer::deprecated_swap_maps; - use substrate_fixed::types::U64F64; - - new_test_ext().execute_with(|| { - let migration = - crate::migrations::migrate_swapv3_to_balancer::migrate_swapv3_to_balancer::; - let netuid = NetUid::from(1); - - // Insert deprecated maps values - deprecated_swap_maps::AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1.23)); - deprecated_swap_maps::ScrapReservoirTao::::insert(netuid, TaoBalance::from(9876)); - deprecated_swap_maps::ScrapReservoirAlpha::::insert(netuid, AlphaBalance::from(9876)); - - // Insert reserves that do not match the 1.23 price - TaoReserve::set_mock_reserve(netuid, TaoBalance::from(1_000_000_000)); - AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(4_000_000_000_u64)); - - // Run migration - migration(); - - // Test that values are removed from state - assert!(!deprecated_swap_maps::AlphaSqrtPrice::::contains_key( - netuid - )); - assert!(!deprecated_swap_maps::ScrapReservoirAlpha::::contains_key(netuid)); - - // Test that subnet price is still 1.23^2 - assert_abs_diff_eq!( - Swap::current_price(netuid).to_num::(), - 1.23 * 1.23, - epsilon = 0.1 - ); - }); -} - -#[test] -fn test_migrate_swapv3_to_balancer_falls_back_to_default_when_price_init_fails() { - use crate::migrations::migrate_swapv3_to_balancer::deprecated_swap_maps; - use substrate_fixed::types::U64F64; - - new_test_ext().execute_with(|| { - let migration = - crate::migrations::migrate_swapv3_to_balancer::migrate_swapv3_to_balancer::; - let migration_name = - frame_support::BoundedVec::truncate_from(b"migrate_swapv3_to_balancer".to_vec()); - let netuid = NetUid::from(1); - - deprecated_swap_maps::AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1)); - deprecated_swap_maps::ScrapReservoirTao::::insert(netuid, TaoBalance::from(9876)); - deprecated_swap_maps::ScrapReservoirAlpha::::insert(netuid, AlphaBalance::from(9876)); - - TaoReserve::set_mock_reserve(netuid, TaoBalance::from(1)); - AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(1_000_000_000_000_u64)); - - migration(); - - assert!(!deprecated_swap_maps::AlphaSqrtPrice::::contains_key( - netuid - )); - assert!(!deprecated_swap_maps::ScrapReservoirTao::::contains_key(netuid)); - assert!(!deprecated_swap_maps::ScrapReservoirAlpha::::contains_key(netuid)); - assert!(PalSwapInitialized::::get(netuid)); - assert_eq!( - SwapBalancer::::get(netuid).get_quote_weight(), - Perquintill::from_rational(1_u64, 2_u64) - ); - assert!(HasMigrationRun::::get(&migration_name)); - }); -} diff --git a/pallets/swap/src/pallet/tests/adjust_protocol_liquidity.rs b/pallets/swap/src/pallet/tests/adjust_protocol_liquidity.rs new file mode 100644 index 0000000000..48331f7ed4 --- /dev/null +++ b/pallets/swap/src/pallet/tests/adjust_protocol_liquidity.rs @@ -0,0 +1,414 @@ +//! Tests for protocol liquidity injection via [`Pallet::adjust_protocol_liquidity`]. + +use super::*; + +fn perquintill_to_f64(p: Perquintill) -> f64 { + let parts = p.deconstruct() as f64; + parts / 1_000_000_000_000_000_000_f64 +} + +/// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::adjust_protocol_liquidity::test_adjust_protocol_liquidity_happy --exact --nocapture +#[test] +fn test_adjust_protocol_liquidity_happy() { + // test case: tao_delta, alpha_delta + [ + (0_u64, 0_u64), + (0_u64, 1_u64), + (1_u64, 0_u64), + (1_u64, 1_u64), + (0_u64, 10_u64), + (10_u64, 0_u64), + (10_u64, 10_u64), + (0_u64, 100_u64), + (100_u64, 0_u64), + (100_u64, 100_u64), + (0_u64, 1_000_u64), + (1_000_u64, 0_u64), + (1_000_u64, 1_000_u64), + (1_000_000_u64, 0_u64), + (0_u64, 1_000_000_u64), + (1_000_000_u64, 1_000_000_u64), + (1_000_000_000_u64, 0_u64), + (0_u64, 1_000_000_000_u64), + (1_000_000_000_u64, 1_000_000_000_u64), + (1_000_000_000_000_u64, 0_u64), + (0_u64, 1_000_000_000_000_u64), + (1_000_000_000_000_u64, 1_000_000_000_000_u64), + (1_u64, 2_u64), + (2_u64, 1_u64), + (10_u64, 20_u64), + (20_u64, 10_u64), + (100_u64, 200_u64), + (200_u64, 100_u64), + (1_000_u64, 2_000_u64), + (2_000_u64, 1_000_u64), + (1_000_000_u64, 2_000_000_u64), + (2_000_000_u64, 1_000_000_u64), + (1_000_000_000_u64, 2_000_000_000_u64), + (2_000_000_000_u64, 1_000_000_000_u64), + (1_000_000_000_000_u64, 2_000_000_000_000_u64), + (2_000_000_000_000_u64, 1_000_000_000_000_u64), + (1_234_567_u64, 2_432_765_u64), + (1_234_567_u64, 2_432_765_890_u64), + ] + .into_iter() + .for_each(|(tao_delta, alpha_delta)| { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let tao_delta = TaoBalance::from(tao_delta); + let alpha_delta = AlphaBalance::from(alpha_delta); + + // Initialize reserves and price + let tao = TaoBalance::from(1_000_000_000_000_u64); + let alpha = AlphaBalance::from(4_000_000_000_000_u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + let price_before = Swap::current_price(netuid); + + // Adjust reserves + Swap::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta); + TaoReserve::set_mock_reserve(netuid, tao + tao_delta); + AlphaReserve::set_mock_reserve(netuid, alpha + alpha_delta); + + // Check that price didn't change + let price_after = Swap::current_price(netuid); + assert_abs_diff_eq!( + price_before.to_num::(), + price_after.to_num::(), + epsilon = price_before.to_num::() / 1_000_000_000_000. + ); + + // Check that reserve weight was properly updated + let new_tao = u64::from(tao + tao_delta) as f64; + let new_alpha = u64::from(alpha + alpha_delta) as f64; + let expected_quote_weight = + new_tao / (new_alpha * price_before.to_num::() + new_tao); + let expected_quote_weight_delta = expected_quote_weight - 0.5; + let res_weights = SwapBalancer::::get(netuid); + let actual_quote_weight_delta = + perquintill_to_f64(res_weights.get_quote_weight()) - 0.5; + let eps = expected_quote_weight / 1_000_000_000_000.; + assert_abs_diff_eq!( + expected_quote_weight_delta, + actual_quote_weight_delta, + epsilon = eps + ); + }); + }); +} + +#[test] +fn test_adjust_protocol_liquidity_materializes_tao_when_reservoiring_tao() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + let tao = TaoBalance::from(1_000_u64); + let alpha = AlphaBalance::from(1_000_u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + + let (price_active_tao, price_active_alpha) = Swap::adjust_protocol_liquidity( + netuid, + TaoBalance::from(200_000_u64), + AlphaBalance::from(1_000_u64), + ); + + assert_eq!(price_active_tao, TaoBalance::ZERO); + assert_eq!(price_active_alpha, AlphaBalance::from(1_000_u64)); + assert_eq!( + BalancerTaoReservoir::::get(netuid), + TaoBalance::from(200_000_u64) + ); + assert_eq!( + BalancerAlphaReservoir::::get(netuid), + AlphaBalance::ZERO + ); + }); +} + +#[test] +fn test_adjust_protocol_liquidity_materializes_alpha_when_reservoiring_alpha() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + let tao = TaoBalance::from(1_000_u64); + let alpha = AlphaBalance::from(1_000_u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + + let (price_active_tao, price_active_alpha) = Swap::adjust_protocol_liquidity( + netuid, + TaoBalance::from(1_000_u64), + AlphaBalance::from(200_000_u64), + ); + + assert_eq!(price_active_tao, TaoBalance::from(1_000_u64)); + assert_eq!(price_active_alpha, AlphaBalance::ZERO); + assert_eq!(BalancerTaoReservoir::::get(netuid), TaoBalance::ZERO); + assert_eq!( + BalancerAlphaReservoir::::get(netuid), + AlphaBalance::from(200_000_u64) + ); + }); +} + +#[test] +fn test_adjust_protocol_liquidity_retries_reservoir_with_new_injection() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + let mut tao = TaoBalance::from(1_000_u64); + let mut alpha = AlphaBalance::from(1_000_u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + + let (price_active_tao, price_active_alpha) = Swap::adjust_protocol_liquidity( + netuid, + TaoBalance::from(200_000_u64), + AlphaBalance::from(1_000_u64), + ); + assert_eq!(price_active_tao, TaoBalance::ZERO); + assert_eq!(price_active_alpha, AlphaBalance::from(1_000_u64)); + tao += price_active_tao; + alpha += price_active_alpha; + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + + let (price_active_tao, price_active_alpha) = Swap::adjust_protocol_liquidity( + netuid, + TaoBalance::from(1_000_u64), + AlphaBalance::from(200_000_u64), + ); + + assert!(price_active_tao >= TaoBalance::from(1_000_u64)); + assert!(price_active_alpha >= AlphaBalance::from(200_000_u64)); + assert_eq!(BalancerTaoReservoir::::get(netuid), TaoBalance::ZERO); + assert_eq!( + BalancerAlphaReservoir::::get(netuid), + AlphaBalance::ZERO + ); + }); +} + +#[test] +fn test_adjust_protocol_liquidity_activates_reservoir_amounts() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + TaoReserve::set_mock_reserve(netuid, TaoBalance::from(1_000_000_u64)); + AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(1_000_000_u64)); + BalancerTaoReservoir::::insert(netuid, TaoBalance::from(10_000_u64)); + BalancerAlphaReservoir::::insert(netuid, AlphaBalance::from(20_000_u64)); + + let tao_delta = TaoBalance::from(300_u64); + let alpha_delta = AlphaBalance::from(400_u64); + let (price_active_tao, price_active_alpha) = + Swap::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta); + + assert_eq!(price_active_tao, TaoBalance::from(10_300_u64)); + assert_eq!(price_active_alpha, AlphaBalance::from(20_400_u64)); + assert_eq!(BalancerTaoReservoir::::get(netuid), TaoBalance::ZERO); + assert_eq!( + BalancerAlphaReservoir::::get(netuid), + AlphaBalance::ZERO + ); + }); +} + +/// This test case verifies that small gradual injections (like emissions in every block) +/// in the worst case +/// - Do not cause price to change +/// - Result in the same weight change as one large injection +/// +/// This is a long test that only tests validity of weights math. Run again if changing +/// Balancer::update_weights_for_added_liquidity +/// +/// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::adjust_protocol_liquidity::test_adjust_protocol_liquidity_deltas --exact --nocapture +#[ignore] +#[test] +fn test_adjust_protocol_liquidity_deltas() { + // The number of times (blocks) over which gradual injections will be made + // One year price drift due to precision is under 1e-6 + const ITERATIONS: u64 = 2_700_000; + const PRICE_PRECISION: f64 = 0.000_001; + const PREC_LARGE_DELTA: f64 = 0.001; + const WEIGHT_PRECISION: f64 = 0.000_000_000_000_000_001; + + let initial_tao_reserve = TaoBalance::from(1_000_000_000_000_000_u64); + let initial_alpha_reserve = AlphaBalance::from(10_000_000_000_000_000_u64); + + // test case: tao_delta, alpha_delta, price_precision + [ + (0_u64, 0_u64, PRICE_PRECISION), + (0_u64, 1_u64, PRICE_PRECISION), + (1_u64, 0_u64, PRICE_PRECISION), + (1_u64, 1_u64, PRICE_PRECISION), + (0_u64, 10_u64, PRICE_PRECISION), + (10_u64, 0_u64, PRICE_PRECISION), + (10_u64, 10_u64, PRICE_PRECISION), + (0_u64, 100_u64, PRICE_PRECISION), + (100_u64, 0_u64, PRICE_PRECISION), + (100_u64, 100_u64, PRICE_PRECISION), + (0_u64, 987_u64, PRICE_PRECISION), + (987_u64, 0_u64, PRICE_PRECISION), + (876_u64, 987_u64, PRICE_PRECISION), + (0_u64, 1_000_u64, PRICE_PRECISION), + (1_000_u64, 0_u64, PRICE_PRECISION), + (1_000_u64, 1_000_u64, PRICE_PRECISION), + (0_u64, 1_234_u64, PRICE_PRECISION), + (1_234_u64, 0_u64, PRICE_PRECISION), + (1_234_u64, 4_321_u64, PRICE_PRECISION), + (1_234_000_u64, 4_321_000_u64, PREC_LARGE_DELTA), + (1_234_u64, 4_321_000_u64, PREC_LARGE_DELTA), + ] + .into_iter() + .for_each(|(tao_delta, alpha_delta, price_precision)| { + new_test_ext().execute_with(|| { + let netuid1 = NetUid::from(1); + + let tao_delta = TaoBalance::from(tao_delta); + let alpha_delta = AlphaBalance::from(alpha_delta); + + // Initialize realistically large reserves + let mut tao = initial_tao_reserve; + let mut alpha = initial_alpha_reserve; + TaoReserve::set_mock_reserve(netuid1, tao); + AlphaReserve::set_mock_reserve(netuid1, alpha); + let price_before = Swap::current_price(netuid1); + + // Adjust reserves gradually + for _ in 0..ITERATIONS { + Swap::adjust_protocol_liquidity(netuid1, tao_delta, alpha_delta); + tao += tao_delta; + alpha += alpha_delta; + TaoReserve::set_mock_reserve(netuid1, tao); + AlphaReserve::set_mock_reserve(netuid1, alpha); + } + // Check that price didn't change + let price_after = Swap::current_price(netuid1); + assert_abs_diff_eq!( + price_before.to_num::(), + price_after.to_num::(), + epsilon = price_precision + ); + + ///////////////////////// + // Now do one-time big injection with another netuid and compare weights + let netuid2 = NetUid::from(2); + + // Initialize same large reserves + TaoReserve::set_mock_reserve(netuid2, initial_tao_reserve); + AlphaReserve::set_mock_reserve(netuid2, initial_alpha_reserve); + + // Adjust reserves by one large amount at once + let tao_delta_once = TaoBalance::from(ITERATIONS * u64::from(tao_delta)); + let alpha_delta_once = AlphaBalance::from(ITERATIONS * u64::from(alpha_delta)); + Swap::adjust_protocol_liquidity(netuid2, tao_delta_once, alpha_delta_once); + TaoReserve::set_mock_reserve(netuid2, initial_tao_reserve + tao_delta_once); + AlphaReserve::set_mock_reserve(netuid2, initial_alpha_reserve + alpha_delta_once); + + // Compare reserve weights for netuid 1 and 2 + let res_weights1 = SwapBalancer::::get(netuid1); + let res_weights2 = SwapBalancer::::get(netuid2); + let actual_quote_weight1 = perquintill_to_f64(res_weights1.get_quote_weight()); + let actual_quote_weight2 = perquintill_to_f64(res_weights2.get_quote_weight()); + assert_abs_diff_eq!( + actual_quote_weight1, + actual_quote_weight2, + epsilon = WEIGHT_PRECISION + ); + }); + }); +} + +/// Should work ok when initial alpha is zero +/// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::adjust_protocol_liquidity::test_adjust_protocol_liquidity_zero_alpha --exact --nocapture +#[test] +fn test_adjust_protocol_liquidity_zero_alpha() { + // test case: tao_delta, alpha_delta + [ + (0_u64, 0_u64), + (0_u64, 1_u64), + (1_u64, 0_u64), + (1_u64, 1_u64), + (0_u64, 10_u64), + (10_u64, 0_u64), + (10_u64, 10_u64), + (0_u64, 100_u64), + (100_u64, 0_u64), + (100_u64, 100_u64), + (0_u64, 1_000_u64), + (1_000_u64, 0_u64), + (1_000_u64, 1_000_u64), + (1_000_000_u64, 0_u64), + (0_u64, 1_000_000_u64), + (1_000_000_u64, 1_000_000_u64), + (1_000_000_000_u64, 0_u64), + (0_u64, 1_000_000_000_u64), + (1_000_000_000_u64, 1_000_000_000_u64), + (1_000_000_000_000_u64, 0_u64), + (0_u64, 1_000_000_000_000_u64), + (1_000_000_000_000_u64, 1_000_000_000_000_u64), + (1_u64, 2_u64), + (2_u64, 1_u64), + (10_u64, 20_u64), + (20_u64, 10_u64), + (100_u64, 200_u64), + (200_u64, 100_u64), + (1_000_u64, 2_000_u64), + (2_000_u64, 1_000_u64), + (1_000_000_u64, 2_000_000_u64), + (2_000_000_u64, 1_000_000_u64), + (1_000_000_000_u64, 2_000_000_000_u64), + (2_000_000_000_u64, 1_000_000_000_u64), + (1_000_000_000_000_u64, 2_000_000_000_000_u64), + (2_000_000_000_000_u64, 1_000_000_000_000_u64), + (1_234_567_u64, 2_432_765_u64), + (1_234_567_u64, 2_432_765_890_u64), + ] + .into_iter() + .for_each(|(tao_delta, alpha_delta)| { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + let tao_delta = TaoBalance::from(tao_delta); + let alpha_delta = AlphaBalance::from(alpha_delta); + + // Initialize reserves and price + // broken state: Zero price because of zero alpha reserve + let tao = TaoBalance::from(1_000_000_000_000_u64); + let alpha = AlphaBalance::from(0_u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + let price_before = Swap::current_price(netuid); + assert_eq!(price_before, U64F64::from_num(0)); + let new_tao = u64::from(tao + tao_delta) as f64; + let new_alpha = u64::from(alpha + alpha_delta) as f64; + + // Adjust reserves + Swap::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta); + TaoReserve::set_mock_reserve(netuid, tao + tao_delta); + AlphaReserve::set_mock_reserve(netuid, alpha + alpha_delta); + + let res_weights = SwapBalancer::::get(netuid); + let actual_quote_weight = perquintill_to_f64(res_weights.get_quote_weight()); + + // Check that price didn't change + let price_after = Swap::current_price(netuid); + if new_alpha == 0. { + // If the pool state is still broken (∆x = 0), no change + assert_eq!(actual_quote_weight, 0.5); + assert_eq!(price_after, U64F64::from_num(0)); + } else { + // Price got fixed + let expected_price = new_tao / new_alpha; + assert_abs_diff_eq!( + expected_price, + price_after.to_num::(), + epsilon = price_before.to_num::() / 1_000_000_000_000. + ); + assert_eq!(actual_quote_weight, 0.5); + } + }); + }); +} diff --git a/pallets/swap/src/pallet/tests/clear_protocol_liquidity.rs b/pallets/swap/src/pallet/tests/clear_protocol_liquidity.rs new file mode 100644 index 0000000000..0205db6a78 --- /dev/null +++ b/pallets/swap/src/pallet/tests/clear_protocol_liquidity.rs @@ -0,0 +1,71 @@ +//! Tests for clearing protocol-owned swap liquidity and related state. + +use super::*; + +/// Reservoir liquidity is already materialized but not price-active; direct +/// cleanup materializes it into the reserve abstraction before clearing. +#[test] +fn test_clear_protocol_liquidity_clears_nonzero_reservoirs() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(202); + + // Insert map values + FeeRate::::insert(netuid, 1_000); + PalSwapInitialized::::insert(netuid, true); + BalancerTaoReservoir::::insert(netuid, TaoBalance::from(12_345_u64)); + BalancerAlphaReservoir::::insert(netuid, AlphaBalance::from(67_890_u64)); + let w_quote_pt = Perquintill::from_rational(1u128, 2u128); + let bal = Balancer::new(w_quote_pt).unwrap(); + SwapBalancer::::insert(netuid, bal); + + // Sanity: PalSwap is not initialized + assert!(PalSwapInitialized::::get(netuid)); + + // ACT + assert!(Pallet::::do_clear_protocol_liquidity( + netuid, + &mut WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)) + )); + + assert!(!FeeRate::::contains_key(netuid)); + assert!(!PalSwapInitialized::::contains_key(netuid)); + assert!(!SwapBalancer::::contains_key(netuid)); + assert!(!BalancerTaoReservoir::::contains_key(netuid)); + assert!(!BalancerAlphaReservoir::::contains_key(netuid)); + }); +} + +#[test] +fn test_clear_protocol_liquidity_green_path() { + new_test_ext().execute_with(|| { + // --- Arrange --- + let netuid = NetUid::from(1); + + // Initialize swap state + assert_ok!(Pallet::::maybe_initialize_palswap(netuid, None)); + assert!( + PalSwapInitialized::::get(netuid), + "Swap must be initialized" + ); + + // --- Act --- + // Green path: just clear protocol liquidity and wipe all V3 state. + assert!(Pallet::::do_clear_protocol_liquidity( + netuid, + &mut WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)) + )); + + // Flags + assert!(!PalSwapInitialized::::contains_key(netuid)); + + // Knobs removed + assert!(!FeeRate::::contains_key(netuid)); + + // --- And it's idempotent --- + assert!(Pallet::::do_clear_protocol_liquidity( + netuid, + &mut WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)) + )); + assert!(!PalSwapInitialized::::contains_key(netuid)); + }); +} diff --git a/pallets/swap/src/pallet/tests/migrate_swapv3_to_balancer.rs b/pallets/swap/src/pallet/tests/migrate_swapv3_to_balancer.rs new file mode 100644 index 0000000000..45fb4cb2eb --- /dev/null +++ b/pallets/swap/src/pallet/tests/migrate_swapv3_to_balancer.rs @@ -0,0 +1,76 @@ +//! Tests for Uniswap-v3 → balancer storage migration. + +use super::*; + +// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_migrate_swapv3_to_balancer --exact --nocapture +#[test] +fn test_migrate_swapv3_to_balancer() { + use crate::migrations::migrate_swapv3_to_balancer::deprecated_swap_maps; + use substrate_fixed::types::U64F64; + + new_test_ext().execute_with(|| { + let migration = + crate::migrations::migrate_swapv3_to_balancer::migrate_swapv3_to_balancer::; + let netuid = NetUid::from(1); + + // Insert deprecated maps values + deprecated_swap_maps::AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1.23)); + deprecated_swap_maps::ScrapReservoirTao::::insert(netuid, TaoBalance::from(9876)); + deprecated_swap_maps::ScrapReservoirAlpha::::insert(netuid, AlphaBalance::from(9876)); + + // Insert reserves that do not match the 1.23 price + TaoReserve::set_mock_reserve(netuid, TaoBalance::from(1_000_000_000)); + AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(4_000_000_000_u64)); + + // Run migration + migration(); + + // Test that values are removed from state + assert!(!deprecated_swap_maps::AlphaSqrtPrice::::contains_key( + netuid + )); + assert!(!deprecated_swap_maps::ScrapReservoirAlpha::::contains_key(netuid)); + + // Test that subnet price is still 1.23^2 + assert_abs_diff_eq!( + Swap::current_price(netuid).to_num::(), + 1.23 * 1.23, + epsilon = 0.1 + ); + }); +} + +#[test] +fn test_migrate_swapv3_to_balancer_falls_back_to_default_when_price_init_fails() { + use crate::migrations::migrate_swapv3_to_balancer::deprecated_swap_maps; + use substrate_fixed::types::U64F64; + + new_test_ext().execute_with(|| { + let migration = + crate::migrations::migrate_swapv3_to_balancer::migrate_swapv3_to_balancer::; + let migration_name = + frame_support::BoundedVec::truncate_from(b"migrate_swapv3_to_balancer".to_vec()); + let netuid = NetUid::from(1); + + deprecated_swap_maps::AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1)); + deprecated_swap_maps::ScrapReservoirTao::::insert(netuid, TaoBalance::from(9876)); + deprecated_swap_maps::ScrapReservoirAlpha::::insert(netuid, AlphaBalance::from(9876)); + + TaoReserve::set_mock_reserve(netuid, TaoBalance::from(1)); + AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(1_000_000_000_000_u64)); + + migration(); + + assert!(!deprecated_swap_maps::AlphaSqrtPrice::::contains_key( + netuid + )); + assert!(!deprecated_swap_maps::ScrapReservoirTao::::contains_key(netuid)); + assert!(!deprecated_swap_maps::ScrapReservoirAlpha::::contains_key(netuid)); + assert!(PalSwapInitialized::::get(netuid)); + assert_eq!( + SwapBalancer::::get(netuid).get_quote_weight(), + Perquintill::from_rational(1_u64, 2_u64) + ); + assert!(HasMigrationRun::::get(&migration_name)); + }); +} diff --git a/pallets/swap/src/pallet/tests/mod.rs b/pallets/swap/src/pallet/tests/mod.rs new file mode 100644 index 0000000000..cad5db4a6b --- /dev/null +++ b/pallets/swap/src/pallet/tests/mod.rs @@ -0,0 +1,62 @@ +//! Integration tests for `pallet-subtensor-swap` (TAO↔alpha balancer AMM). + +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::unwrap_used +)] + +use approx::assert_abs_diff_eq; +use frame_support::weights::WeightMeter; +use frame_support::{assert_noop, assert_ok}; +use sp_arithmetic::Perquintill; +use sp_runtime::DispatchError; +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::{NetUid, Token}; +use subtensor_swap_interface::{Order as OrderT, SwapHandler}; + +use super::*; +use crate::mock::*; +use crate::pallet::swap_step::*; + +/// Minimum alpha price used as a sell-side limit in tests (rao-normalized). +#[allow(dead_code)] +fn get_min_price() -> U64F64 { + U64F64::from_num(Pallet::::min_price_inner::()) + / U64F64::from_num(1_000_000_000) +} + +/// Maximum alpha price used as a buy-side limit in tests (rao-normalized). +#[allow(dead_code)] +fn get_max_price() -> U64F64 { + U64F64::from_num(Pallet::::max_price_inner::()) + / U64F64::from_num(1_000_000_000) +} + +/// Clamp fixed-point `t` into `[a, b]` (debug helper). +#[allow(dead_code)] +fn clamp_fixed_between(t: U64F64, a: U64F64, b: U64F64) -> U64F64 { + if t < a { + a + } else if t > b { + b + } else { + t + } +} + +/// Trace the current balancer alpha price for `netuid` (debug helper). +#[allow(dead_code)] +fn print_current_price(netuid: NetUid) { + let current_price = Pallet::::current_price(netuid); + log::trace!("Current price: {current_price:.6}"); +} + +mod adjust_protocol_liquidity; +mod clear_protocol_liquidity; +mod migrate_swapv3_to_balancer; +mod set_fee_rate; +mod swap_execution; +mod swap_initialization; +mod swap_input_limits; diff --git a/pallets/swap/src/pallet/tests/set_fee_rate.rs b/pallets/swap/src/pallet/tests/set_fee_rate.rs new file mode 100644 index 0000000000..1ff64976aa --- /dev/null +++ b/pallets/swap/src/pallet/tests/set_fee_rate.rs @@ -0,0 +1,28 @@ +//! Tests for root [`Pallet::set_fee_rate`]. + +use super::*; + +#[test] +fn test_set_fee_rate() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let fee_rate = 500; // 0.76% fee + + assert_noop!( + Swap::set_fee_rate(RuntimeOrigin::signed(666), netuid, fee_rate), + DispatchError::BadOrigin + ); + + assert_ok!(Swap::set_fee_rate(RuntimeOrigin::root(), netuid, fee_rate)); + + // Check that fee rate was set correctly + assert_eq!(FeeRate::::get(netuid), fee_rate); + + // Verify fee rate validation - should fail if too high + let too_high_fee = MaxFeeRate::get() + 1; + assert_noop!( + Swap::set_fee_rate(RuntimeOrigin::root(), netuid, too_high_fee), + Error::::FeeRateTooHigh + ); + }); +} diff --git a/pallets/swap/src/pallet/tests/swap_execution.rs b/pallets/swap/src/pallet/tests/swap_execution.rs new file mode 100644 index 0000000000..f01a4bc6c8 --- /dev/null +++ b/pallets/swap/src/pallet/tests/swap_execution.rs @@ -0,0 +1,309 @@ +//! Tests for swap execution paths (`do_swap`, delta conversion, rollback). + +use super::*; + +// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_swap_basic --exact --nocapture +#[test] +fn test_swap_basic() { + new_test_ext().execute_with(|| { + fn perform_test( + netuid: NetUid, + order: Order, + limit_price: f64, + price_should_grow: bool, + ) where + Order: OrderT, + BasicSwapStep: + SwapStep, + { + let swap_amount = order.amount().to_u64(); + + // Setup swap + // Price is 0.25 + let initial_tao_reserve = TaoBalance::from(1_000_000_000_u64); + let initial_alpha_reserve = AlphaBalance::from(4_000_000_000_u64); + TaoReserve::set_mock_reserve(netuid, initial_tao_reserve); + AlphaReserve::set_mock_reserve(netuid, initial_alpha_reserve); + assert_ok!(Pallet::::maybe_initialize_palswap(netuid, None)); + + // Get current price + let current_price_before = Pallet::::current_price(netuid); + + // Get reserves + let tao_reserve = TaoReserve::reserve(netuid.into()).to_u64(); + let alpha_reserve = AlphaReserve::reserve(netuid.into()).to_u64(); + + // Expected fee amount + let fee_rate = FeeRate::::get(netuid) as f64 / u16::MAX as f64; + let expected_fee = (swap_amount as f64 * fee_rate) as u64; + + // Calculate expected output amount using f64 math + // This is a simple case when w1 = w2 = 0.5, so there's no + // exponentiation needed + let x = alpha_reserve as f64; + let y = tao_reserve as f64; + let expected_output_amount = if price_should_grow { + x * (1.0 - y / (y + (swap_amount - expected_fee) as f64)) + } else { + y * (1.0 - x / (x + (swap_amount - expected_fee) as f64)) + }; + + // Swap + let limit_price_fixed = U64F64::from_num(limit_price); + let swap_result = + Pallet::::do_swap(netuid, order.clone(), limit_price_fixed, false, false) + .unwrap(); + assert_abs_diff_eq!( + swap_result.amount_paid_out.to_u64(), + expected_output_amount as u64, + epsilon = 1 + ); + + assert_abs_diff_eq!( + swap_result.paid_in_reserve_delta() as u64, + (swap_amount - expected_fee), + epsilon = 1 + ); + assert_abs_diff_eq!( + swap_result.paid_out_reserve_delta() as i64, + -(expected_output_amount as i64), + epsilon = 1 + ); + + // Update reserves (because it happens outside of do_swap in stake_utils) + if price_should_grow { + TaoReserve::set_mock_reserve( + netuid, + TaoBalance::from( + (u64::from(initial_tao_reserve) as i128 + + swap_result.paid_in_reserve_delta()) as u64, + ), + ); + AlphaReserve::set_mock_reserve( + netuid, + AlphaBalance::from( + (u64::from(initial_alpha_reserve) as i128 + + swap_result.paid_out_reserve_delta()) as u64, + ), + ); + } else { + TaoReserve::set_mock_reserve( + netuid, + TaoBalance::from( + (u64::from(initial_tao_reserve) as i128 + + swap_result.paid_out_reserve_delta()) as u64, + ), + ); + AlphaReserve::set_mock_reserve( + netuid, + AlphaBalance::from( + (u64::from(initial_alpha_reserve) as i128 + + swap_result.paid_in_reserve_delta()) as u64, + ), + ); + } + + // Assert that price movement is in correct direction + let current_price_after = Pallet::::current_price(netuid); + assert_eq!( + current_price_after >= current_price_before, + price_should_grow + ); + } + + // Current price is 0.25 + // Test case is (order_type, liquidity, limit_price, output_amount) + perform_test(1.into(), GetAlphaForTao::with_amount(1_000), 1000.0, true); + perform_test(1.into(), GetAlphaForTao::with_amount(2_000), 1000.0, true); + perform_test(1.into(), GetAlphaForTao::with_amount(123_456), 1000.0, true); + perform_test(2.into(), GetTaoForAlpha::with_amount(1_000), 0.0001, false); + perform_test(2.into(), GetTaoForAlpha::with_amount(2_000), 0.0001, false); + perform_test( + 2.into(), + GetTaoForAlpha::with_amount(123_456), + 0.0001, + false, + ); + perform_test( + 3.into(), + GetAlphaForTao::with_amount(1_000_000_000), + 1000.0, + true, + ); + perform_test( + 3.into(), + GetAlphaForTao::with_amount(10_000_000_000_u64), + 1000.0, + true, + ); + }); +} + +// cargo test --package pallet-subtensor-swap --lib -- pallet::impls::tests::test_swap_precision_edge_case --exact --show-output +#[test] +fn test_swap_precision_edge_case() { + // Test case: tao_reserve, alpha_reserve, swap_amount + [ + (1_000_u64, 1_000_u64, 999_500_u64), + (1_000_000_u64, 1_000_000_u64, 999_500_000_u64), + ] + .into_iter() + .for_each(|(tao_reserve, alpha_reserve, swap_amount)| { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let order = GetTaoForAlpha::with_amount(swap_amount); + + // Very low reserves + TaoReserve::set_mock_reserve(netuid, TaoBalance::from(tao_reserve)); + AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(alpha_reserve)); + + // Minimum possible limit price + let limit_price: U64F64 = get_min_price(); + println!("limit_price = {:?}", limit_price); + + // Swap + let swap_result = + Pallet::::do_swap(netuid, order, limit_price, false, true).unwrap(); + + assert!(swap_result.amount_paid_out > TaoBalance::ZERO); + }); + }); +} + +#[test] +fn test_convert_deltas() { + new_test_ext().execute_with(|| { + for (tao, alpha, w_quote, delta_in) in [ + (1500, 1000, 0.5, 1), + (1500, 1000, 0.5, 10000), + (1500, 1000, 0.5, 1000000), + (1500, 1000, 0.5, u64::MAX), + (1, 1000000, 0.5, 1), + (1, 1000000, 0.5, 10000), + (1, 1000000, 0.5, 1000000), + (1, 1000000, 0.5, u64::MAX), + (1000000, 1, 0.5, 1), + (1000000, 1, 0.5, 10000), + (1000000, 1, 0.5, 1000000), + (1000000, 1, 0.5, u64::MAX), + (1500, 1000, 0.50000001, 1), + (1500, 1000, 0.50000001, 10000), + (1500, 1000, 0.50000001, 1000000), + (1500, 1000, 0.50000001, u64::MAX), + (1, 1000000, 0.50000001, 1), + (1, 1000000, 0.50000001, 10000), + (1, 1000000, 0.50000001, 1000000), + (1, 1000000, 0.50000001, u64::MAX), + (1000000, 1, 0.50000001, 1), + (1000000, 1, 0.50000001, 10000), + (1000000, 1, 0.50000001, 1000000), + (1000000, 1, 0.50000001, u64::MAX), + (1500, 1000, 0.49999999, 1), + (1500, 1000, 0.49999999, 10000), + (1500, 1000, 0.49999999, 1000000), + (1500, 1000, 0.49999999, u64::MAX), + (1, 1000000, 0.49999999, 1), + (1, 1000000, 0.49999999, 10000), + (1, 1000000, 0.49999999, 1000000), + (1, 1000000, 0.49999999, u64::MAX), + (1000000, 1, 0.49999999, 1), + (1000000, 1, 0.49999999, 10000), + (1000000, 1, 0.49999999, 1000000), + (1000000, 1, 0.49999999, u64::MAX), + // Low quote weight + (1500, 1000, 0.1, 1), + (1500, 1000, 0.1, 10000), + (1500, 1000, 0.1, 1000000), + (1500, 1000, 0.1, u64::MAX), + (1, 1000000, 0.1, 1), + (1, 1000000, 0.1, 10000), + (1, 1000000, 0.1, 1000000), + (1, 1000000, 0.1, u64::MAX), + (1000000, 1, 0.1, 1), + (1000000, 1, 0.1, 10000), + (1000000, 1, 0.1, 1000000), + (1000000, 1, 0.1, u64::MAX), + // High quote weight + (1500, 1000, 0.9, 1), + (1500, 1000, 0.9, 10000), + (1500, 1000, 0.9, 1000000), + (1500, 1000, 0.9, u64::MAX), + (1, 1000000, 0.9, 1), + (1, 1000000, 0.9, 10000), + (1, 1000000, 0.9, 1000000), + (1, 1000000, 0.9, u64::MAX), + (1000000, 1, 0.9, 1), + (1000000, 1, 0.9, 10000), + (1000000, 1, 0.9, 1000000), + (1000000, 1, 0.9, u64::MAX), + ] { + // Initialize reserves and weights + let netuid = NetUid::from(1); + TaoReserve::set_mock_reserve(netuid, TaoBalance::from(tao)); + AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(alpha)); + assert_ok!(Pallet::::maybe_initialize_palswap(netuid, None)); + + let w_accuracy = 1_000_000_000_f64; + let w_quote_pt = + Perquintill::from_rational((w_quote * w_accuracy) as u128, w_accuracy as u128); + let bal = Balancer::new(w_quote_pt).unwrap(); + SwapBalancer::::insert(netuid, bal); + + // Calculate expected swap results (buy and sell) using f64 math + let y = tao as f64; + let x = alpha as f64; + let d = delta_in as f64; + let w1_div_w2 = (1. - w_quote) / w_quote; + let w2_div_w1 = w_quote / (1. - w_quote); + let expected_sell = y * (1. - (x / (x + d)).powf(w1_div_w2)); + let expected_buy = x * (1. - (y / (y + d)).powf(w2_div_w1)); + + assert_abs_diff_eq!( + u64::from( + BasicSwapStep::::convert_deltas( + netuid, + delta_in.into() + ) + ), + expected_sell as u64, + epsilon = 2u64 + ); + assert_abs_diff_eq!( + u64::from( + BasicSwapStep::::convert_deltas( + netuid, + delta_in.into() + ) + ), + expected_buy as u64, + epsilon = 2u64 + ); + } + }); +} + +#[test] +fn test_rollback_works() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + assert_eq!( + Pallet::::do_swap( + netuid, + GetAlphaForTao::with_amount(1_000_000), + u64::MAX.into(), + false, + true + ) + .unwrap(), + Pallet::::do_swap( + netuid, + GetAlphaForTao::with_amount(1_000_000), + u64::MAX.into(), + false, + false + ) + .unwrap() + ); + }) +} diff --git a/pallets/swap/src/pallet/tests/swap_initialization.rs b/pallets/swap/src/pallet/tests/swap_initialization.rs new file mode 100644 index 0000000000..80357c3725 --- /dev/null +++ b/pallets/swap/src/pallet/tests/swap_initialization.rs @@ -0,0 +1,64 @@ +//! Tests for balancer pool initialization (`maybe_initialize_palswap`). + +use super::*; + +#[test] +fn test_swap_initialization() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + // Setup reserves + let tao = TaoBalance::from(1_000_000_000u64); + let alpha = AlphaBalance::from(4_000_000_000u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + + assert_ok!(Pallet::::maybe_initialize_palswap(netuid, None)); + assert!(PalSwapInitialized::::get(netuid)); + + // Verify current price is set + let price = Pallet::::current_price(netuid); + let expected_price = U64F64::from_num(0.25_f64); + assert_abs_diff_eq!( + price.to_num::(), + expected_price.to_num::(), + epsilon = 0.000000001 + ); + + // Verify that swap reserve weight is initialized + let reserve_weight = SwapBalancer::::get(netuid); + assert_eq!( + reserve_weight.get_quote_weight(), + Perquintill::from_rational(1_u64, 2_u64), + ); + }); +} + +#[test] +fn test_swap_initialization_with_price() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + // Setup reserves, tao / alpha = 0.25 + let tao = TaoBalance::from(1_000_000_000u64); + let alpha = AlphaBalance::from(4_000_000_000u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + + // Initialize with 0.2 price + assert_ok!(Pallet::::maybe_initialize_palswap( + netuid, + Some(U64F64::from(1u16) / U64F64::from(5u16)) + )); + assert!(PalSwapInitialized::::get(netuid)); + + // Verify current price is set to 0.2 + let price = Pallet::::current_price(netuid); + let expected_price = U64F64::from_num(0.2_f64); + assert_abs_diff_eq!( + price.to_num::(), + expected_price.to_num::(), + epsilon = 0.000000001 + ); + }); +} diff --git a/pallets/swap/src/pallet/tests/swap_input_limits.rs b/pallets/swap/src/pallet/tests/swap_input_limits.rs new file mode 100644 index 0000000000..2acd656e3c --- /dev/null +++ b/pallets/swap/src/pallet/tests/swap_input_limits.rs @@ -0,0 +1,92 @@ +//! Tests for the 1000× input-reserve swap size cap. + +use super::*; + +#[test] +fn test_swap_rejects_input_over_1000x_input_reserve() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + TaoReserve::set_mock_reserve(netuid, TaoBalance::from(1_000)); + AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(1_000)); + + assert_noop!( + Pallet::::do_swap( + netuid, + GetTaoForAlpha::with_amount(1_000_001), + get_min_price(), + true, + false, + ), + Error::::SwapInputTooLarge + ); + assert_noop!( + Pallet::::do_swap( + netuid, + GetAlphaForTao::with_amount(1_000_001), + get_max_price(), + true, + false, + ), + Error::::SwapInputTooLarge + ); + }); +} + +#[test] +fn test_sim_swap_rejects_input_over_1000x_input_reserve() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + TaoReserve::set_mock_reserve(netuid, TaoBalance::from(1_000)); + AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(1_000)); + + assert_noop!( + Pallet::::sim_swap(netuid, GetTaoForAlpha::with_amount(1_001_000)), + Error::::SwapInputTooLarge + ); + assert_noop!( + Pallet::::sim_swap(netuid, GetAlphaForTao::with_amount(1_001_000)), + Error::::SwapInputTooLarge + ); + }); +} + +#[test] +fn test_swap_allows_input_at_1000x_input_reserve() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + TaoReserve::set_mock_reserve(netuid, TaoBalance::from(1_000)); + AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(1_000)); + + assert_ok!(Pallet::::do_swap( + netuid, + GetTaoForAlpha::with_amount(1_000_000), + get_min_price(), + true, + true, + )); + assert_ok!(Pallet::::do_swap( + netuid, + GetAlphaForTao::with_amount(1_000_000), + get_max_price(), + true, + true, + )); + }); +} + +#[allow(dead_code)] +fn bbox(t: U64F64, a: U64F64, b: U64F64) -> U64F64 { + if t < a { + a + } else if t > b { + b + } else { + t + } +} + +#[allow(dead_code)] +fn print_current_price(netuid: NetUid) { + let current_price = Pallet::::current_price(netuid); + log::trace!("Current price: {current_price:.6}"); +} diff --git a/pallets/transaction-fee/src/lib.rs b/pallets/transaction-fee/src/lib.rs index 885476cbf7..49a81b6d78 100644 --- a/pallets/transaction-fee/src/lib.rs +++ b/pallets/transaction-fee/src/lib.rs @@ -1,5 +1,16 @@ #![cfg_attr(not(feature = "std"), no_std)] +//! # Transaction fee pallet +//! +//! Custom charge handlers for Substrate extrinsics and EVM transactions. +//! +//! Prefer TAO free-balance fees; for selected stake-outflow Subtensor calls, fall back to +//! paying the fee in **alpha** (unstaked via the subnet AMM and credited to the block author). +//! Multi-subnet alpha fee deduction is deliberately rejected — callers need TAO for those ops. +//! +//! Wiring: runtime `OnChargeTransaction` / `OnChargeEVMTransaction` point at +//! [`SubtensorTxFeeHandler`] / [`SubtensorEvmFeeHandler`]; fee sinks use [`TransactionFeeHandler`]. + // FRAME use frame_support::{ pallet_prelude::*, @@ -46,6 +57,7 @@ mod tests; type AccountIdOf = ::AccountId; type CallOf = ::RuntimeCall; +/// Maps dispatch weight to a TAO fee via a degree-1 polynomial (frac coeff `500_000` / 10^9). pub struct LinearWeightToFee; impl WeightToFeePolynomial for LinearWeightToFee { type Balance = TaoBalance; @@ -62,26 +74,33 @@ impl WeightToFeePolynomial for LinearWeightToFee { } } -/// Trait that allows working with Alpha +/// Alpha-backed fee payment: quote, withdraw, and enumerate staked `(hotkey, netuid)` pairs. +/// +/// Implementors must only spend free (non-collateral) alpha. Multi-entry `alpha_vec` is +/// invalid for withdrawal — return false / no-op so the extrinsic is rejected for Payment. pub trait AlphaFeeHandler { + /// Returns true when free alpha on the single `(hotkey, netuid)` can cover `tao_amount` + /// at the current AMM quote. True at validation does not guarantee execution-time price. fn can_withdraw_in_alpha( coldkey: &AccountIdOf, alpha_vec: &[(AccountIdOf, NetUid)], tao_amount: TaoBalance, ) -> bool; + /// Unstakes enough free alpha to cover `tao_amount`, pays the block author in TAO. + /// Returns `(alpha_taken, tao_received, netuid)` or `InvalidTransaction::Payment`. fn withdraw_in_alpha( coldkey: &AccountIdOf, alpha_vec: &[(AccountIdOf, NetUid)], tao_amount: TaoBalance, ) -> Result<(AlphaBalance, TaoBalance, NetUid), TransactionValidityError>; + /// Subtoken-enabled netuids where `hotkey` holds nonzero stake for `coldkey`. fn get_all_netuids_for_coldkey_and_hotkey( coldkey: &AccountIdOf, hotkey: &AccountIdOf, ) -> Vec; } -/// Deduct the transaction fee from the Subtensor Pallet TotalIssuance when charging the transaction -/// fee. +/// Fee sink: credits TAO fee imbalances to the current block author (drops them if none). pub struct TransactionFeeHandler(core::marker::PhantomData); impl Default for TransactionFeeHandler { fn default() -> Self { @@ -232,28 +251,31 @@ where } } -/// Enum that describes either a withdrawn amount of transaction fee in TAO or -/// the exact charged Alpha amount. +/// Liquidity withdrawn while charging: either a TAO credit or alpha sold for TAO. pub enum WithdrawnFee>> { - // Contains withdrawn TAO amount + /// Free-balance TAO credit taken via `F::withdraw` (may be partially refunded later). Tao(Credit, F>), - // Contains withdrawn Alpha amount and resulting swapped TAO + /// Alpha unstaked for fees: `(alpha_taken, tao_paid_to_author, netuid)`. Not refunded. Alpha((AlphaBalance, TaoBalance, NetUid)), } -/// Custom OnChargeTransaction implementation based on standard FungibleAdapter from transaction_payment -/// FRAME pallet +/// Substrate extrinsic fee charger: TAO first, then alpha for [`Self::fees_in_alpha`] calls. /// +/// `OU` receives TAO fee imbalances and implements [`AlphaFeeHandler`] for the alpha path. pub struct SubtensorTxFeeHandler(PhantomData<(F, OU)>); +/// EVM transaction fee charger in TAO only (no alpha fallback). +/// +/// Tips go to the EVM block author via `pay_priority_fee`; base fee imbalances go to `OU`. pub struct SubtensorEvmFeeHandler(PhantomData<(F, OU)>); /// This implementation contains the list of calls that require paying transaction /// fees in Alpha impl SubtensorTxFeeHandler { - /// Returns Vec<(hotkey, netuid)> if the given call should pay fees in Alpha instead of TAO. - /// The vector represents all subnets where this hotkey has any alpha stake. Fees will be - /// distributed evenly between subnets in case of multiple subnets. + /// Returns `(hotkey, netuid)` pairs eligible for alpha fee payment for this call. + /// + /// Empty means TAO-only. Multiple pairs mark the call as multi-subnet (alpha withdraw + /// is then rejected). Single-netuid stake-outflow calls populate one entry. pub fn fees_in_alpha(who: &AccountIdOf, call: &CallOf) -> Vec<(AccountIdOf, NetUid)> where T: frame_system::Config + pallet_subtensor::Config + AuthorshipInfo>, @@ -370,6 +392,7 @@ where type LiquidityInfo = Option>; type Balance = ::AccountId>>::Balance; + /// Withdraws `fee` in TAO, or sells free alpha when TAO withdraw fails and the call is alpha-eligible. fn withdraw_fee( who: &AccountIdOf, call: &CallOf, @@ -403,6 +426,7 @@ where } } + /// Prefers TAO `can_withdraw`; otherwise requires a single-subnet alpha quote that fits free stake. fn can_withdraw_fee( who: &AccountIdOf, call: &CallOf, @@ -431,6 +455,7 @@ where } } + /// Refunds unused TAO fees to `who` and routes tip/fee to `OU`; emits alpha-paid event (no refund). fn correct_and_deposit_fee( who: &AccountIdOf, _dispatch_info: &DispatchInfoOf>, @@ -502,6 +527,7 @@ where { type LiquidityInfo = Option>; + /// Withdraws the EVM fee from the mapped Substrate account in TAO (Preserve / Polite). fn withdraw_fee( who: &H160, fee: EvmBalance, @@ -526,6 +552,7 @@ where Ok(Some(imbalance)) } + /// Refunds overpaid gas, pays base fee via `OU`, and returns remaining tip credit (if any). fn correct_and_deposit_fee( who: &H160, corrected_fee: EvmBalance, @@ -557,6 +584,7 @@ where None } + /// Resolves the tip credit to the EVM-mapped block author account. fn pay_priority_fee(tip: Self::LiquidityInfo) { if let Some(tip) = tip { let author = >::into_account_id( diff --git a/pallets/transaction-fee/src/tests/alpha_fee_collateral.rs b/pallets/transaction-fee/src/tests/alpha_fee_collateral.rs new file mode 100644 index 0000000000..877682d539 --- /dev/null +++ b/pallets/transaction-fee/src/tests/alpha_fee_collateral.rs @@ -0,0 +1,196 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +use super::helpers::{drain_coldkey_to_existential, lock_test_miner_collateral}; +use super::mock::*; +use crate::{AlphaFeeHandler, TransactionFeeHandler}; +use frame_support::dispatch::GetDispatchInfo; +use frame_support::pallet_prelude::Zero; +use sp_runtime::{ + traits::DispatchTransaction, + transaction_validity::{InvalidTransaction, TransactionValidityError}, +}; +use subtensor_runtime_common::AlphaBalance; +use subtensor_swap_interface::SwapHandler; + +// Fully collateral-bonded stake must not pay alpha fees. Regression for the +// phantom-bond bug where fee unstake stripped stake while MinerCollateral.locked +// stayed unchanged. +// +// cargo test --package subtensor-transaction-fee --lib -- tests::alpha_fee_collateral::test_alpha_fee_rejects_fully_collateralized_stake --exact --show-output +#[test] +fn test_alpha_fee_rejects_fully_collateralized_stake() { + new_test_ext().execute_with(|| { + let stake_amount = TAO; + let sn = setup_fee_test_subnets(1, 1); + let netuid = sn.subnets[0].netuid; + let hotkey = sn.hotkeys[0]; + + fund_and_add_stake(netuid, &sn.coldkey, &hotkey, stake_amount); + let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &sn.coldkey, + netuid, + ); + assert!(!alpha.is_zero()); + lock_test_miner_collateral(netuid, &hotkey, &sn.coldkey, alpha); + drain_coldkey_to_existential(&sn.coldkey); + + let alpha_vec = vec![(hotkey, netuid)]; + assert_eq!( + SubtensorModule::available_to_unstake_from_hotkey(&sn.coldkey, &hotkey, netuid), + AlphaBalance::ZERO + ); + assert!( + ! as AlphaFeeHandler>::can_withdraw_in_alpha( + &sn.coldkey, + &alpha_vec, + 1.into(), + ) + ); + + let subnet_tao_before = SubnetTAO::::get(netuid); + let subnet_alpha_in_before = SubnetAlphaIn::::get(netuid); + let subnet_alpha_out_before = SubnetAlphaOut::::get(netuid); + let collateral_before = + MinerCollateral::::get((netuid, hotkey, sn.coldkey)).expect("collateral entry"); + let aggregate_before = ColdkeyMinerCollateral::::get(netuid, sn.coldkey); + + assert_eq!( + as AlphaFeeHandler>::withdraw_in_alpha( + &sn.coldkey, + &alpha_vec, + 1.into(), + ), + Err(TransactionValidityError::Invalid( + InvalidTransaction::Payment + )) + ); + + // Also reject through the full charge-extension path. + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { + hotkey, + netuid, + amount_unstaked: AlphaBalance::from(1u64), + }); + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + let result = + ext.dispatch_transaction(RuntimeOrigin::signed(sn.coldkey).into(), call, &info, 0, 0); + assert_eq!( + result.unwrap_err(), + TransactionValidityError::Invalid(InvalidTransaction::Payment) + ); + + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &sn.coldkey, + netuid, + ), + alpha + ); + assert_eq!(SubnetTAO::::get(netuid), subnet_tao_before); + assert_eq!(SubnetAlphaIn::::get(netuid), subnet_alpha_in_before); + assert_eq!(SubnetAlphaOut::::get(netuid), subnet_alpha_out_before); + let collateral_after = + MinerCollateral::::get((netuid, hotkey, sn.coldkey)).expect("collateral entry"); + assert_eq!(collateral_after.locked, collateral_before.locked); + assert_eq!( + ColdkeyMinerCollateral::::get(netuid, sn.coldkey), + aggregate_before + ); + }); +} + +// Only the free (non-collateral) slice of a position may fund alpha fees. +// +// cargo test --package subtensor-transaction-fee --lib -- tests::alpha_fee_collateral::test_alpha_fee_only_from_free_stake_above_collateral --exact --show-output +#[test] +fn test_alpha_fee_only_from_free_stake_above_collateral() { + new_test_ext().execute_with(|| { + let stake_amount = TAO * 10; + let sn = setup_fee_test_subnets(1, 1); + let netuid = sn.subnets[0].netuid; + let hotkey = sn.hotkeys[0]; + + fund_and_add_stake(netuid, &sn.coldkey, &hotkey, stake_amount); + let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &sn.coldkey, + netuid, + ); + let locked = alpha / 2.into(); + let free = alpha.saturating_sub(locked); + assert!(!free.is_zero()); + lock_test_miner_collateral(netuid, &hotkey, &sn.coldkey, locked); + drain_coldkey_to_existential(&sn.coldkey); + + assert_eq!( + SubtensorModule::available_to_unstake_from_hotkey(&sn.coldkey, &hotkey, netuid), + free + ); + + let alpha_vec = vec![(hotkey, netuid)]; + + // A fee larger than the free slice must be rejected up front. + let large_tao_fee = TaoBalance::from(TAO.saturating_mul(20)); + let alpha_for_large = + pallet_subtensor_swap::Pallet::::get_alpha_amount_for_tao(netuid, large_tao_fee); + assert!( + alpha_for_large > free, + "test needs a TAO fee quote larger than free stake (got {alpha_for_large:?} vs free {free:?})" + ); + assert!( + ! as AlphaFeeHandler>::can_withdraw_in_alpha( + &sn.coldkey, + &alpha_vec, + large_tao_fee, + ) + ); + + // A small fee that fits in the free slice succeeds and never touches + // the locked collateral accounting. + let small_tao_fee = TaoBalance::from(1_000_000u64); // 0.001 TAO + let alpha_for_small = + pallet_subtensor_swap::Pallet::::get_alpha_amount_for_tao(netuid, small_tao_fee); + assert!(!alpha_for_small.is_zero()); + assert!(alpha_for_small <= free); + assert!( + as AlphaFeeHandler>::can_withdraw_in_alpha( + &sn.coldkey, + &alpha_vec, + small_tao_fee, + ) + ); + + let collateral_before = MinerCollateral::::get((netuid, hotkey, sn.coldkey)) + .expect("collateral entry") + .locked; + let (taken, _tao_out, fee_netuid) = + as AlphaFeeHandler>::withdraw_in_alpha( + &sn.coldkey, + &alpha_vec, + small_tao_fee, + ) + .expect("free-slice fee should withdraw"); + assert_eq!(fee_netuid, netuid); + assert_eq!(taken, alpha_for_small); + + let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &sn.coldkey, + netuid, + ); + assert_eq!(alpha_after, alpha.saturating_sub(taken)); + assert!(alpha_after >= locked); + assert_eq!( + MinerCollateral::::get((netuid, hotkey, sn.coldkey)) + .expect("collateral entry") + .locked, + collateral_before + ); + assert_eq!( + ColdkeyMinerCollateral::::get(netuid, sn.coldkey), + locked + ); + }); +} diff --git a/pallets/transaction-fee/src/tests/block_builder_fees.rs b/pallets/transaction-fee/src/tests/block_builder_fees.rs new file mode 100644 index 0000000000..7fccf4ae05 --- /dev/null +++ b/pallets/transaction-fee/src/tests/block_builder_fees.rs @@ -0,0 +1,59 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +use super::mock::*; +use frame_support::assert_ok; +use frame_support::dispatch::GetDispatchInfo; +use frame_support::pallet_prelude::Zero; +use sp_runtime::traits::DispatchTransaction; + +// cargo test --package subtensor-transaction-fee --lib -- tests::block_builder_fees::test_add_stake_fees_go_to_block_builder --exact --show-output +#[test] +fn test_add_stake_fees_go_to_block_builder() { + new_test_ext().execute_with(|| { + // Portion of swap fees that should go to the block builder + let block_builder_fee_portion = 1.; + + // Get the block builder balance + let block_builder = U256::from(MOCK_BLOCK_BUILDER); + let block_builder_balance_before = Balances::free_balance(block_builder); + + let stake_amount = TAO; + let sn = setup_fee_test_subnets(1, 1); + + // Simulate add stake to get the expected TAO fee + let (_, swap_fee) = swap_tao_to_alpha(sn.subnets[0].netuid, stake_amount.into()); + + add_balance_to_coldkey_account(&sn.coldkey, (stake_amount * 10).into()); + + // Stake + let balance_before = Balances::free_balance(sn.coldkey); + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::add_stake { + hotkey: sn.hotkeys[0], + netuid: sn.subnets[0].netuid, + amount_staked: stake_amount.into(), + }); + + // Dispatch the extrinsic with ChargeTransactionPayment extension + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + assert_ok!(ext.dispatch_transaction( + RuntimeOrigin::signed(sn.coldkey).into(), + call, + &info, + 0, + 0, + )); + + let final_balance = Balances::free_balance(sn.coldkey); + let actual_tao_fee = balance_before - stake_amount.into() - final_balance; + assert!(!actual_tao_fee.is_zero()); + + // Expect that block builder balance has increased by both the swap fee and the transaction fee + let expected_block_builder_swap_reward = swap_fee as f64 * block_builder_fee_portion; + let expected_tx_fee = 14000.; // Use very low value (0.000014) for less test flakiness, value before we 10x tx fees + let block_builder_balance_after = Balances::free_balance(block_builder); + let actual_reward = block_builder_balance_after - block_builder_balance_before; + assert!( + u64::from(actual_reward) as f64 >= expected_block_builder_swap_reward + expected_tx_fee + ); + }); +} diff --git a/pallets/transaction-fee/src/tests/burn_recycle_alpha_fees.rs b/pallets/transaction-fee/src/tests/burn_recycle_alpha_fees.rs new file mode 100644 index 0000000000..fbccb53585 --- /dev/null +++ b/pallets/transaction-fee/src/tests/burn_recycle_alpha_fees.rs @@ -0,0 +1,128 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +use super::mock::*; +use frame_support::assert_ok; +use frame_support::dispatch::GetDispatchInfo; +use sp_runtime::traits::DispatchTransaction; +use subtensor_runtime_common::AlphaBalance; + +// cargo test --package subtensor-transaction-fee --lib -- tests::burn_recycle_alpha_fees::test_burn_alpha_fees_alpha --exact --show-output +#[test] +fn test_burn_alpha_fees_alpha() { + new_test_ext().execute_with(|| { + let stake_amount = TAO; + let alpha_amount = AlphaBalance::from(TAO / 50); + let sn = setup_fee_test_subnets(1, 1); + fund_and_add_stake( + sn.subnets[0].netuid, + &sn.coldkey, + &sn.hotkeys[0], + stake_amount, + ); + + // Forse-set signer balance to ED + let current_balance = Balances::free_balance(sn.coldkey); + remove_balance_from_coldkey_account( + &sn.coldkey, + current_balance - ExistentialDeposit::get(), + ); + + // Burn alpha + let balance_before = Balances::free_balance(sn.coldkey); + let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::burn_alpha { + hotkey: sn.hotkeys[0], + amount: alpha_amount, + netuid: sn.subnets[0].netuid, + }); + + // Dispatch the extrinsic with ChargeTransactionPayment extension + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + assert_ok!(ext.dispatch_transaction( + RuntimeOrigin::signed(sn.coldkey).into(), + call, + &info, + 0, + 0, + )); + + let final_balance = Balances::free_balance(sn.coldkey); + let alpha_after_0 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + + let actual_tao_fee = balance_before - final_balance; + let actual_alpha_fee = alpha_before - alpha_after_0 - alpha_amount; + + // Extrinsic should pay fees in Alpha + assert_eq!(actual_tao_fee, 0.into()); + assert!(actual_alpha_fee > 0.into()); + }); +} + +// cargo test --package subtensor-transaction-fee --lib -- tests::burn_recycle_alpha_fees::test_recycle_alpha_fees_alpha --exact --show-output +#[test] +fn test_recycle_alpha_fees_alpha() { + new_test_ext().execute_with(|| { + let stake_amount = TAO; + let alpha_amount = AlphaBalance::from(TAO / 50); + let sn = setup_fee_test_subnets(1, 1); + fund_and_add_stake( + sn.subnets[0].netuid, + &sn.coldkey, + &sn.hotkeys[0], + stake_amount, + ); + + // Forse-set signer balance to ED + let current_balance = Balances::free_balance(sn.coldkey); + remove_balance_from_coldkey_account( + &sn.coldkey, + current_balance - ExistentialDeposit::get(), + ); + + // Recycle alpha + let balance_before = Balances::free_balance(sn.coldkey); + let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::recycle_alpha { + hotkey: sn.hotkeys[0], + amount: alpha_amount, + netuid: sn.subnets[0].netuid, + }); + + // Dispatch the extrinsic with ChargeTransactionPayment extension + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + assert_ok!(ext.dispatch_transaction( + RuntimeOrigin::signed(sn.coldkey).into(), + call, + &info, + 0, + 0, + )); + + let final_balance = Balances::free_balance(sn.coldkey); + let alpha_after_0 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + + let actual_tao_fee = balance_before - final_balance; + let actual_alpha_fee = alpha_before - alpha_after_0 - alpha_amount; + + // Extrinsic should pay fees in Alpha + assert_eq!(actual_tao_fee, 0.into()); + assert!(actual_alpha_fee > 0.into()); + }); +} diff --git a/pallets/transaction-fee/src/tests/helpers.rs b/pallets/transaction-fee/src/tests/helpers.rs new file mode 100644 index 0000000000..6c34af4683 --- /dev/null +++ b/pallets/transaction-fee/src/tests/helpers.rs @@ -0,0 +1,30 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::AlphaBalance; + +use super::mock::*; + +/// Marks `locked` alpha as miner collateral so fee logic can only spend the free slice. +pub(super) fn lock_test_miner_collateral( + netuid: NetUid, + hotkey: &U256, + coldkey: &U256, + locked: AlphaBalance, +) { + MinerCollateral::::insert( + (netuid, hotkey, coldkey), + MinerCollateralState { + locked, + drain_ratio: U64F64::from_num(1), + min_locked: AlphaBalance::ZERO, + earned: AlphaBalance::ZERO, + }, + ); + ColdkeyMinerCollateral::::insert(netuid, coldkey, locked); +} + +/// Drains free TAO down to the existential deposit so fees must fall back to alpha. +pub(super) fn drain_coldkey_to_existential(coldkey: &U256) { + let current = Balances::free_balance(*coldkey); + remove_balance_from_coldkey_account(coldkey, current.saturating_sub(ExistentialDeposit::get())); +} diff --git a/pallets/transaction-fee/src/tests/mock.rs b/pallets/transaction-fee/src/tests/mock.rs index cf01824b5a..2ad5a3c9f0 100644 --- a/pallets/transaction-fee/src/tests/mock.rs +++ b/pallets/transaction-fee/src/tests/mock.rs @@ -1,3 +1,9 @@ +//! Mock runtime and helpers for transaction-fee pallet tests. +//! +//! Builds a minimal `Test` runtime with Subtensor + Swap + TransactionPayment wired through +//! [`crate::SubtensorTxFeeHandler`]. Prefer [`setup_fee_test_subnets`] / [`fund_and_add_stake`] +//! over ad-hoc registration when exercising alpha fee paths. + #![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] use core::num::NonZeroU64; @@ -138,8 +144,10 @@ impl pallet_transaction_payment::Config for Test { type WeightInfo = pallet_transaction_payment::weights::SubstrateWeight; } +/// Authorship provider used by fee sinks; always returns [`MOCK_BLOCK_BUILDER`]. pub struct MockAuthorshipProvider; +/// Fixed block-author account id for asserting fee credits in tests. pub const MOCK_BLOCK_BUILDER: u64 = 12345u64; impl AuthorshipInfo for MockAuthorshipProvider { @@ -316,7 +324,7 @@ impl pallet_subtensor::Config for Test { type LeaseDividendsDistributionInterval = LeaseDividendsDistributionInterval; type GetCommitments = (); type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; - type CommitmentsInterface = CommitmentsI; + type CommitmentsInterface = CommitmentsPurgeBridge; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; type AuthorshipProvider = MockAuthorshipProvider; type SubtensorPalletId = SubtensorPalletId; @@ -451,8 +459,8 @@ impl PrivilegeCmp for OriginPrivilegeCmp { } } -pub struct CommitmentsI; -impl pallet_subtensor::CommitmentsInterface for CommitmentsI { +pub struct CommitmentsPurgeBridge; +impl pallet_subtensor::CommitmentsInterface for CommitmentsPurgeBridge { fn purge_netuid( _netuid: NetUid, _weight_meter: &mut frame_support::weights::WeightMeter, @@ -545,7 +553,7 @@ where } } -// Build genesis storage according to the mock runtime. +/// Genesis externalities with block number set to 1. pub fn new_test_ext() -> sp_io::TestExternalities { sp_tracing::try_init_simple(); let t = frame_system::GenesisConfig::::default() @@ -665,11 +673,13 @@ pub fn add_dynamic_network(hotkey: &U256, coldkey: &U256) -> NetUid { netuid } +/// Sets subnet AMM reserves (`SubnetTAO` / `SubnetAlphaIn`) used by fee quotes and swaps. pub(crate) fn setup_reserves(netuid: NetUid, tao: TaoBalance, alpha: AlphaBalance) { SubnetTAO::::set(netuid, tao); SubnetAlphaIn::::set(netuid, alpha); } +/// Swaps `alpha` for TAO on `netuid` (optionally dropping pool fees); panics on zero payout. pub(crate) fn swap_alpha_to_tao_ext( netuid: NetUid, alpha: AlphaBalance, @@ -698,10 +708,12 @@ pub(crate) fn swap_alpha_to_tao_ext( (result.amount_paid_out.to_u64(), result.fee_paid.to_u64()) } +/// Swaps `alpha`→TAO with pool fees enabled. pub(crate) fn swap_alpha_to_tao(netuid: NetUid, alpha: AlphaBalance) -> (u64, u64) { swap_alpha_to_tao_ext(netuid, alpha, false) } +/// Swaps `tao` for alpha on `netuid` (optionally dropping pool fees); panics on zero payout. pub(crate) fn swap_tao_to_alpha_ext( netuid: NetUid, tao: TaoBalance, @@ -730,16 +742,19 @@ pub(crate) fn swap_tao_to_alpha_ext( (result.amount_paid_out.to_u64(), result.fee_paid.to_u64()) } +/// Swaps `tao`→alpha with pool fees enabled. pub(crate) fn swap_tao_to_alpha(netuid: NetUid, tao: TaoBalance) -> (u64, u64) { swap_tao_to_alpha_ext(netuid, tao, false) } +/// Initializes `netuid` and allows registration (used for root / ad-hoc subnet setup). #[allow(dead_code)] pub fn add_network(netuid: NetUid, tempo: u16) { SubtensorModule::init_new_network(netuid, tempo); SubtensorModule::set_network_registration_allowed(netuid, true); } +/// One dynamic subnet created for fee tests (owner coldkey/hotkey + netuid). #[allow(dead_code)] pub struct TestSubnet { pub netuid: NetUid, @@ -747,6 +762,7 @@ pub struct TestSubnet { pub hk_owner: U256, } +/// Bundle of dynamic subnets plus a shared coldkey and neuron hotkeys for fee scenarios. #[allow(dead_code)] pub struct TestSetup { pub subnets: Vec, @@ -754,8 +770,9 @@ pub struct TestSetup { pub hotkeys: Vec, } +/// Registers `sncount` dynamic subnets with `neurons` each, seeds AMM reserves, enables subtoken. #[allow(dead_code)] -pub fn setup_subnets(sncount: u16, neurons: u16) -> TestSetup { +pub fn setup_fee_test_subnets(sncount: u16, neurons: u16) -> TestSetup { let mut subnets: Vec = Vec::new(); let owner_ck_start_id = 100; let owner_hk_start_id = 200; @@ -807,8 +824,9 @@ pub fn setup_subnets(sncount: u16, neurons: u16) -> TestSetup { } } +/// Funds `coldkey` (stake + ED) and calls `add_stake` on `(hotkey, netuid)`. #[allow(dead_code)] -pub fn setup_stake( +pub fn fund_and_add_stake( netuid: subtensor_runtime_common::NetUid, coldkey: &U256, hotkey: &U256, @@ -828,6 +846,7 @@ pub fn setup_stake( )); } +/// Dry-runs alpha fee unstake then quotes the remaining `alpha`→TAO swap (rolled back). pub(crate) fn quote_remove_stake_after_alpha_fee( coldkey: &U256, hotkey: &U256, diff --git a/pallets/transaction-fee/src/tests/mod.rs b/pallets/transaction-fee/src/tests/mod.rs index 92032aa73d..79c9b137cf 100644 --- a/pallets/transaction-fee/src/tests/mod.rs +++ b/pallets/transaction-fee/src/tests/mod.rs @@ -1,1975 +1,17 @@ +//! Unit tests for alpha/TAO transaction fee charging. +//! +//! Concept modules mirror fee paths: remove-stake, unstake-all, hotkey swap, move/transfer/swap, +//! burn/recycle, block-author sinks, and miner-collateral guards. + #![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] -use crate::{AlphaFeeHandler, SubtensorTxFeeHandler, TransactionFeeHandler, TransactionSource}; -use approx::assert_abs_diff_eq; -use frame_support::dispatch::GetDispatchInfo; -use frame_support::pallet_prelude::Zero; -use frame_support::traits::Currency; -use frame_support::{assert_err, assert_ok}; -use sp_runtime::{ - traits::{DispatchTransaction, TransactionExtension, TxBaseImplication}, - transaction_validity::{InvalidTransaction, TransactionValidityError}, -}; -use substrate_fixed::types::U64F64; -use subtensor_runtime_common::AlphaBalance; -use subtensor_swap_interface::SwapHandler; -use mock::*; +mod helpers; mod mock; -fn mark_collateral(netuid: NetUid, hotkey: &U256, coldkey: &U256, locked: AlphaBalance) { - MinerCollateral::::insert( - (netuid, hotkey, coldkey), - MinerCollateralState { - locked, - drain_ratio: U64F64::from_num(1), - min_locked: AlphaBalance::ZERO, - earned: AlphaBalance::ZERO, - }, - ); - ColdkeyMinerCollateral::::insert(netuid, coldkey, locked); -} - -fn drain_coldkey_to_ed(coldkey: &U256) { - let current = Balances::free_balance(*coldkey); - remove_balance_from_coldkey_account(coldkey, current.saturating_sub(ExistentialDeposit::get())); -} - -// cargo test --package subtensor-transaction-fee --lib -- tests::test_remove_stake_fees_tao --exact --show-output -#[test] -fn test_remove_stake_fees_tao() { - new_test_ext().execute_with(|| { - use frame_support::traits::Hooks; - use sp_runtime::traits::SaturatedConversion; - - type BN = frame_system::pallet_prelude::BlockNumberFor; - - // Advance blocks and run hooks so staking-op rate limit windows reset. - let jump_blocks = |delta: u64| { - let current_bn: BN = frame_system::Pallet::::block_number(); - - // Finish current block. - >::on_finalize(current_bn); - as Hooks>::on_finalize(current_bn); - - let current_u64: u64 = current_bn.saturated_into(); - // Use a delta that won’t land on tempo boundaries (tempo is set to 10 in setup_subnets). - let next_u64: u64 = current_u64.saturating_add(delta); - let next_bn: BN = next_u64.saturated_into(); - - frame_system::Pallet::::set_block_number(next_bn); - - // Start next block. - as Hooks>::on_initialize(next_bn); - >::on_initialize(next_bn); - }; - - let stake_amount = TaoBalance::from(TAO); - let unstake_amount = AlphaBalance::from(TAO / 50); - - // setup_subnets() -> register_ok_neuron() calls SubtensorModule::register(...) - // which now requires sufficient balance to stake during registration. - // setup_subnets() uses coldkey=10000 and first neuron hotkey=20001. - let register_prefund = stake_amount - .saturating_mul(10_000.into()) // generous buffer - .saturating_add(ExistentialDeposit::get()); - add_balance_to_coldkey_account(&U256::from(10000), register_prefund); - add_balance_to_coldkey_account(&U256::from(20001), register_prefund); - - let sn = setup_subnets(1, 1); - - // Avoid staking-op rate limit between registration and staking. - jump_blocks(1_000_001); - - setup_stake( - sn.subnets[0].netuid, - &sn.coldkey, - &sn.hotkeys[0], - stake_amount.into(), - ); - add_balance_to_coldkey_account(&sn.coldkey, TaoBalance::from(TAO)); - - // Avoid staking-op rate limit between add_stake and remove_stake. - jump_blocks(1_000_001); - - // Simulate stake removal to get how much TAO should we get for unstaked Alpha - let (expected_unstaked_tao, _swap_fee) = - mock::swap_alpha_to_tao(sn.subnets[0].netuid, unstake_amount); - - // Remove stake - let balance_before = Balances::free_balance(sn.coldkey); - let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { - hotkey: sn.hotkeys[0], - netuid: sn.subnets[0].netuid, - amount_unstaked: unstake_amount, - }); - - // Dispatch the extrinsic with ChargeTransactionPayment extension - let info = call.get_dispatch_info(); - let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); - - // dispatch_transaction() is nested: - // - Outer Result: validation / payment extension checks - // - Inner Result: actual runtime call dispatch result - let inner = ext - .dispatch_transaction(RuntimeOrigin::signed(sn.coldkey).into(), call, &info, 0, 0) - .expect("Expected Ok(_) from dispatch_transaction (validation)"); - assert_ok!(inner); - - let final_balance = Balances::free_balance(sn.coldkey); - let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - - let actual_tao_fee = - balance_before + TaoBalance::from(expected_unstaked_tao) - final_balance; - let actual_alpha_fee = alpha_before - alpha_after - unstake_amount; - - // Remove stake extrinsic should pay fees in TAO because ck has sufficient TAO balance - assert!(actual_tao_fee > 0.into()); - assert_eq!(actual_alpha_fee, AlphaBalance::from(0)); - - let events = System::events(); - assert!(events.iter().any(|event_record| { - matches!( - &event_record.event, - RuntimeEvent::TransactionPayment( - pallet_transaction_payment::Event::TransactionFeePaid { .. } - ) - ) - })); - assert!(!events.iter().any(|event_record| { - matches!( - &event_record.event, - RuntimeEvent::SubtensorModule(SubtensorEvent::TransactionFeePaidWithAlpha { .. }) - ) - })); - }); -} - -// cargo test --package subtensor-transaction-fee --lib -- tests::test_rejects_multi_subnet_alpha_fee_deduction --exact --show-output -#[test] -fn test_rejects_multi_subnet_alpha_fee_deduction() { - new_test_ext().execute_with(|| { - let sn = setup_subnets(2, 1); - let stake_amount = TAO; - setup_stake( - sn.subnets[0].netuid, - &sn.coldkey, - &sn.hotkeys[0], - stake_amount, - ); - setup_stake( - sn.subnets[1].netuid, - &sn.coldkey, - &sn.hotkeys[0], - stake_amount, - ); - - let alpha_before_0 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - let alpha_before_1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[1].netuid, - ); - - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::unstake_all { - hotkey: sn.hotkeys[0], - }); - let alpha_vec = - SubtensorTxFeeHandler::>::fees_in_alpha::( - &sn.coldkey, - &call, - ); - assert_eq!(alpha_vec.len(), 2); - - assert!( - ! as AlphaFeeHandler>::can_withdraw_in_alpha( - &sn.coldkey, - &alpha_vec, - 1.into(), - ) - ); - assert_eq!( - as AlphaFeeHandler>::withdraw_in_alpha( - &sn.coldkey, - &alpha_vec, - 1.into(), - ), - Ok((0.into(), 0.into(), NetUid::ROOT)) - ); - - let alpha_after_0 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - let alpha_after_1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[1].netuid, - ); - - assert_eq!(alpha_before_0, alpha_after_0); - assert_eq!(alpha_before_1, alpha_after_1); - }); -} -// cargo test --package subtensor-transaction-fee --lib -- tests::test_swap_hotkey_fees_alpha --exact --show-output -#[test] -fn test_swap_hotkey_fees_alpha() { - new_test_ext().execute_with(|| { - let sn = setup_subnets(2, 2); - let stake_amount = TAO; - setup_stake( - sn.subnets[0].netuid, - &sn.coldkey, - &sn.hotkeys[0], - stake_amount, - ); - setup_stake( - sn.subnets[1].netuid, - &sn.coldkey, - &sn.hotkeys[0], - stake_amount, - ); - - // swap_hotkey and swap_hotkey_v2 move alpha stake off the origin hotkey, - // so their fees must be eligible to be paid in alpha on every subnet that - // hotkey has stake. Before the fix `fees_in_alpha` returned an empty vec - // for these calls, forcing a TAO fee (and rejecting alpha-only callers). - - // netuid = None -> every subnet the origin hotkey has stake on (2 here). - let call_all = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { - hotkey: sn.hotkeys[0], - new_hotkey: sn.hotkeys[1], - netuid: None, - }); - let alpha_vec_all = - SubtensorTxFeeHandler::>::fees_in_alpha::( - &sn.coldkey, - &call_all, - ); - assert_eq!(alpha_vec_all.len(), 2); - - // netuid = Some(single) -> only that subnet. - let call_one = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { - hotkey: sn.hotkeys[0], - new_hotkey: sn.hotkeys[1], - netuid: Some(sn.subnets[0].netuid), - }); - let alpha_vec_one = - SubtensorTxFeeHandler::>::fees_in_alpha::( - &sn.coldkey, - &call_one, - ); - assert_eq!(alpha_vec_one.len(), 1); - - // swap_hotkey_v2 moves the same alpha and must be eligible too. - let call_v2 = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey_v2 { - hotkey: sn.hotkeys[0], - new_hotkey: sn.hotkeys[1], - netuid: None, - keep_stake: false, - }); - let alpha_vec_v2 = - SubtensorTxFeeHandler::>::fees_in_alpha::( - &sn.coldkey, - &call_v2, - ); - assert_eq!(alpha_vec_v2.len(), 2); - }); -} - -// cargo test --package subtensor-transaction-fee --lib -- tests::test_remove_stake_fees_alpha --exact --show-output -#[test] -fn test_remove_stake_fees_alpha() { - new_test_ext().execute_with(|| { - let stake_amount = TAO; - let unstake_amount = AlphaBalance::from(TAO / 50); - let sn = setup_subnets(1, 1); - setup_stake( - sn.subnets[0].netuid, - &sn.coldkey, - &sn.hotkeys[0], - stake_amount, - ); - - // Simulate stake removal to get how much TAO should we get for unstaked Alpha - // after the alpha-fee pre-withdrawal has already moved the pool. - let (expected_unstaked_tao, swap_fee) = mock::quote_remove_stake_after_alpha_fee( - &sn.coldkey, - &sn.hotkeys[0], - sn.subnets[0].netuid, - unstake_amount, - ); - - // Forse-set signer balance to ED - let current_balance = Balances::free_balance(sn.coldkey); - remove_balance_from_coldkey_account( - &sn.coldkey, - current_balance - ExistentialDeposit::get(), - ); - - // Get the block builder balance - let block_builder = U256::from(MOCK_BLOCK_BUILDER); - let block_builder_balance_before = Balances::free_balance(block_builder); - - // Remove stake - let balance_before = Balances::free_balance(sn.coldkey); - let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { - hotkey: sn.hotkeys[0], - netuid: sn.subnets[0].netuid, - amount_unstaked: unstake_amount, - }); - - System::reset_events(); - - // Dispatch the extrinsic with ChargeTransactionPayment extension - let info = call.get_dispatch_info(); - let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); - assert_ok!(ext.dispatch_transaction( - RuntimeOrigin::signed(sn.coldkey).into(), - call, - &info, - 0, - 0, - )); - - let final_balance = Balances::free_balance(sn.coldkey); - let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - - let actual_tao_fee = - balance_before + TaoBalance::from(expected_unstaked_tao) - final_balance; - let actual_alpha_fee = alpha_before - alpha_after - unstake_amount; - - // Remove stake extrinsic should pay fees in Alpha - assert_abs_diff_eq!(actual_tao_fee, 0.into(), epsilon = 10.into()); - assert!(actual_alpha_fee > 0.into()); - - // Assert that swapped TAO from alpha fee goes to block author - let block_builder_fee_portion = 1.; - let expected_block_builder_swap_reward = swap_fee as f64 * block_builder_fee_portion; - let expected_tx_fee = 14000.; // Use very low value (0.000014) for less test flakiness, value before we 10x tx fees - let block_builder_balance_after = Balances::free_balance(block_builder); - let actual_block_builder_reward = - block_builder_balance_after - block_builder_balance_before; - assert!( - u64::from(actual_block_builder_reward) as f64 - >= expected_block_builder_swap_reward + expected_tx_fee - ); - - let events = System::events(); - let alpha_event = events - .iter() - .position(|event_record| { - matches!( - &event_record.event, - RuntimeEvent::SubtensorModule(SubtensorEvent::TransactionFeePaidWithAlpha { - who, - netuid, - alpha_fee, - tao_amount: _, - }) if who == &sn.coldkey && *alpha_fee == actual_alpha_fee && *netuid == sn.subnets[0].netuid - ) - }) - .expect("expected TransactionFeePaidWithAlpha event"); - let tao_event = events - .iter() - .position(|event_record| { - matches!( - &event_record.event, - RuntimeEvent::TransactionPayment( - pallet_transaction_payment::Event::TransactionFeePaid { who, .. } - ) if who == &sn.coldkey - ) - }) - .expect("expected TransactionFeePaid event"); - - assert!( - alpha_event < tao_event, - "expected TransactionFeePaidWithAlpha before TransactionFeePaid" - ); - }); -} - -#[test] -fn test_alpha_fee_withdraw_failure_aborts_and_rolls_back() { - new_test_ext().execute_with(|| { - let stake_amount = TAO; - let unstake_amount = AlphaBalance::from(TAO / 50); - let sn = setup_subnets(1, 1); - let netuid = sn.subnets[0].netuid; - let hotkey = sn.hotkeys[0]; - - setup_stake(netuid, &sn.coldkey, &hotkey, stake_amount); - - let current_balance = Balances::free_balance(sn.coldkey); - remove_balance_from_coldkey_account( - &sn.coldkey, - current_balance - ExistentialDeposit::get(), - ); - - // Force the alpha-fee unstake to fail after AMM bookkeeping by draining - // the subnet account used by transfer_tao_from_subnet. - let subnet_account = SubtensorModule::get_subnet_account_id(netuid).unwrap(); - Balances::make_free_balance_be(&subnet_account, 0.into()); - - let block_builder = U256::from(MOCK_BLOCK_BUILDER); - let block_builder_balance_before = Balances::free_balance(block_builder); - let signer_balance_before = Balances::free_balance(sn.coldkey); - let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &sn.coldkey, - netuid, - ); - let subnet_alpha_in_before = SubnetAlphaIn::::get(netuid); - let subnet_alpha_out_before = SubnetAlphaOut::::get(netuid); - let subnet_tao_before = SubnetTAO::::get(netuid); - let total_stake_before = TotalStake::::get(); - let subnet_volume_before = SubnetVolume::::get(netuid); - - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { - hotkey, - netuid, - amount_unstaked: unstake_amount, - }); - let info = call.get_dispatch_info(); - let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); - - let result = - ext.dispatch_transaction(RuntimeOrigin::signed(sn.coldkey).into(), call, &info, 0, 0); - - assert_eq!( - result.unwrap_err(), - TransactionValidityError::Invalid(InvalidTransaction::Payment) - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &sn.coldkey, - netuid, - ), - alpha_before - ); - assert_eq!(SubnetAlphaIn::::get(netuid), subnet_alpha_in_before); - assert_eq!(SubnetAlphaOut::::get(netuid), subnet_alpha_out_before); - assert_eq!(SubnetTAO::::get(netuid), subnet_tao_before); - assert_eq!(TotalStake::::get(), total_stake_before); - assert_eq!(SubnetVolume::::get(netuid), subnet_volume_before); - assert_eq!(Balances::free_balance(sn.coldkey), signer_balance_before); - assert_eq!( - Balances::free_balance(block_builder), - block_builder_balance_before - ); - - assert!(!System::events().iter().any(|event_record| { - matches!( - &event_record.event, - RuntimeEvent::SubtensorModule(SubtensorEvent::TransactionFeePaidWithAlpha { .. }) - ) - })); - }); -} - -// Test that unstaking on root with no free balance results in charging fees from -// staked amount -// -// cargo test --package subtensor-transaction-fee --lib -- tests::test_remove_stake_root --exact --show-output -#[test] -fn test_remove_stake_root() { - new_test_ext().execute_with(|| { - let stake_amount = TAO; - let unstake_amount = TAO / 10; - let netuid = NetUid::from(0); - let coldkey = U256::from(100000); - let hotkey = U256::from(100001); - - // Root stake - add_network(netuid, 10); - pallet_subtensor::Owner::::insert(hotkey, coldkey); - pallet_subtensor::SubtokenEnabled::::insert(NetUid::from(0), true); - setup_stake(netuid, &coldkey, &hotkey, stake_amount); - - // Forse-set signer balance to ED - let current_balance = Balances::free_balance(coldkey); - remove_balance_from_coldkey_account(&coldkey, current_balance - ExistentialDeposit::get()); - - // Remove stake - let balance_before = Balances::free_balance(coldkey); - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { - hotkey, - netuid, - amount_unstaked: unstake_amount.into(), - }); - - // Dispatch the extrinsic with ChargeTransactionPayment extension - let info = call.get_dispatch_info(); - let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); - assert_ok!(ext.dispatch_transaction( - RuntimeOrigin::signed(coldkey).into(), - call, - &info, - 0, - 0, - )); - - let final_balance = Balances::free_balance(coldkey); - let alpha_after = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); - - let actual_tao_fee = balance_before + unstake_amount.into() - final_balance; - let actual_alpha_fee = - AlphaBalance::from(stake_amount) - alpha_after - unstake_amount.into(); - - // Remove stake extrinsic should pay fees in Alpha (withdrawn from staked TAO) - assert_eq!(actual_tao_fee, 0.into()); - assert!(actual_alpha_fee > 0.into()); - }); -} - -// Test that unstaking 100% of stake on root is possible with no free balance -// -// cargo test --package subtensor-transaction-fee --lib -- tests::test_remove_stake_completely_root --exact --show-output -#[test] -fn test_remove_stake_completely_root() { - new_test_ext().execute_with(|| { - let stake_amount = TAO; - let unstake_amount = TAO; - let netuid = NetUid::from(0); - let coldkey = U256::from(100000); - let hotkey = U256::from(100001); - - // Root stake - add_network(netuid, 10); - pallet_subtensor::Owner::::insert(hotkey, coldkey); - pallet_subtensor::SubtokenEnabled::::insert(NetUid::from(0), true); - setup_stake(netuid, &coldkey, &hotkey, stake_amount); - - // Forse-set signer balance to ED - let current_balance = Balances::free_balance(coldkey); - remove_balance_from_coldkey_account(&coldkey, current_balance - ExistentialDeposit::get()); - - // Remove stake - let balance_before = Balances::free_balance(coldkey); - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { - hotkey, - netuid, - amount_unstaked: unstake_amount.into(), - }); - - // Dispatch the extrinsic with ChargeTransactionPayment extension - let info = call.get_dispatch_info(); - let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); - assert_ok!(ext.dispatch_transaction( - RuntimeOrigin::signed(coldkey).into(), - call, - &info, - 0, - 0, - )); - - let final_balance = Balances::free_balance(coldkey); - let alpha_after = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); - - assert_eq!(alpha_after, 0.into()); - assert!(final_balance > balance_before); - }); -} - -// cargo test --package subtensor-transaction-fee --lib -- tests::test_remove_stake_completely_fees_alpha --exact --show-output -#[test] -fn test_remove_stake_completely_fees_alpha() { - new_test_ext().execute_with(|| { - let stake_amount = TAO; - let sn = setup_subnets(1, 1); - setup_stake( - sn.subnets[0].netuid, - &sn.coldkey, - &sn.hotkeys[0], - stake_amount, - ); - - // Simulate stake removal to get how much TAO should we get for unstaked Alpha - let unstake_amount = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - let (expected_unstaked_tao, _swap_fee) = - mock::swap_alpha_to_tao(sn.subnets[0].netuid, unstake_amount); - - // Forse-set signer balance to ED - let current_balance = Balances::free_balance(sn.coldkey); - remove_balance_from_coldkey_account( - &sn.coldkey, - current_balance - ExistentialDeposit::get(), - ); - - // Remove stake - let balance_before = Balances::free_balance(sn.coldkey); - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { - hotkey: sn.hotkeys[0], - netuid: sn.subnets[0].netuid, - amount_unstaked: unstake_amount, - }); - - // Dispatch the extrinsic with ChargeTransactionPayment extension - let info = call.get_dispatch_info(); - let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); - assert_ok!(ext.dispatch_transaction( - RuntimeOrigin::signed(sn.coldkey).into(), - call, - &info, - 0, - 0, - )); - - let final_balance = Balances::free_balance(sn.coldkey); - let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - - // Effectively, the fee is paid in TAO in this case because user receives less TAO, - // and all Alpha is gone, and it is not measurable in Alpha - let actual_fee = balance_before + expected_unstaked_tao.into() - final_balance; - assert_eq!(alpha_after, 0.into()); - assert!(actual_fee > 0.into()); - }); -} - -// Validation should fail if both TAO and Alpha balance are lower than tx fees, -// so that transaction is not included in the block -#[test] -fn test_remove_stake_not_enough_balance_for_fees() { - new_test_ext().execute_with(|| { - let stake_amount = TaoBalance::from(TAO); - let sn = setup_subnets(1, 1); - - add_balance_to_coldkey_account( - &sn.coldkey, - stake_amount - .saturating_mul(2.into()) // buffer so staking doesn't attempt to drain the account - .saturating_add(ExistentialDeposit::get()), - ); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(sn.coldkey), - sn.hotkeys[0], - sn.subnets[0].netuid, - stake_amount.into(), - )); - - // Simulate stake removal to get how much TAO should we get for unstaked Alpha - let current_stake = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - - // Forse-set signer balance to ED - let current_balance = Balances::free_balance(sn.coldkey); - remove_balance_from_coldkey_account( - &sn.coldkey, - current_balance - ExistentialDeposit::get(), - ); - - // For-set Alpha balance to low - let new_current_stake = AlphaBalance::from(1_000); - SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - current_stake - new_current_stake, - ); - - // Remove stake - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { - hotkey: sn.hotkeys[0], - netuid: sn.subnets[0].netuid, - amount_unstaked: new_current_stake, - }); - - // Dispatch the extrinsic with ChargeTransactionPayment extension - let info = call.get_dispatch_info(); - let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); - let result = ext.validate( - RuntimeOrigin::signed(sn.coldkey).into(), - &call.clone(), - &info, - 10, - (), - &TxBaseImplication(()), - TransactionSource::External, - ); - - assert_eq!( - result.unwrap_err(), - TransactionValidityError::Invalid(InvalidTransaction::Payment) - ); - }); -} - -// No TAO balance, Alpha fees. If Alpha price is high, it is enough to pay fees, but when Alpha price -// is low, the validation fails -// -// cargo test --package subtensor-transaction-fee --lib -- tests::test_remove_stake_edge_alpha --exact --show-output -#[test] -fn test_remove_stake_edge_alpha() { - new_test_ext().execute_with(|| { - let stake_amount = TAO; - let sn = setup_subnets(1, 1); - setup_stake( - sn.subnets[0].netuid, - &sn.coldkey, - &sn.hotkeys[0], - stake_amount, - ); - - // Simulate stake removal to get how much TAO should we get for unstaked Alpha - let current_stake = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - - // Forse-set signer balance to ED - let current_balance = Balances::free_balance(sn.coldkey); - remove_balance_from_coldkey_account( - &sn.coldkey, - current_balance - ExistentialDeposit::get(), - ); - - // For-set Alpha balance to low, but enough to pay tx fees at the current Alpha price - let new_current_stake = AlphaBalance::from(2_000_000); - SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - current_stake - new_current_stake, - ); - - // Remove stake - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { - hotkey: sn.hotkeys[0], - netuid: sn.subnets[0].netuid, - amount_unstaked: new_current_stake, - }); - - // Dispatch the extrinsic with ChargeTransactionPayment extension - let info = call.get_dispatch_info(); - let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); - let result = ext.validate( - RuntimeOrigin::signed(sn.coldkey).into(), - &call.clone(), - &info, - 10, - (), - &TxBaseImplication(()), - TransactionSource::External, - ); - - // Ok - Validation passed - assert_ok!(result); - - // Lower Alpha price to 0.0001 so that there is not enough alpha to cover tx fees - SubnetTAO::::insert(sn.subnets[0].netuid, TaoBalance::from(1_000_000)); - SubnetAlphaIn::::insert(sn.subnets[0].netuid, AlphaBalance::from(10_000_000_000_u64)); - - let result_low_alpha_price = ext.validate( - RuntimeOrigin::signed(sn.coldkey).into(), - &call.clone(), - &info, - 10, - (), - &TxBaseImplication(()), - TransactionSource::External, - ); - assert_eq!( - result_low_alpha_price.unwrap_err(), - TransactionValidityError::Invalid(InvalidTransaction::Payment) - ); - }); -} - -// Validation passes, but transaction fails => TAO fees are paid -// -// cargo test --package subtensor-transaction-fee --lib -- tests::test_remove_stake_failing_transaction_tao_fees --exact --show-output -#[test] -fn test_remove_stake_failing_transaction_tao_fees() { - new_test_ext().execute_with(|| { - let stake_amount = TaoBalance::from(TAO); - let unstake_amount = AlphaBalance::from(TAO / 50); - let sn = setup_subnets(1, 1); - - add_balance_to_coldkey_account( - &sn.coldkey, - stake_amount - .saturating_mul(2.into()) // buffer so staking doesn't attempt to drain the account - .saturating_add(ExistentialDeposit::get()), - ); - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(sn.coldkey), - sn.hotkeys[0], - sn.subnets[0].netuid, - stake_amount.into(), - )); - - add_balance_to_coldkey_account(&sn.coldkey, TAO.into()); - - // Make unstaking fail by reducing liquidity to critical - SubnetTAO::::insert(sn.subnets[0].netuid, TaoBalance::from(1)); - - // Remove stake - let balance_before = Balances::free_balance(sn.coldkey); - let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { - hotkey: sn.hotkeys[0], - netuid: sn.subnets[0].netuid, - amount_unstaked: unstake_amount, - }); - - // Dispatch the extrinsic with ChargeTransactionPayment extension - let info = call.get_dispatch_info(); - let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); - assert_ok!(ext.dispatch_transaction( - RuntimeOrigin::signed(sn.coldkey).into(), - call, - &info, - 0, - 0, - )); - - let final_balance = Balances::free_balance(sn.coldkey); - let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - - let actual_tao_fee = balance_before - final_balance; - - // Remove stake extrinsic should pay fees in TAO because ck has sufficient TAO balance - assert!(actual_tao_fee > 0.into()); - assert_eq!(alpha_before, alpha_after); - }); -} - -// Validation passes, but transaction fails (artificially disable subtoken) => -// Alpha fees are still paid -// -// cargo test --package subtensor-transaction-fee --lib -- tests::test_remove_stake_failing_transaction_alpha_fees --exact --show-output -#[test] -fn test_remove_stake_failing_transaction_alpha_fees() { - new_test_ext().execute_with(|| { - let stake_amount = TAO; - let unstake_amount = AlphaBalance::from(TAO / 50); - let sn = setup_subnets(1, 1); - setup_stake( - sn.subnets[0].netuid, - &sn.coldkey, - &sn.hotkeys[0], - stake_amount, - ); - - // Provide adequate TAO reserve so that sim swap works ok in validation - SubnetTAO::::insert(sn.subnets[0].netuid, TaoBalance::from(1_000_000_000_u64)); - - // Provide Alpha reserve so that price is about 1.0 - SubnetAlphaIn::::insert(sn.subnets[0].netuid, AlphaBalance::from(1_000_000_000_u64)); - - // Forse-set signer balance to ED - let current_balance = Balances::free_balance(sn.coldkey); - remove_balance_from_coldkey_account( - &sn.coldkey, - current_balance - ExistentialDeposit::get(), - ); - - // Disable subtoken so that removing stake tx fails (still allows the validation to pass) - pallet_subtensor::SubtokenEnabled::::insert(sn.subnets[0].netuid, false); - - // Remove stake - let balance_before = Balances::free_balance(sn.coldkey); - let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { - hotkey: sn.hotkeys[0], - netuid: sn.subnets[0].netuid, - amount_unstaked: alpha_before, - }); - - // Dispatch the extrinsic with ChargeTransactionPayment extension - let info = call.get_dispatch_info(); - let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); - assert_ok!(ext.dispatch_transaction( - RuntimeOrigin::signed(sn.coldkey).into(), - call, - &info, - 0, - 0, - )); - - let final_balance = Balances::free_balance(sn.coldkey); - let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - - let actual_tao_fee = balance_before - final_balance; - let actual_alpha_fee = alpha_before - alpha_after; - - // Remove stake extrinsic should pay fees in Alpha - assert_eq!(actual_tao_fee, 0.into()); - assert!(actual_alpha_fee > 0.into()); - assert!(actual_alpha_fee < unstake_amount); - }); -} - -// cargo test --package subtensor-transaction-fee --lib -- tests::test_remove_stake_limit_fees_alpha --exact --show-output -#[test] -fn test_remove_stake_limit_fees_alpha() { - new_test_ext().execute_with(|| { - let stake_amount = TAO; - let unstake_amount = AlphaBalance::from(TAO / 50); - let sn = setup_subnets(1, 1); - setup_stake( - sn.subnets[0].netuid, - &sn.coldkey, - &sn.hotkeys[0], - stake_amount, - ); - - // Forse-set signer balance to ED - let current_balance = Balances::free_balance(sn.coldkey); - remove_balance_from_coldkey_account( - &sn.coldkey, - current_balance - ExistentialDeposit::get(), - ); - - // Remove stake limit - let balance_before = Balances::free_balance(sn.coldkey); - let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake_limit { - hotkey: sn.hotkeys[0], - netuid: sn.subnets[0].netuid, - amount_unstaked: unstake_amount, - limit_price: 1_000.into(), - allow_partial: false, - }); - - System::reset_events(); - - // Dispatch the extrinsic with ChargeTransactionPayment extension - let info = call.get_dispatch_info(); - let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); - assert_ok!(ext.dispatch_transaction( - RuntimeOrigin::signed(sn.coldkey).into(), - call, - &info, - 0, - 0, - )); - - let final_balance = Balances::free_balance(sn.coldkey); - let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - - let expected_unstaked_tao = System::events() - .iter() - .rev() - .find_map(|event_record| match &event_record.event { - RuntimeEvent::SubtensorModule(SubtensorEvent::StakeRemoved( - coldkey, - hotkey, - tao_amount, - alpha_amount, - netuid, - fee_paid, - )) if coldkey == &sn.coldkey - && hotkey == &sn.hotkeys[0] - && *netuid == sn.subnets[0].netuid - && (*alpha_amount + AlphaBalance::from(*fee_paid) == unstake_amount) => - { - Some(*tao_amount) - } - _ => None, - }) - .expect("expected StakeRemoved event for remove_stake_limit"); - - let actual_tao_fee = balance_before + expected_unstaked_tao - final_balance; - let actual_alpha_fee = alpha_before - alpha_after - unstake_amount; - - // Remove stake extrinsic should pay fees in Alpha - assert_abs_diff_eq!(actual_tao_fee, 0.into(), epsilon = 100.into()); - assert!(actual_alpha_fee > 0.into()); - }); -} - -// cargo test --package subtensor-transaction-fee --lib -- tests::test_unstake_all_fees_alpha --exact --show-output -#[test] -fn test_unstake_all_fees_alpha() { - new_test_ext().execute_with(|| { - let stake_amount = TAO; - let sn = setup_subnets(10, 1); - let coldkey = U256::from(100000); - for i in 0..10 { - setup_stake(sn.subnets[i].netuid, &coldkey, &sn.hotkeys[0], stake_amount); - } - - // Root stake - add_network(NetUid::from(0), 10); - pallet_subtensor::SubtokenEnabled::::insert(NetUid::from(0), true); - setup_stake(0.into(), &coldkey, &sn.hotkeys[0], stake_amount); - - // Simulate stake removal to get how much TAO should we get for unstaked Alpha - let mut expected_unstaked_tao = 0; - for i in 0..10 { - let unstake_amount = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &coldkey, - sn.subnets[i].netuid, - ); - - let (tao, _swap_fee) = mock::swap_alpha_to_tao(sn.subnets[i].netuid, unstake_amount); - expected_unstaked_tao += tao; - } - - // Forse-set signer balance to ED - let current_balance = Balances::free_balance(coldkey); - remove_balance_from_coldkey_account(&coldkey, current_balance - ExistentialDeposit::get()); - - // Unstake all - let balance_before = Balances::free_balance(sn.coldkey); - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::unstake_all { - hotkey: sn.hotkeys[0], - }); - - // Dispatch the extrinsic with ChargeTransactionPayment extension - // Get invalid payment because we cannot pay fees in multiple alphas - let info = call.get_dispatch_info(); - let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); - assert_err!( - ext.clone().dispatch_transaction( - RuntimeOrigin::signed(coldkey).into(), - call.clone(), - &info, - 0, - 0, - ), - TransactionValidityError::Invalid(InvalidTransaction::Payment), - ); - - // Give the coldkey TAO balance - now should unstake ok - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_u64.into()); - assert_ok!(ext.dispatch_transaction( - RuntimeOrigin::signed(coldkey).into(), - call, - &info, - 0, - 0, - )); - - let final_balance = Balances::free_balance(sn.coldkey); - - // Effectively, the fee is paid in TAO in this case because user receives less TAO, - // and all Alpha is gone, and it is not measurable in Alpha - let actual_fee = balance_before + expected_unstaked_tao.into() - final_balance; - assert!(actual_fee > 0.into()); - - // Check that all subnets got unstaked - for i in 0..10 { - let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[i].netuid, - ); - assert_eq!(alpha_after, 0.into()); - } - }); -} - -// cargo test --package subtensor-transaction-fee --lib -- tests::test_unstake_all_alpha_fees_alpha --exact --show-output -#[test] -fn test_unstake_all_alpha_fees_alpha() { - new_test_ext().execute_with(|| { - let stake_amount = TAO; - let sn = setup_subnets(10, 1); - let coldkey = U256::from(100000); - for i in 0..10 { - setup_stake(sn.subnets[i].netuid, &coldkey, &sn.hotkeys[0], stake_amount); - } - - // Simulate stake removal to get how much TAO should we get for unstaked Alpha - let mut expected_unstaked_tao = 0; - for i in 0..10 { - let unstake_amount = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &coldkey, - sn.subnets[i].netuid, - ); - - let (tao, _swap_fee) = mock::swap_alpha_to_tao(sn.subnets[i].netuid, unstake_amount); - expected_unstaked_tao += tao; - } - - // Forse-set signer balance to ED - let current_balance = Balances::free_balance(coldkey); - remove_balance_from_coldkey_account(&coldkey, current_balance - ExistentialDeposit::get()); - - // Unstake all - let balance_before = Balances::free_balance(sn.coldkey); - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::unstake_all_alpha { - hotkey: sn.hotkeys[0], - }); - - // Dispatch the extrinsic with ChargeTransactionPayment extension - // Get invalid payment because we cannot pay fees in multiple alphas - let info = call.get_dispatch_info(); - let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); - assert_err!( - ext.clone().dispatch_transaction( - RuntimeOrigin::signed(coldkey).into(), - call.clone(), - &info, - 0, - 0, - ), - TransactionValidityError::Invalid(InvalidTransaction::Payment), - ); - - // Give the coldkey TAO balance - now should unstake ok - add_balance_to_coldkey_account(&coldkey, 1_000_000_000_u64.into()); - assert_ok!(ext.dispatch_transaction( - RuntimeOrigin::signed(coldkey).into(), - call, - &info, - 0, - 0, - )); - - let final_balance = Balances::free_balance(sn.coldkey); - - // Effectively, the fee is paid in TAO in this case because user receives less TAO, - // and all Alpha is gone, and it is not measurable in Alpha - let actual_fee = balance_before + expected_unstaked_tao.into() - final_balance; - assert!(actual_fee > 0.into()); - - // Check that all subnets got unstaked - for i in 0..10 { - let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[i].netuid, - ); - assert_eq!(alpha_after, 0.into()); - } - }); -} - -// cargo test --package subtensor-transaction-fee --lib -- tests::test_move_stake_fees_alpha --exact --show-output -#[test] -fn test_move_stake_fees_alpha() { - new_test_ext().execute_with(|| { - let stake_amount = TAO; - let unstake_amount = AlphaBalance::from(TAO / 50); - let sn = setup_subnets(2, 2); - setup_stake( - sn.subnets[0].netuid, - &sn.coldkey, - &sn.hotkeys[0], - stake_amount, - ); - - // Forse-set signer balance to ED - let current_balance = Balances::free_balance(sn.coldkey); - remove_balance_from_coldkey_account( - &sn.coldkey, - current_balance - ExistentialDeposit::get(), - ); - - // Move stake - let balance_before = Balances::free_balance(sn.coldkey); - let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::move_stake { - origin_hotkey: sn.hotkeys[0], - destination_hotkey: sn.hotkeys[1], - origin_netuid: sn.subnets[0].netuid, - destination_netuid: sn.subnets[1].netuid, - alpha_amount: unstake_amount, - }); - - // Dispatch the extrinsic with ChargeTransactionPayment extension - let info = call.get_dispatch_info(); - let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); - assert_ok!(ext.dispatch_transaction( - RuntimeOrigin::signed(sn.coldkey).into(), - call, - &info, - 0, - 0, - )); - - let final_balance = Balances::free_balance(sn.coldkey); - let alpha_after_0 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - - // Ensure stake was moved - let alpha_after_1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[1], - &sn.coldkey, - sn.subnets[1].netuid, - ); - assert!(alpha_after_1 > 0.into()); - - let actual_tao_fee = balance_before - final_balance; - let actual_alpha_fee = alpha_before - alpha_after_0 - unstake_amount; - - // Extrinsic should pay fees in Alpha - assert_eq!(actual_tao_fee, 0.into()); - assert!(actual_alpha_fee > 0.into()); - }); -} - -// cargo test --package subtensor-transaction-fee --lib -- tests::test_transfer_stake_fees_alpha --exact --show-output -#[test] -fn test_transfer_stake_fees_alpha() { - new_test_ext().execute_with(|| { - let destination_coldkey = U256::from(100000); - let stake_amount = TAO; - let unstake_amount = AlphaBalance::from(TAO / 50); - let sn = setup_subnets(2, 2); - setup_stake( - sn.subnets[0].netuid, - &sn.coldkey, - &sn.hotkeys[0], - stake_amount, - ); - - // Forse-set signer balance to ED - let current_balance = Balances::free_balance(sn.coldkey); - remove_balance_from_coldkey_account( - &sn.coldkey, - current_balance - ExistentialDeposit::get(), - ); - - // Transfer stake - let balance_before = Balances::free_balance(sn.coldkey); - let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::transfer_stake { - destination_coldkey, - hotkey: sn.hotkeys[0], - origin_netuid: sn.subnets[0].netuid, - destination_netuid: sn.subnets[1].netuid, - alpha_amount: unstake_amount, - }); - - // Dispatch the extrinsic with ChargeTransactionPayment extension - let info = call.get_dispatch_info(); - let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); - assert_ok!(ext.dispatch_transaction( - RuntimeOrigin::signed(sn.coldkey).into(), - call, - &info, - 0, - 0, - )); - - let final_balance = Balances::free_balance(sn.coldkey); - let alpha_after_0 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - - // Ensure stake was transferred - let alpha_after_1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &destination_coldkey, - sn.subnets[1].netuid, - ); - assert!(alpha_after_1 > 0.into()); - - let actual_tao_fee = balance_before - final_balance; - let actual_alpha_fee = alpha_before - alpha_after_0 - unstake_amount; - - // Extrinsic should pay fees in Alpha - assert_eq!(actual_tao_fee, 0.into()); - assert!(actual_alpha_fee > 0.into()); - }); -} - -// cargo test --package subtensor-transaction-fee --lib -- tests::test_transfer_stake_full_amount_fails_when_alpha_fee_reduces_available_stake --exact --show-output -#[test] -fn test_transfer_stake_full_amount_fails_when_alpha_fee_reduces_available_stake() { - new_test_ext().execute_with(|| { - let destination_coldkey = U256::from(100000); - let stake_amount = TAO; - let sn = setup_subnets(2, 2); - setup_stake( - sn.subnets[0].netuid, - &sn.coldkey, - &sn.hotkeys[0], - stake_amount, - ); - - let current_balance = Balances::free_balance(sn.coldkey); - remove_balance_from_coldkey_account(&sn.coldkey, current_balance); - assert_eq!(Balances::free_balance(sn.coldkey), TaoBalance::ZERO); - - let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::transfer_stake { - destination_coldkey, - hotkey: sn.hotkeys[0], - origin_netuid: sn.subnets[0].netuid, - destination_netuid: sn.subnets[1].netuid, - alpha_amount: alpha_before, - }); - let info = call.get_dispatch_info(); - let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); - - let inner = ext - .dispatch_transaction(RuntimeOrigin::signed(sn.coldkey).into(), call, &info, 0, 0) - .expect("alpha fee payment should validate"); - assert_eq!( - inner.unwrap_err().error, - Error::::NotEnoughStakeToWithdraw.into() - ); - - let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - let actual_alpha_fee = alpha_before - alpha_after; - assert!(actual_alpha_fee > AlphaBalance::ZERO); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &destination_coldkey, - sn.subnets[1].netuid, - ), - AlphaBalance::ZERO - ); - }); - - new_test_ext().execute_with(|| { - let destination_coldkey = U256::from(100000); - let stake_amount = TAO; - let sn = setup_subnets(2, 2); - setup_stake( - sn.subnets[0].netuid, - &sn.coldkey, - &sn.hotkeys[0], - stake_amount, - ); - - let current_balance = Balances::free_balance(sn.coldkey); - remove_balance_from_coldkey_account(&sn.coldkey, current_balance); - assert_eq!(Balances::free_balance(sn.coldkey), TaoBalance::ZERO); - - let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - let full_amount_call = - RuntimeCall::SubtensorModule(pallet_subtensor::Call::transfer_stake { - destination_coldkey, - hotkey: sn.hotkeys[0], - origin_netuid: sn.subnets[0].netuid, - destination_netuid: sn.subnets[1].netuid, - alpha_amount: alpha_before, - }); - let info = full_amount_call.get_dispatch_info(); - let tao_fee = pallet_transaction_payment::Pallet::::compute_fee(0, &info, 0.into()); - let alpha_fee = pallet_subtensor_swap::Pallet::::get_alpha_amount_for_tao( - sn.subnets[0].netuid, - tao_fee, - ); - assert!(alpha_fee > AlphaBalance::ZERO); - - let transfer_amount = alpha_before - alpha_fee; - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::transfer_stake { - destination_coldkey, - hotkey: sn.hotkeys[0], - origin_netuid: sn.subnets[0].netuid, - destination_netuid: sn.subnets[1].netuid, - alpha_amount: transfer_amount, - }); - let info = call.get_dispatch_info(); - let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); - - let inner = ext - .dispatch_transaction(RuntimeOrigin::signed(sn.coldkey).into(), call, &info, 0, 0) - .expect("alpha fee payment should validate"); - assert_ok!(inner); - - assert_eq!(Balances::free_balance(sn.coldkey), TaoBalance::ZERO); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ), - AlphaBalance::ZERO - ); - assert!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &destination_coldkey, - sn.subnets[1].netuid, - ) > AlphaBalance::ZERO - ); - }); -} - -// cargo test --package subtensor-transaction-fee --lib -- tests::test_swap_stake_fees_alpha --exact --show-output -#[test] -fn test_swap_stake_fees_alpha() { - new_test_ext().execute_with(|| { - let stake_amount = TAO; - let unstake_amount = AlphaBalance::from(TAO / 50); - let sn = setup_subnets(2, 2); - setup_stake( - sn.subnets[0].netuid, - &sn.coldkey, - &sn.hotkeys[0], - stake_amount, - ); - - // Forse-set signer balance to ED - let current_balance = Balances::free_balance(sn.coldkey); - remove_balance_from_coldkey_account( - &sn.coldkey, - current_balance - ExistentialDeposit::get(), - ); - - // Swap stake - let balance_before = Balances::free_balance(sn.coldkey); - let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_stake { - hotkey: sn.hotkeys[0], - origin_netuid: sn.subnets[0].netuid, - destination_netuid: sn.subnets[1].netuid, - alpha_amount: unstake_amount, - }); - - // Dispatch the extrinsic with ChargeTransactionPayment extension - let info = call.get_dispatch_info(); - let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); - assert_ok!(ext.dispatch_transaction( - RuntimeOrigin::signed(sn.coldkey).into(), - call, - &info, - 0, - 0, - )); - - let final_balance = Balances::free_balance(sn.coldkey); - let alpha_after_0 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - - // Ensure stake was transferred - let alpha_after_1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[1].netuid, - ); - assert!(alpha_after_1 > 0.into()); - - let actual_tao_fee = balance_before - final_balance; - let actual_alpha_fee = alpha_before - alpha_after_0 - unstake_amount; - - // Extrinsic should pay fees in Alpha - assert_eq!(actual_tao_fee, 0.into()); - assert!(actual_alpha_fee > 0.into()); - }); -} - -// cargo test --package subtensor-transaction-fee --lib -- tests::test_swap_stake_limit_fees_alpha --exact --show-output -#[test] -fn test_swap_stake_limit_fees_alpha() { - new_test_ext().execute_with(|| { - let stake_amount = TAO; - let unstake_amount = AlphaBalance::from(TAO / 50); - let sn = setup_subnets(2, 2); - setup_stake( - sn.subnets[0].netuid, - &sn.coldkey, - &sn.hotkeys[0], - stake_amount, - ); - - // Forse-set signer balance to ED - let current_balance = Balances::free_balance(sn.coldkey); - remove_balance_from_coldkey_account( - &sn.coldkey, - current_balance - ExistentialDeposit::get(), - ); - - // Swap stake limit - let balance_before = Balances::free_balance(sn.coldkey); - let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_stake_limit { - hotkey: sn.hotkeys[0], - origin_netuid: sn.subnets[0].netuid, - destination_netuid: sn.subnets[1].netuid, - alpha_amount: unstake_amount, - limit_price: 1_000.into(), - allow_partial: false, - }); - - // Dispatch the extrinsic with ChargeTransactionPayment extension - let info = call.get_dispatch_info(); - let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); - assert_ok!(ext.dispatch_transaction( - RuntimeOrigin::signed(sn.coldkey).into(), - call, - &info, - 0, - 0, - )); - - let final_balance = Balances::free_balance(sn.coldkey); - let alpha_after_0 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - - // Ensure stake was transferred - let alpha_after_1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[1].netuid, - ); - assert!(alpha_after_1 > 0.into()); - - let actual_tao_fee = balance_before - final_balance; - let actual_alpha_fee = alpha_before - alpha_after_0 - unstake_amount; - - // Extrinsic should pay fees in Alpha - assert_eq!(actual_tao_fee, 0.into()); - assert!(actual_alpha_fee > 0.into()); - }); -} - -// cargo test --package subtensor-transaction-fee --lib -- tests::test_burn_alpha_fees_alpha --exact --show-output -#[test] -fn test_burn_alpha_fees_alpha() { - new_test_ext().execute_with(|| { - let stake_amount = TAO; - let alpha_amount = AlphaBalance::from(TAO / 50); - let sn = setup_subnets(1, 1); - setup_stake( - sn.subnets[0].netuid, - &sn.coldkey, - &sn.hotkeys[0], - stake_amount, - ); - - // Forse-set signer balance to ED - let current_balance = Balances::free_balance(sn.coldkey); - remove_balance_from_coldkey_account( - &sn.coldkey, - current_balance - ExistentialDeposit::get(), - ); - - // Burn alpha - let balance_before = Balances::free_balance(sn.coldkey); - let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::burn_alpha { - hotkey: sn.hotkeys[0], - amount: alpha_amount, - netuid: sn.subnets[0].netuid, - }); - - // Dispatch the extrinsic with ChargeTransactionPayment extension - let info = call.get_dispatch_info(); - let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); - assert_ok!(ext.dispatch_transaction( - RuntimeOrigin::signed(sn.coldkey).into(), - call, - &info, - 0, - 0, - )); - - let final_balance = Balances::free_balance(sn.coldkey); - let alpha_after_0 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - - let actual_tao_fee = balance_before - final_balance; - let actual_alpha_fee = alpha_before - alpha_after_0 - alpha_amount; - - // Extrinsic should pay fees in Alpha - assert_eq!(actual_tao_fee, 0.into()); - assert!(actual_alpha_fee > 0.into()); - }); -} - -// cargo test --package subtensor-transaction-fee --lib -- tests::test_recycle_alpha_fees_alpha --exact --show-output -#[test] -fn test_recycle_alpha_fees_alpha() { - new_test_ext().execute_with(|| { - let stake_amount = TAO; - let alpha_amount = AlphaBalance::from(TAO / 50); - let sn = setup_subnets(1, 1); - setup_stake( - sn.subnets[0].netuid, - &sn.coldkey, - &sn.hotkeys[0], - stake_amount, - ); - - // Forse-set signer balance to ED - let current_balance = Balances::free_balance(sn.coldkey); - remove_balance_from_coldkey_account( - &sn.coldkey, - current_balance - ExistentialDeposit::get(), - ); - - // Recycle alpha - let balance_before = Balances::free_balance(sn.coldkey); - let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::recycle_alpha { - hotkey: sn.hotkeys[0], - amount: alpha_amount, - netuid: sn.subnets[0].netuid, - }); - - // Dispatch the extrinsic with ChargeTransactionPayment extension - let info = call.get_dispatch_info(); - let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); - assert_ok!(ext.dispatch_transaction( - RuntimeOrigin::signed(sn.coldkey).into(), - call, - &info, - 0, - 0, - )); - - let final_balance = Balances::free_balance(sn.coldkey); - let alpha_after_0 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &sn.hotkeys[0], - &sn.coldkey, - sn.subnets[0].netuid, - ); - - let actual_tao_fee = balance_before - final_balance; - let actual_alpha_fee = alpha_before - alpha_after_0 - alpha_amount; - - // Extrinsic should pay fees in Alpha - assert_eq!(actual_tao_fee, 0.into()); - assert!(actual_alpha_fee > 0.into()); - }); -} - -// cargo test --package subtensor-transaction-fee --lib -- tests::test_add_stake_fees_go_to_block_builder --exact --show-output -#[test] -fn test_add_stake_fees_go_to_block_builder() { - new_test_ext().execute_with(|| { - // Portion of swap fees that should go to the block builder - let block_builder_fee_portion = 1.; - - // Get the block builder balance - let block_builder = U256::from(MOCK_BLOCK_BUILDER); - let block_builder_balance_before = Balances::free_balance(block_builder); - - let stake_amount = TAO; - let sn = setup_subnets(1, 1); - - // Simulate add stake to get the expected TAO fee - let (_, swap_fee) = mock::swap_tao_to_alpha(sn.subnets[0].netuid, stake_amount.into()); - - add_balance_to_coldkey_account(&sn.coldkey, (stake_amount * 10).into()); - - // Stake - let balance_before = Balances::free_balance(sn.coldkey); - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::add_stake { - hotkey: sn.hotkeys[0], - netuid: sn.subnets[0].netuid, - amount_staked: stake_amount.into(), - }); - - // Dispatch the extrinsic with ChargeTransactionPayment extension - let info = call.get_dispatch_info(); - let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); - assert_ok!(ext.dispatch_transaction( - RuntimeOrigin::signed(sn.coldkey).into(), - call, - &info, - 0, - 0, - )); - - let final_balance = Balances::free_balance(sn.coldkey); - let actual_tao_fee = balance_before - stake_amount.into() - final_balance; - assert!(!actual_tao_fee.is_zero()); - - // Expect that block builder balance has increased by both the swap fee and the transaction fee - let expected_block_builder_swap_reward = swap_fee as f64 * block_builder_fee_portion; - let expected_tx_fee = 14000.; // Use very low value (0.000014) for less test flakiness, value before we 10x tx fees - let block_builder_balance_after = Balances::free_balance(block_builder); - let actual_reward = block_builder_balance_after - block_builder_balance_before; - assert!( - u64::from(actual_reward) as f64 >= expected_block_builder_swap_reward + expected_tx_fee - ); - }); -} - -// Fully collateral-bonded stake must not pay alpha fees. Regression for the -// phantom-bond bug where fee unstake stripped stake while MinerCollateral.locked -// stayed unchanged. -// -// cargo test --package subtensor-transaction-fee --lib -- tests::test_alpha_fee_rejects_fully_collateralized_stake --exact --show-output -#[test] -fn test_alpha_fee_rejects_fully_collateralized_stake() { - new_test_ext().execute_with(|| { - let stake_amount = TAO; - let sn = setup_subnets(1, 1); - let netuid = sn.subnets[0].netuid; - let hotkey = sn.hotkeys[0]; - - setup_stake(netuid, &sn.coldkey, &hotkey, stake_amount); - let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &sn.coldkey, - netuid, - ); - assert!(!alpha.is_zero()); - mark_collateral(netuid, &hotkey, &sn.coldkey, alpha); - drain_coldkey_to_ed(&sn.coldkey); - - let alpha_vec = vec![(hotkey, netuid)]; - assert_eq!( - SubtensorModule::available_to_unstake_from_hotkey(&sn.coldkey, &hotkey, netuid), - AlphaBalance::ZERO - ); - assert!( - ! as AlphaFeeHandler>::can_withdraw_in_alpha( - &sn.coldkey, - &alpha_vec, - 1.into(), - ) - ); - - let subnet_tao_before = SubnetTAO::::get(netuid); - let subnet_alpha_in_before = SubnetAlphaIn::::get(netuid); - let subnet_alpha_out_before = SubnetAlphaOut::::get(netuid); - let collateral_before = - MinerCollateral::::get((netuid, hotkey, sn.coldkey)).expect("collateral entry"); - let aggregate_before = ColdkeyMinerCollateral::::get(netuid, sn.coldkey); - - assert_eq!( - as AlphaFeeHandler>::withdraw_in_alpha( - &sn.coldkey, - &alpha_vec, - 1.into(), - ), - Err(TransactionValidityError::Invalid( - InvalidTransaction::Payment - )) - ); - - // Also reject through the full charge-extension path. - let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { - hotkey, - netuid, - amount_unstaked: AlphaBalance::from(1u64), - }); - let info = call.get_dispatch_info(); - let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); - let result = - ext.dispatch_transaction(RuntimeOrigin::signed(sn.coldkey).into(), call, &info, 0, 0); - assert_eq!( - result.unwrap_err(), - TransactionValidityError::Invalid(InvalidTransaction::Payment) - ); - - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &sn.coldkey, - netuid, - ), - alpha - ); - assert_eq!(SubnetTAO::::get(netuid), subnet_tao_before); - assert_eq!(SubnetAlphaIn::::get(netuid), subnet_alpha_in_before); - assert_eq!(SubnetAlphaOut::::get(netuid), subnet_alpha_out_before); - let collateral_after = - MinerCollateral::::get((netuid, hotkey, sn.coldkey)).expect("collateral entry"); - assert_eq!(collateral_after.locked, collateral_before.locked); - assert_eq!( - ColdkeyMinerCollateral::::get(netuid, sn.coldkey), - aggregate_before - ); - }); -} - -// Only the free (non-collateral) slice of a position may fund alpha fees. -// -// cargo test --package subtensor-transaction-fee --lib -- tests::test_alpha_fee_only_from_free_stake_above_collateral --exact --show-output -#[test] -fn test_alpha_fee_only_from_free_stake_above_collateral() { - new_test_ext().execute_with(|| { - let stake_amount = TAO * 10; - let sn = setup_subnets(1, 1); - let netuid = sn.subnets[0].netuid; - let hotkey = sn.hotkeys[0]; - - setup_stake(netuid, &sn.coldkey, &hotkey, stake_amount); - let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &sn.coldkey, - netuid, - ); - let locked = alpha / 2.into(); - let free = alpha.saturating_sub(locked); - assert!(!free.is_zero()); - mark_collateral(netuid, &hotkey, &sn.coldkey, locked); - drain_coldkey_to_ed(&sn.coldkey); - - assert_eq!( - SubtensorModule::available_to_unstake_from_hotkey(&sn.coldkey, &hotkey, netuid), - free - ); - - let alpha_vec = vec![(hotkey, netuid)]; - - // A fee larger than the free slice must be rejected up front. - let large_tao_fee = TaoBalance::from(TAO.saturating_mul(20)); - let alpha_for_large = - pallet_subtensor_swap::Pallet::::get_alpha_amount_for_tao(netuid, large_tao_fee); - assert!( - alpha_for_large > free, - "test needs a TAO fee quote larger than free stake (got {alpha_for_large:?} vs free {free:?})" - ); - assert!( - ! as AlphaFeeHandler>::can_withdraw_in_alpha( - &sn.coldkey, - &alpha_vec, - large_tao_fee, - ) - ); - - // A small fee that fits in the free slice succeeds and never touches - // the locked collateral accounting. - let small_tao_fee = TaoBalance::from(1_000_000u64); // 0.001 TAO - let alpha_for_small = - pallet_subtensor_swap::Pallet::::get_alpha_amount_for_tao(netuid, small_tao_fee); - assert!(!alpha_for_small.is_zero()); - assert!(alpha_for_small <= free); - assert!( - as AlphaFeeHandler>::can_withdraw_in_alpha( - &sn.coldkey, - &alpha_vec, - small_tao_fee, - ) - ); - - let collateral_before = MinerCollateral::::get((netuid, hotkey, sn.coldkey)) - .expect("collateral entry") - .locked; - let (taken, _tao_out, fee_netuid) = - as AlphaFeeHandler>::withdraw_in_alpha( - &sn.coldkey, - &alpha_vec, - small_tao_fee, - ) - .expect("free-slice fee should withdraw"); - assert_eq!(fee_netuid, netuid); - assert_eq!(taken, alpha_for_small); - - let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &sn.coldkey, - netuid, - ); - assert_eq!(alpha_after, alpha.saturating_sub(taken)); - assert!(alpha_after >= locked); - assert_eq!( - MinerCollateral::::get((netuid, hotkey, sn.coldkey)) - .expect("collateral entry") - .locked, - collateral_before - ); - assert_eq!( - ColdkeyMinerCollateral::::get(netuid, sn.coldkey), - locked - ); - }); -} +mod alpha_fee_collateral; +mod block_builder_fees; +mod burn_recycle_alpha_fees; +mod move_transfer_swap_stake_fees; +mod remove_stake_fees; +mod swap_hotkey_fees; +mod unstake_all_fees; diff --git a/pallets/transaction-fee/src/tests/move_transfer_swap_stake_fees.rs b/pallets/transaction-fee/src/tests/move_transfer_swap_stake_fees.rs new file mode 100644 index 0000000000..7e555d7c64 --- /dev/null +++ b/pallets/transaction-fee/src/tests/move_transfer_swap_stake_fees.rs @@ -0,0 +1,421 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +use super::mock::*; +use frame_support::assert_ok; +use frame_support::dispatch::GetDispatchInfo; +use sp_runtime::traits::DispatchTransaction; +use subtensor_runtime_common::AlphaBalance; +use subtensor_swap_interface::SwapHandler; + +// cargo test --package subtensor-transaction-fee --lib -- tests::move_transfer_swap_stake_fees::test_move_stake_fees_alpha --exact --show-output +#[test] +fn test_move_stake_fees_alpha() { + new_test_ext().execute_with(|| { + let stake_amount = TAO; + let unstake_amount = AlphaBalance::from(TAO / 50); + let sn = setup_fee_test_subnets(2, 2); + fund_and_add_stake( + sn.subnets[0].netuid, + &sn.coldkey, + &sn.hotkeys[0], + stake_amount, + ); + + // Forse-set signer balance to ED + let current_balance = Balances::free_balance(sn.coldkey); + remove_balance_from_coldkey_account( + &sn.coldkey, + current_balance - ExistentialDeposit::get(), + ); + + // Move stake + let balance_before = Balances::free_balance(sn.coldkey); + let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::move_stake { + origin_hotkey: sn.hotkeys[0], + destination_hotkey: sn.hotkeys[1], + origin_netuid: sn.subnets[0].netuid, + destination_netuid: sn.subnets[1].netuid, + alpha_amount: unstake_amount, + }); + + // Dispatch the extrinsic with ChargeTransactionPayment extension + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + assert_ok!(ext.dispatch_transaction( + RuntimeOrigin::signed(sn.coldkey).into(), + call, + &info, + 0, + 0, + )); + + let final_balance = Balances::free_balance(sn.coldkey); + let alpha_after_0 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + + // Ensure stake was moved + let alpha_after_1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[1], + &sn.coldkey, + sn.subnets[1].netuid, + ); + assert!(alpha_after_1 > 0.into()); + + let actual_tao_fee = balance_before - final_balance; + let actual_alpha_fee = alpha_before - alpha_after_0 - unstake_amount; + + // Extrinsic should pay fees in Alpha + assert_eq!(actual_tao_fee, 0.into()); + assert!(actual_alpha_fee > 0.into()); + }); +} + +// cargo test --package subtensor-transaction-fee --lib -- tests::move_transfer_swap_stake_fees::test_transfer_stake_fees_alpha --exact --show-output +#[test] +fn test_transfer_stake_fees_alpha() { + new_test_ext().execute_with(|| { + let destination_coldkey = U256::from(100000); + let stake_amount = TAO; + let unstake_amount = AlphaBalance::from(TAO / 50); + let sn = setup_fee_test_subnets(2, 2); + fund_and_add_stake( + sn.subnets[0].netuid, + &sn.coldkey, + &sn.hotkeys[0], + stake_amount, + ); + + // Forse-set signer balance to ED + let current_balance = Balances::free_balance(sn.coldkey); + remove_balance_from_coldkey_account( + &sn.coldkey, + current_balance - ExistentialDeposit::get(), + ); + + // Transfer stake + let balance_before = Balances::free_balance(sn.coldkey); + let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::transfer_stake { + destination_coldkey, + hotkey: sn.hotkeys[0], + origin_netuid: sn.subnets[0].netuid, + destination_netuid: sn.subnets[1].netuid, + alpha_amount: unstake_amount, + }); + + // Dispatch the extrinsic with ChargeTransactionPayment extension + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + assert_ok!(ext.dispatch_transaction( + RuntimeOrigin::signed(sn.coldkey).into(), + call, + &info, + 0, + 0, + )); + + let final_balance = Balances::free_balance(sn.coldkey); + let alpha_after_0 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + + // Ensure stake was transferred + let alpha_after_1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &destination_coldkey, + sn.subnets[1].netuid, + ); + assert!(alpha_after_1 > 0.into()); + + let actual_tao_fee = balance_before - final_balance; + let actual_alpha_fee = alpha_before - alpha_after_0 - unstake_amount; + + // Extrinsic should pay fees in Alpha + assert_eq!(actual_tao_fee, 0.into()); + assert!(actual_alpha_fee > 0.into()); + }); +} + +// cargo test --package subtensor-transaction-fee --lib -- tests::move_transfer_swap_stake_fees::test_transfer_stake_full_amount_fails_when_alpha_fee_reduces_available_stake --exact --show-output +#[test] +fn test_transfer_stake_full_amount_fails_when_alpha_fee_reduces_available_stake() { + new_test_ext().execute_with(|| { + let destination_coldkey = U256::from(100000); + let stake_amount = TAO; + let sn = setup_fee_test_subnets(2, 2); + fund_and_add_stake( + sn.subnets[0].netuid, + &sn.coldkey, + &sn.hotkeys[0], + stake_amount, + ); + + let current_balance = Balances::free_balance(sn.coldkey); + remove_balance_from_coldkey_account(&sn.coldkey, current_balance); + assert_eq!(Balances::free_balance(sn.coldkey), TaoBalance::ZERO); + + let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::transfer_stake { + destination_coldkey, + hotkey: sn.hotkeys[0], + origin_netuid: sn.subnets[0].netuid, + destination_netuid: sn.subnets[1].netuid, + alpha_amount: alpha_before, + }); + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + + let inner = ext + .dispatch_transaction(RuntimeOrigin::signed(sn.coldkey).into(), call, &info, 0, 0) + .expect("alpha fee payment should validate"); + assert_eq!( + inner.unwrap_err().error, + Error::::NotEnoughStakeToWithdraw.into() + ); + + let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + let actual_alpha_fee = alpha_before - alpha_after; + assert!(actual_alpha_fee > AlphaBalance::ZERO); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &destination_coldkey, + sn.subnets[1].netuid, + ), + AlphaBalance::ZERO + ); + }); + + new_test_ext().execute_with(|| { + let destination_coldkey = U256::from(100000); + let stake_amount = TAO; + let sn = setup_fee_test_subnets(2, 2); + fund_and_add_stake( + sn.subnets[0].netuid, + &sn.coldkey, + &sn.hotkeys[0], + stake_amount, + ); + + let current_balance = Balances::free_balance(sn.coldkey); + remove_balance_from_coldkey_account(&sn.coldkey, current_balance); + assert_eq!(Balances::free_balance(sn.coldkey), TaoBalance::ZERO); + + let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + let full_amount_call = + RuntimeCall::SubtensorModule(pallet_subtensor::Call::transfer_stake { + destination_coldkey, + hotkey: sn.hotkeys[0], + origin_netuid: sn.subnets[0].netuid, + destination_netuid: sn.subnets[1].netuid, + alpha_amount: alpha_before, + }); + let info = full_amount_call.get_dispatch_info(); + let tao_fee = pallet_transaction_payment::Pallet::::compute_fee(0, &info, 0.into()); + let alpha_fee = pallet_subtensor_swap::Pallet::::get_alpha_amount_for_tao( + sn.subnets[0].netuid, + tao_fee, + ); + assert!(alpha_fee > AlphaBalance::ZERO); + + let transfer_amount = alpha_before - alpha_fee; + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::transfer_stake { + destination_coldkey, + hotkey: sn.hotkeys[0], + origin_netuid: sn.subnets[0].netuid, + destination_netuid: sn.subnets[1].netuid, + alpha_amount: transfer_amount, + }); + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + + let inner = ext + .dispatch_transaction(RuntimeOrigin::signed(sn.coldkey).into(), call, &info, 0, 0) + .expect("alpha fee payment should validate"); + assert_ok!(inner); + + assert_eq!(Balances::free_balance(sn.coldkey), TaoBalance::ZERO); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ), + AlphaBalance::ZERO + ); + assert!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &destination_coldkey, + sn.subnets[1].netuid, + ) > AlphaBalance::ZERO + ); + }); +} + +// cargo test --package subtensor-transaction-fee --lib -- tests::move_transfer_swap_stake_fees::test_swap_stake_fees_alpha --exact --show-output +#[test] +fn test_swap_stake_fees_alpha() { + new_test_ext().execute_with(|| { + let stake_amount = TAO; + let unstake_amount = AlphaBalance::from(TAO / 50); + let sn = setup_fee_test_subnets(2, 2); + fund_and_add_stake( + sn.subnets[0].netuid, + &sn.coldkey, + &sn.hotkeys[0], + stake_amount, + ); + + // Forse-set signer balance to ED + let current_balance = Balances::free_balance(sn.coldkey); + remove_balance_from_coldkey_account( + &sn.coldkey, + current_balance - ExistentialDeposit::get(), + ); + + // Swap stake + let balance_before = Balances::free_balance(sn.coldkey); + let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_stake { + hotkey: sn.hotkeys[0], + origin_netuid: sn.subnets[0].netuid, + destination_netuid: sn.subnets[1].netuid, + alpha_amount: unstake_amount, + }); + + // Dispatch the extrinsic with ChargeTransactionPayment extension + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + assert_ok!(ext.dispatch_transaction( + RuntimeOrigin::signed(sn.coldkey).into(), + call, + &info, + 0, + 0, + )); + + let final_balance = Balances::free_balance(sn.coldkey); + let alpha_after_0 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + + // Ensure stake was transferred + let alpha_after_1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[1].netuid, + ); + assert!(alpha_after_1 > 0.into()); + + let actual_tao_fee = balance_before - final_balance; + let actual_alpha_fee = alpha_before - alpha_after_0 - unstake_amount; + + // Extrinsic should pay fees in Alpha + assert_eq!(actual_tao_fee, 0.into()); + assert!(actual_alpha_fee > 0.into()); + }); +} + +// cargo test --package subtensor-transaction-fee --lib -- tests::move_transfer_swap_stake_fees::test_swap_stake_limit_fees_alpha --exact --show-output +#[test] +fn test_swap_stake_limit_fees_alpha() { + new_test_ext().execute_with(|| { + let stake_amount = TAO; + let unstake_amount = AlphaBalance::from(TAO / 50); + let sn = setup_fee_test_subnets(2, 2); + fund_and_add_stake( + sn.subnets[0].netuid, + &sn.coldkey, + &sn.hotkeys[0], + stake_amount, + ); + + // Forse-set signer balance to ED + let current_balance = Balances::free_balance(sn.coldkey); + remove_balance_from_coldkey_account( + &sn.coldkey, + current_balance - ExistentialDeposit::get(), + ); + + // Swap stake limit + let balance_before = Balances::free_balance(sn.coldkey); + let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_stake_limit { + hotkey: sn.hotkeys[0], + origin_netuid: sn.subnets[0].netuid, + destination_netuid: sn.subnets[1].netuid, + alpha_amount: unstake_amount, + limit_price: 1_000.into(), + allow_partial: false, + }); + + // Dispatch the extrinsic with ChargeTransactionPayment extension + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + assert_ok!(ext.dispatch_transaction( + RuntimeOrigin::signed(sn.coldkey).into(), + call, + &info, + 0, + 0, + )); + + let final_balance = Balances::free_balance(sn.coldkey); + let alpha_after_0 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + + // Ensure stake was transferred + let alpha_after_1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[1].netuid, + ); + assert!(alpha_after_1 > 0.into()); + + let actual_tao_fee = balance_before - final_balance; + let actual_alpha_fee = alpha_before - alpha_after_0 - unstake_amount; + + // Extrinsic should pay fees in Alpha + assert_eq!(actual_tao_fee, 0.into()); + assert!(actual_alpha_fee > 0.into()); + }); +} diff --git a/pallets/transaction-fee/src/tests/remove_stake_fees.rs b/pallets/transaction-fee/src/tests/remove_stake_fees.rs new file mode 100644 index 0000000000..f05e38db98 --- /dev/null +++ b/pallets/transaction-fee/src/tests/remove_stake_fees.rs @@ -0,0 +1,880 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +use super::mock::*; +use approx::assert_abs_diff_eq; +use frame_support::assert_ok; +use frame_support::dispatch::GetDispatchInfo; +use frame_support::traits::Currency; +use sp_runtime::{ + traits::{DispatchTransaction, TransactionExtension, TxBaseImplication}, + transaction_validity::{InvalidTransaction, TransactionSource, TransactionValidityError}, +}; +use subtensor_runtime_common::AlphaBalance; + +// cargo test --package subtensor-transaction-fee --lib -- tests::remove_stake_fees::test_remove_stake_fees_tao --exact --show-output +#[test] +fn test_remove_stake_fees_tao() { + new_test_ext().execute_with(|| { + use frame_support::traits::Hooks; + use sp_runtime::traits::SaturatedConversion; + + type BN = frame_system::pallet_prelude::BlockNumberFor; + + // Advance blocks and run hooks so staking-op rate limit windows reset. + let jump_blocks = |delta: u64| { + let current_bn: BN = frame_system::Pallet::::block_number(); + + // Finish current block. + >::on_finalize(current_bn); + as Hooks>::on_finalize(current_bn); + + let current_u64: u64 = current_bn.saturated_into(); + // Use a delta that won’t land on tempo boundaries (tempo is set to 10 in setup_fee_test_subnets). + let next_u64: u64 = current_u64.saturating_add(delta); + let next_bn: BN = next_u64.saturated_into(); + + frame_system::Pallet::::set_block_number(next_bn); + + // Start next block. + as Hooks>::on_initialize(next_bn); + >::on_initialize(next_bn); + }; + + let stake_amount = TaoBalance::from(TAO); + let unstake_amount = AlphaBalance::from(TAO / 50); + + // setup_fee_test_subnets() -> register_ok_neuron() calls SubtensorModule::register(...) + // which now requires sufficient balance to stake during registration. + // setup_fee_test_subnets() uses coldkey=10000 and first neuron hotkey=20001. + let register_prefund = stake_amount + .saturating_mul(10_000.into()) // generous buffer + .saturating_add(ExistentialDeposit::get()); + add_balance_to_coldkey_account(&U256::from(10000), register_prefund); + add_balance_to_coldkey_account(&U256::from(20001), register_prefund); + + let sn = setup_fee_test_subnets(1, 1); + + // Avoid staking-op rate limit between registration and staking. + jump_blocks(1_000_001); + + fund_and_add_stake( + sn.subnets[0].netuid, + &sn.coldkey, + &sn.hotkeys[0], + stake_amount.into(), + ); + add_balance_to_coldkey_account(&sn.coldkey, TaoBalance::from(TAO)); + + // Avoid staking-op rate limit between add_stake and remove_stake. + jump_blocks(1_000_001); + + // Simulate stake removal to get how much TAO should we get for unstaked Alpha + let (expected_unstaked_tao, _swap_fee) = + swap_alpha_to_tao(sn.subnets[0].netuid, unstake_amount); + + // Remove stake + let balance_before = Balances::free_balance(sn.coldkey); + let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { + hotkey: sn.hotkeys[0], + netuid: sn.subnets[0].netuid, + amount_unstaked: unstake_amount, + }); + + // Dispatch the extrinsic with ChargeTransactionPayment extension + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + + // dispatch_transaction() is nested: + // - Outer Result: validation / payment extension checks + // - Inner Result: actual runtime call dispatch result + let inner = ext + .dispatch_transaction(RuntimeOrigin::signed(sn.coldkey).into(), call, &info, 0, 0) + .expect("Expected Ok(_) from dispatch_transaction (validation)"); + assert_ok!(inner); + + let final_balance = Balances::free_balance(sn.coldkey); + let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + + let actual_tao_fee = + balance_before + TaoBalance::from(expected_unstaked_tao) - final_balance; + let actual_alpha_fee = alpha_before - alpha_after - unstake_amount; + + // Remove stake extrinsic should pay fees in TAO because ck has sufficient TAO balance + assert!(actual_tao_fee > 0.into()); + assert_eq!(actual_alpha_fee, AlphaBalance::from(0)); + + let events = System::events(); + assert!(events.iter().any(|event_record| { + matches!( + &event_record.event, + RuntimeEvent::TransactionPayment( + pallet_transaction_payment::Event::TransactionFeePaid { .. } + ) + ) + })); + assert!(!events.iter().any(|event_record| { + matches!( + &event_record.event, + RuntimeEvent::SubtensorModule(SubtensorEvent::TransactionFeePaidWithAlpha { .. }) + ) + })); + }); +} + +// cargo test --package subtensor-transaction-fee --lib -- tests::remove_stake_fees::test_remove_stake_fees_alpha --exact --show-output +#[test] +fn test_remove_stake_fees_alpha() { + new_test_ext().execute_with(|| { + let stake_amount = TAO; + let unstake_amount = AlphaBalance::from(TAO / 50); + let sn = setup_fee_test_subnets(1, 1); + fund_and_add_stake( + sn.subnets[0].netuid, + &sn.coldkey, + &sn.hotkeys[0], + stake_amount, + ); + + // Simulate stake removal to get how much TAO should we get for unstaked Alpha + // after the alpha-fee pre-withdrawal has already moved the pool. + let (expected_unstaked_tao, swap_fee) = quote_remove_stake_after_alpha_fee( + &sn.coldkey, + &sn.hotkeys[0], + sn.subnets[0].netuid, + unstake_amount, + ); + + // Forse-set signer balance to ED + let current_balance = Balances::free_balance(sn.coldkey); + remove_balance_from_coldkey_account( + &sn.coldkey, + current_balance - ExistentialDeposit::get(), + ); + + // Get the block builder balance + let block_builder = U256::from(MOCK_BLOCK_BUILDER); + let block_builder_balance_before = Balances::free_balance(block_builder); + + // Remove stake + let balance_before = Balances::free_balance(sn.coldkey); + let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { + hotkey: sn.hotkeys[0], + netuid: sn.subnets[0].netuid, + amount_unstaked: unstake_amount, + }); + + System::reset_events(); + + // Dispatch the extrinsic with ChargeTransactionPayment extension + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + assert_ok!(ext.dispatch_transaction( + RuntimeOrigin::signed(sn.coldkey).into(), + call, + &info, + 0, + 0, + )); + + let final_balance = Balances::free_balance(sn.coldkey); + let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + + let actual_tao_fee = + balance_before + TaoBalance::from(expected_unstaked_tao) - final_balance; + let actual_alpha_fee = alpha_before - alpha_after - unstake_amount; + + // Remove stake extrinsic should pay fees in Alpha + assert_abs_diff_eq!(actual_tao_fee, 0.into(), epsilon = 10.into()); + assert!(actual_alpha_fee > 0.into()); + + // Assert that swapped TAO from alpha fee goes to block author + let block_builder_fee_portion = 1.; + let expected_block_builder_swap_reward = swap_fee as f64 * block_builder_fee_portion; + let expected_tx_fee = 14000.; // Use very low value (0.000014) for less test flakiness, value before we 10x tx fees + let block_builder_balance_after = Balances::free_balance(block_builder); + let actual_block_builder_reward = + block_builder_balance_after - block_builder_balance_before; + assert!( + u64::from(actual_block_builder_reward) as f64 + >= expected_block_builder_swap_reward + expected_tx_fee + ); + + let events = System::events(); + let alpha_event = events + .iter() + .position(|event_record| { + matches!( + &event_record.event, + RuntimeEvent::SubtensorModule(SubtensorEvent::TransactionFeePaidWithAlpha { + who, + netuid, + alpha_fee, + tao_amount: _, + }) if who == &sn.coldkey && *alpha_fee == actual_alpha_fee && *netuid == sn.subnets[0].netuid + ) + }) + .expect("expected TransactionFeePaidWithAlpha event"); + let tao_event = events + .iter() + .position(|event_record| { + matches!( + &event_record.event, + RuntimeEvent::TransactionPayment( + pallet_transaction_payment::Event::TransactionFeePaid { who, .. } + ) if who == &sn.coldkey + ) + }) + .expect("expected TransactionFeePaid event"); + + assert!( + alpha_event < tao_event, + "expected TransactionFeePaidWithAlpha before TransactionFeePaid" + ); + }); +} + +#[test] +fn test_alpha_fee_withdraw_failure_aborts_and_rolls_back() { + new_test_ext().execute_with(|| { + let stake_amount = TAO; + let unstake_amount = AlphaBalance::from(TAO / 50); + let sn = setup_fee_test_subnets(1, 1); + let netuid = sn.subnets[0].netuid; + let hotkey = sn.hotkeys[0]; + + fund_and_add_stake(netuid, &sn.coldkey, &hotkey, stake_amount); + + let current_balance = Balances::free_balance(sn.coldkey); + remove_balance_from_coldkey_account( + &sn.coldkey, + current_balance - ExistentialDeposit::get(), + ); + + // Force the alpha-fee unstake to fail after AMM bookkeeping by draining + // the subnet account used by transfer_tao_from_subnet. + let subnet_account = SubtensorModule::get_subnet_account_id(netuid).unwrap(); + Balances::make_free_balance_be(&subnet_account, 0.into()); + + let block_builder = U256::from(MOCK_BLOCK_BUILDER); + let block_builder_balance_before = Balances::free_balance(block_builder); + let signer_balance_before = Balances::free_balance(sn.coldkey); + let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &sn.coldkey, + netuid, + ); + let subnet_alpha_in_before = SubnetAlphaIn::::get(netuid); + let subnet_alpha_out_before = SubnetAlphaOut::::get(netuid); + let subnet_tao_before = SubnetTAO::::get(netuid); + let total_stake_before = TotalStake::::get(); + let subnet_volume_before = SubnetVolume::::get(netuid); + + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { + hotkey, + netuid, + amount_unstaked: unstake_amount, + }); + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + + let result = + ext.dispatch_transaction(RuntimeOrigin::signed(sn.coldkey).into(), call, &info, 0, 0); + + assert_eq!( + result.unwrap_err(), + TransactionValidityError::Invalid(InvalidTransaction::Payment) + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &sn.coldkey, + netuid, + ), + alpha_before + ); + assert_eq!(SubnetAlphaIn::::get(netuid), subnet_alpha_in_before); + assert_eq!(SubnetAlphaOut::::get(netuid), subnet_alpha_out_before); + assert_eq!(SubnetTAO::::get(netuid), subnet_tao_before); + assert_eq!(TotalStake::::get(), total_stake_before); + assert_eq!(SubnetVolume::::get(netuid), subnet_volume_before); + assert_eq!(Balances::free_balance(sn.coldkey), signer_balance_before); + assert_eq!( + Balances::free_balance(block_builder), + block_builder_balance_before + ); + + assert!(!System::events().iter().any(|event_record| { + matches!( + &event_record.event, + RuntimeEvent::SubtensorModule(SubtensorEvent::TransactionFeePaidWithAlpha { .. }) + ) + })); + }); +} + +// Test that unstaking on root with no free balance results in charging fees from +// staked amount +// +// cargo test --package subtensor-transaction-fee --lib -- tests::remove_stake_fees::test_remove_stake_root --exact --show-output +#[test] +fn test_remove_stake_root() { + new_test_ext().execute_with(|| { + let stake_amount = TAO; + let unstake_amount = TAO / 10; + let netuid = NetUid::from(0); + let coldkey = U256::from(100000); + let hotkey = U256::from(100001); + + // Root stake + add_network(netuid, 10); + pallet_subtensor::Owner::::insert(hotkey, coldkey); + pallet_subtensor::SubtokenEnabled::::insert(NetUid::from(0), true); + fund_and_add_stake(netuid, &coldkey, &hotkey, stake_amount); + + // Forse-set signer balance to ED + let current_balance = Balances::free_balance(coldkey); + remove_balance_from_coldkey_account(&coldkey, current_balance - ExistentialDeposit::get()); + + // Remove stake + let balance_before = Balances::free_balance(coldkey); + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { + hotkey, + netuid, + amount_unstaked: unstake_amount.into(), + }); + + // Dispatch the extrinsic with ChargeTransactionPayment extension + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + assert_ok!(ext.dispatch_transaction( + RuntimeOrigin::signed(coldkey).into(), + call, + &info, + 0, + 0, + )); + + let final_balance = Balances::free_balance(coldkey); + let alpha_after = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + + let actual_tao_fee = balance_before + unstake_amount.into() - final_balance; + let actual_alpha_fee = + AlphaBalance::from(stake_amount) - alpha_after - unstake_amount.into(); + + // Remove stake extrinsic should pay fees in Alpha (withdrawn from staked TAO) + assert_eq!(actual_tao_fee, 0.into()); + assert!(actual_alpha_fee > 0.into()); + }); +} + +// Test that unstaking 100% of stake on root is possible with no free balance +// +// cargo test --package subtensor-transaction-fee --lib -- tests::remove_stake_fees::test_remove_stake_completely_root --exact --show-output +#[test] +fn test_remove_stake_completely_root() { + new_test_ext().execute_with(|| { + let stake_amount = TAO; + let unstake_amount = TAO; + let netuid = NetUid::from(0); + let coldkey = U256::from(100000); + let hotkey = U256::from(100001); + + // Root stake + add_network(netuid, 10); + pallet_subtensor::Owner::::insert(hotkey, coldkey); + pallet_subtensor::SubtokenEnabled::::insert(NetUid::from(0), true); + fund_and_add_stake(netuid, &coldkey, &hotkey, stake_amount); + + // Forse-set signer balance to ED + let current_balance = Balances::free_balance(coldkey); + remove_balance_from_coldkey_account(&coldkey, current_balance - ExistentialDeposit::get()); + + // Remove stake + let balance_before = Balances::free_balance(coldkey); + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { + hotkey, + netuid, + amount_unstaked: unstake_amount.into(), + }); + + // Dispatch the extrinsic with ChargeTransactionPayment extension + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + assert_ok!(ext.dispatch_transaction( + RuntimeOrigin::signed(coldkey).into(), + call, + &info, + 0, + 0, + )); + + let final_balance = Balances::free_balance(coldkey); + let alpha_after = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + + assert_eq!(alpha_after, 0.into()); + assert!(final_balance > balance_before); + }); +} + +// cargo test --package subtensor-transaction-fee --lib -- tests::remove_stake_fees::test_remove_stake_completely_fees_alpha --exact --show-output +#[test] +fn test_remove_stake_completely_fees_alpha() { + new_test_ext().execute_with(|| { + let stake_amount = TAO; + let sn = setup_fee_test_subnets(1, 1); + fund_and_add_stake( + sn.subnets[0].netuid, + &sn.coldkey, + &sn.hotkeys[0], + stake_amount, + ); + + // Simulate stake removal to get how much TAO should we get for unstaked Alpha + let unstake_amount = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + let (expected_unstaked_tao, _swap_fee) = + swap_alpha_to_tao(sn.subnets[0].netuid, unstake_amount); + + // Forse-set signer balance to ED + let current_balance = Balances::free_balance(sn.coldkey); + remove_balance_from_coldkey_account( + &sn.coldkey, + current_balance - ExistentialDeposit::get(), + ); + + // Remove stake + let balance_before = Balances::free_balance(sn.coldkey); + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { + hotkey: sn.hotkeys[0], + netuid: sn.subnets[0].netuid, + amount_unstaked: unstake_amount, + }); + + // Dispatch the extrinsic with ChargeTransactionPayment extension + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + assert_ok!(ext.dispatch_transaction( + RuntimeOrigin::signed(sn.coldkey).into(), + call, + &info, + 0, + 0, + )); + + let final_balance = Balances::free_balance(sn.coldkey); + let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + + // Effectively, the fee is paid in TAO in this case because user receives less TAO, + // and all Alpha is gone, and it is not measurable in Alpha + let actual_fee = balance_before + expected_unstaked_tao.into() - final_balance; + assert_eq!(alpha_after, 0.into()); + assert!(actual_fee > 0.into()); + }); +} + +// Validation should fail if both TAO and Alpha balance are lower than tx fees, +// so that transaction is not included in the block +#[test] +fn test_remove_stake_not_enough_balance_for_fees() { + new_test_ext().execute_with(|| { + let stake_amount = TaoBalance::from(TAO); + let sn = setup_fee_test_subnets(1, 1); + + add_balance_to_coldkey_account( + &sn.coldkey, + stake_amount + .saturating_mul(2.into()) // buffer so staking doesn't attempt to drain the account + .saturating_add(ExistentialDeposit::get()), + ); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(sn.coldkey), + sn.hotkeys[0], + sn.subnets[0].netuid, + stake_amount.into(), + )); + + // Simulate stake removal to get how much TAO should we get for unstaked Alpha + let current_stake = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + + // Forse-set signer balance to ED + let current_balance = Balances::free_balance(sn.coldkey); + remove_balance_from_coldkey_account( + &sn.coldkey, + current_balance - ExistentialDeposit::get(), + ); + + // For-set Alpha balance to low + let new_current_stake = AlphaBalance::from(1_000); + SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + current_stake - new_current_stake, + ); + + // Remove stake + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { + hotkey: sn.hotkeys[0], + netuid: sn.subnets[0].netuid, + amount_unstaked: new_current_stake, + }); + + // Dispatch the extrinsic with ChargeTransactionPayment extension + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + let result = ext.validate( + RuntimeOrigin::signed(sn.coldkey).into(), + &call.clone(), + &info, + 10, + (), + &TxBaseImplication(()), + TransactionSource::External, + ); + + assert_eq!( + result.unwrap_err(), + TransactionValidityError::Invalid(InvalidTransaction::Payment) + ); + }); +} + +// No TAO balance, Alpha fees. If Alpha price is high, it is enough to pay fees, but when Alpha price +// is low, the validation fails +// +// cargo test --package subtensor-transaction-fee --lib -- tests::remove_stake_fees::test_remove_stake_edge_alpha --exact --show-output +#[test] +fn test_remove_stake_edge_alpha() { + new_test_ext().execute_with(|| { + let stake_amount = TAO; + let sn = setup_fee_test_subnets(1, 1); + fund_and_add_stake( + sn.subnets[0].netuid, + &sn.coldkey, + &sn.hotkeys[0], + stake_amount, + ); + + // Simulate stake removal to get how much TAO should we get for unstaked Alpha + let current_stake = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + + // Forse-set signer balance to ED + let current_balance = Balances::free_balance(sn.coldkey); + remove_balance_from_coldkey_account( + &sn.coldkey, + current_balance - ExistentialDeposit::get(), + ); + + // For-set Alpha balance to low, but enough to pay tx fees at the current Alpha price + let new_current_stake = AlphaBalance::from(2_000_000); + SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + current_stake - new_current_stake, + ); + + // Remove stake + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { + hotkey: sn.hotkeys[0], + netuid: sn.subnets[0].netuid, + amount_unstaked: new_current_stake, + }); + + // Dispatch the extrinsic with ChargeTransactionPayment extension + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + let result = ext.validate( + RuntimeOrigin::signed(sn.coldkey).into(), + &call.clone(), + &info, + 10, + (), + &TxBaseImplication(()), + TransactionSource::External, + ); + + // Ok - Validation passed + assert_ok!(result); + + // Lower Alpha price to 0.0001 so that there is not enough alpha to cover tx fees + SubnetTAO::::insert(sn.subnets[0].netuid, TaoBalance::from(1_000_000)); + SubnetAlphaIn::::insert(sn.subnets[0].netuid, AlphaBalance::from(10_000_000_000_u64)); + + let result_low_alpha_price = ext.validate( + RuntimeOrigin::signed(sn.coldkey).into(), + &call.clone(), + &info, + 10, + (), + &TxBaseImplication(()), + TransactionSource::External, + ); + assert_eq!( + result_low_alpha_price.unwrap_err(), + TransactionValidityError::Invalid(InvalidTransaction::Payment) + ); + }); +} + +// Validation passes, but transaction fails => TAO fees are paid +// +// cargo test --package subtensor-transaction-fee --lib -- tests::remove_stake_fees::test_remove_stake_failing_transaction_tao_fees --exact --show-output +#[test] +fn test_remove_stake_failing_transaction_tao_fees() { + new_test_ext().execute_with(|| { + let stake_amount = TaoBalance::from(TAO); + let unstake_amount = AlphaBalance::from(TAO / 50); + let sn = setup_fee_test_subnets(1, 1); + + add_balance_to_coldkey_account( + &sn.coldkey, + stake_amount + .saturating_mul(2.into()) // buffer so staking doesn't attempt to drain the account + .saturating_add(ExistentialDeposit::get()), + ); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(sn.coldkey), + sn.hotkeys[0], + sn.subnets[0].netuid, + stake_amount.into(), + )); + + add_balance_to_coldkey_account(&sn.coldkey, TAO.into()); + + // Make unstaking fail by reducing liquidity to critical + SubnetTAO::::insert(sn.subnets[0].netuid, TaoBalance::from(1)); + + // Remove stake + let balance_before = Balances::free_balance(sn.coldkey); + let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { + hotkey: sn.hotkeys[0], + netuid: sn.subnets[0].netuid, + amount_unstaked: unstake_amount, + }); + + // Dispatch the extrinsic with ChargeTransactionPayment extension + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + assert_ok!(ext.dispatch_transaction( + RuntimeOrigin::signed(sn.coldkey).into(), + call, + &info, + 0, + 0, + )); + + let final_balance = Balances::free_balance(sn.coldkey); + let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + + let actual_tao_fee = balance_before - final_balance; + + // Remove stake extrinsic should pay fees in TAO because ck has sufficient TAO balance + assert!(actual_tao_fee > 0.into()); + assert_eq!(alpha_before, alpha_after); + }); +} + +// Validation passes, but transaction fails (artificially disable subtoken) => +// Alpha fees are still paid +// +// cargo test --package subtensor-transaction-fee --lib -- tests::remove_stake_fees::test_remove_stake_failing_transaction_alpha_fees --exact --show-output +#[test] +fn test_remove_stake_failing_transaction_alpha_fees() { + new_test_ext().execute_with(|| { + let stake_amount = TAO; + let unstake_amount = AlphaBalance::from(TAO / 50); + let sn = setup_fee_test_subnets(1, 1); + fund_and_add_stake( + sn.subnets[0].netuid, + &sn.coldkey, + &sn.hotkeys[0], + stake_amount, + ); + + // Provide adequate TAO reserve so that sim swap works ok in validation + SubnetTAO::::insert(sn.subnets[0].netuid, TaoBalance::from(1_000_000_000_u64)); + + // Provide Alpha reserve so that price is about 1.0 + SubnetAlphaIn::::insert(sn.subnets[0].netuid, AlphaBalance::from(1_000_000_000_u64)); + + // Forse-set signer balance to ED + let current_balance = Balances::free_balance(sn.coldkey); + remove_balance_from_coldkey_account( + &sn.coldkey, + current_balance - ExistentialDeposit::get(), + ); + + // Disable subtoken so that removing stake tx fails (still allows the validation to pass) + pallet_subtensor::SubtokenEnabled::::insert(sn.subnets[0].netuid, false); + + // Remove stake + let balance_before = Balances::free_balance(sn.coldkey); + let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { + hotkey: sn.hotkeys[0], + netuid: sn.subnets[0].netuid, + amount_unstaked: alpha_before, + }); + + // Dispatch the extrinsic with ChargeTransactionPayment extension + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + assert_ok!(ext.dispatch_transaction( + RuntimeOrigin::signed(sn.coldkey).into(), + call, + &info, + 0, + 0, + )); + + let final_balance = Balances::free_balance(sn.coldkey); + let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + + let actual_tao_fee = balance_before - final_balance; + let actual_alpha_fee = alpha_before - alpha_after; + + // Remove stake extrinsic should pay fees in Alpha + assert_eq!(actual_tao_fee, 0.into()); + assert!(actual_alpha_fee > 0.into()); + assert!(actual_alpha_fee < unstake_amount); + }); +} + +// cargo test --package subtensor-transaction-fee --lib -- tests::remove_stake_fees::test_remove_stake_limit_fees_alpha --exact --show-output +#[test] +fn test_remove_stake_limit_fees_alpha() { + new_test_ext().execute_with(|| { + let stake_amount = TAO; + let unstake_amount = AlphaBalance::from(TAO / 50); + let sn = setup_fee_test_subnets(1, 1); + fund_and_add_stake( + sn.subnets[0].netuid, + &sn.coldkey, + &sn.hotkeys[0], + stake_amount, + ); + + // Forse-set signer balance to ED + let current_balance = Balances::free_balance(sn.coldkey); + remove_balance_from_coldkey_account( + &sn.coldkey, + current_balance - ExistentialDeposit::get(), + ); + + // Remove stake limit + let balance_before = Balances::free_balance(sn.coldkey); + let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake_limit { + hotkey: sn.hotkeys[0], + netuid: sn.subnets[0].netuid, + amount_unstaked: unstake_amount, + limit_price: 1_000.into(), + allow_partial: false, + }); + + System::reset_events(); + + // Dispatch the extrinsic with ChargeTransactionPayment extension + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + assert_ok!(ext.dispatch_transaction( + RuntimeOrigin::signed(sn.coldkey).into(), + call, + &info, + 0, + 0, + )); + + let final_balance = Balances::free_balance(sn.coldkey); + let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + + let expected_unstaked_tao = System::events() + .iter() + .rev() + .find_map(|event_record| match &event_record.event { + RuntimeEvent::SubtensorModule(SubtensorEvent::StakeRemoved( + coldkey, + hotkey, + tao_amount, + alpha_amount, + netuid, + fee_paid, + )) if coldkey == &sn.coldkey + && hotkey == &sn.hotkeys[0] + && *netuid == sn.subnets[0].netuid + && (*alpha_amount + AlphaBalance::from(*fee_paid) == unstake_amount) => + { + Some(*tao_amount) + } + _ => None, + }) + .expect("expected StakeRemoved event for remove_stake_limit"); + + let actual_tao_fee = balance_before + expected_unstaked_tao - final_balance; + let actual_alpha_fee = alpha_before - alpha_after - unstake_amount; + + // Remove stake extrinsic should pay fees in Alpha + assert_abs_diff_eq!(actual_tao_fee, 0.into(), epsilon = 100.into()); + assert!(actual_alpha_fee > 0.into()); + }); +} diff --git a/pallets/transaction-fee/src/tests/swap_hotkey_fees.rs b/pallets/transaction-fee/src/tests/swap_hotkey_fees.rs new file mode 100644 index 0000000000..06baa349cb --- /dev/null +++ b/pallets/transaction-fee/src/tests/swap_hotkey_fees.rs @@ -0,0 +1,69 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +use super::mock::*; +use crate::{SubtensorTxFeeHandler, TransactionFeeHandler}; + +// cargo test --package subtensor-transaction-fee --lib -- tests::swap_hotkey_fees::test_swap_hotkey_fees_alpha --exact --show-output +#[test] +fn test_swap_hotkey_fees_alpha() { + new_test_ext().execute_with(|| { + let sn = setup_fee_test_subnets(2, 2); + let stake_amount = TAO; + fund_and_add_stake( + sn.subnets[0].netuid, + &sn.coldkey, + &sn.hotkeys[0], + stake_amount, + ); + fund_and_add_stake( + sn.subnets[1].netuid, + &sn.coldkey, + &sn.hotkeys[0], + stake_amount, + ); + + // swap_hotkey and swap_hotkey_v2 move alpha stake off the origin hotkey, + // so their fees must be eligible to be paid in alpha on every subnet that + // hotkey has stake. Before the fix `fees_in_alpha` returned an empty vec + // for these calls, forcing a TAO fee (and rejecting alpha-only callers). + + // netuid = None -> every subnet the origin hotkey has stake on (2 here). + let call_all = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { + hotkey: sn.hotkeys[0], + new_hotkey: sn.hotkeys[1], + netuid: None, + }); + let alpha_vec_all = + SubtensorTxFeeHandler::>::fees_in_alpha::( + &sn.coldkey, + &call_all, + ); + assert_eq!(alpha_vec_all.len(), 2); + + // netuid = Some(single) -> only that subnet. + let call_one = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey { + hotkey: sn.hotkeys[0], + new_hotkey: sn.hotkeys[1], + netuid: Some(sn.subnets[0].netuid), + }); + let alpha_vec_one = + SubtensorTxFeeHandler::>::fees_in_alpha::( + &sn.coldkey, + &call_one, + ); + assert_eq!(alpha_vec_one.len(), 1); + + // swap_hotkey_v2 moves the same alpha and must be eligible too. + let call_v2 = RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_hotkey_v2 { + hotkey: sn.hotkeys[0], + new_hotkey: sn.hotkeys[1], + netuid: None, + keep_stake: false, + }); + let alpha_vec_v2 = + SubtensorTxFeeHandler::>::fees_in_alpha::( + &sn.coldkey, + &call_v2, + ); + assert_eq!(alpha_vec_v2.len(), 2); + }); +} diff --git a/pallets/transaction-fee/src/tests/unstake_all_fees.rs b/pallets/transaction-fee/src/tests/unstake_all_fees.rs new file mode 100644 index 0000000000..31f057b781 --- /dev/null +++ b/pallets/transaction-fee/src/tests/unstake_all_fees.rs @@ -0,0 +1,242 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] +use super::mock::*; +use crate::{AlphaFeeHandler, SubtensorTxFeeHandler, TransactionFeeHandler}; +use frame_support::dispatch::GetDispatchInfo; +use frame_support::{assert_err, assert_ok}; +use sp_runtime::{ + traits::DispatchTransaction, + transaction_validity::{InvalidTransaction, TransactionValidityError}, +}; + +// cargo test --package subtensor-transaction-fee --lib -- tests::unstake_all_fees::test_rejects_multi_subnet_alpha_fee_deduction --exact --show-output +#[test] +fn test_rejects_multi_subnet_alpha_fee_deduction() { + new_test_ext().execute_with(|| { + let sn = setup_fee_test_subnets(2, 1); + let stake_amount = TAO; + fund_and_add_stake( + sn.subnets[0].netuid, + &sn.coldkey, + &sn.hotkeys[0], + stake_amount, + ); + fund_and_add_stake( + sn.subnets[1].netuid, + &sn.coldkey, + &sn.hotkeys[0], + stake_amount, + ); + + let alpha_before_0 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + let alpha_before_1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[1].netuid, + ); + + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::unstake_all { + hotkey: sn.hotkeys[0], + }); + let alpha_vec = + SubtensorTxFeeHandler::>::fees_in_alpha::( + &sn.coldkey, + &call, + ); + assert_eq!(alpha_vec.len(), 2); + + assert!( + ! as AlphaFeeHandler>::can_withdraw_in_alpha( + &sn.coldkey, + &alpha_vec, + 1.into(), + ) + ); + assert_eq!( + as AlphaFeeHandler>::withdraw_in_alpha( + &sn.coldkey, + &alpha_vec, + 1.into(), + ), + Ok((0.into(), 0.into(), NetUid::ROOT)) + ); + + let alpha_after_0 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[0].netuid, + ); + let alpha_after_1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[1].netuid, + ); + + assert_eq!(alpha_before_0, alpha_after_0); + assert_eq!(alpha_before_1, alpha_after_1); + }); +} + +// cargo test --package subtensor-transaction-fee --lib -- tests::unstake_all_fees::test_unstake_all_fees_alpha --exact --show-output +#[test] +fn test_unstake_all_fees_alpha() { + new_test_ext().execute_with(|| { + let stake_amount = TAO; + let sn = setup_fee_test_subnets(10, 1); + let coldkey = U256::from(100000); + for i in 0..10 { + fund_and_add_stake(sn.subnets[i].netuid, &coldkey, &sn.hotkeys[0], stake_amount); + } + + // Root stake + add_network(NetUid::from(0), 10); + pallet_subtensor::SubtokenEnabled::::insert(NetUid::from(0), true); + fund_and_add_stake(0.into(), &coldkey, &sn.hotkeys[0], stake_amount); + + // Simulate stake removal to get how much TAO should we get for unstaked Alpha + let mut expected_unstaked_tao = 0; + for i in 0..10 { + let unstake_amount = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &coldkey, + sn.subnets[i].netuid, + ); + + let (tao, _swap_fee) = swap_alpha_to_tao(sn.subnets[i].netuid, unstake_amount); + expected_unstaked_tao += tao; + } + + // Forse-set signer balance to ED + let current_balance = Balances::free_balance(coldkey); + remove_balance_from_coldkey_account(&coldkey, current_balance - ExistentialDeposit::get()); + + // Unstake all + let balance_before = Balances::free_balance(sn.coldkey); + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::unstake_all { + hotkey: sn.hotkeys[0], + }); + + // Dispatch the extrinsic with ChargeTransactionPayment extension + // Get invalid payment because we cannot pay fees in multiple alphas + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + assert_err!( + ext.clone().dispatch_transaction( + RuntimeOrigin::signed(coldkey).into(), + call.clone(), + &info, + 0, + 0, + ), + TransactionValidityError::Invalid(InvalidTransaction::Payment), + ); + + // Give the coldkey TAO balance - now should unstake ok + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_u64.into()); + assert_ok!(ext.dispatch_transaction( + RuntimeOrigin::signed(coldkey).into(), + call, + &info, + 0, + 0, + )); + + let final_balance = Balances::free_balance(sn.coldkey); + + // Effectively, the fee is paid in TAO in this case because user receives less TAO, + // and all Alpha is gone, and it is not measurable in Alpha + let actual_fee = balance_before + expected_unstaked_tao.into() - final_balance; + assert!(actual_fee > 0.into()); + + // Check that all subnets got unstaked + for i in 0..10 { + let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[i].netuid, + ); + assert_eq!(alpha_after, 0.into()); + } + }); +} + +// cargo test --package subtensor-transaction-fee --lib -- tests::unstake_all_fees::test_unstake_all_alpha_fees_alpha --exact --show-output +#[test] +fn test_unstake_all_alpha_fees_alpha() { + new_test_ext().execute_with(|| { + let stake_amount = TAO; + let sn = setup_fee_test_subnets(10, 1); + let coldkey = U256::from(100000); + for i in 0..10 { + fund_and_add_stake(sn.subnets[i].netuid, &coldkey, &sn.hotkeys[0], stake_amount); + } + + // Simulate stake removal to get how much TAO should we get for unstaked Alpha + let mut expected_unstaked_tao = 0; + for i in 0..10 { + let unstake_amount = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &coldkey, + sn.subnets[i].netuid, + ); + + let (tao, _swap_fee) = swap_alpha_to_tao(sn.subnets[i].netuid, unstake_amount); + expected_unstaked_tao += tao; + } + + // Forse-set signer balance to ED + let current_balance = Balances::free_balance(coldkey); + remove_balance_from_coldkey_account(&coldkey, current_balance - ExistentialDeposit::get()); + + // Unstake all + let balance_before = Balances::free_balance(sn.coldkey); + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::unstake_all_alpha { + hotkey: sn.hotkeys[0], + }); + + // Dispatch the extrinsic with ChargeTransactionPayment extension + // Get invalid payment because we cannot pay fees in multiple alphas + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()); + assert_err!( + ext.clone().dispatch_transaction( + RuntimeOrigin::signed(coldkey).into(), + call.clone(), + &info, + 0, + 0, + ), + TransactionValidityError::Invalid(InvalidTransaction::Payment), + ); + + // Give the coldkey TAO balance - now should unstake ok + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_u64.into()); + assert_ok!(ext.dispatch_transaction( + RuntimeOrigin::signed(coldkey).into(), + call, + &info, + 0, + 0, + )); + + let final_balance = Balances::free_balance(sn.coldkey); + + // Effectively, the fee is paid in TAO in this case because user receives less TAO, + // and all Alpha is gone, and it is not measurable in Alpha + let actual_fee = balance_before + expected_unstaked_tao.into() - final_balance; + assert!(actual_fee > 0.into()); + + // Check that all subnets got unstaked + for i in 0..10 { + let alpha_after = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &sn.hotkeys[0], + &sn.coldkey, + sn.subnets[i].netuid, + ); + assert_eq!(alpha_after, 0.into()); + } + }); +} diff --git a/pallets/utility/src/benchmarking.rs b/pallets/utility/src/benchmarking.rs index a9950c2eb2..38ea6978a2 100644 --- a/pallets/utility/src/benchmarking.rs +++ b/pallets/utility/src/benchmarking.rs @@ -15,7 +15,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Benchmarks for Utility Pallet +//! Runtime benchmarks for `pallet-subtensor-utility` dispatchables. #![cfg(feature = "runtime-benchmarks")] @@ -27,15 +27,14 @@ use crate::*; const SEED: u32 = 0; -fn assert_last_event( +fn assert_last_utility_event( generic_event: ::RuntimeEvent, ) { frame_system::Pallet::::assert_last_event(generic_event.into()); } -fn benchmark_batch_calls( - count: u32, -) -> alloc::vec::Vec<::RuntimeCall> +/// Build `count` empty `system.remark` calls for batch-style extrinsic benchmarks. +fn remark_batch_calls(count: u32) -> alloc::vec::Vec<::RuntimeCall> where T: crate::pallet::Config, ::RuntimeCall: From>, @@ -58,13 +57,13 @@ mod benchmark { #[benchmark] fn batch(c: Linear<0, 1000>) { - let calls = benchmark_batch_calls::(c); + let calls = remark_batch_calls::(c); let caller = whitelisted_caller(); #[extrinsic_call] _(RawOrigin::Signed(caller), calls); - assert_last_event::(Event::BatchCompleted.into()); + assert_last_utility_event::(Event::BatchCompleted.into()); } #[benchmark] @@ -80,7 +79,7 @@ mod benchmark { } #[benchmark] fn batch_all(c: Linear<0, 1000>) { - let calls = benchmark_batch_calls::(c); + let calls = remark_batch_calls::(c); let caller = whitelisted_caller(); frame_system::Pallet::::reset_events(); @@ -88,7 +87,7 @@ mod benchmark { #[extrinsic_call] _(RawOrigin::Signed(caller), calls); - assert_last_event::(Event::BatchCompleted.into()); + assert_last_utility_event::(Event::BatchCompleted.into()); } #[benchmark] @@ -105,13 +104,13 @@ mod benchmark { #[benchmark] fn force_batch(c: Linear<0, 1000>) { - let calls = benchmark_batch_calls::(c); + let calls = remark_batch_calls::(c); let caller = whitelisted_caller(); #[extrinsic_call] _(RawOrigin::Signed(caller), calls); - assert_last_event::(Event::BatchCompleted.into()); + assert_last_utility_event::(Event::BatchCompleted.into()); } #[benchmark] diff --git a/pallets/utility/src/lib.rs b/pallets/utility/src/lib.rs index 115a2699fc..75cbb9f94f 100644 --- a/pallets/utility/src/lib.rs +++ b/pallets/utility/src/lib.rs @@ -15,26 +15,33 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! # Utility Pallet -//! A stateless pallet with helpers for dispatch management which does no re-authentication. +//! # Utility Pallet (`pallet-subtensor-utility`) +//! +//! Stateless helpers for **batch dispatch**, **derivative (pseudonym) dispatch**, and +//! **origin-switched dispatch**. This pallet does **not** re-authenticate; it reuses the caller's +//! origin filters (except where root bypasses them). +//! +//! Subtensor fork notes (search: `with_weight`, `Normal`): +//! - [`Call::with_weight`] always reports [`frame_support::dispatch::DispatchClass::Normal`] +//! (upstream FRAME may use Operational for the same extrinsic). +//! - Derivative account IDs are derived via blake2 of `("modlpy/utilisuba", who, index)` — see +//! [`Pallet::derivative_account_id`]. //! //! - [`Config`] //! - [`Call`] +//! - [`Event`] +//! - [`Error`] //! //! ## Overview //! -//! This pallet contains two basic pieces of functionality: -//! - Batch dispatch: A stateless operation, allowing any origin to execute multiple calls in a -//! single dispatch. This can be useful to amalgamate proposals, combining `set_code` with -//! corresponding `set_storage`s, for efficient multiple payouts with just a single signature -//! verify, or in combination with one of the other two dispatch functionality. -//! - Pseudonymal dispatch: A stateless operation, allowing a signed origin to execute a call from -//! an alternative signed origin. Each account has 2 * 2**16 possible "pseudonyms" (alternative -//! account IDs) and these can be stacked. This can be useful as a key management tool, where you -//! need multiple distinct accounts (e.g. as controllers for many staking accounts), but where -//! it's perfectly fine to have each of them controlled by the same underlying keypair. Derivative -//! accounts are, for the purposes of proxy filtering considered exactly the same as the origin -//! and are thus hampered with the origin's filters. +//! - **Batch dispatch** (`batch`, `batch_all`, `force_batch`): run many calls under one signature. +//! - `batch`: stop on first error (`BatchInterrupted`), prior calls stay applied. +//! - `batch_all`: atomic — any error rolls the whole extrinsic back; nested `batch_all` is filtered. +//! - `force_batch`: never interrupt; emits `ItemFailed` / `BatchCompletedWithErrors` as needed. +//! - **Pseudonymal dispatch** (`as_derivative`): signed origin executes as a derived account ID. +//! Proxy filters treat the derivative as the original origin. +//! - **Origin switch** (`dispatch_as`, `dispatch_as_fallible`, `with_weight`, `if_else`): root (or +//! filtered signed for `if_else`) helpers for privileged or fallback dispatch. //! //! Since proxy filters are respected in all dispatches of this pallet, it should never need to be //! filtered by any proxy. @@ -43,11 +50,16 @@ //! //! ### Dispatchable Functions //! -//! #### For batch dispatch -//! * `batch` - Dispatch multiple calls from the sender's origin. -//! -//! #### For pseudonymal dispatch -//! * `as_derivative` - Dispatch a call from a derivative signed origin. +//! | Call | Role | +//! |------|------| +//! | `batch` | Fail-fast multi-call | +//! | `batch_all` | Atomic multi-call | +//! | `force_batch` | Continue-on-error multi-call | +//! | `as_derivative` | Dispatch as indexed derivative account | +//! | `dispatch_as` | Root: dispatch as `PalletsOrigin` (errors become events) | +//! | `dispatch_as_fallible` | Root: same, but forwards inner error | +//! | `with_weight` | Root: dispatch with caller-supplied weight witness | +//! | `if_else` | Main call, else fallback |) // Ensure we're `no_std` when compiling for Wasm. #![cfg_attr(not(feature = "std"), no_std)] @@ -86,10 +98,10 @@ pub mod pallet { #[pallet::pallet] pub struct Pallet(_); - /// Configuration trait. + /// Configuration trait for the utility pallet (batch / derivative / dispatch-as helpers). #[pallet::config] pub trait Config: frame_system::Config { - /// The overarching call type. + /// Runtime call type that utility may nest and dispatch (must include this pallet's `Call`). type RuntimeCall: Parameter + Dispatchable + GetDispatchInfo @@ -98,7 +110,7 @@ pub mod pallet { + IsSubType> + IsType<::RuntimeCall>; - /// The caller origin, overarching type of all pallets origins. + /// Outer origin caller type used by [`Call::dispatch_as`] / [`Call::dispatch_as_fallible`]. type PalletsOrigin: Parameter + Into<::RuntimeOrigin> + IsType<<::RuntimeOrigin as frame_support::traits::OriginTrait>::PalletsOrigin>; @@ -107,25 +119,27 @@ pub mod pallet { type WeightInfo: WeightInfo; } + /// Events emitted by batch, dispatch-as, and if-else helpers. + /// + /// Variant **order is frozen** (SCALE / metadata); do not reorder. #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { - /// Batch of dispatches did not complete fully. Index of first failing dispatch given, as - /// well as the error. + /// `batch` stopped early: `index` is the first failing call; prior calls remain applied. BatchInterrupted { index: u32, error: DispatchError }, - /// Batch of dispatches completed fully with no error. + /// All items in a `batch` / `batch_all` / `force_batch` succeeded. BatchCompleted, - /// Batch of dispatches completed but has errors. + /// `force_batch` finished with at least one `ItemFailed`. BatchCompletedWithErrors, - /// A single item within a Batch of dispatches has completed with no error. + /// One nested call inside a batch succeeded. ItemCompleted, - /// A single item within a Batch of dispatches has completed with error. + /// One nested call inside `force_batch` failed; batch continued. ItemFailed { error: DispatchError }, - /// A call was dispatched. + /// Result of [`Call::dispatch_as`] / successful [`Call::dispatch_as_fallible`]. DispatchedAs { result: DispatchResult }, - /// Main call was dispatched. + /// [`Call::if_else`] main path succeeded; fallback was not run. IfElseMainSuccess, - /// The fallback call was dispatched. + /// [`Call::if_else`] main failed and fallback was dispatched (`main_error` preserved). IfElseFallbackCalled { main_error: DispatchError }, } @@ -137,7 +151,7 @@ pub mod pallet { #[pallet::extra_constants] impl Pallet { - /// The limit on the number of batched calls. + /// Max nested calls allowed in `batch` / `batch_all` / `force_batch` (allocation-safe). fn batched_calls_limit() -> u32 { let allocator_limit = sp_core::MAX_POSSIBLE_ALLOCATION; let call_size = (core::mem::size_of::<::RuntimeCall>() as u32) @@ -163,11 +177,14 @@ pub mod pallet { } } + /// Dispatch errors for the utility pallet. + /// + /// Variant **order is frozen** (SCALE / metadata); do not reorder. #[pallet::error] pub enum Error { - /// Too many calls batched. + /// `calls.len()` exceeded [`Pallet::batched_calls_limit`]. TooManyCalls, - /// Bad input data for derived account ID + /// [`Pallet::derivative_account_id`] could not decode the blake2 entropy into `AccountId`. InvalidDerivedAccount, } @@ -175,27 +192,18 @@ pub mod pallet { impl Pallet { #![deny(clippy::expect_used)] - /// Send a batch of dispatch calls. + /// Fail-fast batch: dispatch `calls` from the same origin; stop on first error. /// - /// May be called from any origin except `None`. + /// May be called from any origin except `None`. Root bypasses origin / base call filters. /// - /// - `calls`: The calls to be dispatched from the same origin. The number of call must not - /// exceed the constant: `batched_calls_limit` (available in constant metadata). + /// Always returns `Ok`; inspect events for outcome: + /// - [`Event::BatchCompleted`] — all items succeeded + /// - [`Event::BatchInterrupted`] — first failure at `index` (prior items stay applied) /// - /// If origin is root then the calls are dispatched without checking origin filter. (This - /// includes bypassing `frame_system::Config::BaseCallFilter`). - /// - /// ## Complexity - /// - O(C) where C is the number of calls to be batched. - /// - /// This will return `Ok` in all circumstances. To determine the success of the batch, an - /// event is deposited. If a call failed and the batch was interrupted, then the - /// `BatchInterrupted` event is deposited, along with the number of successful calls made - /// and the error of the failed call. If all were successful, then the `BatchCompleted` - /// event is deposited. + /// Caps at `batched_calls_limit`. Weight is base + sum of actual inner weights. #[pallet::call_index(0)] #[pallet::weight({ - let (dispatch_weight, pays) = Pallet::::weight_and_dispatch_class(calls); + let (dispatch_weight, pays) = Pallet::::batch_calls_weight_and_pays(calls); let dispatch_weight = dispatch_weight.saturating_add(T::WeightInfo::batch(calls.len() as u32)); (dispatch_weight, DispatchClass::Normal, pays) })] @@ -244,19 +252,13 @@ pub mod pallet { Ok(Some(base_weight.saturating_add(weight)).into()) } - /// Send a call through an indexed pseudonym of the sender. - /// - /// Filter from origin are passed along. The call will be dispatched with an origin which - /// use the same filter as the origin of this call. + /// Dispatch `call` as the signed origin's derivative account at `index`. /// - /// NOTE: If you need to ensure that any account-based filtering is not honored (i.e. - /// because you expect `proxy` to have been used prior in the call stack and you do not want - /// the call restrictions to apply to any sub-accounts), then use `as_multi_threshold_1` - /// in the Multisig pallet instead. + /// Origin must be **Signed**. Origin filters are preserved on the derivative caller + /// (proxy filtering treats derivative ≡ original). See [`Pallet::derivative_account_id`]. /// - /// NOTE: Prior to version *12, this was called `as_limited_sub`. - /// - /// The dispatch origin for this call must be _Signed_. + /// NOTE: To bypass account-based filtering after `proxy`, prefer Multisig + /// `as_multi_threshold_1` instead. Historically named `as_limited_sub` (pre v12). #[pallet::call_index(1)] #[pallet::weight({ let dispatch_info = call.get_dispatch_info(); @@ -292,22 +294,14 @@ pub mod pallet { .map(|_| Some(weight).into()) } - /// Send a batch of dispatch calls and atomically execute them. - /// The whole transaction will rollback and fail if any of the calls failed. - /// - /// May be called from any origin except `None`. - /// - /// - `calls`: The calls to be dispatched from the same origin. The number of call must not - /// exceed the constant: `batched_calls_limit` (available in constant metadata). + /// Atomic batch: dispatch all `calls` or roll the extrinsic back on any failure. /// - /// If origin is root then the calls are dispatched without checking origin filter. (This - /// includes bypassing `frame_system::Config::BaseCallFilter`). - /// - /// ## Complexity - /// - O(C) where C is the number of calls to be batched. + /// May be called from any origin except `None`. Root bypasses filters. Nested + /// [`Call::batch_all`] is rejected via an added origin filter (anti-reentrancy). + /// Caps at `batched_calls_limit`. #[pallet::call_index(2)] #[pallet::weight({ - let (dispatch_weight, pays) = Pallet::::weight_and_dispatch_class(calls); + let (dispatch_weight, pays) = Pallet::::batch_calls_weight_and_pays(calls); let dispatch_weight = dispatch_weight.saturating_add(T::WeightInfo::batch_all(calls.len() as u32)); (dispatch_weight, DispatchClass::Normal, pays) })] @@ -361,12 +355,10 @@ pub mod pallet { Ok(Some(base_weight.saturating_add(weight)).into()) } - /// Dispatches a function call with a provided origin. - /// - /// The dispatch origin for this call must be _Root_. + /// Root-only: dispatch `call` under `as_origin`, recording the result in [`Event::DispatchedAs`]. /// - /// ## Complexity - /// - O(1). + /// Does **not** return the inner call's error (use [`Call::dispatch_as_fallible`] for that). + /// Bypasses origin filters via `dispatch_bypass_filter`. #[pallet::call_index(3)] #[pallet::weight({ let dispatch_info = call.get_dispatch_info(); @@ -391,22 +383,14 @@ pub mod pallet { Ok(()) } - /// Send a batch of dispatch calls. - /// Unlike `batch`, it allows errors and won't interrupt. + /// Continue-on-error batch: run every call; never abort the outer extrinsic on item failure. /// - /// May be called from any origin except `None`. - /// - /// - `calls`: The calls to be dispatched from the same origin. The number of call must not - /// exceed the constant: `batched_calls_limit` (available in constant metadata). - /// - /// If origin is root then the calls are dispatch without checking origin filter. (This - /// includes bypassing `frame_system::Config::BaseCallFilter`). - /// - /// ## Complexity - /// - O(C) where C is the number of calls to be batched. + /// May be called from any origin except `None`. Root bypasses filters. + /// Emits [`Event::ItemFailed`] / [`Event::ItemCompleted`] per item, then + /// [`Event::BatchCompleted`] or [`Event::BatchCompletedWithErrors`]. #[pallet::call_index(4)] #[pallet::weight({ - let (dispatch_weight, pays) = Pallet::::weight_and_dispatch_class(calls); + let (dispatch_weight, pays) = Pallet::::batch_calls_weight_and_pays(calls); let dispatch_weight = dispatch_weight.saturating_add(T::WeightInfo::force_batch(calls.len() as u32)); (dispatch_weight, DispatchClass::Normal, pays) })] @@ -456,12 +440,10 @@ pub mod pallet { Ok(Some(base_weight.saturating_add(weight)).into()) } - /// Dispatch a function call with a specified weight. + /// Root-only: dispatch `call` as root using the supplied `weight` witness (not re-checked). /// - /// This function does not check the weight of the call, and instead allows the - /// Root origin to specify the weight of the call. - /// - /// The dispatch origin for this call must be _Root_. + /// Subtensor: outer dispatch class is always **Normal** (see module docs). Inner call still + /// runs with root via `dispatch_bypass_filter`. #[allow(unknown_lints, benchmarked_weight_not_plugged)] #[pallet::call_index(5)] #[pallet::weight((*weight, DispatchClass::Normal))] @@ -477,29 +459,12 @@ pub mod pallet { res.map(|_| ()).map_err(|e| e.error) } - /// Dispatch a fallback call in the event the main call fails to execute. - /// May be called from any origin except `None`. - /// - /// This function first attempts to dispatch the `main` call. - /// If the `main` call fails, the `fallback` is attemted. - /// if the fallback is successfully dispatched, the weights of both calls - /// are accumulated and an event containing the main call error is deposited. + /// Try `main`; on failure dispatch `fallback` (weights of both attempts accumulate). /// - /// In the event of a fallback failure the whole call fails - /// with the weights returned. - /// - /// - `main`: The main call to be dispatched. This is the primary action to execute. - /// - `fallback`: The fallback call to be dispatched in case the `main` call fails. - /// - /// ## Dispatch Logic - /// - If the origin is `root`, both the main and fallback calls are executed without - /// applying any origin filters. - /// - If the origin is not `root`, the origin filter is applied to both the `main` and - /// `fallback` calls. - /// - /// ## Use Case - /// - Some use cases might involve submitting a `batch` type call in either main, fallback - /// or both. + /// May be called from any origin except `None`. Root bypasses filters for both legs. + /// - Main success → [`Event::IfElseMainSuccess`], fallback skipped. + /// - Fallback success after main error → [`Event::IfElseFallbackCalled`]. + /// - Fallback failure → extrinsic errors with fallback's error and combined weight. #[pallet::call_index(6)] #[pallet::weight({ let main = main.get_dispatch_info(); @@ -571,11 +536,9 @@ pub mod pallet { }) } - /// Dispatches a function call with a provided origin. + /// Root-only: like [`Call::dispatch_as`], but forwards the inner call's error to the caller. /// - /// Almost the same as [`Pallet::dispatch_as`] but forwards any error of the inner call. - /// - /// The dispatch origin for this call must be _Root_. + /// On success still deposits [`Event::DispatchedAs`] with `Ok(())`. #[pallet::call_index(7)] #[pallet::weight({ let dispatch_info = call.get_dispatch_info(); @@ -602,9 +565,10 @@ pub mod pallet { } impl Pallet { - /// Get the accumulated `weight` and `pays` for the given `calls`. - /// The outer dispatch class is intentionally always `Normal`. - fn weight_and_dispatch_class(calls: &[::RuntimeCall]) -> (Weight, Pays) { + /// Sum inner `call_weight`s and OR their `Pays` flags for batch weight annotations. + /// + /// Outer dispatch class is chosen separately (always `Normal` for this pallet's batches). + fn batch_calls_weight_and_pays(calls: &[::RuntimeCall]) -> (Weight, Pays) { let mut total_weight = Weight::zero(); let mut pays = Pays::No; @@ -620,9 +584,12 @@ pub mod pallet { } } -/// A pallet identifier. These are per pallet and should be stored in a registry somewhere. +/// Legacy `TypeId` wrapper (`b"suba"`); not used by [`Pallet::derivative_account_id`]. +/// +/// Derivative IDs use the blake2 entropic path with prefix `modlpy/utilisuba` instead. Kept so the +/// frozen layout / TYPE_ID remain searchable if a migration ever reintroduces pallet-id encoding. #[allow(unused)] -#[freeze_struct("8b0fb6b91f673972")] +#[freeze_struct("17a7798f791a1a47")] #[derive(Clone, Copy, Eq, PartialEq, Encode, Decode)] struct IndexedUtilityPalletId(u16); @@ -631,7 +598,10 @@ impl TypeId for IndexedUtilityPalletId { } impl Pallet { - /// Derive a derivative account ID from the owner account and the sub-account index. + /// Derive the signed pseudonym account for `(who, index)` used by [`Call::as_derivative`]. + /// + /// Entropy: `blake2_256(encode("modlpy/utilisuba", who, index))`, then decode as `AccountId` + /// via [`TrailingZeroInput`]. Returns [`Error::InvalidDerivedAccount`] if decode fails. pub fn derivative_account_id( who: T::AccountId, index: u16, diff --git a/pallets/utility/src/tests.rs b/pallets/utility/src/tests.rs index 14020ec8bf..4469aecdf5 100644 --- a/pallets/utility/src/tests.rs +++ b/pallets/utility/src/tests.rs @@ -1,3 +1,12 @@ +//! Unit tests for `pallet-subtensor-utility`. +//! +//! Sections (search these headings) mirror dispatchables: `as_derivative`, `batch`, +//! `batch_all`, `force_batch`, `dispatch_as`, `if_else`, `with_weight`, plus origin edge cases. +//! +//! Kept as `tests.rs` (not `tests/`) so the metadata fingerprint still sees the in-file +//! example pallet `call_index`s — `extract_metadata_fingerprint.py` skips `tests/` directories +//! but not a `tests.rs` file. + // This file is part of Substrate. // Copyright (C) Parity Technologies (UK) Ltd. @@ -15,8 +24,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Tests for Utility Pallet - #![cfg(test)] #![allow( clippy::arithmetic_side_effects, @@ -35,13 +42,13 @@ use frame_support::{ weights::Weight, }; use sp_runtime::{ - BuildStorage, DispatchError, TokenError, + BuildStorage, DispatchError, Perbill, TokenError, traits::{BadOrigin, Dispatchable}, }; type BlockNumber = u64; -// example module to test behaviors. +/// Minimal example pallet used only to exercise weight refund / error paths in utility tests. #[frame_support::pallet(dev_mode)] #[allow(clippy::large_enum_variant)] pub mod example { @@ -145,6 +152,7 @@ parameter_types! { impl example::Config for Test {} +/// Restricts which runtime calls pass `BaseCallFilter` in the utility mock. pub struct TestBaseCallFilter; impl Contains for TestBaseCallFilter { fn contains(c: &RuntimeCall) -> bool { @@ -175,6 +183,7 @@ use pallet_balances::Call as BalancesCall; use pallet_root_testing::Call as RootTestingCall; use pallet_timestamp::Call as TimestampCall; +/// Build a test externalities with funded accounts `{1..=4}: 10` and `5: 2`. pub fn new_test_ext() -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::::default() .build_storage() @@ -191,11 +200,13 @@ pub fn new_test_ext() -> sp_io::TestExternalities { ext } -fn call_transfer(dest: u64, value: u64) -> RuntimeCall { +/// `Balances::transfer_allow_death` call helper (passes `TestBaseCallFilter`). +fn balances_transfer_call(dest: u64, value: u64) -> RuntimeCall { RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest, value }) } -fn call_foobar(err: bool, start_weight: Weight, end_weight: Option) -> RuntimeCall { +/// `Example::foobar` call helper for weight-refund and error-path tests. +fn example_foobar_call(err: bool, start_weight: Weight, end_weight: Option) -> RuntimeCall { RuntimeCall::Example(ExampleCall::foobar { err, start_weight, @@ -203,7 +214,8 @@ fn call_foobar(err: bool, start_weight: Weight, end_weight: Option) -> R }) } -fn utility_events() -> Vec { +/// Collect deposited `utility::Event`s from the system event queue. +fn collect_utility_events() -> Vec { System::events() .into_iter() .map(|r| r.event) @@ -217,6 +229,8 @@ fn utility_events() -> Vec { .collect() } +// --- as_derivative --- + #[test] fn as_derivative_works() { new_test_ext().execute_with(|| { @@ -227,13 +241,17 @@ fn as_derivative_works() { 5 )); assert_err_ignore_postinfo!( - Utility::as_derivative(RuntimeOrigin::signed(1), 1, Box::new(call_transfer(6, 3)),), + Utility::as_derivative( + RuntimeOrigin::signed(1), + 1, + Box::new(balances_transfer_call(6, 3)), + ), TokenError::FundsUnavailable, ); assert_ok!(Utility::as_derivative( RuntimeOrigin::signed(1), 0, - Box::new(call_transfer(2, 3)), + Box::new(balances_transfer_call(2, 3)), )); assert_eq!(Balances::free_balance(sub_1_0), 2); assert_eq!(Balances::free_balance(2), 13); @@ -248,7 +266,7 @@ fn as_derivative_handles_weight_refund() { let diff = start_weight - end_weight; // Full weight when ok - let inner_call = call_foobar(false, start_weight, None); + let inner_call = example_foobar_call(false, start_weight, None); let call = RuntimeCall::Utility(UtilityCall::as_derivative { index: 0, call: Box::new(inner_call), @@ -259,7 +277,7 @@ fn as_derivative_handles_weight_refund() { assert_eq!(extract_actual_weight(&result, &info), info.call_weight); // Refund weight when ok - let inner_call = call_foobar(false, start_weight, Some(end_weight)); + let inner_call = example_foobar_call(false, start_weight, Some(end_weight)); let call = RuntimeCall::Utility(UtilityCall::as_derivative { index: 0, call: Box::new(inner_call), @@ -274,7 +292,7 @@ fn as_derivative_handles_weight_refund() { ); // Full weight when err - let inner_call = call_foobar(true, start_weight, None); + let inner_call = example_foobar_call(true, start_weight, None); let call = RuntimeCall::Utility(UtilityCall::as_derivative { index: 0, call: Box::new(inner_call), @@ -294,7 +312,7 @@ fn as_derivative_handles_weight_refund() { ); // Refund weight when err - let inner_call = call_foobar(true, start_weight, Some(end_weight)); + let inner_call = example_foobar_call(true, start_weight, Some(end_weight)); let call = RuntimeCall::Utility(UtilityCall::as_derivative { index: 0, call: Box::new(inner_call), @@ -331,6 +349,8 @@ fn as_derivative_filters() { }); } +// --- batch --- + #[test] fn batch_with_root_works() { new_test_ext().execute_with(|| { @@ -370,7 +390,7 @@ fn batch_with_signed_works() { assert_eq!(Balances::free_balance(2), 10); assert_ok!(Utility::batch( RuntimeOrigin::signed(1), - vec![call_transfer(2, 5), call_transfer(2, 5)] + vec![balances_transfer_call(2, 5), balances_transfer_call(2, 5)] ),); assert_eq!(Balances::free_balance(1), 0); assert_eq!(Balances::free_balance(2), 20); @@ -404,9 +424,9 @@ fn batch_early_exit_works() { assert_ok!(Utility::batch( RuntimeOrigin::signed(1), vec![ - call_transfer(2, 5), - call_transfer(2, 10), - call_transfer(2, 5), + balances_transfer_call(2, 5), + balances_transfer_call(2, 10), + balances_transfer_call(2, 5), ] ),); assert_eq!(Balances::free_balance(1), 5); @@ -416,7 +436,6 @@ fn batch_early_exit_works() { #[test] fn batch_weight_calculation_doesnt_overflow() { - use sp_runtime::Perbill; new_test_ext().execute_with(|| { let big_call = RuntimeCall::RootTesting(RootTestingCall::fill_block { ratio: Perbill::from_percent(50), @@ -441,7 +460,7 @@ fn batch_handles_weight_refund() { let batch_len = 4; // Full weight when ok - let inner_call = call_foobar(false, start_weight, None); + let inner_call = example_foobar_call(false, start_weight, None); let batch_calls = vec![inner_call; batch_len as usize]; let call = RuntimeCall::Utility(UtilityCall::batch { calls: batch_calls }); let info = call.get_dispatch_info(); @@ -450,7 +469,7 @@ fn batch_handles_weight_refund() { assert_eq!(extract_actual_weight(&result, &info), info.call_weight); // Refund weight when ok - let inner_call = call_foobar(false, start_weight, Some(end_weight)); + let inner_call = example_foobar_call(false, start_weight, Some(end_weight)); let batch_calls = vec![inner_call; batch_len as usize]; let call = RuntimeCall::Utility(UtilityCall::batch { calls: batch_calls }); let info = call.get_dispatch_info(); @@ -463,8 +482,8 @@ fn batch_handles_weight_refund() { ); // Full weight when err - let good_call = call_foobar(false, start_weight, None); - let bad_call = call_foobar(true, start_weight, None); + let good_call = example_foobar_call(false, start_weight, None); + let bad_call = example_foobar_call(true, start_weight, None); let batch_calls = vec![good_call, bad_call]; let call = RuntimeCall::Utility(UtilityCall::batch { calls: batch_calls }); let info = call.get_dispatch_info(); @@ -481,8 +500,8 @@ fn batch_handles_weight_refund() { assert_eq!(extract_actual_weight(&result, &info), info.call_weight); // Refund weight when err - let good_call = call_foobar(false, start_weight, Some(end_weight)); - let bad_call = call_foobar(true, start_weight, Some(end_weight)); + let good_call = example_foobar_call(false, start_weight, Some(end_weight)); + let bad_call = example_foobar_call(true, start_weight, Some(end_weight)); let batch_calls = vec![good_call, bad_call]; let batch_len = batch_calls.len() as u64; let call = RuntimeCall::Utility(UtilityCall::batch { calls: batch_calls }); @@ -502,8 +521,8 @@ fn batch_handles_weight_refund() { ); // Partial batch completion - let good_call = call_foobar(false, start_weight, Some(end_weight)); - let bad_call = call_foobar(true, start_weight, Some(end_weight)); + let good_call = example_foobar_call(false, start_weight, Some(end_weight)); + let bad_call = example_foobar_call(true, start_weight, Some(end_weight)); let batch_calls = vec![good_call, bad_call.clone(), bad_call]; let call = RuntimeCall::Utility(UtilityCall::batch { calls: batch_calls }); let info = call.get_dispatch_info(); @@ -524,6 +543,41 @@ fn batch_handles_weight_refund() { }); } +#[test] +fn batch_limit() { + new_test_ext().execute_with(|| { + let calls = vec![RuntimeCall::System(SystemCall::remark { remark: vec![] }); 40_000]; + assert_noop!( + Utility::batch(RuntimeOrigin::signed(1), calls.clone()), + Error::::TooManyCalls + ); + assert_noop!( + Utility::batch_all(RuntimeOrigin::signed(1), calls), + Error::::TooManyCalls + ); + }); +} + +#[test] +fn batch_doesnt_work_with_inherents() { + new_test_ext().execute_with(|| { + // fails because inherents expect the origin to be none. + assert_ok!(Utility::batch( + RuntimeOrigin::signed(1), + vec![RuntimeCall::Timestamp(TimestampCall::set { now: 42 }),] + )); + System::assert_last_event( + utility::Event::BatchInterrupted { + index: 0, + error: frame_system::Error::::CallFiltered.into(), + } + .into(), + ); + }) +} + +// --- batch_all --- + #[test] fn batch_all_works() { new_test_ext().execute_with(|| { @@ -531,7 +585,7 @@ fn batch_all_works() { assert_eq!(Balances::free_balance(2), 10); assert_ok!(Utility::batch_all( RuntimeOrigin::signed(1), - vec![call_transfer(2, 5), call_transfer(2, 5)] + vec![balances_transfer_call(2, 5), balances_transfer_call(2, 5)] ),); assert_eq!(Balances::free_balance(1), 0); assert_eq!(Balances::free_balance(2), 20); @@ -541,16 +595,16 @@ fn batch_all_works() { #[test] fn batch_all_revert() { new_test_ext().execute_with(|| { - let call = call_transfer(2, 5); + let call = balances_transfer_call(2, 5); let info = call.get_dispatch_info(); assert_eq!(Balances::free_balance(1), 10); assert_eq!(Balances::free_balance(2), 10); let batch_all_calls = RuntimeCall::Utility(crate::Call::::batch_all { calls: vec![ - call_transfer(2, 5), - call_transfer(2, 10), - call_transfer(2, 5), + balances_transfer_call(2, 5), + balances_transfer_call(2, 10), + balances_transfer_call(2, 5), ], }); assert_noop!( @@ -579,7 +633,7 @@ fn batch_all_handles_weight_refund() { let batch_len = 4; // Full weight when ok - let inner_call = call_foobar(false, start_weight, None); + let inner_call = example_foobar_call(false, start_weight, None); let batch_calls = vec![inner_call; batch_len as usize]; let call = RuntimeCall::Utility(UtilityCall::batch_all { calls: batch_calls }); let info = call.get_dispatch_info(); @@ -588,7 +642,7 @@ fn batch_all_handles_weight_refund() { assert_eq!(extract_actual_weight(&result, &info), info.call_weight); // Refund weight when ok - let inner_call = call_foobar(false, start_weight, Some(end_weight)); + let inner_call = example_foobar_call(false, start_weight, Some(end_weight)); let batch_calls = vec![inner_call; batch_len as usize]; let call = RuntimeCall::Utility(UtilityCall::batch_all { calls: batch_calls }); let info = call.get_dispatch_info(); @@ -601,8 +655,8 @@ fn batch_all_handles_weight_refund() { ); // Full weight when err - let good_call = call_foobar(false, start_weight, None); - let bad_call = call_foobar(true, start_weight, None); + let good_call = example_foobar_call(false, start_weight, None); + let bad_call = example_foobar_call(true, start_weight, None); let batch_calls = vec![good_call, bad_call]; let call = RuntimeCall::Utility(UtilityCall::batch_all { calls: batch_calls }); let info = call.get_dispatch_info(); @@ -612,8 +666,8 @@ fn batch_all_handles_weight_refund() { assert_eq!(extract_actual_weight(&result, &info), info.call_weight); // Refund weight when err - let good_call = call_foobar(false, start_weight, Some(end_weight)); - let bad_call = call_foobar(true, start_weight, Some(end_weight)); + let good_call = example_foobar_call(false, start_weight, Some(end_weight)); + let bad_call = example_foobar_call(true, start_weight, Some(end_weight)); let batch_calls = vec![good_call, bad_call]; let batch_len = batch_calls.len() as u64; let call = RuntimeCall::Utility(UtilityCall::batch_all { calls: batch_calls }); @@ -626,8 +680,8 @@ fn batch_all_handles_weight_refund() { ); // Partial batch completion - let good_call = call_foobar(false, start_weight, Some(end_weight)); - let bad_call = call_foobar(true, start_weight, Some(end_weight)); + let good_call = example_foobar_call(false, start_weight, Some(end_weight)); + let bad_call = example_foobar_call(true, start_weight, Some(end_weight)); let batch_calls = vec![good_call, bad_call.clone(), bad_call]; let call = RuntimeCall::Utility(UtilityCall::batch_all { calls: batch_calls }); let info = call.get_dispatch_info(); @@ -646,9 +700,9 @@ fn batch_all_does_not_nest() { new_test_ext().execute_with(|| { let batch_all = RuntimeCall::Utility(UtilityCall::batch_all { calls: vec![ - call_transfer(2, 1), - call_transfer(2, 1), - call_transfer(2, 1), + balances_transfer_call(2, 1), + balances_transfer_call(2, 1), + balances_transfer_call(2, 1), ], }); @@ -695,20 +749,29 @@ fn batch_all_does_not_nest() { } #[test] -fn batch_limit() { +fn batch_all_doesnt_work_with_inherents() { new_test_ext().execute_with(|| { - let calls = vec![RuntimeCall::System(SystemCall::remark { remark: vec![] }); 40_000]; - assert_noop!( - Utility::batch(RuntimeOrigin::signed(1), calls.clone()), - Error::::TooManyCalls - ); + let batch_all = RuntimeCall::Utility(UtilityCall::batch_all { + calls: vec![RuntimeCall::Timestamp(TimestampCall::set { now: 42 })], + }); + let info = batch_all.get_dispatch_info(); + + // fails because inherents expect the origin to be none. assert_noop!( - Utility::batch_all(RuntimeOrigin::signed(1), calls), - Error::::TooManyCalls + batch_all.dispatch(RuntimeOrigin::signed(1)), + DispatchErrorWithPostInfo { + post_info: PostDispatchInfo { + actual_weight: Some(info.call_weight), + pays_fee: Pays::Yes + }, + error: frame_system::Error::::CallFiltered.into(), + } ); - }); + }) } +// --- force_batch --- + #[test] fn force_batch_works() { new_test_ext().execute_with(|| { @@ -717,10 +780,10 @@ fn force_batch_works() { assert_ok!(Utility::force_batch( RuntimeOrigin::signed(1), vec![ - call_transfer(2, 5), - call_foobar(true, Weight::from_parts(75, 0), None), - call_transfer(2, 10), - call_transfer(2, 5), + balances_transfer_call(2, 5), + example_foobar_call(true, Weight::from_parts(75, 0), None), + balances_transfer_call(2, 10), + balances_transfer_call(2, 5), ] )); System::assert_last_event(utility::Event::BatchCompletedWithErrors.into()); @@ -735,48 +798,18 @@ fn force_batch_works() { assert_ok!(Utility::force_batch( RuntimeOrigin::signed(2), - vec![call_transfer(1, 5), call_transfer(1, 5),] + vec![balances_transfer_call(1, 5), balances_transfer_call(1, 5),] )); System::assert_last_event(utility::Event::BatchCompleted.into()); assert_ok!(Utility::force_batch( RuntimeOrigin::signed(1), - vec![call_transfer(2, 50),] + vec![balances_transfer_call(2, 50),] ),); System::assert_last_event(utility::Event::BatchCompletedWithErrors.into()); }); } -#[test] -fn none_origin_does_not_work() { - new_test_ext().execute_with(|| { - assert_noop!( - Utility::force_batch(RuntimeOrigin::none(), vec![]), - BadOrigin - ); - assert_noop!(Utility::batch(RuntimeOrigin::none(), vec![]), BadOrigin); - assert_noop!(Utility::batch_all(RuntimeOrigin::none(), vec![]), BadOrigin); - }) -} - -#[test] -fn batch_doesnt_work_with_inherents() { - new_test_ext().execute_with(|| { - // fails because inherents expect the origin to be none. - assert_ok!(Utility::batch( - RuntimeOrigin::signed(1), - vec![RuntimeCall::Timestamp(TimestampCall::set { now: 42 }),] - )); - System::assert_last_event( - utility::Event::BatchInterrupted { - index: 0, - error: frame_system::Error::::CallFiltered.into(), - } - .into(), - ); - }) -} - #[test] fn force_batch_doesnt_work_with_inherents() { new_test_ext().execute_with(|| { @@ -789,60 +822,7 @@ fn force_batch_doesnt_work_with_inherents() { }) } -#[test] -fn batch_all_doesnt_work_with_inherents() { - new_test_ext().execute_with(|| { - let batch_all = RuntimeCall::Utility(UtilityCall::batch_all { - calls: vec![RuntimeCall::Timestamp(TimestampCall::set { now: 42 })], - }); - let info = batch_all.get_dispatch_info(); - - // fails because inherents expect the origin to be none. - assert_noop!( - batch_all.dispatch(RuntimeOrigin::signed(1)), - DispatchErrorWithPostInfo { - post_info: PostDispatchInfo { - actual_weight: Some(info.call_weight), - pays_fee: Pays::Yes - }, - error: frame_system::Error::::CallFiltered.into(), - } - ); - }) -} - -#[test] -fn with_weight_works() { - new_test_ext().execute_with(|| { - use frame_system::WeightInfo; - let upgrade_code_call = Box::new(RuntimeCall::System( - frame_system::Call::set_code_without_checks { code: vec![] }, - )); - // Weight before is max. - assert_eq!( - upgrade_code_call.get_dispatch_info().call_weight, - ::SystemWeightInfo::set_code() - ); - assert_eq!( - upgrade_code_call.get_dispatch_info().class, - frame_support::dispatch::DispatchClass::Operational - ); - - let with_weight_call = Call::::with_weight { - call: upgrade_code_call, - weight: Weight::from_parts(123, 456), - }; - // Weight after is set by Root. - assert_eq!( - with_weight_call.get_dispatch_info().call_weight, - Weight::from_parts(123, 456) - ); - assert_eq!( - with_weight_call.get_dispatch_info().class, - frame_support::dispatch::DispatchClass::Normal // We only allow normal in subtensor - ); - }) -} +// --- dispatch_as --- #[test] fn dispatch_as_works() { @@ -853,7 +833,7 @@ fn dispatch_as_works() { assert_ok!(Utility::dispatch_as( RuntimeOrigin::root(), Box::new(OriginCaller::system(frame_system::RawOrigin::Signed(666))), - Box::new(call_transfer(777, 100)) + Box::new(balances_transfer_call(777, 100)) )); assert_eq!(Balances::free_balance(666), 0); assert_eq!(Balances::free_balance(777), 100); @@ -865,7 +845,7 @@ fn dispatch_as_works() { Box::new(RuntimeCall::Timestamp(TimestampCall::set { now: 0 })) )); assert_eq!( - utility_events(), + collect_utility_events(), vec![Event::DispatchedAs { result: Err(DispatchError::BadOrigin) }] @@ -873,6 +853,33 @@ fn dispatch_as_works() { }) } +#[test] +fn dispatch_as_fallible_works() { + new_test_ext().execute_with(|| { + Balances::force_set_balance(RuntimeOrigin::root(), 666, 100).unwrap(); + assert_eq!(Balances::free_balance(666), 100); + assert_eq!(Balances::free_balance(777), 0); + assert_ok!(Utility::dispatch_as_fallible( + RuntimeOrigin::root(), + Box::new(OriginCaller::system(frame_system::RawOrigin::Signed(666))), + Box::new(balances_transfer_call(777, 100)) + )); + assert_eq!(Balances::free_balance(666), 0); + assert_eq!(Balances::free_balance(777), 100); + + assert_noop!( + Utility::dispatch_as_fallible( + RuntimeOrigin::root(), + Box::new(OriginCaller::system(frame_system::RawOrigin::Signed(777))), + Box::new(RuntimeCall::Timestamp(TimestampCall::set { now: 0 })) + ), + DispatchError::BadOrigin, + ); + }) +} + +// --- if_else --- + #[test] fn if_else_with_root_works() { new_test_ext().execute_with(|| { @@ -912,8 +919,8 @@ fn if_else_with_signed_works() { assert_eq!(Balances::free_balance(2), 10); assert_ok!(Utility::if_else( RuntimeOrigin::signed(1), - call_transfer(2, 11).into(), - call_transfer(2, 5).into() + balances_transfer_call(2, 11).into(), + balances_transfer_call(2, 5).into() )); assert_eq!(Balances::free_balance(1), 5); assert_eq!(Balances::free_balance(2), 15); @@ -934,8 +941,8 @@ fn if_else_successful_main_call() { assert_eq!(Balances::free_balance(2), 10); assert_ok!(Utility::if_else( RuntimeOrigin::signed(1), - call_transfer(2, 9).into(), - call_transfer(2, 1).into() + balances_transfer_call(2, 9).into(), + balances_transfer_call(2, 1).into() )); assert_eq!(Balances::free_balance(1), 1); assert_eq!(Balances::free_balance(2), 19); @@ -944,31 +951,6 @@ fn if_else_successful_main_call() { }) } -#[test] -fn dispatch_as_fallible_works() { - new_test_ext().execute_with(|| { - Balances::force_set_balance(RuntimeOrigin::root(), 666, 100).unwrap(); - assert_eq!(Balances::free_balance(666), 100); - assert_eq!(Balances::free_balance(777), 0); - assert_ok!(Utility::dispatch_as_fallible( - RuntimeOrigin::root(), - Box::new(OriginCaller::system(frame_system::RawOrigin::Signed(666))), - Box::new(call_transfer(777, 100)) - )); - assert_eq!(Balances::free_balance(666), 0); - assert_eq!(Balances::free_balance(777), 100); - - assert_noop!( - Utility::dispatch_as_fallible( - RuntimeOrigin::root(), - Box::new(OriginCaller::system(frame_system::RawOrigin::Signed(777))), - Box::new(RuntimeCall::Timestamp(TimestampCall::set { now: 0 })) - ), - DispatchError::BadOrigin, - ); - }) -} - #[test] fn if_else_failing_fallback_call() { new_test_ext().execute_with(|| { @@ -977,8 +959,8 @@ fn if_else_failing_fallback_call() { assert_err_ignore_postinfo!( Utility::if_else( RuntimeOrigin::signed(1), - call_transfer(2, 11).into(), - call_transfer(2, 11).into() + balances_transfer_call(2, 11).into(), + balances_transfer_call(2, 11).into() ), TokenError::FundsUnavailable ); @@ -993,8 +975,8 @@ fn if_else_with_nested_if_else_works() { assert_eq!(Balances::free_balance(1), 10); assert_eq!(Balances::free_balance(2), 10); - let main_call = call_transfer(2, 11).into(); - let fallback_call = call_transfer(2, 5).into(); + let main_call = balances_transfer_call(2, 11).into(); + let fallback_call = balances_transfer_call(2, 5).into(); let nested_if_else_call = RuntimeCall::Utility(UtilityCall::if_else { main: main_call, @@ -1006,7 +988,7 @@ fn if_else_with_nested_if_else_works() { assert_ok!(Utility::if_else( RuntimeOrigin::signed(1), nested_if_else_call, - call_transfer(2, 7).into() + balances_transfer_call(2, 7).into() )); // inner if_else fallback is executed. @@ -1017,3 +999,53 @@ fn if_else_with_nested_if_else_works() { System::assert_last_event(utility::Event::IfElseMainSuccess.into()); }); } + +// --- with_weight --- + +#[test] +fn with_weight_works() { + new_test_ext().execute_with(|| { + use frame_system::WeightInfo; + let upgrade_code_call = Box::new(RuntimeCall::System( + frame_system::Call::set_code_without_checks { code: vec![] }, + )); + // Weight before is max. + assert_eq!( + upgrade_code_call.get_dispatch_info().call_weight, + ::SystemWeightInfo::set_code() + ); + assert_eq!( + upgrade_code_call.get_dispatch_info().class, + frame_support::dispatch::DispatchClass::Operational + ); + + let with_weight_call = Call::::with_weight { + call: upgrade_code_call, + weight: Weight::from_parts(123, 456), + }; + // Weight after is set by Root. + assert_eq!( + with_weight_call.get_dispatch_info().call_weight, + Weight::from_parts(123, 456) + ); + assert_eq!( + with_weight_call.get_dispatch_info().class, + // Subtensor fork: outer class is always Normal (not Operational). + frame_support::dispatch::DispatchClass::Normal + ); + }) +} + +// --- batch_origins --- + +#[test] +fn none_origin_does_not_work() { + new_test_ext().execute_with(|| { + assert_noop!( + Utility::force_batch(RuntimeOrigin::none(), vec![]), + BadOrigin + ); + assert_noop!(Utility::batch(RuntimeOrigin::none(), vec![]), BadOrigin); + assert_noop!(Utility::batch_all(RuntimeOrigin::none(), vec![]), BadOrigin); + }) +} diff --git a/precompiles/src/address_mapping.rs b/precompiles/src/address_mapping.rs index c8f3815c49..25e627c24a 100644 --- a/precompiles/src/address_mapping.rs +++ b/precompiles/src/address_mapping.rs @@ -1,3 +1,8 @@ +//! EVM ↔ Substrate address mapping precompile (`INDEX` 2060). +//! +//! Exposes `addressMapping(address)` so contracts can resolve an `H160` to the +//! runtime `AccountId` used by pallet storage (via `Config::AddressMapping`). + extern crate alloc; use core::marker::PhantomData; use pallet_evm::AddressMapping; @@ -13,6 +18,7 @@ use precompile_utils::EvmResult; use precompile_utils::prelude::Address; use sp_runtime::traits::{AsSystemOriginSigner, Dispatchable}; +/// Precompile that maps an EVM address to a 32-byte Substrate account id. pub struct AddressMappingPrecompile(PhantomData); impl PrecompileExt for AddressMappingPrecompile @@ -65,6 +71,7 @@ where + Dispatchable, ::AddressMapping: AddressMapping, { + /// Returns the Substrate `AccountId` bytes for `target_address` (runtime address mapping). #[precompile::public("addressMapping(address)")] #[precompile::view] fn address_mapping( diff --git a/precompiles/src/alpha.rs b/precompiles/src/alpha.rs index 9840c42575..29c4e7064f 100644 --- a/precompiles/src/alpha.rs +++ b/precompiles/src/alpha.rs @@ -1,3 +1,10 @@ +//! Alpha / subnet AMM view + swap-simulation precompile (`INDEX` 2056). +//! +//! Reads pool reserves, prices, emissions, and global weights from +//! `pallet_subtensor` / `pallet_subtensor_swap`. Spot and moving prices are +//! scaled by `1e9` then converted to EVM balance units. `simSwap*` methods are +//! view-only (no state mutation). + use core::marker::PhantomData; use crate::PrecompileExt; @@ -11,6 +18,7 @@ use sp_core::U256; use substrate_fixed::types::U64F64; use subtensor_runtime_common::{NetUid, Token}; use subtensor_swap_interface::{Order, SwapHandler}; +/// EVM surface for subnet alpha pool prices, reserves, emissions, and swap sims. pub struct AlphaPrecompile(PhantomData); impl PrecompileExt for AlphaPrecompile @@ -32,6 +40,7 @@ where + pallet_subtensor_swap::Config + pallet_evm::Config, { + /// Spot alpha/TAO price for `netuid`, scaled by 1e9 and converted to EVM balance units. #[precompile::public("getAlphaPrice(uint16)")] #[precompile::view] fn get_alpha_price(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { @@ -48,6 +57,7 @@ where Ok(price_eth) } + /// EMA moving alpha price for `netuid`, scaled by 1e9 and converted to EVM balance units. #[precompile::public("getMovingAlphaPrice(uint16)")] #[precompile::view] fn get_moving_alpha_price(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { @@ -64,6 +74,7 @@ where Ok(price_eth) } + /// TAO reserve in the subnet AMM pool (`SubnetTAO`). #[precompile::public("getTaoInPool(uint16)")] #[precompile::view] fn get_tao_in_pool(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { @@ -71,6 +82,7 @@ where Ok(pallet_subtensor::SubnetTAO::::get(NetUid::from(netuid)).to_u64()) } + /// Alpha reserve held in the subnet AMM pool (`SubnetAlphaIn`). #[precompile::public("getAlphaInPool(uint16)")] #[precompile::view] fn get_alpha_in_pool(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { @@ -78,6 +90,7 @@ where Ok(pallet_subtensor::SubnetAlphaIn::::get(NetUid::from(netuid)).into()) } + /// Alpha outstanding outside the pool (`SubnetAlphaOut`). #[precompile::public("getAlphaOutPool(uint16)")] #[precompile::view] fn get_alpha_out_pool(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { @@ -85,6 +98,7 @@ where Ok(pallet_subtensor::SubnetAlphaOut::::get(NetUid::from(netuid)).into()) } + /// Total alpha issuance for `netuid` (in + out pool). #[precompile::public("getAlphaIssuance(uint16)")] #[precompile::view] fn get_alpha_issuance(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { @@ -93,6 +107,7 @@ where Ok(pallet_subtensor::Pallet::::get_alpha_issuance(netuid.into()).into()) } + /// Global TAO weight used in stake/price calculations. #[precompile::public("getTaoWeight()")] #[precompile::view] fn get_tao_weight(handle: &mut impl PrecompileHandle) -> EvmResult { @@ -101,6 +116,7 @@ where Ok(U256::from(tao_weight)) } + /// Global coldkey swap burn parameter (`CKBurn`). #[precompile::public("getCKBurn()")] #[precompile::view] fn get_ck_burn(handle: &mut impl PrecompileHandle) -> EvmResult { @@ -109,6 +125,7 @@ where Ok(U256::from(ck_burn)) } + /// View-only simulation: TAO in → alpha out for `netuid` (does not mutate pool state). #[precompile::public("simSwapTaoForAlpha(uint16,uint64)")] #[precompile::view] fn sim_swap_tao_for_alpha( @@ -128,6 +145,7 @@ where Ok(U256::from(swap_result.amount_paid_out.to_u64())) } + /// View-only simulation: alpha in → TAO out for `netuid` (does not mutate pool state). #[precompile::public("simSwapAlphaForTao(uint16,uint64)")] #[precompile::view] fn sim_swap_alpha_for_tao( @@ -147,6 +165,7 @@ where Ok(U256::from(swap_result.amount_paid_out.to_u64())) } + /// Subnet mechanism enum value (`SubnetMechanism`) for `netuid`. #[precompile::public("getSubnetMechanism(uint16)")] #[precompile::view] fn get_subnet_mechanism(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { @@ -156,12 +175,14 @@ where ))) } + /// Root subnet netuid constant (`NetUid::ROOT`). #[precompile::public("getRootNetuid()")] #[precompile::view] fn get_root_netuid(_handle: &mut impl PrecompileHandle) -> EvmResult { Ok(NetUid::ROOT.into()) } + /// EMA price halving period in blocks for `netuid`. #[precompile::public("getEMAPriceHalvingBlocks(uint16)")] #[precompile::view] fn get_ema_price_halving_blocks( @@ -174,6 +195,7 @@ where )) } + /// Cumulative subnet swap volume for `netuid`. #[precompile::public("getSubnetVolume(uint16)")] #[precompile::view] fn get_subnet_volume(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { @@ -183,6 +205,7 @@ where ))) } + /// Last tempo TAO-in emission for `netuid`. #[precompile::public("getTaoInEmission(uint16)")] #[precompile::view] fn get_tao_in_emission(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { @@ -192,6 +215,7 @@ where )) } + /// Last tempo alpha-in emission for `netuid`. #[precompile::public("getAlphaInEmission(uint16)")] #[precompile::view] fn get_alpha_in_emission(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { @@ -201,6 +225,7 @@ where )) } + /// Last tempo alpha-out emission for `netuid`. #[precompile::public("getAlphaOutEmission(uint16)")] #[precompile::view] fn get_alpha_out_emission(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { @@ -210,6 +235,7 @@ where )) } + /// Sum of current alpha prices over non-root subnets where price < 1 (scaled to EVM units). #[precompile::public("getSumAlphaPrice()")] #[precompile::view] fn get_sum_alpha_price(handle: &mut impl PrecompileHandle) -> EvmResult { diff --git a/precompiles/src/balance.rs b/precompiles/src/balance.rs index 36b80489e3..51760d071c 100644 --- a/precompiles/src/balance.rs +++ b/precompiles/src/balance.rs @@ -1,3 +1,8 @@ +//! Free-balance view precompile for Substrate coldkeys (`INDEX` 2062). +//! +//! `getFreeBalance(bytes32)` returns `pallet_balances` free balance for the given +//! 32-byte account id — not total, reserved, or reducible balance. + use core::marker::PhantomData; use pallet_evm::PrecompileHandle; @@ -7,6 +12,7 @@ use sp_core::{H256, U256}; use crate::PrecompileExt; use crate::PrecompileHandleExt; +/// Read-only precompile for coldkey free balance lookups from EVM. pub struct BalancePrecompile(PhantomData); impl PrecompileExt for BalancePrecompile @@ -25,6 +31,7 @@ where R::AccountId: From<[u8; 32]>, ::Balance: Into, { + /// Free balance of `coldkey` (excludes reserved/held amounts; ignores freeze for the value). #[precompile::public("getFreeBalance(bytes32)")] #[precompile::view] fn get_free_balance(handle: &mut impl PrecompileHandle, coldkey: H256) -> EvmResult { diff --git a/precompiles/src/balance_transfer.rs b/precompiles/src/balance_transfer.rs index d8d10970a3..496c24f9a4 100644 --- a/precompiles/src/balance_transfer.rs +++ b/precompiles/src/balance_transfer.rs @@ -1,3 +1,9 @@ +//! Payable TAO transfer precompile (`INDEX` 2048). +//! +//! `transfer(bytes32)` moves `msg.value` from the precompile's own Substrate account +//! (`PrecompileExt::account_id`) to the destination coldkey via +//! `balances.transfer_allow_death`. Zero value is a no-op success. + use core::marker::PhantomData; use frame_support::dispatch::{DispatchInfo, GetDispatchInfo, PostDispatchInfo}; @@ -10,6 +16,7 @@ use sp_runtime::traits::{AsSystemOriginSigner, Dispatchable, StaticLookup, Uniqu use crate::{PrecompileExt, PrecompileHandleExt}; +/// Precompile that forwards attached EVM value as a Substrate balance transfer. pub struct BalanceTransferPrecompile(PhantomData); impl PrecompileExt for BalanceTransferPrecompile @@ -66,6 +73,7 @@ where <::Lookup as StaticLookup>::Source: From, ::Balance: TryFrom, { + /// Transfers `msg.value` (converted to TAO) to `address` from this precompile's account. #[precompile::public("transfer(bytes32)")] #[precompile::payable] fn transfer(handle: &mut impl PrecompileHandle, address: H256) -> EvmResult<()> { diff --git a/precompiles/src/crowdloan.rs b/precompiles/src/crowdloan.rs index 1c66d941ca..b5d7e8b587 100644 --- a/precompiles/src/crowdloan.rs +++ b/precompiles/src/crowdloan.rs @@ -1,3 +1,9 @@ +//! Crowdloan pallet bridge precompile (`INDEX` 2057). +//! +//! Views and mutates `pallet_crowdloan` from EVM: create/contribute/withdraw/ +//! finalize/refund/dissolve and term updates. Mutations dispatch as the mapped +//! EVM caller; `create` sets `target_address` from an EVM address. + use alloc::string::String; use core::marker::PhantomData; @@ -15,6 +21,7 @@ use sp_runtime::traits::{AsSystemOriginSigner, Dispatchable, UniqueSaturatedInto use crate::{PrecompileExt, PrecompileHandleExt}; +/// EVM bridge to `pallet_crowdloan` views and extrinsics. pub struct CrowdloanPrecompile(PhantomData); impl PrecompileExt for CrowdloanPrecompile @@ -72,12 +79,13 @@ where + Dispatchable, ::AddressMapping: AddressMapping, { + /// Crowdloan record for `crowdloan_id`, or error if missing. #[precompile::public("getCrowdloan(uint32)")] #[precompile::view] fn get_crowdloan( handle: &mut impl PrecompileHandle, crowdloan_id: u32, - ) -> EvmResult { + ) -> EvmResult { handle.record_db_reads::(1)?; let crowdloan = pallet_crowdloan::Crowdloans::::get(crowdloan_id).ok_or( PrecompileFailure::Error { @@ -85,7 +93,7 @@ where }, )?; - Ok(CrowdloanInfo { + Ok(EvmCrowdloanInfo { creator: H256::from_slice(crowdloan.creator.as_slice()), deposit: u64::from(crowdloan.deposit), min_contribution: u64::from(crowdloan.min_contribution), @@ -103,6 +111,7 @@ where }) } + /// Contribution of `coldkey` to `crowdloan_id`, or error if missing. #[precompile::public("getContribution(uint32,bytes32)")] #[precompile::view] fn get_contribution( @@ -121,6 +130,7 @@ where Ok(u64::from(contribution)) } + /// Creates a crowdloan with EVM `target_address` (no embedded call). #[precompile::public("create(uint64,uint64,uint64,uint32,address)")] #[precompile::payable] fn create( @@ -145,6 +155,7 @@ where handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(who)) } + /// Contributes `amount` from the EVM caller to `crowdloan_id`. #[precompile::public("contribute(uint32,uint64)")] #[precompile::payable] fn contribute( @@ -161,6 +172,7 @@ where handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } + /// Withdraws the caller's contribution before finalization. #[precompile::public("withdraw(uint32)")] #[precompile::payable] fn withdraw(handle: &mut impl PrecompileHandle, crowdloan_id: u32) -> EvmResult<()> { @@ -170,6 +182,7 @@ where handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } + /// Finalizes a capped/ended crowdloan (creator or permitted caller). #[precompile::public("finalize(uint32)")] #[precompile::payable] fn finalize(handle: &mut impl PrecompileHandle, crowdloan_id: u32) -> EvmResult<()> { @@ -179,6 +192,7 @@ where handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } + /// Refunds contributors of a failed/non-finalized crowdloan. #[precompile::public("refund(uint32)")] #[precompile::payable] fn refund(handle: &mut impl PrecompileHandle, crowdloan_id: u32) -> EvmResult<()> { @@ -188,6 +202,7 @@ where handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } + /// Dissolves an empty crowdloan after refunds. #[precompile::public("dissolve(uint32)")] #[precompile::payable] fn dissolve(handle: &mut impl PrecompileHandle, crowdloan_id: u32) -> EvmResult<()> { @@ -197,6 +212,7 @@ where handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } + /// Updates minimum contribution for an open crowdloan. #[precompile::public("updateMinContribution(uint32,uint64)")] #[precompile::payable] fn update_min_contribution( @@ -213,6 +229,7 @@ where handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } + /// Updates the end block for an open crowdloan. #[precompile::public("updateEnd(uint32,uint32)")] #[precompile::payable] fn update_end( @@ -229,6 +246,7 @@ where handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } + /// Updates the raise cap for an open crowdloan. #[precompile::public("updateCap(uint32,uint64)")] #[precompile::payable] fn update_cap( @@ -246,8 +264,9 @@ where } } +/// Solidity-encoded crowdloan snapshot for `getCrowdloan` (ABI field order frozen). #[derive(Codec)] -struct CrowdloanInfo { +struct EvmCrowdloanInfo { creator: H256, deposit: u64, min_contribution: u64, @@ -281,7 +300,7 @@ mod tests { const END: u32 = 50; const ACCOUNT_BALANCE: u64 = 1_000; - fn get_crowdloan(caller: H160, crowdloan_id: u32, expected: CrowdloanInfo) { + fn get_crowdloan(caller: H160, crowdloan_id: u32, expected: EvmCrowdloanInfo) { let precompile_addr = addr_from_index(CrowdloanPrecompile::::INDEX); precompiles::>() @@ -294,11 +313,11 @@ mod tests { .execute_returns_raw(encode_return_value(expected)); } - fn expected_crowdloan_info(crowdloan_id: u32) -> CrowdloanInfo { + fn expected_crowdloan_info(crowdloan_id: u32) -> EvmCrowdloanInfo { let crowdloan = pallet_crowdloan::Crowdloans::::get(crowdloan_id) .expect("crowdloan should exist"); - CrowdloanInfo { + EvmCrowdloanInfo { creator: H256::from_slice(crowdloan.creator.as_slice()), deposit: u64::from(crowdloan.deposit), min_contribution: u64::from(crowdloan.min_contribution), diff --git a/precompiles/src/ed25519.rs b/precompiles/src/ed25519.rs index 38204c4304..000d0500c8 100644 --- a/precompiles/src/ed25519.rs +++ b/precompiles/src/ed25519.rs @@ -1,3 +1,10 @@ +//! Ed25519 signature verification precompile (`INDEX` 1026). +//! +//! Linear-cost precompile: input is raw bytes +//! `[pad4 | msg32 | pubkey32 | sig64]`; returns a 32-byte word with `1` in the +//! last byte on success. Gas base is 6000 (higher than EIP-665's 2000) because +//! Ed25519 verify is more expensive than secp256k1 recover in practice. + extern crate alloc; use alloc::vec::Vec; @@ -8,6 +15,7 @@ use fp_evm::{ExitError, ExitSucceed, LinearCostPrecompile, PrecompileFailure}; use crate::{PrecompileExt, parse_slice}; +/// Ed25519 verify precompile (Frontier linear-cost style, not Solidity ABI codec). pub struct Ed25519Verify(PhantomData); impl PrecompileExt for Ed25519Verify diff --git a/precompiles/src/extensions.rs b/precompiles/src/extensions.rs index 984b70a91b..0d21969d0c 100644 --- a/precompiles/src/extensions.rs +++ b/precompiles/src/extensions.rs @@ -1,3 +1,9 @@ +//! Shared helpers for Subtensor EVM precompiles: gas metering, call dispatch, enable gates. +//! +//! [`PrecompileHandleExt`] extends Frontier's `PrecompileHandle` with Substrate account +//! mapping and runtime-call dispatch. [`PrecompileExt`] binds each precompile's frozen +//! `INDEX` address and gates execution through `PrecompileEnable`. + extern crate alloc; use alloc::format; @@ -16,7 +22,9 @@ use sp_runtime::traits::{Dispatchable, ExtensionPostDispatchWeightHandler}; use sp_std::vec::Vec; use subtensor_runtime_common::with_evm_context; +/// `PrecompileHandle` helpers used by Subtensor precompile method bodies. pub(crate) trait PrecompileHandleExt: PrecompileHandle { + /// Maps the EVM caller (`context().caller`) to a Substrate `AccountId`. fn caller_account_id(&self) -> R::AccountId where R: frame_system::Config + pallet_evm::Config, @@ -25,6 +33,7 @@ pub(crate) trait PrecompileHandleExt: PrecompileHandle { ::AddressMapping::into_account_id(self.context().caller) } + /// Charges gas for `reads` DB reads using the runtime's read gas cost. fn record_db_reads(&mut self, reads: u64) -> EvmResult<()> where R: frame_system::Config + pallet_evm::Config, @@ -33,6 +42,7 @@ pub(crate) trait PrecompileHandleExt: PrecompileHandle { Ok(()) } + /// Charges gas for `writes` DB writes using the runtime's write gas cost. fn record_db_writes(&mut self, writes: u64) -> EvmResult<()> where R: frame_system::Config + pallet_evm::Config, @@ -42,6 +52,7 @@ pub(crate) trait PrecompileHandleExt: PrecompileHandle { Ok(()) } + /// Converts `apparent_value` (msg.value) from EVM balance units to Substrate balance. fn try_convert_apparent_value(&self) -> EvmResult where R: pallet_evm::Config, @@ -56,7 +67,7 @@ pub(crate) trait PrecompileHandleExt: PrecompileHandle { Ok(result.into()) } - /// Dispatches a runtime call, but also checks and records the gas costs. + /// Dispatches a runtime call under `origin`, metering weight as EVM gas (and refunding unused). fn try_dispatch_runtime_call( &mut self, call: Call, @@ -99,7 +110,7 @@ pub(crate) trait PrecompileHandleExt: PrecompileHandle { Ok(mut post_info) => { post_info.set_extension_weight(&info); log::debug!("Dispatch succeeded. Post info: {post_info:?}"); - self.charge_and_refund_after_dispatch::(&info, &post_info)?; + self.refund_unused_dispatch_gas::(&info, &post_info)?; Ok(()) } @@ -108,7 +119,7 @@ pub(crate) trait PrecompileHandleExt: PrecompileHandle { let mut post_info = e.post_info; post_info.set_extension_weight(&info); log::info!("Precompile dispatch failed. message as: {e:?}"); - self.charge_and_refund_after_dispatch::(&info, &post_info)?; + self.refund_unused_dispatch_gas::(&info, &post_info)?; Err(PrecompileFailure::Error { exit_status: ExitError::Other( @@ -119,7 +130,8 @@ pub(crate) trait PrecompileHandleExt: PrecompileHandle { } } - fn charge_and_refund_after_dispatch( + /// After dispatch, charges actual weight as gas and refunds the pre-reserved weight surplus. + fn refund_unused_dispatch_gas( &mut self, info: &DispatchInfo, post_info: &PostDispatchInfo, @@ -152,11 +164,15 @@ pub(crate) trait PrecompileHandleExt: PrecompileHandle { impl PrecompileHandleExt for T where T: PrecompileHandle {} +/// Marker trait: frozen `INDEX` → `H160`, derived Substrate account, and admin enable gate. pub trait PrecompileExt>: Precompile { + /// Frozen precompile address index (`H160::from_low_u64_be(INDEX)`). Do not change. const INDEX: u64; - // ss58 public key i.e., the contract sends funds it received to the destination address from - // the method parameter. + /// Substrate account id for this precompile contract (`blake2_256(b"evm:" || address)`). + /// + /// Used when the contract itself is the signed origin (e.g. balance transfer from the + /// precompile's own balance). fn account_id() -> AccountId { let hash = H160::from_low_u64_be(Self::INDEX); let prefix = b"evm:"; @@ -171,6 +187,7 @@ pub trait PrecompileExt>: Precompile { hash.into() } + /// Runs `execute` only when `PrecompileEnable` for `precompile_enum` is true; else errors. fn try_execute( handle: &mut impl PrecompileHandle, precompile_enum: PrecompileEnum, diff --git a/precompiles/src/leasing.rs b/precompiles/src/leasing.rs index 5ebf03cb3c..cd72a60157 100644 --- a/precompiles/src/leasing.rs +++ b/precompiles/src/leasing.rs @@ -1,3 +1,9 @@ +//! Subnet leasing precompile (`INDEX` 2058). +//! +//! Reads lease/share state and creates lease crowdloans that embed +//! `register_leased_network` as the crowdloan `call`. `terminateLease` transfers +//! subnet ownership after the lease ends. + use alloc::{boxed::Box, string::String}; use core::marker::PhantomData; @@ -17,6 +23,7 @@ use subtensor_runtime_common::NetUid; use crate::{PrecompileExt, PrecompileHandleExt}; +/// EVM surface for subnet leases and lease-crowdloan creation. pub struct LeasingPrecompile(PhantomData); impl PrecompileExt for LeasingPrecompile @@ -71,16 +78,17 @@ where + IsSubType>, ::AddressMapping: AddressMapping, { + /// Lease record for `lease_id`, or error if missing. #[precompile::public("getLease(uint32)")] #[precompile::view] - fn get_lease(handle: &mut impl PrecompileHandle, lease_id: u32) -> EvmResult { + fn get_lease(handle: &mut impl PrecompileHandle, lease_id: u32) -> EvmResult { handle.record_db_reads::(1)?; let lease = pallet_subtensor::SubnetLeases::::get(lease_id).ok_or(PrecompileFailure::Error { exit_status: ExitError::Other("Lease not found".into()), })?; - Ok(LeaseInfo { + Ok(EvmLeaseInfo { beneficiary: H256::from_slice(lease.beneficiary.as_slice()), coldkey: H256::from_slice(lease.coldkey.as_slice()), hotkey: H256::from_slice(lease.hotkey.as_slice()), @@ -95,6 +103,7 @@ where }) } + /// Contributor share bits `(int, frac)` for `(lease_id, contributor)`. #[precompile::public("getContributorShare(uint32,bytes32)")] #[precompile::view] fn get_contributor_share( @@ -109,6 +118,7 @@ where Ok((share.int().to_bits(), share.frac().to_bits())) } + /// Lease id owning `netuid`, or error if the subnet is not leased. #[precompile::public("getLeaseIdForSubnet(uint16)")] #[precompile::view] fn get_lease_id_for_subnet(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { @@ -122,6 +132,7 @@ where Ok(lease_id.into()) } + /// Creates a crowdloan whose success call is `register_leased_network`. #[precompile::public("createLeaseCrowdloan(uint64,uint64,uint64,uint32,uint8,bool,uint32)")] #[precompile::payable] #[allow(clippy::too_many_arguments)] @@ -164,6 +175,7 @@ where handle.try_dispatch_runtime_call::(crowdloan_call, RawOrigin::Signed(who)) } + /// Terminates an ended lease and transfers subnet ownership along the beneficiary path. #[precompile::public("terminateLease(uint32,bytes32)")] #[precompile::payable] fn terminate_lease( @@ -179,8 +191,9 @@ where } } +/// Solidity-encoded lease snapshot for `getLease` (ABI field order frozen). #[derive(Codec)] -struct LeaseInfo { +struct EvmLeaseInfo { beneficiary: H256, coldkey: H256, hotkey: H256, @@ -216,11 +229,11 @@ mod tests { const LEASING_END_BLOCK: u32 = 80; const ACCOUNT_BALANCE: u64 = 1_000; - fn expected_lease_info(lease_id: u32) -> LeaseInfo { + fn expected_lease_info(lease_id: u32) -> EvmLeaseInfo { let lease = pallet_subtensor::SubnetLeases::::get(lease_id).expect("lease should exist"); - LeaseInfo { + EvmLeaseInfo { beneficiary: H256::from_slice(lease.beneficiary.as_slice()), coldkey: H256::from_slice(lease.coldkey.as_slice()), hotkey: H256::from_slice(lease.hotkey.as_slice()), @@ -232,7 +245,7 @@ mod tests { } } - fn get_lease(caller: H160, lease_id: u32, expected: LeaseInfo) { + fn get_lease(caller: H160, lease_id: u32, expected: EvmLeaseInfo) { let precompile_addr = addr_from_index(LeasingPrecompile::::INDEX); precompiles::>() diff --git a/precompiles/src/lib.rs b/precompiles/src/lib.rs index d70c3b5eea..8e2db90954 100644 --- a/precompiles/src/lib.rs +++ b/precompiles/src/lib.rs @@ -1,3 +1,14 @@ +//! # Subtensor EVM precompiles +//! +//! Frontier `PrecompileSet` for the Subtensor runtime: standard Ethereum/Frontier +//! precompiles (ECRecover, Modexp, …) plus Bittensor-specific contracts at fixed +//! `H160` addresses derived from each type's `INDEX` via +//! [`precompile_h160_from_index`]. +//! +//! Admin can disable individual Subtensor precompiles through +//! `pallet_admin_utils::PrecompileEnable` (see [`extensions::PrecompileExt::try_execute`]). +//! **Never change** `INDEX` values or `#[precompile::public("…")]` Solidity selectors — +//! they are a frozen EVM ABI surface. #![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; @@ -64,6 +75,7 @@ mod voting_power; #[cfg(test)] mod mock; +/// Runtime precompile set: routes `code_address` to Ethereum, Frontier, or Subtensor handlers. pub struct Precompiles(PhantomData); impl Default for Precompiles @@ -135,40 +147,42 @@ where ::Balance: Into + TryFrom, <::Lookup as StaticLookup>::Source: From, { + /// Constructs an empty marker set (routing is address-based, not stateful). pub fn new() -> Self { Self(Default::default()) } + /// All `H160` addresses this set treats as precompiles (Ethereum + Frontier + Subtensor). pub fn used_addresses() -> [H160; 28] { [ - hash(1), - hash(2), - hash(3), - hash(4), - hash(5), - hash(6), - hash(7), - hash(8), - hash(9), - hash(1024), - hash(1025), - hash(Ed25519Verify::::INDEX), - hash(Sr25519Verify::::INDEX), - hash(BalanceTransferPrecompile::::INDEX), - hash(StakingPrecompile::::INDEX), - hash(SubnetPrecompile::::INDEX), - hash(MetagraphPrecompile::::INDEX), - hash(NeuronPrecompile::::INDEX), - hash(StakingPrecompileV2::::INDEX), - hash(StorageQueryPrecompile::::INDEX), - hash(UidLookupPrecompile::::INDEX), - hash(AlphaPrecompile::::INDEX), - hash(CrowdloanPrecompile::::INDEX), - hash(LeasingPrecompile::::INDEX), - hash(VotingPowerPrecompile::::INDEX), - hash(ProxyPrecompile::::INDEX), - hash(AddressMappingPrecompile::::INDEX), - hash(BalancePrecompile::::INDEX), + precompile_h160_from_index(1), + precompile_h160_from_index(2), + precompile_h160_from_index(3), + precompile_h160_from_index(4), + precompile_h160_from_index(5), + precompile_h160_from_index(6), + precompile_h160_from_index(7), + precompile_h160_from_index(8), + precompile_h160_from_index(9), + precompile_h160_from_index(1024), + precompile_h160_from_index(1025), + precompile_h160_from_index(Ed25519Verify::::INDEX), + precompile_h160_from_index(Sr25519Verify::::INDEX), + precompile_h160_from_index(BalanceTransferPrecompile::::INDEX), + precompile_h160_from_index(StakingPrecompile::::INDEX), + precompile_h160_from_index(SubnetPrecompile::::INDEX), + precompile_h160_from_index(MetagraphPrecompile::::INDEX), + precompile_h160_from_index(NeuronPrecompile::::INDEX), + precompile_h160_from_index(StakingPrecompileV2::::INDEX), + precompile_h160_from_index(StorageQueryPrecompile::::INDEX), + precompile_h160_from_index(UidLookupPrecompile::::INDEX), + precompile_h160_from_index(AlphaPrecompile::::INDEX), + precompile_h160_from_index(CrowdloanPrecompile::::INDEX), + precompile_h160_from_index(LeasingPrecompile::::INDEX), + precompile_h160_from_index(VotingPowerPrecompile::::INDEX), + precompile_h160_from_index(ProxyPrecompile::::INDEX), + precompile_h160_from_index(AddressMappingPrecompile::::INDEX), + precompile_h160_from_index(BalancePrecompile::::INDEX), ] } } @@ -210,74 +224,74 @@ where fn execute(&self, handle: &mut impl PrecompileHandle) -> Option { match handle.code_address() { // Ethereum precompiles : - a if a == hash(1) => Some(ECRecover::execute(handle)), - a if a == hash(2) => Some(Sha256::execute(handle)), - a if a == hash(3) => Some(Ripemd160::execute(handle)), - a if a == hash(4) => Some(Identity::execute(handle)), - a if a == hash(5) => Some(Modexp::execute(handle)), - a if a == hash(6) => Some(Dispatch::::execute(handle)), - a if a == hash(7) => Some(Bn128Mul::execute(handle)), - a if a == hash(8) => Some(Bn128Pairing::execute(handle)), - a if a == hash(9) => Some(Bn128Add::execute(handle)), + a if a == precompile_h160_from_index(1) => Some(ECRecover::execute(handle)), + a if a == precompile_h160_from_index(2) => Some(Sha256::execute(handle)), + a if a == precompile_h160_from_index(3) => Some(Ripemd160::execute(handle)), + a if a == precompile_h160_from_index(4) => Some(Identity::execute(handle)), + a if a == precompile_h160_from_index(5) => Some(Modexp::execute(handle)), + a if a == precompile_h160_from_index(6) => Some(Dispatch::::execute(handle)), + a if a == precompile_h160_from_index(7) => Some(Bn128Mul::execute(handle)), + a if a == precompile_h160_from_index(8) => Some(Bn128Pairing::execute(handle)), + a if a == precompile_h160_from_index(9) => Some(Bn128Add::execute(handle)), // Non-Frontier specific nor Ethereum precompiles : - a if a == hash(1024) => Some(Sha3FIPS256::execute(handle)), - a if a == hash(1025) => Some(ECRecoverPublicKey::execute(handle)), - a if a == hash(Ed25519Verify::::INDEX) => { + a if a == precompile_h160_from_index(1024) => Some(Sha3FIPS256::execute(handle)), + a if a == precompile_h160_from_index(1025) => Some(ECRecoverPublicKey::execute(handle)), + a if a == precompile_h160_from_index(Ed25519Verify::::INDEX) => { Some(Ed25519Verify::::execute(handle)) } - a if a == hash(Sr25519Verify::::INDEX) => { + a if a == precompile_h160_from_index(Sr25519Verify::::INDEX) => { Some(Sr25519Verify::::execute(handle)) } // Subtensor specific precompiles : - a if a == hash(BalanceTransferPrecompile::::INDEX) => { + a if a == precompile_h160_from_index(BalanceTransferPrecompile::::INDEX) => { BalanceTransferPrecompile::::try_execute::( handle, PrecompileEnum::BalanceTransfer, ) } - a if a == hash(StakingPrecompile::::INDEX) => { + a if a == precompile_h160_from_index(StakingPrecompile::::INDEX) => { StakingPrecompile::::try_execute::(handle, PrecompileEnum::Staking) } - a if a == hash(StakingPrecompileV2::::INDEX) => { + a if a == precompile_h160_from_index(StakingPrecompileV2::::INDEX) => { StakingPrecompileV2::::try_execute::(handle, PrecompileEnum::Staking) } - a if a == hash(SubnetPrecompile::::INDEX) => { + a if a == precompile_h160_from_index(SubnetPrecompile::::INDEX) => { SubnetPrecompile::::try_execute::(handle, PrecompileEnum::Subnet) } - a if a == hash(MetagraphPrecompile::::INDEX) => { + a if a == precompile_h160_from_index(MetagraphPrecompile::::INDEX) => { MetagraphPrecompile::::try_execute::(handle, PrecompileEnum::Metagraph) } - a if a == hash(NeuronPrecompile::::INDEX) => { + a if a == precompile_h160_from_index(NeuronPrecompile::::INDEX) => { NeuronPrecompile::::try_execute::(handle, PrecompileEnum::Neuron) } - a if a == hash(UidLookupPrecompile::::INDEX) => { + a if a == precompile_h160_from_index(UidLookupPrecompile::::INDEX) => { UidLookupPrecompile::::try_execute::(handle, PrecompileEnum::UidLookup) } - a if a == hash(StorageQueryPrecompile::::INDEX) => { + a if a == precompile_h160_from_index(StorageQueryPrecompile::::INDEX) => { Some(StorageQueryPrecompile::::execute(handle)) } - a if a == hash(AlphaPrecompile::::INDEX) => { + a if a == precompile_h160_from_index(AlphaPrecompile::::INDEX) => { AlphaPrecompile::::try_execute::(handle, PrecompileEnum::Alpha) } - a if a == hash(CrowdloanPrecompile::::INDEX) => { + a if a == precompile_h160_from_index(CrowdloanPrecompile::::INDEX) => { CrowdloanPrecompile::::try_execute::(handle, PrecompileEnum::Crowdloan) } - a if a == hash(LeasingPrecompile::::INDEX) => { + a if a == precompile_h160_from_index(LeasingPrecompile::::INDEX) => { LeasingPrecompile::::try_execute::(handle, PrecompileEnum::Leasing) } - a if a == hash(VotingPowerPrecompile::::INDEX) => { + a if a == precompile_h160_from_index(VotingPowerPrecompile::::INDEX) => { VotingPowerPrecompile::::try_execute::(handle, PrecompileEnum::VotingPower) } - a if a == hash(ProxyPrecompile::::INDEX) => { + a if a == precompile_h160_from_index(ProxyPrecompile::::INDEX) => { ProxyPrecompile::::try_execute::(handle, PrecompileEnum::Proxy) } - a if a == hash(AddressMappingPrecompile::::INDEX) => { + a if a == precompile_h160_from_index(AddressMappingPrecompile::::INDEX) => { AddressMappingPrecompile::::try_execute::( handle, PrecompileEnum::AddressMapping, ) } - a if a == hash(BalancePrecompile::::INDEX) => { + a if a == precompile_h160_from_index(BalancePrecompile::::INDEX) => { BalancePrecompile::::try_execute::(handle, PrecompileEnum::AccountBalance) } _ => None, @@ -292,15 +306,14 @@ where } } -fn hash(a: u64) -> H160 { - H160::from_low_u64_be(a) +/// Maps a precompile `INDEX` (or Ethereum 1–9 / Frontier 1024–1025 id) to its `H160` address. +fn precompile_h160_from_index(index: u64) -> H160 { + H160::from_low_u64_be(index) } -/* - * - * This is used to parse a slice from bytes with PrecompileFailure as Error - * - */ +/// Slices `data[from..to]` for signature precompiles, mapping OOB to `InvalidRange`. +/// +/// Used by [`Ed25519Verify`] and `Sr25519Verify` (linear-cost raw input layout). fn parse_slice(data: &[u8], from: usize, to: usize) -> Result<&[u8], PrecompileFailure> { let maybe_slice = data.get(from..to); if let Some(slice) = maybe_slice { diff --git a/precompiles/src/metagraph.rs b/precompiles/src/metagraph.rs index d7eeb14a7e..7b3efdcf5a 100644 --- a/precompiles/src/metagraph.rs +++ b/precompiles/src/metagraph.rs @@ -1,3 +1,9 @@ +//! Metagraph (per-uid neuron metrics) view precompile (`INDEX` 2050). +//! +//! Exposes stake, consensus scores, emission, axon endpoint, and hotkey/coldkey +//! for a `(netuid, uid)`. `getRank` / `getTrust` are frozen ABI stubs that always +//! return 0 (metrics no longer computed on-chain). + use alloc::string::String; use core::marker::PhantomData; @@ -10,6 +16,7 @@ use subtensor_runtime_common::{NetUid, Token}; use crate::PrecompileExt; use crate::PrecompileHandleExt; +/// EVM surface for per-`(netuid, uid)` metagraph fields from `pallet_subtensor`. pub struct MetagraphPrecompile(PhantomData); impl PrecompileExt for MetagraphPrecompile @@ -26,6 +33,7 @@ where R: frame_system::Config + pallet_subtensor::Config + pallet_evm::Config, R::AccountId: ByteArray, { + /// Number of registered uids on `netuid` (`SubnetworkN`). #[precompile::public("getUidCount(uint16)")] #[precompile::view] fn get_uid_count(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { @@ -35,6 +43,7 @@ where ))) } + /// Total hotkey stake for the neuron at `(netuid, uid)`. #[precompile::public("getStake(uint16,uint16)")] #[precompile::view] fn get_stake(handle: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { @@ -48,20 +57,21 @@ where Ok(pallet_subtensor::Pallet::::get_total_stake_for_hotkey(&hotkey).to_u64()) } - /// Deprecated: Rank is no longer computed. Always returns 0. + /// Deprecated ABI stub: rank is no longer computed on-chain; always returns 0. #[precompile::public("getRank(uint16,uint16)")] #[precompile::view] fn get_rank(_: &mut impl PrecompileHandle, _netuid: u16, _uid: u16) -> EvmResult { Ok(0) } - /// Deprecated: Trust is no longer computed. Always returns 0. + /// Deprecated ABI stub: trust is no longer computed on-chain; always returns 0. #[precompile::public("getTrust(uint16,uint16)")] #[precompile::view] fn get_trust(_: &mut impl PrecompileHandle, _netuid: u16, _uid: u16) -> EvmResult { Ok(0) } + /// Consensus score for `(netuid, uid)`. #[precompile::public("getConsensus(uint16,uint16)")] #[precompile::view] fn get_consensus(handle: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { @@ -72,6 +82,7 @@ where )) } + /// Incentive score for `(netuid, uid)`. #[precompile::public("getIncentive(uint16,uint16)")] #[precompile::view] fn get_incentive(handle: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { @@ -82,6 +93,7 @@ where )) } + /// Dividend score for `(netuid, uid)`. #[precompile::public("getDividends(uint16,uint16)")] #[precompile::view] fn get_dividends(handle: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { @@ -92,6 +104,7 @@ where )) } + /// Emission for `(netuid, uid)`. #[precompile::public("getEmission(uint16,uint16)")] #[precompile::view] fn get_emission(handle: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { @@ -99,6 +112,7 @@ where Ok(pallet_subtensor::Pallet::::get_emission_for_uid(netuid.into(), uid).into()) } + /// Validator trust for `(netuid, uid)`. #[precompile::public("getVtrust(uint16,uint16)")] #[precompile::view] fn get_vtrust(handle: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { @@ -109,6 +123,7 @@ where )) } + /// Whether `(netuid, uid)` holds a validator permit. #[precompile::public("getValidatorStatus(uint16,uint16)")] #[precompile::view] fn get_validator_status( @@ -123,6 +138,7 @@ where )) } + /// Block of last weights update for `(netuid, uid)`. #[precompile::public("getLastUpdate(uint16,uint16)")] #[precompile::view] fn get_last_update( @@ -137,6 +153,7 @@ where )) } + /// Whether `(netuid, uid)` is marked active. #[precompile::public("getIsActive(uint16,uint16)")] #[precompile::view] fn get_is_active(handle: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { @@ -147,9 +164,14 @@ where )) } + /// Axon endpoint info for the hotkey at `(netuid, uid)`. #[precompile::public("getAxon(uint16,uint16)")] #[precompile::view] - fn get_axon(handle: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { + fn get_axon( + handle: &mut impl PrecompileHandle, + netuid: u16, + uid: u16, + ) -> EvmResult { // Keys + Axons reads handle.record_db_reads::(2)?; let hotkey = pallet_subtensor::Pallet::::get_hotkey_for_net_and_uid(netuid.into(), uid) @@ -160,6 +182,7 @@ where Ok(pallet_subtensor::Pallet::::get_axon_info(netuid.into(), &hotkey).into()) } + /// Hotkey account id for `(netuid, uid)` as `bytes32`. #[precompile::public("getHotkey(uint16,uint16)")] #[precompile::view] fn get_hotkey(handle: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { @@ -171,6 +194,7 @@ where }) } + /// Coldkey owner of the hotkey at `(netuid, uid)` as `bytes32`. #[precompile::public("getColdkey(uint16,uint16)")] #[precompile::view] fn get_coldkey(handle: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { @@ -186,8 +210,9 @@ where } } +/// Solidity-encoded axon endpoint returned by `getAxon` (ABI field order frozen). #[derive(Codec)] -struct AxonInfo { +struct MetagraphAxonInfo { block: u64, version: u32, ip: u128, @@ -196,7 +221,7 @@ struct AxonInfo { protocol: u8, } -impl From for AxonInfo { +impl From for MetagraphAxonInfo { fn from(value: SubtensorModuleAxonInfo) -> Self { Self { block: value.block, diff --git a/precompiles/src/mock.rs b/precompiles/src/mock.rs index d42fb253eb..febb8738fe 100644 --- a/precompiles/src/mock.rs +++ b/precompiles/src/mock.rs @@ -2,6 +2,12 @@ #![allow(clippy::expect_used)] #![allow(clippy::arithmetic_side_effects)] +//! Shared test runtime and helpers for precompile unit tests. +//! +//! Builds a minimal `construct_runtime!` with Subtensor, EVM, proxy, swap, and +//! related pallets, plus helpers to fund accounts, map H160 to AccountId, and +//! execute a single-precompile set. + use core::{marker::PhantomData, num::NonZeroU64}; use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; @@ -431,8 +437,8 @@ impl AuthorshipInfo for MockAuthorshipProvider { } } -pub struct CommitmentsI; -impl pallet_subtensor::CommitmentsInterface for CommitmentsI { +pub struct CommitmentsPurgeBridge; +impl pallet_subtensor::CommitmentsInterface for CommitmentsPurgeBridge { fn purge_netuid( _netuid: NetUid, _weight_meter: &mut frame_support::weights::WeightMeter, @@ -518,7 +524,7 @@ impl pallet_subtensor::Config for Runtime { type LeaseDividendsDistributionInterval = LeaseDividendsDistributionInterval; type GetCommitments = (); type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; - type CommitmentsInterface = CommitmentsI; + type CommitmentsInterface = CommitmentsPurgeBridge; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; type AuthorshipProvider = MockAuthorshipProvider; type SubtensorPalletId = SubtensorPalletId; @@ -556,6 +562,7 @@ impl pallet_subtensor_proxy::Config for Runtime { type BlockNumberProvider = System; } +/// [`PrecompileSet`] that routes only to one precompile at `H160::from_low_u64_be(P::INDEX)`. pub(crate) struct SinglePrecompileSet

(PhantomData

); impl

Default for SinglePrecompileSet

{ @@ -580,6 +587,7 @@ where } } +/// Construct a [`SinglePrecompileSet`] for precompile type `P`. pub(crate) fn precompiles

() -> SinglePrecompileSet

where P: pallet_evm::Precompile + PrecompileExt, @@ -587,6 +595,7 @@ where SinglePrecompileSet::default() } +/// Genesis externalities with block number set to 1. pub(crate) fn new_test_ext() -> sp_io::TestExternalities { let mut ext: sp_io::TestExternalities = RuntimeGenesisConfig::default() .build_storage() @@ -596,6 +605,7 @@ pub(crate) fn new_test_ext() -> sp_io::TestExternalities { ext } +/// Execute `input` against `precompiles` with the given caller and apparent value. pub(crate) fn execute_precompile( precompiles: &PSet, precompile_address: H160, @@ -615,14 +625,17 @@ pub(crate) fn execute_precompile( precompiles.execute(&mut handle) } +/// Map a precompile INDEX (or test id) to the low-64-be H160 address used in tests. pub(crate) fn addr_from_index(index: u64) -> H160 { H160::from_low_u64_be(index) } +/// HashedAddressMapping of an H160 into the mock runtime AccountId. pub(crate) fn mapped_account(address: H160) -> AccountId { ::AddressMapping::into_account_id(address) } +/// Mint `amount` RAO of TAO to `account` via the subtensor pallet. pub(crate) fn fund_account(account: &AccountId, amount: u64) { let amount = TaoBalance::from(amount); let credit = pallet_subtensor::Pallet::::mint_tao(amount); @@ -649,6 +662,7 @@ pub(crate) fn assert_static_call( .execute_returns_raw(abi_word(expected)); } +/// First four bytes of keccak256(`signature`) as the Solidity function selector. pub(crate) fn selector_u32(signature: &str) -> u32 { let hash = sp_io::hashing::keccak_256(signature.as_bytes()); u32::from_be_bytes([hash[0], hash[1], hash[2], hash[3]]) diff --git a/precompiles/src/neuron.rs b/precompiles/src/neuron.rs index 8a7eac497f..74f6f8daa4 100644 --- a/precompiles/src/neuron.rs +++ b/precompiles/src/neuron.rs @@ -1,3 +1,8 @@ +//! Neuron precompile: weights, registration, and axon/prometheus serve from EVM. +//! +//! Each method dispatches a `pallet_subtensor` call as the EVM caller coldkey. +//! INDEX and Solidity selectors are frozen. + use core::marker::PhantomData; use frame_support::dispatch::{DispatchInfo, GetDispatchInfo, PostDispatchInfo}; diff --git a/precompiles/src/proxy.rs b/precompiles/src/proxy.rs index 78d59f5ce2..9e9600f210 100644 --- a/precompiles/src/proxy.rs +++ b/precompiles/src/proxy.rs @@ -1,3 +1,9 @@ +//! Proxy precompile: create/kill pure proxies, add/remove proxies, and proxy-call from EVM. +//! +//! Dispatches into `pallet_subtensor_proxy` as the EVM caller mapped Substrate account. +//! `proxyCall` SCALE-decodes the inner runtime call with a bounded depth. +//! INDEX and Solidity selectors are frozen. + use core::marker::PhantomData; use crate::{PrecompileExt, PrecompileHandleExt}; @@ -20,8 +26,11 @@ use sp_std::convert::{TryFrom, TryInto}; use sp_std::vec; use sp_std::vec::Vec; use subtensor_runtime_common::ProxyType; +/// EVM surface for Substrate proxy lifecycle and filtered proxy dispatch (INDEX 2059). pub struct ProxyPrecompile(PhantomData); -const MAX_DECODE_DEPTH: u32 = 8; + +/// Max SCALE decode nesting for `proxyCall` inner runtime-call payloads. +const PROXY_CALL_MAX_DECODE_DEPTH: u32 = 8; impl PrecompileExt for ProxyPrecompile where @@ -78,6 +87,7 @@ where + IsSubType>, <::Lookup as StaticLookup>::Source: From, { + /// Create a pure proxy account and return its Substrate account id as `bytes32`. #[precompile::public("createPureProxy(uint8,uint32,uint16)")] pub fn create_pure_proxy( handle: &mut impl PrecompileHandle, @@ -123,6 +133,7 @@ where }) } + /// Kill a pure proxy previously spawned by `spawner` (must match create params). #[precompile::public("killPureProxy(bytes32,uint8,uint16,uint32,uint32)")] pub fn kill_pure_proxy( handle: &mut impl PrecompileHandle, @@ -150,6 +161,7 @@ where handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } + /// Dispatch a SCALE-encoded runtime call as `real`, optionally forcing a proxy type. #[precompile::public("proxyCall(bytes32,uint8[],uint8[])")] pub fn proxy_call( handle: &mut impl PrecompileHandle, @@ -160,7 +172,7 @@ where let account_id = handle.caller_account_id::(); let call = ::RuntimeCall::decode_with_depth_limit( - MAX_DECODE_DEPTH, + PROXY_CALL_MAX_DECODE_DEPTH, &mut &call[..], ) .map_err(|_| PrecompileFailure::Error { @@ -201,6 +213,7 @@ where } } + /// Register `delegate` as a proxy of the caller with the given type and delay. #[precompile::public("addProxy(bytes32,uint8,uint32)")] pub fn add_proxy( handle: &mut impl PrecompileHandle, @@ -224,6 +237,7 @@ where handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } + /// Remove a previously registered proxy matching type and delay. #[precompile::public("removeProxy(bytes32,uint8,uint32)")] pub fn remove_proxy( handle: &mut impl PrecompileHandle, @@ -247,6 +261,7 @@ where handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } + /// Remove all proxies for the caller and unlock the reserved deposit. #[precompile::public("removeProxies()")] pub fn remove_proxies(handle: &mut impl PrecompileHandle) -> EvmResult<()> { let account_id = handle.caller_account_id::(); @@ -256,6 +271,7 @@ where handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } + /// Recompute and adjust the caller proxy deposit reservation. #[precompile::public("pokeDeposit()")] pub fn poke_deposit(handle: &mut impl PrecompileHandle) -> EvmResult<()> { let account_id = handle.caller_account_id::(); @@ -265,6 +281,7 @@ where handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } + /// List proxies for `account_id` as `(delegate, proxy_type, delay)` tuples. #[precompile::public("getProxies(bytes32)")] #[precompile::view] pub fn get_proxies( diff --git a/precompiles/src/sr25519.rs b/precompiles/src/sr25519.rs index 324bd7abca..3a99082c9b 100644 --- a/precompiles/src/sr25519.rs +++ b/precompiles/src/sr25519.rs @@ -1,3 +1,10 @@ +//! Sr25519 signature verification precompile (INDEX 1027). +//! +//! Input layout (after the 4-byte Solidity selector when routed via the set): +//! `msg[32] || pubkey[32] || signature[64]`. Returns a 32-byte word with `1` in +//! the last byte on success and `0` otherwise. Gas uses the Ed25519 base cost +//! (no EIP for sr25519). INDEX is frozen. + extern crate alloc; use alloc::vec::Vec; @@ -9,6 +16,7 @@ use fp_evm::{ExitError, ExitSucceed, LinearCostPrecompile, PrecompileFailure}; use crate::{PrecompileExt, parse_slice}; +/// Linear-cost precompile that verifies an sr25519 signature over a 32-byte message. pub struct Sr25519Verify(PhantomData); impl PrecompileExt for Sr25519Verify diff --git a/precompiles/src/staking.rs b/precompiles/src/staking.rs index 7f47bf8ab3..cb533ae5a6 100644 --- a/precompiles/src/staking.rs +++ b/precompiles/src/staking.rs @@ -1,34 +1,21 @@ -// The goal of staking precompile is to allow interaction between EVM users and smart contracts and -// subtensor staking functionality, namely add_stake, and remove_stake extrinsicsk, as well as the -// staking state. -// -// Additional requirement is to preserve compatibility with Ethereum indexers, which requires -// no balance transfers from EVM accounts without a corresponding transaction that can be -// parsed by an indexer. -// -// Implementation of add_stake: -// - User transfers balance that will be staked to the precompile address with a payable -// method addStake. This method also takes hotkey public key (bytes32) of the hotkey -// that the stake should be assigned to. -// - Precompile transfers the balance back to the signing address, and then invokes -// do_add_stake from subtensor pallet with signing origin that mmatches to HashedAddressMapping -// of the message sender, which will effectively withdraw and stake balance from the message -// sender. -// - Precompile checks the result of do_add_stake and, in case of a failure, reverts the transaction, -// and leaves the balance on the message sender account. -// -// Implementation of remove_stake: -// - User involkes removeStake method and specifies hotkey public key (bytes32) of the hotkey -// to remove stake from, and the amount to unstake. -// - Precompile calls do_remove_stake method of the subtensor pallet with the signing origin of message -// sender, which effectively unstakes the specified amount and credits it to the message sender -// - Precompile checks the result of do_remove_stake and, in case of a failure, reverts the transaction. -// -// Without an approve/allowance system, when an EOA transfers stake to a contract it is impossible for the -// contract to know who sent funds and how much. For that reason, the precompile provides an `approve` -// function for the sender to approve a spender (the contract) to call `transferStakeFrom`. -// The allowance is specific to a pair of `(spender, netuid)`, but doesn't specify the `hotkey` which is instead -// provided only in `transferStakeFrom`. +//! EVM staking precompiles for add/remove/move/transfer stake and stake-state reads. +//! +//! ## Indexer-safe value flow (legacy V1) +//! Ethereum indexers require every balance movement to correspond to a parseable EVM +//! transaction. Legacy [`StakingPrecompile`] therefore: +//! 1. Accepts TAO via a payable `addStake` call (value lands on the precompile account). +//! 2. Refunds that value to the caller, then dispatches `pallet_subtensor::add_stake` +//! with the caller's mapped Substrate origin so the stake is withdrawn from the caller. +//! 3. Reverts the whole EVM call if the runtime dispatch fails, leaving the refunded balance. +//! +//! [`StakingPrecompileV2`] takes stake amounts as ABI arguments (RAO / alpha units) and +//! is the surface for all new methods. V1 remains only for backward compatibility. +//! +//! ## Allowances +//! Without approve/allowance, a contract cannot tell which EOA funded it. Callers +//! `approve` a spender for a `(spender, netuid)` pair (keyed with a registration +//! counter so dissolved/re-registered netuids invalidate old allowances); the spender +//! then calls `transferStakeFrom` with the hotkey. use alloc::collections::BTreeSet; use alloc::vec::Vec; @@ -53,22 +40,9 @@ use subtensor_runtime_common::{AlphaBalance, NetUid, ProxyType, Token}; use crate::{PrecompileExt, PrecompileHandleExt}; -// `get_stake_for_hotkey_and_coldkey_on_subnet` reads the transitional V1/V2 -// share storage. In the V2 fallback case it performs two reads for the initial -// share lookup, then five more for the value, share, and denominator. -const STAKE_INFO_READS_PER_HOTKEY: u64 = 7; -// Conservative charge for decoding and validating each 32-byte hotkey. -const STAKE_INFO_INPUT_GAS_PER_HOTKEY: u64 = 64; -const MAX_STAKE_INFO_HOTKEYS: usize = 64; -const MAX_CONVICTION_HOTKEYS: usize = 64; -// Individual state reads the lock row, mode, owner hotkey, global rates, and current block. -const COLDKEY_LOCK_READS: u64 = 6; -// Aggregate state reads the owner hotkey, global rates, current block, and up to four buckets. -const HOTKEY_LOCK_READS: u64 = 8; - -/// Prefix for the Allowances map in Substrate storage. -pub struct AllowancesPrefix; -impl StorageInstance for AllowancesPrefix { +/// Twox storage-instance prefix for [`AllowancesStorage`] under pallet `EvmPrecompileStaking`. +pub struct StakingAllowancesPrefix; +impl StorageInstance for StakingAllowancesPrefix { const STORAGE_PREFIX: &'static str = "Allowances"; fn pallet_prefix() -> &'static str { @@ -77,7 +51,7 @@ impl StorageInstance for AllowancesPrefix { } pub type AllowancesStorage = StorageDoubleMap< - AllowancesPrefix, + StakingAllowancesPrefix, // For each approver (EVM address as only EVM-natives need the precompile) Blake2_128Concat, H160, @@ -90,11 +64,22 @@ pub type AllowancesStorage = StorageDoubleMap< ValueQuery, >; -// Old StakingPrecompile had ETH-precision in values, which was not alligned with Substrate API. So -// it's kinda deprecated, but exists for backward compatibility. Eventually, we should remove it -// to stop supporting both precompiles. -// -// All the future extensions should happen in StakingPrecompileV2. +// `get_stake_for_hotkey_and_coldkey_on_subnet` reads the transitional V1/V2 +// share storage. In the V2 fallback case it performs two reads for the initial +// share lookup, then five more for the value, share, and denominator. +const STAKE_INFO_READS_PER_HOTKEY: u64 = 7; +// Conservative charge for decoding and validating each 32-byte hotkey. +const STAKE_INFO_INPUT_GAS_PER_HOTKEY: u64 = 64; +const MAX_STAKE_INFO_HOTKEYS: usize = 64; +const MAX_CONVICTION_HOTKEYS: usize = 64; +// Individual state reads the lock row, mode, owner hotkey, global rates, and current block. +const COLDKEY_LOCK_READS: u64 = 6; +// Aggregate state reads the owner hotkey, global rates, current block, and up to four buckets. +const HOTKEY_LOCK_READS: u64 = 8; + +/// Current staking precompile (INDEX 2053): RAO/alpha ABI amounts, locks, allowances, proxies. +/// +/// Prefer this over legacy [`StakingPrecompile`]. New Solidity methods belong here only. pub struct StakingPrecompileV2(PhantomData); impl PrecompileExt for StakingPrecompileV2 @@ -151,6 +136,7 @@ where ::AddressMapping: AddressMapping, <::Lookup as StaticLookup>::Source: From, { + /// Stake `amount_rao` TAO to `address` (hotkey) on `netuid` (amounts in Substrate units). #[precompile::public("addStake(bytes32,uint256,uint256)")] #[precompile::payable] fn add_stake( @@ -162,7 +148,7 @@ where let account_id = handle.caller_account_id::(); let amount_staked: u64 = amount_rao.unique_saturated_into(); let hotkey = R::AccountId::from(address.0); - let netuid = try_u16_from_u256(netuid)?; + let netuid = u16_from_evm_u256(netuid)?; let call = pallet_subtensor::Call::::add_stake { hotkey, netuid: netuid.into(), @@ -182,7 +168,7 @@ where ) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let hotkey = R::AccountId::from(address.0); - let netuid = try_u16_from_u256(netuid)?; + let netuid = u16_from_evm_u256(netuid)?; let amount_unstaked: u64 = amount_alpha.unique_saturated_into(); let call = pallet_subtensor::Call::::remove_stake { hotkey, @@ -193,7 +179,7 @@ where handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } - fn call_remove_stake_full_limit( + fn dispatch_remove_stake_full_limit( handle: &mut impl PrecompileHandle, hotkey: H256, netuid: U256, @@ -201,7 +187,7 @@ where ) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let hotkey = R::AccountId::from(hotkey.0); - let netuid = try_u16_from_u256(netuid)?; + let netuid = u16_from_evm_u256(netuid)?; let call = pallet_subtensor::Call::::remove_stake_full_limit { hotkey, netuid: netuid.into(), @@ -218,7 +204,7 @@ where hotkey: H256, netuid: U256, ) -> EvmResult<()> { - Self::call_remove_stake_full_limit(handle, hotkey, netuid, None) + Self::dispatch_remove_stake_full_limit(handle, hotkey, netuid, None) } #[precompile::public("removeStakeFullLimit(bytes32,uint256,uint256)")] @@ -229,8 +215,8 @@ where netuid: U256, limit_price: U256, ) -> EvmResult<()> { - let limit_price = try_u64_from_u256(limit_price)?; - Self::call_remove_stake_full_limit(handle, hotkey, netuid, Some(limit_price)) + let limit_price = u64_from_evm_u256(limit_price)?; + Self::dispatch_remove_stake_full_limit(handle, hotkey, netuid, Some(limit_price)) } #[precompile::public("moveStake(bytes32,bytes32,uint256,uint256,uint256)")] @@ -246,8 +232,8 @@ where let account_id = handle.caller_account_id::(); let origin_hotkey = R::AccountId::from(origin_hotkey.0); let destination_hotkey = R::AccountId::from(destination_hotkey.0); - let origin_netuid = try_u16_from_u256(origin_netuid)?; - let destination_netuid = try_u16_from_u256(destination_netuid)?; + let origin_netuid = u16_from_evm_u256(origin_netuid)?; + let destination_netuid = u16_from_evm_u256(destination_netuid)?; let alpha_amount: u64 = amount_alpha.unique_saturated_into(); let call = pallet_subtensor::Call::::move_stake { origin_hotkey, @@ -273,8 +259,8 @@ where let account_id = handle.caller_account_id::(); let destination_coldkey = R::AccountId::from(destination_coldkey.0); let hotkey = R::AccountId::from(hotkey.0); - let origin_netuid = try_u16_from_u256(origin_netuid)?; - let destination_netuid = try_u16_from_u256(destination_netuid)?; + let origin_netuid = u16_from_evm_u256(origin_netuid)?; + let destination_netuid = u16_from_evm_u256(destination_netuid)?; let alpha_amount: u64 = amount_alpha.unique_saturated_into(); let call = pallet_subtensor::Call::::transfer_stake { destination_coldkey, @@ -297,7 +283,7 @@ where ) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let hotkey = R::AccountId::from(hotkey.0); - let netuid = try_u16_from_u256(netuid)?; + let netuid = u16_from_evm_u256(netuid)?; let amount: u64 = amount.unique_saturated_into(); let call = pallet_subtensor::Call::::burn_alpha { hotkey, @@ -345,7 +331,7 @@ where handle.record_db_reads::(STAKE_INFO_READS_PER_HOTKEY)?; let hotkey = R::AccountId::from(hotkey.0); let coldkey = R::AccountId::from(coldkey.0); - let netuid = try_u16_from_u256(netuid)?; + let netuid = u16_from_evm_u256(netuid)?; let stake = pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( &hotkey, &coldkey, @@ -368,7 +354,7 @@ where let hotkeys: Vec = hotkeys.into(); let coldkey = R::AccountId::from(coldkey.0); - let netuid = NetUid::from(try_u16_from_u256(netuid)?); + let netuid = NetUid::from(u16_from_evm_u256(netuid)?); let mut seen = BTreeSet::new(); for hotkey in &hotkeys { @@ -410,7 +396,7 @@ where ) -> EvmResult> { let hotkey = R::AccountId::from(hotkey.0); let mut coldkeys: Vec = vec![]; - let netuid = NetUid::from(try_u16_from_u256(netuid)?); + let netuid = NetUid::from(u16_from_evm_u256(netuid)?); for (coldkey, netuid_in_alpha, _) in pallet_subtensor::Pallet::::alpha_iter_single_prefix(&hotkey) { @@ -433,7 +419,7 @@ where ) -> EvmResult { handle.record_db_reads::(2)?; let hotkey = R::AccountId::from(hotkey.0); - let netuid = try_u16_from_u256(netuid)?; + let netuid = u16_from_evm_u256(netuid)?; let stake = pallet_subtensor::Pallet::::get_stake_for_hotkey_on_subnet(&hotkey, netuid.into()); @@ -470,8 +456,8 @@ where ) -> EvmResult<()> { let call = pallet_subtensor::Call::::lock_stake { hotkey: R::AccountId::from(hotkey.0), - netuid: NetUid::from(try_u16_from_u256(netuid)?), - amount: try_u64_from_u256(amount_alpha)?.into(), + netuid: NetUid::from(u16_from_evm_u256(netuid)?), + amount: u64_from_evm_u256(amount_alpha)?.into(), }; handle.try_dispatch_runtime_call::( @@ -490,7 +476,7 @@ where ) -> EvmResult<()> { let call = pallet_subtensor::Call::::move_lock { destination_hotkey: R::AccountId::from(destination_hotkey.0), - netuid: NetUid::from(try_u16_from_u256(netuid)?), + netuid: NetUid::from(u16_from_evm_u256(netuid)?), }; handle.try_dispatch_runtime_call::( @@ -508,7 +494,7 @@ where enabled: bool, ) -> EvmResult<()> { let call = pallet_subtensor::Call::::set_perpetual_lock { - netuid: NetUid::from(try_u16_from_u256(netuid)?), + netuid: NetUid::from(u16_from_evm_u256(netuid)?), enabled, }; @@ -544,7 +530,7 @@ where ) -> EvmResult<(bool, H256, U256, u128, bool)> { handle.record_db_reads::(COLDKEY_LOCK_READS)?; let coldkey = R::AccountId::from(coldkey.0); - let netuid = NetUid::from(try_u16_from_u256(netuid)?); + let netuid = NetUid::from(u16_from_evm_u256(netuid)?); let perpetual = pallet_subtensor::DecayingLock::::get(&coldkey, netuid) == Some(false); let Some((hotkey, lock)) = @@ -588,7 +574,7 @@ where ) -> EvmResult<(bool, U256, u128)> { handle.record_db_reads::(HOTKEY_LOCK_READS)?; let hotkey = R::AccountId::from(hotkey.0); - let netuid = NetUid::from(try_u16_from_u256(netuid)?); + let netuid = NetUid::from(u16_from_evm_u256(netuid)?); let now = pallet_subtensor::Pallet::::get_current_block_as_u64(); let unlock_rate = pallet_subtensor::UnlockRate::::get(); let maturity_rate = pallet_subtensor::MaturityRate::::get(); @@ -650,7 +636,7 @@ where let hotkey_count: u64 = hotkeys.len().unique_saturated_into(); handle.record_cost(hotkey_count.saturating_mul(STAKE_INFO_INPUT_GAS_PER_HOTKEY))?; let hotkeys: Vec = hotkeys.into(); - let netuid = NetUid::from(try_u16_from_u256(netuid)?); + let netuid = NetUid::from(u16_from_evm_u256(netuid)?); let mut seen = BTreeSet::new(); for hotkey in &hotkeys { @@ -739,7 +725,7 @@ where let amount_staked: u64 = amount_rao.unique_saturated_into(); let limit_price: u64 = limit_price_rao.unique_saturated_into(); let hotkey = R::AccountId::from(address.0); - let netuid = try_u16_from_u256(netuid)?; + let netuid = u16_from_evm_u256(netuid)?; let call = pallet_subtensor::Call::::add_stake_limit { hotkey, netuid: netuid.into(), @@ -763,7 +749,7 @@ where ) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let hotkey = R::AccountId::from(address.0); - let netuid = try_u16_from_u256(netuid)?; + let netuid = u16_from_evm_u256(netuid)?; let amount_unstaked: u64 = amount_alpha.unique_saturated_into(); let limit_price: u64 = limit_price_rao.unique_saturated_into(); let call = pallet_subtensor::Call::::remove_stake_limit { @@ -787,7 +773,7 @@ where // StakingHotkeys + per-hotkey stake reads handle.record_db_reads::(2)?; let coldkey = R::AccountId::from(coldkey.0); - let netuid = try_u16_from_u256(netuid)?; + let netuid = u16_from_evm_u256(netuid)?; let stake = pallet_subtensor::Pallet::::get_total_stake_for_coldkey_on_subnet( &coldkey, netuid.into(), @@ -799,10 +785,11 @@ where /// Current registration counter for `netuid`, used as part of the /// `AllowancesStorage` secondary key to invalidate approvals granted /// for a previous registration of the same netuid. - fn current_subnet_counter(netuid: u16) -> u64 { + fn netuid_registration_counter(netuid: u16) -> u64 { pallet_subtensor::Pallet::::get_registered_subnet_counter(netuid.into()) } + /// Set alpha allowance for `spender` on `origin_netuid` (zero clears the entry). #[precompile::public("approve(address,uint256,uint256)")] fn approve( handle: &mut impl PrecompileHandle, @@ -816,8 +803,8 @@ where let approver = handle.context().caller; let spender = spender_address.0; - let netuid = try_u16_from_u256(origin_netuid)?; - let counter = Self::current_subnet_counter(netuid); + let netuid = u16_from_evm_u256(origin_netuid)?; + let counter = Self::netuid_registration_counter(netuid); if amount_alpha.is_zero() { AllowancesStorage::remove(approver, (spender, netuid, counter)); @@ -840,8 +827,8 @@ where handle.record_db_reads::(2)?; let spender = spender_address.0; - let netuid = try_u16_from_u256(origin_netuid)?; - let counter = Self::current_subnet_counter(netuid); + let netuid = u16_from_evm_u256(origin_netuid)?; + let counter = Self::netuid_registration_counter(netuid); Ok(AllowancesStorage::get( source_address.0, @@ -866,8 +853,8 @@ where let approver = handle.context().caller; let spender = spender_address.0; - let netuid = try_u16_from_u256(origin_netuid)?; - let counter = Self::current_subnet_counter(netuid); + let netuid = u16_from_evm_u256(origin_netuid)?; + let counter = Self::netuid_registration_counter(netuid); let approval_key = (spender, netuid, counter); @@ -896,8 +883,8 @@ where let approver = handle.context().caller; let spender = spender_address.0; - let netuid = try_u16_from_u256(origin_netuid)?; - let counter = Self::current_subnet_counter(netuid); + let netuid = u16_from_evm_u256(origin_netuid)?; + let counter = Self::netuid_registration_counter(netuid); let approval_key = (spender, netuid, counter); @@ -913,7 +900,7 @@ where Ok(()) } - fn try_consume_allowance( + fn consume_stake_allowance( handle: &mut impl PrecompileHandle, approver: H160, spender: H160, @@ -928,7 +915,7 @@ where handle.record_db_reads::(2)?; handle.record_db_writes::(1)?; - let counter = Self::current_subnet_counter(netuid); + let counter = Self::netuid_registration_counter(netuid); let approval_key = (spender, netuid, counter); let current_amount = AllowancesStorage::get(approver, approval_key); @@ -945,6 +932,7 @@ where Ok(()) } + /// Spender moves stake from `source` to `destination` after consuming allowance. #[precompile::public("transferStakeFrom(address,address,bytes32,uint256,uint256,uint256)")] fn transfer_stake_from( handle: &mut impl PrecompileHandle, @@ -960,11 +948,17 @@ where let destination_coldkey = ::AddressMapping::into_account_id(destination_address.0); let hotkey = R::AccountId::from(hotkey.0); - let origin_netuid = try_u16_from_u256(origin_netuid)?; - let destination_netuid = try_u16_from_u256(destination_netuid)?; + let origin_netuid = u16_from_evm_u256(origin_netuid)?; + let destination_netuid = u16_from_evm_u256(destination_netuid)?; let alpha_amount: u64 = amount_alpha.unique_saturated_into(); - Self::try_consume_allowance(handle, source_address, spender, origin_netuid, amount_alpha)?; + Self::consume_stake_allowance( + handle, + source_address, + spender, + origin_netuid, + amount_alpha, + )?; let call = pallet_subtensor::Call::::transfer_stake { destination_coldkey, @@ -979,7 +973,9 @@ where } } -// Deprecated, exists for backward compatibility. +/// Legacy staking precompile (INDEX 2049): ETH-decimal amounts via payable `addStake`. +/// +/// Kept for indexer-compatible callers; do not add new methods — extend [`StakingPrecompileV2`]. pub struct StakingPrecompile(PhantomData); impl PrecompileExt for StakingPrecompile @@ -1047,12 +1043,12 @@ where let amount = handle.context().apparent_value; if !amount.is_zero() { - Self::transfer_back_to_caller(&account_id, amount)?; + Self::refund_evm_value_to_caller(&account_id, amount)?; } let amount_sub = handle.try_convert_apparent_value::()?; let hotkey = R::AccountId::from(address.0); - let netuid = try_u16_from_u256(netuid)?; + let netuid = u16_from_evm_u256(netuid)?; let amount_staked: u64 = amount_sub.unique_saturated_into(); let call = pallet_subtensor::Call::::add_stake { hotkey, @@ -1073,7 +1069,7 @@ where ) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let hotkey = R::AccountId::from(address.0); - let netuid = try_u16_from_u256(netuid)?; + let netuid = u16_from_evm_u256(netuid)?; let amount = EvmBalance::new(amount); let amount_unstaked = ::BalanceConverter::into_substrate_balance(amount) @@ -1141,7 +1137,7 @@ where handle.record_db_reads::(STAKE_INFO_READS_PER_HOTKEY)?; let hotkey = R::AccountId::from(hotkey.0); let coldkey = R::AccountId::from(coldkey.0); - let netuid = try_u16_from_u256(netuid)?; + let netuid = u16_from_evm_u256(netuid)?; let stake = pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( &hotkey, &coldkey, @@ -1185,7 +1181,7 @@ where handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } - fn transfer_back_to_caller( + fn refund_evm_value_to_caller( account_id: &::AccountId, amount: U256, ) -> Result<(), PrecompileFailure> { @@ -1220,13 +1216,15 @@ where } } -fn try_u16_from_u256(value: U256) -> Result { +/// Narrow an ABI `uint256` to `u16` or revert with an out-of-bounds error. +fn u16_from_evm_u256(value: U256) -> Result { value.try_into().map_err(|_| PrecompileFailure::Error { exit_status: ExitError::Other("the value is outside of u16 bounds".into()), }) } -fn try_u64_from_u256(value: U256) -> Result { +/// Narrow an ABI `uint256` to `u64` or revert with an out-of-bounds error. +fn u64_from_evm_u256(value: U256) -> Result { value.try_into().map_err(|_| PrecompileFailure::Error { exit_status: ExitError::Other("the value is outside of u64 bounds".into()), }) @@ -1248,6 +1246,7 @@ mod tests { execute_precompile, fund_account, mapped_account, new_test_ext, precompiles, selector_u32, substrate_to_evm, }; + use frame_support::traits::Get; use precompile_utils::prelude::RuntimeHelper; use precompile_utils::solidity::{encode_return_value, encode_with_selector}; use precompile_utils::testing::PrecompileTesterExt; @@ -1536,60 +1535,6 @@ mod tests { }); } - #[test] - fn staking_precompile_v2_only_reads_caller_supplied_hotkeys() { - new_test_ext().execute_with(|| { - let netuid = setup_staking_subnet(); - let caller = addr_from_index(0x1105); - let coldkey = mapped_account(caller); - let historical_hotkeys: Vec = (0..=MAX_STAKE_INFO_HOTKEYS) - .map(|index| { - let mut account = [0u8; 32]; - let index = u64::try_from(index).expect("test index fits in u64"); - account[..8].copy_from_slice(&index.to_le_bytes()); - AccountId::from(account) - }) - .collect(); - let active_hotkey = historical_hotkeys - .last() - .expect("historical hotkeys is non-empty") - .clone(); - - pallet_subtensor::StakingHotkeys::::insert( - &coldkey, - historical_hotkeys.clone(), - ); - pallet_subtensor::Pallet::::increase_stake_for_hotkey_and_coldkey_on_subnet( - &active_hotkey, - &coldkey, - netuid, - AlphaBalance::from(INITIAL_STAKE_RAO), - ); - let active_stake = stake_for(&active_hotkey, &coldkey, netuid); - assert!(active_stake > 0); - - precompiles::>() - .prepare_test( - caller, - addr_from_index(StakingPrecompileV2::::INDEX), - encode_with_selector( - selector_u32("getStakeInfoForColdkeyAndNetuid(bytes32,uint256,bytes32[])"), - ( - H256::from_slice(coldkey.as_ref()), - U256::from(TEST_NETUID_U16), - vec![H256::from_slice(active_hotkey.as_ref())], - ), - ), - ) - .with_static_call(true) - .expect_cost(stake_info_cost(1)) - .execute_returns_raw(encode_return_value(vec![( - H256::from_slice(active_hotkey.as_ref()), - U256::from(active_stake), - )])); - }); - } - #[test] fn staking_precompile_v2_codec_rejects_more_than_64_requested_hotkeys() { new_test_ext().execute_with(|| { @@ -2173,6 +2118,60 @@ mod tests { }); } + #[test] + fn staking_precompile_v2_only_reads_caller_supplied_hotkeys() { + new_test_ext().execute_with(|| { + let netuid = setup_staking_subnet(); + let caller = addr_from_index(0x1105); + let coldkey = mapped_account(caller); + let historical_hotkeys: Vec = (0..=MAX_STAKE_INFO_HOTKEYS) + .map(|index| { + let mut account = [0u8; 32]; + let index = u64::try_from(index).expect("test index fits in u64"); + account[..8].copy_from_slice(&index.to_le_bytes()); + AccountId::from(account) + }) + .collect(); + let active_hotkey = historical_hotkeys + .last() + .expect("historical hotkeys is non-empty") + .clone(); + + pallet_subtensor::StakingHotkeys::::insert( + &coldkey, + historical_hotkeys.clone(), + ); + pallet_subtensor::Pallet::::increase_stake_for_hotkey_and_coldkey_on_subnet( + &active_hotkey, + &coldkey, + netuid, + AlphaBalance::from(INITIAL_STAKE_RAO), + ); + let active_stake = stake_for(&active_hotkey, &coldkey, netuid); + assert!(active_stake > 0); + + precompiles::>() + .prepare_test( + caller, + addr_from_index(StakingPrecompileV2::::INDEX), + encode_with_selector( + selector_u32("getStakeInfoForColdkeyAndNetuid(bytes32,uint256,bytes32[])"), + ( + H256::from_slice(coldkey.as_ref()), + U256::from(TEST_NETUID_U16), + vec![H256::from_slice(active_hotkey.as_ref())], + ), + ), + ) + .with_static_call(true) + .expect_cost(stake_info_cost(1)) + .execute_returns_raw(encode_return_value(vec![( + H256::from_slice(active_hotkey.as_ref()), + U256::from(active_stake), + )])); + }); + } + #[test] fn staking_precompile_v1_add_stake_and_reads_match_runtime_state() { new_test_ext().execute_with(|| { @@ -2999,6 +2998,7 @@ mod tests { } // cargo test --package subtensor-precompiles --lib -- staking::tests::staking_precompile_v2_burn_alpha_caps_to_available_stake --exact --nocapture + #[test] fn staking_precompile_v2_burn_alpha_caps_to_available_stake() { new_test_ext().execute_with(|| { diff --git a/precompiles/src/storage_query.rs b/precompiles/src/storage_query.rs index 455a9e81c1..9fa75b7014 100644 --- a/precompiles/src/storage_query.rs +++ b/precompiles/src/storage_query.rs @@ -1,3 +1,10 @@ +//! Raw Substrate storage read precompile with an allow-listed pallet-prefix set. +//! +//! Calldata must begin with one of the Twox128 pallet prefixes in +//! [`AUTHORIZED_STORAGE_PREFIXES`]; other keys revert with `Invalid key`. +//! Returns the raw SCALE value bytes, or empty output when the key is absent. +//! INDEX is frozen. + use core::marker::PhantomData; use fp_evm::{ExitError, PrecompileFailure}; @@ -62,6 +69,7 @@ const AUTHORIZED_PREFIXES: [[u8; 16]; 10] = [ use crate::PrecompileExt; +/// EVM precompile that reads allow-listed FRAME storage keys by raw key bytes (INDEX 2055). pub struct StorageQueryPrecompile(PhantomData); impl PrecompileExt for StorageQueryPrecompile diff --git a/precompiles/src/subnet.rs b/precompiles/src/subnet.rs index a591a7f0d8..8a58d8d3cb 100644 --- a/precompiles/src/subnet.rs +++ b/precompiles/src/subnet.rs @@ -1,3 +1,9 @@ +//! Subnet precompile: register networks and get/set owner hyperparameters from EVM. +//! +//! Dispatchable methods run as the EVM caller's mapped Substrate account (subnet +//! owner for setters). View methods read `pallet_subtensor` storage directly. +//! INDEX and Solidity selectors are frozen — do not renumber or rename them. + use core::marker::PhantomData; use frame_support::dispatch::{DispatchInfo, GetDispatchInfo, PostDispatchInfo}; @@ -13,6 +19,7 @@ use subtensor_runtime_common::{NetUid, Token}; use crate::{PrecompileExt, PrecompileHandleExt}; +/// EVM surface for subnet registration and owner hyperparameter get/set (INDEX 2051). pub struct SubnetPrecompile(PhantomData); impl PrecompileExt for SubnetPrecompile @@ -67,6 +74,7 @@ where + IsSubType>, ::AddressMapping: AddressMapping, { + /// Register a new subnet with `hotkey` as owner hotkey and no identity metadata. #[precompile::public("registerNetwork(bytes32)")] #[precompile::payable] fn register_network(handle: &mut impl PrecompileHandle, hotkey: H256) -> EvmResult<()> { @@ -161,6 +169,7 @@ where ) } + /// Block number when `netuid` was registered (`NetworkRegisteredAt`). #[precompile::public("getNetworkRegistrationBlock(uint16)")] #[precompile::view] fn get_network_registration_block( @@ -867,6 +876,7 @@ where ) } + /// Owner toggle for alpha/stake transfers on the subnet. #[precompile::public("toggleTransfers(uint16,bool)")] #[precompile::payable] fn toggle_transfers( @@ -885,6 +895,7 @@ where ) } + /// Whether `netuid` is currently in the dissolve cleanup queue. #[precompile::public("isSubnetDissolving(uint16)")] #[precompile::view] fn is_subnet_dissolving(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { @@ -977,10 +988,7 @@ mod tests { let precompiles = precompiles::>(); let precompile_addr = addr_from_index(SubnetPrecompile::::INDEX); - add_balance_to_coldkey_account( - &caller_account, - 1_000_000_000_000_u64.into(), - ); + add_balance_to_coldkey_account(&caller_account, 1_000_000_000_000_u64.into()); let total_before = pallet_subtensor::TotalNetworks::::get(); let netuid = pallet_subtensor::Pallet::::get_next_netuid(); @@ -1008,7 +1016,10 @@ mod tests { let total_after = pallet_subtensor::TotalNetworks::::get(); assert_eq!(total_after, total_before + 1); - assert_eq!(pallet_subtensor::SubnetOwner::::get(netuid), caller_account); + assert_eq!( + pallet_subtensor::SubnetOwner::::get(netuid), + caller_account + ); assert!(pallet_subtensor::SubnetIdentitiesV3::::contains_key(netuid)); }); } diff --git a/precompiles/src/uid_lookup.rs b/precompiles/src/uid_lookup.rs index 9846eb0463..800a9f69d2 100644 --- a/precompiles/src/uid_lookup.rs +++ b/precompiles/src/uid_lookup.rs @@ -1,3 +1,8 @@ +//! UID lookup precompile: map an associated EVM address to subnet UIDs. +//! +//! Wraps [`pallet_subtensor::Pallet::uid_lookup`]. INDEX and the Solidity +//! selector are frozen. + use core::marker::PhantomData; use frame_support::dispatch::{GetDispatchInfo, PostDispatchInfo}; @@ -8,6 +13,7 @@ use sp_std::vec::Vec; use crate::{PrecompileExt, PrecompileHandleExt}; +/// View precompile returning `(uid, block)` pairs for an EVM address on a subnet (INDEX 2054). pub struct UidLookupPrecompile(PhantomData); impl PrecompileExt for UidLookupPrecompile @@ -36,6 +42,7 @@ where + Dispatchable, <::Lookup as StaticLookup>::Source: From, { + /// Return up to `limit` `(uid, associated_block)` pairs for `evm_address` on `netuid`. #[precompile::public("uidLookup(uint16,address,uint16)")] #[precompile::view] fn uid_lookup( diff --git a/precompiles/src/voting_power.rs b/precompiles/src/voting_power.rs index 4cad7fcb89..5aab23a5ea 100644 --- a/precompiles/src/voting_power.rs +++ b/precompiles/src/voting_power.rs @@ -1,3 +1,8 @@ +//! Voting-power precompile: EMA stake used for on-chain governance queries. +//! +//! Reads `VotingPower*` storage items from `pallet_subtensor`. INDEX and +//! Solidity selectors are frozen. + use core::marker::PhantomData; use fp_evm::PrecompileHandle; diff --git a/primitives/safe-math/src/lib.rs b/primitives/safe-math/src/lib.rs index 966dbb4de4..ff77773bab 100644 --- a/primitives/safe-math/src/lib.rs +++ b/primitives/safe-math/src/lib.rs @@ -1,3 +1,8 @@ +//! Checked and fall-back arithmetic helpers for primitive integers and `substrate_fixed` types. +//! +//! Prefer these over panicking `/` or silent wrapping when implementing emission, pricing, +//! and stake math that must tolerate edge cases (zero divisors, negative roots, etc.). + #![cfg_attr(not(feature = "std"), no_std)] #![allow(clippy::result_unit_err)] #![cfg_attr(test, allow(clippy::arithmetic_side_effects))] @@ -8,15 +13,15 @@ use core::f64::consts::LN_2; use sp_arithmetic::traits::UniqueSaturatedInto; use substrate_fixed::traits::Fixed; -/// Safe division trait +/// Division that never panics: zero divisor yields a caller-supplied or `Default` value. pub trait SafeDiv { - /// Safe division that returns supplied default value for division by zero + /// `self / rhs`, or `def` when `rhs` is zero. fn safe_div_or(self, rhs: Self, def: Self) -> Self; - /// Safe division that returns default value for division by zero + /// `self / rhs`, or `Default::default()` when `rhs` is zero. fn safe_div(self, rhs: Self) -> Self; } -/// Implementation of safe division trait for primitive types +/// Implement [`SafeDiv`] for the listed primitive integer types via `checked_div`. macro_rules! impl_safe_div_for_primitive { ($($t:ty),*) => { $( @@ -34,7 +39,11 @@ macro_rules! impl_safe_div_for_primitive { } impl_safe_div_for_primitive!(u8, u16, u32, u64, u128, i8, i16, i32, i64, usize); +/// Extra checked ops on [`Fixed`] values used by subnet pricing and emission math. pub trait FixedExt: Fixed { + /// Integer power via binary exponentiation; `None` for `0^negative`. + /// + /// Negative exponents compute `1 / base^|exp|` with saturating intermediate muls. fn checked_pow(&self, exponent: E) -> Option where E: UniqueSaturatedInto, @@ -73,7 +82,9 @@ pub trait FixedExt: Fixed { Some(result) } - /// Safe sqrt with good precision + /// Non-negative square root by bisection until `|x/mid - mid| <= epsilon`, or 128 iterations. + /// + /// Returns `None` for negative `self`. fn checked_sqrt(&self, epsilon: Self) -> Option { let zero = Self::saturating_from_num(0); let one = Self::saturating_from_num(1); @@ -119,7 +130,9 @@ pub trait FixedExt: Fixed { Some(middle) } - /// Natural logarithm (base e) + /// Natural logarithm (`ln`); `None` for non-positive inputs. + /// + /// Scales into `[1, 2)` then applies an 8-term Taylor series for `ln(1+z)`. fn checked_ln(&self) -> Option { if *self <= Self::from_num(0) { return None; @@ -172,7 +185,9 @@ pub trait FixedExt: Fixed { ln_y.checked_add(exp_ln2) } - /// Logarithm with arbitrary base + /// Logarithm at arbitrary `base` via change-of-base (`ln(x) / ln(base)`). + /// + /// Returns `None` for non-positive `self`, or base `<= 0` or `== 1`. fn checked_log(&self, base: Self) -> Option { // Check for invalid base if base <= Self::from_num(0) || base == Self::from_num(1) { @@ -186,7 +201,7 @@ pub trait FixedExt: Fixed { ln_x.checked_div(ln_base) } - /// Returns the largest integer less than or equal to the fixed-point number. + /// Largest integer `<= self` (floor toward −∞ for negatives with a fractional part). fn checked_floor(&self) -> Option { // Approach using the integer and fractional parts if *self >= Self::from_num(0) { @@ -207,6 +222,7 @@ pub trait FixedExt: Fixed { int_part.checked_sub(Self::from_num(1)) } + /// Absolute difference `|self - b|` using saturating subtraction. fn abs_diff(&self, b: Self) -> Self { if *self < b { b.saturating_sub(*self) @@ -215,10 +231,12 @@ pub trait FixedExt: Fixed { } } + /// `self / rhs`, or `def` when division fails (e.g. zero divisor). fn safe_div_or(&self, rhs: Self, def: Self) -> Self { self.checked_div(rhs).unwrap_or(def) } + /// `self / rhs`, or the fixed-point default when division fails. fn safe_div(&self, rhs: Self) -> Self { self.checked_div(rhs).unwrap_or_default() } diff --git a/primitives/share-pool/src/lib.rs b/primitives/share-pool/src/lib.rs index 848ee64448..9b3727544e 100644 --- a/primitives/share-pool/src/lib.rs +++ b/primitives/share-pool/src/lib.rs @@ -1,1646 +1,17 @@ +//! Proportional stake share pool with controlled-precision [`SafeFloat`] arithmetic. +//! +//! Used by subtensor staking to track each coldkey's claim on a hotkey's alpha +//! (rao) without losing 1-rao precision across large emissions and unstakes. + #![cfg_attr(not(feature = "std"), no_std)] #![allow(clippy::result_unit_err, clippy::indexing_slicing)] -use codec::{Decode, Encode}; -#[cfg(not(feature = "std"))] -use num_traits::float::FloatCore as _; -use scale_info::TypeInfo; -use sp_core::U256; -use sp_std::marker; -use sp_std::ops::Neg; -use substrate_fixed::types::U64F64; -use subtensor_macros::freeze_struct; - -// Maximum mantissa that can be used with SafeFloat -pub const SAFE_FLOAT_MAX: u128 = 1_000_000_000_000_000_000_000_u128; -pub const SAFE_FLOAT_MAX_EXP: i64 = 21_i64; - -/// Controlled precision floating point number with efficient storage -/// -/// Precision is controlled in a way that keeps enough mantissa digits so -/// that updating hotkey stake by 1 rao makes difference in the resulting shared -/// pool variables (both coldkey share and share pool denominator), but also -/// precision should be limited so that updating by 0.1 rao does not make the -/// difference (because there's no such thing as 0.1 rao, rao is integer). -#[freeze_struct("9a55fbe2d60efb41")] -#[derive(Encode, Decode, Default, TypeInfo, Clone, PartialEq, Eq, Debug)] -pub struct SafeFloat { - mantissa: u128, - exponent: i64, -} - -/// Capped power of 10 in U256 -/// Cap at 10^SAFE_FLOAT_MAX_EXP+1, we don't need greater powers here -fn cappow10(e: u64) -> U256 { - if e > (SAFE_FLOAT_MAX_EXP as u64).saturating_add(1) { - return U256::from(SAFE_FLOAT_MAX.saturating_mul(10)); - } - if e == 0 { - return U256::from(1); - } - U256::from(10) - .checked_pow(U256::from(e)) - .unwrap_or_default() -} - -impl SafeFloat { - pub fn zero() -> Self { - SafeFloat { - mantissa: 0_u128, - exponent: 0_i64, - } - } - - pub fn new(mantissa: u128, exponent: i64) -> Option { - // Cap mantissa at SAFE_FLOAT_MAX - if mantissa > SAFE_FLOAT_MAX { - return None; - } - - let mut safe_float = SafeFloat::zero(); - - if safe_float.normalize(&U256::from(mantissa), exponent) { - Some(safe_float) - } else { - None - } - } - - /// Sets the new mantissa and exponent adjustsing mantissa and exponent so that - /// SAFE_FLOAT_MAX / 10 < mantissa <= SAFE_FLOAT_MAX - /// - /// Returns true in case of success or false if exponent over- or underflows - pub(crate) fn normalize(&mut self, new_mantissa: &U256, new_exponent: i64) -> bool { - if new_mantissa.is_zero() { - self.mantissa = 0; - self.exponent = 0; - return true; - } - - let ten = U256::from(10); - let max_mantissa = U256::from(SAFE_FLOAT_MAX); - let min_mantissa = U256::from(SAFE_FLOAT_MAX) - .checked_div(ten) - .unwrap_or_default(); - - // Loops are safe because they are bounded by U256 size and result - // in no more than 78 iterations together - let mut normalized_mantissa = *new_mantissa; - let mut normalized_exponent = new_exponent; - - while normalized_mantissa > max_mantissa { - let Some(next_mantissa) = normalized_mantissa.checked_div(ten) else { - return false; - }; - let Some(next_exponent) = normalized_exponent.checked_add(1) else { - return false; - }; - - normalized_mantissa = next_mantissa; - normalized_exponent = next_exponent; - } - - while normalized_mantissa <= min_mantissa { - let Some(next_mantissa) = normalized_mantissa.checked_mul(ten) else { - return false; - }; - let Some(next_exponent) = normalized_exponent.checked_sub(1) else { - return false; - }; - - normalized_mantissa = next_mantissa; - normalized_exponent = next_exponent; - } - - self.mantissa = normalized_mantissa.low_u128(); - self.exponent = normalized_exponent; - - true - } - - /// Divide current value by a preserving precision (SAFE_FLOAT_MAX digits in mantissa) - /// result = m1 * 10^e1 / m2 * 10^e2 - pub fn div(&self, a: &SafeFloat) -> Option { - // - In m1 / m2 division we need enough digits for a u128. - // This can be calculated in a lossless way in U256 as m1 * MAX_MANTISSA / m2 - // - The new exponent is e1 - e2 - SAFE_FLOAT_MAX_EXP - let maybe_m1_scaled_u256 = - U256::from(self.mantissa).checked_mul(U256::from(SAFE_FLOAT_MAX)); - let m2_u256 = U256::from(a.mantissa); - - // Calculate new exponent - let new_exponent_i128 = (self.exponent as i128) - .saturating_sub(a.exponent as i128) - .saturating_sub(SAFE_FLOAT_MAX_EXP as i128); - if (new_exponent_i128 > i64::MAX as i128) || (new_exponent_i128 < i64::MIN as i128) { - return None; - } - let new_exponent = new_exponent_i128 as i64; - - // Calcuate new mantissa, normalize, and return result - if let Some(m1_scaled_u256) = maybe_m1_scaled_u256 { - let maybe_new_mantissa_u256 = m1_scaled_u256.checked_div(m2_u256); - if let Some(new_mantissa_u256) = maybe_new_mantissa_u256 { - let mut safe_float = SafeFloat::zero(); - if safe_float.normalize(&new_mantissa_u256, new_exponent) { - Some(safe_float) - } else { - None - } - } else { - None - } - } else { - None - } - } - - pub fn add(&self, a: &SafeFloat) -> Option { - if self.is_zero() { - return Some(a.clone()); - } - if a.is_zero() { - return Some(self.clone()); - } - - let (new_mantissa, new_exponent) = if self.exponent >= a.exponent { - let exp_diff = self.exponent.saturating_sub(a.exponent); - let m1 = U256::from(self.mantissa); - let m2 = U256::from(a.mantissa) - .checked_div(cappow10(exp_diff as u64)) - .unwrap_or_default(); - (m1.saturating_add(m2), self.exponent) - } else { - let exp_diff = a.exponent.saturating_sub(self.exponent); - let m1 = U256::from(self.mantissa) - .checked_div(cappow10(exp_diff as u64)) - .unwrap_or_default(); - let m2 = U256::from(a.mantissa); - (m1.saturating_add(m2), a.exponent) - }; - - let mut safe_float = SafeFloat::zero(); - if safe_float.normalize(&new_mantissa, new_exponent) { - Some(safe_float) - } else { - None - } - } - - pub fn sub(&self, a: &SafeFloat) -> Option { - if self.is_zero() && a.is_zero() { - return Some(Self::zero()); - } else if self.is_zero() { - return None; - } - if a.is_zero() { - return Some(self.clone()); - } - - let (new_mantissa, new_exponent) = if self.exponent >= a.exponent { - let exp_diff = self.exponent.saturating_sub(a.exponent); - let m1 = U256::from(self.mantissa); - let m2 = U256::from(a.mantissa) - .checked_div(cappow10(exp_diff as u64)) - .unwrap_or_default(); - (m1.saturating_sub(m2), self.exponent) - } else { - let exp_diff = a.exponent.saturating_sub(self.exponent); - let m1 = U256::from(self.mantissa) - .checked_div(cappow10(exp_diff as u64)) - .unwrap_or_default(); - let m2 = U256::from(a.mantissa); - (m1.saturating_sub(m2), a.exponent) - }; - - let mut safe_float = SafeFloat::zero(); - if safe_float.normalize(&new_mantissa, new_exponent) { - Some(safe_float) - } else { - None - } - } - - /// Calculate self * a / b without loss of precision - pub fn mul_div(&self, a: &SafeFloat, b: &SafeFloat) -> Option { - if b.mantissa == 0_u128 { - return None; - } - - // No overflows here, just unwrap or default - let self_a_mantissa_u256 = U256::from(self.mantissa) - .checked_mul(U256::from(a.mantissa)) - .unwrap_or_default(); - let maybe_self_a_exponent = self.exponent.checked_add(a.exponent); - - if let Some(self_a_exponent) = maybe_self_a_exponent { - // Divide by b in U256 - let maybe_new_exponent = self_a_exponent.checked_sub(b.exponent); - if let Some(new_exponent) = maybe_new_exponent { - let new_mantissa = self_a_mantissa_u256 - .checked_div(U256::from(b.mantissa)) - .unwrap_or_default(); - let mut result = SafeFloat::zero(); - if result.normalize(&new_mantissa, new_exponent) { - Some(result) - } else { - None - } - } else { - None - } - } else { - None - } - } - - pub fn is_zero(&self) -> bool { - self.mantissa == 0u128 - } - - /// Returns true if self > a - /// Both values should be normalized - pub fn gt(&self, a: &SafeFloat) -> bool { - let ten = U256::from(10); - - if self.exponent == a.exponent { - self.mantissa > a.mantissa - } else if self.exponent > a.exponent { - let exp_diff = self.exponent.saturating_sub(a.exponent); - if exp_diff > 1_i64 { - true - } else { - ten.saturating_mul(U256::from(self.mantissa)) > U256::from(a.mantissa) - } - } else { - let exp_diff = a.exponent.saturating_sub(self.exponent); - if exp_diff > 1_i64 { - false - } else { - U256::from(self.mantissa) > ten.saturating_mul(U256::from(a.mantissa)) - } - } - } -} - -// Saturating conversion: negatives -> 0, overflow -> u64::MAX -impl From<&SafeFloat> for u64 { - fn from(value: &SafeFloat) -> Self { - // If exponent is zero, it's just an integer mantissa - if value.exponent == 0 { - return u64::try_from(value.mantissa).unwrap_or(u64::MAX); - } - - // scale = 10^exponent - let scale = cappow10(value.exponent.unsigned_abs()); - - // mantissa * 10^exponent - let q: U256 = if value.exponent > 0 { - U256::from(value.mantissa).saturating_mul(scale) - } else { - U256::from(value.mantissa) - .checked_div(scale) - .unwrap_or_default() - }; - - // Convert quotient to u64, saturating on overflow - if q.is_zero() { - 0 - } else { - q.try_into().unwrap_or(u64::MAX) - } - } -} - -// Convenience impl for owning values -impl From for u64 { - fn from(value: SafeFloat) -> Self { - u64::from(&value) - } -} - -impl From for SafeFloat { - fn from(value: u64) -> Self { - SafeFloat::new(value as u128, 0).unwrap_or_default() - } -} - -impl From for SafeFloat { - fn from(value: U64F64) -> Self { - let bits = value.to_bits(); - // High 64 bits = integer part - let int = (bits >> 64) as u64; - // Low 64 bits = fractional part - let frac = (bits & 0xFFFF_FFFF_FFFF_FFFF) as u64; - - // If strictly zero, shortcut - if bits == 0 { - return SafeFloat::zero(); - } - - // SafeFloat for integer part: int * 10^0 - let safe_int = SafeFloat::new(int as u128, 0).unwrap_or_default(); - - // Numerator of fractional part: frac * 10^0 - let safe_frac_num = SafeFloat::new(frac as u128, 0).unwrap_or_default(); - - // Denominator = 2^64 as an integer SafeFloat: (2^64) * 10^0 - let two64: u128 = 1u128 << 64; - let safe_two64 = SafeFloat::new(two64, 0).unwrap_or_default(); - - // frac_part = frac / 2^64 - let safe_frac = safe_frac_num.div(&safe_two64).unwrap_or_default(); - - // int + frac/2^64, with all mantissa/exponent normalization - safe_int.add(&safe_frac).unwrap_or_default() - } -} - -impl From<&SafeFloat> for f64 { - #[allow( - clippy::arithmetic_side_effects, - reason = "This code is only used in tests" - )] - fn from(value: &SafeFloat) -> Self { - let mant = value.mantissa as f64; - - // powi takes i32, so clamp i64 exponent into i32 range (test-only). - let e = value.exponent.clamp(i32::MIN as i64, i32::MAX as i64) as i32; +mod safe_float; +mod share_pool; - mant * 10_f64.powi(e) - } -} +pub use safe_float::{SAFE_FLOAT_MAX, SAFE_FLOAT_MAX_EXP, SafeFloat}; +pub use share_pool::{SharePool, SharePoolDataOperations}; -impl From for f64 { - fn from(value: SafeFloat) -> Self { - f64::from(&value) - } -} - -pub trait SharePoolDataOperations { - /// Gets shared value (always "the real thing" measured in rao, not fractional) - fn get_shared_value(&self) -> u64; - /// Gets single share for a given key - fn get_share(&self, key: &Key) -> SafeFloat; - // Tries to get a single share for a given key, as a result. - fn try_get_share(&self, key: &Key) -> Result; - /// Gets share pool denominator - fn get_denominator(&self) -> SafeFloat; - /// Updates shared value by provided signed value - fn set_shared_value(&mut self, value: u64); - /// Update single share for a given key by provided signed value - fn set_share(&mut self, key: &Key, share: SafeFloat); - /// Update share pool denominator by provided signed value - fn set_denominator(&mut self, update: SafeFloat); -} - -/// SharePool struct that depends on the Key type and uses the SharePoolDataOperations -#[derive(Debug)] -pub struct SharePool -where - K: Eq, - Ops: SharePoolDataOperations, -{ - state_ops: Ops, - phantom_key: marker::PhantomData, -} - -impl SharePool -where - K: Eq, - Ops: SharePoolDataOperations, -{ - pub fn new(ops: Ops) -> Self { - SharePool { - state_ops: ops, - phantom_key: marker::PhantomData, - } - } - - pub fn get_value(&self, key: &K) -> u64 { - let shared_value: SafeFloat = - SafeFloat::new(self.state_ops.get_shared_value() as u128, 0).unwrap_or_default(); - let current_share: SafeFloat = self.state_ops.get_share(key); - let denominator: SafeFloat = self.state_ops.get_denominator(); - shared_value - .mul_div(¤t_share, &denominator) - .unwrap_or_default() - .into() - } - - pub fn get_value_from_shares(&self, current_share: SafeFloat) -> u64 { - let shared_value: SafeFloat = - SafeFloat::new(self.state_ops.get_shared_value() as u128, 0).unwrap_or_default(); - let denominator: SafeFloat = self.state_ops.get_denominator(); - shared_value - .mul_div(¤t_share, &denominator) - .unwrap_or_default() - .into() - } - - pub fn try_get_value(&self, key: &K) -> Result { - match self.state_ops.try_get_share(key) { - Ok(_) => Ok(self.get_value(key)), - Err(i) => Err(i), - } - } - - /// Update the total shared value. - /// Every key's associated value effectively updates with this operation - pub fn update_value_for_all(&mut self, update: i64) { - let shared_value: u64 = self.state_ops.get_shared_value(); - self.state_ops.set_shared_value(if update >= 0 { - shared_value.saturating_add(update as u64) - } else { - shared_value.saturating_sub(update.neg() as u64) - }); - } - - pub fn sim_update_value_for_one(&mut self, update: i64) -> bool { - let shared_value: u64 = self.state_ops.get_shared_value(); - let denominator: SafeFloat = self.state_ops.get_denominator(); - - // Then, update this key's share - if denominator.mantissa == 0 { - true - } else { - // There are already keys in the pool, set or update this key - let shares_per_update = self.get_shares_per_update(update, shared_value, &denominator); - - !shares_per_update.is_zero() - } - } - - fn get_shares_per_update( - &self, - update: i64, - shared_value: u64, - denominator: &SafeFloat, - ) -> SafeFloat { - let shared_value: SafeFloat = SafeFloat::new(shared_value as u128, 0).unwrap_or_default(); - let update_sf: SafeFloat = - SafeFloat::new(update.unsigned_abs() as u128, 0).unwrap_or_default(); - update_sf - .mul_div(denominator, &shared_value) - .unwrap_or_default() - } - - /// Update the value associated with an item identified by the Key - /// Returns actual update - /// - pub fn update_value_for_one(&mut self, key: &K, update: i64) { - let shared_value: u64 = self.state_ops.get_shared_value(); - let current_share: SafeFloat = self.state_ops.get_share(key); - let denominator: SafeFloat = self.state_ops.get_denominator(); - - // Then, update this key's share - if denominator.is_zero() { - // Initialize the pool. The first key gets all. - let update_float: SafeFloat = - SafeFloat::new(update.unsigned_abs() as u128, 0).unwrap_or_default(); - self.state_ops.set_denominator(update_float.clone()); - self.state_ops.set_share(key, update_float); - } else { - let new_denominator; - let new_current_share; - - let shares_per_update: SafeFloat = - self.get_shares_per_update(update, shared_value, &denominator); - - // Handle SafeFloat overflows quietly here because this overflow of i64 exponent - // is extremely hypothetical and should never happen in practice. - if update > 0 { - new_denominator = match denominator.add(&shares_per_update) { - Some(new_denominator) => new_denominator, - None => { - log::error!( - "SafeFloat::add overflow when adding {:?} to {:?}; keeping old denominator", - shares_per_update, - denominator, - ); - // Return the value as it was before the failed addition - denominator - } - }; - - new_current_share = match current_share.add(&shares_per_update) { - Some(new_current_share) => new_current_share, - None => { - log::error!( - "SafeFloat::add overflow when adding {:?} to {:?}; keeping old current_share", - shares_per_update, - current_share, - ); - // Return the value as it was before the failed addition - current_share - } - }; - } else { - new_denominator = match denominator.sub(&shares_per_update) { - Some(new_denominator) => new_denominator, - None => { - log::error!( - "SafeFloat::add overflow when adding {:?} to {:?}; keeping old denominator", - shares_per_update, - denominator, - ); - // Return the value as it was before the failed addition - denominator - } - }; - - new_current_share = match current_share.sub(&shares_per_update) { - Some(new_current_share) => new_current_share, - None => { - log::error!( - "SafeFloat::add overflow when adding {:?} to {:?}; keeping old current_share", - shares_per_update, - current_share, - ); - // Return the value as it was before the failed addition - current_share - } - }; - } - - self.state_ops.set_denominator(new_denominator); - self.state_ops.set_share(key, new_current_share); - } - - // Update shared value - self.update_value_for_all(update); - } -} - -// cargo test --package share-pool --lib -- tests --nocapture #[cfg(test)] #[allow(clippy::unwrap_used)] -mod tests { - use super::*; - use approx::assert_abs_diff_eq; - use std::collections::BTreeMap; - use substrate_fixed::types::U64F64; - - struct MockSharePoolDataOperations { - shared_value: u64, - share: BTreeMap, - denominator: SafeFloat, - } - - impl MockSharePoolDataOperations { - fn new() -> Self { - MockSharePoolDataOperations { - shared_value: 0u64, - share: BTreeMap::new(), - denominator: SafeFloat::zero(), - } - } - } - - impl SharePoolDataOperations for MockSharePoolDataOperations { - fn get_shared_value(&self) -> u64 { - self.shared_value - } - - fn get_share(&self, key: &u16) -> SafeFloat { - self.share.get(key).cloned().unwrap_or_else(SafeFloat::zero) - } - - fn try_get_share(&self, key: &u16) -> Result { - match self.share.get(key).cloned() { - Some(value) => Ok(value), - None => Err(()), - } - } - - fn get_denominator(&self) -> SafeFloat { - self.denominator.clone() - } - - fn set_shared_value(&mut self, value: u64) { - self.shared_value = value; - } - - fn set_share(&mut self, key: &u16, share: SafeFloat) { - self.share.insert(*key, share); - } - - fn set_denominator(&mut self, update: SafeFloat) { - self.denominator = update; - } - } - - #[test] - fn test_get_value() { - let mut mock_ops = MockSharePoolDataOperations::new(); - mock_ops.set_denominator(10u64.into()); - mock_ops.set_share(&1_u16, 3u64.into()); - mock_ops.set_share(&2_u16, 7u64.into()); - mock_ops.set_shared_value(100u64.into()); - let share_pool = SharePool::new(mock_ops); - let result1 = share_pool.get_value(&1); - let result2 = share_pool.get_value(&2); - assert_eq!(result1, 30); - assert_eq!(result2, 70); - } - - #[test] - fn test_division_by_zero() { - let mut mock_ops = MockSharePoolDataOperations::new(); - mock_ops.set_denominator(SafeFloat::zero()); // Zero denominator - let pool = SharePool::::new(mock_ops); - - let value = pool.get_value(&1); - assert_eq!(value, 0, "Value should be 0 when denominator is zero"); - } - - #[test] - fn test_max_shared_value() { - let mut mock_ops = MockSharePoolDataOperations::new(); - mock_ops.set_shared_value(u64::MAX.into()); - mock_ops.set_share(&1, 3u64.into()); // Use a neutral value for share - mock_ops.set_share(&2, 7u64.into()); // Use a neutral value for share - mock_ops.set_denominator(10u64.into()); // Neutral value to see max effect - let pool = SharePool::::new(mock_ops); - - let max_value = pool.get_value(&1) + pool.get_value(&2); - assert!(u64::MAX - max_value <= 5, "Max value should map to u64 MAX"); - } - - #[test] - fn test_max_share_value() { - let mut mock_ops = MockSharePoolDataOperations::new(); - mock_ops.set_shared_value(1_000_000_000u64); // Use a neutral value for shared value - mock_ops.set_share(&1, (u64::MAX / 2).into()); - mock_ops.set_share(&2, (u64::MAX / 2).into()); - mock_ops.set_denominator((u64::MAX).into()); - let pool = SharePool::::new(mock_ops); - - let value1 = pool.get_value(&1) as i128; - let value2 = pool.get_value(&2) as i128; - - assert_abs_diff_eq!(value1 as f64, 500_000_000_f64, epsilon = 1.); - assert!((value2 - 500_000_000).abs() <= 1); - } - - #[test] - fn test_denom_precision() { - let mock_ops = MockSharePoolDataOperations::new(); - let mut pool = SharePool::::new(mock_ops); - - pool.update_value_for_one(&1, 1000); - - let value_tmp = pool.get_value(&1) as i128; - assert_eq!(value_tmp, 1000); - - pool.update_value_for_one(&1, -990); - pool.update_value_for_one(&2, 1000); - pool.update_value_for_one(&2, -990); - - let value1 = pool.get_value(&1) as i128; - let value2 = pool.get_value(&2) as i128; - - assert_eq!(value1, 10); - assert_eq!(value2, 10); - } - - // cargo test --package share-pool --lib -- tests::test_denom_high_precision --exact --show-output - #[test] - fn test_denom_high_precision() { - let mock_ops = MockSharePoolDataOperations::new(); - let mut pool = SharePool::::new(mock_ops); - - // 50%/50% stakes consisting of 1 rao each - pool.update_value_for_one(&1, 1); - pool.update_value_for_one(&2, 1); - - // Huge emission resulting in 1M Alpha - // Both stakers should have 500k Alpha each - pool.update_value_for_all(999_999_999_999_998); - - // Everyone unstakes almost everything, leaving 10 rao in the stake - pool.update_value_for_one(&1, -499_999_999_999_990); - pool.update_value_for_one(&2, -499_999_999_999_990); - - // Huge emission resulting in 1M Alpha - // Both stakers should have 500k Alpha each - pool.update_value_for_all(999_999_999_999_980); - - // Stakers add 1k Alpha each - pool.update_value_for_one(&1, 1_000_000_000_000); - pool.update_value_for_one(&2, 1_000_000_000_000); - - let value1 = pool.get_value(&1) as f64; - let value2 = pool.get_value(&2) as f64; - assert_abs_diff_eq!(value1, 501_000_000_000_000_f64, epsilon = 1.); - assert_abs_diff_eq!(value2, 501_000_000_000_000_f64, epsilon = 1.); - } - - // cargo test --package share-pool --lib -- tests::test_denom_high_precision_many_small_unstakes --exact --show-output - #[test] - fn test_denom_high_precision_many_small_unstakes() { - let mock_ops = MockSharePoolDataOperations::new(); - let mut pool = SharePool::::new(mock_ops); - - // 50%/50% stakes consisting of 1 rao each - pool.update_value_for_one(&1, 1); - pool.update_value_for_one(&2, 1); - - // Huge emission resulting in 1M Alpha - // Both stakers should have 500k Alpha + 1 rao each - pool.update_value_for_all(1_000_000_000_000_000); - - // Run X number of small unstake transactions - let tx_count = 1000; - let unstake_amount = -500_000_000; - for _ in 0..tx_count { - pool.update_value_for_one(&1, unstake_amount); - pool.update_value_for_one(&2, unstake_amount); - } - - // Emit 1M - each gets 500k Alpha - pool.update_value_for_all(1_000_000_000_000_000); - - // Each adds 1k Alpha - pool.update_value_for_one(&1, 1_000_000_000_000); - pool.update_value_for_one(&2, 1_000_000_000_000); - - // Result, each should get - // (500k+1) + tx_count * unstake_amount + 500k + 1k - let value1 = pool.get_value(&1) as i128; - let value2 = pool.get_value(&2) as i128; - let expected = 1_001_000_000_000_000 + tx_count * unstake_amount; - - assert_abs_diff_eq!(value1 as f64, expected as f64, epsilon = 1.); - assert_abs_diff_eq!(value2 as f64, expected as f64, epsilon = 1.); - } - - #[test] - fn test_update_value_for_one() { - let mock_ops = MockSharePoolDataOperations::new(); - let mut pool = SharePool::::new(mock_ops); - - pool.update_value_for_one(&1, 1000); - - let value = pool.get_value(&1) as i128; - assert_eq!(value, 1000); - } - - #[test] - fn test_update_value_for_all() { - let mock_ops = MockSharePoolDataOperations::new(); - let mut pool = SharePool::::new(mock_ops); - - pool.update_value_for_all(1000); - assert_eq!( - pool.state_ops.shared_value, - U64F64::saturating_from_num(1000) - ); - } - - // cargo test --package share-pool --lib -- tests::test_get_shares_per_update --exact --show-output - #[test] - fn test_get_shares_per_update() { - // Test case (update, shared_value, denominator_mantissa, denominator_exponent) - [ - (1_i64, 1_u64, 1_u64, 0_i64), - (1, 1_000_000_000_000_000_000, 1, 0), - (1, 21_000_000_000_000_000, 1, 5), - (1, 21_000_000_000_000_000, 1, -1_000_000), - (1, 21_000_000_000_000_000, 1, -1_000_000_000), - (1, 21_000_000_000_000_000, 1, -1_000_000_001), - (1_000, 21_000_000_000_000_000, 1, 5), - (21_000_000_000_000_000, 21_000_000_000_000_000, 1, 5), - (21_000_000_000_000_000, 21_000_000_000_000_000, 1, -5), - (21_000_000_000_000_000, 21_000_000_000_000_000, 1, -100), - (21_000_000_000_000_000, 21_000_000_000_000_000, 1, 100), - (210_000_000_000_000_000, 21_000_000_000_000_000, 1, 5), - (1_000, 1_000, 21_000_000_000_000_000, 0), - (1_000, 1_000, 21_000_000_000_000_000, -1), - ] - .into_iter() - .for_each( - |(update, shared_value, denominator_mantissa, denominator_exponent)| { - let mock_ops = MockSharePoolDataOperations::new(); - let pool = SharePool::::new(mock_ops); - - let denominator_float = - SafeFloat::new(denominator_mantissa as u128, denominator_exponent) - .unwrap_or_default(); - let denominator_f64: f64 = denominator_float.clone().into(); - let spu: f64 = pool - .get_shares_per_update(update, shared_value, &denominator_float) - .into(); - let expected = update as f64 * denominator_f64 / shared_value as f64; - let precision = 1000.; - assert_abs_diff_eq!(expected, spu, epsilon = expected / precision); - }, - ); - } - - #[test] - fn test_safefloat_normalize() { - // Test case: mantissa, exponent, expected mantissa, expected exponent - [ - (1_u128, 0, 1_000_000_000_000_000_000_000_u128, -21_i64), - (0, 0, 0, 0), - (10_u128, 0, 1_000_000_000_000_000_000_000_u128, -20), - (1_000_u128, 0, 1_000_000_000_000_000_000_000_u128, -18), - ( - 100_000_000_000_000_000_000_u128, - 0, - 1_000_000_000_000_000_000_000_u128, - -1, - ), - (SAFE_FLOAT_MAX, 0, SAFE_FLOAT_MAX, 0), - ] - .into_iter() - .for_each(|(m, e, expected_m, expected_e)| { - let a = SafeFloat::new(m, e).unwrap(); - assert_eq!(a.mantissa, expected_m); - assert_eq!(a.exponent, expected_e); - }); - } - - #[test] - fn test_safefloat_add() { - // Test case: man_a, exp_a, man_b, exp_b, expected mantissa of a+b, expected exponent of a+b - [ - // 1 + 1 = 2 - ( - 1_u128, - 0, - 1_u128, - 0, - 200_000_000_000_000_000_000_u128, - -20_i64, - ), - // 0 + 1 = 1 - (0, 0, 1, 0, 1_000_000_000_000_000_000_000_u128, -21_i64), - // 0 + 0.1 = 0.1 - (0, 0, 1, -1, 1_000_000_000_000_000_000_000_u128, -22_i64), - // 1e-1000 + 0.1 = 0.1 - (1, -1000, 1, -1, 1_000_000_000_000_000_000_000_u128, -22_i64), - // SAFE_FLOAT_MAX + SAFE_FLOAT_MAX - ( - SAFE_FLOAT_MAX, - 0, - SAFE_FLOAT_MAX, - 0, - SAFE_FLOAT_MAX * 2 / 10, - 1_i64, - ), - // Expected loss of precision: tiny + huge - ( - 1_u128, - 0, - 1_000_000_000_000_000_000_000_u128, - 1, - 1_000_000_000_000_000_000_000_u128, - 1_i64, - ), - ( - 1_u128, - 0, - 1_u128, - 22, - 1_000_000_000_000_000_000_000_u128, - 1_i64, - ), - ( - 1_u128, - 0, - 1_u128, - 23, - 1_000_000_000_000_000_000_000_u128, - 2_i64, - ), - ( - 123_u128, - 0, - 1_u128, - 23, - 1_000_000_000_000_000_000_000_u128, - 2_i64, - ), - ( - 123_u128, - 1, - 1_u128, - 23, - 100_000_000_000_000_000_001_u128, - 3_i64, - ), - // Small-ish + very large (10^22 + 42) - // 42 * 10^0 + 1 * 10^22 ≈ 1e22 + 42 - // Normalized ≈ (1e21 + 4) * 10^1 - ( - 42_u128, - 0, - 1_u128, - 22, - 1_000_000_000_000_000_000_000_u128, - 1_i64, - ), - // "Almost 10^21" + 10^22 - // (10^21 - 1) + 10^22 → floor((10^22 + 10^21 - 1) / 100) * 10^2 - ( - 999_999_999_999_999_999_999_u128, - 0, - 1_u128, - 22, - 109_999_999_999_999_999_999_u128, - 2_i64, - ), - // Small-ish + 10^23 where the small part is completely lost - // 42 + 10^23 -> floor((10^23 + 42)/100) * 10^2 ≈ 1e21 * 10^2 - ( - 42_u128, - 0, - 1_u128, - 23, - 1_000_000_000_000_000_000_000_u128, - 2_i64, - ), - // Small-ish + 10^23 where tiny part slightly affects mantissa - // 4200 + 10^23 -> floor((10^23 + 4200)/100) * 10^2 = (1e21 + 42) * 10^2 - ( - 4_200_u128, - 0, - 1_u128, - 23, - 100_000_000_000_000_000_004_u128, - 3_i64, - ), - // (10^21 - 1) + 10^23 - // -> floor((10^23 + 10^21 - 1)/100) = 1e21 + 1e19 - 1 - ( - 999_999_999_999_999_999_999_u128, - 0, - 1_u128, - 23, - 100_999_999_999_999_999_999_u128, - 3_i64, - ), - // Medium + 10^23 with exponent 1 on the smaller term - // 999_999 * 10^1 + 1 * 10^23 -> (10^22 + 999_999) * 10^1 - // Normalized ≈ (1e21 + 99_999) * 10^2 - ( - 999_999_u128, - 1, - 1_u128, - 23, - 100_000_000_000_000_009_999_u128, - 3_i64, - ), - // Check behaviour with exponent 24, tiny second term - // 1 * 10^24 + 1 -> floor((10^24 + 1)/1000) * 10^3 ≈ 1e21 * 10^3 - ( - 1_u128, - 24, - 1_u128, - 0, - 1_000_000_000_000_000_000_000_u128, - 3_i64, - ), - // 1 * 10^24 + a non-trivial small mantissa - // 1e24 + 123456789012345678901 -> floor(/1000) = 1e21 + 123456789012345678 - ( - 1_u128, - 24, - 123_456_789_012_345_678_901_u128, - 0, - 100_012_345_678_901_234_567_u128, - 4_i64, - ), - // 10^22 and 10^23 combined: - // 1 * 10^22 + 1 * 10^23 = 11 * 10^22 = (1.1 * 10^23) - // Normalized → (1.1e20) * 10^3 - ( - 1_u128, - 22, - 1_u128, - 23, - 110_000_000_000_000_000_000_u128, - 3_i64, - ), - // Both operands already aligned at a huge scale: - // (10^21 - 1) * 10^22 + 1 * 10^22 = 10^21 * 10^22 = 10^43 - // Canonical form: (1e21) * 10^22 - ( - 999_999_999_999_999_999_999_u128, - 22, - 1_u128, - 22, - 1_000_000_000_000_000_000_000_u128, - 22_i64, - ), - ] - .into_iter() - .for_each(|(m_a, e_a, m_b, e_b, expected_m, expected_e)| { - let a = SafeFloat::new(m_a, e_a).unwrap(); - let b = SafeFloat::new(m_b, e_b).unwrap(); - - let a_plus_b = a.add(&b).unwrap(); - let b_plus_a = b.add(&a).unwrap(); - - assert_eq!(a_plus_b.mantissa, expected_m); - assert_eq!(a_plus_b.exponent, expected_e); - assert_eq!(b_plus_a.mantissa, expected_m); - assert_eq!(b_plus_a.exponent, expected_e); - }); - } - - #[test] - fn test_safefloat_div_by_zero_is_none() { - let a = SafeFloat::new(1u128, 0).unwrap(); - assert!(a.div(&SafeFloat::zero()).is_none()); - } - - #[test] - fn test_safefloat_div() { - // Test case: man_a, exp_a, man_b, exp_b - [ - (1_u128, 0_i64, 100_000_000_000_000_000_000_u128, -20_i64), - (1_u128, 0, 1_u128, 0), - (1_u128, 1, 1_u128, 0), - (1_u128, 7, 1_u128, 0), - (1_u128, 50, 1_u128, 0), - (1_u128, 100, 1_u128, 0), - (1_u128, 0, 7_u128, 0), - (1_u128, 1, 7_u128, 0), - (1_u128, 7, 7_u128, 0), - (1_u128, 50, 7_u128, 0), - (1_u128, 100, 7_u128, 0), - (1_u128, 0, 3_u128, 0), - (1_u128, 1, 3_u128, 0), - (1_u128, 7, 3_u128, 0), - (1_u128, 50, 3_u128, 0), - (1_u128, 100, 3_u128, 0), - (2_u128, 0, 3_u128, 0), - (2_u128, 1, 3_u128, 0), - (2_u128, 7, 3_u128, 0), - (2_u128, 50, 3_u128, 0), - (2_u128, 100, 3_u128, 0), - (5_u128, 0, 3_u128, 0), - (5_u128, 1, 3_u128, 0), - (5_u128, 7, 3_u128, 0), - (5_u128, 50, 3_u128, 0), - (5_u128, 100, 3_u128, 0), - (10_u128, 0, 100_000_000_000_000_000_000_u128, -19), - (1_000_u128, 0, 100_000_000_000_000_000_000_u128, -17), - ( - 100_000_000_000_000_000_000_u128, - 0, - 1_000_000_000_000_000_000_000_u128, - -1, - ), - (SAFE_FLOAT_MAX, 0, SAFE_FLOAT_MAX, 0), - (SAFE_FLOAT_MAX, 100, SAFE_FLOAT_MAX, -100), - (SAFE_FLOAT_MAX, 100, SAFE_FLOAT_MAX - 1, -100), - (SAFE_FLOAT_MAX - 1, 100, SAFE_FLOAT_MAX, -100), - (SAFE_FLOAT_MAX - 2, 100, SAFE_FLOAT_MAX, -100), - (SAFE_FLOAT_MAX, 100, SAFE_FLOAT_MAX / 2 - 1, -100), - (SAFE_FLOAT_MAX, 100, SAFE_FLOAT_MAX / 2 - 1, 100), - (1_u128, 0, 100_000_000_000_000_000_000_u128, -20_i64), - ( - 123_456_789_123_456_789_123_u128, - 20_i64, - 87_654_321_987_654_321_987_u128, - -20_i64, - ), - ( - 123_456_789_123_456_789_123_u128, - 100_i64, - 87_654_321_987_654_321_987_u128, - -100_i64, - ), - ( - 123_456_789_123_456_789_123_u128, - -100_i64, - 87_654_321_987_654_321_987_u128, - 100_i64, - ), - ( - 123_456_789_123_456_789_123_u128, - -99_i64, - 87_654_321_987_654_321_987_u128, - 99_i64, - ), - ( - 123_456_789_123_456_789_123_u128, - 123_i64, - 87_654_321_987_654_321_987_u128, - -32_i64, - ), - ( - 123_456_789_123_456_789_123_u128, - -123_i64, - 87_654_321_987_654_321_987_u128, - 32_i64, - ), - ] - .into_iter() - .for_each(|(ma, ea, mb, eb)| { - let a = SafeFloat::new(ma, ea).unwrap(); - let b = SafeFloat::new(mb, eb).unwrap(); - - let actual: f64 = a.div(&b).unwrap().into(); - let expected = - ma as f64 * (10_f64).powi(ea as i32) / (mb as f64 * (10_f64).powi(eb as i32)); - - assert_abs_diff_eq!(actual, expected, epsilon = actual / 100_000_000_000_000_f64); - }); - } - - #[test] - fn test_safefloat_mul_div() { - // result = a * b / c - // should not lose precision gained in a * b - // Test case: man_a, exp_a, man_b, exp_b, man_c, exp_c - [ - (1_u128, -20_i64, 1_u128, -20_i64, 1_u128, -20_i64), - (123_u128, 20_i64, 123_u128, -20_i64, 321_u128, 0_i64), - ( - 123_123_123_123_123_123_u128, - 20_i64, - 321_321_321_321_321_321_u128, - -20_i64, - 777_777_777_777_777_777_u128, - 0_i64, - ), - ( - 11_111_111_111_111_111_111_u128, - 20_i64, - 99_321_321_321_321_321_321_u128, - -20_i64, - 77_777_777_777_777_777_777_u128, - 0_i64, - ), - ] - .into_iter() - .for_each(|(ma, ea, mb, eb, mc, ec)| { - let a = SafeFloat::new(ma, ea).unwrap(); - let b = SafeFloat::new(mb, eb).unwrap(); - let c = SafeFloat::new(mc, ec).unwrap(); - - let actual: f64 = a.mul_div(&b, &c).unwrap().into(); - let expected = (ma as f64 * (10_f64).powi(ea as i32)) - * (mb as f64 * (10_f64).powi(eb as i32)) - / (mc as f64 * (10_f64).powi(ec as i32)); - - assert_abs_diff_eq!(actual, expected, epsilon = actual / 100_000_000_000_000_f64); - }); - } - - #[test] - fn test_safefloat_from_u64f64() { - [ - // U64F64::from_num(1000.0), - // U64F64::from_num(10.0), - // U64F64::from_num(1.0), - U64F64::from_num(0.1), - // U64F64::from_num(0.00000001), - // U64F64::from_num(123_456_789_123_456u128), - // // Exact zero - // U64F64::from_num(0.0), - // // Very small positive value (well above Q64.64 resolution) - // U64F64::from_num(1e-18), - // // Value just below 1 - // U64F64::from_num(0.999_999_999_999_999_f64), - // // Value just above 1 - // U64F64::from_num(1.000_000_000_000_001_f64), - // // "Random-looking" fractional with many digits - // U64F64::from_num(1.234_567_890_123_45_f64), - // // Large integer, but smaller than the max integer part of U64F64 - // U64F64::from_num(999_999_999_999_999_999u128), - // // Very large integer near the upper bound of integer range - // U64F64::from_num(u64::MAX as u128), - // // Large number with fractional part - // U64F64::from_num(123_456_789_123_456.78_f64), - // // Medium-large with tiny fractional part to test precision on tail digits - // U64F64::from_num(1_000_000_000_000.000_001_f64), - // // Smallish with long fractional part - // U64F64::from_num(0.123_456_789_012_345_f64), - ] - .into_iter() - .for_each(|f| { - let safe_float: SafeFloat = f.into(); - let actual: f64 = safe_float.into(); - let expected = f.to_num::(); - - // Relative epsilon ~1e-14 of the magnitude - let epsilon = if actual == 0.0 { - 0.0 - } else { - actual.abs() / 100_000_000_000_000_f64 - }; - - assert_abs_diff_eq!(actual, expected, epsilon = epsilon); - }); - } - - /// This is a real-life scenario test when someone lost 7 TAO on Chutes (SN64) - /// when paying fees in Alpha. The scenario occured because the update of share value - /// of one coldkey (update_value_for_one) hit the scenario of full unstake. - /// - /// Specifically, the following condition was triggered: - /// - /// `(shared_value + 2_628_000_000_000_000_u64).checked_div(new_denominator)` - /// - /// returned None because new_denominator was too low and division of - /// `shared_value + 2_628_000_000_000_000_u64` by new_denominator has overflown U64F64. - /// - /// This test fails on the old version of share pool (with much lower tolerances). - /// - /// cargo test --package share-pool --lib -- tests::test_loss_due_to_precision --exact --nocapture - #[test] - fn test_loss_due_to_precision() { - let mock_ops = MockSharePoolDataOperations::new(); - let mut pool = SharePool::::new(mock_ops); - - // Setup pool so that initial coldkey's alpha is 10% of 1e12 = 1e11 rao. - let low_denominator = SafeFloat::new(1u128, -14).unwrap(); - let low_share = SafeFloat::new(1u128, -15).unwrap(); - pool.state_ops.set_denominator(low_denominator); - pool.state_ops.set_shared_value(1_000_000_000_000_u64); - pool.state_ops.set_share(&1, low_share); - - let value_before = pool.get_value(&1) as i128; - assert_abs_diff_eq!(value_before as f64, 100_000_000_000., epsilon = 0.1); - - // Remove a little stake - let unstake_amount = 1000i64; - pool.update_value_for_one(&1, unstake_amount.neg()); - - let value_after = pool.get_value(&1) as i128; - assert_abs_diff_eq!( - (value_before - value_after) as f64, - unstake_amount as f64, - epsilon = unstake_amount as f64 / 1_000_000_000. - ); - } - - fn rel_err(a: f64, b: f64) -> f64 { - let denom = a.abs().max(b.abs()).max(1.0); - (a - b).abs() / denom - } - - fn push_unique(v: &mut Vec, x: u128) { - if x != 0 && !v.contains(&x) { - v.push(x); - } - } - - // cargo test --package share-pool --lib -- tests::test_safefloat_mul_div_wide_range --exact --include-ignored --show-output - #[test] - #[ignore = "long-running sweep test; run explicitly when needed"] - fn test_safefloat_mul_div_wide_range() { - use rayon::prelude::*; - use std::sync::Arc; - use std::sync::atomic::{AtomicUsize, Ordering}; - - // Build mantissa corpus - let mut mantissas = Vec::::new(); - - let linear_steps: u128 = 200; - let linear_step = (SAFE_FLOAT_MAX / linear_steps).max(1); - let mut m = 1u128; - while m <= SAFE_FLOAT_MAX { - push_unique(&mut mantissas, m); - match m.checked_add(linear_step) { - Some(next) if next > m => m = next, - _ => break, - } - } - push_unique(&mut mantissas, SAFE_FLOAT_MAX); - - let mut p = 1u128; - while p <= SAFE_FLOAT_MAX { - push_unique(&mut mantissas, p); - if p > 1 { - push_unique(&mut mantissas, p - 1); - } - if let Some(next) = p.checked_add(1) - && next <= SAFE_FLOAT_MAX - { - push_unique(&mut mantissas, next); - } - - match p.checked_mul(10) { - Some(next) if next > p && next <= SAFE_FLOAT_MAX => p = next, - _ => break, - } - } - - for delta in [ - 0u128, 1, 2, 3, 7, 9, 10, 11, 99, 100, 101, 999, 1_000, 10_000, - ] { - if SAFE_FLOAT_MAX > delta { - push_unique(&mut mantissas, SAFE_FLOAT_MAX - delta); - } - } - - mantissas.sort_unstable(); - mantissas.dedup(); - - let exp_min: i64 = -120; - let exp_max: i64 = 120; - let exp_step: usize = 5; - let exponents: Vec = (exp_min..=exp_max).step_by(exp_step).collect(); - - // Precompute all (a, b) pairs as outer work items. - // Each Rayon task will then iterate all c's sequentially. - let mut outer_cases: Vec<(u128, i64, u128, i64)> = Vec::new(); - - for &ma in &mantissas { - for &ea in &exponents { - for &mb in &mantissas { - for &eb in &exponents { - outer_cases.push((ma, ea, mb, eb)); - } - } - } - } - - let checked = Arc::new(AtomicUsize::new(0)); - let skipped_non_finite = Arc::new(AtomicUsize::new(0)); - let skipped_invalid_sf = Arc::new(AtomicUsize::new(0)); - - let progress_step = 10_000usize; - let total_outer = outer_cases.len(); - - outer_cases.into_par_iter().for_each(|(ma, ea, mb, eb)| { - let a = match SafeFloat::new(ma, ea) { - Some(x) => x, - None => { - skipped_invalid_sf.fetch_add(1, Ordering::Relaxed); - return; - } - }; - - let b = match SafeFloat::new(mb, eb) { - Some(x) => x, - None => { - skipped_invalid_sf.fetch_add(1, Ordering::Relaxed); - return; - } - }; - - for &mc in &mantissas { - for &ec in &exponents { - let c = match SafeFloat::new(mc, ec) { - Some(x) => x, - None => { - skipped_invalid_sf.fetch_add(1, Ordering::Relaxed); - continue; - } - }; - - let actual_sf = a.mul_div(&b, &c).unwrap(); - let actual: f64 = actual_sf.into(); - - let expected = - (ma as f64 * 10_f64.powi(ea as i32)) - * (mb as f64 * 10_f64.powi(eb as i32)) - / (mc as f64 * 10_f64.powi(ec as i32)); - - if !expected.is_finite() || !actual.is_finite() { - skipped_non_finite.fetch_add(1, Ordering::Relaxed); - continue; - } - - let err = rel_err(actual, expected); - - assert!( - err <= 1e-12, - concat!( - "mul_div mismatch:\n", - " a = {}e{}\n", - " b = {}e{}\n", - " c = {}e{}\n", - " actual = {:.20e}\n", - " expected = {:.20e}\n", - " rel_err = {:.20e}" - ), - ma, ea, mb, eb, mc, ec, actual, expected, err - ); - - checked.fetch_add(1, Ordering::Relaxed); - } - } - - let done_outer = checked.load(Ordering::Relaxed); - if done_outer % progress_step == 0 { - let invalid = skipped_invalid_sf.load(Ordering::Relaxed); - let non_finite = skipped_non_finite.load(Ordering::Relaxed); - log::debug!( - "progress: checked={}, skipped_invalid_sf={}, skipped_non_finite={}, outer_total={}", - done_outer, - invalid, - non_finite, - total_outer, - ); - } - }); - - let checked = checked.load(Ordering::Relaxed); - let skipped_non_finite = skipped_non_finite.load(Ordering::Relaxed); - let skipped_invalid_sf = skipped_invalid_sf.load(Ordering::Relaxed); - - println!( - "checked={}, skipped_non_finite={}, skipped_invalid_sf={}, mantissas={}, exponents={}, outer_cases={}", - checked, - skipped_non_finite, - skipped_invalid_sf, - mantissas.len(), - exponents.len(), - total_outer, - ); - - assert!(checked > 0, "test did not validate any finite cases"); - } - - #[test] - #[ignore = "long-running broad-range test; run explicitly when needed"] - fn test_safefloat_div_wide_range() { - use rayon::prelude::*; - use std::sync::Arc; - use std::sync::atomic::{AtomicUsize, Ordering}; - - fn rel_err(a: f64, b: f64) -> f64 { - let denom = a.abs().max(b.abs()).max(1.0); - (a - b).abs() / denom - } - - fn push_unique(v: &mut Vec, x: u128) { - if x != 0 && !v.contains(&x) { - v.push(x); - } - } - - // Build a broad mantissa corpus: - // - coarse linear sweep - // - powers of 10 and neighbors - // - values near SAFE_FLOAT_MAX - let mut mantissas = Vec::::new(); - - let linear_steps: u128 = 200; - let linear_step = (SAFE_FLOAT_MAX / linear_steps).max(1); - let mut m = 1u128; - while m <= SAFE_FLOAT_MAX { - push_unique(&mut mantissas, m); - match m.checked_add(linear_step) { - Some(next) if next > m => m = next, - _ => break, - } - } - push_unique(&mut mantissas, SAFE_FLOAT_MAX); - - let mut p = 1u128; - while p <= SAFE_FLOAT_MAX { - push_unique(&mut mantissas, p); - if p > 1 { - push_unique(&mut mantissas, p - 1); - } - if let Some(next) = p.checked_add(1) - && next <= SAFE_FLOAT_MAX - { - push_unique(&mut mantissas, next); - } - - match p.checked_mul(10) { - Some(next) if next > p && next <= SAFE_FLOAT_MAX => p = next, - _ => break, - } - } - - for delta in [ - 0u128, 1, 2, 3, 7, 9, 10, 11, 99, 100, 101, 999, 1_000, 10_000, - ] { - if SAFE_FLOAT_MAX > delta { - push_unique(&mut mantissas, SAFE_FLOAT_MAX - delta); - } - } - - mantissas.sort_unstable(); - mantissas.dedup(); - - // Exponent sweep. - // Keep it large enough to stress normalization / exponent math, - // but still practical for f64 reference calculations. - let exp_min: i64 = -120; - let exp_max: i64 = 120; - let exp_step: usize = 5; - let exponents: Vec = (exp_min..=exp_max).step_by(exp_step).collect(); - - let m_len = mantissas.len(); - let e_len = exponents.len(); - let total_cases = m_len * e_len * m_len * e_len; - - let checked = Arc::new(AtomicUsize::new(0)); - let skipped_non_finite = Arc::new(AtomicUsize::new(0)); - let skipped_invalid_sf = Arc::new(AtomicUsize::new(0)); - let done_counter = Arc::new(AtomicUsize::new(0)); - - (0..total_cases).into_par_iter().for_each(|idx| { - let mut rem = idx; - - let eb_idx = rem % e_len; - rem /= e_len; - - let mb_idx = rem % m_len; - rem /= m_len; - - let ea_idx = rem % e_len; - rem /= e_len; - - let ma_idx = rem % m_len; - - let ma = mantissas[ma_idx]; - let ea = exponents[ea_idx]; - let mb = mantissas[mb_idx]; - let eb = exponents[eb_idx]; - - let a = match SafeFloat::new(ma, ea) { - Some(x) => x, - None => { - skipped_invalid_sf.fetch_add(1, Ordering::Relaxed); - done_counter.fetch_add(1, Ordering::Relaxed); - return; - } - }; - - let b = match SafeFloat::new(mb, eb) { - Some(x) => x, - None => { - skipped_invalid_sf.fetch_add(1, Ordering::Relaxed); - done_counter.fetch_add(1, Ordering::Relaxed); - return; - } - }; - - let actual_sf = match a.div(&b) { - Some(x) => x, - None => { - skipped_invalid_sf.fetch_add(1, Ordering::Relaxed); - done_counter.fetch_add(1, Ordering::Relaxed); - return; - } - }; - - let actual: f64 = actual_sf.into(); - let expected = - (ma as f64 * 10_f64.powi(ea as i32)) / (mb as f64 * 10_f64.powi(eb as i32)); - - if !actual.is_finite() || !expected.is_finite() { - skipped_non_finite.fetch_add(1, Ordering::Relaxed); - } else { - let err = rel_err(actual, expected); - - assert!( - err <= 1e-12, - concat!( - "div mismatch:\n", - " a = {}e{}\n", - " b = {}e{}\n", - " actual = {:.20e}\n", - " expected = {:.20e}\n", - " rel_err = {:.20e}" - ), - ma, - ea, - mb, - eb, - actual, - expected, - err - ); - - checked.fetch_add(1, Ordering::Relaxed); - } - - let done = done_counter.fetch_add(1, Ordering::Relaxed) + 1; - if done % 10_000 == 0 { - let progress = done as f64 / total_cases as f64 * 100.0; - log::debug!("div progress = {progress:.4}%"); - } - }); - - let checked = checked.load(Ordering::Relaxed); - let skipped_non_finite = skipped_non_finite.load(Ordering::Relaxed); - let skipped_invalid_sf = skipped_invalid_sf.load(Ordering::Relaxed); - - println!( - "div checked={}, skipped_non_finite={}, skipped_invalid_sf={}, mantissas={}, exponents={}, total_cases={}", - checked, - skipped_non_finite, - skipped_invalid_sf, - mantissas.len(), - exponents.len(), - total_cases, - ); - - assert!(checked > 0, "div test did not validate any finite cases"); - } -} +mod tests; diff --git a/primitives/share-pool/src/safe_float.rs b/primitives/share-pool/src/safe_float.rs new file mode 100644 index 0000000000..1b8b670d05 --- /dev/null +++ b/primitives/share-pool/src/safe_float.rs @@ -0,0 +1,395 @@ +//! [`SafeFloat`]: controlled-precision decimal used by [`crate::SharePool`] stake shares. +//! +//! Backed by a u128 mantissa and i64 base-10 exponent, normalized so that +//! `SAFE_FLOAT_MAX / 10 < mantissa <= SAFE_FLOAT_MAX` (except zero). + +use codec::{Decode, Encode}; +#[cfg(not(feature = "std"))] +use num_traits::float::FloatCore as _; +use scale_info::TypeInfo; +use sp_core::U256; +use substrate_fixed::types::U64F64; +use subtensor_macros::freeze_struct; + +/// Maximum mantissa digits retained after [`SafeFloat::normalize`] (10^21). +pub const SAFE_FLOAT_MAX: u128 = 1_000_000_000_000_000_000_000_u128; +/// `log10(SAFE_FLOAT_MAX)`; also the scale used when dividing mantissas in U256. +pub const SAFE_FLOAT_MAX_EXP: i64 = 21_i64; + +/// Controlled-precision float for share-pool stake accounting (rao-scale). +/// +/// Mantissa precision is tuned so a +1 rao hotkey stake update moves both the +/// coldkey share and the share-pool denominator, while a fractional 0.1 rao +/// (which cannot exist on-chain) does not. +#[freeze_struct("9358e1962fcbda0d")] +#[derive(Encode, Decode, Default, TypeInfo, Clone, PartialEq, Eq, Debug)] +pub struct SafeFloat { + mantissa: u128, + exponent: i64, +} + +/// Return `10^e` as [`U256`], capped at `10^(SAFE_FLOAT_MAX_EXP+1)`. +/// +/// Used when aligning mantissas across exponents during add/sub and u64 conversion. +fn capped_pow10(e: u64) -> U256 { + if e > (SAFE_FLOAT_MAX_EXP as u64).saturating_add(1) { + return U256::from(SAFE_FLOAT_MAX.saturating_mul(10)); + } + if e == 0 { + return U256::from(1); + } + U256::from(10) + .checked_pow(U256::from(e)) + .unwrap_or_default() +} + +impl SafeFloat { + /// Zero value (`mantissa = 0`, `exponent = 0`). + pub fn zero() -> Self { + SafeFloat { + mantissa: 0_u128, + exponent: 0_i64, + } + } + + /// Construct and normalize; returns `None` if `mantissa > SAFE_FLOAT_MAX`. + pub fn new(mantissa: u128, exponent: i64) -> Option { + // Cap mantissa at SAFE_FLOAT_MAX + if mantissa > SAFE_FLOAT_MAX { + return None; + } + + let mut safe_float = SafeFloat::zero(); + + if safe_float.normalize(&U256::from(mantissa), exponent) { + Some(safe_float) + } else { + None + } + } + + /// Sets the new mantissa and exponent adjusting mantissa and exponent so that + /// SAFE_FLOAT_MAX / 10 < mantissa <= SAFE_FLOAT_MAX + /// + /// Returns true in case of success or false if exponent over- or underflows + pub(crate) fn normalize(&mut self, new_mantissa: &U256, new_exponent: i64) -> bool { + if new_mantissa.is_zero() { + self.mantissa = 0; + self.exponent = 0; + return true; + } + + let ten = U256::from(10); + let max_mantissa = U256::from(SAFE_FLOAT_MAX); + let min_mantissa = U256::from(SAFE_FLOAT_MAX) + .checked_div(ten) + .unwrap_or_default(); + + // Loops are safe because they are bounded by U256 size and result + // in no more than 78 iterations together + let mut normalized_mantissa = *new_mantissa; + let mut normalized_exponent = new_exponent; + + while normalized_mantissa > max_mantissa { + let Some(next_mantissa) = normalized_mantissa.checked_div(ten) else { + return false; + }; + let Some(next_exponent) = normalized_exponent.checked_add(1) else { + return false; + }; + + normalized_mantissa = next_mantissa; + normalized_exponent = next_exponent; + } + + while normalized_mantissa <= min_mantissa { + let Some(next_mantissa) = normalized_mantissa.checked_mul(ten) else { + return false; + }; + let Some(next_exponent) = normalized_exponent.checked_sub(1) else { + return false; + }; + + normalized_mantissa = next_mantissa; + normalized_exponent = next_exponent; + } + + self.mantissa = normalized_mantissa.low_u128(); + self.exponent = normalized_exponent; + + true + } + + /// Divide current value by a preserving precision (SAFE_FLOAT_MAX digits in mantissa) + /// result = m1 * 10^e1 / m2 * 10^e2 + pub fn div(&self, a: &SafeFloat) -> Option { + // - In m1 / m2 division we need enough digits for a u128. + // This can be calculated in a lossless way in U256 as m1 * MAX_MANTISSA / m2 + // - The new exponent is e1 - e2 - SAFE_FLOAT_MAX_EXP + let maybe_m1_scaled_u256 = + U256::from(self.mantissa).checked_mul(U256::from(SAFE_FLOAT_MAX)); + let m2_u256 = U256::from(a.mantissa); + + // Calculate new exponent + let new_exponent_i128 = (self.exponent as i128) + .saturating_sub(a.exponent as i128) + .saturating_sub(SAFE_FLOAT_MAX_EXP as i128); + if (new_exponent_i128 > i64::MAX as i128) || (new_exponent_i128 < i64::MIN as i128) { + return None; + } + let new_exponent = new_exponent_i128 as i64; + + // Calculate new mantissa, normalize, and return result + if let Some(m1_scaled_u256) = maybe_m1_scaled_u256 { + let maybe_new_mantissa_u256 = m1_scaled_u256.checked_div(m2_u256); + if let Some(new_mantissa_u256) = maybe_new_mantissa_u256 { + let mut safe_float = SafeFloat::zero(); + if safe_float.normalize(&new_mantissa_u256, new_exponent) { + Some(safe_float) + } else { + None + } + } else { + None + } + } else { + None + } + } + + /// Add two normalized values, aligning exponents via [`capped_pow10`]. + pub fn add(&self, a: &SafeFloat) -> Option { + if self.is_zero() { + return Some(a.clone()); + } + if a.is_zero() { + return Some(self.clone()); + } + + let (new_mantissa, new_exponent) = if self.exponent >= a.exponent { + let exp_diff = self.exponent.saturating_sub(a.exponent); + let m1 = U256::from(self.mantissa); + let m2 = U256::from(a.mantissa) + .checked_div(capped_pow10(exp_diff as u64)) + .unwrap_or_default(); + (m1.saturating_add(m2), self.exponent) + } else { + let exp_diff = a.exponent.saturating_sub(self.exponent); + let m1 = U256::from(self.mantissa) + .checked_div(capped_pow10(exp_diff as u64)) + .unwrap_or_default(); + let m2 = U256::from(a.mantissa); + (m1.saturating_add(m2), a.exponent) + }; + + let mut safe_float = SafeFloat::zero(); + if safe_float.normalize(&new_mantissa, new_exponent) { + Some(safe_float) + } else { + None + } + } + + /// Subtract `a` from `self`; returns `None` if the result would be negative. + pub fn sub(&self, a: &SafeFloat) -> Option { + if self.is_zero() && a.is_zero() { + return Some(Self::zero()); + } else if self.is_zero() { + return None; + } + if a.is_zero() { + return Some(self.clone()); + } + + let (new_mantissa, new_exponent) = if self.exponent >= a.exponent { + let exp_diff = self.exponent.saturating_sub(a.exponent); + let m1 = U256::from(self.mantissa); + let m2 = U256::from(a.mantissa) + .checked_div(capped_pow10(exp_diff as u64)) + .unwrap_or_default(); + (m1.saturating_sub(m2), self.exponent) + } else { + let exp_diff = a.exponent.saturating_sub(self.exponent); + let m1 = U256::from(self.mantissa) + .checked_div(capped_pow10(exp_diff as u64)) + .unwrap_or_default(); + let m2 = U256::from(a.mantissa); + (m1.saturating_sub(m2), a.exponent) + }; + + let mut safe_float = SafeFloat::zero(); + if safe_float.normalize(&new_mantissa, new_exponent) { + Some(safe_float) + } else { + None + } + } + + /// Calculate self * a / b without loss of precision + pub fn mul_div(&self, a: &SafeFloat, b: &SafeFloat) -> Option { + if b.mantissa == 0_u128 { + return None; + } + + // No overflows here, just unwrap or default + let self_a_mantissa_u256 = U256::from(self.mantissa) + .checked_mul(U256::from(a.mantissa)) + .unwrap_or_default(); + let maybe_self_a_exponent = self.exponent.checked_add(a.exponent); + + if let Some(self_a_exponent) = maybe_self_a_exponent { + // Divide by b in U256 + let maybe_new_exponent = self_a_exponent.checked_sub(b.exponent); + if let Some(new_exponent) = maybe_new_exponent { + let new_mantissa = self_a_mantissa_u256 + .checked_div(U256::from(b.mantissa)) + .unwrap_or_default(); + let mut result = SafeFloat::zero(); + if result.normalize(&new_mantissa, new_exponent) { + Some(result) + } else { + None + } + } else { + None + } + } else { + None + } + } + + /// True when the mantissa is zero (canonical zero representation). + pub fn is_zero(&self) -> bool { + self.mantissa == 0u128 + } + + /// Normalized mantissa digits (test-only; production code uses [`Self::is_zero`] etc.). + #[cfg(test)] + pub(crate) fn mantissa(&self) -> u128 { + self.mantissa + } + + /// Base-10 exponent (test-only). + #[cfg(test)] + pub(crate) fn exponent(&self) -> i64 { + self.exponent + } + + /// Returns true if self > a + /// Both values should be normalized + pub fn gt(&self, a: &SafeFloat) -> bool { + let ten = U256::from(10); + + if self.exponent == a.exponent { + self.mantissa > a.mantissa + } else if self.exponent > a.exponent { + let exp_diff = self.exponent.saturating_sub(a.exponent); + if exp_diff > 1_i64 { + true + } else { + ten.saturating_mul(U256::from(self.mantissa)) > U256::from(a.mantissa) + } + } else { + let exp_diff = a.exponent.saturating_sub(self.exponent); + if exp_diff > 1_i64 { + false + } else { + U256::from(self.mantissa) > ten.saturating_mul(U256::from(a.mantissa)) + } + } + } +} + +// Saturating conversion: negatives -> 0, overflow -> u64::MAX +impl From<&SafeFloat> for u64 { + fn from(value: &SafeFloat) -> Self { + // If exponent is zero, it's just an integer mantissa + if value.exponent == 0 { + return u64::try_from(value.mantissa).unwrap_or(u64::MAX); + } + + // scale = 10^exponent + let scale = capped_pow10(value.exponent.unsigned_abs()); + + // mantissa * 10^exponent + let q: U256 = if value.exponent > 0 { + U256::from(value.mantissa).saturating_mul(scale) + } else { + U256::from(value.mantissa) + .checked_div(scale) + .unwrap_or_default() + }; + + // Convert quotient to u64, saturating on overflow + if q.is_zero() { + 0 + } else { + q.try_into().unwrap_or(u64::MAX) + } + } +} + +// Convenience impl for owning values +impl From for u64 { + fn from(value: SafeFloat) -> Self { + u64::from(&value) + } +} + +impl From for SafeFloat { + fn from(value: u64) -> Self { + SafeFloat::new(value as u128, 0).unwrap_or_default() + } +} + +impl From for SafeFloat { + fn from(value: U64F64) -> Self { + let bits = value.to_bits(); + // High 64 bits = integer part + let int = (bits >> 64) as u64; + // Low 64 bits = fractional part + let frac = (bits & 0xFFFF_FFFF_FFFF_FFFF) as u64; + + // If strictly zero, shortcut + if bits == 0 { + return SafeFloat::zero(); + } + + // SafeFloat for integer part: int * 10^0 + let safe_int = SafeFloat::new(int as u128, 0).unwrap_or_default(); + + // Numerator of fractional part: frac * 10^0 + let safe_frac_num = SafeFloat::new(frac as u128, 0).unwrap_or_default(); + + // Denominator = 2^64 as an integer SafeFloat: (2^64) * 10^0 + let two64: u128 = 1u128 << 64; + let safe_two64 = SafeFloat::new(two64, 0).unwrap_or_default(); + + // frac_part = frac / 2^64 + let safe_frac = safe_frac_num.div(&safe_two64).unwrap_or_default(); + + // int + frac/2^64, with all mantissa/exponent normalization + safe_int.add(&safe_frac).unwrap_or_default() + } +} + +impl From<&SafeFloat> for f64 { + #[allow( + clippy::arithmetic_side_effects, + reason = "This code is only used in tests" + )] + fn from(value: &SafeFloat) -> Self { + let mant = value.mantissa as f64; + + // powi takes i32, so clamp i64 exponent into i32 range (test-only). + let e = value.exponent.clamp(i32::MIN as i64, i32::MAX as i64) as i32; + + mant * 10_f64.powi(e) + } +} + +impl From for f64 { + fn from(value: SafeFloat) -> Self { + f64::from(&value) + } +} diff --git a/primitives/share-pool/src/share_pool.rs b/primitives/share-pool/src/share_pool.rs new file mode 100644 index 0000000000..98cbaecd47 --- /dev/null +++ b/primitives/share-pool/src/share_pool.rs @@ -0,0 +1,212 @@ +//! [`SharePool`]: proportional rao ownership keyed by coldkey/hotkey (or any `Eq` key). + +use sp_std::marker; +use sp_std::ops::Neg; + +use crate::SafeFloat; + +/// Persistence backend for a [`SharePool`]: total rao, per-key shares, denominator. +pub trait SharePoolDataOperations { + /// Total shared value in integer rao (not a fractional share). + fn get_shared_value(&self) -> u64; + /// Share units held by `key` (zero if unset). + fn get_share(&self, key: &Key) -> SafeFloat; + /// Share units for `key`, or `Err(())` if the key has no entry. + fn try_get_share(&self, key: &Key) -> Result; + /// Sum of all share units in the pool (denominator for ownership ratios). + fn get_denominator(&self) -> SafeFloat; + /// Replace the total shared rao value. + fn set_shared_value(&mut self, value: u64); + /// Replace the share units stored for `key`. + fn set_share(&mut self, key: &Key, share: SafeFloat); + /// Replace the pool denominator. + fn set_denominator(&mut self, update: SafeFloat); +} + +/// Proportional ownership pool: each key owns `share / denominator` of `shared_value` rao. +#[derive(Debug)] +pub struct SharePool +where + K: Eq, + Ops: SharePoolDataOperations, +{ + /// Storage backend; `pub(crate)` so unit tests can seed pool state directly. + pub(crate) state_ops: Ops, + phantom_key: marker::PhantomData, +} + +impl SharePool +where + K: Eq, + Ops: SharePoolDataOperations, +{ + /// Wrap a storage backend; no pool state is created until the first update. + pub fn new(ops: Ops) -> Self { + SharePool { + state_ops: ops, + phantom_key: marker::PhantomData, + } + } + + /// Absolute rao owned by `key`: `shared_value * share(key) / denominator`. + pub fn get_value(&self, key: &K) -> u64 { + let shared_value: SafeFloat = + SafeFloat::new(self.state_ops.get_shared_value() as u128, 0).unwrap_or_default(); + let current_share: SafeFloat = self.state_ops.get_share(key); + let denominator: SafeFloat = self.state_ops.get_denominator(); + shared_value + .mul_div(¤t_share, &denominator) + .unwrap_or_default() + .into() + } + + /// Absolute rao for an arbitrary share amount (without looking up a key). + pub fn get_value_from_shares(&self, current_share: SafeFloat) -> u64 { + let shared_value: SafeFloat = + SafeFloat::new(self.state_ops.get_shared_value() as u128, 0).unwrap_or_default(); + let denominator: SafeFloat = self.state_ops.get_denominator(); + shared_value + .mul_div(¤t_share, &denominator) + .unwrap_or_default() + .into() + } + + /// Like [`Self::get_value`], but `Err` if `key` has no share entry. + pub fn try_get_value(&self, key: &K) -> Result { + match self.state_ops.try_get_share(key) { + Ok(_) => Ok(self.get_value(key)), + Err(i) => Err(i), + } + } + + /// Apply a signed rao delta to the shared total; every key's absolute value scales with it. + pub fn update_value_for_all(&mut self, update: i64) { + let shared_value: u64 = self.state_ops.get_shared_value(); + self.state_ops.set_shared_value(if update >= 0 { + shared_value.saturating_add(update as u64) + } else { + shared_value.saturating_sub(update.neg() as u64) + }); + } + + /// Dry-run whether a non-zero share delta would result from `update` for some key. + /// + /// Does not mutate share state; used by staking to reject dust updates. + pub fn sim_update_value_for_one(&mut self, update: i64) -> bool { + let shared_value: u64 = self.state_ops.get_shared_value(); + let denominator: SafeFloat = self.state_ops.get_denominator(); + + // Then, update this key's share + if denominator.is_zero() { + true + } else { + // There are already keys in the pool, set or update this key + let shares_per_update = + self.shares_for_value_update(update, shared_value, &denominator); + + !shares_per_update.is_zero() + } + } + + /// Share units corresponding to an absolute rao `update` at current pool scale. + pub(crate) fn shares_for_value_update( + &self, + update: i64, + shared_value: u64, + denominator: &SafeFloat, + ) -> SafeFloat { + let shared_value: SafeFloat = SafeFloat::new(shared_value as u128, 0).unwrap_or_default(); + let update_sf: SafeFloat = + SafeFloat::new(update.unsigned_abs() as u128, 0).unwrap_or_default(); + update_sf + .mul_div(denominator, &shared_value) + .unwrap_or_default() + } + + /// Apply a signed rao delta to one key's ownership and to the shared total. + /// + /// Initializes the pool on the first non-empty update (that key gets all shares). + /// SafeFloat overflows are logged and skipped rather than panicking. + pub fn update_value_for_one(&mut self, key: &K, update: i64) { + let shared_value: u64 = self.state_ops.get_shared_value(); + let current_share: SafeFloat = self.state_ops.get_share(key); + let denominator: SafeFloat = self.state_ops.get_denominator(); + + // Then, update this key's share + if denominator.is_zero() { + // Initialize the pool. The first key gets all. + let update_float: SafeFloat = + SafeFloat::new(update.unsigned_abs() as u128, 0).unwrap_or_default(); + self.state_ops.set_denominator(update_float.clone()); + self.state_ops.set_share(key, update_float); + } else { + let new_denominator; + let new_current_share; + + let shares_per_update: SafeFloat = + self.shares_for_value_update(update, shared_value, &denominator); + + // Handle SafeFloat overflows quietly here because this overflow of i64 exponent + // is extremely hypothetical and should never happen in practice. + if update > 0 { + new_denominator = match denominator.add(&shares_per_update) { + Some(new_denominator) => new_denominator, + None => { + log::error!( + "SafeFloat::add overflow when adding {:?} to {:?}; keeping old denominator", + shares_per_update, + denominator, + ); + // Return the value as it was before the failed addition + denominator + } + }; + + new_current_share = match current_share.add(&shares_per_update) { + Some(new_current_share) => new_current_share, + None => { + log::error!( + "SafeFloat::add overflow when adding {:?} to {:?}; keeping old current_share", + shares_per_update, + current_share, + ); + // Return the value as it was before the failed addition + current_share + } + }; + } else { + new_denominator = match denominator.sub(&shares_per_update) { + Some(new_denominator) => new_denominator, + None => { + log::error!( + "SafeFloat::add overflow when adding {:?} to {:?}; keeping old denominator", + shares_per_update, + denominator, + ); + // Return the value as it was before the failed addition + denominator + } + }; + + new_current_share = match current_share.sub(&shares_per_update) { + Some(new_current_share) => new_current_share, + None => { + log::error!( + "SafeFloat::add overflow when adding {:?} to {:?}; keeping old current_share", + shares_per_update, + current_share, + ); + // Return the value as it was before the failed addition + current_share + } + }; + } + + self.state_ops.set_denominator(new_denominator); + self.state_ops.set_share(key, new_current_share); + } + + // Update shared value + self.update_value_for_all(update); + } +} diff --git a/primitives/share-pool/src/tests/mod.rs b/primitives/share-pool/src/tests/mod.rs new file mode 100644 index 0000000000..6968bf4863 --- /dev/null +++ b/primitives/share-pool/src/tests/mod.rs @@ -0,0 +1,1069 @@ +//! Unit tests for [`SafeFloat`](crate::SafeFloat) and [`SharePool`](crate::SharePool). + +use super::*; +use approx::assert_abs_diff_eq; +use std::collections::BTreeMap; +use std::ops::Neg; +use substrate_fixed::types::U64F64; + +struct MockSharePoolDataOperations { + shared_value: u64, + share: BTreeMap, + denominator: SafeFloat, +} + +impl MockSharePoolDataOperations { + fn new() -> Self { + MockSharePoolDataOperations { + shared_value: 0u64, + share: BTreeMap::new(), + denominator: SafeFloat::zero(), + } + } +} + +impl SharePoolDataOperations for MockSharePoolDataOperations { + fn get_shared_value(&self) -> u64 { + self.shared_value + } + + fn get_share(&self, key: &u16) -> SafeFloat { + self.share.get(key).cloned().unwrap_or_else(SafeFloat::zero) + } + + fn try_get_share(&self, key: &u16) -> Result { + match self.share.get(key).cloned() { + Some(value) => Ok(value), + None => Err(()), + } + } + + fn get_denominator(&self) -> SafeFloat { + self.denominator.clone() + } + + fn set_shared_value(&mut self, value: u64) { + self.shared_value = value; + } + + fn set_share(&mut self, key: &u16, share: SafeFloat) { + self.share.insert(*key, share); + } + + fn set_denominator(&mut self, update: SafeFloat) { + self.denominator = update; + } +} + +#[test] +fn test_get_value() { + let mut mock_ops = MockSharePoolDataOperations::new(); + mock_ops.set_denominator(10u64.into()); + mock_ops.set_share(&1_u16, 3u64.into()); + mock_ops.set_share(&2_u16, 7u64.into()); + mock_ops.set_shared_value(100u64.into()); + let share_pool = SharePool::new(mock_ops); + let result1 = share_pool.get_value(&1); + let result2 = share_pool.get_value(&2); + assert_eq!(result1, 30); + assert_eq!(result2, 70); +} + +#[test] +fn test_division_by_zero() { + let mut mock_ops = MockSharePoolDataOperations::new(); + mock_ops.set_denominator(SafeFloat::zero()); // Zero denominator + let pool = SharePool::::new(mock_ops); + + let value = pool.get_value(&1); + assert_eq!(value, 0, "Value should be 0 when denominator is zero"); +} + +#[test] +fn test_max_shared_value() { + let mut mock_ops = MockSharePoolDataOperations::new(); + mock_ops.set_shared_value(u64::MAX.into()); + mock_ops.set_share(&1, 3u64.into()); // Use a neutral value for share + mock_ops.set_share(&2, 7u64.into()); // Use a neutral value for share + mock_ops.set_denominator(10u64.into()); // Neutral value to see max effect + let pool = SharePool::::new(mock_ops); + + let max_value = pool.get_value(&1) + pool.get_value(&2); + assert!(u64::MAX - max_value <= 5, "Max value should map to u64 MAX"); +} + +#[test] +fn test_max_share_value() { + let mut mock_ops = MockSharePoolDataOperations::new(); + mock_ops.set_shared_value(1_000_000_000u64); // Use a neutral value for shared value + mock_ops.set_share(&1, (u64::MAX / 2).into()); + mock_ops.set_share(&2, (u64::MAX / 2).into()); + mock_ops.set_denominator((u64::MAX).into()); + let pool = SharePool::::new(mock_ops); + + let value1 = pool.get_value(&1) as i128; + let value2 = pool.get_value(&2) as i128; + + assert_abs_diff_eq!(value1 as f64, 500_000_000_f64, epsilon = 1.); + assert!((value2 - 500_000_000).abs() <= 1); +} + +#[test] +fn test_denom_precision() { + let mock_ops = MockSharePoolDataOperations::new(); + let mut pool = SharePool::::new(mock_ops); + + pool.update_value_for_one(&1, 1000); + + let value_tmp = pool.get_value(&1) as i128; + assert_eq!(value_tmp, 1000); + + pool.update_value_for_one(&1, -990); + pool.update_value_for_one(&2, 1000); + pool.update_value_for_one(&2, -990); + + let value1 = pool.get_value(&1) as i128; + let value2 = pool.get_value(&2) as i128; + + assert_eq!(value1, 10); + assert_eq!(value2, 10); +} + +// cargo test --package share-pool --lib -- tests::test_denom_high_precision --exact --show-output +#[test] +fn test_denom_high_precision() { + let mock_ops = MockSharePoolDataOperations::new(); + let mut pool = SharePool::::new(mock_ops); + + // 50%/50% stakes consisting of 1 rao each + pool.update_value_for_one(&1, 1); + pool.update_value_for_one(&2, 1); + + // Huge emission resulting in 1M Alpha + // Both stakers should have 500k Alpha each + pool.update_value_for_all(999_999_999_999_998); + + // Everyone unstakes almost everything, leaving 10 rao in the stake + pool.update_value_for_one(&1, -499_999_999_999_990); + pool.update_value_for_one(&2, -499_999_999_999_990); + + // Huge emission resulting in 1M Alpha + // Both stakers should have 500k Alpha each + pool.update_value_for_all(999_999_999_999_980); + + // Stakers add 1k Alpha each + pool.update_value_for_one(&1, 1_000_000_000_000); + pool.update_value_for_one(&2, 1_000_000_000_000); + + let value1 = pool.get_value(&1) as f64; + let value2 = pool.get_value(&2) as f64; + assert_abs_diff_eq!(value1, 501_000_000_000_000_f64, epsilon = 1.); + assert_abs_diff_eq!(value2, 501_000_000_000_000_f64, epsilon = 1.); +} + +// cargo test --package share-pool --lib -- tests::test_denom_high_precision_many_small_unstakes --exact --show-output +#[test] +fn test_denom_high_precision_many_small_unstakes() { + let mock_ops = MockSharePoolDataOperations::new(); + let mut pool = SharePool::::new(mock_ops); + + // 50%/50% stakes consisting of 1 rao each + pool.update_value_for_one(&1, 1); + pool.update_value_for_one(&2, 1); + + // Huge emission resulting in 1M Alpha + // Both stakers should have 500k Alpha + 1 rao each + pool.update_value_for_all(1_000_000_000_000_000); + + // Run X number of small unstake transactions + let tx_count = 1000; + let unstake_amount = -500_000_000; + for _ in 0..tx_count { + pool.update_value_for_one(&1, unstake_amount); + pool.update_value_for_one(&2, unstake_amount); + } + + // Emit 1M - each gets 500k Alpha + pool.update_value_for_all(1_000_000_000_000_000); + + // Each adds 1k Alpha + pool.update_value_for_one(&1, 1_000_000_000_000); + pool.update_value_for_one(&2, 1_000_000_000_000); + + // Result, each should get + // (500k+1) + tx_count * unstake_amount + 500k + 1k + let value1 = pool.get_value(&1) as i128; + let value2 = pool.get_value(&2) as i128; + let expected = 1_001_000_000_000_000 + tx_count * unstake_amount; + + assert_abs_diff_eq!(value1 as f64, expected as f64, epsilon = 1.); + assert_abs_diff_eq!(value2 as f64, expected as f64, epsilon = 1.); +} + +#[test] +fn test_update_value_for_one() { + let mock_ops = MockSharePoolDataOperations::new(); + let mut pool = SharePool::::new(mock_ops); + + pool.update_value_for_one(&1, 1000); + + let value = pool.get_value(&1) as i128; + assert_eq!(value, 1000); +} + +#[test] +fn test_update_value_for_all() { + let mock_ops = MockSharePoolDataOperations::new(); + let mut pool = SharePool::::new(mock_ops); + + pool.update_value_for_all(1000); + assert_eq!( + pool.state_ops.shared_value, + U64F64::saturating_from_num(1000) + ); +} + +// cargo test --package share-pool --lib -- tests::test_shares_for_value_update --exact --show-output +#[test] +fn test_shares_for_value_update() { + // Test case (update, shared_value, denominator_mantissa, denominator_exponent) + [ + (1_i64, 1_u64, 1_u64, 0_i64), + (1, 1_000_000_000_000_000_000, 1, 0), + (1, 21_000_000_000_000_000, 1, 5), + (1, 21_000_000_000_000_000, 1, -1_000_000), + (1, 21_000_000_000_000_000, 1, -1_000_000_000), + (1, 21_000_000_000_000_000, 1, -1_000_000_001), + (1_000, 21_000_000_000_000_000, 1, 5), + (21_000_000_000_000_000, 21_000_000_000_000_000, 1, 5), + (21_000_000_000_000_000, 21_000_000_000_000_000, 1, -5), + (21_000_000_000_000_000, 21_000_000_000_000_000, 1, -100), + (21_000_000_000_000_000, 21_000_000_000_000_000, 1, 100), + (210_000_000_000_000_000, 21_000_000_000_000_000, 1, 5), + (1_000, 1_000, 21_000_000_000_000_000, 0), + (1_000, 1_000, 21_000_000_000_000_000, -1), + ] + .into_iter() + .for_each( + |(update, shared_value, denominator_mantissa, denominator_exponent)| { + let mock_ops = MockSharePoolDataOperations::new(); + let pool = SharePool::::new(mock_ops); + + let denominator_float = + SafeFloat::new(denominator_mantissa as u128, denominator_exponent) + .unwrap_or_default(); + let denominator_f64: f64 = denominator_float.clone().into(); + let spu: f64 = pool + .shares_for_value_update(update, shared_value, &denominator_float) + .into(); + let expected = update as f64 * denominator_f64 / shared_value as f64; + let precision = 1000.; + assert_abs_diff_eq!(expected, spu, epsilon = expected / precision); + }, + ); +} + +#[test] +fn test_safefloat_normalize() { + // Test case: mantissa, exponent, expected mantissa, expected exponent + [ + (1_u128, 0, 1_000_000_000_000_000_000_000_u128, -21_i64), + (0, 0, 0, 0), + (10_u128, 0, 1_000_000_000_000_000_000_000_u128, -20), + (1_000_u128, 0, 1_000_000_000_000_000_000_000_u128, -18), + ( + 100_000_000_000_000_000_000_u128, + 0, + 1_000_000_000_000_000_000_000_u128, + -1, + ), + (SAFE_FLOAT_MAX, 0, SAFE_FLOAT_MAX, 0), + ] + .into_iter() + .for_each(|(m, e, expected_m, expected_e)| { + let a = SafeFloat::new(m, e).unwrap(); + assert_eq!(a.mantissa(), expected_m); + assert_eq!(a.exponent(), expected_e); + }); +} + +#[test] +fn test_safefloat_add() { + // Test case: man_a, exp_a, man_b, exp_b, expected mantissa of a+b, expected exponent of a+b + [ + // 1 + 1 = 2 + ( + 1_u128, + 0, + 1_u128, + 0, + 200_000_000_000_000_000_000_u128, + -20_i64, + ), + // 0 + 1 = 1 + (0, 0, 1, 0, 1_000_000_000_000_000_000_000_u128, -21_i64), + // 0 + 0.1 = 0.1 + (0, 0, 1, -1, 1_000_000_000_000_000_000_000_u128, -22_i64), + // 1e-1000 + 0.1 = 0.1 + (1, -1000, 1, -1, 1_000_000_000_000_000_000_000_u128, -22_i64), + // SAFE_FLOAT_MAX + SAFE_FLOAT_MAX + ( + SAFE_FLOAT_MAX, + 0, + SAFE_FLOAT_MAX, + 0, + SAFE_FLOAT_MAX * 2 / 10, + 1_i64, + ), + // Expected loss of precision: tiny + huge + ( + 1_u128, + 0, + 1_000_000_000_000_000_000_000_u128, + 1, + 1_000_000_000_000_000_000_000_u128, + 1_i64, + ), + ( + 1_u128, + 0, + 1_u128, + 22, + 1_000_000_000_000_000_000_000_u128, + 1_i64, + ), + ( + 1_u128, + 0, + 1_u128, + 23, + 1_000_000_000_000_000_000_000_u128, + 2_i64, + ), + ( + 123_u128, + 0, + 1_u128, + 23, + 1_000_000_000_000_000_000_000_u128, + 2_i64, + ), + ( + 123_u128, + 1, + 1_u128, + 23, + 100_000_000_000_000_000_001_u128, + 3_i64, + ), + // Small-ish + very large (10^22 + 42) + // 42 * 10^0 + 1 * 10^22 ≈ 1e22 + 42 + // Normalized ≈ (1e21 + 4) * 10^1 + ( + 42_u128, + 0, + 1_u128, + 22, + 1_000_000_000_000_000_000_000_u128, + 1_i64, + ), + // "Almost 10^21" + 10^22 + // (10^21 - 1) + 10^22 → floor((10^22 + 10^21 - 1) / 100) * 10^2 + ( + 999_999_999_999_999_999_999_u128, + 0, + 1_u128, + 22, + 109_999_999_999_999_999_999_u128, + 2_i64, + ), + // Small-ish + 10^23 where the small part is completely lost + // 42 + 10^23 -> floor((10^23 + 42)/100) * 10^2 ≈ 1e21 * 10^2 + ( + 42_u128, + 0, + 1_u128, + 23, + 1_000_000_000_000_000_000_000_u128, + 2_i64, + ), + // Small-ish + 10^23 where tiny part slightly affects mantissa + // 4200 + 10^23 -> floor((10^23 + 4200)/100) * 10^2 = (1e21 + 42) * 10^2 + ( + 4_200_u128, + 0, + 1_u128, + 23, + 100_000_000_000_000_000_004_u128, + 3_i64, + ), + // (10^21 - 1) + 10^23 + // -> floor((10^23 + 10^21 - 1)/100) = 1e21 + 1e19 - 1 + ( + 999_999_999_999_999_999_999_u128, + 0, + 1_u128, + 23, + 100_999_999_999_999_999_999_u128, + 3_i64, + ), + // Medium + 10^23 with exponent 1 on the smaller term + // 999_999 * 10^1 + 1 * 10^23 -> (10^22 + 999_999) * 10^1 + // Normalized ≈ (1e21 + 99_999) * 10^2 + ( + 999_999_u128, + 1, + 1_u128, + 23, + 100_000_000_000_000_009_999_u128, + 3_i64, + ), + // Check behaviour with exponent 24, tiny second term + // 1 * 10^24 + 1 -> floor((10^24 + 1)/1000) * 10^3 ≈ 1e21 * 10^3 + ( + 1_u128, + 24, + 1_u128, + 0, + 1_000_000_000_000_000_000_000_u128, + 3_i64, + ), + // 1 * 10^24 + a non-trivial small mantissa + // 1e24 + 123456789012345678901 -> floor(/1000) = 1e21 + 123456789012345678 + ( + 1_u128, + 24, + 123_456_789_012_345_678_901_u128, + 0, + 100_012_345_678_901_234_567_u128, + 4_i64, + ), + // 10^22 and 10^23 combined: + // 1 * 10^22 + 1 * 10^23 = 11 * 10^22 = (1.1 * 10^23) + // Normalized → (1.1e20) * 10^3 + ( + 1_u128, + 22, + 1_u128, + 23, + 110_000_000_000_000_000_000_u128, + 3_i64, + ), + // Both operands already aligned at a huge scale: + // (10^21 - 1) * 10^22 + 1 * 10^22 = 10^21 * 10^22 = 10^43 + // Canonical form: (1e21) * 10^22 + ( + 999_999_999_999_999_999_999_u128, + 22, + 1_u128, + 22, + 1_000_000_000_000_000_000_000_u128, + 22_i64, + ), + ] + .into_iter() + .for_each(|(m_a, e_a, m_b, e_b, expected_m, expected_e)| { + let a = SafeFloat::new(m_a, e_a).unwrap(); + let b = SafeFloat::new(m_b, e_b).unwrap(); + + let a_plus_b = a.add(&b).unwrap(); + let b_plus_a = b.add(&a).unwrap(); + + assert_eq!(a_plus_b.mantissa(), expected_m); + assert_eq!(a_plus_b.exponent(), expected_e); + assert_eq!(b_plus_a.mantissa(), expected_m); + assert_eq!(b_plus_a.exponent(), expected_e); + }); +} + +#[test] +fn test_safefloat_div_by_zero_is_none() { + let a = SafeFloat::new(1u128, 0).unwrap(); + assert!(a.div(&SafeFloat::zero()).is_none()); +} + +#[test] +fn test_safefloat_div() { + // Test case: man_a, exp_a, man_b, exp_b + [ + (1_u128, 0_i64, 100_000_000_000_000_000_000_u128, -20_i64), + (1_u128, 0, 1_u128, 0), + (1_u128, 1, 1_u128, 0), + (1_u128, 7, 1_u128, 0), + (1_u128, 50, 1_u128, 0), + (1_u128, 100, 1_u128, 0), + (1_u128, 0, 7_u128, 0), + (1_u128, 1, 7_u128, 0), + (1_u128, 7, 7_u128, 0), + (1_u128, 50, 7_u128, 0), + (1_u128, 100, 7_u128, 0), + (1_u128, 0, 3_u128, 0), + (1_u128, 1, 3_u128, 0), + (1_u128, 7, 3_u128, 0), + (1_u128, 50, 3_u128, 0), + (1_u128, 100, 3_u128, 0), + (2_u128, 0, 3_u128, 0), + (2_u128, 1, 3_u128, 0), + (2_u128, 7, 3_u128, 0), + (2_u128, 50, 3_u128, 0), + (2_u128, 100, 3_u128, 0), + (5_u128, 0, 3_u128, 0), + (5_u128, 1, 3_u128, 0), + (5_u128, 7, 3_u128, 0), + (5_u128, 50, 3_u128, 0), + (5_u128, 100, 3_u128, 0), + (10_u128, 0, 100_000_000_000_000_000_000_u128, -19), + (1_000_u128, 0, 100_000_000_000_000_000_000_u128, -17), + ( + 100_000_000_000_000_000_000_u128, + 0, + 1_000_000_000_000_000_000_000_u128, + -1, + ), + (SAFE_FLOAT_MAX, 0, SAFE_FLOAT_MAX, 0), + (SAFE_FLOAT_MAX, 100, SAFE_FLOAT_MAX, -100), + (SAFE_FLOAT_MAX, 100, SAFE_FLOAT_MAX - 1, -100), + (SAFE_FLOAT_MAX - 1, 100, SAFE_FLOAT_MAX, -100), + (SAFE_FLOAT_MAX - 2, 100, SAFE_FLOAT_MAX, -100), + (SAFE_FLOAT_MAX, 100, SAFE_FLOAT_MAX / 2 - 1, -100), + (SAFE_FLOAT_MAX, 100, SAFE_FLOAT_MAX / 2 - 1, 100), + (1_u128, 0, 100_000_000_000_000_000_000_u128, -20_i64), + ( + 123_456_789_123_456_789_123_u128, + 20_i64, + 87_654_321_987_654_321_987_u128, + -20_i64, + ), + ( + 123_456_789_123_456_789_123_u128, + 100_i64, + 87_654_321_987_654_321_987_u128, + -100_i64, + ), + ( + 123_456_789_123_456_789_123_u128, + -100_i64, + 87_654_321_987_654_321_987_u128, + 100_i64, + ), + ( + 123_456_789_123_456_789_123_u128, + -99_i64, + 87_654_321_987_654_321_987_u128, + 99_i64, + ), + ( + 123_456_789_123_456_789_123_u128, + 123_i64, + 87_654_321_987_654_321_987_u128, + -32_i64, + ), + ( + 123_456_789_123_456_789_123_u128, + -123_i64, + 87_654_321_987_654_321_987_u128, + 32_i64, + ), + ] + .into_iter() + .for_each(|(ma, ea, mb, eb)| { + let a = SafeFloat::new(ma, ea).unwrap(); + let b = SafeFloat::new(mb, eb).unwrap(); + + let actual: f64 = a.div(&b).unwrap().into(); + let expected = + ma as f64 * (10_f64).powi(ea as i32) / (mb as f64 * (10_f64).powi(eb as i32)); + + assert_abs_diff_eq!(actual, expected, epsilon = actual / 100_000_000_000_000_f64); + }); +} + +#[test] +fn test_safefloat_mul_div() { + // result = a * b / c + // should not lose precision gained in a * b + // Test case: man_a, exp_a, man_b, exp_b, man_c, exp_c + [ + (1_u128, -20_i64, 1_u128, -20_i64, 1_u128, -20_i64), + (123_u128, 20_i64, 123_u128, -20_i64, 321_u128, 0_i64), + ( + 123_123_123_123_123_123_u128, + 20_i64, + 321_321_321_321_321_321_u128, + -20_i64, + 777_777_777_777_777_777_u128, + 0_i64, + ), + ( + 11_111_111_111_111_111_111_u128, + 20_i64, + 99_321_321_321_321_321_321_u128, + -20_i64, + 77_777_777_777_777_777_777_u128, + 0_i64, + ), + ] + .into_iter() + .for_each(|(ma, ea, mb, eb, mc, ec)| { + let a = SafeFloat::new(ma, ea).unwrap(); + let b = SafeFloat::new(mb, eb).unwrap(); + let c = SafeFloat::new(mc, ec).unwrap(); + + let actual: f64 = a.mul_div(&b, &c).unwrap().into(); + let expected = (ma as f64 * (10_f64).powi(ea as i32)) + * (mb as f64 * (10_f64).powi(eb as i32)) + / (mc as f64 * (10_f64).powi(ec as i32)); + + assert_abs_diff_eq!(actual, expected, epsilon = actual / 100_000_000_000_000_f64); + }); +} + +#[test] +fn test_safefloat_from_u64f64() { + [ + // U64F64::from_num(1000.0), + // U64F64::from_num(10.0), + // U64F64::from_num(1.0), + U64F64::from_num(0.1), + // U64F64::from_num(0.00000001), + // U64F64::from_num(123_456_789_123_456u128), + // // Exact zero + // U64F64::from_num(0.0), + // // Very small positive value (well above Q64.64 resolution) + // U64F64::from_num(1e-18), + // // Value just below 1 + // U64F64::from_num(0.999_999_999_999_999_f64), + // // Value just above 1 + // U64F64::from_num(1.000_000_000_000_001_f64), + // // "Random-looking" fractional with many digits + // U64F64::from_num(1.234_567_890_123_45_f64), + // // Large integer, but smaller than the max integer part of U64F64 + // U64F64::from_num(999_999_999_999_999_999u128), + // // Very large integer near the upper bound of integer range + // U64F64::from_num(u64::MAX as u128), + // // Large number with fractional part + // U64F64::from_num(123_456_789_123_456.78_f64), + // // Medium-large with tiny fractional part to test precision on tail digits + // U64F64::from_num(1_000_000_000_000.000_001_f64), + // // Smallish with long fractional part + // U64F64::from_num(0.123_456_789_012_345_f64), + ] + .into_iter() + .for_each(|f| { + let safe_float: SafeFloat = f.into(); + let actual: f64 = safe_float.into(); + let expected = f.to_num::(); + + // Relative epsilon ~1e-14 of the magnitude + let epsilon = if actual == 0.0 { + 0.0 + } else { + actual.abs() / 100_000_000_000_000_f64 + }; + + assert_abs_diff_eq!(actual, expected, epsilon = epsilon); + }); +} + +/// This is a real-life scenario test when someone lost 7 TAO on Chutes (SN64) +/// when paying fees in Alpha. The scenario occured because the update of share value +/// of one coldkey (update_value_for_one) hit the scenario of full unstake. +/// +/// Specifically, the following condition was triggered: +/// +/// `(shared_value + 2_628_000_000_000_000_u64).checked_div(new_denominator)` +/// +/// returned None because new_denominator was too low and division of +/// `shared_value + 2_628_000_000_000_000_u64` by new_denominator has overflown U64F64. +/// +/// This test fails on the old version of share pool (with much lower tolerances). +/// +/// cargo test --package share-pool --lib -- tests::test_loss_due_to_precision --exact --nocapture +#[test] +fn test_loss_due_to_precision() { + let mock_ops = MockSharePoolDataOperations::new(); + let mut pool = SharePool::::new(mock_ops); + + // Setup pool so that initial coldkey's alpha is 10% of 1e12 = 1e11 rao. + let low_denominator = SafeFloat::new(1u128, -14).unwrap(); + let low_share = SafeFloat::new(1u128, -15).unwrap(); + pool.state_ops.set_denominator(low_denominator); + pool.state_ops.set_shared_value(1_000_000_000_000_u64); + pool.state_ops.set_share(&1, low_share); + + let value_before = pool.get_value(&1) as i128; + assert_abs_diff_eq!(value_before as f64, 100_000_000_000., epsilon = 0.1); + + // Remove a little stake + let unstake_amount = 1000i64; + pool.update_value_for_one(&1, unstake_amount.neg()); + + let value_after = pool.get_value(&1) as i128; + assert_abs_diff_eq!( + (value_before - value_after) as f64, + unstake_amount as f64, + epsilon = unstake_amount as f64 / 1_000_000_000. + ); +} + +fn rel_err(a: f64, b: f64) -> f64 { + let denom = a.abs().max(b.abs()).max(1.0); + (a - b).abs() / denom +} + +fn push_unique(v: &mut Vec, x: u128) { + if x != 0 && !v.contains(&x) { + v.push(x); + } +} + +// cargo test --package share-pool --lib -- tests::test_safefloat_mul_div_wide_range --exact --include-ignored --show-output +#[test] +#[ignore = "long-running sweep test; run explicitly when needed"] +fn test_safefloat_mul_div_wide_range() { + use rayon::prelude::*; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + + // Build mantissa corpus + let mut mantissas = Vec::::new(); + + let linear_steps: u128 = 200; + let linear_step = (SAFE_FLOAT_MAX / linear_steps).max(1); + let mut m = 1u128; + while m <= SAFE_FLOAT_MAX { + push_unique(&mut mantissas, m); + match m.checked_add(linear_step) { + Some(next) if next > m => m = next, + _ => break, + } + } + push_unique(&mut mantissas, SAFE_FLOAT_MAX); + + let mut p = 1u128; + while p <= SAFE_FLOAT_MAX { + push_unique(&mut mantissas, p); + if p > 1 { + push_unique(&mut mantissas, p - 1); + } + if let Some(next) = p.checked_add(1) + && next <= SAFE_FLOAT_MAX + { + push_unique(&mut mantissas, next); + } + + match p.checked_mul(10) { + Some(next) if next > p && next <= SAFE_FLOAT_MAX => p = next, + _ => break, + } + } + + for delta in [ + 0u128, 1, 2, 3, 7, 9, 10, 11, 99, 100, 101, 999, 1_000, 10_000, + ] { + if SAFE_FLOAT_MAX > delta { + push_unique(&mut mantissas, SAFE_FLOAT_MAX - delta); + } + } + + mantissas.sort_unstable(); + mantissas.dedup(); + + let exp_min: i64 = -120; + let exp_max: i64 = 120; + let exp_step: usize = 5; + let exponents: Vec = (exp_min..=exp_max).step_by(exp_step).collect(); + + // Precompute all (a, b) pairs as outer work items. + // Each Rayon task will then iterate all c's sequentially. + let mut outer_cases: Vec<(u128, i64, u128, i64)> = Vec::new(); + + for &ma in &mantissas { + for &ea in &exponents { + for &mb in &mantissas { + for &eb in &exponents { + outer_cases.push((ma, ea, mb, eb)); + } + } + } + } + + let checked = Arc::new(AtomicUsize::new(0)); + let skipped_non_finite = Arc::new(AtomicUsize::new(0)); + let skipped_invalid_sf = Arc::new(AtomicUsize::new(0)); + + let progress_step = 10_000usize; + let total_outer = outer_cases.len(); + + outer_cases.into_par_iter().for_each(|(ma, ea, mb, eb)| { + let a = match SafeFloat::new(ma, ea) { + Some(x) => x, + None => { + skipped_invalid_sf.fetch_add(1, Ordering::Relaxed); + return; + } + }; + + let b = match SafeFloat::new(mb, eb) { + Some(x) => x, + None => { + skipped_invalid_sf.fetch_add(1, Ordering::Relaxed); + return; + } + }; + + for &mc in &mantissas { + for &ec in &exponents { + let c = match SafeFloat::new(mc, ec) { + Some(x) => x, + None => { + skipped_invalid_sf.fetch_add(1, Ordering::Relaxed); + continue; + } + }; + + let actual_sf = a.mul_div(&b, &c).unwrap(); + let actual: f64 = actual_sf.into(); + + let expected = + (ma as f64 * 10_f64.powi(ea as i32)) + * (mb as f64 * 10_f64.powi(eb as i32)) + / (mc as f64 * 10_f64.powi(ec as i32)); + + if !expected.is_finite() || !actual.is_finite() { + skipped_non_finite.fetch_add(1, Ordering::Relaxed); + continue; + } + + let err = rel_err(actual, expected); + + assert!( + err <= 1e-12, + concat!( + "mul_div mismatch:\n", + " a = {}e{}\n", + " b = {}e{}\n", + " c = {}e{}\n", + " actual = {:.20e}\n", + " expected = {:.20e}\n", + " rel_err = {:.20e}" + ), + ma, ea, mb, eb, mc, ec, actual, expected, err + ); + + checked.fetch_add(1, Ordering::Relaxed); + } + } + + let done_outer = checked.load(Ordering::Relaxed); + if done_outer % progress_step == 0 { + let invalid = skipped_invalid_sf.load(Ordering::Relaxed); + let non_finite = skipped_non_finite.load(Ordering::Relaxed); + log::debug!( + "progress: checked={}, skipped_invalid_sf={}, skipped_non_finite={}, outer_total={}", + done_outer, + invalid, + non_finite, + total_outer, + ); + } + }); + + let checked = checked.load(Ordering::Relaxed); + let skipped_non_finite = skipped_non_finite.load(Ordering::Relaxed); + let skipped_invalid_sf = skipped_invalid_sf.load(Ordering::Relaxed); + + println!( + "checked={}, skipped_non_finite={}, skipped_invalid_sf={}, mantissas={}, exponents={}, outer_cases={}", + checked, + skipped_non_finite, + skipped_invalid_sf, + mantissas.len(), + exponents.len(), + total_outer, + ); + + assert!(checked > 0, "test did not validate any finite cases"); +} + +#[test] +#[ignore = "long-running broad-range test; run explicitly when needed"] +fn test_safefloat_div_wide_range() { + use rayon::prelude::*; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + + fn rel_err(a: f64, b: f64) -> f64 { + let denom = a.abs().max(b.abs()).max(1.0); + (a - b).abs() / denom + } + + fn push_unique(v: &mut Vec, x: u128) { + if x != 0 && !v.contains(&x) { + v.push(x); + } + } + + // Build a broad mantissa corpus: + // - coarse linear sweep + // - powers of 10 and neighbors + // - values near SAFE_FLOAT_MAX + let mut mantissas = Vec::::new(); + + let linear_steps: u128 = 200; + let linear_step = (SAFE_FLOAT_MAX / linear_steps).max(1); + let mut m = 1u128; + while m <= SAFE_FLOAT_MAX { + push_unique(&mut mantissas, m); + match m.checked_add(linear_step) { + Some(next) if next > m => m = next, + _ => break, + } + } + push_unique(&mut mantissas, SAFE_FLOAT_MAX); + + let mut p = 1u128; + while p <= SAFE_FLOAT_MAX { + push_unique(&mut mantissas, p); + if p > 1 { + push_unique(&mut mantissas, p - 1); + } + if let Some(next) = p.checked_add(1) + && next <= SAFE_FLOAT_MAX + { + push_unique(&mut mantissas, next); + } + + match p.checked_mul(10) { + Some(next) if next > p && next <= SAFE_FLOAT_MAX => p = next, + _ => break, + } + } + + for delta in [ + 0u128, 1, 2, 3, 7, 9, 10, 11, 99, 100, 101, 999, 1_000, 10_000, + ] { + if SAFE_FLOAT_MAX > delta { + push_unique(&mut mantissas, SAFE_FLOAT_MAX - delta); + } + } + + mantissas.sort_unstable(); + mantissas.dedup(); + + // Exponent sweep. + // Keep it large enough to stress normalization / exponent math, + // but still practical for f64 reference calculations. + let exp_min: i64 = -120; + let exp_max: i64 = 120; + let exp_step: usize = 5; + let exponents: Vec = (exp_min..=exp_max).step_by(exp_step).collect(); + + let m_len = mantissas.len(); + let e_len = exponents.len(); + let total_cases = m_len * e_len * m_len * e_len; + + let checked = Arc::new(AtomicUsize::new(0)); + let skipped_non_finite = Arc::new(AtomicUsize::new(0)); + let skipped_invalid_sf = Arc::new(AtomicUsize::new(0)); + let done_counter = Arc::new(AtomicUsize::new(0)); + + (0..total_cases).into_par_iter().for_each(|idx| { + let mut rem = idx; + + let eb_idx = rem % e_len; + rem /= e_len; + + let mb_idx = rem % m_len; + rem /= m_len; + + let ea_idx = rem % e_len; + rem /= e_len; + + let ma_idx = rem % m_len; + + let ma = mantissas[ma_idx]; + let ea = exponents[ea_idx]; + let mb = mantissas[mb_idx]; + let eb = exponents[eb_idx]; + + let a = match SafeFloat::new(ma, ea) { + Some(x) => x, + None => { + skipped_invalid_sf.fetch_add(1, Ordering::Relaxed); + done_counter.fetch_add(1, Ordering::Relaxed); + return; + } + }; + + let b = match SafeFloat::new(mb, eb) { + Some(x) => x, + None => { + skipped_invalid_sf.fetch_add(1, Ordering::Relaxed); + done_counter.fetch_add(1, Ordering::Relaxed); + return; + } + }; + + let actual_sf = match a.div(&b) { + Some(x) => x, + None => { + skipped_invalid_sf.fetch_add(1, Ordering::Relaxed); + done_counter.fetch_add(1, Ordering::Relaxed); + return; + } + }; + + let actual: f64 = actual_sf.into(); + let expected = (ma as f64 * 10_f64.powi(ea as i32)) / (mb as f64 * 10_f64.powi(eb as i32)); + + if !actual.is_finite() || !expected.is_finite() { + skipped_non_finite.fetch_add(1, Ordering::Relaxed); + } else { + let err = rel_err(actual, expected); + + assert!( + err <= 1e-12, + concat!( + "div mismatch:\n", + " a = {}e{}\n", + " b = {}e{}\n", + " actual = {:.20e}\n", + " expected = {:.20e}\n", + " rel_err = {:.20e}" + ), + ma, + ea, + mb, + eb, + actual, + expected, + err + ); + + checked.fetch_add(1, Ordering::Relaxed); + } + + let done = done_counter.fetch_add(1, Ordering::Relaxed) + 1; + if done % 10_000 == 0 { + let progress = done as f64 / total_cases as f64 * 100.0; + log::debug!("div progress = {progress:.4}%"); + } + }); + + let checked = checked.load(Ordering::Relaxed); + let skipped_non_finite = skipped_non_finite.load(Ordering::Relaxed); + let skipped_invalid_sf = skipped_invalid_sf.load(Ordering::Relaxed); + + println!( + "div checked={}, skipped_non_finite={}, skipped_invalid_sf={}, mantissas={}, exponents={}, total_cases={}", + checked, + skipped_non_finite, + skipped_invalid_sf, + mantissas.len(), + exponents.len(), + total_cases, + ); + + assert!(checked > 0, "div test did not validate any finite cases"); +} diff --git a/primitives/swap-interface/src/lib.rs b/primitives/swap-interface/src/lib.rs index ed76dcda43..389a9e3292 100644 --- a/primitives/swap-interface/src/lib.rs +++ b/primitives/swap-interface/src/lib.rs @@ -1,3 +1,9 @@ +//! Traits and result types for subnet TAO↔alpha AMM swaps and limit-order execution. +//! +//! - [`SwapEngine`] / [`SwapHandler`]: pool swap and protocol-liquidity operations +//! - [`OrderSwapInterface`]: buy/sell that also move user balances / stake +//! - [`order`]: typed buy (`GetAlphaForTao`) and sell (`GetTaoForAlpha`) orders + #![cfg_attr(not(feature = "std"), no_std)] #![allow(clippy::too_many_arguments)] use core::ops::Neg; @@ -11,7 +17,12 @@ use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; mod order; +/// Low-level AMM engine that executes a typed [`Order`] against a subnet pool. pub trait SwapEngine: DefaultPriceLimit { + /// Execute `order` on `netuid`, respecting `price_limit`. + /// + /// `drop_fees` skips fee charging; `should_rollback` asks the engine to revert + /// pool mutations after computing the result (simulation / quote path). fn swap( netuid: NetUid, order: O, @@ -21,7 +32,9 @@ pub trait SwapEngine: DefaultPriceLimit { ) -> Result, DispatchError>; } +/// Runtime-facing swap API: execute or simulate orders, quote fees/prices, manage protocol liquidity. pub trait SwapHandler { + /// Execute a typed order through the implementing [`SwapEngine`]. fn swap( netuid: NetUid, order: O, @@ -31,6 +44,7 @@ pub trait SwapHandler { ) -> Result, DispatchError> where Self: SwapEngine; + /// Quote an order without committing pool state (`should_rollback`-style simulation). fn sim_swap( netuid: NetUid, order: O, @@ -38,20 +52,31 @@ pub trait SwapHandler { where Self: SwapEngine; + /// Approximate fee charged for swapping `amount` on `netuid` (same token type in/out of fee). fn approx_fee_amount(netuid: NetUid, amount: T) -> T; + /// Spot price: TAO per alpha on `netuid` (fixed-point). fn current_alpha_price(netuid: NetUid) -> U64F64; + /// Upper bound used when a call site wants “no effective max price”. fn max_price() -> C; + /// Lower bound used when a call site wants “no effective min price”. fn min_price() -> C; + /// Apply protocol-owned liquidity deltas; returns the applied `(tao, alpha)` amounts. fn adjust_protocol_liquidity( netuid: NetUid, tao_delta: TaoBalance, alpha_delta: AlphaBalance, ) -> (TaoBalance, AlphaBalance); + /// Protocol-owned alpha sitting outside the AMM reserves for `netuid`. fn protocol_alpha_reservoir(netuid: NetUid) -> AlphaBalance; + /// Protocol-owned TAO sitting outside the AMM reserves for `netuid`. fn protocol_tao_reservoir(netuid: NetUid) -> TaoBalance; + /// Zero both protocol liquidity reservoirs for `netuid`. fn clear_protocol_liquidity_reservoirs(netuid: NetUid); + /// Drain protocol liquidity into the pool (or remove it), metering weight; `false` if unfinished. fn clear_protocol_liquidity(netuid: NetUid, weight_meter: &mut WeightMeter) -> bool; + /// Initialize swap state for a new/activated subnet; optional starting price. fn init_swap(netuid: NetUid, maybe_price: Option); + /// How much alpha `tao_amount` would buy at the current pool state (no execution). fn get_alpha_amount_for_tao(netuid: NetUid, tao_amount: TaoBalance) -> AlphaBalance; } @@ -70,8 +95,6 @@ pub trait OrderSwapInterface { /// coldkey balance, and sets the staking rate-limit flag for `(hotkey, /// coldkey, netuid)` after a successful stake. Pass `false` for internal /// pallet-intermediary swaps that must bypass these user-facing guards. - /// Buy alpha with TAO: debit `tao_amount` from `coldkey`'s free balance, - /// credit resulting alpha as stake at `hotkey` on `netuid`. /// /// **Implementations MUST be transactional** (wrap in /// `frame_support::storage::with_transaction` or annotate with @@ -95,8 +118,6 @@ pub trait OrderSwapInterface { /// balance, and checks that the staking rate-limit flag is not set for /// `(hotkey, coldkey, netuid)` (i.e. the account did not stake this /// block). Pass `false` for internal pallet-intermediary swaps. - /// Sell alpha for TAO: remove `alpha_amount` from `coldkey`'s stake at - /// `hotkey` on `netuid`, credit resulting TAO to `coldkey`'s free balance. /// /// **Implementations MUST be transactional** (wrap in /// `frame_support::storage::with_transaction` or annotate with @@ -189,15 +210,17 @@ pub trait OrderSwapInterface { fn set_up_acc_for_benchmark(_hotkey: &AccountId, _coldkey: &AccountId) {} } +/// Provides a default limit price for an order's paid-in / paid-out token pair. pub trait DefaultPriceLimit where PaidIn: Token, PaidOut: Token, { + /// Default price limit in units of `C` (typically “no binding limit”). fn default_price_limit() -> C; } -/// Externally used swap result (for RPC) +/// Swap fill sizes and fees returned to RPC / extrinsic callers (`PaidIn` / `PaidOut` tokens). #[freeze_struct("6a03533fc53ccfb8")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] pub struct SwapResult @@ -216,19 +239,23 @@ where PaidIn: Token, PaidOut: Token, { + /// Signed reserve delta for the paid-in side (positive = reserve increases). pub fn paid_in_reserve_delta(&self) -> i128 { self.amount_paid_in.to_u64() as i128 } + /// [`Self::paid_in_reserve_delta`] clamped to `i64`. pub fn paid_in_reserve_delta_i64(&self) -> i64 { self.paid_in_reserve_delta() .clamp(i64::MIN as i128, i64::MAX as i128) as i64 } + /// Signed reserve delta for the paid-out side (negative = reserve decreases). pub fn paid_out_reserve_delta(&self) -> i128 { (self.amount_paid_out.to_u64() as i128).neg() } + /// [`Self::paid_out_reserve_delta`] clamped to `i64`. pub fn paid_out_reserve_delta_i64(&self) -> i64 { (self.amount_paid_out.to_u64() as i128) .neg() diff --git a/primitives/swap-interface/src/order.rs b/primitives/swap-interface/src/order.rs index 7b9970f123..3faea090a7 100644 --- a/primitives/swap-interface/src/order.rs +++ b/primitives/swap-interface/src/order.rs @@ -1,19 +1,26 @@ +//! Typed AMM orders: pay TAO for alpha ([`GetAlphaForTao`]) or alpha for TAO ([`GetTaoForAlpha`]). + use core::marker::PhantomData; use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaBalance, TaoBalance, Token, TokenReserve}; +/// Directional swap order: fixed paid-in amount against a subnet's TAO/alpha reserves. pub trait Order: Clone { type PaidIn: Token; type PaidOut: Token; type ReserveIn: TokenReserve; type ReserveOut: TokenReserve; + /// Build an order that pays exactly `amount` of [`Self::PaidIn`]. fn with_amount(amount: impl Into) -> Self; + /// Paid-in size of this order. fn amount(&self) -> Self::PaidIn; + /// True when the current spot price has moved past `limit_price` for this direction. fn is_beyond_price_limit(&self, current_price: U64F64, limit_price: U64F64) -> bool; } +/// Buy order: pay TAO into the pool, receive alpha out. #[derive(Clone, Default)] pub struct GetAlphaForTao where @@ -46,10 +53,12 @@ where } fn is_beyond_price_limit(&self, current_price: U64F64, limit_price: U64F64) -> bool { + // Buying alpha: reject when spot is already below the caller's minimum TAO/alpha. current_price < limit_price } } +/// Sell order: pay alpha into the pool, receive TAO out. #[derive(Clone, Default)] pub struct GetTaoForAlpha where @@ -82,6 +91,7 @@ where } fn is_beyond_price_limit(&self, current_price: U64F64, limit_price: U64F64) -> bool { + // Selling alpha: reject when spot is already above the caller's maximum TAO/alpha. current_price > limit_price } } diff --git a/refactor/FREEZE_STRUCT.md b/refactor/FREEZE_STRUCT.md new file mode 100644 index 0000000000..8ca2a5b7dd --- /dev/null +++ b/refactor/FREEZE_STRUCT.md @@ -0,0 +1,15 @@ +# freeze_struct × discoverability docs + +Verified against [`support/macros/src/visitor.rs`](../support/macros/src/visitor.rs): + +- Before hashing, `CleanDocComments` rewrites every `#[doc = "…"]` / `///` to `#[doc = ""]`. +- The **attribute presence** remains. Therefore: + +| Edit | Hash update needed? | +|------|---------------------| +| Change text of an existing doc comment | No | +| Add a doc comment where none existed | **Yes** | +| Remove a doc comment | **Yes** | +| Change fields/types/order | **Yes** (+ migration if storage) | + +**Migration rule:** agents may update a `#[freeze_struct("…")]` hash **only** when `git diff` on that struct (ignoring doc attribute text) is empty — i.e. the change is doc presence/absence only, or the hash update is paired with a deliberate layout change that already has a migration plan (out of scope for this migration). diff --git a/refactor/WAVE1_GATE.md b/refactor/WAVE1_GATE.md new file mode 100644 index 0000000000..f19d2cbb38 --- /dev/null +++ b/refactor/WAVE1_GATE.md @@ -0,0 +1,6 @@ +# Wave 1 gate + +- metadata fingerprint: OK (`sha256:7cc9b85a0a732d1976b5558cea02ea0c8ad696bf333099dd2dbfc8ad61e18f35`) +- zepter `propagate-feature`: OK +- per-shard lib tests: passed in each shard worktree before merge (see shard commit messages) +- full workspace nextest / try-runtime: deferred (disk pressure from parallel worktrees; re-run before PR to main) diff --git a/refactor/WAVE2_GATE.md b/refactor/WAVE2_GATE.md new file mode 100644 index 0000000000..995d64589d --- /dev/null +++ b/refactor/WAVE2_GATE.md @@ -0,0 +1,8 @@ +# Wave 2 gate + +- metadata fingerprint: OK (`sha256:7cc9b85a0a732d1976b5558cea02ea0c8ad696bf333099dd2dbfc8ad61e18f35`) +- giant test splits landed: weights, staking, migration, locks, children, coinbase, epoch, networks, swap_hotkey_with_subnet, math +- docs-only frozen surfaces: storage, dispatches, events, errors, migrations +- source areas: coinbase, epoch, staking, subnets, swap (identity), rpc_info, utils, guards, extensions, benchmarks, rpc +- per-shard tests: passed in shard worktrees before merge +- full workspace nextest / try-runtime: deferred until disk allows; re-run before PR to main diff --git a/refactor/WAVE3_GATE.md b/refactor/WAVE3_GATE.md new file mode 100644 index 0000000000..4d1d9f39d4 --- /dev/null +++ b/refactor/WAVE3_GATE.md @@ -0,0 +1,7 @@ +# Wave 3 gate + +- Applied 30 safe cross-shard renames from `rename-proposals.md` +- Deferred: `uid_lookup` (precompile collision), bare `fixed` helper, `extensions/subtensor.rs` module rename (SDK fixtures) +- Precompile fingerprint made path-agnostic; baseline refreshed (`sha256:ee98fab0…`) +- `cargo check -p pallet-subtensor --lib` and `cargo check -p node-subtensor-runtime --lib` OK +- Full workspace nextest / try-runtime / clone-upgrade: run in CI on the PR diff --git a/refactor/metadata-baseline.txt b/refactor/metadata-baseline.txt new file mode 100644 index 0000000000..2be780aca9 --- /dev/null +++ b/refactor/metadata-baseline.txt @@ -0,0 +1,1259 @@ +sha256:ee98fab009eb2df9483086ea63196d0d426521c1859673d75b8b4b004b481d7f + +# subtensor frozen-surface fingerprint (docs stripped) +# tiers: storage, call, event, error, runtime, precompile, rpc, runtime_api +storage pallets/admin-utils/src/lib.rs PrecompileEnable +storage pallets/crowdloan/src/lib.rs Contributions +storage pallets/crowdloan/src/lib.rs Crowdloans +storage pallets/crowdloan/src/lib.rs CurrentCrowdloanId +storage pallets/crowdloan/src/lib.rs HasMigrationRun +storage pallets/crowdloan/src/lib.rs MaxContributions +storage pallets/crowdloan/src/lib.rs NextCrowdloanId +storage pallets/drand/src/lib.rs BeaconConfig +storage pallets/drand/src/lib.rs HasMigrationRun +storage pallets/drand/src/lib.rs LastStoredRound +storage pallets/drand/src/lib.rs NextUnsignedAt +storage pallets/drand/src/lib.rs OldestStoredRound +storage pallets/drand/src/lib.rs Pulses +storage pallets/limit-orders/src/lib.rs HasMigrationRun +storage pallets/limit-orders/src/lib.rs LimitOrdersEnabled +storage pallets/limit-orders/src/lib.rs Orders +storage pallets/proxy/src/lib.rs Announcements +storage pallets/proxy/src/lib.rs LastCallResult +storage pallets/proxy/src/lib.rs Proxies +storage pallets/proxy/src/lib.rs RealPaysFee +storage pallets/shield/src/lib.rs AuthorKeys +storage pallets/shield/src/lib.rs CurrentKey +storage pallets/shield/src/lib.rs ExtrinsicLifetime +storage pallets/shield/src/lib.rs HasMigrationRun +storage pallets/shield/src/lib.rs MaxExtrinsicWeight +storage pallets/shield/src/lib.rs MaxPendingExtrinsicsLimit +storage pallets/shield/src/lib.rs NextKey +storage pallets/shield/src/lib.rs NextKeyExpiresAt +storage pallets/shield/src/lib.rs NextPendingExtrinsicIndex +storage pallets/shield/src/lib.rs OnInitializeWeight +storage pallets/shield/src/lib.rs PendingExtrinsics +storage pallets/shield/src/lib.rs PendingKey +storage pallets/shield/src/lib.rs PendingKeyExpiresAt +storage pallets/subtensor/src/lib.rs AccountFlags +storage pallets/subtensor/src/lib.rs AccumulatedLeaseDividends +storage pallets/subtensor/src/lib.rs Active +storage pallets/subtensor/src/lib.rs ActivityCutoff +storage pallets/subtensor/src/lib.rs ActivityCutoffFactorMilli +storage pallets/subtensor/src/lib.rs AdjustmentAlpha +storage pallets/subtensor/src/lib.rs AdjustmentInterval +storage pallets/subtensor/src/lib.rs AdminFreezeWindow +storage pallets/subtensor/src/lib.rs Alpha +storage pallets/subtensor/src/lib.rs AlphaDividendsPerSubnet +storage pallets/subtensor/src/lib.rs AlphaMapLastKey +storage pallets/subtensor/src/lib.rs AlphaSigmoidSteepness +storage pallets/subtensor/src/lib.rs AlphaV2 +storage pallets/subtensor/src/lib.rs AlphaV2MapLastKey +storage pallets/subtensor/src/lib.rs AlphaValues +storage pallets/subtensor/src/lib.rs AssociatedEvmAddress +storage pallets/subtensor/src/lib.rs AssociatedUidsByEvmAddress +storage pallets/subtensor/src/lib.rs AutoParentDelegationEnabled +storage pallets/subtensor/src/lib.rs AutoStakeDestination +storage pallets/subtensor/src/lib.rs AutoStakeDestinationColdkeys +storage pallets/subtensor/src/lib.rs Axons +storage pallets/subtensor/src/lib.rs BlockAtRegistration +storage pallets/subtensor/src/lib.rs BlockEmission +storage pallets/subtensor/src/lib.rs BlocksSinceLastStep +storage pallets/subtensor/src/lib.rs Bonds +storage pallets/subtensor/src/lib.rs BondsMovingAverage +storage pallets/subtensor/src/lib.rs BondsPenalty +storage pallets/subtensor/src/lib.rs BondsResetOn +storage pallets/subtensor/src/lib.rs Burn +storage pallets/subtensor/src/lib.rs BurnHalfLife +storage pallets/subtensor/src/lib.rs BurnIncreaseMult +storage pallets/subtensor/src/lib.rs BurnRegistrationsThisInterval +storage pallets/subtensor/src/lib.rs CKBurn +storage pallets/subtensor/src/lib.rs CRV3WeightCommits +storage pallets/subtensor/src/lib.rs CRV3WeightCommitsV2 +storage pallets/subtensor/src/lib.rs ChildKeys +storage pallets/subtensor/src/lib.rs ChildkeyTake +storage pallets/subtensor/src/lib.rs ColdkeyCollateralHotkeys +storage pallets/subtensor/src/lib.rs ColdkeyMinerCollateral +storage pallets/subtensor/src/lib.rs ColdkeyRoot +storage pallets/subtensor/src/lib.rs ColdkeySuccessor +storage pallets/subtensor/src/lib.rs ColdkeySwapAnnouncementDelay +storage pallets/subtensor/src/lib.rs ColdkeySwapAnnouncements +storage pallets/subtensor/src/lib.rs ColdkeySwapDisputes +storage pallets/subtensor/src/lib.rs ColdkeySwapReannouncementDelay +storage pallets/subtensor/src/lib.rs CollateralDrainRatio +storage pallets/subtensor/src/lib.rs CollateralLockShare +storage pallets/subtensor/src/lib.rs CommitRevealWeightsEnabled +storage pallets/subtensor/src/lib.rs CommitRevealWeightsVersion +storage pallets/subtensor/src/lib.rs Consensus +storage pallets/subtensor/src/lib.rs CurrentDissolveCleanupStatus +storage pallets/subtensor/src/lib.rs DecayingHotkeyLock +storage pallets/subtensor/src/lib.rs DecayingLock +storage pallets/subtensor/src/lib.rs DecayingOwnerLock +storage pallets/subtensor/src/lib.rs Delegates +storage pallets/subtensor/src/lib.rs Difficulty +storage pallets/subtensor/src/lib.rs DissolveCleanupQueue +storage pallets/subtensor/src/lib.rs DissolveNetworkScheduleDuration +storage pallets/subtensor/src/lib.rs Dividends +storage pallets/subtensor/src/lib.rs EMAPriceHalvingBlocks +storage pallets/subtensor/src/lib.rs Emission +storage pallets/subtensor/src/lib.rs FirstEmissionBlockNumber +storage pallets/subtensor/src/lib.rs FlowEmaSmoothingFactor +storage pallets/subtensor/src/lib.rs FlowNormExponent +storage pallets/subtensor/src/lib.rs HasMigrationRun +storage pallets/subtensor/src/lib.rs HotkeyLock +storage pallets/subtensor/src/lib.rs HotkeyRoot +storage pallets/subtensor/src/lib.rs HotkeySuccessor +storage pallets/subtensor/src/lib.rs IdentitiesV2 +storage pallets/subtensor/src/lib.rs ImmuneOwnerUidsLimit +storage pallets/subtensor/src/lib.rs ImmunityPeriod +storage pallets/subtensor/src/lib.rs Incentive +storage pallets/subtensor/src/lib.rs IsNetworkMember +storage pallets/subtensor/src/lib.rs Kappa +storage pallets/subtensor/src/lib.rs Keys +storage pallets/subtensor/src/lib.rs LargestLocked +storage pallets/subtensor/src/lib.rs LastAdjustmentBlock +storage pallets/subtensor/src/lib.rs LastColdkeyHotkeyStakeBlock +storage pallets/subtensor/src/lib.rs LastEpochBlock +storage pallets/subtensor/src/lib.rs LastHotkeyEmissionOnNetuid +storage pallets/subtensor/src/lib.rs LastHotkeySwapOnNetuid +storage pallets/subtensor/src/lib.rs LastMechansimStepBlock +storage pallets/subtensor/src/lib.rs LastRateLimitedBlock +storage pallets/subtensor/src/lib.rs LastTxBlock +storage pallets/subtensor/src/lib.rs LastTxBlockChildKeyTake +storage pallets/subtensor/src/lib.rs LastTxBlockDelegateTake +storage pallets/subtensor/src/lib.rs LastUpdate +storage pallets/subtensor/src/lib.rs LiquidAlphaOn +storage pallets/subtensor/src/lib.rs LoadedEmission +storage pallets/subtensor/src/lib.rs Lock +storage pallets/subtensor/src/lib.rs LockingColdkeys +storage pallets/subtensor/src/lib.rs MaturityRate +storage pallets/subtensor/src/lib.rs MaxAllowedUids +storage pallets/subtensor/src/lib.rs MaxAllowedValidators +storage pallets/subtensor/src/lib.rs MaxBurn +storage pallets/subtensor/src/lib.rs MaxChildkeyTake +storage pallets/subtensor/src/lib.rs MaxDelegateTake +storage pallets/subtensor/src/lib.rs MaxDifficulty +storage pallets/subtensor/src/lib.rs MaxEpochsPerBlock +storage pallets/subtensor/src/lib.rs MaxMechanismCount +storage pallets/subtensor/src/lib.rs MaxRegistrationsPerBlock +storage pallets/subtensor/src/lib.rs MaxWeightsLimit +storage pallets/subtensor/src/lib.rs MechanismCountCurrent +storage pallets/subtensor/src/lib.rs MechanismEmissionSplit +storage pallets/subtensor/src/lib.rs MinActivityCutoff +storage pallets/subtensor/src/lib.rs MinAllowedUids +storage pallets/subtensor/src/lib.rs MinAllowedWeights +storage pallets/subtensor/src/lib.rs MinBurn +storage pallets/subtensor/src/lib.rs MinChildkeyTake +storage pallets/subtensor/src/lib.rs MinChildkeyTakePerSubnet +storage pallets/subtensor/src/lib.rs MinDelegateTake +storage pallets/subtensor/src/lib.rs MinDifficulty +storage pallets/subtensor/src/lib.rs MinNonImmuneUids +storage pallets/subtensor/src/lib.rs MinerBurned +storage pallets/subtensor/src/lib.rs MinerCollateral +storage pallets/subtensor/src/lib.rs NetTaoFlowEnabled +storage pallets/subtensor/src/lib.rs NetworkImmunityPeriod +storage pallets/subtensor/src/lib.rs NetworkLastLockCost +storage pallets/subtensor/src/lib.rs NetworkLockReductionInterval +storage pallets/subtensor/src/lib.rs NetworkMinLockCost +storage pallets/subtensor/src/lib.rs NetworkPowRegistrationAllowed +storage pallets/subtensor/src/lib.rs NetworkRateLimit +storage pallets/subtensor/src/lib.rs NetworkRegisteredAt +storage pallets/subtensor/src/lib.rs NetworkRegistrationAllowed +storage pallets/subtensor/src/lib.rs NetworkRegistrationLockId +storage pallets/subtensor/src/lib.rs NetworkRegistrationQueue +storage pallets/subtensor/src/lib.rs NetworkRegistrationStartBlock +storage pallets/subtensor/src/lib.rs NetworksAdded +storage pallets/subtensor/src/lib.rs NeuronCertificates +storage pallets/subtensor/src/lib.rs NextStakeJobId +storage pallets/subtensor/src/lib.rs NextSubnetLeaseId +storage pallets/subtensor/src/lib.rs NominatorMinRequiredStake +storage pallets/subtensor/src/lib.rs NumRootClaim +storage pallets/subtensor/src/lib.rs NumStakingColdkeys +storage pallets/subtensor/src/lib.rs OwnedHotkeys +storage pallets/subtensor/src/lib.rs Owner +storage pallets/subtensor/src/lib.rs OwnerCutAutoLockEnabled +storage pallets/subtensor/src/lib.rs OwnerCutEnabled +storage pallets/subtensor/src/lib.rs OwnerHyperparamRateLimit +storage pallets/subtensor/src/lib.rs OwnerLock +storage pallets/subtensor/src/lib.rs POWRegistrationsThisInterval +storage pallets/subtensor/src/lib.rs ParentKeys +storage pallets/subtensor/src/lib.rs PendingChildKeyCooldown +storage pallets/subtensor/src/lib.rs PendingChildKeys +storage pallets/subtensor/src/lib.rs PendingEpochAt +storage pallets/subtensor/src/lib.rs PendingOwnerCut +storage pallets/subtensor/src/lib.rs PendingRootAlphaDivs +storage pallets/subtensor/src/lib.rs PendingServerEmission +storage pallets/subtensor/src/lib.rs PendingValidatorEmission +storage pallets/subtensor/src/lib.rs Prometheus +storage pallets/subtensor/src/lib.rs RAORecycledForRegistration +storage pallets/subtensor/src/lib.rs RecycleOrBurn +storage pallets/subtensor/src/lib.rs RegisteredSubnetCounter +storage pallets/subtensor/src/lib.rs RegistrationsThisBlock +storage pallets/subtensor/src/lib.rs RegistrationsThisInterval +storage pallets/subtensor/src/lib.rs RevealPeriodEpochs +storage pallets/subtensor/src/lib.rs Rho +storage pallets/subtensor/src/lib.rs RootAlphaDividendsPerSubnet +storage pallets/subtensor/src/lib.rs RootClaimType +storage pallets/subtensor/src/lib.rs RootClaimable +storage pallets/subtensor/src/lib.rs RootClaimableThreshold +storage pallets/subtensor/src/lib.rs RootClaimed +storage pallets/subtensor/src/lib.rs RootProp +storage pallets/subtensor/src/lib.rs ScalingLawPower +storage pallets/subtensor/src/lib.rs ServingRateLimit +storage pallets/subtensor/src/lib.rs StakeThreshold +storage pallets/subtensor/src/lib.rs StakeWeight +storage pallets/subtensor/src/lib.rs StakingColdkeys +storage pallets/subtensor/src/lib.rs StakingColdkeysByIndex +storage pallets/subtensor/src/lib.rs StakingHotkeys +storage pallets/subtensor/src/lib.rs StartCallDelay +storage pallets/subtensor/src/lib.rs SubnetAlphaIn +storage pallets/subtensor/src/lib.rs SubnetAlphaInEmission +storage pallets/subtensor/src/lib.rs SubnetAlphaOut +storage pallets/subtensor/src/lib.rs SubnetAlphaOutEmission +storage pallets/subtensor/src/lib.rs SubnetEmaProtocolFlow +storage pallets/subtensor/src/lib.rs SubnetEmaTaoFlow +storage pallets/subtensor/src/lib.rs SubnetEmissionEnabled +storage pallets/subtensor/src/lib.rs SubnetEpochIndex +storage pallets/subtensor/src/lib.rs SubnetExcessTao +storage pallets/subtensor/src/lib.rs SubnetIdentitiesV3 +storage pallets/subtensor/src/lib.rs SubnetLeaseShares +storage pallets/subtensor/src/lib.rs SubnetLeases +storage pallets/subtensor/src/lib.rs SubnetLimit +storage pallets/subtensor/src/lib.rs SubnetLocked +storage pallets/subtensor/src/lib.rs SubnetMechanism +storage pallets/subtensor/src/lib.rs SubnetMovingAlpha +storage pallets/subtensor/src/lib.rs SubnetMovingPrice +storage pallets/subtensor/src/lib.rs SubnetOwner +storage pallets/subtensor/src/lib.rs SubnetOwnerCut +storage pallets/subtensor/src/lib.rs SubnetOwnerHotkey +storage pallets/subtensor/src/lib.rs SubnetProtocolAlpha +storage pallets/subtensor/src/lib.rs SubnetProtocolFlow +storage pallets/subtensor/src/lib.rs SubnetRootSellTao +storage pallets/subtensor/src/lib.rs SubnetTAO +storage pallets/subtensor/src/lib.rs SubnetTaoFlow +storage pallets/subtensor/src/lib.rs SubnetTaoInEmission +storage pallets/subtensor/src/lib.rs SubnetUidToLeaseId +storage pallets/subtensor/src/lib.rs SubnetVolume +storage pallets/subtensor/src/lib.rs SubnetworkN +storage pallets/subtensor/src/lib.rs SubtokenEnabled +storage pallets/subtensor/src/lib.rs TaoFlowCutoff +storage pallets/subtensor/src/lib.rs TaoInRefundDeploymentBlock +storage pallets/subtensor/src/lib.rs TaoWeight +storage pallets/subtensor/src/lib.rs TargetRegistrationsPerInterval +storage pallets/subtensor/src/lib.rs Tempo +storage pallets/subtensor/src/lib.rs TimelockedWeightCommits +storage pallets/subtensor/src/lib.rs TokenSymbol +storage pallets/subtensor/src/lib.rs TotalHotkeyAlpha +storage pallets/subtensor/src/lib.rs TotalHotkeyAlphaLastEpoch +storage pallets/subtensor/src/lib.rs TotalHotkeyShares +storage pallets/subtensor/src/lib.rs TotalHotkeySharesV2 +storage pallets/subtensor/src/lib.rs TotalIssuance +storage pallets/subtensor/src/lib.rs TotalNetworks +storage pallets/subtensor/src/lib.rs TotalStake +storage pallets/subtensor/src/lib.rs TransactionKeyLastBlock +storage pallets/subtensor/src/lib.rs TransferToggle +storage pallets/subtensor/src/lib.rs TxChildkeyTakeRateLimit +storage pallets/subtensor/src/lib.rs TxDelegateTakeRateLimit +storage pallets/subtensor/src/lib.rs TxRateLimit +storage pallets/subtensor/src/lib.rs Uids +storage pallets/subtensor/src/lib.rs UnlockRate +storage pallets/subtensor/src/lib.rs UsedWork +storage pallets/subtensor/src/lib.rs ValidatorPermit +storage pallets/subtensor/src/lib.rs ValidatorPruneLen +storage pallets/subtensor/src/lib.rs ValidatorTrust +storage pallets/subtensor/src/lib.rs VotingPower +storage pallets/subtensor/src/lib.rs VotingPowerDisableAtBlock +storage pallets/subtensor/src/lib.rs VotingPowerEmaAlpha +storage pallets/subtensor/src/lib.rs VotingPowerTrackingEnabled +storage pallets/subtensor/src/lib.rs WeightCommits +storage pallets/subtensor/src/lib.rs Weights +storage pallets/subtensor/src/lib.rs WeightsSetRateLimit +storage pallets/subtensor/src/lib.rs WeightsVersionKey +storage pallets/subtensor/src/lib.rs WeightsVersionKeyRateLimit +storage pallets/subtensor/src/lib.rs Yuma3On +storage pallets/swap/src/pallet/mod.rs BalancerAlphaReservoir +storage pallets/swap/src/pallet/mod.rs BalancerTaoReservoir +storage pallets/swap/src/pallet/mod.rs FeeRate +storage pallets/swap/src/pallet/mod.rs HasMigrationRun +storage pallets/swap/src/pallet/mod.rs PalSwapInitialized +storage pallets/swap/src/pallet/mod.rs ScrapReservoirAlpha +storage pallets/swap/src/pallet/mod.rs SwapBalancer +call pallets/admin-utils/src/lib.rs 0 swap_authorities +call pallets/admin-utils/src/lib.rs 1 sudo_set_default_take +call pallets/admin-utils/src/lib.rs 13 sudo_set_immunity_period +call pallets/admin-utils/src/lib.rs 14 sudo_set_min_allowed_weights +call pallets/admin-utils/src/lib.rs 15 sudo_set_max_allowed_uids +call pallets/admin-utils/src/lib.rs 16 sudo_set_kappa +call pallets/admin-utils/src/lib.rs 17 sudo_set_rho +call pallets/admin-utils/src/lib.rs 18 sudo_set_activity_cutoff +call pallets/admin-utils/src/lib.rs 19 sudo_set_network_registration_allowed +call pallets/admin-utils/src/lib.rs 2 sudo_set_tx_rate_limit +call pallets/admin-utils/src/lib.rs 20 sudo_set_network_pow_registration_allowed +call pallets/admin-utils/src/lib.rs 21 sudo_set_target_registrations_per_interval +call pallets/admin-utils/src/lib.rs 22 sudo_set_min_burn +call pallets/admin-utils/src/lib.rs 23 sudo_set_max_burn +call pallets/admin-utils/src/lib.rs 24 sudo_set_difficulty +call pallets/admin-utils/src/lib.rs 25 sudo_set_max_allowed_validators +call pallets/admin-utils/src/lib.rs 26 sudo_set_bonds_moving_average +call pallets/admin-utils/src/lib.rs 27 sudo_set_max_registrations_per_block +call pallets/admin-utils/src/lib.rs 28 sudo_set_subnet_owner_cut +call pallets/admin-utils/src/lib.rs 29 sudo_set_network_rate_limit +call pallets/admin-utils/src/lib.rs 3 sudo_set_serving_rate_limit +call pallets/admin-utils/src/lib.rs 30 sudo_set_tempo +call pallets/admin-utils/src/lib.rs 33 sudo_set_total_issuance +call pallets/admin-utils/src/lib.rs 35 sudo_set_network_immunity_period +call pallets/admin-utils/src/lib.rs 36 sudo_set_network_min_lock_cost +call pallets/admin-utils/src/lib.rs 37 sudo_set_subnet_limit +call pallets/admin-utils/src/lib.rs 38 sudo_set_lock_reduction_interval +call pallets/admin-utils/src/lib.rs 39 sudo_set_rao_recycled +call pallets/admin-utils/src/lib.rs 4 sudo_set_min_difficulty +call pallets/admin-utils/src/lib.rs 42 sudo_set_stake_threshold +call pallets/admin-utils/src/lib.rs 43 sudo_set_nominator_min_required_stake +call pallets/admin-utils/src/lib.rs 45 sudo_set_tx_delegate_take_rate_limit +call pallets/admin-utils/src/lib.rs 46 sudo_set_min_delegate_take +call pallets/admin-utils/src/lib.rs 49 sudo_set_commit_reveal_weights_enabled +call pallets/admin-utils/src/lib.rs 5 sudo_set_max_difficulty +call pallets/admin-utils/src/lib.rs 50 sudo_set_liquid_alpha_enabled +call pallets/admin-utils/src/lib.rs 51 sudo_set_alpha_values +call pallets/admin-utils/src/lib.rs 55 sudo_set_dissolve_network_schedule_duration +call pallets/admin-utils/src/lib.rs 57 sudo_set_commit_reveal_weights_interval +call pallets/admin-utils/src/lib.rs 58 sudo_set_evm_chain_id +call pallets/admin-utils/src/lib.rs 59 schedule_grandpa_change +call pallets/admin-utils/src/lib.rs 6 sudo_set_weights_version_key +call pallets/admin-utils/src/lib.rs 60 sudo_set_bonds_penalty +call pallets/admin-utils/src/lib.rs 61 sudo_set_toggle_transfer +call pallets/admin-utils/src/lib.rs 62 sudo_toggle_evm_precompile +call pallets/admin-utils/src/lib.rs 63 sudo_set_subnet_moving_alpha +call pallets/admin-utils/src/lib.rs 65 sudo_set_ema_price_halving_period +call pallets/admin-utils/src/lib.rs 66 sudo_set_subtoken_enabled +call pallets/admin-utils/src/lib.rs 67 sudo_set_sn_owner_hotkey +call pallets/admin-utils/src/lib.rs 68 sudo_set_alpha_sigmoid_steepness +call pallets/admin-utils/src/lib.rs 69 sudo_set_yuma3_enabled +call pallets/admin-utils/src/lib.rs 7 sudo_set_weights_set_rate_limit +call pallets/admin-utils/src/lib.rs 70 sudo_set_bonds_reset_enabled +call pallets/admin-utils/src/lib.rs 71 sudo_set_commit_reveal_version +call pallets/admin-utils/src/lib.rs 72 sudo_set_owner_immune_neuron_limit +call pallets/admin-utils/src/lib.rs 73 sudo_set_ck_burn +call pallets/admin-utils/src/lib.rs 74 sudo_set_admin_freeze_window +call pallets/admin-utils/src/lib.rs 75 sudo_set_owner_hparam_rate_limit +call pallets/admin-utils/src/lib.rs 76 sudo_set_mechanism_count +call pallets/admin-utils/src/lib.rs 77 sudo_set_mechanism_emission_split +call pallets/admin-utils/src/lib.rs 78 sudo_trim_to_max_allowed_uids +call pallets/admin-utils/src/lib.rs 79 sudo_set_min_allowed_uids +call pallets/admin-utils/src/lib.rs 8 sudo_set_adjustment_interval +call pallets/admin-utils/src/lib.rs 80 sudo_set_recycle_or_burn +call pallets/admin-utils/src/lib.rs 81 sudo_set_tao_flow_cutoff +call pallets/admin-utils/src/lib.rs 82 sudo_set_tao_flow_normalization_exponent +call pallets/admin-utils/src/lib.rs 83 sudo_set_tao_flow_smoothing_factor +call pallets/admin-utils/src/lib.rs 84 sudo_set_min_non_immune_uids +call pallets/admin-utils/src/lib.rs 85 sudo_set_start_call_delay +call pallets/admin-utils/src/lib.rs 86 sudo_set_coldkey_swap_announcement_delay +call pallets/admin-utils/src/lib.rs 87 sudo_set_coldkey_swap_reannouncement_delay +call pallets/admin-utils/src/lib.rs 88 sudo_set_max_mechanism_count +call pallets/admin-utils/src/lib.rs 89 sudo_set_burn_half_life +call pallets/admin-utils/src/lib.rs 9 sudo_set_adjustment_alpha +call pallets/admin-utils/src/lib.rs 90 sudo_set_burn_increase_mult +call pallets/admin-utils/src/lib.rs 91 sudo_set_net_tao_flow_enabled +call pallets/admin-utils/src/lib.rs 92 sudo_set_owner_cut_enabled +call pallets/admin-utils/src/lib.rs 93 sudo_set_min_childkey_take_per_subnet +call pallets/admin-utils/src/lib.rs 94 sudo_set_subnet_emission_enabled +call pallets/admin-utils/src/lib.rs 95 sudo_set_owner_cut_auto_lock_enabled +call pallets/admin-utils/src/lib.rs 96 sudo_set_max_epochs_per_block +call pallets/admin-utils/src/lib.rs 97 sudo_set_activity_cutoff_factor +call pallets/admin-utils/src/lib.rs 98 sudo_set_collateral_lock_share +call pallets/admin-utils/src/lib.rs 99 sudo_set_collateral_drain_ratio +call pallets/commitments/src/lib.rs 0 set_commitment +call pallets/commitments/src/lib.rs 2 set_max_space +call pallets/crowdloan/src/lib.rs 0 create +call pallets/crowdloan/src/lib.rs 1 contribute +call pallets/crowdloan/src/lib.rs 2 withdraw +call pallets/crowdloan/src/lib.rs 3 finalize +call pallets/crowdloan/src/lib.rs 4 refund +call pallets/crowdloan/src/lib.rs 5 dissolve +call pallets/crowdloan/src/lib.rs 6 update_min_contribution +call pallets/crowdloan/src/lib.rs 7 update_end +call pallets/crowdloan/src/lib.rs 8 update_cap +call pallets/crowdloan/src/lib.rs 9 set_max_contribution +call pallets/drand/src/lib.rs 0 write_pulse +call pallets/drand/src/lib.rs 1 set_beacon_config +call pallets/drand/src/lib.rs 2 set_oldest_stored_round +call pallets/limit-orders/src/lib.rs 0 execute_orders +call pallets/limit-orders/src/lib.rs 1 execute_batched_orders +call pallets/limit-orders/src/lib.rs 2 cancel_order +call pallets/limit-orders/src/lib.rs 3 set_pallet_status +call pallets/proxy/src/lib.rs 0 proxy +call pallets/proxy/src/lib.rs 1 add_proxy +call pallets/proxy/src/lib.rs 10 poke_deposit +call pallets/proxy/src/lib.rs 11 set_real_pays_fee +call pallets/proxy/src/lib.rs 2 remove_proxy +call pallets/proxy/src/lib.rs 3 remove_proxies +call pallets/proxy/src/lib.rs 4 create_pure +call pallets/proxy/src/lib.rs 5 kill_pure +call pallets/proxy/src/lib.rs 6 announce +call pallets/proxy/src/lib.rs 7 remove_announcement +call pallets/proxy/src/lib.rs 8 reject_announcement +call pallets/proxy/src/lib.rs 9 proxy_announced +call pallets/shield/src/lib.rs 0 announce_next_key +call pallets/shield/src/lib.rs 1 submit_encrypted +call pallets/shield/src/lib.rs 2 store_encrypted +call pallets/shield/src/lib.rs 3 set_max_pending_extrinsics_number +call pallets/shield/src/lib.rs 4 set_on_initialize_weight +call pallets/shield/src/lib.rs 5 set_stored_extrinsic_lifetime +call pallets/shield/src/lib.rs 6 set_max_extrinsic_weight +call pallets/subtensor/src/macros/dispatches.rs 0 set_weights +call pallets/subtensor/src/macros/dispatches.rs 100 batch_commit_weights +call pallets/subtensor/src/macros/dispatches.rs 101 recycle_alpha +call pallets/subtensor/src/macros/dispatches.rs 102 burn_alpha +call pallets/subtensor/src/macros/dispatches.rs 103 remove_stake_full_limit +call pallets/subtensor/src/macros/dispatches.rs 109 set_pending_childkey_cooldown +call pallets/subtensor/src/macros/dispatches.rs 110 register_leased_network +call pallets/subtensor/src/macros/dispatches.rs 111 terminate_lease +call pallets/subtensor/src/macros/dispatches.rs 112 update_symbol +call pallets/subtensor/src/macros/dispatches.rs 113 commit_timelocked_weights +call pallets/subtensor/src/macros/dispatches.rs 114 set_coldkey_auto_stake_hotkey +call pallets/subtensor/src/macros/dispatches.rs 115 commit_mechanism_weights +call pallets/subtensor/src/macros/dispatches.rs 116 reveal_mechanism_weights +call pallets/subtensor/src/macros/dispatches.rs 117 commit_crv3_mechanism_weights +call pallets/subtensor/src/macros/dispatches.rs 118 commit_timelocked_mechanism_weights +call pallets/subtensor/src/macros/dispatches.rs 119 set_mechanism_weights +call pallets/subtensor/src/macros/dispatches.rs 120 root_dissolve_network +call pallets/subtensor/src/macros/dispatches.rs 121 claim_root +call pallets/subtensor/src/macros/dispatches.rs 122 set_root_claim_type +call pallets/subtensor/src/macros/dispatches.rs 123 sudo_set_num_root_claims +call pallets/subtensor/src/macros/dispatches.rs 124 sudo_set_root_claim_threshold +call pallets/subtensor/src/macros/dispatches.rs 125 announce_coldkey_swap +call pallets/subtensor/src/macros/dispatches.rs 126 swap_coldkey_announced +call pallets/subtensor/src/macros/dispatches.rs 127 dispute_coldkey_swap +call pallets/subtensor/src/macros/dispatches.rs 128 reset_coldkey_swap +call pallets/subtensor/src/macros/dispatches.rs 129 enable_voting_power_tracking +call pallets/subtensor/src/macros/dispatches.rs 130 disable_voting_power_tracking +call pallets/subtensor/src/macros/dispatches.rs 131 sudo_set_voting_power_ema_alpha +call pallets/subtensor/src/macros/dispatches.rs 132 add_stake_burn +call pallets/subtensor/src/macros/dispatches.rs 133 clear_coldkey_swap_announcement +call pallets/subtensor/src/macros/dispatches.rs 134 register_limit +call pallets/subtensor/src/macros/dispatches.rs 135 set_auto_parent_delegation_enabled +call pallets/subtensor/src/macros/dispatches.rs 136 lock_stake +call pallets/subtensor/src/macros/dispatches.rs 137 move_lock +call pallets/subtensor/src/macros/dispatches.rs 138 set_perpetual_lock +call pallets/subtensor/src/macros/dispatches.rs 139 set_tempo +call pallets/subtensor/src/macros/dispatches.rs 140 set_activity_cutoff_factor +call pallets/subtensor/src/macros/dispatches.rs 141 trigger_epoch +call pallets/subtensor/src/macros/dispatches.rs 142 set_reject_locked_alpha +call pallets/subtensor/src/macros/dispatches.rs 143 transfer_stake_and_hotkey +call pallets/subtensor/src/macros/dispatches.rs 144 add_collateral +call pallets/subtensor/src/macros/dispatches.rs 145 set_min_collateral +call pallets/subtensor/src/macros/dispatches.rs 2 add_stake +call pallets/subtensor/src/macros/dispatches.rs 3 remove_stake +call pallets/subtensor/src/macros/dispatches.rs 4 serve_axon +call pallets/subtensor/src/macros/dispatches.rs 40 serve_axon_tls +call pallets/subtensor/src/macros/dispatches.rs 5 serve_prometheus +call pallets/subtensor/src/macros/dispatches.rs 59 register_network +call pallets/subtensor/src/macros/dispatches.rs 6 register +call pallets/subtensor/src/macros/dispatches.rs 60 faucet +call pallets/subtensor/src/macros/dispatches.rs 61 dissolve_network +call pallets/subtensor/src/macros/dispatches.rs 62 root_register +call pallets/subtensor/src/macros/dispatches.rs 65 decrease_take +call pallets/subtensor/src/macros/dispatches.rs 66 increase_take +call pallets/subtensor/src/macros/dispatches.rs 67 set_children +call pallets/subtensor/src/macros/dispatches.rs 68 set_identity +call pallets/subtensor/src/macros/dispatches.rs 69 sudo_set_tx_childkey_take_rate_limit +call pallets/subtensor/src/macros/dispatches.rs 7 burned_register +call pallets/subtensor/src/macros/dispatches.rs 70 swap_hotkey +call pallets/subtensor/src/macros/dispatches.rs 71 swap_coldkey +call pallets/subtensor/src/macros/dispatches.rs 72 swap_hotkey_v2 +call pallets/subtensor/src/macros/dispatches.rs 73 schedule_swap_coldkey +call pallets/subtensor/src/macros/dispatches.rs 75 set_childkey_take +call pallets/subtensor/src/macros/dispatches.rs 76 sudo_set_min_childkey_take +call pallets/subtensor/src/macros/dispatches.rs 77 sudo_set_max_childkey_take +call pallets/subtensor/src/macros/dispatches.rs 78 set_subnet_identity +call pallets/subtensor/src/macros/dispatches.rs 79 register_network_with_identity +call pallets/subtensor/src/macros/dispatches.rs 80 batch_set_weights +call pallets/subtensor/src/macros/dispatches.rs 83 unstake_all +call pallets/subtensor/src/macros/dispatches.rs 84 unstake_all_alpha +call pallets/subtensor/src/macros/dispatches.rs 85 move_stake +call pallets/subtensor/src/macros/dispatches.rs 86 transfer_stake +call pallets/subtensor/src/macros/dispatches.rs 87 swap_stake +call pallets/subtensor/src/macros/dispatches.rs 88 add_stake_limit +call pallets/subtensor/src/macros/dispatches.rs 89 remove_stake_limit +call pallets/subtensor/src/macros/dispatches.rs 90 swap_stake_limit +call pallets/subtensor/src/macros/dispatches.rs 91 try_associate_hotkey +call pallets/subtensor/src/macros/dispatches.rs 92 start_call +call pallets/subtensor/src/macros/dispatches.rs 93 associate_evm_key +call pallets/subtensor/src/macros/dispatches.rs 96 commit_weights +call pallets/subtensor/src/macros/dispatches.rs 97 reveal_weights +call pallets/subtensor/src/macros/dispatches.rs 98 batch_reveal_weights +call pallets/swap/src/pallet/mod.rs 0 set_fee_rate +call pallets/swap/src/pallet/mod.rs 1 add_liquidity +call pallets/swap/src/pallet/mod.rs 2 remove_liquidity +call pallets/swap/src/pallet/mod.rs 3 modify_position +call pallets/swap/src/pallet/mod.rs 4 toggle_user_liquidity +call pallets/swap/src/pallet/mod.rs 5 disable_lp +call pallets/utility/src/lib.rs 0 batch +call pallets/utility/src/lib.rs 1 as_derivative +call pallets/utility/src/lib.rs 2 batch_all +call pallets/utility/src/lib.rs 3 dispatch_as +call pallets/utility/src/lib.rs 4 force_batch +call pallets/utility/src/lib.rs 5 with_weight +call pallets/utility/src/lib.rs 6 if_else +call pallets/utility/src/lib.rs 7 dispatch_as_fallible +call pallets/utility/src/tests.rs 0 noop +call pallets/utility/src/tests.rs 1 foobar +call pallets/utility/src/tests.rs 2 big_variant +event pallets/admin-utils/src/lib.rs 0 PrecompileUpdated +event pallets/admin-utils/src/lib.rs 1 Yuma3EnableToggled +event pallets/admin-utils/src/lib.rs 2 BondsResetToggled +event pallets/admin-utils/src/lib.rs 3 BurnHalfLifeSet +event pallets/admin-utils/src/lib.rs 4 BurnIncreaseMultSet +event pallets/admin-utils/src/lib.rs 5 SubnetEmissionEnabledSet +event pallets/admin-utils/src/lib.rs 6 CollateralLockShareSet +event pallets/admin-utils/src/lib.rs 7 CollateralDrainRatioSet +error pallets/admin-utils/src/lib.rs 0 SubnetDoesNotExist +error pallets/admin-utils/src/lib.rs 1 MaxValidatorsLargerThanMaxUIds +error pallets/admin-utils/src/lib.rs 2 MaxAllowedUIdsLessThanCurrentUIds +error pallets/admin-utils/src/lib.rs 3 BondsMovingAverageMaxReached +error pallets/admin-utils/src/lib.rs 4 NegativeSigmoidSteepness +error pallets/admin-utils/src/lib.rs 5 ValueNotInBounds +error pallets/admin-utils/src/lib.rs 6 MinAllowedUidsGreaterThanCurrentUids +error pallets/admin-utils/src/lib.rs 7 MinAllowedUidsGreaterThanMaxAllowedUids +error pallets/admin-utils/src/lib.rs 8 MaxAllowedUidsLessThanMinAllowedUids +error pallets/admin-utils/src/lib.rs 9 MaxAllowedUidsGreaterThanDefaultMaxAllowedUids +error pallets/admin-utils/src/lib.rs 10 InvalidValue +error pallets/admin-utils/src/lib.rs 11 NotPermittedOnRootSubnet +error pallets/admin-utils/src/lib.rs 12 POWRegistrationDisabled +error pallets/admin-utils/src/lib.rs 13 Deprecated +error pallets/admin-utils/src/lib.rs 14 CollateralLockShareTooHigh +error pallets/admin-utils/src/lib.rs 15 CollateralDrainRatioOutOfBounds +event pallets/commitments/src/lib.rs 0 Commitment +event pallets/commitments/src/lib.rs 1 TimelockCommitment +event pallets/commitments/src/lib.rs 2 CommitmentRevealed +error pallets/commitments/src/lib.rs 0 TooManyFieldsInCommitmentInfo +error pallets/commitments/src/lib.rs 1 AccountNotAllowedCommit +error pallets/commitments/src/lib.rs 2 SpaceLimitExceeded +error pallets/commitments/src/lib.rs 3 UnexpectedUnreserveLeftover +event pallets/crowdloan/src/lib.rs 0 Created +event pallets/crowdloan/src/lib.rs 1 Contributed +event pallets/crowdloan/src/lib.rs 2 Withdrew +event pallets/crowdloan/src/lib.rs 3 PartiallyRefunded +event pallets/crowdloan/src/lib.rs 4 AllRefunded +event pallets/crowdloan/src/lib.rs 5 Finalized +event pallets/crowdloan/src/lib.rs 6 Dissolved +event pallets/crowdloan/src/lib.rs 7 MinContributionUpdated +event pallets/crowdloan/src/lib.rs 8 EndUpdated +event pallets/crowdloan/src/lib.rs 9 CapUpdated +event pallets/crowdloan/src/lib.rs 10 MaxContributionUpdated +error pallets/crowdloan/src/lib.rs 0 DepositTooLow +error pallets/crowdloan/src/lib.rs 1 CapTooLow +error pallets/crowdloan/src/lib.rs 2 MinimumContributionTooLow +error pallets/crowdloan/src/lib.rs 3 CannotEndInPast +error pallets/crowdloan/src/lib.rs 4 BlockDurationTooShort +error pallets/crowdloan/src/lib.rs 5 BlockDurationTooLong +error pallets/crowdloan/src/lib.rs 6 InsufficientBalance +error pallets/crowdloan/src/lib.rs 7 Overflow +error pallets/crowdloan/src/lib.rs 8 InvalidCrowdloanId +error pallets/crowdloan/src/lib.rs 9 CapRaised +error pallets/crowdloan/src/lib.rs 10 ContributionPeriodEnded +error pallets/crowdloan/src/lib.rs 11 ContributionTooLow +error pallets/crowdloan/src/lib.rs 12 InvalidOrigin +error pallets/crowdloan/src/lib.rs 13 AlreadyFinalized +error pallets/crowdloan/src/lib.rs 14 AlreadyFinalizing +error pallets/crowdloan/src/lib.rs 15 ContributionPeriodNotEnded +error pallets/crowdloan/src/lib.rs 16 NoContribution +error pallets/crowdloan/src/lib.rs 17 CapNotRaised +error pallets/crowdloan/src/lib.rs 18 Underflow +error pallets/crowdloan/src/lib.rs 19 CallUnavailable +error pallets/crowdloan/src/lib.rs 20 NotReadyToDissolve +error pallets/crowdloan/src/lib.rs 21 DepositCannotBeWithdrawn +error pallets/crowdloan/src/lib.rs 22 MaxContributorsReached +error pallets/crowdloan/src/lib.rs 23 InvalidFinalizationConfig +error pallets/crowdloan/src/lib.rs 24 MaxContributionReached +error pallets/crowdloan/src/lib.rs 25 MaximumContributionTooLow +error pallets/crowdloan/src/lib.rs 26 MinimumContributionTooHigh +event pallets/drand/src/lib.rs 0 BeaconConfigChanged +event pallets/drand/src/lib.rs 1 NewPulse +event pallets/drand/src/lib.rs 2 SetOldestStoredRound +error pallets/drand/src/lib.rs 0 NoneValue +error pallets/drand/src/lib.rs 1 StorageOverflow +error pallets/drand/src/lib.rs 2 DrandConnectionFailure +error pallets/drand/src/lib.rs 3 UnverifiedPulse +error pallets/drand/src/lib.rs 4 InvalidRoundNumber +error pallets/drand/src/lib.rs 5 PulseVerificationError +event pallets/limit-orders/src/lib.rs 0 OrderExecuted +event pallets/limit-orders/src/lib.rs 1 OrderSkipped +event pallets/limit-orders/src/lib.rs 2 OrderCancelled +event pallets/limit-orders/src/lib.rs 3 GroupExecutionSummary +event pallets/limit-orders/src/lib.rs 4 LimitOrdersPalletStatusChanged +error pallets/limit-orders/src/lib.rs 0 InvalidSignature +error pallets/limit-orders/src/lib.rs 1 OrderAlreadyProcessed +error pallets/limit-orders/src/lib.rs 2 OrderCancelled +error pallets/limit-orders/src/lib.rs 3 OrderExpired +error pallets/limit-orders/src/lib.rs 4 PriceConditionNotMet +error pallets/limit-orders/src/lib.rs 5 Unauthorized +error pallets/limit-orders/src/lib.rs 6 SwapReturnedZero +error pallets/limit-orders/src/lib.rs 7 RootNetUidNotAllowed +error pallets/limit-orders/src/lib.rs 8 OrderNetUidMismatch +error pallets/limit-orders/src/lib.rs 9 LimitOrdersDisabled +error pallets/limit-orders/src/lib.rs 10 RelayerMissMatch +error pallets/limit-orders/src/lib.rs 11 PartialFillsNotEnabled +error pallets/limit-orders/src/lib.rs 12 IncorrectPartialFillAmount +error pallets/limit-orders/src/lib.rs 13 RelayerRequiredForPartialFill +error pallets/limit-orders/src/lib.rs 14 ChainIdMismatch +error pallets/limit-orders/src/lib.rs 15 PalletHotkeyNotRegistered +error pallets/limit-orders/src/lib.rs 16 ArithmeticOverflow +error pallets/limit-orders/src/lib.rs 17 DuplicateOrderInBatch +error pallets/limit-orders/src/lib.rs 18 ZeroShareInBatch +event pallets/proxy/src/lib.rs 0 ProxyExecuted +event pallets/proxy/src/lib.rs 1 PureCreated +event pallets/proxy/src/lib.rs 2 PureKilled +event pallets/proxy/src/lib.rs 3 Announced +event pallets/proxy/src/lib.rs 4 ProxyAdded +event pallets/proxy/src/lib.rs 5 ProxyRemoved +event pallets/proxy/src/lib.rs 6 DepositPoked +event pallets/proxy/src/lib.rs 7 RealPaysFeeSet +error pallets/proxy/src/lib.rs 0 TooMany +error pallets/proxy/src/lib.rs 1 NotFound +error pallets/proxy/src/lib.rs 2 NotProxy +error pallets/proxy/src/lib.rs 3 Unproxyable +error pallets/proxy/src/lib.rs 4 Duplicate +error pallets/proxy/src/lib.rs 5 NoPermission +error pallets/proxy/src/lib.rs 6 Unannounced +error pallets/proxy/src/lib.rs 7 NoSelfProxy +error pallets/proxy/src/lib.rs 8 AnnouncementDepositInvariantViolated +error pallets/proxy/src/lib.rs 9 InvalidDerivedAccountId +event pallets/shield/src/lib.rs 0 EncryptedSubmitted +event pallets/shield/src/lib.rs 1 ExtrinsicStored +event pallets/shield/src/lib.rs 2 ExtrinsicDecodeFailed +event pallets/shield/src/lib.rs 3 ExtrinsicDispatchFailed +event pallets/shield/src/lib.rs 4 ExtrinsicDispatched +event pallets/shield/src/lib.rs 5 ExtrinsicExpired +event pallets/shield/src/lib.rs 6 ExtrinsicPostponed +event pallets/shield/src/lib.rs 7 MaxPendingExtrinsicsNumberSet +event pallets/shield/src/lib.rs 8 OnInitializeWeightSet +event pallets/shield/src/lib.rs 9 ExtrinsicLifetimeSet +event pallets/shield/src/lib.rs 10 MaxExtrinsicWeightSet +event pallets/shield/src/lib.rs 11 ExtrinsicWeightExceeded +error pallets/shield/src/lib.rs 0 BadEncKeyLen +error pallets/shield/src/lib.rs 1 Unreachable +error pallets/shield/src/lib.rs 2 TooManyPendingExtrinsics +error pallets/shield/src/lib.rs 3 WeightExceedsAbsoluteMax +error pallets/subtensor/src/macros/errors.rs 0 RootNetworkDoesNotExist +error pallets/subtensor/src/macros/errors.rs 1 InvalidIpType +error pallets/subtensor/src/macros/errors.rs 2 InvalidIpAddress +error pallets/subtensor/src/macros/errors.rs 3 InvalidPort +error pallets/subtensor/src/macros/errors.rs 4 HotKeyNotRegisteredInSubNet +error pallets/subtensor/src/macros/errors.rs 5 HotKeyAccountNotExists +error pallets/subtensor/src/macros/errors.rs 6 HotKeyNotRegisteredInNetwork +error pallets/subtensor/src/macros/errors.rs 7 NonAssociatedColdKey +error pallets/subtensor/src/macros/errors.rs 8 NotEnoughStake +error pallets/subtensor/src/macros/errors.rs 9 NotEnoughStakeToWithdraw +error pallets/subtensor/src/macros/errors.rs 10 NotEnoughStakeToSetWeights +error pallets/subtensor/src/macros/errors.rs 11 NotEnoughStakeToSetChildkeys +error pallets/subtensor/src/macros/errors.rs 12 NotEnoughBalanceToStake +error pallets/subtensor/src/macros/errors.rs 13 BalanceWithdrawalError +error pallets/subtensor/src/macros/errors.rs 14 ZeroBalanceAfterWithdrawn +error pallets/subtensor/src/macros/errors.rs 15 NeuronNoValidatorPermit +error pallets/subtensor/src/macros/errors.rs 16 WeightVecNotEqualSize +error pallets/subtensor/src/macros/errors.rs 17 DuplicateUids +error pallets/subtensor/src/macros/errors.rs 18 UidVecContainInvalidOne +error pallets/subtensor/src/macros/errors.rs 19 WeightVecLengthIsLow +error pallets/subtensor/src/macros/errors.rs 20 TooManyRegistrationsThisBlock +error pallets/subtensor/src/macros/errors.rs 21 HotKeyAlreadyRegisteredInSubNet +error pallets/subtensor/src/macros/errors.rs 22 NewHotKeyIsSameWithOld +error pallets/subtensor/src/macros/errors.rs 23 NewHotKeyNotCleanForRootSwap +error pallets/subtensor/src/macros/errors.rs 24 InvalidWorkBlock +error pallets/subtensor/src/macros/errors.rs 25 InvalidDifficulty +error pallets/subtensor/src/macros/errors.rs 26 InvalidSeal +error pallets/subtensor/src/macros/errors.rs 27 MaxWeightExceeded +error pallets/subtensor/src/macros/errors.rs 28 HotKeyAlreadyDelegate +error pallets/subtensor/src/macros/errors.rs 29 SettingWeightsTooFast +error pallets/subtensor/src/macros/errors.rs 30 IncorrectWeightVersionKey +error pallets/subtensor/src/macros/errors.rs 31 ServingRateLimitExceeded +error pallets/subtensor/src/macros/errors.rs 32 UidsLengthExceedUidsInSubNet +error pallets/subtensor/src/macros/errors.rs 33 NetworkTxRateLimitExceeded +error pallets/subtensor/src/macros/errors.rs 34 DelegateTxRateLimitExceeded +error pallets/subtensor/src/macros/errors.rs 35 HotKeySetTxRateLimitExceeded +error pallets/subtensor/src/macros/errors.rs 36 StakingRateLimitExceeded +error pallets/subtensor/src/macros/errors.rs 37 SubNetRegistrationDisabled +error pallets/subtensor/src/macros/errors.rs 38 TooManyRegistrationsThisInterval +error pallets/subtensor/src/macros/errors.rs 39 TransactorAccountShouldBeHotKey +error pallets/subtensor/src/macros/errors.rs 40 FaucetDisabled +error pallets/subtensor/src/macros/errors.rs 41 NotSubnetOwner +error pallets/subtensor/src/macros/errors.rs 42 RegistrationNotPermittedOnRootSubnet +error pallets/subtensor/src/macros/errors.rs 43 StakeTooLowForRoot +error pallets/subtensor/src/macros/errors.rs 44 AllNetworksInImmunity +error pallets/subtensor/src/macros/errors.rs 45 NotEnoughBalanceToPaySwapHotKey +error pallets/subtensor/src/macros/errors.rs 46 NotRootSubnet +error pallets/subtensor/src/macros/errors.rs 47 CanNotSetRootNetworkWeights +error pallets/subtensor/src/macros/errors.rs 48 NoNeuronIdAvailable +error pallets/subtensor/src/macros/errors.rs 49 DelegateTakeTooLow +error pallets/subtensor/src/macros/errors.rs 50 DelegateTakeTooHigh +error pallets/subtensor/src/macros/errors.rs 51 NoWeightsCommitFound +error pallets/subtensor/src/macros/errors.rs 52 InvalidRevealCommitHashNotMatch +error pallets/subtensor/src/macros/errors.rs 53 CommitRevealEnabled +error pallets/subtensor/src/macros/errors.rs 54 CommitRevealDisabled +error pallets/subtensor/src/macros/errors.rs 55 LiquidAlphaDisabled +error pallets/subtensor/src/macros/errors.rs 56 AlphaHighTooLow +error pallets/subtensor/src/macros/errors.rs 57 AlphaLowOutOfRange +error pallets/subtensor/src/macros/errors.rs 58 ColdKeyAlreadyAssociated +error pallets/subtensor/src/macros/errors.rs 59 NotEnoughBalanceToPaySwapColdKey +error pallets/subtensor/src/macros/errors.rs 60 InvalidChild +error pallets/subtensor/src/macros/errors.rs 61 DuplicateChild +error pallets/subtensor/src/macros/errors.rs 62 ProportionOverflow +error pallets/subtensor/src/macros/errors.rs 63 TooManyChildren +error pallets/subtensor/src/macros/errors.rs 64 TxRateLimitExceeded +error pallets/subtensor/src/macros/errors.rs 65 ColdkeySwapAnnouncementNotFound +error pallets/subtensor/src/macros/errors.rs 66 ColdkeySwapTooEarly +error pallets/subtensor/src/macros/errors.rs 67 ColdkeySwapReannouncedTooEarly +error pallets/subtensor/src/macros/errors.rs 68 AnnouncedColdkeyHashDoesNotMatch +error pallets/subtensor/src/macros/errors.rs 69 ColdkeySwapAlreadyDisputed +error pallets/subtensor/src/macros/errors.rs 70 NewColdKeyIsHotkey +error pallets/subtensor/src/macros/errors.rs 71 InvalidChildkeyTake +error pallets/subtensor/src/macros/errors.rs 72 TxChildkeyTakeRateLimitExceeded +error pallets/subtensor/src/macros/errors.rs 73 InvalidIdentity +error pallets/subtensor/src/macros/errors.rs 74 MechanismDoesNotExist +error pallets/subtensor/src/macros/errors.rs 75 StakeUnavailable +error pallets/subtensor/src/macros/errors.rs 76 SubnetNotExists +error pallets/subtensor/src/macros/errors.rs 77 TooManyUnrevealedCommits +error pallets/subtensor/src/macros/errors.rs 78 ExpiredWeightCommit +error pallets/subtensor/src/macros/errors.rs 79 RevealTooEarly +error pallets/subtensor/src/macros/errors.rs 80 InputLengthsUnequal +error pallets/subtensor/src/macros/errors.rs 81 CommittingWeightsTooFast +error pallets/subtensor/src/macros/errors.rs 82 AmountTooLow +error pallets/subtensor/src/macros/errors.rs 83 InsufficientLiquidity +error pallets/subtensor/src/macros/errors.rs 84 SlippageTooHigh +error pallets/subtensor/src/macros/errors.rs 85 TransferDisallowed +error pallets/subtensor/src/macros/errors.rs 86 ActivityCutoffTooLow +error pallets/subtensor/src/macros/errors.rs 87 CallDisabled +error pallets/subtensor/src/macros/errors.rs 88 FirstEmissionBlockNumberAlreadySet +error pallets/subtensor/src/macros/errors.rs 89 NeedWaitingMoreBlocksToStarCall +error pallets/subtensor/src/macros/errors.rs 90 NotEnoughAlphaOutToRecycle +error pallets/subtensor/src/macros/errors.rs 91 CannotBurnOrRecycleOnRootSubnet +error pallets/subtensor/src/macros/errors.rs 92 UnableToRecoverPublicKey +error pallets/subtensor/src/macros/errors.rs 93 InvalidRecoveredPublicKey +error pallets/subtensor/src/macros/errors.rs 94 SubtokenDisabled +error pallets/subtensor/src/macros/errors.rs 95 HotKeySwapOnSubnetIntervalNotPassed +error pallets/subtensor/src/macros/errors.rs 96 KeepStakeBlockedByCollateral +error pallets/subtensor/src/macros/errors.rs 97 SameNetuid +error pallets/subtensor/src/macros/errors.rs 98 InsufficientTaoBalance +error pallets/subtensor/src/macros/errors.rs 99 InvalidLeaseBeneficiary +error pallets/subtensor/src/macros/errors.rs 100 LeaseCannotEndInThePast +error pallets/subtensor/src/macros/errors.rs 101 LeaseNetuidNotFound +error pallets/subtensor/src/macros/errors.rs 102 LeaseDoesNotExist +error pallets/subtensor/src/macros/errors.rs 103 LeaseHasNoEndBlock +error pallets/subtensor/src/macros/errors.rs 104 LeaseHasNotEnded +error pallets/subtensor/src/macros/errors.rs 105 Overflow +error pallets/subtensor/src/macros/errors.rs 106 BeneficiaryDoesNotOwnHotkey +error pallets/subtensor/src/macros/errors.rs 107 ExpectedBeneficiaryOrigin +error pallets/subtensor/src/macros/errors.rs 108 AdminActionProhibitedDuringWeightsWindow +error pallets/subtensor/src/macros/errors.rs 109 SymbolDoesNotExist +error pallets/subtensor/src/macros/errors.rs 110 SymbolAlreadyInUse +error pallets/subtensor/src/macros/errors.rs 111 IncorrectCommitRevealVersion +error pallets/subtensor/src/macros/errors.rs 112 InvalidRevealRound +error pallets/subtensor/src/macros/errors.rs 113 RevealPeriodTooLarge +error pallets/subtensor/src/macros/errors.rs 114 RevealPeriodTooSmall +error pallets/subtensor/src/macros/errors.rs 115 InvalidValue +error pallets/subtensor/src/macros/errors.rs 116 SubnetLimitReached +error pallets/subtensor/src/macros/errors.rs 117 CannotAffordLockCost +error pallets/subtensor/src/macros/errors.rs 118 EvmKeyAssociateRateLimitExceeded +error pallets/subtensor/src/macros/errors.rs 119 EvmKeyAssociationLimitExceeded +error pallets/subtensor/src/macros/errors.rs 120 SameAutoStakeHotkeyAlreadySet +error pallets/subtensor/src/macros/errors.rs 121 UidMapCouldNotBeCleared +error pallets/subtensor/src/macros/errors.rs 122 TrimmingWouldExceedMaxImmunePercentage +error pallets/subtensor/src/macros/errors.rs 123 ChildParentInconsistency +error pallets/subtensor/src/macros/errors.rs 124 InvalidNumRootClaim +error pallets/subtensor/src/macros/errors.rs 125 InvalidRootClaimThreshold +error pallets/subtensor/src/macros/errors.rs 126 InvalidSubnetNumber +error pallets/subtensor/src/macros/errors.rs 127 TooManyUIDsPerMechanism +error pallets/subtensor/src/macros/errors.rs 128 VotingPowerTrackingNotEnabled +error pallets/subtensor/src/macros/errors.rs 129 InvalidVotingPowerEmaAlpha +error pallets/subtensor/src/macros/errors.rs 130 Deprecated +error pallets/subtensor/src/macros/errors.rs 131 SubnetBuybackRateLimitExceeded +error pallets/subtensor/src/macros/errors.rs 132 NetworkDissolveAlreadyQueued +error pallets/subtensor/src/macros/errors.rs 133 AddStakeBurnRateLimitExceeded +error pallets/subtensor/src/macros/errors.rs 134 ColdkeySwapAnnounced +error pallets/subtensor/src/macros/errors.rs 135 ColdkeySwapDisputed +error pallets/subtensor/src/macros/errors.rs 136 ColdkeySwapClearTooEarly +error pallets/subtensor/src/macros/errors.rs 137 DisabledTemporarily +error pallets/subtensor/src/macros/errors.rs 138 RegistrationPriceLimitExceeded +error pallets/subtensor/src/macros/errors.rs 139 LockHotkeyMismatch +error pallets/subtensor/src/macros/errors.rs 140 InsufficientStakeForLock +error pallets/subtensor/src/macros/errors.rs 141 NoExistingLock +error pallets/subtensor/src/macros/errors.rs 142 ActiveLockExists +error pallets/subtensor/src/macros/errors.rs 143 CannotUseSystemAccount +error pallets/subtensor/src/macros/errors.rs 144 UnlockAmountTooHigh +error pallets/subtensor/src/macros/errors.rs 145 WaitingForDissolvedSubnetCleanup +error pallets/subtensor/src/macros/errors.rs 146 TempoOutOfBounds +error pallets/subtensor/src/macros/errors.rs 147 ActivityCutoffFactorMilliOutOfBounds +error pallets/subtensor/src/macros/errors.rs 148 EpochTriggerAlreadyPending +error pallets/subtensor/src/macros/errors.rs 149 AutoEpochAlreadyImminent +error pallets/subtensor/src/macros/errors.rs 150 DynamicTempoBlockedByCommitReveal +error pallets/subtensor/src/macros/errors.rs 151 AccountRejectsLockedAlpha +error pallets/subtensor/src/macros/errors.rs 152 LockIdOverFlow +error pallets/subtensor/src/macros/errors.rs 153 StartCallNotReady +error pallets/subtensor/src/macros/errors.rs 154 InsufficientAlphaBalance +error pallets/subtensor/src/macros/errors.rs 155 ColdkeyCollateralIncomplete +error pallets/subtensor/src/macros/errors.rs 156 ColdkeyCollateralPositionsFull +event pallets/subtensor/src/macros/events.rs 0 NetworkAdded +event pallets/subtensor/src/macros/events.rs 1 NetworkRemoved +event pallets/subtensor/src/macros/events.rs 2 StakeAdded +event pallets/subtensor/src/macros/events.rs 3 StakeRemoved +event pallets/subtensor/src/macros/events.rs 4 StakeMoved +event pallets/subtensor/src/macros/events.rs 5 WeightsSet +event pallets/subtensor/src/macros/events.rs 6 NeuronRegistered +event pallets/subtensor/src/macros/events.rs 7 BulkNeuronsRegistered +event pallets/subtensor/src/macros/events.rs 8 BulkBalancesSet +event pallets/subtensor/src/macros/events.rs 9 MaxAllowedUidsSet +event pallets/subtensor/src/macros/events.rs 10 MaxWeightLimitSet +event pallets/subtensor/src/macros/events.rs 11 DifficultySet +event pallets/subtensor/src/macros/events.rs 12 AdjustmentIntervalSet +event pallets/subtensor/src/macros/events.rs 13 RegistrationPerIntervalSet +event pallets/subtensor/src/macros/events.rs 14 MaxRegistrationsPerBlockSet +event pallets/subtensor/src/macros/events.rs 15 ActivityCutoffSet +event pallets/subtensor/src/macros/events.rs 16 RhoSet +event pallets/subtensor/src/macros/events.rs 17 AlphaSigmoidSteepnessSet +event pallets/subtensor/src/macros/events.rs 18 KappaSet +event pallets/subtensor/src/macros/events.rs 19 MinAllowedWeightSet +event pallets/subtensor/src/macros/events.rs 20 ValidatorPruneLenSet +event pallets/subtensor/src/macros/events.rs 21 ScalingLawPowerSet +event pallets/subtensor/src/macros/events.rs 22 WeightsSetRateLimitSet +event pallets/subtensor/src/macros/events.rs 23 ImmunityPeriodSet +event pallets/subtensor/src/macros/events.rs 24 BondsMovingAverageSet +event pallets/subtensor/src/macros/events.rs 25 BondsPenaltySet +event pallets/subtensor/src/macros/events.rs 26 BondsResetOnSet +event pallets/subtensor/src/macros/events.rs 27 MaxAllowedValidatorsSet +event pallets/subtensor/src/macros/events.rs 28 AxonServed +event pallets/subtensor/src/macros/events.rs 29 PrometheusServed +event pallets/subtensor/src/macros/events.rs 30 DelegateAdded +event pallets/subtensor/src/macros/events.rs 31 DefaultTakeSet +event pallets/subtensor/src/macros/events.rs 32 WeightsVersionKeySet +event pallets/subtensor/src/macros/events.rs 33 MinDifficultySet +event pallets/subtensor/src/macros/events.rs 34 MaxDifficultySet +event pallets/subtensor/src/macros/events.rs 35 ServingRateLimitSet +event pallets/subtensor/src/macros/events.rs 36 BurnSet +event pallets/subtensor/src/macros/events.rs 37 MaxBurnSet +event pallets/subtensor/src/macros/events.rs 38 MinBurnSet +event pallets/subtensor/src/macros/events.rs 39 MaxEpochsPerBlockSet +event pallets/subtensor/src/macros/events.rs 40 TxRateLimitSet +event pallets/subtensor/src/macros/events.rs 41 TxDelegateTakeRateLimitSet +event pallets/subtensor/src/macros/events.rs 42 TxChildKeyTakeRateLimitSet +event pallets/subtensor/src/macros/events.rs 43 AdminFreezeWindowSet +event pallets/subtensor/src/macros/events.rs 44 OwnerHyperparamRateLimitSet +event pallets/subtensor/src/macros/events.rs 45 MinChildKeyTakeSet +event pallets/subtensor/src/macros/events.rs 46 MinChildKeyTakePerSubnetSet +event pallets/subtensor/src/macros/events.rs 47 MaxChildKeyTakeSet +event pallets/subtensor/src/macros/events.rs 48 ChildKeyTakeSet +event pallets/subtensor/src/macros/events.rs 49 Sudid +event pallets/subtensor/src/macros/events.rs 50 RegistrationAllowed +event pallets/subtensor/src/macros/events.rs 51 PowRegistrationAllowed +event pallets/subtensor/src/macros/events.rs 52 TempoSet +event pallets/subtensor/src/macros/events.rs 53 RAORecycledForRegistrationSet +event pallets/subtensor/src/macros/events.rs 54 StakeThresholdSet +event pallets/subtensor/src/macros/events.rs 55 AdjustmentAlphaSet +event pallets/subtensor/src/macros/events.rs 56 Faucet +event pallets/subtensor/src/macros/events.rs 57 SubnetOwnerCutSet +event pallets/subtensor/src/macros/events.rs 58 NetworkRateLimitSet +event pallets/subtensor/src/macros/events.rs 59 NetworkImmunityPeriodSet +event pallets/subtensor/src/macros/events.rs 60 StartCallDelaySet +event pallets/subtensor/src/macros/events.rs 61 NetworkMinLockCostSet +event pallets/subtensor/src/macros/events.rs 62 SubnetLimitSet +event pallets/subtensor/src/macros/events.rs 63 NetworkLockCostReductionIntervalSet +event pallets/subtensor/src/macros/events.rs 64 TakeDecreased +event pallets/subtensor/src/macros/events.rs 65 TakeIncreased +event pallets/subtensor/src/macros/events.rs 66 HotkeySwapped +event pallets/subtensor/src/macros/events.rs 67 MaxDelegateTakeSet +event pallets/subtensor/src/macros/events.rs 68 MinDelegateTakeSet +event pallets/subtensor/src/macros/events.rs 69 ColdkeySwapAnnounced +event pallets/subtensor/src/macros/events.rs 70 ColdkeySwapReset +event pallets/subtensor/src/macros/events.rs 71 ColdkeySwapped +event pallets/subtensor/src/macros/events.rs 72 ColdkeySwapDisputed +event pallets/subtensor/src/macros/events.rs 73 AllBalanceUnstakedAndTransferredToNewColdkey +event pallets/subtensor/src/macros/events.rs 74 ArbitrationPeriodExtended +event pallets/subtensor/src/macros/events.rs 75 SetChildrenScheduled +event pallets/subtensor/src/macros/events.rs 76 SetChildren +event pallets/subtensor/src/macros/events.rs 77 ChainIdentitySet +event pallets/subtensor/src/macros/events.rs 78 SubnetIdentitySet +event pallets/subtensor/src/macros/events.rs 79 SubnetIdentityRemoved +event pallets/subtensor/src/macros/events.rs 80 DissolveNetworkScheduled +event pallets/subtensor/src/macros/events.rs 81 ColdkeySwapAnnouncementDelaySet +event pallets/subtensor/src/macros/events.rs 82 ColdkeySwapReannouncementDelaySet +event pallets/subtensor/src/macros/events.rs 83 DissolveNetworkScheduleDurationSet +event pallets/subtensor/src/macros/events.rs 84 CRV3WeightsCommitted +event pallets/subtensor/src/macros/events.rs 85 WeightsCommitted +event pallets/subtensor/src/macros/events.rs 86 WeightsRevealed +event pallets/subtensor/src/macros/events.rs 87 WeightsBatchRevealed +event pallets/subtensor/src/macros/events.rs 88 BatchWeightsCompleted +event pallets/subtensor/src/macros/events.rs 89 BatchCompletedWithErrors +event pallets/subtensor/src/macros/events.rs 90 BatchWeightItemFailed +event pallets/subtensor/src/macros/events.rs 91 StakeTransferred +event pallets/subtensor/src/macros/events.rs 92 StakeSwapped +event pallets/subtensor/src/macros/events.rs 93 TransferToggle +event pallets/subtensor/src/macros/events.rs 94 SubnetOwnerHotkeySet +event pallets/subtensor/src/macros/events.rs 95 FirstEmissionBlockNumberSet +event pallets/subtensor/src/macros/events.rs 96 AlphaRecycled +event pallets/subtensor/src/macros/events.rs 97 AlphaBurned +event pallets/subtensor/src/macros/events.rs 98 EvmKeyAssociated +event pallets/subtensor/src/macros/events.rs 99 CRV3WeightsRevealed +event pallets/subtensor/src/macros/events.rs 100 CommitRevealPeriodsSet +event pallets/subtensor/src/macros/events.rs 101 CommitRevealEnabled +event pallets/subtensor/src/macros/events.rs 102 HotkeySwappedOnSubnet +event pallets/subtensor/src/macros/events.rs 103 SubnetLeaseCreated +event pallets/subtensor/src/macros/events.rs 104 SubnetLeaseTerminated +event pallets/subtensor/src/macros/events.rs 105 SymbolUpdated +event pallets/subtensor/src/macros/events.rs 106 CommitRevealVersionSet +event pallets/subtensor/src/macros/events.rs 107 TimelockedWeightsCommitted +event pallets/subtensor/src/macros/events.rs 108 TimelockedWeightsRevealed +event pallets/subtensor/src/macros/events.rs 109 AutoStakeAdded +event pallets/subtensor/src/macros/events.rs 110 IncentiveAlphaEmittedToMiners +event pallets/subtensor/src/macros/events.rs 111 MinAllowedUidsSet +event pallets/subtensor/src/macros/events.rs 112 AutoStakeDestinationSet +event pallets/subtensor/src/macros/events.rs 113 MinNonImmuneUidsSet +event pallets/subtensor/src/macros/events.rs 114 RootClaimed +event pallets/subtensor/src/macros/events.rs 115 RootClaimTypeSet +event pallets/subtensor/src/macros/events.rs 116 VotingPowerTrackingEnabled +event pallets/subtensor/src/macros/events.rs 117 VotingPowerTrackingDisableScheduled +event pallets/subtensor/src/macros/events.rs 118 VotingPowerTrackingDisabled +event pallets/subtensor/src/macros/events.rs 119 VotingPowerEmaAlphaSet +event pallets/subtensor/src/macros/events.rs 120 SubnetLeaseDividendsDistributed +event pallets/subtensor/src/macros/events.rs 121 AddStakeBurn +event pallets/subtensor/src/macros/events.rs 122 NetworkDissolveCleanupCompleted +event pallets/subtensor/src/macros/events.rs 123 ColdkeySwapCleared +event pallets/subtensor/src/macros/events.rs 124 TransactionFeePaidWithAlpha +event pallets/subtensor/src/macros/events.rs 125 BurnHalfLifeSet +event pallets/subtensor/src/macros/events.rs 126 BurnIncreaseMultSet +event pallets/subtensor/src/macros/events.rs 127 AutoParentDelegationEnabledSet +event pallets/subtensor/src/macros/events.rs 128 StakeLocked +event pallets/subtensor/src/macros/events.rs 129 StakeUnlocked +event pallets/subtensor/src/macros/events.rs 130 LockMoved +event pallets/subtensor/src/macros/events.rs 131 ActivityCutoffFactorMilliSet +event pallets/subtensor/src/macros/events.rs 132 EpochTriggered +event pallets/subtensor/src/macros/events.rs 133 EpochDeferred +event pallets/subtensor/src/macros/events.rs 134 EpochSkipped +event pallets/subtensor/src/macros/events.rs 135 SubnetOwnerChanged +event pallets/subtensor/src/macros/events.rs 136 PerpetualLockUpdated +event pallets/subtensor/src/macros/events.rs 137 NetworkRegistrationQueued +event pallets/subtensor/src/macros/events.rs 138 RejectLockedAlphaUpdated +event pallets/subtensor/src/macros/events.rs 139 StakeAndHotkeyTransferred +event pallets/subtensor/src/macros/events.rs 140 CollateralLocked +event pallets/subtensor/src/macros/events.rs 141 MinCollateralSet +event pallets/swap/src/pallet/mod.rs 0 FeeRateSet +error pallets/swap/src/pallet/mod.rs 0 FeeRateTooHigh +error pallets/swap/src/pallet/mod.rs 1 InsufficientInputAmount +error pallets/swap/src/pallet/mod.rs 2 InsufficientLiquidity +error pallets/swap/src/pallet/mod.rs 3 PriceLimitExceeded +error pallets/swap/src/pallet/mod.rs 4 InsufficientBalance +error pallets/swap/src/pallet/mod.rs 5 InvalidTickRange +error pallets/swap/src/pallet/mod.rs 6 InvalidLiquidityValue +error pallets/swap/src/pallet/mod.rs 7 ReservesTooLow +error pallets/swap/src/pallet/mod.rs 8 MechanismDoesNotExist +error pallets/swap/src/pallet/mod.rs 9 SubtokenDisabled +error pallets/swap/src/pallet/mod.rs 10 ReservesOutOfBalance +error pallets/swap/src/pallet/mod.rs 11 SwapInputTooLarge +error pallets/swap/src/pallet/mod.rs 12 Deprecated +event pallets/utility/src/lib.rs 0 BatchInterrupted +event pallets/utility/src/lib.rs 1 BatchCompleted +event pallets/utility/src/lib.rs 2 BatchCompletedWithErrors +event pallets/utility/src/lib.rs 3 ItemCompleted +event pallets/utility/src/lib.rs 4 ItemFailed +event pallets/utility/src/lib.rs 5 DispatchedAs +event pallets/utility/src/lib.rs 6 IfElseMainSuccess +event pallets/utility/src/lib.rs 7 IfElseFallbackCalled +error pallets/utility/src/lib.rs 0 TooManyCalls +error pallets/utility/src/lib.rs 1 InvalidDerivedAccount +runtime System 0 +runtime RandomnessCollectiveFlip 1 +runtime Timestamp 2 +runtime Aura 3 +runtime Grandpa 4 +runtime Balances 5 +runtime TransactionPayment 6 +runtime SubtensorModule 7 +runtime Utility 11 +runtime Sudo 12 +runtime Multisig 13 +runtime Preimage 14 +runtime Scheduler 15 +runtime Proxy 16 +runtime Commitments 18 +runtime AdminUtils 19 +runtime SafeMode 20 +runtime Ethereum 21 +runtime EVM 22 +runtime EVMChainId 23 +runtime BaseFee 25 +runtime Drand 26 +runtime Crowdloan 27 +runtime Swap 28 +runtime Contracts 29 +runtime MevShield 30 +runtime AlphaAssets 31 +runtime LimitOrders 32 +precompile_index 1026 +precompile_index 1027 +precompile_index 2048 +precompile_index 2049 +precompile_index 2050 +precompile_index 2051 +precompile_index 2051 +precompile_index 2052 +precompile_index 2053 +precompile_index 2054 +precompile_index 2055 +precompile_index 2056 +precompile_index 2057 +precompile_index 2058 +precompile_index 2059 +precompile_index 2060 +precompile_index 2061 +precompile_index 2062 +precompile_selector addProxy(bytes32) +precompile_selector addProxy(bytes32) +precompile_selector addProxy(bytes32,uint8,uint32) +precompile_selector addStake(bytes32,uint256) +precompile_selector addStake(bytes32,uint256,uint256) +precompile_selector addStakeLimit(bytes32,uint256,uint256,bool,uint256) +precompile_selector addressMapping(address) +precompile_selector allowance(address,address,uint256) +precompile_selector approve(address,uint256,uint256) +precompile_selector burnAlpha(bytes32,uint256,uint256) +precompile_selector burnedRegister(uint16,bytes32) +precompile_selector commitWeights(uint16,bytes32) +precompile_selector contribute(uint32,uint64) +precompile_selector create(uint64,uint64,uint64,uint32,address) +precompile_selector createLeaseCrowdloan(uint64,uint64,uint64,uint32,uint8,bool,uint32) +precompile_selector createPureProxy(uint8,uint32,uint16) +precompile_selector decreaseAllowance(address,uint256,uint256) +precompile_selector dissolve(uint32) +precompile_selector finalize(uint32) +precompile_selector getActivityCutoff(uint16) +precompile_selector getActivityCutoffFactor(uint16) +precompile_selector getAdjustmentAlpha(uint16) +precompile_selector getAlphaInEmission(uint16) +precompile_selector getAlphaInPool(uint16) +precompile_selector getAlphaIssuance(uint16) +precompile_selector getAlphaOutEmission(uint16) +precompile_selector getAlphaOutPool(uint16) +precompile_selector getAlphaPrice(uint16) +precompile_selector getAlphaSigmoidSteepness(uint16) +precompile_selector getAlphaStakedValidators(bytes32,uint256) +precompile_selector getAlphaValues(uint16) +precompile_selector getAxon(uint16,uint16) +precompile_selector getBondsMovingAverage(uint16) +precompile_selector getBondsResetEnabled(uint16) +precompile_selector getCKBurn() +precompile_selector getColdkey(uint16,uint16) +precompile_selector getColdkeyLock(bytes32,uint256) +precompile_selector getCommitRevealWeightsEnabled(uint16) +precompile_selector getCommitRevealWeightsInterval(uint16) +precompile_selector getConsensus(uint16,uint16) +precompile_selector getContribution(uint32,bytes32) +precompile_selector getContributorShare(uint32,bytes32) +precompile_selector getCrowdloan(uint32) +precompile_selector getDefaultMinStake() +precompile_selector getDifficulty(uint16) +precompile_selector getDividends(uint16,uint16) +precompile_selector getEMAPriceHalvingBlocks(uint16) +precompile_selector getEmission(uint16,uint16) +precompile_selector getFreeBalance(bytes32) +precompile_selector getHotkey(uint16,uint16) +precompile_selector getHotkeyConvictions(uint256,bytes32[]) +precompile_selector getHotkeyLock(bytes32,uint256) +precompile_selector getImmunityPeriod(uint16) +precompile_selector getIncentive(uint16,uint16) +precompile_selector getIsActive(uint16,uint16) +precompile_selector getKappa(uint16) +precompile_selector getLastUpdate(uint16,uint16) +precompile_selector getLease(uint32) +precompile_selector getLeaseIdForSubnet(uint16) +precompile_selector getLiquidAlphaEnabled(uint16) +precompile_selector getLockRates() +precompile_selector getMaxBurn(uint16) +precompile_selector getMaxDifficulty(uint16) +precompile_selector getMaxWeightLimit(uint16) +precompile_selector getMinAllowedWeights(uint16) +precompile_selector getMinBurn(uint16) +precompile_selector getMinDifficulty(uint16) +precompile_selector getMovingAlphaPrice(uint16) +precompile_selector getNetworkPowRegistrationAllowed(uint16) +precompile_selector getNetworkRegistrationAllowed(uint16) +precompile_selector getNetworkRegistrationBlock(uint16) +precompile_selector getNominatorMinRequiredStake() +precompile_selector getOwnerCutAutoLockEnabled(uint16) +precompile_selector getProxies(bytes32) +precompile_selector getRank(uint16,uint16) +precompile_selector getRejectLockedAlpha(bytes32) +precompile_selector getRho(uint16) +precompile_selector getRootNetuid() +precompile_selector getServingRateLimit(uint16) +precompile_selector getStake(bytes32,bytes32,uint256) +precompile_selector getStake(bytes32,bytes32,uint256) +precompile_selector getStake(uint16,uint16) +precompile_selector getStakeInfoForColdkeyAndNetuid(bytes32,uint256,bytes32[]) +precompile_selector getSubnetMechanism(uint16) +precompile_selector getSubnetVolume(uint16) +precompile_selector getSumAlphaPrice() +precompile_selector getTaoInEmission(uint16) +precompile_selector getTaoInPool(uint16) +precompile_selector getTaoWeight() +precompile_selector getTotalAlphaStaked(bytes32,uint256) +precompile_selector getTotalColdkeyStake(bytes32) +precompile_selector getTotalColdkeyStake(bytes32) +precompile_selector getTotalColdkeyStakeOnSubnet(bytes32,uint256) +precompile_selector getTotalHotkeyStake(bytes32) +precompile_selector getTotalHotkeyStake(bytes32) +precompile_selector getTotalVotingPower(uint16) +precompile_selector getTrust(uint16,uint16) +precompile_selector getUidCount(uint16) +precompile_selector getValidatorStatus(uint16,uint16) +precompile_selector getVotingPower(uint16,bytes32) +precompile_selector getVotingPowerDisableAtBlock(uint16) +precompile_selector getVotingPowerEmaAlpha(uint16) +precompile_selector getVtrust(uint16,uint16) +precompile_selector getWeightsSetRateLimit(uint16) +precompile_selector getWeightsVersionKey(uint16) +precompile_selector getYuma3Enabled(uint16) +precompile_selector increaseAllowance(address,uint256,uint256) +precompile_selector isSubnetDissolving(uint16) +precompile_selector isVotingPowerTrackingEnabled(uint16) +precompile_selector killPureProxy(bytes32,uint8,uint16,uint32,uint32) +precompile_selector lockStake(bytes32,uint256,uint256) +precompile_selector moveLock(bytes32,uint256) +precompile_selector moveStake(bytes32,bytes32,uint256,uint256,uint256) +precompile_selector pokeDeposit() +precompile_selector proxyCall(bytes32,uint8[],uint8[]) +precompile_selector refund(uint32) +precompile_selector registerLimit(uint16,bytes32,uint64) +precompile_selector registerNetwork(bytes32) +precompile_selector removeProxies() +precompile_selector removeProxy(bytes32) +precompile_selector removeProxy(bytes32) +precompile_selector removeProxy(bytes32,uint8,uint32) +precompile_selector removeStake(bytes32,uint256,uint256) +precompile_selector removeStake(bytes32,uint256,uint256) +precompile_selector removeStakeFull(bytes32,uint256) +precompile_selector removeStakeFullLimit(bytes32,uint256,uint256) +precompile_selector removeStakeLimit(bytes32,uint256,uint256,bool,uint256) +precompile_selector revealWeights(uint16,uint16[],uint16[],uint16[],uint64) +precompile_selector serveAxon(uint16,uint32,uint128,uint16,uint8,uint8,uint8,uint8) +precompile_selector servePrometheus(uint16,uint32,uint128,uint16,uint8) +precompile_selector setActivityCutoff(uint16,uint16) +precompile_selector setActivityCutoffFactor(uint16,uint32) +precompile_selector setAdjustmentAlpha(uint16,uint64) +precompile_selector setAlphaSigmoidSteepness(uint16,uint16) +precompile_selector setAlphaValues(uint16,uint16,uint16) +precompile_selector setBondsMovingAverage(uint16,uint64) +precompile_selector setBondsResetEnabled(uint16,bool) +precompile_selector setCommitRevealWeightsEnabled(uint16,bool) +precompile_selector setCommitRevealWeightsInterval(uint16,uint64) +precompile_selector setDifficulty(uint16,uint64) +precompile_selector setImmunityPeriod(uint16,uint16) +precompile_selector setKappa(uint16,uint16) +precompile_selector setLiquidAlphaEnabled(uint16,bool) +precompile_selector setMaxBurn(uint16,uint64) +precompile_selector setMaxDifficulty(uint16,uint64) +precompile_selector setMinAllowedWeights(uint16,uint16) +precompile_selector setMinBurn(uint16,uint64) +precompile_selector setMinDifficulty(uint16,uint64) +precompile_selector setNetworkPowRegistrationAllowed(uint16,bool) +precompile_selector setNetworkRegistrationAllowed(uint16,bool) +precompile_selector setOwnerCutAutoLockEnabled(uint16,bool) +precompile_selector setPerpetualLock(uint256,bool) +precompile_selector setRejectLockedAlpha(bool) +precompile_selector setRho(uint16,uint16) +precompile_selector setServingRateLimit(uint16,uint64) +precompile_selector setWeights(uint16,uint16[],uint16[],uint64) +precompile_selector setWeightsSetRateLimit(uint16,uint64) +precompile_selector setWeightsVersionKey(uint16,uint64) +precompile_selector setYuma3Enabled(uint16,bool) +precompile_selector simSwapAlphaForTao(uint16,uint64) +precompile_selector simSwapTaoForAlpha(uint16,uint64) +precompile_selector terminateLease(uint32,bytes32) +precompile_selector toggleTransfers(uint16,bool) +precompile_selector transfer(bytes32) +precompile_selector transferStake(bytes32,bytes32,uint256,uint256,uint256) +precompile_selector transferStakeFrom(address,address,bytes32,uint256,uint256,uint256) +precompile_selector uidLookup(uint16,address,uint16) +precompile_selector updateCap(uint32,uint64) +precompile_selector updateEnd(uint32,uint32) +precompile_selector updateMinContribution(uint32,uint64) +precompile_selector withdraw(uint32) +rpc pallets/subtensor/rpc/src/lib.rs delegateInfo_getDelegate +rpc pallets/subtensor/rpc/src/lib.rs delegateInfo_getDelegated +rpc pallets/subtensor/rpc/src/lib.rs delegateInfo_getDelegates +rpc pallets/subtensor/rpc/src/lib.rs neuronInfo_getNeuron +rpc pallets/subtensor/rpc/src/lib.rs neuronInfo_getNeuronLite +rpc pallets/subtensor/rpc/src/lib.rs neuronInfo_getNeurons +rpc pallets/subtensor/rpc/src/lib.rs neuronInfo_getNeuronsLite +rpc pallets/subtensor/rpc/src/lib.rs stakeInfo_getColdkeyLock +rpc pallets/subtensor/rpc/src/lib.rs subnetInfo_getAllDynamicInfo +rpc pallets/subtensor/rpc/src/lib.rs subnetInfo_getAllMechagraphs +rpc pallets/subtensor/rpc/src/lib.rs subnetInfo_getAllMetagraphs +rpc pallets/subtensor/rpc/src/lib.rs subnetInfo_getBlockEmission +rpc pallets/subtensor/rpc/src/lib.rs subnetInfo_getColdkeyAutoStakeHotkey +rpc pallets/subtensor/rpc/src/lib.rs subnetInfo_getDynamicInfo +rpc pallets/subtensor/rpc/src/lib.rs subnetInfo_getLockCost +rpc pallets/subtensor/rpc/src/lib.rs subnetInfo_getMechagraph +rpc pallets/subtensor/rpc/src/lib.rs subnetInfo_getMetagraph +rpc pallets/subtensor/rpc/src/lib.rs subnetInfo_getSelectiveMechagraph +rpc pallets/subtensor/rpc/src/lib.rs subnetInfo_getSelectiveMetagraph +rpc pallets/subtensor/rpc/src/lib.rs subnetInfo_getSubnetAccountId +rpc pallets/subtensor/rpc/src/lib.rs subnetInfo_getSubnetHyperparams +rpc pallets/subtensor/rpc/src/lib.rs subnetInfo_getSubnetHyperparamsV2 +rpc pallets/subtensor/rpc/src/lib.rs subnetInfo_getSubnetInfo +rpc pallets/subtensor/rpc/src/lib.rs subnetInfo_getSubnetInfo_v2 +rpc pallets/subtensor/rpc/src/lib.rs subnetInfo_getSubnetState +rpc pallets/subtensor/rpc/src/lib.rs subnetInfo_getSubnetToPrune +rpc pallets/subtensor/rpc/src/lib.rs subnetInfo_getSubnetsInfo +rpc pallets/subtensor/rpc/src/lib.rs subnetInfo_getSubnetsInfo_v2 +rpc pallets/swap/rpc/src/lib.rs swap_currentAlphaPrice +rpc pallets/swap/rpc/src/lib.rs swap_currentAlphaPriceAll +rpc pallets/swap/rpc/src/lib.rs swap_simSwapAlphaForTao +rpc pallets/swap/rpc/src/lib.rs swap_simSwapTaoForAlpha +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_all_dynamic_info +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_all_mechagraphs +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_all_metagraphs +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_block_emission +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_coldkey_auto_stake_hotkey +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_coldkey_lock +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_delegate +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_delegated +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_delegates +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_dynamic_info +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_hotkey_conviction +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_mechagraph +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_metagraph +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_most_convicted_hotkey_on_subnet +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_network_registration_cost +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_neuron +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_neuron_lite +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_neurons +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_neurons_lite +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_next_epoch_start_block +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_proxy_filters +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_proxy_types +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_selective_mechagraph +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_selective_metagraph +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_stake_availability_for_coldkeys +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_stake_fee +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_stake_info_for_coldkey +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_stake_info_for_coldkeys +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_stake_info_for_hotkey_coldkey_netuid +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_subnet_account_id +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_subnet_hyperparams +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_subnet_hyperparams_v2 +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_subnet_hyperparams_v3 +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_subnet_info +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_subnet_info_v2 +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_subnet_state +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_subnet_to_prune +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_subnets_info +runtime_api_fn pallets/subtensor/runtime-api/src/lib.rs get_subnets_info_v2 +runtime_api_fn pallets/swap/runtime-api/src/lib.rs current_alpha_price +runtime_api_fn pallets/swap/runtime-api/src/lib.rs current_alpha_price_all +runtime_api_fn pallets/swap/runtime-api/src/lib.rs sim_swap_alpha_for_tao +runtime_api_fn pallets/swap/runtime-api/src/lib.rs sim_swap_tao_for_alpha +runtime_api_trait pallets/subtensor/runtime-api/src/lib.rs DelegateInfoRuntimeApi +runtime_api_trait pallets/subtensor/runtime-api/src/lib.rs NeuronInfoRuntimeApi +runtime_api_trait pallets/subtensor/runtime-api/src/lib.rs ProxyFilterRuntimeApi +runtime_api_trait pallets/subtensor/runtime-api/src/lib.rs StakeInfoRuntimeApi +runtime_api_trait pallets/subtensor/runtime-api/src/lib.rs SubnetInfoRuntimeApi +runtime_api_trait pallets/subtensor/runtime-api/src/lib.rs SubnetRegistrationRuntimeApi +runtime_api_trait pallets/swap/runtime-api/src/lib.rs SwapRuntimeApi diff --git a/refactor/refactor-manifest.json b/refactor/refactor-manifest.json new file mode 100644 index 0000000000..9036b1c09b --- /dev/null +++ b/refactor/refactor-manifest.json @@ -0,0 +1,1154 @@ +{ + "branch": "refactor/discoverability", + "baseline": "refactor/metadata-baseline.txt", + "conventions": [ + "AGENTS.md", + ".agents/skills/write-discoverable-code/SKILL.md" + ], + "oracle": "scripts/check_metadata_unchanged.sh", + "shards": [ + { + "id": "w1-admin-utils", + "wave": 1, + "task": "discoverability", + "status": "merged", + "notes": "Docs on storage/calls/events/errors; rename private helpers; split files >1000 lines", + "files": [ + "pallets/admin-utils/src/benchmarking.rs", + "pallets/admin-utils/src/lib.rs", + "pallets/admin-utils/src/tests/mock.rs", + "pallets/admin-utils/src/tests/mod.rs" + ] + }, + { + "id": "w1-alpha-assets", + "wave": 1, + "task": "discoverability", + "status": "merged", + "notes": "Docs on storage/calls/events/errors; rename private helpers; split files >1000 lines", + "files": [ + "pallets/alpha-assets/src/lib.rs", + "pallets/alpha-assets/src/mock.rs", + "pallets/alpha-assets/src/tests.rs" + ] + }, + { + "id": "w1-commitments", + "wave": 1, + "task": "discoverability", + "status": "merged", + "notes": "Docs on storage/calls/events/errors; rename private helpers; split files >1000 lines", + "files": [ + "pallets/commitments/src/benchmarking.rs", + "pallets/commitments/src/lib.rs", + "pallets/commitments/src/mock.rs", + "pallets/commitments/src/tests.rs", + "pallets/commitments/src/types.rs" + ] + }, + { + "id": "w1-crowdloan", + "wave": 1, + "task": "discoverability", + "status": "merged", + "notes": "Docs on storage/calls/events/errors; rename private helpers; split files >1000 lines", + "files": [ + "pallets/crowdloan/src/benchmarking.rs", + "pallets/crowdloan/src/lib.rs", + "pallets/crowdloan/src/migrations/migrate_add_contributors_count.rs", + "pallets/crowdloan/src/migrations/mod.rs", + "pallets/crowdloan/src/mock.rs", + "pallets/crowdloan/src/tests.rs" + ] + }, + { + "id": "w1-drand", + "wave": 1, + "task": "discoverability", + "status": "merged", + "notes": "Docs on storage/calls/events/errors; rename private helpers; split files >1000 lines", + "files": [ + "pallets/drand/src/benchmarking.rs", + "pallets/drand/src/bls12_381.rs", + "pallets/drand/src/drand_priority.rs", + "pallets/drand/src/lib.rs", + "pallets/drand/src/migrations/migrate_prune_old_pulses.rs", + "pallets/drand/src/migrations/migrate_set_oldest_round.rs", + "pallets/drand/src/migrations/mod.rs", + "pallets/drand/src/mock.rs", + "pallets/drand/src/tests.rs", + "pallets/drand/src/types.rs", + "pallets/drand/src/utils.rs", + "pallets/drand/src/verifier.rs" + ] + }, + { + "id": "w1-limit-orders", + "wave": 1, + "task": "discoverability", + "status": "merged", + "notes": "Docs on storage/calls/events/errors; rename private helpers; split files >1000 lines", + "files": [ + "pallets/limit-orders/src/benchmarking.rs", + "pallets/limit-orders/src/lib.rs", + "pallets/limit-orders/src/migrations/migrate_register_pallet_hotkey.rs", + "pallets/limit-orders/src/migrations/mod.rs", + "pallets/limit-orders/src/tests/auxiliary.rs", + "pallets/limit-orders/src/tests/extrinsics.rs", + "pallets/limit-orders/src/tests/migration.rs", + "pallets/limit-orders/src/tests/mock.rs", + "pallets/limit-orders/src/tests/mod.rs" + ] + }, + { + "id": "w1-shield", + "wave": 1, + "task": "discoverability", + "status": "merged", + "notes": "Docs on storage/calls/events/errors; rename private helpers; split files >1000 lines", + "files": [ + "pallets/shield/src/benchmarking.rs", + "pallets/shield/src/extension.rs", + "pallets/shield/src/lib.rs", + "pallets/shield/src/migrations/migrate_clear_v1_storage.rs", + "pallets/shield/src/migrations/mod.rs", + "pallets/shield/src/mock.rs", + "pallets/shield/src/tests.rs" + ] + }, + { + "id": "w1-transaction-fee", + "wave": 1, + "task": "discoverability", + "status": "merged", + "notes": "Docs on storage/calls/events/errors; rename private helpers; split files >1000 lines", + "files": [ + "pallets/transaction-fee/src/lib.rs", + "pallets/transaction-fee/src/tests/mock.rs", + "pallets/transaction-fee/src/tests/mod.rs" + ] + }, + { + "id": "w1-proxy", + "wave": 1, + "task": "discoverability", + "status": "merged", + "notes": "Docs on storage/calls/events/errors; rename private helpers; split files >1000 lines", + "files": [ + "pallets/proxy/src/benchmarking.rs", + "pallets/proxy/src/lib.rs", + "pallets/proxy/src/tests.rs" + ] + }, + { + "id": "w1-utility", + "wave": 1, + "task": "discoverability", + "status": "merged", + "notes": "Docs on storage/calls/events/errors; rename private helpers; split files >1000 lines", + "files": [ + "pallets/utility/src/benchmarking.rs", + "pallets/utility/src/lib.rs", + "pallets/utility/src/tests.rs" + ] + }, + { + "id": "w1-swap", + "wave": 1, + "task": "discoverability", + "status": "merged", + "notes": "Pallet swap + rpc + runtime-api; freeze Solidity/RPC strings", + "files": [ + "pallets/swap/rpc/src/lib.rs", + "pallets/swap/runtime-api/src/lib.rs", + "pallets/swap/src/benchmarking.rs", + "pallets/swap/src/lib.rs", + "pallets/swap/src/mock.rs", + "pallets/swap/src/pallet/balancer.rs", + "pallets/swap/src/pallet/hooks.rs", + "pallets/swap/src/pallet/impls.rs", + "pallets/swap/src/pallet/migrations/migrate_swapv3_to_balancer.rs", + "pallets/swap/src/pallet/migrations/mod.rs", + "pallets/swap/src/pallet/mod.rs", + "pallets/swap/src/pallet/swap_step.rs", + "pallets/swap/src/pallet/tests.rs" + ] + }, + { + "id": "w1-node", + "wave": 1, + "task": "discoverability", + "status": "merged", + "notes": "", + "files": [ + "node/build.rs", + "node/src/benchmarking.rs", + "node/src/chain_spec/devnet.rs", + "node/src/chain_spec/finney.rs", + "node/src/chain_spec/localnet.rs", + "node/src/chain_spec/mod.rs", + "node/src/chain_spec/testnet.rs", + "node/src/cli.rs", + "node/src/client.rs", + "node/src/clone_spec.rs", + "node/src/command.rs", + "node/src/conditional_evm_block_import.rs", + "node/src/consensus/aura_consensus.rs", + "node/src/consensus/babe_consensus.rs", + "node/src/consensus/consensus_mechanism.rs", + "node/src/consensus/hybrid_import_queue.rs", + "node/src/consensus/mod.rs", + "node/src/dev_keystore.rs", + "node/src/ethereum.rs", + "node/src/lib.rs", + "node/src/main.rs", + "node/src/rpc.rs", + "node/src/service/grandpa_warp_sync.rs", + "node/src/service.rs", + "node/tests/chain_spec.rs" + ] + }, + { + "id": "w1-common", + "wave": 1, + "task": "discoverability", + "status": "merged", + "notes": "", + "files": [ + "common/src/currency.rs", + "common/src/evm_context.rs", + "common/src/lib.rs", + "common/src/proxy.rs", + "common/src/transaction_error.rs" + ] + }, + { + "id": "w1-primitives", + "wave": 1, + "task": "discoverability", + "status": "merged", + "notes": "", + "files": [ + "primitives/safe-math/src/lib.rs", + "primitives/share-pool/src/lib.rs", + "primitives/swap-interface/src/lib.rs", + "primitives/swap-interface/src/order.rs" + ] + }, + { + "id": "w1-support", + "wave": 1, + "task": "discoverability", + "status": "merged", + "notes": "", + "files": [ + "support/linting/src/forbid_as_primitive.rs", + "support/linting/src/forbid_keys_remove.rs", + "support/linting/src/forbid_saturating_math.rs", + "support/linting/src/lib.rs", + "support/linting/src/lint.rs", + "support/linting/src/pallet_index.rs", + "support/linting/src/require_extrinsic_benchmarks.rs", + "support/linting/src/require_freeze_struct.rs", + "support/macros/src/call_filter_group.rs", + "support/macros/src/lib.rs", + "support/macros/src/visitor.rs", + "support/macros/tests/tests.rs", + "support/procedural-fork/src/benchmark.rs", + "support/procedural-fork/src/construct_runtime/expand/call.rs", + "support/procedural-fork/src/construct_runtime/expand/composite_helper.rs", + "support/procedural-fork/src/construct_runtime/expand/config.rs", + "support/procedural-fork/src/construct_runtime/expand/freeze_reason.rs", + "support/procedural-fork/src/construct_runtime/expand/hold_reason.rs", + "support/procedural-fork/src/construct_runtime/expand/inherent.rs", + "support/procedural-fork/src/construct_runtime/expand/lock_id.rs", + "support/procedural-fork/src/construct_runtime/expand/metadata.rs", + "support/procedural-fork/src/construct_runtime/expand/mod.rs", + "support/procedural-fork/src/construct_runtime/expand/origin.rs", + "support/procedural-fork/src/construct_runtime/expand/outer_enums.rs", + "support/procedural-fork/src/construct_runtime/expand/slash_reason.rs", + "support/procedural-fork/src/construct_runtime/expand/task.rs", + "support/procedural-fork/src/construct_runtime/expand/unsigned.rs", + "support/procedural-fork/src/construct_runtime/mod.rs", + "support/procedural-fork/src/construct_runtime/parse.rs", + "support/procedural-fork/src/crate_version.rs", + "support/procedural-fork/src/derive_impl.rs", + "support/procedural-fork/src/dummy_part_checker.rs", + "support/procedural-fork/src/dynamic_params.rs", + "support/procedural-fork/src/key_prefix.rs", + "support/procedural-fork/src/lib.rs", + "support/procedural-fork/src/match_and_insert.rs", + "support/procedural-fork/src/no_bound/clone.rs", + "support/procedural-fork/src/no_bound/debug.rs", + "support/procedural-fork/src/no_bound/default.rs", + "support/procedural-fork/src/no_bound/mod.rs", + "support/procedural-fork/src/no_bound/ord.rs", + "support/procedural-fork/src/no_bound/partial_eq.rs", + "support/procedural-fork/src/no_bound/partial_ord.rs", + "support/procedural-fork/src/pallet/expand/call.rs", + "support/procedural-fork/src/pallet/expand/composite.rs", + "support/procedural-fork/src/pallet/expand/config.rs", + "support/procedural-fork/src/pallet/expand/constants.rs", + "support/procedural-fork/src/pallet/expand/doc_only.rs", + "support/procedural-fork/src/pallet/expand/documentation.rs", + "support/procedural-fork/src/pallet/expand/error.rs", + "support/procedural-fork/src/pallet/expand/event.rs", + "support/procedural-fork/src/pallet/expand/genesis_build.rs", + "support/procedural-fork/src/pallet/expand/genesis_config.rs", + "support/procedural-fork/src/pallet/expand/hooks.rs", + "support/procedural-fork/src/pallet/expand/inherent.rs", + "support/procedural-fork/src/pallet/expand/instances.rs", + "support/procedural-fork/src/pallet/expand/mod.rs", + "support/procedural-fork/src/pallet/expand/origin.rs", + "support/procedural-fork/src/pallet/expand/pallet_struct.rs", + "support/procedural-fork/src/pallet/expand/storage.rs", + "support/procedural-fork/src/pallet/expand/tasks.rs", + "support/procedural-fork/src/pallet/expand/tt_default_parts.rs", + "support/procedural-fork/src/pallet/expand/type_value.rs", + "support/procedural-fork/src/pallet/expand/validate_unsigned.rs", + "support/procedural-fork/src/pallet/expand/warnings.rs", + "support/procedural-fork/src/pallet/mod.rs", + "support/procedural-fork/src/pallet/parse/call.rs", + "support/procedural-fork/src/pallet/parse/composite.rs", + "support/procedural-fork/src/pallet/parse/config.rs", + "support/procedural-fork/src/pallet/parse/error.rs", + "support/procedural-fork/src/pallet/parse/event.rs", + "support/procedural-fork/src/pallet/parse/extra_constants.rs", + "support/procedural-fork/src/pallet/parse/genesis_build.rs", + "support/procedural-fork/src/pallet/parse/genesis_config.rs", + "support/procedural-fork/src/pallet/parse/helper.rs", + "support/procedural-fork/src/pallet/parse/hooks.rs", + "support/procedural-fork/src/pallet/parse/inherent.rs", + "support/procedural-fork/src/pallet/parse/mod.rs", + "support/procedural-fork/src/pallet/parse/origin.rs", + "support/procedural-fork/src/pallet/parse/pallet_struct.rs", + "support/procedural-fork/src/pallet/parse/storage.rs", + "support/procedural-fork/src/pallet/parse/tasks.rs", + "support/procedural-fork/src/pallet/parse/tests/mod.rs", + "support/procedural-fork/src/pallet/parse/tests/tasks.rs", + "support/procedural-fork/src/pallet/parse/type_value.rs", + "support/procedural-fork/src/pallet/parse/validate_unsigned.rs", + "support/procedural-fork/src/pallet_error.rs", + "support/procedural-fork/src/runtime/expand/mod.rs", + "support/procedural-fork/src/runtime/mod.rs", + "support/procedural-fork/src/runtime/parse/helper.rs", + "support/procedural-fork/src/runtime/parse/mod.rs", + "support/procedural-fork/src/runtime/parse/pallet.rs", + "support/procedural-fork/src/runtime/parse/pallet_decl.rs", + "support/procedural-fork/src/runtime/parse/runtime_struct.rs", + "support/procedural-fork/src/runtime/parse/runtime_types.rs", + "support/procedural-fork/src/storage_alias.rs", + "support/procedural-fork/src/transactional.rs", + "support/procedural-fork/src/tt_macro.rs", + "support/tools/src/bump_version.rs", + "support/tools/src/spec_version.rs", + "support/weight-tools/src/weight_compare.rs" + ] + }, + { + "id": "w1-chain-extensions", + "wave": 1, + "task": "discoverability", + "status": "merged", + "notes": "", + "files": [ + "chain-extensions/src/lib.rs", + "chain-extensions/src/mock.rs", + "chain-extensions/src/tests.rs", + "chain-extensions/src/types.rs" + ] + }, + { + "id": "w1-runtime", + "wave": 1, + "task": "discoverability", + "status": "merged", + "notes": "Internal renames + docs; construct_runtime names/indices frozen", + "files": [ + "runtime/build.rs", + "runtime/src/check_mortality.rs", + "runtime/src/check_nonce.rs", + "runtime/src/fee_filters.rs", + "runtime/src/lib.rs", + "runtime/src/proxy_filters/call_groups.rs", + "runtime/src/proxy_filters/mod.rs", + "runtime/src/sudo_wrapper.rs", + "runtime/src/transaction_payment_wrapper.rs", + "runtime/target/srtool/production/build/cranelift-codegen-2cddad35fb8e85b0/out/inst_builder.rs", + "runtime/target/srtool/production/build/cranelift-codegen-2cddad35fb8e85b0/out/isle_aarch64.rs", + "runtime/target/srtool/production/build/cranelift-codegen-2cddad35fb8e85b0/out/isle_opt.rs", + "runtime/target/srtool/production/build/cranelift-codegen-2cddad35fb8e85b0/out/isle_riscv64.rs", + "runtime/target/srtool/production/build/cranelift-codegen-2cddad35fb8e85b0/out/isle_s390x.rs", + "runtime/target/srtool/production/build/cranelift-codegen-2cddad35fb8e85b0/out/isle_x64.rs", + "runtime/target/srtool/production/build/cranelift-codegen-2cddad35fb8e85b0/out/opcodes.rs", + "runtime/target/srtool/production/build/cranelift-codegen-2cddad35fb8e85b0/out/settings-x86.rs", + "runtime/target/srtool/production/build/cranelift-codegen-2cddad35fb8e85b0/out/settings.rs", + "runtime/target/srtool/production/build/cranelift-codegen-2cddad35fb8e85b0/out/types.rs", + "runtime/target/srtool/production/build/cranelift-codegen-2cddad35fb8e85b0/out/version.rs", + "runtime/target/srtool/production/build/cranelift-isle-7f30b694efe1b515/out/isle_tests.rs", + "runtime/target/srtool/production/build/crunchy-3d9b6d0c9ef24647/out/lib.rs", + "runtime/target/srtool/production/build/crunchy-be4fcb7112d4a763/out/lib.rs", + "runtime/target/srtool/production/build/libsecp256k1-a663f5ad90c4257e/out/const.rs", + "runtime/target/srtool/production/build/libsecp256k1-a663f5ad90c4257e/out/const_gen.rs", + "runtime/target/srtool/production/build/libsecp256k1-d19d4684983a4b86/out/const.rs", + "runtime/target/srtool/production/build/libsecp256k1-d19d4684983a4b86/out/const_gen.rs", + "runtime/target/srtool/production/build/node-subtensor-runtime-2c55dbb56e769d45/out/wasm_binary.rs", + "runtime/target/srtool/production/build/pallet-contracts-602de8f57b15dd9c/out/migration_codegen.rs", + "runtime/target/srtool/production/build/ref-cast-45d77d869fee354d/out/private.rs", + "runtime/target/srtool/production/build/ref-cast-54ab8bdd9fac841e/out/private.rs", + "runtime/target/srtool/production/build/serde-4374c590df191659/out/private.rs", + "runtime/target/srtool/production/build/serde-de0ff9bac41e86dc/out/private.rs", + "runtime/target/srtool/production/build/serde_core-ac3c94688ae619f7/out/private.rs", + "runtime/target/srtool/production/build/serde_core-c201414ec6150cae/out/private.rs", + "runtime/target/srtool/production/build/ss58-registry-2a70b7741933b276/out/registry_gen.rs", + "runtime/target/srtool/production/build/ss58-registry-ca6a51d5eaf741a7/out/registry_gen.rs", + "runtime/target/srtool/production/build/substrate-typenum-83524b60180f4626/out/consts.rs", + "runtime/target/srtool/production/build/substrate-typenum-83524b60180f4626/out/op.rs", + "runtime/target/srtool/production/build/substrate-typenum-83524b60180f4626/out/tests.rs", + "runtime/target/srtool/production/build/target-lexicon-c3cb4d10f814ee45/out/host.rs", + "runtime/target/srtool/production/build/thiserror-d351c35c84bf618e/out/private.rs", + "runtime/target/srtool/production/build/typenum-7ec5b9d5a885d655/out/tests.rs", + "runtime/target/srtool/production/build/typenum-cfde24593ce94e10/out/tests.rs", + "runtime/target/srtool/production/build/wasm-opt-cxx-sys-e95119bc8288e5ff/out/cxxbridge/include/wasm-opt-cxx-sys/src/lib.rs", + "runtime/target/srtool/production/build/wasm-opt-sys-0e9d06c45e25825b/out/cxxbridge/include/wasm-opt-sys/src/lib.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/src/lib.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/cranelift-codegen-6a9645b3967b2a07/out/inst_builder.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/cranelift-codegen-6a9645b3967b2a07/out/isle_aarch64.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/cranelift-codegen-6a9645b3967b2a07/out/isle_opt.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/cranelift-codegen-6a9645b3967b2a07/out/isle_riscv64.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/cranelift-codegen-6a9645b3967b2a07/out/isle_s390x.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/cranelift-codegen-6a9645b3967b2a07/out/isle_x64.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/cranelift-codegen-6a9645b3967b2a07/out/opcodes.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/cranelift-codegen-6a9645b3967b2a07/out/settings-x86.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/cranelift-codegen-6a9645b3967b2a07/out/settings.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/cranelift-codegen-6a9645b3967b2a07/out/types.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/cranelift-codegen-6a9645b3967b2a07/out/version.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/cranelift-isle-f1ace7897cf9d688/out/isle_tests.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/crunchy-2e619343c4d35aea/out/lib.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/libsecp256k1-df84ae29bdbdfda9/out/const.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/libsecp256k1-df84ae29bdbdfda9/out/const_gen.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/ref-cast-ac3c8734623fa6b2/out/private.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/serde-412cf0c221cee1b8/out/private.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/serde_core-663414d267f4c041/out/private.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/ss58-registry-eec055fdab7a1515/out/registry_gen.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/target-lexicon-251773c37c21e27a/out/host.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/thiserror-3e700ece359173ad/out/private.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/typenum-10f78c1c52e7b093/out/tests.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/wasm-opt-cxx-sys-c442ca2e58e96c4d/out/cxxbridge/include/wasm-opt-cxx-sys/src/lib.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/production/build/wasm-opt-sys-6e33e2d20105f4d4/out/cxxbridge/include/wasm-opt-sys/src/lib.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/wasm32v1-none/production/build/crunchy-c894c75edb198e0b/out/lib.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/wasm32v1-none/production/build/libsecp256k1-35fa7f3709a4e166/out/const.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/wasm32v1-none/production/build/libsecp256k1-35fa7f3709a4e166/out/const_gen.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/wasm32v1-none/production/build/pallet-contracts-8c25b059f6a3204f/out/migration_codegen.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/wasm32v1-none/production/build/ref-cast-52ef614fb0282a9c/out/private.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/wasm32v1-none/production/build/serde-58088acb90269010/out/private.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/wasm32v1-none/production/build/serde_core-72a01ce9a933c756/out/private.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/wasm32v1-none/production/build/ss58-registry-a409ac5b3ade2454/out/registry_gen.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/wasm32v1-none/production/build/substrate-typenum-ae2da8bc177820ad/out/consts.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/wasm32v1-none/production/build/substrate-typenum-ae2da8bc177820ad/out/op.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/wasm32v1-none/production/build/substrate-typenum-ae2da8bc177820ad/out/tests.rs", + "runtime/target/srtool/production/wbuild/node-subtensor-runtime/target/wasm32v1-none/production/build/typenum-63b5c65687b19814/out/tests.rs", + "runtime/tests/account_conversion.rs", + "runtime/tests/balances_dust.rs", + "runtime/tests/evm_transaction_fee.rs", + "runtime/tests/ghsa_repro.rs", + "runtime/tests/limit_orders.rs", + "runtime/tests/metadata.rs", + "runtime/tests/precompiles.rs", + "runtime/tests/sudo_wrapper.rs" + ] + }, + { + "id": "w1-precompiles-a", + "wave": 1, + "task": "discoverability", + "status": "merged", + "notes": "INDEX values and #[precompile::public] selectors frozen", + "files": [ + "precompiles/src/address_mapping.rs", + "precompiles/src/alpha.rs", + "precompiles/src/balance.rs", + "precompiles/src/balance_transfer.rs", + "precompiles/src/crowdloan.rs", + "precompiles/src/ed25519.rs", + "precompiles/src/extensions.rs", + "precompiles/src/leasing.rs", + "precompiles/src/lib.rs", + "precompiles/src/metagraph.rs" + ] + }, + { + "id": "w1-precompiles-b", + "wave": 1, + "task": "discoverability", + "status": "merged", + "notes": "INDEX values and #[precompile::public] selectors frozen", + "files": [ + "precompiles/src/mock.rs", + "precompiles/src/neuron.rs", + "precompiles/src/proxy.rs", + "precompiles/src/sr25519.rs", + "precompiles/src/staking.rs", + "precompiles/src/storage_query.rs", + "precompiles/src/subnet.rs", + "precompiles/src/uid_lookup.rs", + "precompiles/src/voting_power.rs" + ] + }, + { + "id": "w2-src-coinbase", + "wave": 2, + "task": "discoverability", + "status": "merged", + "notes": "pallet-subtensor src/coinbase", + "files": [ + "pallets/subtensor/src/coinbase/alpha.rs", + "pallets/subtensor/src/coinbase/block_emission.rs", + "pallets/subtensor/src/coinbase/block_step.rs", + "pallets/subtensor/src/coinbase/mod.rs", + "pallets/subtensor/src/coinbase/reveal_commits.rs", + "pallets/subtensor/src/coinbase/root.rs", + "pallets/subtensor/src/coinbase/run_coinbase/dividend_distribution.rs", + "pallets/subtensor/src/coinbase/run_coinbase/drain_pending_emissions.rs", + "pallets/subtensor/src/coinbase/run_coinbase/emission_injection.rs", + "pallets/subtensor/src/coinbase/run_coinbase/fixed_point.rs", + "pallets/subtensor/src/coinbase/run_coinbase/mod.rs", + "pallets/subtensor/src/coinbase/subnet_emissions.rs", + "pallets/subtensor/src/coinbase/tao.rs", + "pallets/subtensor/src/coinbase/tempo_control.rs" + ] + }, + { + "id": "w2-src-epoch", + "wave": 2, + "task": "discoverability", + "status": "merged", + "notes": "pallet-subtensor src/epoch", + "files": [ + "pallets/subtensor/src/epoch/math/ema_interpolate.rs", + "pallets/subtensor/src/epoch/math/fixed_conversions.rs", + "pallets/subtensor/src/epoch/math/matmul_clip.rs", + "pallets/subtensor/src/epoch/math/matrix_normalize_mask.rs", + "pallets/subtensor/src/epoch/math/mod.rs", + "pallets/subtensor/src/epoch/math/vector_ops.rs", + "pallets/subtensor/src/epoch/math/weighted_median.rs", + "pallets/subtensor/src/epoch/mod.rs", + "pallets/subtensor/src/epoch/run_epoch/bonds_ema_liquid_alpha.rs", + "pallets/subtensor/src/epoch/run_epoch/epoch_dense.rs", + "pallets/subtensor/src/epoch/run_epoch/epoch_mechanism.rs", + "pallets/subtensor/src/epoch/run_epoch/epoch_terms.rs", + "pallets/subtensor/src/epoch/run_epoch/mod.rs", + "pallets/subtensor/src/epoch/run_epoch/persist_epoch_terms.rs", + "pallets/subtensor/src/epoch/run_epoch/weights_bonds_loaders.rs" + ] + }, + { + "id": "w2-src-staking", + "wave": 2, + "task": "discoverability", + "status": "merged", + "notes": "pallet-subtensor src/staking", + "files": [ + "pallets/subtensor/src/staking/account.rs", + "pallets/subtensor/src/staking/add_stake.rs", + "pallets/subtensor/src/staking/claim_root.rs", + "pallets/subtensor/src/staking/decrease_take.rs", + "pallets/subtensor/src/staking/helpers.rs", + "pallets/subtensor/src/staking/increase_take.rs", + "pallets/subtensor/src/staking/lock/conviction_model.rs", + "pallets/subtensor/src/staking/lock/lock_availability.rs", + "pallets/subtensor/src/staking/lock/lock_key_swaps.rs", + "pallets/subtensor/src/staking/lock/lock_operations.rs", + "pallets/subtensor/src/staking/lock/lock_storage.rs", + "pallets/subtensor/src/staking/lock/lock_transfer.rs", + "pallets/subtensor/src/staking/lock/mod.rs", + "pallets/subtensor/src/staking/lock/subnet_conviction.rs", + "pallets/subtensor/src/staking/mod.rs", + "pallets/subtensor/src/staking/move_stake.rs", + "pallets/subtensor/src/staking/order_swap.rs", + "pallets/subtensor/src/staking/recycle_alpha.rs", + "pallets/subtensor/src/staking/remove_stake/destroy_alpha.rs", + "pallets/subtensor/src/staking/remove_stake/mod.rs", + "pallets/subtensor/src/staking/remove_stake/remove_stake_ops.rs", + "pallets/subtensor/src/staking/set_children/childkey_take.rs", + "pallets/subtensor/src/staking/set_children/mod.rs", + "pallets/subtensor/src/staking/set_children/parent_child_relations.rs", + "pallets/subtensor/src/staking/set_children/parent_child_storage.rs", + "pallets/subtensor/src/staking/set_children/repair_children.rs", + "pallets/subtensor/src/staking/set_children/root_validators.rs", + "pallets/subtensor/src/staking/set_children/schedule_children.rs", + "pallets/subtensor/src/staking/stake_utils/alpha_price.rs", + "pallets/subtensor/src/staking/stake_utils/alpha_share_pool.rs", + "pallets/subtensor/src/staking/stake_utils/inherited_stake.rs", + "pallets/subtensor/src/staking/stake_utils/mod.rs", + "pallets/subtensor/src/staking/stake_utils/provided_reserves.rs", + "pallets/subtensor/src/staking/stake_utils/stake_balances.rs", + "pallets/subtensor/src/staking/stake_utils/stake_swap.rs", + "pallets/subtensor/src/staking/stake_utils/stake_validation.rs" + ] + }, + { + "id": "w2-src-subnets", + "wave": 2, + "task": "discoverability", + "status": "merged", + "notes": "pallet-subtensor src/subnets", + "files": [ + "pallets/subtensor/src/subnets/collateral.rs", + "pallets/subtensor/src/subnets/dissolution/mod.rs", + "pallets/subtensor/src/subnets/dissolution/cleanup_status.rs", + "pallets/subtensor/src/subnets/dissolution/phased_cleanup.rs", + "pallets/subtensor/src/subnets/dissolution/purge_network_storage.rs", + "pallets/subtensor/src/subnets/leasing.rs", + "pallets/subtensor/src/subnets/mechanism.rs", + "pallets/subtensor/src/subnets/mod.rs", + "pallets/subtensor/src/subnets/registration.rs", + "pallets/subtensor/src/subnets/serving.rs", + "pallets/subtensor/src/subnets/subnet.rs", + "pallets/subtensor/src/subnets/symbols.rs", + "pallets/subtensor/src/subnets/uids.rs" + ] + }, + { + "id": "w2-src-swap", + "wave": 2, + "task": "discoverability", + "status": "merged", + "notes": "pallet-subtensor src/swap", + "files": [ + "pallets/subtensor/src/swap/coldkey_lineage.rs", + "pallets/subtensor/src/swap/hotkey_lineage.rs", + "pallets/subtensor/src/swap/mod.rs", + "pallets/subtensor/src/swap/swap_coldkey.rs", + "pallets/subtensor/src/swap/swap_hotkey.rs" + ] + }, + { + "id": "w2-src-rpc_info", + "wave": 2, + "task": "discoverability", + "status": "merged", + "notes": "pallet-subtensor src/rpc_info", + "files": [ + "pallets/subtensor/src/rpc_info/delegate_info.rs", + "pallets/subtensor/src/rpc_info/dynamic_info.rs", + "pallets/subtensor/src/rpc_info/metagraph.rs", + "pallets/subtensor/src/rpc_info/mod.rs", + "pallets/subtensor/src/rpc_info/neuron_info.rs", + "pallets/subtensor/src/rpc_info/show_subnet.rs", + "pallets/subtensor/src/rpc_info/stake_info.rs", + "pallets/subtensor/src/rpc_info/subnet_info.rs" + ] + }, + { + "id": "w2-src-utils", + "wave": 2, + "task": "discoverability", + "status": "merged", + "notes": "pallet-subtensor src/utils", + "files": [ + "pallets/subtensor/src/utils/cleanup.rs", + "pallets/subtensor/src/utils/evm.rs", + "pallets/subtensor/src/utils/identity.rs", + "pallets/subtensor/src/utils/misc/consensus_params.rs", + "pallets/subtensor/src/utils/misc/mod.rs", + "pallets/subtensor/src/utils/misc/origin_and_admin.rs", + "pallets/subtensor/src/utils/misc/q32_math.rs", + "pallets/subtensor/src/utils/misc/subnet_hyperparams.rs", + "pallets/subtensor/src/utils/misc/take_and_locks.rs", + "pallets/subtensor/src/utils/misc/tempo_and_counters.rs", + "pallets/subtensor/src/utils/mod.rs", + "pallets/subtensor/src/utils/rate_limiting.rs", + "pallets/subtensor/src/utils/try_state.rs", + "pallets/subtensor/src/utils/voting_power.rs" + ] + }, + { + "id": "w2-src-guards", + "wave": 2, + "task": "discoverability", + "status": "merged", + "notes": "pallet-subtensor src/guards", + "files": [ + "pallets/subtensor/src/guards/check_coldkey_swap.rs", + "pallets/subtensor/src/guards/check_delegate_take.rs", + "pallets/subtensor/src/guards/check_evm_key_association.rs", + "pallets/subtensor/src/guards/check_rate_limits.rs", + "pallets/subtensor/src/guards/check_serving_endpoints.rs", + "pallets/subtensor/src/guards/check_weights.rs", + "pallets/subtensor/src/guards/mod.rs" + ] + }, + { + "id": "w2-src-extensions", + "wave": 2, + "task": "discoverability", + "status": "merged", + "notes": "pallet-subtensor src/extensions", + "files": [ + "pallets/subtensor/src/extensions/mod.rs", + "pallets/subtensor/src/extensions/subtensor.rs" + ] + }, + { + "id": "w2-src-benchmarks", + "wave": 2, + "task": "discoverability", + "status": "merged", + "notes": "pallet-subtensor src/benchmarks", + "files": [ + "pallets/subtensor/src/benchmarks/benchmarks.rs", + "pallets/subtensor/src/benchmarks/helpers.rs" + ] + }, + { + "id": "w2-rpc", + "wave": 2, + "task": "discoverability", + "status": "merged", + "notes": "RPC method strings and runtime API trait/method names frozen", + "files": [ + "pallets/subtensor/rpc/src/lib.rs", + "pallets/subtensor/runtime-api/src/lib.rs" + ] + }, + { + "id": "w2-docs-storage", + "wave": 2, + "task": "docs-only", + "status": "merged", + "notes": "DOCS ONLY on #[pallet::storage] items; do not rename types", + "files": [ + "pallets/subtensor/src/lib.rs" + ] + }, + { + "id": "w2-docs-dispatches", + "wave": 2, + "task": "docs-only", + "status": "merged", + "notes": "DOCS ONLY; call names and call_index frozen", + "files": [ + "pallets/subtensor/src/macros/dispatches.rs" + ] + }, + { + "id": "w2-docs-events", + "wave": 2, + "task": "docs-only", + "status": "merged", + "notes": "DOCS ONLY; variant order and names frozen", + "files": [ + "pallets/subtensor/src/macros/events.rs" + ] + }, + { + "id": "w2-docs-errors", + "wave": 2, + "task": "docs-only", + "status": "merged", + "notes": "DOCS ONLY; variant order and names frozen", + "files": [ + "pallets/subtensor/src/macros/errors.rs" + ] + }, + { + "id": "w2-docs-migrations", + "wave": 2, + "task": "docs-only", + "status": "merged", + "notes": "DOCS ONLY; migration name strings frozen", + "files": [ + "pallets/subtensor/src/migrations/migrate_associated_evm_address_index.rs", + "pallets/subtensor/src/migrations/migrate_auto_stake_destination.rs", + "pallets/subtensor/src/migrations/migrate_cleanup_swap_v3.rs", + "pallets/subtensor/src/migrations/migrate_clear_deprecated_registration_maps.rs", + "pallets/subtensor/src/migrations/migrate_clear_orphan_subnet_identities_v3.rs", + "pallets/subtensor/src/migrations/migrate_coldkey_collateral_hotkeys.rs", + "pallets/subtensor/src/migrations/migrate_coldkey_swap_scheduled.rs", + "pallets/subtensor/src/migrations/migrate_coldkey_swap_scheduled_to_announcements.rs", + "pallets/subtensor/src/migrations/migrate_commit_reveal_settings.rs", + "pallets/subtensor/src/migrations/migrate_commit_reveal_v2.rs", + "pallets/subtensor/src/migrations/migrate_create_root_network.rs", + "pallets/subtensor/src/migrations/migrate_crv3_commits_add_block.rs", + "pallets/subtensor/src/migrations/migrate_crv3_v2_to_timelocked.rs", + "pallets/subtensor/src/migrations/migrate_delete_subnet_21.rs", + "pallets/subtensor/src/migrations/migrate_delete_subnet_3.rs", + "pallets/subtensor/src/migrations/migrate_disable_commit_reveal.rs", + "pallets/subtensor/src/migrations/migrate_dynamic_tempo.rs", + "pallets/subtensor/src/migrations/migrate_fix_bad_hk_swap.rs", + "pallets/subtensor/src/migrations/migrate_fix_childkeys.rs", + "pallets/subtensor/src/migrations/migrate_fix_is_network_member.rs", + "pallets/subtensor/src/migrations/migrate_fix_root_claimed_overclaim.rs", + "pallets/subtensor/src/migrations/migrate_fix_root_subnet_tao.rs", + "pallets/subtensor/src/migrations/migrate_fix_root_tao_and_alpha_in.rs", + "pallets/subtensor/src/migrations/migrate_fix_staking_hot_keys.rs", + "pallets/subtensor/src/migrations/migrate_fix_subnet_hotkey_lock_swaps.rs", + "pallets/subtensor/src/migrations/migrate_fix_total_issuance_evm_fees.rs", + "pallets/subtensor/src/migrations/migrate_init_tao_flow.rs", + "pallets/subtensor/src/migrations/migrate_init_total_issuance.rs", + "pallets/subtensor/src/migrations/migrate_kappa_map_to_default.rs", + "pallets/subtensor/src/migrations/migrate_network_immunity_period.rs", + "pallets/subtensor/src/migrations/migrate_network_lock_cost_2500.rs", + "pallets/subtensor/src/migrations/migrate_network_lock_reduction_interval.rs", + "pallets/subtensor/src/migrations/migrate_orphaned_storage_items.rs", + "pallets/subtensor/src/migrations/migrate_pending_emissions.rs", + "pallets/subtensor/src/migrations/migrate_populate_locking_coldkeys.rs", + "pallets/subtensor/src/migrations/migrate_populate_owned_hotkeys.rs", + "pallets/subtensor/src/migrations/migrate_rao.rs", + "pallets/subtensor/src/migrations/migrate_rate_limit_keys.rs", + "pallets/subtensor/src/migrations/migrate_rate_limiting_last_blocks.rs", + "pallets/subtensor/src/migrations/migrate_remove_add_stake_burn_rate_limit.rs", + "pallets/subtensor/src/migrations/migrate_remove_commitments_rate_limit.rs", + "pallets/subtensor/src/migrations/migrate_remove_deprecated_conviction_maps.rs", + "pallets/subtensor/src/migrations/migrate_remove_network_modality.rs", + "pallets/subtensor/src/migrations/migrate_remove_old_identity_maps.rs", + "pallets/subtensor/src/migrations/migrate_remove_stake_map.rs", + "pallets/subtensor/src/migrations/migrate_remove_tao_dividends.rs", + "pallets/subtensor/src/migrations/migrate_remove_total_hotkey_coldkey_stakes_this_interval.rs", + "pallets/subtensor/src/migrations/migrate_remove_unknown_neuron_axon_cert_prom.rs", + "pallets/subtensor/src/migrations/migrate_remove_unused_maps_and_values.rs", + "pallets/subtensor/src/migrations/migrate_remove_zero_total_hotkey_alpha.rs", + "pallets/subtensor/src/migrations/migrate_reset_bonds_moving_average.rs", + "pallets/subtensor/src/migrations/migrate_reset_max_burn.rs", + "pallets/subtensor/src/migrations/migrate_reset_tnet_conviction_locks.rs", + "pallets/subtensor/src/migrations/migrate_reset_unactive_sn.rs", + "pallets/subtensor/src/migrations/migrate_set_first_emission_block_number.rs", + "pallets/subtensor/src/migrations/migrate_set_min_burn.rs", + "pallets/subtensor/src/migrations/migrate_set_min_difficulty.rs", + "pallets/subtensor/src/migrations/migrate_set_nominator_min_stake.rs", + "pallets/subtensor/src/migrations/migrate_set_registration_enable.rs", + "pallets/subtensor/src/migrations/migrate_set_subtoken_enabled.rs", + "pallets/subtensor/src/migrations/migrate_stake_threshold.rs", + "pallets/subtensor/src/migrations/migrate_subnet_balances.rs", + "pallets/subtensor/src/migrations/migrate_subnet_limit_to_default.rs", + "pallets/subtensor/src/migrations/migrate_subnet_locked.rs", + "pallets/subtensor/src/migrations/migrate_subnet_symbols.rs", + "pallets/subtensor/src/migrations/migrate_subnet_volume.rs", + "pallets/subtensor/src/migrations/migrate_tao_in_refund_deployment_block.rs", + "pallets/subtensor/src/migrations/migrate_to_v1_separate_emission.rs", + "pallets/subtensor/src/migrations/migrate_to_v2_fixed_total_stake.rs", + "pallets/subtensor/src/migrations/migrate_total_issuance.rs", + "pallets/subtensor/src/migrations/migrate_transfer_ownership_to_foundation.rs", + "pallets/subtensor/src/migrations/migrate_upgrade_revealed_commitments.rs", + "pallets/subtensor/src/migrations/mod.rs" + ] + }, + { + "id": "w2-docs-macros-other", + "wave": 2, + "task": "discoverability", + "status": "merged", + "notes": "Docs + safe internal renames in remaining macros", + "files": [ + "pallets/subtensor/src/macros/config.rs", + "pallets/subtensor/src/macros/genesis.rs", + "pallets/subtensor/src/macros/hooks.rs", + "pallets/subtensor/src/macros/mod.rs" + ] + }, + { + "id": "w2-test-weights", + "wave": 2, + "task": "split-and-name", + "status": "merged", + "notes": "Split ~6248-line test file into concept-named modules under tests/weights/", + "files": [ + "pallets/subtensor/src/tests/weights/mod.rs", + "pallets/subtensor/src/tests/weights/helpers.rs", + "pallets/subtensor/src/tests/weights/set_weights.rs", + "pallets/subtensor/src/tests/weights/weight_checks.rs", + "pallets/subtensor/src/tests/weights/commit_reveal.rs", + "pallets/subtensor/src/tests/weights/commit_reveal_timing.rs", + "pallets/subtensor/src/tests/weights/batch_reveal.rs", + "pallets/subtensor/src/tests/weights/timelocked_commit.rs", + "pallets/subtensor/src/tests/weights/timelocked_reveal.rs", + "pallets/subtensor/src/tests/weights/timelocked_reveal_hotkey.rs", + "pallets/subtensor/src/tests/weights/owner_permit.rs" + ] + }, + { + "id": "w2-test-staking", + "wave": 2, + "task": "split-and-name", + "status": "merged", + "notes": "Split ~6006-line test file into concept-named modules under tests/staking/ mirroring staking/", + "files": [ + "pallets/subtensor/src/tests/staking/mod.rs", + "pallets/subtensor/src/tests/staking/add_stake.rs", + "pallets/subtensor/src/tests/staking/add_stake_limit.rs", + "pallets/subtensor/src/tests/staking/remove_stake.rs", + "pallets/subtensor/src/tests/staking/remove_stake_limit.rs", + "pallets/subtensor/src/tests/staking/unstake.rs", + "pallets/subtensor/src/tests/staking/move_stake.rs", + "pallets/subtensor/src/tests/staking/delegate_take.rs", + "pallets/subtensor/src/tests/staking/helpers.rs", + "pallets/subtensor/src/tests/staking/stake_utils.rs", + "pallets/subtensor/src/tests/staking/sharepool.rs" + ] + }, + { + "id": "w2-test-migration", + "wave": 2, + "task": "split-and-name", + "status": "merged", + "notes": "Split ~5167-line migration tests into concept modules under tests/migration/", + "files": [ + "pallets/subtensor/src/tests/migration/mod.rs", + "pallets/subtensor/src/tests/migration/prelude.rs", + "pallets/subtensor/src/tests/migration/helpers.rs", + "pallets/subtensor/src/tests/migration/associated_evm_address_index.rs", + "pallets/subtensor/src/tests/migration/auto_stake_destination.rs", + "pallets/subtensor/src/tests/migration/commit_reveal.rs", + "pallets/subtensor/src/tests/migration/conviction_and_tempo.rs", + "pallets/subtensor/src/tests/migration/fix_bad_hk_swap_genesis.rs", + "pallets/subtensor/src/tests/migration/fix_bad_hk_swap_mainnet.rs", + "pallets/subtensor/src/tests/migration/fix_root_claimed.rs", + "pallets/subtensor/src/tests/migration/fix_staking_and_root_tao.rs", + "pallets/subtensor/src/tests/migration/fix_subnet_hotkey_lock_swaps.rs", + "pallets/subtensor/src/tests/migration/network_modality_and_locks.rs", + "pallets/subtensor/src/tests/migration/populate_locking_coldkeys.rs", + "pallets/subtensor/src/tests/migration/rate_limit_keys.rs", + "pallets/subtensor/src/tests/migration/remove_unused_storage.rs", + "pallets/subtensor/src/tests/migration/reset_unactive_sn.rs", + "pallets/subtensor/src/tests/migration/subnet_balances_and_issuance.rs", + "pallets/subtensor/src/tests/migration/subnet_volume_emission_flags.rs", + "pallets/subtensor/src/tests/migration/swap_cleanup.rs", + "pallets/subtensor/src/tests/migration/transfer_and_delete_subnets.rs" + ] + }, + { + "id": "w2-test-children", + "wave": 2, + "task": "split-and-name", + "status": "merged", + "notes": "Split ~4705-line test file into concept-named modules under tests/children/", + "files": [ + "pallets/subtensor/src/tests/children/mod.rs", + "pallets/subtensor/src/tests/children/helpers.rs", + "pallets/subtensor/src/tests/children/schedule_singular.rs", + "pallets/subtensor/src/tests/children/schedule_multiple.rs", + "pallets/subtensor/src/tests/children/pending_children.rs", + "pallets/subtensor/src/tests/children/childkey_take.rs", + "pallets/subtensor/src/tests/children/inherited_stake.rs", + "pallets/subtensor/src/tests/children/child_weights.rs", + "pallets/subtensor/src/tests/children/child_emission.rs", + "pallets/subtensor/src/tests/children/child_dividends.rs", + "pallets/subtensor/src/tests/children/root_validators.rs" + ] + }, + { + "id": "w2-test-coinbase", + "wave": 2, + "task": "split-and-name", + "status": "merged", + "notes": "Split ~4705-line test file into concept-named modules under tests/coinbase/", + "files": [ + "pallets/subtensor/src/tests/coinbase/mod.rs", + "pallets/subtensor/src/tests/coinbase/helpers.rs", + "pallets/subtensor/src/tests/coinbase/prelude.rs", + "pallets/subtensor/src/tests/coinbase/tao_issuance.rs", + "pallets/subtensor/src/tests/coinbase/moving_price.rs", + "pallets/subtensor/src/tests/coinbase/alpha_issuance.rs", + "pallets/subtensor/src/tests/coinbase/owner_cut.rs", + "pallets/subtensor/src/tests/coinbase/pending_emission.rs", + "pallets/subtensor/src/tests/coinbase/drain_emission.rs", + "pallets/subtensor/src/tests/coinbase/root_children_drain.rs", + "pallets/subtensor/src/tests/coinbase/incentive_burn.rs", + "pallets/subtensor/src/tests/coinbase/dividend_distribution.rs", + "pallets/subtensor/src/tests/coinbase/distribute_emission.rs", + "pallets/subtensor/src/tests/coinbase/run_coinbase_lifecycle.rs", + "pallets/subtensor/src/tests/coinbase/incentive_autostake.rs", + "pallets/subtensor/src/tests/coinbase/mining_emission.rs", + "pallets/subtensor/src/tests/coinbase/subnet_terms.rs", + "pallets/subtensor/src/tests/coinbase/inject_and_swap.rs", + "pallets/subtensor/src/tests/coinbase/drain_pending_epoch.rs", + "pallets/subtensor/src/tests/coinbase/emit_to_subnets.rs", + "pallets/subtensor/src/tests/coinbase/root_proportion.rs", + "pallets/subtensor/src/tests/coinbase/epoch_cap_deferral.rs", + "pallets/subtensor/src/tests/coinbase/alpha_dividends.rs" + ] + }, + { + "id": "w2-test-locks", + "wave": 2, + "task": "split-and-name", + "status": "merged", + "notes": "Split ~5087-line test file into concept-named modules under tests/locks/", + "files": [ + "pallets/subtensor/src/tests/locks/mod.rs", + "pallets/subtensor/src/tests/locks/helpers.rs", + "pallets/subtensor/src/tests/locks/prelude.rs", + "pallets/subtensor/src/tests/locks/account_flags_reject_locked_alpha.rs", + "pallets/subtensor/src/tests/locks/lock_stake_creation.rs", + "pallets/subtensor/src/tests/locks/lock_queries.rs", + "pallets/subtensor/src/tests/locks/lock_topup.rs", + "pallets/subtensor/src/tests/locks/lock_rejection.rs", + "pallets/subtensor/src/tests/locks/conviction_roll_forward.rs", + "pallets/subtensor/src/tests/locks/unstake_lock_invariant.rs", + "pallets/subtensor/src/tests/locks/move_transfer_lock.rs", + "pallets/subtensor/src/tests/locks/multi_subnet_locks.rs", + "pallets/subtensor/src/tests/locks/hotkey_conviction_subnet_king.rs", + "pallets/subtensor/src/tests/locks/force_reduce_lock.rs", + "pallets/subtensor/src/tests/locks/coldkey_swap_lock.rs", + "pallets/subtensor/src/tests/locks/hotkey_swap_lock.rs", + "pallets/subtensor/src/tests/locks/lock_stake_extrinsic.rs", + "pallets/subtensor/src/tests/locks/recycle_burn_lock.rs", + "pallets/subtensor/src/tests/locks/subnet_dissolution_lock.rs", + "pallets/subtensor/src/tests/locks/clear_small_nomination_lock.rs", + "pallets/subtensor/src/tests/locks/emission_lock.rs", + "pallets/subtensor/src/tests/locks/neuron_replacement_lock.rs", + "pallets/subtensor/src/tests/locks/moving_lock.rs" + ] + }, + { + "id": "w2-test-epoch", + "wave": 2, + "task": "split-and-name", + "status": "merged", + "notes": "Split ~3973-line test file into concept-named modules under tests/epoch/", + "files": [ + "pallets/subtensor/src/tests/epoch/mod.rs", + "pallets/subtensor/src/tests/epoch/helpers.rs", + "pallets/subtensor/src/tests/epoch/graph_epochs.rs", + "pallets/subtensor/src/tests/epoch/bonds.rs", + "pallets/subtensor/src/tests/epoch/liquid_alpha.rs", + "pallets/subtensor/src/tests/epoch/active_stake.rs", + "pallets/subtensor/src/tests/epoch/weight_activity.rs", + "pallets/subtensor/src/tests/epoch/validator_permits.rs", + "pallets/subtensor/src/tests/epoch/epoch_timing.rs", + "pallets/subtensor/src/tests/epoch/self_weight.rs", + "pallets/subtensor/src/tests/epoch/epoch_outputs.rs", + "pallets/subtensor/src/tests/epoch/yuma_3.rs", + "pallets/subtensor/src/tests/epoch/snipe_weight_mask.rs", + "pallets/subtensor/src/tests/epoch/epoch_input_state.rs" + ] + }, + { + "id": "w2-test-networks", + "wave": 2, + "task": "split-and-name", + "status": "merged", + "notes": "Split ~3863-line test file into concept-named modules under tests/networks/", + "files": [ + "pallets/subtensor/src/tests/networks/mod.rs", + "pallets/subtensor/src/tests/networks/prelude.rs", + "pallets/subtensor/src/tests/networks/helpers.rs", + "pallets/subtensor/src/tests/networks/dissolve_refunds.rs", + "pallets/subtensor/src/tests/networks/dissolve_storage_cleanup.rs", + "pallets/subtensor/src/tests/networks/dissolve_async_cleanup.rs", + "pallets/subtensor/src/tests/networks/destroy_alpha_stakes.rs", + "pallets/subtensor/src/tests/networks/prune_network.rs", + "pallets/subtensor/src/tests/networks/register_network.rs", + "pallets/subtensor/src/tests/networks/median_subnet_alpha_price.rs", + "pallets/subtensor/src/tests/networks/registered_subnet_counter.rs", + "pallets/subtensor/src/tests/networks/migrate_network_immunity.rs", + "pallets/subtensor/src/tests/networks/set_new_network_state.rs", + "pallets/subtensor/src/tests/networks/network_registration_queue.rs", + "pallets/subtensor/src/tests/networks/massive_dissolve_reregistration.rs", + "pallets/subtensor/src/tests/networks/tempo_rate_limit.rs" + ] + }, + { + "id": "w2-test-swap_hotkey_with_subnet", + "wave": 2, + "task": "split-and-name", + "status": "merged", + "notes": "Split ~3251-line test file into concept-named modules under tests/swap_hotkey_with_subnet/", + "files": [ + "pallets/subtensor/src/tests/swap_hotkey_with_subnet/mod.rs", + "pallets/subtensor/src/tests/swap_hotkey_with_subnet/owner_identity.rs", + "pallets/subtensor/src/tests/swap_hotkey_with_subnet/membership_serve.rs", + "pallets/subtensor/src/tests/swap_hotkey_with_subnet/stake_transfer.rs", + "pallets/subtensor/src/tests/swap_hotkey_with_subnet/parent_child_maps.rs", + "pallets/subtensor/src/tests/swap_hotkey_with_subnet/rate_limits.rs", + "pallets/subtensor/src/tests/swap_hotkey_with_subnet/revert_swap.rs", + "pallets/subtensor/src/tests/swap_hotkey_with_subnet/root_claims.rs" + ] + }, + { + "id": "w2-test-math", + "wave": 2, + "task": "split-and-name", + "status": "merged", + "notes": "Split ~2601-line test file into concept-named modules under tests/math/", + "files": [ + "pallets/subtensor/src/tests/math/mod.rs", + "pallets/subtensor/src/tests/math/helpers.rs", + "pallets/subtensor/src/tests/math/fixed_conversions.rs", + "pallets/subtensor/src/tests/math/vector_ops.rs", + "pallets/subtensor/src/tests/math/matrix_normalize_mask.rs", + "pallets/subtensor/src/tests/math/matmul_clip.rs", + "pallets/subtensor/src/tests/math/weighted_median.rs", + "pallets/subtensor/src/tests/math/ema_interpolate.rs" + ] + }, + { + "id": "w2-test-remainder", + "wave": 2, + "task": "discoverability", + "status": "merged", + "notes": "Wire mod.rs after giant splits land; improve smaller test modules", + "files": [ + "pallets/subtensor/src/tests/mod.rs", + "pallets/subtensor/src/tests/auto_stake_hotkey.rs", + "pallets/subtensor/src/tests/batch_tx.rs", + "pallets/subtensor/src/tests/claim_root.rs", + "pallets/subtensor/src/tests/cleanup_tests.rs", + "pallets/subtensor/src/tests/coldkey_lineage.rs", + "pallets/subtensor/src/tests/consensus.rs", + "pallets/subtensor/src/tests/delegate_info.rs", + "pallets/subtensor/src/tests/destroy_alpha_tests.rs", + "pallets/subtensor/src/tests/dissolution.rs", + "pallets/subtensor/src/tests/emission.rs", + "pallets/subtensor/src/tests/ensure.rs", + "pallets/subtensor/src/tests/epoch_logs.rs", + "pallets/subtensor/src/tests/evm.rs", + "pallets/subtensor/src/tests/hotkey_lineage.rs", + "pallets/subtensor/src/tests/leasing.rs", + "pallets/subtensor/src/tests/mechanism.rs", + "pallets/subtensor/src/tests/mock.rs", + "pallets/subtensor/src/tests/mock_high_ed.rs", + "pallets/subtensor/src/tests/move_stake.rs", + "pallets/subtensor/src/tests/neuron_info.rs", + "pallets/subtensor/src/tests/recycle_alpha.rs", + "pallets/subtensor/src/tests/registration.rs", + "pallets/subtensor/src/tests/remove_data_tests.rs", + "pallets/subtensor/src/tests/serving.rs", + "pallets/subtensor/src/tests/staking2.rs", + "pallets/subtensor/src/tests/subnet.rs", + "pallets/subtensor/src/tests/subnet_emissions.rs", + "pallets/subtensor/src/tests/subnet_info.rs", + "pallets/subtensor/src/tests/swap_coldkey.rs", + "pallets/subtensor/src/tests/swap_hotkey.rs", + "pallets/subtensor/src/tests/tao.rs", + "pallets/subtensor/src/tests/tempo_control.rs", + "pallets/subtensor/src/tests/uids.rs", + "pallets/subtensor/src/tests/voting_power.rs" + ] + }, + { + "id": "w3-rename-queue", + "wave": 3, + "task": "cross-cutting", + "status": "merged", + "notes": "Process rename-proposals.md serially; path-agnostic precompile fingerprint", + "files": [ + "refactor/rename-proposals.md", + "scripts/extract_metadata_fingerprint.py", + "refactor/metadata-baseline.txt" + ] + }, + { + "id": "w3-glossary", + "wave": 3, + "task": "cross-cutting", + "status": "merged", + "notes": "Glossary consistency pass; finalize AGENTS.md", + "files": [ + "AGENTS.md", + ".agents/skills/write-discoverable-code/SKILL.md" + ] + } + ] +} diff --git a/refactor/rename-proposals.md b/refactor/rename-proposals.md new file mode 100644 index 0000000000..cb613e9ad5 --- /dev/null +++ b/refactor/rename-proposals.md @@ -0,0 +1,438 @@ +# Cross-shard rename proposals + +Symbols whose `rg -w OldName` hits span more than one shard's owned files. +Wave 3 processes this queue serially. + +Format: + +``` +## OldName -> NewName +- reason: +- hits: (paste `rg -w OldName -g '*.rs' -g '!target/**' -g '!vendor/**' -l` output) +- proposed by: +- status: pending +``` + +--- + +## do_proxy -> dispatch_filtered_proxy_call +- reason: private helper that dispatches a call as the real account under ProxyType filters; name `do_proxy` is vague and collides with the `proxy` extrinsic in search results +- hits: + - pallets/proxy/src/impls.rs + - pallets/proxy/src/lib.rs + - pallets/subtensor/src/guards/check_coldkey_swap.rs (comment only) +- proposed by: w1-proxy +- status: done (w3) + +## weight_and_dispatch_class -> batch_calls_weight_and_pays +- reason: Private batch weight helper; name should include domain word `batch` and clarify it returns `(Weight, Pays)` not a dispatch class. Hits outside w1-utility are string fixtures in the linting crate. +- hits: +``` +pallets/utility/src/lib.rs +support/linting/src/require_extrinsic_benchmarks/tests.rs +``` +- proposed by: w1-utility +- status: done (w3) +- note (w1-support): fixture path updated after splitting `require_extrinsic_benchmarks.rs` → `require_extrinsic_benchmarks/` + +## staking.rs / subnet.rs file splits (precompile path fingerprint) +- reason: `extract_metadata_fingerprint.py` records precompile INDEX/selectors with source file paths. Splitting `staking.rs` → `staking/mod.rs` (+ `legacy_v1.rs`) or `subnet.rs` → `subnet/mod.rs` changes the fingerprint even when INDEX values and Solidity selectors are unchanged. Wave-3 (or a coordinated baseline refresh) should either (a) make precompile fingerprint paths module-name based / path-agnostic, or (b) re-split these files and rewrite `refactor/metadata-baseline.txt` in the same commit. Both files remain >1000 lines after this shard's docs/renames. +- hits: +``` +precompiles/src/staking.rs +precompiles/src/subnet.rs +scripts/extract_metadata_fingerprint.py +refactor/metadata-baseline.txt +``` +- proposed by: w1-precompiles-b +- status: done (w3) — fingerprint made path-agnostic; baseline refreshed. Optional file splits deferred (time). + +## CommitmentsI -> CommitmentsPurgeBridge +- reason: runtime adapter that forwards `purge_netuid` into pallet_commitments; `CommitmentsI` is an opaque abbreviation. Same name is re-declared in several test mocks. +- hits: +``` +runtime/src/lib.rs +eco-tests/src/mock.rs +chain-extensions/src/mock.rs +precompiles/src/mock.rs +pallets/transaction-fee/src/tests/mock.rs +``` +- proposed by: w1-runtime +- status: done (w3) + +## GrandpaInterfaceImpl -> GrandpaAuthorityInterface +- reason: mirror the clearer `AuraAuthorityInterface` naming for the admin-utils Grandpa bridge. +- hits: +``` +runtime/src/lib.rs +pallets/admin-utils/src/tests/mock.rs +``` +- proposed by: w1-runtime +- status: done (w3) + +## TempoInterface -> SubtensorTempoBridge +- reason: runtime/commitments tempo lookup via Subtensor epoch index; name collides with trait-shaped helpers in pallet mocks. +- hits: +``` +runtime/src/lib.rs +pallets/subtensor/src/tests/mock.rs +pallets/commitments/src/mock.rs +pallets/commitments/src/lib.rs +``` +- proposed by: w1-runtime +- status: done (w3) — Config associated type + runtime/mock wiring; `MockTempoInterface` / `GetTempoInterface` kept. + +## applicable_call -> subtensor_call_if +- reason: shared guard helper that yields a Subtensor `Call` when a predicate matches; name should include the domain word `subtensor` and read as a filter, not a boolean check. Used from both guards and the transaction extension. +- hits: +``` +pallets/subtensor/src/guards/mod.rs +pallets/subtensor/src/guards/check_delegate_take.rs +pallets/subtensor/src/guards/check_evm_key_association.rs +pallets/subtensor/src/guards/check_rate_limits.rs +pallets/subtensor/src/guards/check_serving_endpoints.rs +pallets/subtensor/src/guards/check_weights.rs +pallets/subtensor/src/extensions/subtensor.rs +``` +- proposed by: w2-src-guards +- status: done (w3) + +## guards::CallOf -> GuardsRuntimeCallOf +- reason: short `CallOf` alias collides in search with `pallet::CallOf`, extensions' local `CallOf`, and transaction-fee's `CallOf`; guards-specific name would disambiguate. Only the `pub(crate)` alias in `guards/mod.rs` (and its guard call sites) — not the pallet-module or other crates' aliases. +- hits: +``` +pallets/subtensor/src/guards/mod.rs +pallets/subtensor/src/guards/check_coldkey_swap.rs +pallets/subtensor/src/guards/check_delegate_take.rs +pallets/subtensor/src/guards/check_evm_key_association.rs +pallets/subtensor/src/guards/check_rate_limits.rs +pallets/subtensor/src/guards/check_serving_endpoints.rs +pallets/subtensor/src/guards/check_weights.rs +``` +- note: `rg -w CallOf` also hits unrelated same-named aliases in `lib.rs` (pallet module), `extensions/subtensor.rs`, and `transaction-fee`; do not rename those under this proposal. +- proposed by: w2-src-guards +- status: done (w3) + +## ensure_sn_owner_or_root_with_limits -> ensure_subnet_owner_or_root_with_limits +- reason: `sn` abbreviation is opaque next to sibling `ensure_subnet_owner_or_root`; spell out `subnet`. +- hits: +``` +pallets/subtensor/src/utils/misc/origin_and_admin.rs +pallets/admin-utils/src/lib.rs +pallets/subtensor/src/tests/ensure.rs +``` +- proposed by: w2-src-utils +- status: done (w3) + +## record_owner_rl -> record_owner_rate_limits +- reason: `rl` abbreviation hides that this stamps [`TransactionType`] last-block markers after owner admin calls. +- hits: +``` +pallets/subtensor/src/utils/misc/origin_and_admin.rs +pallets/admin-utils/src/lib.rs +``` +- proposed by: w2-src-utils +- status: done (w3) + +## uid_lookup -> associated_uids_for_evm_key +- reason: name does not say EVM reverse-index lookup; collides with the `UidLookup` precompile module name in search results. +- hits: +``` +pallets/subtensor/src/utils/evm.rs +pallets/subtensor/src/lib.rs +pallets/subtensor/src/migrations/migrate_associated_evm_address_index.rs +pallets/admin-utils/src/tests/uids_validators.rs +precompiles/src/uid_lookup.rs +precompiles/src/lib.rs +``` +- proposed by: w2-src-utils +- status: deferred (w3) — would rename / collide with precompile module `uid_lookup`; skip per wave-3 instructions. + +## get_shares -> subnet_emission_shares +- reason: `get_shares` is opaque at search time; name should say these are per-subnet TAO emission weights (price-EMA + miner-burn). Used from coinbase and tests. +- hits: +``` +pallets/subtensor/src/coinbase/subnet_emissions.rs +pallets/subtensor/src/tests/subnet_emissions.rs +``` +- proposed by: w2-src-coinbase +- status: done (w3) + +## inject_and_maybe_swap -> inject_pool_liquidity_and_swap_excess +- reason: clarifies that this materializes tao_in/alpha_in into the pool and swaps excess_tao for protocol alpha; "maybe" hides the always-attempted swap path when excess > 0. +- hits: +``` +pallets/subtensor/src/coinbase/run_coinbase/emission_injection.rs +pallets/subtensor/src/tests/coinbase.rs +``` +- proposed by: w2-src-coinbase +- status: done (w3) + +## get_subnet_terms -> compute_subnet_emission_terms +- reason: "terms" alone is ambiguous; this splits block TAO emission into tao_in/alpha_in/alpha_out/excess_tao per the dTAO injection cap. +- hits: +``` +pallets/subtensor/src/coinbase/run_coinbase/emission_injection.rs +pallets/subtensor/src/tests/coinbase.rs +``` +- proposed by: w2-src-coinbase +- status: done (w3) + +## drain_pending -> drain_pending_subnet_emissions +- reason: `drain_pending` does not say what is drained; this takes pending server/validator/root/owner alpha on epoch fire. +- hits: +``` +pallets/subtensor/src/coinbase/run_coinbase/drain_pending_emissions.rs +pallets/subtensor/src/coinbase/run_coinbase/mod.rs +pallets/subtensor/src/tests/coinbase.rs +``` +- proposed by: w2-src-coinbase +- status: done (w3) + +## get_network_root_sell_flag -> should_accumulate_root_alpha_dividends +- reason: boolean name should read as a predicate; "root sell" is jargon for whether root alpha divs are accumulated vs recycled when total EMA price ≤ 1. +- hits: +``` +pallets/subtensor/src/coinbase/run_coinbase/emission_injection.rs +pallets/subtensor/src/coinbase/run_coinbase/mod.rs +pallets/subtensor/src/tests/coinbase.rs +pallets/subtensor/src/tests/claim_root.rs +``` +- proposed by: w2-src-coinbase +- status: done (w3) + +## fixed -> i32f32_from_f32 +- reason: bare `fixed` collides with countless unrelated hits; this helper is specifically `f32` → epoch `I32F32`. +- hits: +``` +pallets/subtensor/src/epoch/math/fixed_conversions.rs +pallets/subtensor/src/tests/epoch/ +pallets/subtensor/src/tests/math.rs +``` +- proposed by: w2-src-epoch +- status: deferred (w3) — mechanical `\bfixed\b` has too many false positives (fixed-point prose, other crates); needs call-site-aware rename. + +## vecdiv -> elementwise_safe_div +- reason: opaque abbreviation; performs element-wise `safe_div` of two `I32F32` vectors (0 divisor → 0). +- hits: +``` +pallets/subtensor/src/epoch/math/vector_ops.rs +pallets/subtensor/src/tests/math.rs +``` +- proposed by: w2-src-epoch +- status: done (w3) + +## is_epoch_input_state_consistent -> epoch_keys_have_unique_hotkeys +- reason: name does not say what is checked (duplicate hotkeys in `Keys`); used from coinbase preflight and tests. +- hits: +``` +pallets/subtensor/src/epoch/run_epoch/bonds_ema_liquid_alpha.rs +pallets/subtensor/src/coinbase/run_coinbase.rs +pallets/subtensor/src/tests/epoch/ +pallets/subtensor/src/tests/coinbase.rs +``` +- proposed by: w2-src-epoch +- status: done (w3) + +## do_reset_bonds -> reset_bonds_column_for_hotkey +- reason: `do_` prefix mimics dispatchables; this clears one hotkey column in `Bonds` when bonds-reset is enabled. Hits runtime + tests. +- hits: +``` +pallets/subtensor/src/epoch/run_epoch/bonds_ema_liquid_alpha.rs +runtime/src/lib.rs +pallets/subtensor/src/tests/epoch/ +``` +- proposed by: w2-src-epoch +- status: done (w3) + +## get_weights_sparse -> unnormalized_weights_sparse +- reason: parallel to in-shard `unnormalized_bonds_sparse`; clarifies storage weights are not row-normalized. Hits tests outside shard. +- hits: +``` +pallets/subtensor/src/epoch/run_epoch/weights_bonds_loaders.rs +pallets/subtensor/src/tests/epoch/ +pallets/subtensor/src/tests/mechanism.rs +pallets/subtensor/src/tests/weights.rs +``` +- proposed by: w2-src-epoch +- status: done (w3) + +## if_subnet_exist -> subnet_exists +- reason: `if_` prefix reads as a statement; boolean helpers elsewhere use `is_`/`*_exists`. Hits span many shards (dispatches, staking, rpc_info, runtime, admin-utils). +- hits: +``` +pallets/subtensor/src/subnets/subnet.rs +pallets/subtensor/src/macros/dispatches.rs +pallets/subtensor/src/lib.rs +pallets/subtensor/src/coinbase/root.rs +pallets/subtensor/src/coinbase/tempo_control.rs +pallets/subtensor/src/staking/*.rs +pallets/subtensor/src/rpc_info/*.rs +pallets/subtensor/src/swap/swap_hotkey.rs +pallets/subtensor/src/utils/*.rs +pallets/admin-utils/src/lib.rs +runtime/src/lib.rs +chain-extensions/src/lib.rs +pallets/subtensor/src/tests/*.rs +pallets/subtensor/src/migrations/*.rs +``` +- proposed by: w2-src-subnets +- status: done (w3) + +## get_netuid -> netuid_from_mechanism_storage_index +- reason: bare `get_netuid` hides that the argument is a packed mechanism [`NetUidStorageIndex`], not a raw netuid lookup. +- hits: +``` +pallets/subtensor/src/subnets/mechanism.rs +pallets/subtensor/src/utils/misc/consensus_params.rs +``` +- proposed by: w2-src-subnets +- status: done (w3) + +## set_element_at -> set_vec_element_at +- reason: generic helper used when clearing/replacing neuron vectors; name should say it mutates a slice/vec slot. +- hits: +``` +pallets/subtensor/src/subnets/uids.rs +pallets/subtensor/src/tests/uids.rs +``` +- proposed by: w2-src-subnets +- status: done (w3) + +## is_uid_exist_on_network -> uid_exists_on_network +- reason: grammar (`is_uid_exist`); weights module (same directory but outside this shard's file list) also calls it. +- hits: +``` +pallets/subtensor/src/subnets/uids.rs +pallets/subtensor/src/subnets/weights.rs +``` +- proposed by: w2-src-subnets +- status: done (w3) + +## is_subnet_account_id -> netuid_for_subnet_account +- reason: returns `Option`, not a bool; name should not start with `is_`. +- hits: +``` +pallets/subtensor/src/subnets/subnet.rs +pallets/subtensor/src/utils/misc/subnet_hyperparams.rs +pallets/subtensor/src/swap/swap_hotkey.rs +pallets/subtensor/src/staking/helpers.rs +pallets/subtensor/src/macros/errors.rs +runtime/tests/account_conversion.rs +pallets/subtensor/src/tests/subnet.rs +``` +- proposed by: w2-src-subnets +- status: done (w3) + +## do_swap_coldkey -> perform_coldkey_swap +- reason: `do_` prefix is opaque next to the `swap_coldkey` extrinsic; name should say it performs the coldkey identity migration body (not a dispatchable). +- hits: +``` +pallets/subtensor/src/swap/swap_coldkey.rs +pallets/subtensor/src/swap/mod.rs +pallets/subtensor/src/macros/dispatches.rs +pallets/subtensor/src/tests/swap_coldkey.rs +pallets/subtensor/src/tests/coldkey_lineage.rs +pallets/subtensor/src/tests/claim_root.rs +pallets/subtensor/src/tests/locks.rs +``` +- proposed by: w2-src-swap +- status: done (w3) + +## do_swap_hotkey -> perform_hotkey_swap +- reason: same as coldkey — extrinsic body helper; `do_swap_hotkey` collides with extrinsic search and does not say "identity rename". +- hits: +``` +pallets/subtensor/src/swap/swap_hotkey.rs +pallets/subtensor/src/swap/mod.rs +pallets/subtensor/src/macros/dispatches.rs +pallets/subtensor/src/staking/claim_root.rs +pallets/subtensor/src/tests/swap_hotkey.rs +pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs +pallets/subtensor/src/tests/hotkey_lineage.rs +pallets/subtensor/src/tests/locks.rs +``` +- proposed by: w2-src-swap +- status: done (w3) + +## charge_swap_cost -> charge_coldkey_swap_cost +- reason: helper only recycles the coldkey-swap fee; name should include `coldkey` so it does not read as a generic/hotkey fee charger. +- hits: +``` +pallets/subtensor/src/swap/swap_coldkey.rs +pallets/subtensor/src/macros/dispatches.rs +``` +- proposed by: w2-src-swap +- status: done (w3) + +## swap_hotkey_v2_dispatch_weight -> hotkey_swap_dispatch_weight +- reason: `v2` is a call-site version tag, not what the helper computes; name should lead with `hotkey_swap` and say it returns pre-dispatch `Weight`. +- hits: +``` +pallets/subtensor/src/swap/swap_hotkey.rs +pallets/subtensor/src/macros/dispatches.rs +``` +- proposed by: w2-src-swap +- status: done (w3) + +## return_per_1000_tao_test -> delegator_return_per_1000_tao_test +- reason: test wrapper for private `delegator_return_per_1000_tao`; keep the domain word `delegator` so it greps with the production helper (renamed in-shard). +- hits: +``` +pallets/subtensor/src/rpc_info/delegate_info.rs +pallets/subtensor/src/tests/delegate_info.rs +``` +- proposed by: w2-src-rpc_info +- status: done (w3) + +## maybe_coldkey_for_hotkey -> owning_coldkey_for_hotkey_if_set +- reason: name does not say this is an `Owner` lookup returning `Option`; used from transaction-payment fee path. +- hits: +``` +pallets/subtensor/src/rpc_info/delegate_info.rs +runtime/src/transaction_payment_wrapper.rs +``` +- proposed by: w2-src-rpc_info +- status: done (w3) + +## extensions/subtensor.rs module -> extensions/transaction_extension.rs +- reason: module name `subtensor` under `extensions` is redundant with the crate path and hides that the file owns [`SubtensorTransactionExtension`]. Rename would change the rustdoc/SDK path `pallet_subtensor::extensions::subtensor::…`. +- hits: +``` +pallets/subtensor/src/extensions/mod.rs +pallets/subtensor/src/extensions/subtensor.rs +sdk/python/tests/fixtures/shape_corpus/corpus.json +``` +- proposed by: w2-src-extensions +- status: deferred (w3) — touches SDK golden fixtures / rustdoc path; risky for this wave. +- note: `SubtensorTransactionExtension` type name and `IDENTIFIER` string stay frozen (extrinsic format / runtime wiring in `runtime/src/lib.rs`, `node/src/benchmarking.rs`, tests). + +## SubtensorCustom -> SubtensorCustomRpc +- reason: handler type name does not say it is the JSON-RPC adapter; `Custom` alone is opaque next to runtime-api crates named `subtensor-custom-rpc*`. +- hits: +``` +pallets/subtensor/rpc/src/lib.rs +node/src/rpc.rs +``` +- proposed by: w2-rpc +- status: done (w3) + +## SubtensorCustomApi -> SubtensorCustomRpcApi +- reason: jsonrpsee RPC trait; align with `SubtensorCustomRpc` and make `rg SubtensorCustomRpc` find trait + handler + generated `*Server`. Hits `SubtensorCustomApiServer` in node wiring. +- hits: +``` +pallets/subtensor/rpc/src/lib.rs +node/src/rpc.rs +``` +- proposed by: w2-rpc +- status: done (w3) — also renamed generated `SubtensorCustomApiServer` → `SubtensorCustomRpcApiServer`. + +## clean_up_hotkey_swap_records -> purge_expired_hotkey_swap_on_netuid_records +- reason: `pub(crate)` hook helper that removes stale `LastHotkeySwapOnNetuid` rows for the block's netuid slot; name should lead with purge/expired and the storage concept so it greps with the map. +- hits: +``` +pallets/subtensor/src/macros/hooks.rs +pallets/subtensor/src/tests/remove_data_tests.rs +``` +- proposed by: w2-docs-macros-other +- status: done (w3) diff --git a/runtime/build.rs b/runtime/build.rs index c0fa0405b2..9ce0116bed 100644 --- a/runtime/build.rs +++ b/runtime/build.rs @@ -1,3 +1,8 @@ +//! Builds the runtime WASM blob when compiling with `std`. +//! +//! With the `metadata-hash` feature, embeds a metadata hash for token `TAO` +//! (9 decimals) so clients can verify metadata against the runtime binary. + fn main() { #[cfg(all(feature = "std", not(feature = "metadata-hash")))] { diff --git a/runtime/src/check_mortality.rs b/runtime/src/check_mortality.rs index 2ec5233ed1..35a5963667 100644 --- a/runtime/src/check_mortality.rs +++ b/runtime/src/check_mortality.rs @@ -1,3 +1,11 @@ +//! Mortality / era check with a short-period cap for shield encrypted txs. +//! +//! Drop-in replacement for [`frame_system::CheckMortality`] in +//! [`SystemTxExtension`](crate::SystemTxExtension). Shares +//! `IDENTIFIER = "CheckMortality"` and identical SCALE encoding; only +//! [`pallet_shield::Call::submit_encrypted`] is additionally restricted to a +//! mortal era ≤ [`MAX_SHIELD_ERA_PERIOD`]. + use codec::{Decode, DecodeWithMemTracking, Encode}; use core::marker::PhantomData; use frame_support::pallet_prelude::TypeInfo; diff --git a/runtime/src/check_nonce.rs b/runtime/src/check_nonce.rs index 7ec9488d0d..7ced210d7f 100644 --- a/runtime/src/check_nonce.rs +++ b/runtime/src/check_nonce.rs @@ -1,5 +1,10 @@ -// Customized from the original implementation in the Polkadot SDK. -// https://github.com/paritytech/polkadot-sdk/blob/b600af050d6b6c8da59ae2a2a793ee2d8827ab1e/substrate/frame/system/src/extensions/check_nonce.rs +//! Account nonce check for the signed transaction extension pipeline. +//! +//! Customized from the Polkadot SDK +//! ([`check_nonce`](https://github.com/paritytech/polkadot-sdk/blob/b600af050d6b6c8da59ae2a2a793ee2d8827ab1e/substrate/frame/system/src/extensions/check_nonce.rs)): +//! declares DbWeight for the account read/mutate pair and refunds it for +//! non-signed origins. `IDENTIFIER = "CheckNonce"` and compact nonce encoding +//! match stock FRAME so clients stay compatible. use codec::{Decode, DecodeWithMemTracking, Encode}; use frame_support::{ diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index d30d7cf41a..d4a3e6a120 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1,3 +1,26 @@ +//! # node-subtensor runtime +//! +//! Composes FRAME pallets into the Bittensor/Subtensor chain runtime: staking and +//! subnets (`SubtensorModule`), TAO↔alpha swap, EVM/Frontier, proxies, commitments, +//! crowdloan, shield, contracts, and limit orders. +//! +//! ## Frozen surfaces +//! +//! - `construct_runtime!` pallet **names and indices** are wire-stable (Twox128 +//! storage prefixes). Never rename or renumber them. +//! - Signed extension `IDENTIFIER` strings and SCALE layouts in +//! [`check_mortality`], [`check_nonce`], [`sudo_wrapper`], and +//! [`transaction_payment_wrapper`] are client-facing. +//! +//! ## Search anchors +//! +//! | Module / type | Role | +//! |---------------|------| +//! | `proxy_filters` | ProxyType allow-lists + runtime API metadata | +//! | `fee_filters` | Extrinsics whose fees charge the hotkey's coldkey | +//! | `Migrations` | On-upgrade migration tuple wired into [`Executive`] | +//! | [`SystemTxExtension`] / [`CustomTxExtension`] | Signed tx validation pipeline | + #![cfg_attr(not(feature = "std"), no_std)] // `construct_runtime!` does a lot of recursion and requires us to increase the limit to 256. #![recursion_limit = "256"] @@ -132,6 +155,7 @@ impl frame_system::offchain::SigningTypes for Runtime { type Signature = Signature; } +/// Resolves the current / next-next Aura author for MevShield key announcement. pub struct FindAuraAuthors; impl pallet_shield::FindAuthors for FindAuraAuthors { fn find_current_author() -> Option { @@ -270,6 +294,10 @@ parameter_types! { pub const SS58Prefix: u8 = 42; } +/// Rejects Utility batch calls that nest another Utility batch (one level only). +/// +/// Combined with SafeMode in `BaseCallFilter` so nested `batch` / `batch_all` / +/// `force_batch` cannot amplify dispatch depth through Utility. pub struct NoNestingCallFilter; impl Contains for NoNestingCallFilter { @@ -433,6 +461,7 @@ parameter_types! { pub const DisallowPermissionlessRelease: Option = None; } +/// Calls still dispatchable while SafeMode is entered (sudo, multisig, weights, …). pub struct SafeModeWhitelistedCalls; impl Contains for SafeModeWhitelistedCalls { fn contains(call: &RuntimeCall) -> bool { @@ -498,8 +527,7 @@ impl pallet_balances::Config for Runtime { impl pallet_alpha_assets::Config for Runtime {} -// Implement AuthorshipInfo trait for Runtime to satisfy pallet transaction -// fee OnUnbalanced trait bounds +/// Looks up the Aura block author account from the current digest (fee credit sink). pub struct BlockAuthorFromAura(core::marker::PhantomData); impl> BlockAuthorFromAura { @@ -600,8 +628,9 @@ impl pallet_proxy::Config for Runtime { type BlockNumberProvider = System; } -pub struct Proxier; -impl ProxyInterface for Proxier { +/// Adds/removes `SubnetLeaseBeneficiary` proxy delegates for leased subnets. +pub struct LeaseBeneficiaryProxy; +impl ProxyInterface for LeaseBeneficiaryProxy { fn add_lease_beneficiary_proxy(lease: &AccountId, beneficiary: &AccountId) -> DispatchResult { pallet_proxy::Pallet::::add_proxy_delegate( lease, @@ -624,8 +653,9 @@ impl ProxyInterface for Proxier { } } -pub struct CommitmentsI; -impl CommitmentsInterface for CommitmentsI { +/// Forwards subnet commitment purge into `pallet_commitments` on network dissolve. +pub struct CommitmentsPurgeBridge; +impl CommitmentsInterface for CommitmentsPurgeBridge { fn purge_netuid( netuid: NetUid, weight_meter: &mut frame_support::weights::WeightMeter, @@ -700,7 +730,8 @@ parameter_types! { pub const CommitmentFieldDeposit: Balance = TaoBalance::ZERO; // Free } -#[subtensor_macros::freeze_struct("7c76bd954afbb54e")] +/// Max metadata fields per commitment (`MaxCommitFieldsInner`). +#[subtensor_macros::freeze_struct("7da615b380d4c27b")] #[derive(Clone, Eq, PartialEq, Encode, Decode, TypeInfo)] pub struct MaxCommitFields; impl Get for MaxCommitFields { @@ -709,12 +740,13 @@ impl Get for MaxCommitFields { } } -#[subtensor_macros::freeze_struct("c39297f5eb97ee82")] -pub struct AllowCommitments; -impl CanCommit for AllowCommitments { +/// Allows commitment only when `address` is a hotkey registered on an existing subnet. +#[subtensor_macros::freeze_struct("9ec6b2e99dec0ac9")] +pub struct RegisteredHotkeyCanCommit; +impl CanCommit for RegisteredHotkeyCanCommit { #[cfg(not(feature = "runtime-benchmarks"))] fn can_commit(netuid: NetUid, address: &AccountId) -> bool { - SubtensorModule::if_subnet_exist(netuid) + SubtensorModule::subnet_exists(netuid) && SubtensorModule::is_hotkey_registered_on_network(netuid, address) } @@ -724,6 +756,7 @@ impl CanCommit for AllowCommitments { } } +/// On metadata commit, resets validator bonds targeting the committing hotkey (per mechanism). pub struct ResetBondsOnCommit; impl OnMetadataCommitment for ResetBondsOnCommit { #[cfg(not(feature = "runtime-benchmarks"))] @@ -732,7 +765,7 @@ impl OnMetadataCommitment for ResetBondsOnCommit { let mechanism_count = SubtensorModule::get_current_mechanism_count(netuid); for mecid in 0..>::from(mechanism_count) { let netuid_index = SubtensorModule::get_mechanism_storage_index(netuid, mecid.into()); - let _ = SubtensorModule::do_reset_bonds(netuid_index, address); + let _ = SubtensorModule::reset_bonds_column_for_hotkey(netuid_index, address); } } @@ -740,8 +773,9 @@ impl OnMetadataCommitment for ResetBondsOnCommit { fn on_metadata_commitment(_: NetUid, _: &AccountId) {} } -pub struct GetCommitmentsStruct; -impl GetCommitments for GetCommitmentsStruct { +/// Reads per-subnet commitment blobs from `pallet_commitments` for Subtensor RPCs. +pub struct SubnetCommitmentsLookup; +impl GetCommitments for SubnetCommitmentsLookup { fn get_commitments(netuid: NetUid) -> Vec<(AccountId, Vec)> { pallet_commitments::Pallet::::get_commitments(netuid) } @@ -751,17 +785,18 @@ impl pallet_commitments::Config for Runtime { type Currency = Balances; type WeightInfo = pallet_commitments::weights::SubstrateWeight; - type CanCommit = AllowCommitments; + type CanCommit = RegisteredHotkeyCanCommit; type OnMetadataCommitment = ResetBondsOnCommit; type MaxFields = MaxCommitFields; type InitialDeposit = CommitmentInitialDeposit; type FieldDeposit = CommitmentFieldDeposit; - type TempoInterface = TempoInterface; + type SubtensorTempoBridge = SubtensorTempoBridge; } -pub struct TempoInterface; -impl pallet_commitments::GetTempoInterface for TempoInterface { +/// Maps `(netuid, block)` to the subnet epoch index via Subtensor tempo. +pub struct SubtensorTempoBridge; +impl pallet_commitments::GetTempoInterface for SubtensorTempoBridge { fn get_epoch_index(netuid: NetUid, cur_block: u64) -> u64 { SubtensorModule::get_epoch_index(netuid, cur_block) } @@ -931,11 +966,11 @@ impl pallet_subtensor::Config for Runtime { type SwapInterface = Swap; type KeySwapOnSubnetCost = SubtensorInitialKeySwapOnSubnetCost; type HotkeySwapOnSubnetInterval = HotkeySwapOnSubnetInterval; - type ProxyInterface = Proxier; + type ProxyInterface = LeaseBeneficiaryProxy; type LeaseDividendsDistributionInterval = LeaseDividendsDistributionInterval; - type GetCommitments = GetCommitmentsStruct; + type GetCommitments = SubnetCommitmentsLookup; type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; - type CommitmentsInterface = CommitmentsI; + type CommitmentsInterface = CommitmentsPurgeBridge; type AlphaAssets = AlphaAssets; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; type AuthorshipProvider = BlockAuthorFromAura; @@ -984,15 +1019,17 @@ use crate::sudo_wrapper::SudoTransactionExtension; use crate::transaction_payment_wrapper::ChargeTransactionPaymentWrapper; use sp_runtime::BoundedVec; -pub struct AuraPalletIntrf; -impl pallet_admin_utils::AuraInterface> for AuraPalletIntrf { +/// Admin-utils bridge: applies Aura authority set changes from sudo/governance. +pub struct AuraAuthorityInterface; +impl pallet_admin_utils::AuraInterface> for AuraAuthorityInterface { fn change_authorities(new: BoundedVec>) { Aura::change_authorities(new); } } -pub struct GrandpaInterfaceImpl; -impl pallet_admin_utils::GrandpaInterface for GrandpaInterfaceImpl { +/// Admin-utils bridge: schedules Grandpa authority set changes. +pub struct GrandpaAuthorityInterface; +impl pallet_admin_utils::GrandpaInterface for GrandpaAuthorityInterface { fn schedule_change( next_authorities: Vec<(pallet_grandpa::AuthorityId, u64)>, in_blocks: BlockNumber, @@ -1005,8 +1042,8 @@ impl pallet_admin_utils::GrandpaInterface for GrandpaInterfaceImpl { impl pallet_admin_utils::Config for Runtime { type AuthorityId = AuraId; type MaxAuthorities = ConstU32<32>; - type Aura = AuraPalletIntrf; - type Grandpa = GrandpaInterfaceImpl; + type Aura = AuraAuthorityInterface; + type Grandpa = GrandpaAuthorityInterface; type Balance = Balance; type WeightInfo = pallet_admin_utils::weights::SubstrateWeight; } @@ -1069,6 +1106,7 @@ parameter_types! { /// difference factor is 9 decimals, or 10^9 const EVM_TO_SUBSTRATE_DECIMALS: u64 = 1_000_000_000_u64; +/// Converts between Substrate 9-decimal TAO balances and EVM 18-decimal Wei amounts. pub struct SubtensorEvmBalanceConverter; impl BalanceConverter for SubtensorEvmBalanceConverter { @@ -1302,6 +1340,7 @@ parameter_types! { pub const LimitOrdersMaxOrdersPerBatch: u32 = 100; } +/// Deterministic pallet account used as the limit-orders system hotkey. pub struct LimitOrdersPalletHotkey; impl Get for LimitOrdersPalletHotkey { fn get() -> AccountId { @@ -1367,9 +1406,9 @@ parameter_types! { pub const ContractMaxDelegateDependencies: u32 = 32; } +/// Contracts may only dispatch `Proxy::proxy` (no direct Subtensor/Balances calls). pub struct ContractCallFilter; -/// Whitelist dispatchables that are allowed to be called from contracts impl Contains for ContractCallFilter { fn contains(call: &RuntimeCall) -> bool { match call { @@ -1416,6 +1455,9 @@ impl pallet_contracts::Config for Runtime { } // Create the runtime by composing the FRAME pallets that were previously configured. +// +// FROZEN: pallet names and numeric indices below are on-chain storage prefixes. +// Removing a pallet leaves a hole; never reuse an index for a different pallet. construct_runtime!( pub struct Runtime { @@ -1464,7 +1506,8 @@ pub type Address = sp_runtime::MultiAddress; pub type Header = generic::Header; // Block type as expected by this runtime. pub type Block = generic::Block; -// The extensions to the basic transaction logic. + +/// Stock FRAME checks plus Subtensor mortality/nonce (era cap for shield txs). pub type SystemTxExtension = ( frame_system::CheckNonZeroSender, frame_system::CheckSpecVersion, @@ -1474,6 +1517,7 @@ pub type SystemTxExtension = ( check_nonce::CheckNonce, frame_system::CheckWeight, ); +/// Fee routing, sudo signer gate, shield validity, Subtensor guards, Drand priority. pub type CustomTxExtension = ( ChargeTransactionPaymentWrapper, SudoTransactionExtension, @@ -1481,15 +1525,18 @@ pub type CustomTxExtension = ( pallet_subtensor::SubtensorTransactionExtension, pallet_drand::drand_priority::DrandPriority, ); +/// Full signed-extension pipeline attached to every extrinsic. pub type TxExtension = ( SystemTxExtension, CustomTxExtension, frame_metadata_hash_extension::CheckMetadataHash, ); +/// On-upgrade migrations run by [`Executive`] after each runtime upgrade. +/// +/// Kept permanent: re-syncs total issuance each upgrade so tiny floating-point +/// rounding drift (fractions of a cent) cannot accumulate. type Migrations = ( - // Leave this migration in the runtime, so every runtime upgrade tiny rounding errors (fractions of fractions - // of a cent) are cleaned up. These tiny rounding errors occur due to floating point coversion. pallet_subtensor::migrations::migrate_init_total_issuance::initialise_total_issuance::Migration< Runtime, >, diff --git a/runtime/src/proxy_filters/mod.rs b/runtime/src/proxy_filters/mod.rs index d0a356770d..3a2253364b 100644 --- a/runtime/src/proxy_filters/mod.rs +++ b/runtime/src/proxy_filters/mod.rs @@ -1,3 +1,12 @@ +//! Proxy-type allow-lists for `pallet_subtensor_proxy`. +//! +//! Policy lives here as additive unions of inventory groups from +//! [`call_groups`]. [`proxy_type_filter`] is the executable gate used by +//! [`InstanceFilter`]; [`get_proxy_filters`] / [`get_all_proxy_type_infos`] +//! expose the same sets to runtime APIs so clients cannot drift from on-chain +//! checks. Deprecated proxies (`Triumvirate`, `Senate`, `Governance`, +//! `RootWeights`) allow nothing. + mod call_groups; use alloc::{format, vec::Vec}; diff --git a/runtime/src/sudo_wrapper.rs b/runtime/src/sudo_wrapper.rs index 154fbcb89d..ae17c54de5 100644 --- a/runtime/src/sudo_wrapper.rs +++ b/runtime/src/sudo_wrapper.rs @@ -1,3 +1,11 @@ +//! Transaction extension that rejects sudo calls signed by anyone but the +//! configured sudo key. +//! +//! Drop-in check in the runtime's [`CustomTxExtension`](crate::CustomTxExtension) +//! pipeline. Non-sudo calls and unsigned origins pass through unchanged. +//! `IDENTIFIER` and SCALE layout are frozen — do not rename the type or alter +//! its encoding without a client migration. + use codec::{Decode, DecodeWithMemTracking, Encode}; use frame_support::dispatch::{DispatchInfo, PostDispatchInfo}; use frame_support::traits::IsSubType; @@ -13,7 +21,12 @@ use sp_runtime::transaction_validity::{InvalidTransaction, TransactionSource}; use sp_std::marker::PhantomData; use subtensor_macros::freeze_struct; -#[freeze_struct("99dce71278b36b44")] +/// Rejects `pallet_sudo` calls whose signer is not the on-chain sudo key. +/// +/// Returns `InvalidTransaction::BadSigner` when no sudo key is configured or +/// when the signer differs. Never alters origin; sudo dispatch still enforces +/// its own checks. +#[freeze_struct("290a37b979472932")] #[derive(Default, Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo)] pub struct SudoTransactionExtension(pub PhantomData); diff --git a/runtime/src/transaction_payment_wrapper.rs b/runtime/src/transaction_payment_wrapper.rs index 67c032ebad..f2e8cf62ba 100644 --- a/runtime/src/transaction_payment_wrapper.rs +++ b/runtime/src/transaction_payment_wrapper.rs @@ -1,3 +1,16 @@ +//! Transaction-payment extension with proxy `RealPaysFee` and coldkey fee routing. +//! +//! Wraps FRAME's [`ChargeTransactionPayment`] and, before charging: +//! 1. Prefer a proxy real that opted into `RealPaysFee` (up to two nesting levels / +//! homogeneous proxy batches). +//! 2. Else charge an owned hotkey's coldkey for allow-listed calls +//! ([`ColdkeyFeeCallFilter`] / `fee_filters`). +//! +//! Priority is class-based ([`NORMAL_DISPATCH_BASE_PRIORITY`] / +//! [`OPERATIONAL_DISPATCH_PRIORITY`]), not tip-based. Coldkey-paid tips are +//! zeroed so a hotkey cannot drain its owner via tip. `IDENTIFIER` and SCALE +//! layout are frozen. + use crate::{NORMAL_DISPATCH_BASE_PRIORITY, OPERATIONAL_DISPATCH_PRIORITY, Weight}; use codec::{Decode, DecodeWithMemTracking, Encode}; use frame_election_provider_support::private::sp_arithmetic::traits::SaturatedConversion; @@ -35,7 +48,9 @@ pub trait ColdkeyFeeCallFilter { fn charges_coldkey(call: &Call) -> bool; } -#[freeze_struct("f003cde1f9da4a90")] +/// SCALE-compatible wrapper around [`ChargeTransactionPayment`] with Subtensor +/// fee-payer resolution (proxy `RealPaysFee`, then coldkey allow-list). +#[freeze_struct("808d9c4ddeec2af0")] #[derive(Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo)] #[scale_info(skip_type_params(T))] pub struct ChargeTransactionPaymentWrapper { @@ -208,7 +223,7 @@ where } let signer = origin.as_system_origin_signer()?; - pallet_subtensor::Pallet::::maybe_coldkey_for_hotkey(signer) + pallet_subtensor::Pallet::::owning_coldkey_for_hotkey_if_set(signer) } } diff --git a/runtime/tests/account_conversion.rs b/runtime/tests/account_conversion.rs index 11aad3da85..a404f273c5 100644 --- a/runtime/tests/account_conversion.rs +++ b/runtime/tests/account_conversion.rs @@ -30,7 +30,7 @@ fn test_subnet_account_id_no_panics() { SubtensorModule::init_new_network(netuid, 10); let account_id = SubtensorModule::get_subnet_account_id(netuid).unwrap(); - let roudtrip_netuid = SubtensorModule::is_subnet_account_id(&account_id); + let roudtrip_netuid = SubtensorModule::netuid_for_subnet_account(&account_id); assert_eq!(netuid, roudtrip_netuid.unwrap()); } }); @@ -46,7 +46,7 @@ fn test_subnet_account_id_no_panics_quick() { SubtensorModule::init_new_network(netuid, 10); let account_id = SubtensorModule::get_subnet_account_id(netuid).unwrap(); - let roudtrip_netuid = SubtensorModule::is_subnet_account_id(&account_id); + let roudtrip_netuid = SubtensorModule::netuid_for_subnet_account(&account_id); assert_eq!(netuid, roudtrip_netuid.unwrap()); } }); diff --git a/scripts/check_metadata_unchanged.sh b/scripts/check_metadata_unchanged.sh new file mode 100755 index 0000000000..ea244b556b --- /dev/null +++ b/scripts/check_metadata_unchanged.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Safety oracle for the discoverability migration: docs-stripped structural +# fingerprint of Tier A–C surfaces must match the committed baseline. +# +# Usage: +# ./scripts/check_metadata_unchanged.sh # compare to baseline +# ./scripts/check_metadata_unchanged.sh --write # refresh baseline +# ./scripts/check_metadata_unchanged.sh --print # print fingerprint only +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BASELINE="${ROOT}/refactor/metadata-baseline.txt" +EXTRACT="${ROOT}/scripts/extract_metadata_fingerprint.py" + +if [[ ! -f "${EXTRACT}" ]]; then + echo "missing ${EXTRACT}" >&2 + exit 2 +fi + +case "${1:-}" in + --write) + python3 "${EXTRACT}" --write "${BASELINE}" + ;; + --print) + python3 "${EXTRACT}" + ;; + "") + if [[ ! -f "${BASELINE}" ]]; then + echo "missing baseline ${BASELINE}; run with --write first" >&2 + exit 2 + fi + python3 "${EXTRACT}" --check "${BASELINE}" + ;; + *) + echo "usage: $0 [--write|--print]" >&2 + exit 2 + ;; +esac diff --git a/scripts/extract_metadata_fingerprint.py b/scripts/extract_metadata_fingerprint.py new file mode 100755 index 0000000000..50fe38d4a2 --- /dev/null +++ b/scripts/extract_metadata_fingerprint.py @@ -0,0 +1,342 @@ +#!/usr/bin/env python3 +"""Extract a docs-stripped structural fingerprint of subtensor's frozen surface. + +This is the discoverability-migration safety oracle: if the fingerprint matches +the committed baseline, Tier A–C surfaces (storage names, call indices/names, +event/error order+names, construct_runtime, RPC methods, runtime API methods, +precompile indices/selectors) are unchanged. Doc comments are ignored. + +Usage: + python3 scripts/extract_metadata_fingerprint.py + python3 scripts/extract_metadata_fingerprint.py --write refactor/metadata-baseline.txt + python3 scripts/extract_metadata_fingerprint.py --check refactor/metadata-baseline.txt +""" + +from __future__ import annotations + +import argparse +import hashlib +import re +import sys +from difflib import unified_diff +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + +CALL_INDEX_RE = re.compile( + r"#\[pallet::call_index\((\d+)\)\]\s*(?:#\[[^\]]+\]\s*)*" + r"(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+(\w+)", + re.MULTILINE, +) +CONSTRUCT_RUNTIME_RE = re.compile( + r"construct_runtime!\s*\(\s*pub\s+struct\s+Runtime\s*\{([\s\S]*?)\n\s*\}\s*\)", + re.MULTILINE, +) +RUNTIME_ENTRY_RE = re.compile(r"^\s*(\w+)\s*:\s*[\w:]+(?:\s*=\s*(\d+))?", re.MULTILINE) +PRECOMPILE_INDEX_RE = re.compile( + r"(?:const\s+INDEX\s*:\s*u64\s*=\s*(\d+)|H160::from_low_u64_be\((\d+)\))", +) +SOLIDITY_PUBLIC_RE = re.compile(r'#\[precompile::public\("([^"]+)"\)\]') +RPC_METHOD_RE = re.compile(r'#\[method\(name\s*=\s*"([^"]+)"\)\]') +API_TRAIT_FN_RE = re.compile(r"^\s*fn\s+(\w+)\s*\(", re.MULTILINE) + + +def read(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except FileNotFoundError: + return "" + + +def strip_line_docs(src: str) -> str: + out = [] + for line in src.splitlines(): + s = line.lstrip() + if s.startswith("///") or s.startswith("//!"): + continue + if s.startswith("#[doc"): + continue + out.append(line) + return "\n".join(out) + + +def iter_pallet_rs() -> list[Path]: + files: list[Path] = [] + pallets = ROOT / "pallets" + if not pallets.is_dir(): + return files + for path in sorted(pallets.rglob("*.rs")): + parts = path.parts + if "tests" in parts or path.name in {"weights.rs", "benchmarking.rs"}: + continue + if "benchmarks" in parts: + continue + if "mock.rs" in path.name: + continue + files.append(path) + return files + + +def collect_storage(paths: list[Path]) -> list[str]: + """After #[pallet::storage], the next `pub type Name` is the storage item.""" + items: list[str] = [] + type_re = re.compile(r"\b(?:pub(?:\([^)]*\))?\s+)?type\s+(\w+)\s*<") + for path in paths: + lines = strip_line_docs(read(path)).splitlines() + rel = path.relative_to(ROOT).as_posix() + i = 0 + while i < len(lines): + if "#[pallet::storage]" in lines[i]: + j = i + 1 + while j < len(lines): + m = type_re.search(lines[j]) + if m: + items.append(f"storage\t{rel}\t{m.group(1)}") + break + # Stop if we hit another pallet attr without finding a type + if lines[j].strip().startswith("#[pallet::") and "storage" not in lines[j]: + break + j += 1 + if j > i + 15: + break + i += 1 + return sorted(items) + + +def collect_calls(paths: list[Path]) -> list[str]: + items: list[str] = [] + for path in paths: + text = strip_line_docs(read(path)) + rel = path.relative_to(ROOT).as_posix() + for idx, name in CALL_INDEX_RE.findall(text): + items.append(f"call\t{rel}\t{idx}\t{name}") + return sorted(items) + + +def collect_pallet_enum(path: Path, kind: str) -> list[str]: + """Collect Event/Error variant names in declaration order.""" + text = strip_line_docs(read(path)) + marker = f"#[pallet::{kind}]" + idx = text.find(marker) + if idx < 0: + return [] + # Find `enum ... {` + rest = text[idx:] + m = re.search(r"\benum\s+\w+[^{]*\{", rest) + if not m: + return [] + body_start = idx + m.end() + # Brace-depth scan to end of enum + depth = 1 + i = body_start + while i < len(text) and depth: + c = text[i] + if c == "{": + depth += 1 + elif c == "}": + depth -= 1 + i += 1 + body = text[body_start : i - 1] + rel = path.relative_to(ROOT).as_posix() + items: list[str] = [] + order = 0 + # Variant at line start: Name( or Name, + # Ignore deeper-indented type args by requiring the line's first token. + for line in body.splitlines(): + # Skip attribute-only lines + stripped = line.strip() + if not stripped or stripped.startswith("#["): + continue + vm = re.match(r"^([A-Z][A-Za-z0-9]*)\s*(\(|,|\{|$)", stripped) + if not vm: + continue + items.append(f"{kind}\t{rel}\t{order}\t{vm.group(1)}") + order += 1 + return items + + +def collect_enums(paths: list[Path]) -> list[str]: + items: list[str] = [] + for path in paths: + items.extend(collect_pallet_enum(path, "event")) + items.extend(collect_pallet_enum(path, "error")) + return items + + +def refine_enum_items(items: list[str]) -> list[str]: + """Drop false-positive 'variants' that are type args (deeper indent than true variants). + + Re-parse from source with indent tracking for accuracy. + """ + # Group by (kind, rel) + by_key: dict[tuple[str, str], list[tuple[int, str]]] = {} + for line in items: + kind, rel, order, name = line.split("\t") + by_key.setdefault((kind, rel), []).append((int(order), name)) + + # Re-extract with indent filter + out: list[str] = [] + for (kind, rel), _ in by_key.items(): + path = ROOT / rel + text = strip_line_docs(read(path)) + marker = f"#[pallet::{kind}]" + idx = text.find(marker) + if idx < 0: + continue + rest = text[idx:] + m = re.search(r"\benum\s+\w+[^{]*\{", rest) + if not m: + continue + body_start = idx + m.end() + depth = 1 + i = body_start + while i < len(text) and depth: + if text[i] == "{": + depth += 1 + elif text[i] == "}": + depth -= 1 + i += 1 + body = text[body_start : i - 1] + + candidates: list[tuple[int, str]] = [] + for line in body.splitlines(): + if not line.strip() or line.strip().startswith("#["): + continue + # Only consider lines at paren-depth 0 relative to the enum body line. + # Approximate: count net parens before this line in body — skip if > 0. + pass + + # Indent-based: find minimum indent among PascalCase lines ending with ( or , + raw: list[tuple[int, str]] = [] + for line in body.splitlines(): + m2 = re.match(r"^(\s*)([A-Z][A-Za-z0-9]*)\s*(\(|,|\{)\s*", line) + if not m2: + continue + indent = len(m2.group(1).expandtabs(4)) + raw.append((indent, m2.group(2))) + if not raw: + continue + min_indent = min(i for i, _ in raw) + order = 0 + for indent, name in raw: + if indent != min_indent: + continue + out.append(f"{kind}\t{rel}\t{order}\t{name}") + order += 1 + return out + + +def collect_construct_runtime() -> list[str]: + text = strip_line_docs(read(ROOT / "runtime/src/lib.rs")) + m = CONSTRUCT_RUNTIME_RE.search(text) + if not m: + return ["construct_runtime\tMISSING"] + items: list[str] = [] + for name, idx in RUNTIME_ENTRY_RE.findall(m.group(1)): + items.append(f"runtime\t{name}\t{idx or '?'}") + return items + + +def collect_precompiles() -> list[str]: + """Collect precompile INDEX values and Solidity selectors without source paths. + + Paths are omitted so `foo.rs` → `foo/mod.rs` file splits do not change the + fingerprint when INDEX / `#[precompile::public]` selectors are unchanged. + """ + items: list[str] = [] + pre_root = ROOT / "precompiles/src" + if not pre_root.exists(): + return items + for path in sorted(pre_root.rglob("*.rs")): + text = strip_line_docs(read(path)) + for a, b in PRECOMPILE_INDEX_RE.findall(text): + items.append(f"precompile_index\t{a or b}") + for sig in SOLIDITY_PUBLIC_RE.findall(text): + items.append(f"precompile_selector\t{sig}") + return sorted(items) + + +def collect_rpc() -> list[str]: + items: list[str] = [] + for path in sorted(ROOT.glob("pallets/*/rpc/src/**/*.rs")): + text = strip_line_docs(read(path)) + rel = path.relative_to(ROOT).as_posix() + for name in RPC_METHOD_RE.findall(text): + items.append(f"rpc\t{rel}\t{name}") + return sorted(items) + + +def collect_runtime_apis() -> list[str]: + items: list[str] = [] + for path in sorted(ROOT.glob("pallets/*/runtime-api/src/**/*.rs")): + text = strip_line_docs(read(path)) + rel = path.relative_to(ROOT).as_posix() + for trait in re.findall(r"pub\s+trait\s+(\w+)", text): + items.append(f"runtime_api_trait\t{rel}\t{trait}") + for fn in API_TRAIT_FN_RE.findall(text): + items.append(f"runtime_api_fn\t{rel}\t{fn}") + return sorted(items) + + +def build_fingerprint() -> str: + sources = iter_pallet_rs() + lines: list[str] = [ + "# subtensor frozen-surface fingerprint (docs stripped)", + "# tiers: storage, call, event, error, runtime, precompile, rpc, runtime_api", + ] + lines.extend(collect_storage(sources)) + lines.extend(collect_calls(sources)) + raw_enums = collect_enums(sources) + lines.extend(refine_enum_items(raw_enums)) + lines.extend(collect_construct_runtime()) + lines.extend(collect_precompiles()) + lines.extend(collect_rpc()) + lines.extend(collect_runtime_apis()) + body = "\n".join(lines) + "\n" + digest = hashlib.sha256(body.encode("utf-8")).hexdigest() + return f"sha256:{digest}\n\n{body}" + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--write", type=Path, help="Write fingerprint to this path") + parser.add_argument("--check", type=Path, help="Compare against baseline; exit 1 on mismatch") + args = parser.parse_args() + fp = build_fingerprint() + + if args.write: + args.write.parent.mkdir(parents=True, exist_ok=True) + args.write.write_text(fp, encoding="utf-8") + print(f"wrote {args.write}", file=sys.stderr) + print(fp.splitlines()[0]) + return 0 + + if args.check: + baseline = args.check.read_text(encoding="utf-8") + if baseline != fp: + print("METADATA FINGERPRINT MISMATCH", file=sys.stderr) + print(f"baseline: {baseline.splitlines()[0]}", file=sys.stderr) + print(f"current: {fp.splitlines()[0]}", file=sys.stderr) + for i, line in enumerate( + unified_diff( + baseline.splitlines(), + fp.splitlines(), + fromfile="baseline", + tofile="current", + lineterm="", + ) + ): + if i >= 80: + print("...", file=sys.stderr) + break + print(line, file=sys.stderr) + return 1 + print(f"OK {fp.splitlines()[0]}") + return 0 + + sys.stdout.write(fp) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/generate_refactor_manifest.py b/scripts/generate_refactor_manifest.py new file mode 100644 index 0000000000..2309c70674 --- /dev/null +++ b/scripts/generate_refactor_manifest.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python3 +"""Generate refactor/refactor-manifest.json with exclusive file ownership shards.""" + +from __future__ import annotations + +import json +import os +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def rs_files(base: Path) -> list[str]: + if not base.exists(): + return [] + out = [] + for p in sorted(base.rglob("*.rs")): + # Never assign generated weights to a mutating shard + if p.name == "weights.rs" and "subtensor" in p.parts: + continue + out.append(p.relative_to(ROOT).as_posix()) + return out + + +def shard(sid: str, wave: int, task: str, files: list[str], notes: str = "") -> dict: + return { + "id": sid, + "wave": wave, + "task": task, + "status": "pending", + "notes": notes, + "files": files, + } + + +def main() -> None: + shards: list[dict] = [] + + # Wave 1 — independent crates + small_pallets = [ + "admin-utils", + "alpha-assets", + "commitments", + "crowdloan", + "drand", + "limit-orders", + "shield", + "transaction-fee", + "proxy", + "utility", + ] + for name in small_pallets: + files = rs_files(ROOT / "pallets" / name) + shards.append( + shard( + f"w1-{name}", + 1, + "discoverability", + files, + "Docs on storage/calls/events/errors; rename private helpers; split files >1000 lines", + ) + ) + + # swap is its own tree + shards.append( + shard( + "w1-swap", + 1, + "discoverability", + rs_files(ROOT / "pallets" / "swap"), + "Pallet swap + rpc + runtime-api; freeze Solidity/RPC strings", + ) + ) + + for name, path in [ + ("node", ROOT / "node"), + ("common", ROOT / "common"), + ("primitives", ROOT / "primitives"), + ("support", ROOT / "support"), + ("chain-extensions", ROOT / "chain-extensions"), + ("runtime", ROOT / "runtime"), + ]: + files = rs_files(path) + # runtime: docs-only / internal helpers — construct_runtime frozen + notes = "Internal renames + docs; construct_runtime names/indices frozen" if name == "runtime" else "" + shards.append(shard(f"w1-{name}", 1, "discoverability", files, notes)) + + # precompiles split by file size into two shards + pre_files = rs_files(ROOT / "precompiles") + mid = (len(pre_files) + 1) // 2 + shards.append( + shard( + "w1-precompiles-a", + 1, + "discoverability", + pre_files[:mid], + "INDEX values and #[precompile::public] selectors frozen", + ) + ) + shards.append( + shard( + "w1-precompiles-b", + 1, + "discoverability", + pre_files[mid:], + "INDEX values and #[precompile::public] selectors frozen", + ) + ) + + # Wave 2 — pallet-subtensor by subtree + st = ROOT / "pallets" / "subtensor" + subtrees = [ + "coinbase", + "epoch", + "staking", + "subnets", + "swap", + "rpc_info", + "utils", + "guards", + "extensions", + "benchmarks", + ] + for sub in subtrees: + files = rs_files(st / "src" / sub) + shards.append( + shard( + f"w2-src-{sub}", + 2, + "discoverability", + files, + f"pallet-subtensor src/{sub}", + ) + ) + + # rpc + runtime-api crates + shards.append( + shard( + "w2-rpc", + 2, + "discoverability", + rs_files(st / "rpc") + rs_files(st / "runtime-api"), + "RPC method strings and runtime API trait/method names frozen", + ) + ) + + # docs-only frozen surfaces + frozen_docs = [ + ("w2-docs-storage", [ "pallets/subtensor/src/lib.rs" ], "DOCS ONLY on #[pallet::storage] items; do not rename types"), + ("w2-docs-dispatches", [ "pallets/subtensor/src/macros/dispatches.rs" ], "DOCS ONLY; call names and call_index frozen"), + ("w2-docs-events", [ "pallets/subtensor/src/macros/events.rs" ], "DOCS ONLY; variant order and names frozen"), + ("w2-docs-errors", [ "pallets/subtensor/src/macros/errors.rs" ], "DOCS ONLY; variant order and names frozen"), + ( + "w2-docs-migrations", + [p.relative_to(ROOT).as_posix() for p in sorted((st / "src" / "migrations").rglob("*.rs"))], + "DOCS ONLY; migration name strings frozen", + ), + ( + "w2-docs-macros-other", + [ + p.relative_to(ROOT).as_posix() + for p in sorted((st / "src" / "macros").glob("*.rs")) + if p.name not in {"dispatches.rs", "events.rs", "errors.rs"} + ], + "Docs + safe internal renames in remaining macros", + ), + ] + for sid, files, notes in frozen_docs: + shards.append(shard(sid, 2, "docs-only" if "DOCS ONLY" in notes else "discoverability", files, notes)) + + # Giant test files — one shard each for the biggest + tests_dir = st / "src" / "tests" + test_files = sorted(tests_dir.glob("*.rs"), key=lambda p: p.stat().st_size, reverse=True) + # Top giants get their own shard; remainder bundled + giants = [] + remainder = [] + for p in test_files: + if p.name == "mod.rs": + continue + rel = p.relative_to(ROOT).as_posix() + lines = sum(1 for _ in open(p, encoding="utf-8", errors="ignore")) + if lines >= 2500: + giants.append((p.stem, [rel], lines)) + else: + remainder.append(rel) + + for stem, files, lines in giants: + shards.append( + shard( + f"w2-test-{stem}", + 2, + "split-and-name", + files, + f"Split ~{lines}-line test file into concept-named modules under tests/{stem}/", + ) + ) + + # mod.rs + smaller tests + mod_rel = (tests_dir / "mod.rs").relative_to(ROOT).as_posix() + shards.append( + shard( + "w2-test-remainder", + 2, + "discoverability", + [mod_rel] + sorted(remainder), + "Wire mod.rs after giant splits land; improve smaller test modules", + ) + ) + + # Wave 3 + shards.append( + shard( + "w3-rename-queue", + 3, + "cross-cutting", + ["refactor/rename-proposals.md"], + "Process rename-proposals.md serially", + ) + ) + shards.append( + shard( + "w3-glossary", + 3, + "cross-cutting", + ["AGENTS.md", ".agents/skills/write-discoverable-code/SKILL.md"], + "Glossary consistency pass; finalize AGENTS.md", + ) + ) + + # Ownership check: no file in two shards of the same wave + by_wave: dict[int, dict[str, str]] = {} + conflicts = [] + for s in shards: + owned = by_wave.setdefault(s["wave"], {}) + for f in s["files"]: + if f in owned: + conflicts.append((s["wave"], f, owned[f], s["id"])) + else: + owned[f] = s["id"] + if conflicts: + raise SystemExit(f"ownership conflicts: {conflicts[:10]}") + + manifest = { + "branch": "refactor/discoverability", + "baseline": "refactor/metadata-baseline.txt", + "conventions": [ + "AGENTS.md", + ".agents/skills/write-discoverable-code/SKILL.md", + ], + "oracle": "scripts/check_metadata_unchanged.sh", + "shards": shards, + } + out = ROOT / "refactor" / "refactor-manifest.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + print(f"wrote {out} ({len(shards)} shards)") + for w in (1, 2, 3): + n = sum(1 for s in shards if s["wave"] == w) + files = sum(len(s["files"]) for s in shards if s["wave"] == w) + print(f" wave {w}: {n} shards, {files} files") + + +if __name__ == "__main__": + main() diff --git a/scripts/shard_work.md b/scripts/shard_work.md new file mode 100644 index 0000000000..de88527eb7 --- /dev/null +++ b/scripts/shard_work.md @@ -0,0 +1,36 @@ +# Shard agent instructions + +You are a discoverability shard worker on branch `refactor/discoverability`. + +## Before editing + +1. Read `AGENTS.md` and `.agents/skills/write-discoverable-code/SKILL.md`. +2. Read `refactor/FREEZE_STRUCT.md`. +3. Load your shard from `refactor/refactor-manifest.json` by id (argument). +4. **Only modify files listed in your shard's `files` array.** Creating new files under a directory you own (e.g. split `foo.rs` → `foo/mod.rs`) is allowed; add them to the mental ownership of your shard. +5. Never edit `pallets/subtensor/src/weights.rs`. + +## Work to do + +Depending on `task`: + +- **discoverability**: Add definition-site doc comments; rename private/`pub(crate)` helpers to 2–3 word domain names when `rg -w OldName` hits are entirely inside your files; split files >~1000 lines via `foo.rs` → `foo/mod.rs` + concept modules; name tests after sources. +- **docs-only**: Only add/improve doc comments. Do not rename anything. Do not reorder enum variants. For `freeze_struct`, follow FREEZE_STRUCT.md (update hash only for doc presence changes). +- **split-and-name**: Split the listed giant test file into `tests//mod.rs` + concept modules; update `tests/mod.rs` only if it is in your file list (otherwise note the needed `mod` wiring in rename-proposals). + +If a rename would touch files outside your list, append to `refactor/rename-proposals.md` (that file is shared — only append under a `##` heading for your symbol). + +## Exit checklist (must pass) + +```bash +./scripts/check_metadata_unchanged.sh +cargo fmt --all +# Prefer package-scoped checks when possible: +cargo clippy -p --all-targets -- --deny warnings +SKIP_WASM_BUILD=1 cargo nextest run -p +``` + +Commit on a branch named `refactor/shard-` with message: +`refactor(): discoverability improvements` + +Do not push to main. Do not change call_index, storage type names, event/error order, RPC strings, or precompile selectors. diff --git a/support/linting/src/forbid_as_primitive.rs b/support/linting/src/forbid_as_primitive.rs index 5e01d0741c..17f5914eef 100644 --- a/support/linting/src/forbid_as_primitive.rs +++ b/support/linting/src/forbid_as_primitive.rs @@ -1,6 +1,9 @@ +//! Ban panic-prone `as_u32` / `as_u64` / `as_u128` / `as_usize` conversions; prefer `try_into()`. + use super::*; use syn::{ExprMethodCall, File, Ident, visit::Visit}; +/// Lint: reject `as_u32`/`as_u64`/`as_u128`/`as_usize` method calls that can panic on overflow. pub struct ForbidAsPrimitiveConversion; impl Lint for ForbidAsPrimitiveConversion { @@ -24,7 +27,7 @@ struct AsPrimitiveVisitor { impl<'ast> Visit<'ast> for AsPrimitiveVisitor { fn visit_expr_method_call(&mut self, node: &'ast ExprMethodCall) { - if is_as_primitive(&node.method) { + if is_banned_as_primitive_method(&node.method) { self.errors.push(syn::Error::new( node.method.span(), "Using 'as_*()' methods is banned to avoid accidental panics. Use `try_into()` instead.", @@ -35,7 +38,8 @@ impl<'ast> Visit<'ast> for AsPrimitiveVisitor { } } -fn is_as_primitive(ident: &Ident) -> bool { +/// True for the panic-prone `as_u{32,64,128}` / `as_usize` conversion methods this lint bans. +fn is_banned_as_primitive_method(ident: &Ident) -> bool { matches!( ident.to_string().as_str(), "as_u32" | "as_u64" | "as_u128" | "as_usize" diff --git a/support/linting/src/forbid_keys_remove.rs b/support/linting/src/forbid_keys_remove.rs index 163eeb703a..9398afce3a 100644 --- a/support/linting/src/forbid_keys_remove.rs +++ b/support/linting/src/forbid_keys_remove.rs @@ -1,9 +1,14 @@ +//! Ban `Keys::::remove(netuid, uid)` — it can break the dense neuron uid sequence. +//! +//! Prefer `SubtensorModule::replace_neuron()` when swapping neurons. Opt out with `#[allow(unknown_lints)]`. + use super::*; use syn::{ Expr, ExprCall, ExprPath, File, Path, punctuated::Punctuated, spanned::Spanned, token::Comma, visit::Visit, }; +/// Lint: reject `Keys::::remove(netuid, uid)` calls that can corrupt neuron ordering. pub struct ForbidKeysRemoveCall; impl Lint for ForbidKeysRemoveCall { diff --git a/support/linting/src/forbid_saturating_math.rs b/support/linting/src/forbid_saturating_math.rs index 02f99c0bcd..7367191b77 100644 --- a/support/linting/src/forbid_saturating_math.rs +++ b/support/linting/src/forbid_saturating_math.rs @@ -1,6 +1,9 @@ +//! Ban `saturating_*` arithmetic so overflowing tests panic instead of silently clamping. + use super::*; use syn::{Expr, ExprCall, ExprMethodCall, ExprPath, File, Path, spanned::Spanned, visit::Visit}; +/// Lint: reject `saturating_*` methods and UFCS calls (production code should use checked/overflowing ops or accept panics in tests). pub struct ForbidSaturatingMath; impl Lint for ForbidSaturatingMath { diff --git a/support/linting/src/lib.rs b/support/linting/src/lib.rs index 56d44d7175..96ab608b25 100644 --- a/support/linting/src/lib.rs +++ b/support/linting/src/lib.rs @@ -1,3 +1,9 @@ +//! Custom workspace lints for Subtensor (run from repo-root `build.rs`). +//! +//! Each lint implements [`Lint`] and is invoked over parsed Rust sources. Lint type names +//! (`ForbidAsPrimitiveConversion`, `RequireFreezeStruct`, …) are referenced by string from +//! `build.rs` — rename them only with a matching update there. + pub mod lint; pub use lint::*; diff --git a/support/linting/src/lint.rs b/support/linting/src/lint.rs index fdf35ae7c5..f0b2518b0e 100644 --- a/support/linting/src/lint.rs +++ b/support/linting/src/lint.rs @@ -1,6 +1,9 @@ +//! Shared lint trait and `#[allow(unknown_lints)]` opt-out helper for workspace custom lints. + use proc_macro2::TokenTree; use syn::{Attribute, File, Meta, MetaList, Path}; +/// Aggregate of lint failures for one source file (`Ok(())` means clean). pub type Result = core::result::Result<(), Vec>; /// A trait that defines custom lints that can be run within our workspace. @@ -13,8 +16,9 @@ pub trait Lint: Send + Sync { fn lint(source: &File) -> Result; } -pub fn is_allowed(attibutes: &[Attribute]) -> bool { - attibutes.iter().any(|attribute| { +/// Returns `true` when `#[allow(unknown_lints)]` is present (opt-out used by custom lints). +pub fn is_allowed(attributes: &[Attribute]) -> bool { + attributes.iter().any(|attribute| { let Attribute { meta: Meta::List(MetaList { diff --git a/support/linting/src/pallet_index.rs b/support/linting/src/pallet_index.rs index f6fae12fbe..66f24afca8 100644 --- a/support/linting/src/pallet_index.rs +++ b/support/linting/src/pallet_index.rs @@ -1,9 +1,14 @@ +//! Require every pallet in `construct_runtime!` to declare an explicit index (`= N`). +//! +//! Implicit indices shift when pallets are inserted and break storage keys / metadata. + use super::*; use proc_macro2::TokenStream as TokenStream2; use procedural_fork::exports::construct_runtime::parse::RuntimeDeclaration; use quote::ToTokens; use syn::{File, visit::Visit}; +/// Lint: every pallet entry in `construct_runtime!` must set an explicit `= index`. pub struct RequireExplicitPalletIndex; impl Lint for RequireExplicitPalletIndex { diff --git a/support/linting/src/require_extrinsic_benchmarks.rs b/support/linting/src/require_extrinsic_benchmarks.rs deleted file mode 100644 index 9a621453a8..0000000000 --- a/support/linting/src/require_extrinsic_benchmarks.rs +++ /dev/null @@ -1,1366 +0,0 @@ -#![allow( - clippy::arithmetic_side_effects, - clippy::collapsible_if, - clippy::indexing_slicing, - clippy::question_mark -)] - -use super::*; -use proc_macro2::{Delimiter, TokenStream, TokenTree}; -use std::{ - collections::BTreeSet, - fs, - path::{Path, PathBuf}, - str::FromStr, -}; -use syn::File; - -pub struct RequireExtrinsicBenchmarks; - -impl Lint for RequireExtrinsicBenchmarks { - fn lint(_source: &File) -> Result { - // Dispatchables and benchmarks live in different files, so build.rs runs - // the real check once at workspace scope via `lint_workspace`. - Ok(()) - } -} - -impl RequireExtrinsicBenchmarks { - pub fn lint_workspace(workspace_root: &Path) -> Vec { - let pallets_dir = workspace_root.join("pallets"); - if !pallets_dir.is_dir() { - return Vec::new(); - } - - let mut rust_files = Vec::new(); - collect_runtime_rust_files(&pallets_dir, &mut rust_files); - - let mut errors = Vec::new(); - for file in rust_files { - let Ok(source) = fs::read_to_string(&file) else { - continue; - }; - - let dispatchables = collect_dispatchables_from_source(&source); - if dispatchables.is_empty() { - continue; - } - - let pallet_root = find_pallet_root(&file, workspace_root); - let benchmarks = collect_benchmarks_for_pallet(&pallet_root); - let benchmark_hint = benchmark_location_hint(&pallet_root, workspace_root); - let file_path = display_path(&file, workspace_root); - - for dispatchable in dispatchables { - if dispatchable.name.starts_with('_') { - continue; - } - - if !benchmarks.contains(&dispatchable.name) { - errors.push(format!( - "{}:{}:{}: dispatchable extrinsic `{}` is missing a matching benchmark; add `#[benchmark] fn {}(...)` to {}", - file_path, - dispatchable.line, - dispatchable.column, - dispatchable.name, - dispatchable.name, - benchmark_hint, - )); - continue; - } - - let uses_matching_weight_info = is_benchmarked_weight_plugged( - &dispatchable.name, - dispatchable.weight_attr.as_deref(), - ) - || source_has_matching_weight_info_for_dispatchable( - &source, - &dispatchable.name, - ); - - if !uses_matching_weight_info { - errors.push(format!( - "{}:{}:{}: dispatchable extrinsic `{}` has a matching benchmark but its #[pallet::weight] does not call WeightInfo::{}(...); plug the generated benchmark weight into the dispatch annotation", - file_path, - dispatchable.line, - dispatchable.column, - dispatchable.name, - dispatchable.name, - )); - } - } - } - - errors - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -struct Dispatchable { - name: String, - line: usize, - column: usize, - weight_attr: Option, -} - -fn source_has_matching_weight_info_for_dispatchable(source: &str, name: &str) -> bool { - // If weight_attr capture failed, fall back to the source text around the - // dispatchable itself. We intentionally keep this as a fallback instead of - // replacing the structured collection path: the lint is a source scanner - // over FRAME macro input, and complex #[pallet::weight({ ... })] blocks can - // confuse the backwards attribute walk even though the dispatch is valid. - for needle in [format!("pub fn {name}"), format!("pub(crate) fn {name}")] { - let mut search_from = 0usize; - - while let Some(offset) = source - .get(search_from..) - .and_then(|tail| tail.find(&needle)) - { - let fn_pos = search_from.saturating_add(offset); - let Some(prefix) = source.get(..fn_pos) else { - break; - }; - let Some(attr_start) = prefix.rfind("#[pallet::weight") else { - search_from = fn_pos.saturating_add(needle.len()); - continue; - }; - let Some(attr) = source.get(attr_start..fn_pos) else { - search_from = fn_pos.saturating_add(needle.len()); - continue; - }; - - // If another dispatchable starts between that attr and this function, - // the attr belongs to the earlier dispatchable, not this one. - if attr.contains("pub fn ") || attr.contains("pub(crate) fn ") { - search_from = fn_pos.saturating_add(needle.len()); - continue; - } - - let normalized = normalize_attr(attr); - if normalized.contains("benchmarked_weight_not_plugged") - || weight_attr_calls_weight_info_for(name, attr) - { - return true; - } - - search_from = fn_pos.saturating_add(needle.len()); - } - } - - false -} - -fn collect_dispatchables_from_source(source: &str) -> Vec { - let masked = mask_comments_and_strings(source); - let non_runtime_ranges = collect_non_runtime_cfg_ranges(&masked); - let mut dispatchables = Vec::new(); - let mut search_from = 0; - - while let Some((attr_start, attr_end)) = find_next_attr(&masked, search_from, "pallet::call") { - search_from = attr_end; - - if is_in_ranges(attr_start, &non_runtime_ranges) - || has_non_runtime_cfg_attr_before(&masked, attr_start, 0) - { - continue; - } - - let Some(impl_pos) = find_word(&masked, "impl", attr_end) else { - continue; - }; - let Some(open_brace) = masked[impl_pos..].find('{').map(|offset| impl_pos + offset) else { - continue; - }; - let Some(close_brace) = find_matching_brace(&masked, open_brace) else { - continue; - }; - - if is_in_ranges(impl_pos, &non_runtime_ranges) { - search_from = close_brace + 1; - continue; - } - - collect_pub_fns_in_impl( - source, - &masked, - open_brace + 1, - close_brace, - &non_runtime_ranges, - &mut dispatchables, - ); - search_from = close_brace + 1; - } - - dispatchables -} - -fn collect_pub_fns_in_impl( - source: &str, - masked: &str, - start: usize, - end: usize, - non_runtime_ranges: &[(usize, usize)], - dispatchables: &mut Vec, -) { - let bytes = masked.as_bytes(); - let mut idx = start; - let mut depth = 0usize; - - while idx < end { - match bytes[idx] { - b'{' => { - depth = depth.saturating_add(1); - idx += 1; - } - b'}' => { - depth = depth.saturating_sub(1); - idx += 1; - } - _ if depth == 0 && starts_with_word(masked, idx, "pub") => { - if is_in_ranges(idx, non_runtime_ranges) - || has_non_runtime_cfg_attr_before(masked, idx, start) - { - idx += 3; - continue; - } - - let mut cursor = skip_ws(masked, idx + 3); - - // Support `pub(crate) fn` even though FRAME dispatchables are normally `pub fn`. - if masked.as_bytes().get(cursor) == Some(&b'(') { - if let Some(close) = find_matching_paren(masked, cursor) { - cursor = skip_ws(masked, close + 1); - } - } - - if starts_with_word(masked, cursor, "fn") { - cursor = skip_ws(masked, cursor + 2); - if let Some((name, _name_end)) = parse_ident(masked, cursor) { - let (line, column) = line_column(source, cursor); - let weight_attr = preceding_weight_attr(source, masked, idx, start); - dispatchables.push(Dispatchable { - name, - line, - column, - weight_attr, - }); - } - } - - idx += 3; - } - _ => idx += 1, - } - } -} - -fn preceding_weight_attr( - source: &str, - masked: &str, - item_start: usize, - scope_start: usize, -) -> Option { - // `item_start` is the beginning of the dispatchable item as found by the - // source scanner, normally the `pub` in `pub fn`. Find the nearest - // #[pallet::weight(...)] before that item, then expand backward over the - // contiguous attribute cluster that belongs to the same dispatchable. - let prefix = masked.get(scope_start..item_start)?; - let attr_start = prefix.rfind("#[pallet::weight")? + scope_start; - - // Do not accidentally reuse a previous dispatchable's weight attr when the - // current item has no weight. If another function begins between the attr - // and this item, this attr is not for the current dispatchable. - let attr_to_item = masked.get(attr_start..item_start)?; - if attr_to_item.contains("pub fn ") || attr_to_item.contains("pub(crate) fn ") { - return None; - } - - let mut cluster_start = attr_start; - let mut cursor = attr_start; - while let Some(trimmed_end) = rtrim_ws(masked, scope_start, cursor) { - if masked.as_bytes().get(trimmed_end) != Some(&b']') { - break; - } - let Some(prev_attr_start) = masked - .get(scope_start..=trimmed_end) - .and_then(|section| section.rfind("#[")) - else { - break; - }; - cluster_start = scope_start + prev_attr_start; - cursor = cluster_start; - } - - source.get(cluster_start..item_start).map(ToOwned::to_owned) -} - -const BENCHMARKED_WEIGHT_NOT_PLUGGED_ALLOW: &str = "benchmarked_weight_not_plugged"; - -fn has_benchmark_weightinfo_plug_ignore_attr(weight_attr_cluster: &str) -> bool { - let attr = normalize_attr(weight_attr_cluster); - attr.contains("allow(") && attr.contains(BENCHMARKED_WEIGHT_NOT_PLUGGED_ALLOW) -} - -fn weight_attr_calls_weight_info_for(name: &str, weight_attr: &str) -> bool { - let normalized = normalize_attr(weight_attr); - if !normalized.contains("WeightInfo") { - return false; - } - - let mut search_from = 0usize; - while let Some(relative_method_start) = normalized - .get(search_from..) - .and_then(|tail| tail.find(name)) - { - let method_start = search_from + relative_method_start; - - // Method name must be reached through `::name`, not as part of another - // identifier. This rejects `WeightInfo::swap_coldkey_announced()` for a - // dispatchable named `swap_coldkey`. - if method_start < 2 || normalized.get(method_start - 2..method_start) != Some("::") { - search_from = method_start.saturating_add(name.len()); - continue; - } - - let after_name = method_start.saturating_add(name.len()); - if !is_call_boundary_after_method(&normalized, after_name) { - search_from = after_name; - continue; - } - - let before_method = &normalized[..method_start - 2]; - let Some(weight_info_start) = before_method.rfind("WeightInfo") else { - search_from = after_name; - continue; - }; - let after_weight_info = weight_info_start.saturating_add("WeightInfo".len()); - let between = &before_method[after_weight_info..]; - - // Accept: - // T::WeightInfo::foo(...) - // ::WeightInfo::foo(...) - // ::WeightInfo::foo(...) - // WeightInfo::::foo(...) - if between.is_empty() || turbofish_suffix_consumes_all(between) { - return true; - } - - search_from = after_name; - } - - false -} - -fn is_call_boundary_after_method(source: &str, after_name: usize) -> bool { - match source.get(after_name..) { - Some(rest) if rest.starts_with('(') => true, - Some(rest) if rest.starts_with("::<") => skip_turbofish_generics(source, after_name) - .and_then(|call_start| source.get(call_start..)) - .is_some_and(|rest| rest.starts_with('(')), - _ => false, - } -} - -fn turbofish_suffix_consumes_all(suffix: &str) -> bool { - suffix.starts_with("::<") - && skip_turbofish_generics(suffix, 0).is_some_and(|end| end == suffix.len()) -} - -fn skip_turbofish_generics(source: &str, start: usize) -> Option { - if !source.get(start..)?.starts_with("::<") { - return None; - } - - let bytes = source.as_bytes(); - let mut idx = start.checked_add(3)?; - let mut angle_depth = 1usize; - - while let Some(byte) = bytes.get(idx).copied() { - match byte { - b'<' => angle_depth = angle_depth.saturating_add(1), - b'>' => { - angle_depth = angle_depth.saturating_sub(1); - if angle_depth == 0 { - return idx.checked_add(1); - } - } - _ => {} - } - idx = idx.checked_add(1)?; - } - - None -} - -fn is_benchmarked_weight_plugged(name: &str, weight_attr: Option<&str>) -> bool { - let Some(weight_attr) = weight_attr else { - return false; - }; - - // This is our custom-lint allow marker. It intentionally uses an unknown - // lint name plus `unknown_lints` so rustc accepts the attribute while this - // source scanner can still recognize it. - if normalize_attr(weight_attr).contains("benchmarked_weight_not_plugged") { - return true; - } - - has_benchmark_weightinfo_plug_ignore_attr(weight_attr) - || weight_attr_calls_weight_info_for(name, weight_attr) -} - -fn normalize_attr(attr: &str) -> String { - attr.chars().filter(|ch| !ch.is_whitespace()).collect() -} - -fn collect_benchmarks_for_pallet(pallet_root: &Path) -> BTreeSet { - let mut rust_files = Vec::new(); - collect_rust_files(&pallet_root.join("src"), &mut rust_files); - - let mut benchmarks = BTreeSet::new(); - for file in rust_files { - if !is_benchmark_file(&file) { - continue; - } - - let Ok(source) = fs::read_to_string(&file) else { - continue; - }; - collect_frame_v2_benchmarks(&source, &mut benchmarks); - collect_legacy_benchmarks(&source, &mut benchmarks); - } - - benchmarks -} - -fn collect_frame_v2_benchmarks(source: &str, benchmarks: &mut BTreeSet) { - let masked = mask_comments_and_strings(source); - let mut search_from = 0; - - while let Some((_attr_start, attr_end)) = find_next_attr(&masked, search_from, "benchmark") { - search_from = attr_end; - let Some(fn_pos) = find_word(&masked, "fn", attr_end) else { - continue; - }; - let name_start = skip_ws(&masked, fn_pos + 2); - if let Some((name, _name_end)) = parse_ident(&masked, name_start) { - benchmarks.insert(name); - } - } -} - -fn collect_legacy_benchmarks(source: &str, benchmarks: &mut BTreeSet) { - let Ok(tokens) = TokenStream::from_str(source) else { - return; - }; - collect_legacy_benchmarks_from_tokens(&tokens, benchmarks); -} - -fn collect_legacy_benchmarks_from_tokens(tokens: &TokenStream, benchmarks: &mut BTreeSet) { - let tokens: Vec<_> = tokens.clone().into_iter().collect(); - let mut idx = 0; - - while idx < tokens.len() { - match &tokens[idx] { - TokenTree::Ident(ident) if ident == "benchmarks" => { - if matches!(tokens.get(idx + 1), Some(TokenTree::Punct(punct)) if punct.as_char() == '!') - { - if let Some(TokenTree::Group(group)) = tokens.get(idx + 2) { - if group.delimiter() == Delimiter::Brace { - collect_legacy_benchmark_names(&group.stream(), benchmarks); - idx += 3; - continue; - } - } - } - } - TokenTree::Group(group) => { - collect_legacy_benchmarks_from_tokens(&group.stream(), benchmarks); - } - _ => {} - } - - idx += 1; - } -} - -fn collect_legacy_benchmark_names(tokens: &TokenStream, benchmarks: &mut BTreeSet) { - let tokens: Vec<_> = tokens.clone().into_iter().collect(); - let mut idx = 0; - - while idx < tokens.len() { - let TokenTree::Ident(ident) = &tokens[idx] else { - idx += 1; - continue; - }; - - let name = ident.to_string(); - if matches!( - name.as_str(), - "where_clause" | "verify" | "impl_benchmark_test_suite" - ) { - idx += 1; - continue; - } - - let mut lookahead = idx + 1; - if matches!( - tokens.get(lookahead), - Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Parenthesis - ) { - lookahead += 1; - } - - if matches!( - tokens.get(lookahead), - Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Brace - ) { - benchmarks.insert(name); - } - - idx += 1; - } -} - -fn find_next_attr(masked: &str, from: usize, attr_path: &str) -> Option<(usize, usize)> { - let mut search_from = from; - while let Some(offset) = masked[search_from..].find("#[") { - let start = search_from + offset; - let Some(close) = masked[start..].find(']').map(|offset| start + offset) else { - return None; - }; - let normalized: String = masked[start..=close] - .chars() - .filter(|ch| !ch.is_whitespace()) - .collect(); - let expected = format!("#[{attr_path}"); - - if normalized.starts_with(&expected) - && matches!( - normalized.as_bytes().get(expected.len()), - Some(b']') | Some(b'(') - ) - { - return Some((start, close + 1)); - } - - search_from = close + 1; - } - - None -} - -fn collect_non_runtime_cfg_ranges(masked: &str) -> Vec<(usize, usize)> { - let mut ranges = Vec::new(); - let mut search_from = 0; - - while let Some((attr_start, attr_end)) = find_next_attr(masked, search_from, "cfg") { - search_from = attr_end; - - if !is_non_runtime_cfg_attr(&masked[attr_start..attr_end]) { - continue; - } - - let item_start = skip_outer_attrs(masked, skip_ws(masked, attr_end)); - let Some(open_brace) = find_item_open_brace(masked, item_start) else { - ranges.push((attr_start, end_of_line(masked, attr_end))); - continue; - }; - let Some(close_brace) = find_matching_brace(masked, open_brace) else { - ranges.push((attr_start, end_of_line(masked, attr_end))); - continue; - }; - - ranges.push((attr_start, close_brace + 1)); - search_from = close_brace + 1; - } - - ranges -} - -fn has_non_runtime_cfg_attr_before(masked: &str, item_start: usize, scope_start: usize) -> bool { - let mut cursor = item_start; - - loop { - let Some(trimmed_end) = rtrim_ws(masked, scope_start, cursor) else { - return false; - }; - if masked.as_bytes().get(trimmed_end) != Some(&b']') { - return false; - } - - let Some(attr_start) = masked[scope_start..=trimmed_end].rfind("#[") else { - return false; - }; - let attr_start = scope_start + attr_start; - let attr = &masked[attr_start..=trimmed_end]; - if is_non_runtime_cfg_attr(attr) { - return true; - } - - cursor = attr_start; - } -} - -fn is_non_runtime_cfg_attr(attr: &str) -> bool { - let normalized: String = attr.chars().filter(|ch| !ch.is_whitespace()).collect(); - let Some(cfg) = normalized - .strip_prefix("#[cfg(") - .and_then(|value| value.strip_suffix(")]")) - else { - return false; - }; - - cfg == "test" - || cfg.contains("feature=") - || cfg.starts_with("all(test,") - || cfg.starts_with("any(test,") - || cfg.contains(",test,") - || cfg.contains(",test)") -} - -fn skip_outer_attrs(masked: &str, mut idx: usize) -> usize { - loop { - idx = skip_ws(masked, idx); - if !masked[idx..].starts_with("#[") { - return idx; - } - let Some(close) = masked[idx..].find(']').map(|offset| idx + offset) else { - return idx; - }; - idx = close + 1; - } -} - -fn find_item_open_brace(masked: &str, item_start: usize) -> Option { - let bytes = masked.as_bytes(); - let mut idx = item_start; - let mut paren_depth = 0usize; - let mut bracket_depth = 0usize; - let mut angle_depth = 0usize; - - while idx < bytes.len() { - match bytes[idx] { - b'(' => paren_depth += 1, - b')' => paren_depth = paren_depth.saturating_sub(1), - b'[' => bracket_depth += 1, - b']' => bracket_depth = bracket_depth.saturating_sub(1), - b'<' => angle_depth += 1, - b'>' => angle_depth = angle_depth.saturating_sub(1), - b';' if paren_depth == 0 && bracket_depth == 0 && angle_depth == 0 => return None, - b'{' if paren_depth == 0 && bracket_depth == 0 && angle_depth == 0 => return Some(idx), - _ => {} - } - idx += 1; - } - - None -} - -fn is_in_ranges(idx: usize, ranges: &[(usize, usize)]) -> bool { - ranges - .iter() - .any(|(start, end)| idx >= *start && idx < *end) -} - -fn mask_comments_and_strings(source: &str) -> String { - let bytes = source.as_bytes(); - let mut out = String::with_capacity(source.len()); - let mut idx = 0; - - while idx < bytes.len() { - match (bytes[idx], bytes.get(idx + 1).copied()) { - (b'/', Some(b'/')) => { - out.push(' '); - out.push(' '); - idx += 2; - while idx < bytes.len() && bytes[idx] != b'\n' { - out.push(' '); - idx += 1; - } - } - (b'/', Some(b'*')) => { - out.push(' '); - out.push(' '); - idx += 2; - let mut depth = 1usize; - while idx < bytes.len() && depth > 0 { - if bytes[idx] == b'\n' { - out.push('\n'); - idx += 1; - } else if bytes[idx] == b'/' && bytes.get(idx + 1) == Some(&b'*') { - out.push(' '); - out.push(' '); - idx += 2; - depth += 1; - } else if bytes[idx] == b'*' && bytes.get(idx + 1) == Some(&b'/') { - out.push(' '); - out.push(' '); - idx += 2; - depth -= 1; - } else { - out.push(' '); - idx += 1; - } - } - } - (b'"', _) => { - out.push(' '); - idx += 1; - let mut escaped = false; - while idx < bytes.len() { - let byte = bytes[idx]; - if byte == b'\n' { - out.push('\n'); - idx += 1; - break; - } - out.push(' '); - idx += 1; - if escaped { - escaped = false; - } else if byte == b'\\' { - escaped = true; - } else if byte == b'"' { - break; - } - } - } - (b'\'', _) if !bytes.get(idx + 1).is_some_and(|byte| is_ident_start(*byte)) => { - out.push(' '); - idx += 1; - let mut escaped = false; - while idx < bytes.len() { - let byte = bytes[idx]; - if byte == b'\n' { - out.push('\n'); - idx += 1; - break; - } - out.push(' '); - idx += 1; - if escaped { - escaped = false; - } else if byte == b'\\' { - escaped = true; - } else if byte == b'\'' { - break; - } - } - } - (byte, _) => { - out.push(byte as char); - idx += 1; - } - } - } - - out -} - -fn find_matching_brace(masked: &str, open_brace: usize) -> Option { - find_matching_delimiter(masked, open_brace, b'{', b'}') -} - -fn find_matching_paren(masked: &str, open_paren: usize) -> Option { - find_matching_delimiter(masked, open_paren, b'(', b')') -} - -fn find_matching_delimiter( - masked: &str, - open: usize, - open_byte: u8, - close_byte: u8, -) -> Option { - let bytes = masked.as_bytes(); - if bytes.get(open) != Some(&open_byte) { - return None; - } - - let mut depth = 0usize; - for (idx, byte) in bytes.iter().enumerate().skip(open) { - if *byte == open_byte { - depth += 1; - } else if *byte == close_byte { - depth = depth.saturating_sub(1); - if depth == 0 { - return Some(idx); - } - } - } - - None -} - -fn find_word(masked: &str, word: &str, from: usize) -> Option { - let mut search_from = from; - while let Some(offset) = masked[search_from..].find(word) { - let idx = search_from + offset; - if starts_with_word(masked, idx, word) { - return Some(idx); - } - search_from = idx + word.len(); - } - - None -} - -fn starts_with_word(masked: &str, idx: usize, word: &str) -> bool { - let bytes = masked.as_bytes(); - let word_bytes = word.as_bytes(); - - if bytes.get(idx..idx + word_bytes.len()) != Some(word_bytes) { - return false; - } - - let before_ok = idx == 0 || !is_ident_continue(bytes[idx - 1]); - let after_idx = idx + word_bytes.len(); - let after_ok = after_idx >= bytes.len() || !is_ident_continue(bytes[after_idx]); - before_ok && after_ok -} - -fn parse_ident(masked: &str, start: usize) -> Option<(String, usize)> { - let bytes = masked.as_bytes(); - let first = *bytes.get(start)?; - if !is_ident_start(first) { - return None; - } - - let mut end = start + 1; - while bytes.get(end).is_some_and(|byte| is_ident_continue(*byte)) { - end += 1; - } - - Some((masked[start..end].to_owned(), end)) -} - -fn skip_ws(masked: &str, mut idx: usize) -> usize { - let bytes = masked.as_bytes(); - while bytes - .get(idx) - .is_some_and(|byte| byte.is_ascii_whitespace()) - { - idx += 1; - } - idx -} - -fn rtrim_ws(masked: &str, start: usize, mut end: usize) -> Option { - let bytes = masked.as_bytes(); - while end > start - && bytes - .get(end - 1) - .is_some_and(|byte| byte.is_ascii_whitespace()) - { - end -= 1; - } - end.checked_sub(1).filter(|idx| *idx >= start) -} - -fn end_of_line(masked: &str, from: usize) -> usize { - masked[from..] - .find('\n') - .map(|offset| from + offset) - .unwrap_or(masked.len()) -} - -fn is_ident_start(byte: u8) -> bool { - byte == b'_' || byte.is_ascii_alphabetic() -} - -fn is_ident_continue(byte: u8) -> bool { - is_ident_start(byte) || byte.is_ascii_digit() -} - -fn line_column(source: &str, idx: usize) -> (usize, usize) { - let line = source[..idx].bytes().filter(|byte| *byte == b'\n').count() + 1; - let column = source[..idx] - .rfind('\n') - .map(|line_start| idx - line_start) - .unwrap_or(idx + 1); - (line, column) -} - -fn is_benchmark_file(file: &Path) -> bool { - file.file_name() - .and_then(|name| name.to_str()) - .is_some_and(|name| name.contains("benchmark")) - || file - .components() - .any(|component| component.as_os_str() == "benchmarks") -} - -fn is_test_or_mock_source_path(path: &Path) -> bool { - path.components().any(|component| { - let Some(raw_name) = component.as_os_str().to_str() else { - return false; - }; - let name = raw_name.to_ascii_lowercase(); - let stem = Path::new(&name) - .file_stem() - .and_then(|stem| stem.to_str()) - .unwrap_or(&name); - - matches!( - stem, - "benchmark" - | "benchmarks" - | "benchmarking" - | "mock" - | "mocks" - | "test" - | "tests" - | "testing" - | "test_utils" - | "test_util" - | "test_helpers" - | "tests_helpers" - ) || stem.starts_with("mock_") - || stem.ends_with("_mock") - || stem.starts_with("test_") - || stem.ends_with("_test") - || stem.ends_with("_tests") - }) -} - -fn find_pallet_root(file: &Path, workspace_root: &Path) -> PathBuf { - let pallets_dir = workspace_root.join("pallets"); - let mut current = file.parent(); - - while let Some(dir) = current { - if dir.starts_with(&pallets_dir) && dir.join("Cargo.toml").is_file() { - return dir.to_path_buf(); - } - - if dir == workspace_root { - break; - } - - current = dir.parent(); - } - - file.parent().unwrap_or(workspace_root).to_path_buf() -} - -fn benchmark_location_hint(pallet_root: &Path, workspace_root: &Path) -> String { - for location in [ - pallet_root.join("src/benchmarks.rs"), - pallet_root.join("src/benchmarking.rs"), - ] { - if location.exists() { - return display_path(&location, workspace_root); - } - } - - display_path(&pallet_root.join("src/benchmarks.rs"), workspace_root) -} - -fn collect_runtime_rust_files(dir: &Path, rust_files: &mut Vec) { - let Ok(entries) = fs::read_dir(dir) else { - return; - }; - - for entry in entries.flatten() { - let path = entry.path(); - if path - .components() - .any(|component| component.as_os_str() == "target" || component.as_os_str() == ".git") - { - continue; - } - - if is_test_or_mock_source_path(&path) { - continue; - } - - if path.is_dir() { - collect_runtime_rust_files(&path, rust_files); - } else if path.extension().and_then(|ext| ext.to_str()) == Some("rs") { - rust_files.push(path); - } - } -} - -fn collect_rust_files(dir: &Path, rust_files: &mut Vec) { - let Ok(entries) = fs::read_dir(dir) else { - return; - }; - - for entry in entries.flatten() { - let path = entry.path(); - if path - .components() - .any(|component| component.as_os_str() == "target" || component.as_os_str() == ".git") - { - continue; - } - - if path.is_dir() { - collect_rust_files(&path, rust_files); - } else if path.extension().and_then(|ext| ext.to_str()) == Some("rs") { - rust_files.push(path); - } - } -} - -fn display_path(path: &Path, workspace_root: &Path) -> String { - path.strip_prefix(workspace_root) - .unwrap_or(path) - .display() - .to_string() -} - -#[cfg(test)] -#[allow(clippy::expect_used, clippy::unwrap_used)] -mod tests { - - #[test] - fn weightinfo_plug_helper_accepts_captured_attrs_and_custom_allow() { - let batch_attr = r#" - #[pallet::call_index(0)] - #[pallet::weight({ - let (dispatch_weight, pays) = Pallet::::weight_and_dispatch_class(calls); - let dispatch_weight = dispatch_weight - .saturating_add(T::WeightInfo::batch(calls.len() as u32)); - (dispatch_weight, DispatchClass::Normal, pays) - })] - "#; - assert!(is_benchmarked_weight_plugged("batch", Some(batch_attr))); - - let allow_attr = r#" - #[allow(unknown_lints, benchmarked_weight_not_plugged)] - #[pallet::weight(store_encrypted_weight())] - "#; - assert!(is_benchmarked_weight_plugged( - "store_encrypted", - Some(allow_attr), - )); - } - - #[test] - fn weightinfo_plug_matcher_accepts_exact_methods_and_rejects_prefixes() { - assert!(weight_attr_calls_weight_info_for( - "batch", - "#[pallet::weight(T::WeightInfo::batch(calls.len() as u32))]", - )); - assert!(weight_attr_calls_weight_info_for( - "proxy", - "#[pallet::weight(WeightInfo::::proxy(p.into()))]", - )); - assert!(weight_attr_calls_weight_info_for( - "set_weights", - "#[pallet::weight(::WeightInfo::set_weights())]", - )); - assert!(weight_attr_calls_weight_info_for( - "set_fee_rate", - "#[pallet::weight(::WeightInfo::set_fee_rate())]", - )); - assert!(weight_attr_calls_weight_info_for( - "swap_coldkey", - "#[pallet::weight(T::WeightInfo::swap_coldkey::())]", - )); - - assert!(!weight_attr_calls_weight_info_for( - "swap_coldkey", - "#[pallet::weight(T::WeightInfo::swap_coldkey_announced())]", - )); - assert!(!weight_attr_calls_weight_info_for( - "set_weight", - "#[pallet::weight(T::WeightInfo::set_weights())]", - )); - } - - #[test] - fn source_fallback_accepts_valid_complex_weight_attrs() { - let source = r#" - #[pallet::call] - impl Pallet { - #[pallet::call_index(0)] - #[pallet::weight({ - let (dispatch_weight, pays) = Pallet::::weight_and_dispatch_class(calls); - let dispatch_weight = dispatch_weight - .saturating_add(T::WeightInfo::batch(calls.len() as u32)); - (dispatch_weight, DispatchClass::Normal, pays) - })] - pub fn batch(origin: OriginFor, calls: Vec) -> DispatchResult { - Ok(()) - } - } - "#; - - assert!(source_has_matching_weight_info_for_dispatchable( - source, "batch" - )); - } - - #[test] - fn source_fallback_does_not_use_previous_dispatchable_weight_attr() { - let source = r#" - #[pallet::call] - impl Pallet { - #[pallet::call_index(0)] - #[pallet::weight(T::WeightInfo::first())] - pub fn first(origin: OriginFor) -> DispatchResult { Ok(()) } - - #[pallet::call_index(1)] - #[pallet::weight(Weight::from_parts(1, 0))] - pub fn second(origin: OriginFor) -> DispatchResult { Ok(()) } - } - "#; - - assert!(source_has_matching_weight_info_for_dispatchable( - source, "first" - )); - assert!(!source_has_matching_weight_info_for_dispatchable( - source, "second" - )); - } - - #[test] - fn weightinfo_plug_check_accepts_common_valid_forms() { - assert!(weight_attr_calls_weight_info_for( - "batch", - "#[pallet::weight({ let w = T::WeightInfo::batch(calls.len() as u32); w })]", - )); - assert!(weight_attr_calls_weight_info_for( - "set_fee_rate", - "#[pallet::weight(::WeightInfo::set_fee_rate())]", - )); - assert!(weight_attr_calls_weight_info_for( - "set_weights", - "#[pallet::weight((::WeightInfo::set_weights(), DispatchClass::Normal, Pays::No))]", - )); - assert!(weight_attr_calls_weight_info_for( - "proxy", - "#[pallet::weight((WeightInfo::::proxy(T::MaxProxies::get()), DispatchClass::Normal))]", - )); - assert!(!weight_attr_calls_weight_info_for( - "proxy", - "#[pallet::weight(Weight::from_parts(10, 0))]", - )); - } - - #[test] - fn dispatchable_weight_attr_is_found_for_complex_weight_blocks() { - let input = r#" - #[pallet::call] - impl Pallet { - #[pallet::call_index(0)] - #[pallet::weight({ - let (dispatch_weight, pays) = Pallet::::weight_and_dispatch_class(calls); - let dispatch_weight = dispatch_weight - .saturating_add(T::WeightInfo::batch(calls.len() as u32)); - (dispatch_weight, DispatchClass::Normal, pays) - })] - pub fn batch(origin: OriginFor, calls: Vec) -> DispatchResult { - Ok(()) - } - } - "#; - - let dispatchables = collect_dispatchables_from_source(input); - let batch = dispatchables - .iter() - .find(|dispatchable| dispatchable.name == "batch") - .expect("batch dispatchable is collected"); - - assert!(is_benchmarked_weight_plugged( - &batch.name, - batch.weight_attr.as_deref(), - )); - } - - use super::*; - - #[test] - fn custom_allow_attr_skips_weightinfo_plug_check() { - let dispatch_source = r#" - #[pallet::call] - impl Pallet { - #[allow(unknown_lints, benchmarked_weight_not_plugged)] - #[pallet::call_index(2)] - #[pallet::weight(store_encrypted_weight())] - pub fn store_encrypted(origin: OriginFor) -> DispatchResult { Ok(()) } - } - "#; - - let dispatchables = collect_dispatchables_from_source(dispatch_source); - let dispatchable = dispatchables - .iter() - .find(|dispatchable| dispatchable.name == "store_encrypted") - .expect("store_encrypted dispatchable is collected"); - - assert!(is_benchmarked_weight_plugged( - &dispatchable.name, - dispatchable.weight_attr.as_deref() - )); - } - - #[test] - fn collects_dispatchables_from_pallet_call_impl() { - let input = r#" - #[pallet::call] - impl Pallet { - #[pallet::call_index(0)] - pub fn set_weights(origin: OriginFor) -> DispatchResult { - Ok(()) - } - - fn helper() {} - } - "#; - - let dispatchables = collect_dispatchables_from_source(input); - assert_eq!(dispatchables.len(), 1); - assert_eq!(dispatchables[0].name, "set_weights"); - } - - #[test] - fn ignores_cfg_test_pallet_call_impls() { - let input = r#" - #[cfg(test)] - mod tests { - #[pallet::call] - impl Pallet { - pub fn mock_only(origin: OriginFor) -> DispatchResult { - Ok(()) - } - } - } - - #[pallet::call] - impl Pallet { - pub fn real_call(origin: OriginFor) -> DispatchResult { - Ok(()) - } - } - "#; - - let dispatchables = collect_dispatchables_from_source(input); - assert_eq!(dispatchables.len(), 1); - assert_eq!(dispatchables[0].name, "real_call"); - } - - #[test] - fn ignores_cfg_test_dispatchable_fns_inside_real_call_impls() { - let input = r#" - #[pallet::call] - impl Pallet { - #[cfg(test)] - pub fn mock_only(origin: OriginFor) -> DispatchResult { - Ok(()) - } - - pub fn real_call(origin: OriginFor) -> DispatchResult { - Ok(()) - } - } - "#; - - let dispatchables = collect_dispatchables_from_source(input); - assert_eq!(dispatchables.len(), 1); - assert_eq!(dispatchables[0].name, "real_call"); - } - - #[test] - fn ignores_feature_gated_dispatchable_fns_inside_real_call_impls() { - let input = r#" - #[pallet::call] - impl Pallet { - #[cfg(feature = "pow-faucet")] - pub fn faucet(origin: OriginFor) -> DispatchResult { - Ok(()) - } - - pub fn real_call(origin: OriginFor) -> DispatchResult { - Ok(()) - } - } - "#; - - let dispatchables = collect_dispatchables_from_source(input); - assert_eq!(dispatchables.len(), 1); - assert_eq!(dispatchables[0].name, "real_call"); - } - - #[test] - fn recognizes_mock_and_test_paths_as_non_runtime() { - assert!(is_test_or_mock_source_path(Path::new( - "pallets/example/src/mock.rs" - ))); - assert!(is_test_or_mock_source_path(Path::new( - "pallets/example/src/tests/register.rs" - ))); - assert!(is_test_or_mock_source_path(Path::new( - "pallets/example/src/benchmarking.rs" - ))); - assert!(!is_test_or_mock_source_path(Path::new( - "pallets/example/src/macros/dispatches.rs" - ))); - } - - #[test] - fn collects_frame_v2_benchmarks() { - let input = r#" - #[benchmarks] - mod benchmarks { - #[benchmark] - fn set_weights() { - #[block] - {} - } - - fn helper() {} - } - "#; - - let mut benchmarks = BTreeSet::new(); - collect_frame_v2_benchmarks(input, &mut benchmarks); - assert!(benchmarks.contains("set_weights")); - assert!(!benchmarks.contains("helper")); - } - - #[test] - fn collects_legacy_benchmarks_macro_names() { - let input = r#" - benchmarks! { - where_clause { where T: Config } - - set_weights { - let caller = account("caller", 0, 0); - }: _(RawOrigin::Signed(caller)) - verify {} - } - "#; - - let mut benchmarks = BTreeSet::new(); - collect_legacy_benchmarks(input, &mut benchmarks); - assert!(benchmarks.contains("set_weights")); - assert!(!benchmarks.contains("where_clause")); - assert!(!benchmarks.contains("verify")); - } - - #[test] - fn register_limit_is_missing_when_no_matching_benchmark_exists() { - let dispatch_source = r#" - #[pallet::call] - impl Pallet { - #[pallet::call_index(134)] - pub fn register_limit(origin: OriginFor) -> DispatchResult { Ok(()) } - } - "#; - let benchmark_source = r#" - #[benchmarks] - mod benchmarks { - #[benchmark] - fn root_register() { #[block] {} } - } - "#; - - let dispatchables = collect_dispatchables_from_source(dispatch_source); - let mut benchmarks = BTreeSet::new(); - collect_frame_v2_benchmarks(benchmark_source, &mut benchmarks); - - assert_eq!(dispatchables[0].name, "register_limit"); - assert!(!benchmarks.contains(&dispatchables[0].name)); - } -} diff --git a/support/linting/src/require_extrinsic_benchmarks/benchmark_scan.rs b/support/linting/src/require_extrinsic_benchmarks/benchmark_scan.rs new file mode 100644 index 0000000000..65ee0af6d8 --- /dev/null +++ b/support/linting/src/require_extrinsic_benchmarks/benchmark_scan.rs @@ -0,0 +1,125 @@ +//! Collect benchmark function names from FRAME v2 `#[benchmark]` and legacy `benchmarks!`. + +use super::dispatchable_scan::find_next_attr; +use super::pallet_paths::{collect_rust_files, is_benchmark_file}; +use super::source_scan::*; +use proc_macro2::{Delimiter, TokenStream, TokenTree}; +use std::collections::BTreeSet; +use std::fs; +use std::path::Path; +use std::str::FromStr; + +pub(super) fn collect_benchmarks_for_pallet(pallet_root: &Path) -> BTreeSet { + let mut rust_files = Vec::new(); + collect_rust_files(&pallet_root.join("src"), &mut rust_files); + + let mut benchmarks = BTreeSet::new(); + for file in rust_files { + if !is_benchmark_file(&file) { + continue; + } + + let Ok(source) = fs::read_to_string(&file) else { + continue; + }; + collect_frame_v2_benchmarks(&source, &mut benchmarks); + collect_legacy_benchmarks(&source, &mut benchmarks); + } + + benchmarks +} + +pub(super) fn collect_frame_v2_benchmarks(source: &str, benchmarks: &mut BTreeSet) { + let masked = mask_comments_and_strings(source); + let mut search_from = 0; + + while let Some((_attr_start, attr_end)) = find_next_attr(&masked, search_from, "benchmark") { + search_from = attr_end; + let Some(fn_pos) = find_word(&masked, "fn", attr_end) else { + continue; + }; + let name_start = skip_ws(&masked, fn_pos + 2); + if let Some((name, _name_end)) = parse_ident(&masked, name_start) { + benchmarks.insert(name); + } + } +} + +pub(super) fn collect_legacy_benchmarks(source: &str, benchmarks: &mut BTreeSet) { + let Ok(tokens) = TokenStream::from_str(source) else { + return; + }; + collect_legacy_benchmarks_from_tokens(&tokens, benchmarks); +} + +pub(super) fn collect_legacy_benchmarks_from_tokens( + tokens: &TokenStream, + benchmarks: &mut BTreeSet, +) { + let tokens: Vec<_> = tokens.clone().into_iter().collect(); + let mut idx = 0; + + while idx < tokens.len() { + match &tokens[idx] { + TokenTree::Ident(ident) if ident == "benchmarks" => { + if matches!(tokens.get(idx + 1), Some(TokenTree::Punct(punct)) if punct.as_char() == '!') + { + if let Some(TokenTree::Group(group)) = tokens.get(idx + 2) { + if group.delimiter() == Delimiter::Brace { + collect_legacy_benchmark_names(&group.stream(), benchmarks); + idx += 3; + continue; + } + } + } + } + TokenTree::Group(group) => { + collect_legacy_benchmarks_from_tokens(&group.stream(), benchmarks); + } + _ => {} + } + + idx += 1; + } +} + +pub(super) fn collect_legacy_benchmark_names( + tokens: &TokenStream, + benchmarks: &mut BTreeSet, +) { + let tokens: Vec<_> = tokens.clone().into_iter().collect(); + let mut idx = 0; + + while idx < tokens.len() { + let TokenTree::Ident(ident) = &tokens[idx] else { + idx += 1; + continue; + }; + + let name = ident.to_string(); + if matches!( + name.as_str(), + "where_clause" | "verify" | "impl_benchmark_test_suite" + ) { + idx += 1; + continue; + } + + let mut lookahead = idx + 1; + if matches!( + tokens.get(lookahead), + Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Parenthesis + ) { + lookahead += 1; + } + + if matches!( + tokens.get(lookahead), + Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Brace + ) { + benchmarks.insert(name); + } + + idx += 1; + } +} diff --git a/support/linting/src/require_extrinsic_benchmarks/dispatchable_scan.rs b/support/linting/src/require_extrinsic_benchmarks/dispatchable_scan.rs new file mode 100644 index 0000000000..a274221f64 --- /dev/null +++ b/support/linting/src/require_extrinsic_benchmarks/dispatchable_scan.rs @@ -0,0 +1,296 @@ +//! Locate `#[pallet::call]` dispatchables and their `#[pallet::weight]` attribute clusters. +//! +//! Skips `#[cfg(test)]` / feature-gated call impls so mock-only extrinsics are not required +//! to have production benchmarks. + +use super::Dispatchable; +use super::source_scan::*; + +pub(super) fn collect_dispatchables_from_source(source: &str) -> Vec { + let masked = mask_comments_and_strings(source); + let non_runtime_ranges = collect_non_runtime_cfg_ranges(&masked); + let mut dispatchables = Vec::new(); + let mut search_from = 0; + + while let Some((attr_start, attr_end)) = find_next_attr(&masked, search_from, "pallet::call") { + search_from = attr_end; + + if is_in_ranges(attr_start, &non_runtime_ranges) + || has_non_runtime_cfg_attr_before(&masked, attr_start, 0) + { + continue; + } + + let Some(impl_pos) = find_word(&masked, "impl", attr_end) else { + continue; + }; + let Some(open_brace) = masked[impl_pos..].find('{').map(|offset| impl_pos + offset) else { + continue; + }; + let Some(close_brace) = find_matching_brace(&masked, open_brace) else { + continue; + }; + + if is_in_ranges(impl_pos, &non_runtime_ranges) { + search_from = close_brace + 1; + continue; + } + + collect_pub_fns_in_impl( + source, + &masked, + open_brace + 1, + close_brace, + &non_runtime_ranges, + &mut dispatchables, + ); + search_from = close_brace + 1; + } + + dispatchables +} + +pub(super) fn collect_pub_fns_in_impl( + source: &str, + masked: &str, + start: usize, + end: usize, + non_runtime_ranges: &[(usize, usize)], + dispatchables: &mut Vec, +) { + let bytes = masked.as_bytes(); + let mut idx = start; + let mut depth = 0usize; + + while idx < end { + match bytes[idx] { + b'{' => { + depth = depth.saturating_add(1); + idx += 1; + } + b'}' => { + depth = depth.saturating_sub(1); + idx += 1; + } + _ if depth == 0 && starts_with_word(masked, idx, "pub") => { + if is_in_ranges(idx, non_runtime_ranges) + || has_non_runtime_cfg_attr_before(masked, idx, start) + { + idx += 3; + continue; + } + + let mut cursor = skip_ws(masked, idx + 3); + + // Support `pub(crate) fn` even though FRAME dispatchables are normally `pub fn`. + if masked.as_bytes().get(cursor) == Some(&b'(') { + if let Some(close) = find_matching_paren(masked, cursor) { + cursor = skip_ws(masked, close + 1); + } + } + + if starts_with_word(masked, cursor, "fn") { + cursor = skip_ws(masked, cursor + 2); + if let Some((name, _name_end)) = parse_ident(masked, cursor) { + let (line, column) = line_column(source, cursor); + let weight_attr = preceding_weight_attr(source, masked, idx, start); + dispatchables.push(Dispatchable { + name, + line, + column, + weight_attr, + }); + } + } + + idx += 3; + } + _ => idx += 1, + } + } +} + +pub(super) fn preceding_weight_attr( + source: &str, + masked: &str, + item_start: usize, + scope_start: usize, +) -> Option { + // `item_start` is the beginning of the dispatchable item as found by the + // source scanner, normally the `pub` in `pub fn`. Find the nearest + // #[pallet::weight(...)] before that item, then expand backward over the + // contiguous attribute cluster that belongs to the same dispatchable. + let prefix = masked.get(scope_start..item_start)?; + let attr_start = prefix.rfind("#[pallet::weight")? + scope_start; + + // Do not accidentally reuse a previous dispatchable's weight attr when the + // current item has no weight. If another function begins between the attr + // and this item, this attr is not for the current dispatchable. + let attr_to_item = masked.get(attr_start..item_start)?; + if attr_to_item.contains("pub fn ") || attr_to_item.contains("pub(crate) fn ") { + return None; + } + + let mut cluster_start = attr_start; + let mut cursor = attr_start; + while let Some(trimmed_end) = rtrim_ws(masked, scope_start, cursor) { + if masked.as_bytes().get(trimmed_end) != Some(&b']') { + break; + } + let Some(prev_attr_start) = masked + .get(scope_start..=trimmed_end) + .and_then(|section| section.rfind("#[")) + else { + break; + }; + cluster_start = scope_start + prev_attr_start; + cursor = cluster_start; + } + + source.get(cluster_start..item_start).map(ToOwned::to_owned) +} + +pub(super) fn find_next_attr(masked: &str, from: usize, attr_path: &str) -> Option<(usize, usize)> { + let mut search_from = from; + while let Some(offset) = masked[search_from..].find("#[") { + let start = search_from + offset; + let Some(close) = masked[start..].find(']').map(|offset| start + offset) else { + return None; + }; + let normalized: String = masked[start..=close] + .chars() + .filter(|ch| !ch.is_whitespace()) + .collect(); + let expected = format!("#[{attr_path}"); + + if normalized.starts_with(&expected) + && matches!( + normalized.as_bytes().get(expected.len()), + Some(b']') | Some(b'(') + ) + { + return Some((start, close + 1)); + } + + search_from = close + 1; + } + + None +} + +pub(super) fn collect_non_runtime_cfg_ranges(masked: &str) -> Vec<(usize, usize)> { + let mut ranges = Vec::new(); + let mut search_from = 0; + + while let Some((attr_start, attr_end)) = find_next_attr(masked, search_from, "cfg") { + search_from = attr_end; + + if !is_non_runtime_cfg_attr(&masked[attr_start..attr_end]) { + continue; + } + + let item_start = skip_outer_attrs(masked, skip_ws(masked, attr_end)); + let Some(open_brace) = find_item_open_brace(masked, item_start) else { + ranges.push((attr_start, end_of_line(masked, attr_end))); + continue; + }; + let Some(close_brace) = find_matching_brace(masked, open_brace) else { + ranges.push((attr_start, end_of_line(masked, attr_end))); + continue; + }; + + ranges.push((attr_start, close_brace + 1)); + search_from = close_brace + 1; + } + + ranges +} + +pub(super) fn has_non_runtime_cfg_attr_before( + masked: &str, + item_start: usize, + scope_start: usize, +) -> bool { + let mut cursor = item_start; + + loop { + let Some(trimmed_end) = rtrim_ws(masked, scope_start, cursor) else { + return false; + }; + if masked.as_bytes().get(trimmed_end) != Some(&b']') { + return false; + } + + let Some(attr_start) = masked[scope_start..=trimmed_end].rfind("#[") else { + return false; + }; + let attr_start = scope_start + attr_start; + let attr = &masked[attr_start..=trimmed_end]; + if is_non_runtime_cfg_attr(attr) { + return true; + } + + cursor = attr_start; + } +} + +pub(super) fn is_non_runtime_cfg_attr(attr: &str) -> bool { + let normalized: String = attr.chars().filter(|ch| !ch.is_whitespace()).collect(); + let Some(cfg) = normalized + .strip_prefix("#[cfg(") + .and_then(|value| value.strip_suffix(")]")) + else { + return false; + }; + + cfg == "test" + || cfg.contains("feature=") + || cfg.starts_with("all(test,") + || cfg.starts_with("any(test,") + || cfg.contains(",test,") + || cfg.contains(",test)") +} + +pub(super) fn skip_outer_attrs(masked: &str, mut idx: usize) -> usize { + loop { + idx = skip_ws(masked, idx); + if !masked[idx..].starts_with("#[") { + return idx; + } + let Some(close) = masked[idx..].find(']').map(|offset| idx + offset) else { + return idx; + }; + idx = close + 1; + } +} + +pub(super) fn find_item_open_brace(masked: &str, item_start: usize) -> Option { + let bytes = masked.as_bytes(); + let mut idx = item_start; + let mut paren_depth = 0usize; + let mut bracket_depth = 0usize; + let mut angle_depth = 0usize; + + while idx < bytes.len() { + match bytes[idx] { + b'(' => paren_depth += 1, + b')' => paren_depth = paren_depth.saturating_sub(1), + b'[' => bracket_depth += 1, + b']' => bracket_depth = bracket_depth.saturating_sub(1), + b'<' => angle_depth += 1, + b'>' => angle_depth = angle_depth.saturating_sub(1), + b';' if paren_depth == 0 && bracket_depth == 0 && angle_depth == 0 => return None, + b'{' if paren_depth == 0 && bracket_depth == 0 && angle_depth == 0 => return Some(idx), + _ => {} + } + idx += 1; + } + + None +} + +pub(super) fn is_in_ranges(idx: usize, ranges: &[(usize, usize)]) -> bool { + ranges + .iter() + .any(|(start, end)| idx >= *start && idx < *end) +} diff --git a/support/linting/src/require_extrinsic_benchmarks/mod.rs b/support/linting/src/require_extrinsic_benchmarks/mod.rs new file mode 100644 index 0000000000..df3523b69c --- /dev/null +++ b/support/linting/src/require_extrinsic_benchmarks/mod.rs @@ -0,0 +1,127 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::collapsible_if, + clippy::indexing_slicing, + clippy::question_mark +)] + +//! Ensure every runtime dispatchable has a matching benchmark and plugs `WeightInfo`. +//! +//! `Lint::lint` is a no-op per file: dispatchables and benchmarks live in different paths, so +//! [`RequireExtrinsicBenchmarks::lint_workspace`] (invoked from workspace `build.rs`) walks +//! `pallets/` once and reports missing benchmarks or unplugged weights. + +use super::*; +use std::fs; +use std::path::Path; +use syn::File; + +mod benchmark_scan; +mod dispatchable_scan; +mod pallet_paths; +mod source_scan; +mod weight_info_plug; + +use benchmark_scan::collect_benchmarks_for_pallet; +use dispatchable_scan::collect_dispatchables_from_source; +use pallet_paths::{ + benchmark_location_hint, collect_runtime_rust_files, display_path, find_pallet_root, +}; +use weight_info_plug::{ + is_benchmarked_weight_plugged, source_has_matching_weight_info_for_dispatchable, +}; + +/// Workspace lint: every non-`_` dispatchable must have a same-named benchmark and WeightInfo plug. +pub struct RequireExtrinsicBenchmarks; + +impl Lint for RequireExtrinsicBenchmarks { + fn lint(_source: &File) -> Result { + // Dispatchables and benchmarks live in different files, so build.rs runs + // the real check once at workspace scope via `lint_workspace`. + Ok(()) + } +} + +impl RequireExtrinsicBenchmarks { + /// Scan all runtime pallet sources under `workspace_root/pallets` for unpaired extrinsics. + pub fn lint_workspace(workspace_root: &Path) -> Vec { + let pallets_dir = workspace_root.join("pallets"); + if !pallets_dir.is_dir() { + return Vec::new(); + } + + let mut rust_files = Vec::new(); + collect_runtime_rust_files(&pallets_dir, &mut rust_files); + + let mut errors = Vec::new(); + for file in rust_files { + let Ok(source) = fs::read_to_string(&file) else { + continue; + }; + + let dispatchables = collect_dispatchables_from_source(&source); + if dispatchables.is_empty() { + continue; + } + + let pallet_root = find_pallet_root(&file, workspace_root); + let benchmarks = collect_benchmarks_for_pallet(&pallet_root); + let benchmark_hint = benchmark_location_hint(&pallet_root, workspace_root); + let file_path = display_path(&file, workspace_root); + + for dispatchable in dispatchables { + if dispatchable.name.starts_with('_') { + continue; + } + + if !benchmarks.contains(&dispatchable.name) { + errors.push(format!( + "{}:{}:{}: dispatchable extrinsic `{}` is missing a matching benchmark; add `#[benchmark] fn {}(...)` to {}", + file_path, + dispatchable.line, + dispatchable.column, + dispatchable.name, + dispatchable.name, + benchmark_hint, + )); + continue; + } + + let uses_matching_weight_info = is_benchmarked_weight_plugged( + &dispatchable.name, + dispatchable.weight_attr.as_deref(), + ) + || source_has_matching_weight_info_for_dispatchable( + &source, + &dispatchable.name, + ); + + if !uses_matching_weight_info { + errors.push(format!( + "{}:{}:{}: dispatchable extrinsic `{}` has a matching benchmark but its #[pallet::weight] does not call WeightInfo::{}(...); plug the generated benchmark weight into the dispatch annotation", + file_path, + dispatchable.line, + dispatchable.column, + dispatchable.name, + dispatchable.name, + )); + } + } + } + + errors + } +} + +/// One `pub fn` / `pub(crate) fn` found inside a `#[pallet::call]` impl. +#[derive(Debug, Clone, Eq, PartialEq)] +struct Dispatchable { + name: String, + line: usize, + column: usize, + weight_attr: Option, +} + +#[cfg(test)] +#[allow(clippy::expect_used, clippy::unwrap_used)] +mod tests; diff --git a/support/linting/src/require_extrinsic_benchmarks/pallet_paths.rs b/support/linting/src/require_extrinsic_benchmarks/pallet_paths.rs new file mode 100644 index 0000000000..bc6cd9f132 --- /dev/null +++ b/support/linting/src/require_extrinsic_benchmarks/pallet_paths.rs @@ -0,0 +1,133 @@ +//! Workspace path helpers for walking `pallets/` while skipping tests, mocks, and benchmarks. + +use std::fs; +use std::path::{Path, PathBuf}; + +pub(super) fn is_benchmark_file(file: &Path) -> bool { + file.file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.contains("benchmark")) + || file + .components() + .any(|component| component.as_os_str() == "benchmarks") +} + +pub(super) fn is_test_or_mock_source_path(path: &Path) -> bool { + path.components().any(|component| { + let Some(raw_name) = component.as_os_str().to_str() else { + return false; + }; + let name = raw_name.to_ascii_lowercase(); + let stem = Path::new(&name) + .file_stem() + .and_then(|stem| stem.to_str()) + .unwrap_or(&name); + + matches!( + stem, + "benchmark" + | "benchmarks" + | "benchmarking" + | "mock" + | "mocks" + | "test" + | "tests" + | "testing" + | "test_utils" + | "test_util" + | "test_helpers" + | "tests_helpers" + ) || stem.starts_with("mock_") + || stem.ends_with("_mock") + || stem.starts_with("test_") + || stem.ends_with("_test") + || stem.ends_with("_tests") + }) +} + +pub(super) fn find_pallet_root(file: &Path, workspace_root: &Path) -> PathBuf { + let pallets_dir = workspace_root.join("pallets"); + let mut current = file.parent(); + + while let Some(dir) = current { + if dir.starts_with(&pallets_dir) && dir.join("Cargo.toml").is_file() { + return dir.to_path_buf(); + } + + if dir == workspace_root { + break; + } + + current = dir.parent(); + } + + file.parent().unwrap_or(workspace_root).to_path_buf() +} + +pub(super) fn benchmark_location_hint(pallet_root: &Path, workspace_root: &Path) -> String { + for location in [ + pallet_root.join("src/benchmarks.rs"), + pallet_root.join("src/benchmarking.rs"), + ] { + if location.exists() { + return display_path(&location, workspace_root); + } + } + + display_path(&pallet_root.join("src/benchmarks.rs"), workspace_root) +} + +pub(super) fn collect_runtime_rust_files(dir: &Path, rust_files: &mut Vec) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; + + for entry in entries.flatten() { + let path = entry.path(); + if path + .components() + .any(|component| component.as_os_str() == "target" || component.as_os_str() == ".git") + { + continue; + } + + if is_test_or_mock_source_path(&path) { + continue; + } + + if path.is_dir() { + collect_runtime_rust_files(&path, rust_files); + } else if path.extension().and_then(|ext| ext.to_str()) == Some("rs") { + rust_files.push(path); + } + } +} + +pub(super) fn collect_rust_files(dir: &Path, rust_files: &mut Vec) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; + + for entry in entries.flatten() { + let path = entry.path(); + if path + .components() + .any(|component| component.as_os_str() == "target" || component.as_os_str() == ".git") + { + continue; + } + + if path.is_dir() { + collect_rust_files(&path, rust_files); + } else if path.extension().and_then(|ext| ext.to_str()) == Some("rs") { + rust_files.push(path); + } + } +} + +pub(super) fn display_path(path: &Path, workspace_root: &Path) -> String { + path.strip_prefix(workspace_root) + .unwrap_or(path) + .display() + .to_string() +} diff --git a/support/linting/src/require_extrinsic_benchmarks/source_scan.rs b/support/linting/src/require_extrinsic_benchmarks/source_scan.rs new file mode 100644 index 0000000000..0abac8019e --- /dev/null +++ b/support/linting/src/require_extrinsic_benchmarks/source_scan.rs @@ -0,0 +1,222 @@ +//! Mask comments/strings and locate identifiers/braces in pallet source text. +//! +//! These helpers underwrite the dispatchable and benchmark scanners: both walk +//! comment-masked source so string literals and `//` / `/* */` cannot fake a match. + +pub(super) fn mask_comments_and_strings(source: &str) -> String { + let bytes = source.as_bytes(); + let mut out = String::with_capacity(source.len()); + let mut idx = 0; + + while idx < bytes.len() { + match (bytes[idx], bytes.get(idx + 1).copied()) { + (b'/', Some(b'/')) => { + out.push(' '); + out.push(' '); + idx += 2; + while idx < bytes.len() && bytes[idx] != b'\n' { + out.push(' '); + idx += 1; + } + } + (b'/', Some(b'*')) => { + out.push(' '); + out.push(' '); + idx += 2; + let mut depth = 1usize; + while idx < bytes.len() && depth > 0 { + if bytes[idx] == b'\n' { + out.push('\n'); + idx += 1; + } else if bytes[idx] == b'/' && bytes.get(idx + 1) == Some(&b'*') { + out.push(' '); + out.push(' '); + idx += 2; + depth += 1; + } else if bytes[idx] == b'*' && bytes.get(idx + 1) == Some(&b'/') { + out.push(' '); + out.push(' '); + idx += 2; + depth -= 1; + } else { + out.push(' '); + idx += 1; + } + } + } + (b'"', _) => { + out.push(' '); + idx += 1; + let mut escaped = false; + while idx < bytes.len() { + let byte = bytes[idx]; + if byte == b'\n' { + out.push('\n'); + idx += 1; + break; + } + out.push(' '); + idx += 1; + if escaped { + escaped = false; + } else if byte == b'\\' { + escaped = true; + } else if byte == b'"' { + break; + } + } + } + (b'\'', _) if !bytes.get(idx + 1).is_some_and(|byte| is_ident_start(*byte)) => { + out.push(' '); + idx += 1; + let mut escaped = false; + while idx < bytes.len() { + let byte = bytes[idx]; + if byte == b'\n' { + out.push('\n'); + idx += 1; + break; + } + out.push(' '); + idx += 1; + if escaped { + escaped = false; + } else if byte == b'\\' { + escaped = true; + } else if byte == b'\'' { + break; + } + } + } + (byte, _) => { + out.push(byte as char); + idx += 1; + } + } + } + + out +} + +pub(super) fn find_matching_brace(masked: &str, open_brace: usize) -> Option { + find_matching_delimiter(masked, open_brace, b'{', b'}') +} + +pub(super) fn find_matching_paren(masked: &str, open_paren: usize) -> Option { + find_matching_delimiter(masked, open_paren, b'(', b')') +} + +pub(super) fn find_matching_delimiter( + masked: &str, + open: usize, + open_byte: u8, + close_byte: u8, +) -> Option { + let bytes = masked.as_bytes(); + if bytes.get(open) != Some(&open_byte) { + return None; + } + + let mut depth = 0usize; + for (idx, byte) in bytes.iter().enumerate().skip(open) { + if *byte == open_byte { + depth += 1; + } else if *byte == close_byte { + depth = depth.saturating_sub(1); + if depth == 0 { + return Some(idx); + } + } + } + + None +} + +pub(super) fn find_word(masked: &str, word: &str, from: usize) -> Option { + let mut search_from = from; + while let Some(offset) = masked[search_from..].find(word) { + let idx = search_from + offset; + if starts_with_word(masked, idx, word) { + return Some(idx); + } + search_from = idx + word.len(); + } + + None +} + +pub(super) fn starts_with_word(masked: &str, idx: usize, word: &str) -> bool { + let bytes = masked.as_bytes(); + let word_bytes = word.as_bytes(); + + if bytes.get(idx..idx + word_bytes.len()) != Some(word_bytes) { + return false; + } + + let before_ok = idx == 0 || !is_ident_continue(bytes[idx - 1]); + let after_idx = idx + word_bytes.len(); + let after_ok = after_idx >= bytes.len() || !is_ident_continue(bytes[after_idx]); + before_ok && after_ok +} + +pub(super) fn parse_ident(masked: &str, start: usize) -> Option<(String, usize)> { + let bytes = masked.as_bytes(); + let first = *bytes.get(start)?; + if !is_ident_start(first) { + return None; + } + + let mut end = start + 1; + while bytes.get(end).is_some_and(|byte| is_ident_continue(*byte)) { + end += 1; + } + + Some((masked[start..end].to_owned(), end)) +} + +pub(super) fn skip_ws(masked: &str, mut idx: usize) -> usize { + let bytes = masked.as_bytes(); + while bytes + .get(idx) + .is_some_and(|byte| byte.is_ascii_whitespace()) + { + idx += 1; + } + idx +} + +pub(super) fn rtrim_ws(masked: &str, start: usize, mut end: usize) -> Option { + let bytes = masked.as_bytes(); + while end > start + && bytes + .get(end - 1) + .is_some_and(|byte| byte.is_ascii_whitespace()) + { + end -= 1; + } + end.checked_sub(1).filter(|idx| *idx >= start) +} + +pub(super) fn end_of_line(masked: &str, from: usize) -> usize { + masked[from..] + .find('\n') + .map(|offset| from + offset) + .unwrap_or(masked.len()) +} + +pub(super) fn is_ident_start(byte: u8) -> bool { + byte == b'_' || byte.is_ascii_alphabetic() +} + +pub(super) fn is_ident_continue(byte: u8) -> bool { + is_ident_start(byte) || byte.is_ascii_digit() +} + +pub(super) fn line_column(source: &str, idx: usize) -> (usize, usize) { + let line = source[..idx].bytes().filter(|byte| *byte == b'\n').count() + 1; + let column = source[..idx] + .rfind('\n') + .map(|line_start| idx - line_start) + .unwrap_or(idx + 1); + (line, column) +} diff --git a/support/linting/src/require_extrinsic_benchmarks/tests.rs b/support/linting/src/require_extrinsic_benchmarks/tests.rs new file mode 100644 index 0000000000..9334536070 --- /dev/null +++ b/support/linting/src/require_extrinsic_benchmarks/tests.rs @@ -0,0 +1,360 @@ +//! Unit tests for extrinsic↔benchmark pairing and WeightInfo plug detection. + +use super::benchmark_scan::{collect_frame_v2_benchmarks, collect_legacy_benchmarks}; +use super::dispatchable_scan::collect_dispatchables_from_source; +use super::pallet_paths::is_test_or_mock_source_path; +use super::weight_info_plug::{ + is_benchmarked_weight_plugged, source_has_matching_weight_info_for_dispatchable, + weight_attr_calls_weight_info_for, +}; +use std::collections::BTreeSet; +use std::path::Path; + +#[test] +fn weightinfo_plug_helper_accepts_captured_attrs_and_custom_allow() { + let batch_attr = r#" + #[pallet::call_index(0)] + #[pallet::weight({ + let (dispatch_weight, pays) = Pallet::::batch_calls_weight_and_pays(calls); + let dispatch_weight = dispatch_weight + .saturating_add(T::WeightInfo::batch(calls.len() as u32)); + (dispatch_weight, DispatchClass::Normal, pays) + })] + "#; + assert!(is_benchmarked_weight_plugged("batch", Some(batch_attr))); + + let allow_attr = r#" + #[allow(unknown_lints, benchmarked_weight_not_plugged)] + #[pallet::weight(store_encrypted_weight())] + "#; + assert!(is_benchmarked_weight_plugged( + "store_encrypted", + Some(allow_attr), + )); +} + +#[test] +fn weightinfo_plug_matcher_accepts_exact_methods_and_rejects_prefixes() { + assert!(weight_attr_calls_weight_info_for( + "batch", + "#[pallet::weight(T::WeightInfo::batch(calls.len() as u32))]", + )); + assert!(weight_attr_calls_weight_info_for( + "proxy", + "#[pallet::weight(WeightInfo::::proxy(p.into()))]", + )); + assert!(weight_attr_calls_weight_info_for( + "set_weights", + "#[pallet::weight(::WeightInfo::set_weights())]", + )); + assert!(weight_attr_calls_weight_info_for( + "set_fee_rate", + "#[pallet::weight(::WeightInfo::set_fee_rate())]", + )); + assert!(weight_attr_calls_weight_info_for( + "swap_coldkey", + "#[pallet::weight(T::WeightInfo::swap_coldkey::())]", + )); + + assert!(!weight_attr_calls_weight_info_for( + "swap_coldkey", + "#[pallet::weight(T::WeightInfo::swap_coldkey_announced())]", + )); + assert!(!weight_attr_calls_weight_info_for( + "set_weight", + "#[pallet::weight(T::WeightInfo::set_weights())]", + )); +} + +#[test] +fn source_fallback_accepts_valid_complex_weight_attrs() { + let source = r#" + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight({ + let (dispatch_weight, pays) = Pallet::::batch_calls_weight_and_pays(calls); + let dispatch_weight = dispatch_weight + .saturating_add(T::WeightInfo::batch(calls.len() as u32)); + (dispatch_weight, DispatchClass::Normal, pays) + })] + pub fn batch(origin: OriginFor, calls: Vec) -> DispatchResult { + Ok(()) + } + } + "#; + + assert!(source_has_matching_weight_info_for_dispatchable( + source, "batch" + )); +} + +#[test] +fn source_fallback_does_not_use_previous_dispatchable_weight_attr() { + let source = r#" + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::first())] + pub fn first(origin: OriginFor) -> DispatchResult { Ok(()) } + + #[pallet::call_index(1)] + #[pallet::weight(Weight::from_parts(1, 0))] + pub fn second(origin: OriginFor) -> DispatchResult { Ok(()) } + } + "#; + + assert!(source_has_matching_weight_info_for_dispatchable( + source, "first" + )); + assert!(!source_has_matching_weight_info_for_dispatchable( + source, "second" + )); +} + +#[test] +fn weightinfo_plug_check_accepts_common_valid_forms() { + assert!(weight_attr_calls_weight_info_for( + "batch", + "#[pallet::weight({ let w = T::WeightInfo::batch(calls.len() as u32); w })]", + )); + assert!(weight_attr_calls_weight_info_for( + "set_fee_rate", + "#[pallet::weight(::WeightInfo::set_fee_rate())]", + )); + assert!(weight_attr_calls_weight_info_for( + "set_weights", + "#[pallet::weight((::WeightInfo::set_weights(), DispatchClass::Normal, Pays::No))]", + )); + assert!(weight_attr_calls_weight_info_for( + "proxy", + "#[pallet::weight((WeightInfo::::proxy(T::MaxProxies::get()), DispatchClass::Normal))]", + )); + assert!(!weight_attr_calls_weight_info_for( + "proxy", + "#[pallet::weight(Weight::from_parts(10, 0))]", + )); +} + +#[test] +fn dispatchable_weight_attr_is_found_for_complex_weight_blocks() { + let input = r#" + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight({ + let (dispatch_weight, pays) = Pallet::::batch_calls_weight_and_pays(calls); + let dispatch_weight = dispatch_weight + .saturating_add(T::WeightInfo::batch(calls.len() as u32)); + (dispatch_weight, DispatchClass::Normal, pays) + })] + pub fn batch(origin: OriginFor, calls: Vec) -> DispatchResult { + Ok(()) + } + } + "#; + + let dispatchables = collect_dispatchables_from_source(input); + let batch = dispatchables + .iter() + .find(|dispatchable| dispatchable.name == "batch") + .expect("batch dispatchable is collected"); + + assert!(is_benchmarked_weight_plugged( + &batch.name, + batch.weight_attr.as_deref(), + )); +} + +#[test] +fn custom_allow_attr_skips_weightinfo_plug_check() { + let dispatch_source = r#" + #[pallet::call] + impl Pallet { + #[allow(unknown_lints, benchmarked_weight_not_plugged)] + #[pallet::call_index(2)] + #[pallet::weight(store_encrypted_weight())] + pub fn store_encrypted(origin: OriginFor) -> DispatchResult { Ok(()) } + } + "#; + + let dispatchables = collect_dispatchables_from_source(dispatch_source); + let dispatchable = dispatchables + .iter() + .find(|dispatchable| dispatchable.name == "store_encrypted") + .expect("store_encrypted dispatchable is collected"); + + assert!(is_benchmarked_weight_plugged( + &dispatchable.name, + dispatchable.weight_attr.as_deref() + )); +} + +#[test] +fn collects_dispatchables_from_pallet_call_impl() { + let input = r#" + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + pub fn set_weights(origin: OriginFor) -> DispatchResult { + Ok(()) + } + + fn helper() {} + } + "#; + + let dispatchables = collect_dispatchables_from_source(input); + assert_eq!(dispatchables.len(), 1); + assert_eq!(dispatchables[0].name, "set_weights"); +} + +#[test] +fn ignores_cfg_test_pallet_call_impls() { + let input = r#" + #[cfg(test)] + mod tests { + #[pallet::call] + impl Pallet { + pub fn mock_only(origin: OriginFor) -> DispatchResult { + Ok(()) + } + } + } + + #[pallet::call] + impl Pallet { + pub fn real_call(origin: OriginFor) -> DispatchResult { + Ok(()) + } + } + "#; + + let dispatchables = collect_dispatchables_from_source(input); + assert_eq!(dispatchables.len(), 1); + assert_eq!(dispatchables[0].name, "real_call"); +} + +#[test] +fn ignores_cfg_test_dispatchable_fns_inside_real_call_impls() { + let input = r#" + #[pallet::call] + impl Pallet { + #[cfg(test)] + pub fn mock_only(origin: OriginFor) -> DispatchResult { + Ok(()) + } + + pub fn real_call(origin: OriginFor) -> DispatchResult { + Ok(()) + } + } + "#; + + let dispatchables = collect_dispatchables_from_source(input); + assert_eq!(dispatchables.len(), 1); + assert_eq!(dispatchables[0].name, "real_call"); +} + +#[test] +fn ignores_feature_gated_dispatchable_fns_inside_real_call_impls() { + let input = r#" + #[pallet::call] + impl Pallet { + #[cfg(feature = "pow-faucet")] + pub fn faucet(origin: OriginFor) -> DispatchResult { + Ok(()) + } + + pub fn real_call(origin: OriginFor) -> DispatchResult { + Ok(()) + } + } + "#; + + let dispatchables = collect_dispatchables_from_source(input); + assert_eq!(dispatchables.len(), 1); + assert_eq!(dispatchables[0].name, "real_call"); +} + +#[test] +fn recognizes_mock_and_test_paths_as_non_runtime() { + assert!(is_test_or_mock_source_path(Path::new( + "pallets/example/src/mock.rs" + ))); + assert!(is_test_or_mock_source_path(Path::new( + "pallets/example/src/tests/register.rs" + ))); + assert!(is_test_or_mock_source_path(Path::new( + "pallets/example/src/benchmarking.rs" + ))); + assert!(!is_test_or_mock_source_path(Path::new( + "pallets/example/src/macros/dispatches.rs" + ))); +} + +#[test] +fn collects_frame_v2_benchmarks() { + let input = r#" + #[benchmarks] + mod benchmarks { + #[benchmark] + fn set_weights() { + #[block] + {} + } + + fn helper() {} + } + "#; + + let mut benchmarks = BTreeSet::new(); + collect_frame_v2_benchmarks(input, &mut benchmarks); + assert!(benchmarks.contains("set_weights")); + assert!(!benchmarks.contains("helper")); +} + +#[test] +fn collects_legacy_benchmarks_macro_names() { + let input = r#" + benchmarks! { + where_clause { where T: Config } + + set_weights { + let caller = account("caller", 0, 0); + }: _(RawOrigin::Signed(caller)) + verify {} + } + "#; + + let mut benchmarks = BTreeSet::new(); + collect_legacy_benchmarks(input, &mut benchmarks); + assert!(benchmarks.contains("set_weights")); + assert!(!benchmarks.contains("where_clause")); + assert!(!benchmarks.contains("verify")); +} + +#[test] +fn register_limit_is_missing_when_no_matching_benchmark_exists() { + let dispatch_source = r#" + #[pallet::call] + impl Pallet { + #[pallet::call_index(134)] + pub fn register_limit(origin: OriginFor) -> DispatchResult { Ok(()) } + } + "#; + let benchmark_source = r#" + #[benchmarks] + mod benchmarks { + #[benchmark] + fn root_register() { #[block] {} } + } + "#; + + let dispatchables = collect_dispatchables_from_source(dispatch_source); + let mut benchmarks = BTreeSet::new(); + collect_frame_v2_benchmarks(benchmark_source, &mut benchmarks); + + assert_eq!(dispatchables[0].name, "register_limit"); + assert!(!benchmarks.contains(&dispatchables[0].name)); +} diff --git a/support/linting/src/require_extrinsic_benchmarks/weight_info_plug.rs b/support/linting/src/require_extrinsic_benchmarks/weight_info_plug.rs new file mode 100644 index 0000000000..fbc631393f --- /dev/null +++ b/support/linting/src/require_extrinsic_benchmarks/weight_info_plug.rs @@ -0,0 +1,169 @@ +//! Check that a dispatchable's `#[pallet::weight]` plugs generated `WeightInfo::`. +//! +//! Also recognizes the custom allow marker `benchmarked_weight_not_plugged` (paired with +//! `unknown_lints`) used when a weight expression intentionally does not call WeightInfo. + +pub(super) fn source_has_matching_weight_info_for_dispatchable(source: &str, name: &str) -> bool { + // If weight_attr capture failed, fall back to the source text around the + // dispatchable itself. We intentionally keep this as a fallback instead of + // replacing the structured collection path: the lint is a source scanner + // over FRAME macro input, and complex #[pallet::weight({ ... })] blocks can + // confuse the backwards attribute walk even though the dispatch is valid. + for needle in [format!("pub fn {name}"), format!("pub(crate) fn {name}")] { + let mut search_from = 0usize; + + while let Some(offset) = source + .get(search_from..) + .and_then(|tail| tail.find(&needle)) + { + let fn_pos = search_from.saturating_add(offset); + let Some(prefix) = source.get(..fn_pos) else { + break; + }; + let Some(attr_start) = prefix.rfind("#[pallet::weight") else { + search_from = fn_pos.saturating_add(needle.len()); + continue; + }; + let Some(attr) = source.get(attr_start..fn_pos) else { + search_from = fn_pos.saturating_add(needle.len()); + continue; + }; + + // If another dispatchable starts between that attr and this function, + // the attr belongs to the earlier dispatchable, not this one. + if attr.contains("pub fn ") || attr.contains("pub(crate) fn ") { + search_from = fn_pos.saturating_add(needle.len()); + continue; + } + + let normalized = normalize_attr(attr); + if normalized.contains("benchmarked_weight_not_plugged") + || weight_attr_calls_weight_info_for(name, attr) + { + return true; + } + + search_from = fn_pos.saturating_add(needle.len()); + } + } + + false +} + +pub(super) const BENCHMARKED_WEIGHT_NOT_PLUGGED_ALLOW: &str = "benchmarked_weight_not_plugged"; + +pub(super) fn has_benchmark_weightinfo_plug_ignore_attr(weight_attr_cluster: &str) -> bool { + let attr = normalize_attr(weight_attr_cluster); + attr.contains("allow(") && attr.contains(BENCHMARKED_WEIGHT_NOT_PLUGGED_ALLOW) +} + +pub(super) fn weight_attr_calls_weight_info_for(name: &str, weight_attr: &str) -> bool { + let normalized = normalize_attr(weight_attr); + if !normalized.contains("WeightInfo") { + return false; + } + + let mut search_from = 0usize; + while let Some(relative_method_start) = normalized + .get(search_from..) + .and_then(|tail| tail.find(name)) + { + let method_start = search_from + relative_method_start; + + // Method name must be reached through `::name`, not as part of another + // identifier. This rejects `WeightInfo::swap_coldkey_announced()` for a + // dispatchable named `swap_coldkey`. + if method_start < 2 || normalized.get(method_start - 2..method_start) != Some("::") { + search_from = method_start.saturating_add(name.len()); + continue; + } + + let after_name = method_start.saturating_add(name.len()); + if !is_call_boundary_after_method(&normalized, after_name) { + search_from = after_name; + continue; + } + + let before_method = &normalized[..method_start - 2]; + let Some(weight_info_start) = before_method.rfind("WeightInfo") else { + search_from = after_name; + continue; + }; + let after_weight_info = weight_info_start.saturating_add("WeightInfo".len()); + let between = &before_method[after_weight_info..]; + + // Accept: + // T::WeightInfo::foo(...) + // ::WeightInfo::foo(...) + // ::WeightInfo::foo(...) + // WeightInfo::::foo(...) + if between.is_empty() || turbofish_suffix_consumes_all(between) { + return true; + } + + search_from = after_name; + } + + false +} + +pub(super) fn is_call_boundary_after_method(source: &str, after_name: usize) -> bool { + match source.get(after_name..) { + Some(rest) if rest.starts_with('(') => true, + Some(rest) if rest.starts_with("::<") => skip_turbofish_generics(source, after_name) + .and_then(|call_start| source.get(call_start..)) + .is_some_and(|rest| rest.starts_with('(')), + _ => false, + } +} + +pub(super) fn turbofish_suffix_consumes_all(suffix: &str) -> bool { + suffix.starts_with("::<") + && skip_turbofish_generics(suffix, 0).is_some_and(|end| end == suffix.len()) +} + +pub(super) fn skip_turbofish_generics(source: &str, start: usize) -> Option { + if !source.get(start..)?.starts_with("::<") { + return None; + } + + let bytes = source.as_bytes(); + let mut idx = start.checked_add(3)?; + let mut angle_depth = 1usize; + + while let Some(byte) = bytes.get(idx).copied() { + match byte { + b'<' => angle_depth = angle_depth.saturating_add(1), + b'>' => { + angle_depth = angle_depth.saturating_sub(1); + if angle_depth == 0 { + return idx.checked_add(1); + } + } + _ => {} + } + idx = idx.checked_add(1)?; + } + + None +} + +pub(super) fn is_benchmarked_weight_plugged(name: &str, weight_attr: Option<&str>) -> bool { + let Some(weight_attr) = weight_attr else { + return false; + }; + + // This is our custom-lint allow marker. It intentionally uses an unknown + // lint name plus `unknown_lints` so rustc accepts the attribute while this + // source scanner can still recognize it. + if normalize_attr(weight_attr).contains("benchmarked_weight_not_plugged") { + return true; + } + + has_benchmark_weightinfo_plug_ignore_attr(weight_attr) + || weight_attr_calls_weight_info_for(name, weight_attr) +} + +pub(super) fn normalize_attr(attr: &str) -> String { + attr.chars().filter(|ch| !ch.is_whitespace()).collect() +} diff --git a/support/linting/src/require_freeze_struct.rs b/support/linting/src/require_freeze_struct.rs index 288f2a2b1a..4a4e0ac682 100644 --- a/support/linting/src/require_freeze_struct.rs +++ b/support/linting/src/require_freeze_struct.rs @@ -1,14 +1,19 @@ +//! Require `#[freeze_struct("…")]` on every struct that derives `Encode` and/or `Decode`. +//! +//! Layout hashes catch accidental SCALE field reorder/type changes that corrupt storage. + use super::*; use syn::{ Attribute, File, ItemStruct, Meta, MetaList, Path, Token, parse_quote, punctuated::Punctuated, visit::Visit, }; +/// Lint: structs deriving `Encode`/`Decode` must also carry `#[freeze_struct("hash")]`. pub struct RequireFreezeStruct; impl Lint for RequireFreezeStruct { fn lint(source: &File) -> Result { - let mut visitor = EncodeDecodeVisitor::default(); + let mut visitor = FreezeStructEncodeDecodeVisitor::default(); visitor.visit_file(source); @@ -21,14 +26,14 @@ impl Lint for RequireFreezeStruct { } #[derive(Default)] -struct EncodeDecodeVisitor { +struct FreezeStructEncodeDecodeVisitor { errors: Vec, } -impl<'ast> Visit<'ast> for EncodeDecodeVisitor { +impl<'ast> Visit<'ast> for FreezeStructEncodeDecodeVisitor { fn visit_item_struct(&mut self, node: &'ast ItemStruct) { let has_encode_decode = node.attrs.iter().any(is_derive_encode_or_decode); - let has_freeze_struct = node.attrs.iter().any(is_freeze_struct); + let has_freeze_struct = node.attrs.iter().any(attr_is_freeze_struct); if has_encode_decode && !has_freeze_struct { self.errors.push(syn::Error::new( @@ -41,7 +46,8 @@ impl<'ast> Visit<'ast> for EncodeDecodeVisitor { } } -fn is_freeze_struct(attr: &Attribute) -> bool { +/// True when the attribute is `#[freeze_struct("…")]` with a non-empty hash argument. +fn attr_is_freeze_struct(attr: &Attribute) -> bool { if let Meta::List(meta_list) = &attr.meta { let Some(seg) = meta_list.path.segments.last() else { return false; @@ -73,7 +79,7 @@ mod tests { fn lint_struct(input: &str) -> Result { let item_struct: ItemStruct = syn::parse_str(input).expect("should only use on a struct"); - let mut visitor = EncodeDecodeVisitor::default(); + let mut visitor = FreezeStructEncodeDecodeVisitor::default(); visitor.visit_item_struct(&item_struct); if !visitor.errors.is_empty() { return Err(visitor.errors); diff --git a/support/macros/src/call_filter_group.rs b/support/macros/src/call_filter_group.rs index 37bbf994cc..98ccc91c36 100644 --- a/support/macros/src/call_filter_group.rs +++ b/support/macros/src/call_filter_group.rs @@ -1,3 +1,8 @@ +//! Expand `call_filter_group!(Name, [RuntimeCall::Pallet(pallet::Call::method), …])`. +//! +//! Emits `Contains` for proxy/filter execution and `CallFilterMetadata` for the +//! runtime API that lists allowed calls (and optional param/nested-call constraints). + use proc_macro2::TokenStream as TokenStream2; use quote::quote; use syn::{ @@ -6,7 +11,7 @@ use syn::{ punctuated::Punctuated, }; -/// Parsed input for one call filter group. +/// Parsed input for one call filter group (`GroupName` + allowlisted `RuntimeCall` paths). pub struct CallFilterGroupInput { group: Ident, rules: Punctuated, diff --git a/support/macros/src/lib.rs b/support/macros/src/lib.rs index 0580e1fd62..940d982906 100644 --- a/support/macros/src/lib.rs +++ b/support/macros/src/lib.rs @@ -1,3 +1,9 @@ +//! Subtensor proc-macros: [`freeze_struct`] (SCALE layout hash) and [`call_filter_group`]. +//! +//! `freeze_struct` blanks doc *text* before hashing but keeps doc attributes — see +//! `refactor/FREEZE_STRUCT.md`. Adding/removing docs on a frozen struct changes the hash; +//! editing existing doc text does not. + use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::ToTokens; @@ -7,8 +13,10 @@ mod call_filter_group; mod visitor; use visitor::*; -/// Freezes the layout of a struct to the current hash of its fields, ensuring that future -/// changes require updating the hash. +/// Freezes the SCALE layout of a struct to a hex hash of its (doc-cleaned) token stream. +/// +/// Future field/type/order changes — or adding/removing doc attributes — require updating the +/// hash. Changing only the text inside an existing `///` does not. /// /// ``` /// use subtensor_macros::freeze_struct; @@ -40,6 +48,7 @@ pub fn call_filter_group(input: TokenStream) -> TokenStream { } } +/// Compare the attribute hash to [`generate_hash`] of the struct after [`CleanDocComments`]. fn freeze_struct_impl( attr: impl Into, tokens: impl Into, diff --git a/support/macros/src/visitor.rs b/support/macros/src/visitor.rs index a5fc15dc74..064e401ad6 100644 --- a/support/macros/src/visitor.rs +++ b/support/macros/src/visitor.rs @@ -1,6 +1,12 @@ +//! Doc-stripping visitor and stable hasher used by `#[freeze_struct("…")]`. + use ahash::RandomState; use syn::{parse_quote, visit_mut::VisitMut}; +/// Rewrites `#[doc = "…"]` to `#[doc = ""]` and strips the freeze_struct hash argument. +/// +/// Attribute *presence* is preserved so adding/removing docs still changes the hash; only the +/// doc string contents are blanked. pub struct CleanDocComments; impl CleanDocComments { @@ -21,6 +27,7 @@ impl VisitMut for CleanDocComments { } } +/// Stable `ahash` of a syn item using fixed seeds (must stay constant for freeze_struct hashes). pub fn generate_hash + Clone>(item: &T) -> u64 { let item = item.clone(); diff --git a/support/macros/tests/tests.rs b/support/macros/tests/tests.rs index 9c8159e440..bf18de6910 100644 --- a/support/macros/tests/tests.rs +++ b/support/macros/tests/tests.rs @@ -1,3 +1,5 @@ +//! Smoke compile-test that `#[freeze_struct("…")]` accepts a matching layout hash. + use subtensor_macros::freeze_struct; #[freeze_struct("ecdcaac0f6da589a")] diff --git a/support/procedural-fork/src/benchmark.rs b/support/procedural-fork/src/benchmark.rs index 61cda35c66..077c1d2949 100644 --- a/support/procedural-fork/src/benchmark.rs +++ b/support/procedural-fork/src/benchmark.rs @@ -15,7 +15,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Home of the parsing and expansion code for the new pallet benchmarking syntax +//! Parsing and expansion for FRAME pallet benchmarking macros (`#[benchmarks]`, `#[benchmark]`). +//! +//! Subtensor's `RequireExtrinsicBenchmarks` lint looks for the same attribute/fn shapes this +//! expander understands (plus legacy `benchmarks!`). use derive_syn_parse::Parse; use frame_support_procedural_tools::generate_access_from_frame_or_crate; diff --git a/support/procedural-fork/src/construct_runtime/expand/mod.rs b/support/procedural-fork/src/construct_runtime/expand/mod.rs index 2d3538fcff..7889414d09 100644 --- a/support/procedural-fork/src/construct_runtime/expand/mod.rs +++ b/support/procedural-fork/src/construct_runtime/expand/mod.rs @@ -15,6 +15,8 @@ // See the License for the specific language governing permissions and // limitations under the License +//! Expand `construct_runtime!` into outer enums, metadata, origins, and related runtime glue. + mod call; pub mod composite_helper; mod config; diff --git a/support/procedural-fork/src/construct_runtime/mod.rs b/support/procedural-fork/src/construct_runtime/mod.rs index cf39972461..969ed46112 100644 --- a/support/procedural-fork/src/construct_runtime/mod.rs +++ b/support/procedural-fork/src/construct_runtime/mod.rs @@ -15,7 +15,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Implementation of `construct_runtime`. +//! Implementation of `construct_runtime!` (parse + expand). +//! +//! Subtensor's `RequireExplicitPalletIndex` lint parses the same input via +//! `exports::construct_runtime::parse::RuntimeDeclaration` so pallet indices stay explicit. //! //! `construct_runtime` implementation is recursive and can generate code which will call itself in //! order to get all the pallet parts for each pallet. diff --git a/support/procedural-fork/src/construct_runtime/parse.rs b/support/procedural-fork/src/construct_runtime/parse.rs index d2b2a4e4f6..b87178da8d 100644 --- a/support/procedural-fork/src/construct_runtime/parse.rs +++ b/support/procedural-fork/src/construct_runtime/parse.rs @@ -15,6 +15,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Parse `construct_runtime!` input into [`RuntimeDeclaration`] / [`Pallet`] (exported for lints). + use frame_support_procedural_tools::syn_ext as ext; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; diff --git a/support/procedural-fork/src/lib.rs b/support/procedural-fork/src/lib.rs index c7f1472b2a..1caa453df7 100644 --- a/support/procedural-fork/src/lib.rs +++ b/support/procedural-fork/src/lib.rs @@ -1,14 +1,15 @@ -//! This crate is a fork of the `frame-support-procedural` crate from -//! `substrate/frame/support/procedural` in `polkadot-sdk`. The purpose of this fork is to -//! re-export all parsing code from the original crate to make it accessible to other crates, -//! since the original crate is a `proc-macro` crate and therefore cannot have any non-macro -//! public exports. If Parity ever decides to move the parsing code to a separate crate, this -//! fork will no longer need to exist, but right now this is the only reliable way to get -//! access to the core parsing logic of substrate. +//! Fork of `frame-support-procedural` that re-exports FRAME parse/expand APIs as a normal crate. //! -//! Tags will be created for each major version of `polkadot-sdk` that `subtensor` relies on, -//! on an as-needed, ad-hoc basis, and versions will matched the corresponding `polkadot-sdk` -//! version/tag name. +//! Upstream `frame-support-procedural` is a `proc-macro` crate, so its parsers (e.g. +//! `construct_runtime::parse::RuntimeDeclaration`) cannot be depended on by Subtensor's +//! workspace lints such as `RequireExplicitPalletIndex`. This fork mirrors the polkadot-sdk +//! version Subtensor pins and exposes those internals under `procedural_fork::exports`. +//! +//! Prefer searching here for `construct_runtime` / `#[pallet]` expansion behavior when the +//! lint or runtime tooling needs to understand FRAME macro input. Do not rename Substrate +//! symbols lightly — they are compared against upstream when rebasing the fork. +//! +//! Tags are created ad-hoc per major `polkadot-sdk` bump Subtensor consumes. #![recursion_limit = "512"] #![allow(warnings)] #![allow(clippy::all)] diff --git a/support/procedural-fork/src/pallet/expand/mod.rs b/support/procedural-fork/src/pallet/expand/mod.rs index ff4423f859..51493d9da7 100644 --- a/support/procedural-fork/src/pallet/expand/mod.rs +++ b/support/procedural-fork/src/pallet/expand/mod.rs @@ -15,6 +15,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Codegen for each `#[pallet::*]` part (call, storage, event, error, hooks, …). + mod call; mod composite; mod config; diff --git a/support/procedural-fork/src/runtime/expand/mod.rs b/support/procedural-fork/src/runtime/expand/mod.rs index b51012541b..efcf8f18e8 100644 --- a/support/procedural-fork/src/runtime/expand/mod.rs +++ b/support/procedural-fork/src/runtime/expand/mod.rs @@ -15,6 +15,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Expand `#[frame_support::runtime]` into pallet wiring and derived runtime enums. + use super::parse::runtime_types::RuntimeType; use crate::{ construct_runtime::{ diff --git a/support/procedural-fork/src/runtime/parse/mod.rs b/support/procedural-fork/src/runtime/parse/mod.rs index 67fc03c59f..d53a2dbb2e 100644 --- a/support/procedural-fork/src/runtime/parse/mod.rs +++ b/support/procedural-fork/src/runtime/parse/mod.rs @@ -15,6 +15,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Parse `#[frame_support::runtime]` module input (pallet decls, runtime struct, derives). + pub mod helper; pub mod pallet; pub mod pallet_decl; diff --git a/support/tools/src/bump_version.rs b/support/tools/src/bump_version.rs index 24e267f234..cedddecfff 100644 --- a/support/tools/src/bump_version.rs +++ b/support/tools/src/bump_version.rs @@ -1,3 +1,8 @@ +//! CLI: set `package.version` across the Subtensor crates listed in [`TOML_PATHS`]. +//! +//! Usage: `bump-version ` from the workspace root. Updates each path's `Cargo.toml` +//! in place (macros, pallets, runtime, node). + use clap::Parser; use semver::Version; use std::{ @@ -7,6 +12,7 @@ use std::{ }; use toml_edit::{DocumentMut, Item, Value}; +/// Workspace-relative crate dirs whose `Cargo.toml` `package.version` is bumped together. const TOML_PATHS: [&str; 8] = [ "support/macros", "pallets/commitments", diff --git a/support/tools/src/spec_version.rs b/support/tools/src/spec_version.rs index 20b75ac041..c498b5301f 100644 --- a/support/tools/src/spec_version.rs +++ b/support/tools/src/spec_version.rs @@ -1,3 +1,8 @@ +//! CLI: print the runtime `spec_version` from `node_subtensor_runtime::VERSION`. +//! +//! Used by release/CI scripts that need the on-chain-facing runtime version without parsing +//! `runtime/src/lib.rs` by hand. + use node_subtensor_runtime::VERSION; fn main() { diff --git a/support/weight-tools/src/weight_compare.rs b/support/weight-tools/src/weight_compare.rs index 7fd7dcca46..cc5d0b1f62 100644 --- a/support/weight-tools/src/weight_compare.rs +++ b/support/weight-tools/src/weight_compare.rs @@ -1,14 +1,13 @@ -//! Compare two weights.rs files and report benchmark-level drift. +//! Compare two generated `weights.rs` files and report per-benchmark weight/proof drift. //! -//! Parses both files with `syn`, extracts per-function weight data including -//! base values, proof sizes, and parameterized slopes, then compares the whole -//! generated weight/proof across the benchmarked component ranges with a -//! configurable percentage threshold. +//! Binary name: `weight-compare`. Parses both files with `syn`, extracts per-function +//! `WeightInfo` data (base ref-time, proof size, storage IO, component slopes/ranges), then +//! evaluates total weight/proof at component-range corners against a percentage threshold. //! //! Exit codes: -//! 0 — all within threshold -//! 1 — error -//! 2 — drift exceeds threshold +//! - 0 — all benchmarks within threshold +//! - 1 — parse/IO error +//! - 2 — drift exceeds `--threshold` (default 40%) use anyhow::{Context, Result}; use clap::Parser; @@ -275,14 +274,17 @@ fn signed_pct(old: u128, new: u128) -> f64 { } } +/// Worst-case signed ref-time percent drift across component-range corners. fn max_ref_time_drift(old: &WeightValues, new: &WeightValues) -> Drift { max_benchmark_drift(old, new, DriftMetric::RefTime) } +/// Worst-case signed proof-size percent drift across component-range corners. fn max_proof_size_drift(old: &WeightValues, new: &WeightValues) -> Drift { max_benchmark_drift(old, new, DriftMetric::ProofSize) } +/// Max absolute percent drift of `metric` over the union of old/new component ranges. fn max_benchmark_drift(old: &WeightValues, new: &WeightValues, metric: DriftMetric) -> Drift { let ranges = comparison_ranges(old, new); if ranges.is_empty() {