diff --git a/.github/workflows/check-devnet.yml b/.github/workflows/check-devnet.yml index 8d3db55001..56442398ce 100644 --- a/.github/workflows/check-devnet.yml +++ b/.github/workflows/check-devnet.yml @@ -4,7 +4,7 @@ on: pull_request: branches: [devnet, devnet-ready] types: [labeled, unlabeled, synchronize, opened] - + concurrency: group: check-devnet-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/check-docker.yml b/.github/workflows/check-docker.yml index da5054fd6d..c584e49905 100644 --- a/.github/workflows/check-docker.yml +++ b/.github/workflows/check-docker.yml @@ -2,7 +2,7 @@ name: Build Docker Image on: pull_request: - + concurrency: group: check-docker-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/check-finney.yml b/.github/workflows/check-finney.yml index 6b056ef97e..165c862f33 100644 --- a/.github/workflows/check-finney.yml +++ b/.github/workflows/check-finney.yml @@ -4,7 +4,7 @@ on: pull_request: branches: [finney, main] types: [labeled, unlabeled, synchronize, opened] - + concurrency: group: check-finney-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/check-node-compat.yml b/.github/workflows/check-node-compat.yml index b52a7a88ba..f9affd989b 100644 --- a/.github/workflows/check-node-compat.yml +++ b/.github/workflows/check-node-compat.yml @@ -30,7 +30,7 @@ jobs: run: | sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y --no-install-recommends -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" build-essential clang curl git make libssl-dev llvm libudev-dev protobuf-compiler pkg-config unzip - + - name: Install Rust uses: actions-rs/toolchain@v1 with: @@ -40,7 +40,7 @@ jobs: uses: Swatinem/rust-cache@v2 with: key: "check-node-compat-${{ matrix.version.name }}" - + - name: Checkout ${{ matrix.version.name }} uses: actions/checkout@v4 with: @@ -50,14 +50,14 @@ jobs: - name: Build ${{ matrix.version.name }} working-directory: ${{ matrix.version.name }} run: cargo build --release --locked - + - name: Upload ${{ matrix.version.name }} node binary uses: actions/upload-artifact@v4 with: name: node-subtensor-${{ matrix.version.name }} path: ${{ matrix.version.name }}/target/release/node-subtensor retention-days: 1 - + test: needs: [build] runs-on: [self-hosted, type-ccx33] @@ -67,13 +67,13 @@ jobs: with: name: node-subtensor-old path: /tmp/node-subtensor-old - + - name: Download new node binary uses: actions/download-artifact@v4 with: name: node-subtensor-new path: /tmp/node-subtensor-new - + - name: Set up Node.js uses: actions/setup-node@v4 with: @@ -82,7 +82,7 @@ jobs: - name: Install npm dependencies working-directory: ${{ github.workspace }}/.github/workflows/check-node-compat run: npm install - + - name: Run test working-directory: ${{ github.workspace }}/.github/workflows/check-node-compat run: npm run test \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a60f0f9d82..c935728eec 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -14,7 +14,7 @@ on: - devnet-ready - devnet - testnet - + concurrency: group: docker-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/require-clean-merges.yml b/.github/workflows/require-clean-merges.yml index dd7a8829e7..0cabbf4b87 100644 --- a/.github/workflows/require-clean-merges.yml +++ b/.github/workflows/require-clean-merges.yml @@ -34,7 +34,7 @@ jobs: else echo "MERGE_BRANCHES=devnet-ready devnet testnet main" >> $GITHUB_ENV fi - + - name: Add Fork Remote and Fetch PR Branch if: github.event.pull_request.head.repo.fork == true run: | @@ -68,7 +68,7 @@ jobs: for branch in $MERGE_BRANCHES; do echo "Checking merge from $branch into $PR_BRANCH_REF..." - + # Ensure PR branch is up to date git reset --hard $PR_BRANCH_REF @@ -79,7 +79,7 @@ jobs: echo "❌ Merge conflict detected when merging $branch into $PR_BRANCH_REF" exit 1 fi - + # Abort merge if one was started, suppressing errors if no merge happened git merge --abort 2>/dev/null || true done diff --git a/.github/workflows/typescript-e2e.yml b/.github/workflows/typescript-e2e.yml index 82c63e1356..16c514fd09 100644 --- a/.github/workflows/typescript-e2e.yml +++ b/.github/workflows/typescript-e2e.yml @@ -50,7 +50,7 @@ jobs: strategy: matrix: include: - - variant: release + - variant: release flags: "" - variant: fast flags: "--features fast-runtime" @@ -138,6 +138,6 @@ jobs: run: pnpm install --frozen-lockfile - name: Run tests - run: | + run: | cd ts-tests pnpm moonwall test ${{ matrix.test }} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index c2650935ed..6534ef15dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2374,7 +2374,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom 0.2.17", + "getrandom 0.2.16", "once_cell", "tiny-keccak", ] @@ -5536,9 +5536,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.17" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", @@ -5556,20 +5556,20 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasi 0.14.7+wasi-0.2.4", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.0", "wasip2", "wasip3", @@ -6966,7 +6966,7 @@ dependencies = [ "either", "futures", "futures-timer", - "getrandom 0.2.17", + "getrandom 0.2.16", "libp2p-allow-block-list", "libp2p-connection-limits", "libp2p-core", @@ -7892,9 +7892,9 @@ dependencies = [ [[package]] name = "ml-kem" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcaee19a45f916d98f24a551cc9a2cdae705a040e66f3cbc4f3a282ea6a2e982" +checksum = "8de49b3df74c35498c0232031bb7e85f9389f913e2796169c8ab47a53993a18f" dependencies = [ "hybrid-array", "kem", @@ -10859,6 +10859,7 @@ dependencies = [ "sha2 0.10.9", "share-pool", "sp-core", + "sp-debug-derive", "sp-io", "sp-keyring", "sp-runtime", @@ -13660,6 +13661,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -13695,7 +13702,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ "chacha20 0.10.0", - "getrandom 0.4.1", + "getrandom 0.4.2", "rand_core 0.10.0", ] @@ -13725,7 +13732,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.17", + "getrandom 0.2.16", ] [[package]] @@ -13843,7 +13850,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.17", + "getrandom 0.2.16", "libredox", "thiserror 1.0.69", ] @@ -13988,7 +13995,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.17", + "getrandom 0.2.16", "libc", "untrusted 0.9.0", "windows-sys 0.52.0", @@ -18297,6 +18304,7 @@ dependencies = [ "serde", "sp-arithmetic", "sp-core", + "sp-io", "sp-rpc", "sp-runtime", "substrate-fixed", diff --git a/Dockerfile b/Dockerfile index efa124db33..af6f550ede 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,7 @@ FROM ${BASE_IMAGE} AS subtensor # ---- security hardening: create least-privilege user ---- RUN addgroup --system --gid 10001 subtensor && \ adduser --system --uid 10001 --gid 10001 --home /home/subtensor --disabled-password subtensor - + # Install gosu for privilege dropping RUN apt-get update && apt-get install -y gosu && \ rm -rf /var/lib/apt/lists/* @@ -71,7 +71,7 @@ RUN chmod +x /entrypoint.sh EXPOSE 30333 9933 9944 -# Run entrypoint as root to handle permissions, then drop to subtensor user +# Run entrypoint as root to handle permissions, then drop to subtensor user # in the script USER root ENTRYPOINT ["/entrypoint.sh"] diff --git a/chain-extensions/src/mock.rs b/chain-extensions/src/mock.rs index 9c4b3bd4a6..bb65145897 100644 --- a/chain-extensions/src/mock.rs +++ b/chain-extensions/src/mock.rs @@ -468,7 +468,12 @@ impl PrivilegeCmp for OriginPrivilegeCmp { pub struct CommitmentsI; impl CommitmentsInterface for CommitmentsI { - fn purge_netuid(_netuid: NetUid) {} + fn purge_netuid( + _netuid: NetUid, + _weight_meter: &mut frame_support::weights::WeightMeter, + ) -> bool { + true + } } parameter_types! { diff --git a/common/Cargo.toml b/common/Cargo.toml index 9fa9bd1856..841f896bbd 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -19,6 +19,7 @@ scale-info.workspace = true serde.workspace = true sp-arithmetic.workspace = true sp-core.workspace = true +sp-io.workspace = true sp-runtime.workspace = true sp-rpc = { workspace = true, optional = true } substrate-fixed.workspace = true @@ -47,6 +48,7 @@ std = [ "serde/std", "sp-arithmetic/std", "sp-core/std", + "sp-io/std", "sp-runtime/std", "sp-rpc", "substrate-fixed/std", diff --git a/common/src/lib.rs b/common/src/lib.rs index a606dca71d..c0458b6cf8 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -12,6 +12,8 @@ use sp_runtime::{ MultiSignature, Vec, traits::{IdentifyAccount, Verify}, }; + +pub use sp_io::MultiRemovalResults; use subtensor_macros::freeze_struct; pub use currency::*; @@ -443,12 +445,69 @@ impl TypeInfo for NetUidStorageIndex { } } +#[macro_export] +macro_rules! WeightMeterWrapper { + ( $meter:expr, $weight:expr ) => {{ + if !$meter.can_consume($weight.clone()) { + return false; + } + $meter.consume($weight.clone()); + }}; +} + +#[macro_export] +macro_rules! LoopRemovePrefixWithWeightMeter { + ( $meter:expr, $weight:expr, $storage:ty, $netuid:expr ) => {{ + let weight = $weight.clone(); + let limit = if weight.is_zero() { + u32::MAX + } else { + match $meter.remaining().checked_div_per_component(&weight) { + Some(limit) => u32::try_from(limit).unwrap_or(u32::MAX), + None => return false, + } + }; + + let result: $crate::MultiRemovalResults = <$storage>::clear_prefix($netuid, limit, None); + $meter.consume(weight.saturating_mul(result.backend.into())); + + if !result.maybe_cursor.is_none() { + return false; + } + }}; +} + #[cfg(test)] mod tests { use super::*; + use frame_support::weights::WeightMeter; + const REF_TIME_WEIGHT: u64 = 100; + const PROOF_SIZE_WEIGHT: u64 = 100; #[test] fn netuid_has_u16_bin_repr() { assert_eq!(NetUid(5).encode(), 5u16.encode()); } + + fn test_weight(weight_meter: &mut WeightMeter, weight: Weight) -> bool { + WeightMeterWrapper!(weight_meter, weight); + true + } + + #[test] + fn test_weight_meter_wrapper() { + // Enough budget for one (ref, proof) unit of `weight`. + let remaining_weight = Weight::from_parts(REF_TIME_WEIGHT * 2, PROOF_SIZE_WEIGHT * 2); + let weight = Weight::from_parts(REF_TIME_WEIGHT, PROOF_SIZE_WEIGHT); + let mut weight_meter = WeightMeter::with_limit(remaining_weight); + assert!(test_weight(&mut weight_meter, weight)); + + // Not enough to consume 3x ref and 3x proof in one step. + let mut weight_meter = WeightMeter::with_limit(remaining_weight); + let consumed = test_weight( + &mut weight_meter, + Weight::from_parts(REF_TIME_WEIGHT * 3, PROOF_SIZE_WEIGHT * 3), + ); + assert!(!consumed); + } } diff --git a/contract-tests/get-metadata.sh b/contract-tests/get-metadata.sh index 64d76bff29..bb39ab818f 100755 --- a/contract-tests/get-metadata.sh +++ b/contract-tests/get-metadata.sh @@ -1,3 +1,3 @@ rm -rf .papi npx papi add devnet -w ws://localhost:9944 -npx papi ink add ./bittensor/target/ink/bittensor.json \ No newline at end of file +npx papi ink add ./bittensor/target/ink/bittensor.json \ No newline at end of file diff --git a/contract-tests/run-ci.sh b/contract-tests/run-ci.sh index 0ea0e72297..b2156f7fcd 100755 --- a/contract-tests/run-ci.sh +++ b/contract-tests/run-ci.sh @@ -7,8 +7,8 @@ cd contract-tests cd bittensor rustup component add rust-src -cargo install cargo-contract -cargo contract build --release +cargo install cargo-contract +cargo contract build --release cd ../.. diff --git a/contract-tests/test/eth.substrate-transfer.test.ts b/contract-tests/test/eth.substrate-transfer.test.ts index fc8073585c..55c44fa1b3 100644 --- a/contract-tests/test/eth.substrate-transfer.test.ts +++ b/contract-tests/test/eth.substrate-transfer.test.ts @@ -137,10 +137,10 @@ describe("Balance transfers between substrate and EVM", () => { const tx = api.tx.EVM.call({ source: source, target: target, - // it is U256 in the extrinsic. + // it is U256 in the extrinsic. value: [raoToEth(tao(1)), tao(0), tao(0), tao(0)], gas_limit: BigInt(1000000), - // it is U256 in the extrinsic. + // it is U256 in the extrinsic. max_fee_per_gas: [BigInt(10e9), BigInt(0), BigInt(0), BigInt(0)], max_priority_fee_per_gas: undefined, input: Binary.fromText(""), @@ -309,7 +309,7 @@ describe("Balance transfers between substrate and EVM", () => { // it is U256 in the extrinsic, the value is more than u64::MAX value: [raoToEth(tao(1)), tao(0), tao(0), tao(1)], gas_limit: BigInt(1000000), - // it is U256 in the extrinsic. + // it is U256 in the extrinsic. max_fee_per_gas: [BigInt(10e9), BigInt(0), BigInt(0), BigInt(0)], max_priority_fee_per_gas: undefined, input: Binary.fromText(""), diff --git a/contract-tests/test/wasm.contract.test.ts b/contract-tests/test/wasm.contract.test.ts index 465b3214f8..4b847cc1a0 100644 --- a/contract-tests/test/wasm.contract.test.ts +++ b/contract-tests/test/wasm.contract.test.ts @@ -70,7 +70,7 @@ describe("Test wasm contract", () => { } before(async () => { - // init variables got from await and async + // init variables got from await and async api = await getDevnetApi() await setAdminFreezeWindow(api); diff --git a/eco-tests/src/mock.rs b/eco-tests/src/mock.rs index 9ab48c12a7..3f92b97a78 100644 --- a/eco-tests/src/mock.rs +++ b/eco-tests/src/mock.rs @@ -349,7 +349,12 @@ impl PrivilegeCmp for OriginPrivilegeCmp { pub struct CommitmentsI; impl CommitmentsInterface for CommitmentsI { - fn purge_netuid(_netuid: NetUid) {} + fn purge_netuid( + _netuid: NetUid, + _weight_meter: &mut frame_support::weights::WeightMeter, + ) -> bool { + true + } } parameter_types! { diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index 9faf870cbe..a11d684af0 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -371,7 +371,12 @@ impl PrivilegeCmp for OriginPrivilegeCmp { pub struct CommitmentsI; impl pallet_subtensor::CommitmentsInterface for CommitmentsI { - fn purge_netuid(_netuid: NetUid) {} + fn purge_netuid( + _netuid: NetUid, + _weight_meter: &mut frame_support::weights::WeightMeter, + ) -> bool { + true + } } pub struct GrandpaInterfaceImpl; diff --git a/pallets/admin-utils/src/tests/mod.rs b/pallets/admin-utils/src/tests/mod.rs index c94e1e96e8..c568ff1c3d 100644 --- a/pallets/admin-utils/src/tests/mod.rs +++ b/pallets/admin-utils/src/tests/mod.rs @@ -1135,6 +1135,7 @@ 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( diff --git a/pallets/commitments/src/lib.rs b/pallets/commitments/src/lib.rs index 2bbf8ecaf1..e55294b09b 100644 --- a/pallets/commitments/src/lib.rs +++ b/pallets/commitments/src/lib.rs @@ -13,6 +13,7 @@ pub mod weights; use ark_serialize::CanonicalDeserialize; use codec::Encode; use frame_support::IterableStorageDoubleMap; +use frame_support::weights::WeightMeter; use frame_support::{ BoundedVec, traits::{Currency, Get}, @@ -23,7 +24,7 @@ use scale_info::prelude::collections::BTreeSet; use sp_runtime::SaturatedConversion; use sp_runtime::{Saturating, Weight, traits::Zero}; use sp_std::{boxed::Box, vec::Vec}; -use subtensor_runtime_common::NetUid; +use subtensor_runtime_common::{LoopRemovePrefixWithWeightMeter, NetUid, WeightMeterWrapper}; use tle::{ curves::drand::TinyBLS381, stream_ciphers::AESGCMStreamCipherProvider, @@ -559,16 +560,48 @@ impl Pallet { commitments } - pub fn purge_netuid(netuid: NetUid) { - let _ = CommitmentOf::::clear_prefix(netuid, u32::MAX, None); - let _ = LastCommitment::::clear_prefix(netuid, u32::MAX, None); - let _ = LastBondsReset::::clear_prefix(netuid, u32::MAX, None); - let _ = RevealedCommitments::::clear_prefix(netuid, u32::MAX, None); - let _ = UsedSpaceOf::::clear_prefix(netuid, u32::MAX, None); + pub fn purge_netuid(netuid: NetUid, weight_meter: &mut WeightMeter) -> bool { + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + CommitmentOf, + netuid + ); + + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + LastCommitment, + netuid + ); + + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + LastBondsReset, + netuid + ); + + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + RevealedCommitments, + netuid + ); + + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + UsedSpaceOf, + netuid + ); + + WeightMeterWrapper!(weight_meter, T::DbWeight::get().writes(1)); TimelockedIndex::::mutate(|index| { index.retain(|(n, _)| *n != netuid); }); + true } } diff --git a/pallets/commitments/src/mock.rs b/pallets/commitments/src/mock.rs index 3db0e8f312..1295addb10 100644 --- a/pallets/commitments/src/mock.rs +++ b/pallets/commitments/src/mock.rs @@ -4,6 +4,7 @@ use frame_support::{ derive_impl, pallet_prelude::{Get, TypeInfo}, traits::{ConstU32, ConstU64, InherentBuilder}, + weights::constants::RocksDbWeight, }; use frame_system::offchain::CreateTransactionBase; use sp_core::H256; @@ -35,7 +36,7 @@ impl frame_system::Config for Test { type BaseCallFilter = frame_support::traits::Everything; type BlockWeights = (); type BlockLength = (); - type DbWeight = (); + type DbWeight = RocksDbWeight; type RuntimeOrigin = RuntimeOrigin; type RuntimeCall = RuntimeCall; type Hash = H256; diff --git a/pallets/commitments/src/tests.rs b/pallets/commitments/src/tests.rs index 8050b455fa..0d322a0406 100644 --- a/pallets/commitments/src/tests.rs +++ b/pallets/commitments/src/tests.rs @@ -18,9 +18,15 @@ use frame_support::pallet_prelude::Hooks; use frame_support::{ BoundedVec, assert_noop, assert_ok, traits::{Currency, Get, ReservableCurrency}, + weights::Weight, }; 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(); @@ -2265,7 +2271,7 @@ fn purge_netuid_clears_only_that_netuid() { assert!(TimelockedIndex::::get().contains(&(net_a, who_a1))); // Act - Pallet::::purge_netuid(net_a); + 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); @@ -2298,8 +2304,68 @@ fn purge_netuid_clears_only_that_netuid() { assert!(idx_after.contains(&(net_b, who_b))); // Idempotency - Pallet::::purge_netuid(net_a); + 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 final `WeightMeterWrapper!` fails; `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: prefix loops do not debit `WeightMeter` today, so + // this reliably fails at the final `WeightMeterWrapper!` inside `purge_netuid`. + let budget = write1.saturating_sub(Weight::from_parts(1, 1)); + + let done = purge_netuid_with_meter(net_a, budget); + assert!( + !done, + "final timelock-index write uses WeightMeterWrapper and must fail 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/crowdloan/README.md b/pallets/crowdloan/README.md index 9977c782c3..bc3dcf81a4 100644 --- a/pallets/crowdloan/README.md +++ b/pallets/crowdloan/README.md @@ -26,7 +26,7 @@ If the crowdloan fails to reach the cap, the creator can decide to refund all co The following functions are only callable by the creator of the crowdloan: -- `finalize`: Finalize a successful crowdloan. The call will transfer the raised amount to the target address if it was provided when the crowdloan was created and dispatch the call that was provided using the creator origin. +- `finalize`: Finalize a successful crowdloan. The call will transfer the raised amount to the target address if it was provided when the crowdloan was created and dispatch the call that was provided using the creator origin. - `dissolve`: Dissolve a crowdloan. The crowdloan will be removed from the storage. All contributions must have been refunded before the crowdloan can be dissolved (except the creator's one). diff --git a/pallets/drand/README.md b/pallets/drand/README.md index d0bdf0b7e7..70f5181714 100644 --- a/pallets/drand/README.md +++ b/pallets/drand/README.md @@ -1,6 +1,6 @@ # Drand Bridge Pallet -This is a [FRAME](https://docs.substrate.io/reference/frame-pallets/) pallet that allows Substrate-based chains to bridge to drand. It only supports bridging to drand's [Quicknet](https://drand.love/blog/quicknet-is-live-on-the-league-of-entropy-mainnet), which provides fresh randomness every 3 seconds. Adding this pallet to a runtime allows it to acquire verifiable on-chain randomness which can be used in runtime modules or ink! smart contracts. +This is a [FRAME](https://docs.substrate.io/reference/frame-pallets/) pallet that allows Substrate-based chains to bridge to drand. It only supports bridging to drand's [Quicknet](https://drand.love/blog/quicknet-is-live-on-the-league-of-entropy-mainnet), which provides fresh randomness every 3 seconds. Adding this pallet to a runtime allows it to acquire verifiable on-chain randomness which can be used in runtime modules or ink! smart contracts. Read [here](https://github.com/ideal-lab5/pallet-drand/blob/main/docs/how_it_works.md) for a deep-dive into the pallet. @@ -13,14 +13,14 @@ Use this pallet in a Substrate runtime to acquire verifiable randomness from dra Usage of this pallet requires that the node support: - arkworks host functions - offchain workers -- (optional - in case of smart contracts) Contracts pallet and drand chain extension enabled +- (optional - in case of smart contracts) Contracts pallet and drand chain extension enabled We have included a node in this repo, [substrate-node-template](https://github.com/ideal-lab5/pallet-drand/tree/main/substrate-node-template), that meets these requirements that you can use to get started. See [here](https://github.com/ideal-lab5/pallet-drand/blob/main/docs/integration.md) for a detailed guide on integrating this pallet into a runtime. ### For Pallets -This pallet implements the [Randomness](https://paritytech.github.io/polkadot-sdk/master/frame_support/traits/trait.Randomness.html) trait. FRAME pallets can use it by configuring their runtimes +This pallet implements the [Randomness](https://paritytech.github.io/polkadot-sdk/master/frame_support/traits/trait.Randomness.html) trait. FRAME pallets can use it by configuring their runtimes ``` rust impl pallet_with_randomness for Runtime { @@ -48,7 +48,7 @@ cargo build ## Testing -We maintain a minimum of 85% coverage on all new code. You can check coverage with tarpauling by running +We maintain a minimum of 85% coverage on all new code. You can check coverage with tarpauling by running ``` shell cargo tarpaulin --rustflags="-C opt-level=0" diff --git a/pallets/subtensor/Cargo.toml b/pallets/subtensor/Cargo.toml index 99ba71629f..c8bba167dc 100644 --- a/pallets/subtensor/Cargo.toml +++ b/pallets/subtensor/Cargo.toml @@ -58,6 +58,7 @@ pallet-crowdloan.workspace = true pallet-subtensor-proxy.workspace = true pallet-shield.workspace = true pallet-scheduler.workspace = true +sp-debug-derive = {workspace = true, features = ["force-debug"]} [dev-dependencies] pallet-balances = { workspace = true, features = ["std"] } @@ -141,6 +142,7 @@ std = [ "serde_json/std", "sha2/std", "rand/std", + "sp-debug-derive/std" ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", diff --git a/pallets/subtensor/src/benchmarks.rs b/pallets/subtensor/src/benchmarks.rs index 563dd211fe..ddb5c145a5 100644 --- a/pallets/subtensor/src/benchmarks.rs +++ b/pallets/subtensor/src/benchmarks.rs @@ -2025,6 +2025,22 @@ mod pallet_benchmarks { ); } + #[benchmark] + fn dissolve_network() { + let netuid = NetUid::from(1); + let tempo: u16 = 1; + let coldkey: T::AccountId = account("Owner", 0, 1); + + // Initialize network + Subtensor::::init_new_network(netuid, tempo); + + // Set network owner + SubnetOwner::::insert(netuid, coldkey.clone()); + + #[extrinsic_call] + _(RawOrigin::Root, coldkey.clone(), netuid); + } + #[benchmark] fn set_pending_childkey_cooldown() { let cooldown: u64 = 7200; diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index b2926323db..d57c05c1bc 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -16,12 +16,11 @@ // DEALINGS IN THE SOFTWARE. use super::*; -use crate::CommitmentsInterface; +use frame_support::weights::WeightMeter; use safe_math::*; +use sp_std::collections::btree_map::BTreeMap; use substrate_fixed::types::{I64F64, U96F32}; use subtensor_runtime_common::{AlphaBalance, NetUid, NetUidStorageIndex, TaoBalance, Token}; -use subtensor_swap_interface::SwapHandler; - impl Pallet { /// Fetches the total count of root network validators /// @@ -210,74 +209,179 @@ impl Pallet { Error::::SubnetNotExists ); - Self::finalize_all_subnet_root_dividends(netuid); + let mut dissolved_networks = DissolvedNetworks::::get(); + ensure!( + !dissolved_networks.contains(&netuid), + Error::::NetworkAlreadyDissolved + ); - // --- Perform the cleanup before removing the network. - // Will handle it in dissolve network PR. - T::SwapInterface::dissolve_all_liquidity_providers(netuid).map_err(|e| e.error)?; - Self::destroy_alpha_in_out_stakes(netuid)?; - T::SwapInterface::clear_protocol_liquidity(netuid)?; - T::CommitmentsInterface::purge_netuid(netuid); + // TODO Most of data cleanup is done in the block hook, should we charge the user for this? - // --- Remove the network - Self::remove_network(netuid); + // 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)); + + dissolved_networks.push(netuid); + DissolvedNetworks::::set(dissolved_networks); - // --- Emit the NetworkRemoved event log::info!("NetworkRemoved( netuid:{netuid:?} )"); + + // --- Emit the NetworkRemoved event Self::deposit_event(Event::NetworkRemoved(netuid)); Ok(()) } - pub fn remove_network(netuid: NetUid) { - // --- 1. Get the owner and remove from SubnetOwner. - let owner_coldkey: T::AccountId = SubnetOwner::::get(netuid); - SubnetOwner::::remove(netuid); + pub fn remove_network_map_parameters(netuid: NetUid, weight_meter: &mut WeightMeter) -> bool { + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + Keys, + netuid + ); - // --- 2. Remove network count. - SubnetworkN::::remove(netuid); + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + Uids, + netuid + ); - // --- 3. Remove netuid from added networks. - NetworksAdded::::remove(netuid); + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + BlockAtRegistration, + netuid + ); + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + Axons, + netuid + ); + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + NeuronCertificates, + netuid + ); + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + Prometheus, + netuid + ); + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + AlphaDividendsPerSubnet, + netuid + ); + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + PendingChildKeys, + netuid + ); + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + AssociatedEvmAddress, + netuid + ); - // --- 4. Decrement the network counter. - TotalNetworks::::mutate(|n: &mut u16| *n = n.saturating_sub(1)); + WeightMeterWrapper!(weight_meter, T::DbWeight::get().reads(1)); + let mechanisms: u8 = MechanismCountCurrent::::get(netuid).into(); - // --- 5. Remove various network-related storages. - NetworkRegisteredAt::::remove(netuid); + for subid in 0..mechanisms { + WeightMeterWrapper!(weight_meter, T::DbWeight::get().reads_writes(1, 2)); + let netuid_index = Self::get_mechanism_storage_index(netuid, subid.into()); - // --- 6. Remove incentive mechanism memory. - let _ = Uids::::clear_prefix(netuid, u32::MAX, None); - let keys = Keys::::iter_prefix(netuid).collect::>(); - let _ = Keys::::clear_prefix(netuid, u32::MAX, None); + LastUpdate::::remove(netuid_index); + Incentive::::remove(netuid_index); - // --- 8. Iterate over stored weights and fill the matrix. - for (uid_i, weights_i) in Weights::::iter_prefix(NetUidStorageIndex::ROOT) { - // Create a new vector to hold modified weights. - let mut modified_weights = weights_i.clone(); - for (subnet_id, weight) in modified_weights.iter_mut() { - // If the root network had a weight pointing to this netuid, set it to 0 - if subnet_id == &u16::from(netuid) { - *weight = 0; - } - } - Weights::::insert(NetUidStorageIndex::ROOT, uid_i, modified_weights); + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + WeightCommits, + netuid_index + ); + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + TimelockedWeightCommits, + netuid_index + ); + + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + CRV3WeightCommits, + netuid_index + ); + + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + CRV3WeightCommitsV2, + netuid_index + ); + + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + Bonds, + netuid_index + ); + + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + Weights, + netuid_index + ); } - // --- 9. Remove various network-related parameters. + WeightMeterWrapper!(weight_meter, T::DbWeight::get().writes(3)); + RevealPeriodEpochs::::remove(netuid); + MechanismCountCurrent::::remove(netuid); + MechanismEmissionSplit::::remove(netuid); + + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + LastHotkeySwapOnNetuid, + netuid + ); + + if let Some(lease_id) = SubnetUidToLeaseId::::get(netuid) { + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + SubnetLeaseShares, + lease_id + ); + WeightMeterWrapper!(weight_meter, T::DbWeight::get().writes(3)); + SubnetLeases::::remove(lease_id); + AccumulatedLeaseDividends::::remove(lease_id); + SubnetUidToLeaseId::::remove(netuid); + } + + true + } + + pub fn remove_network_parameters(netuid: NetUid, weight_meter: &mut WeightMeter) -> bool { + WeightMeterWrapper!(weight_meter, T::DbWeight::get().writes(80)); + 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); - - for (_uid, key) in keys { - IsNetworkMember::::remove(key, netuid); - } - - // --- 10. Erase network parameters. Tempo::::remove(netuid); Kappa::::remove(netuid); Difficulty::::remove(netuid); @@ -288,9 +392,6 @@ impl Pallet { RegistrationsThisInterval::::remove(netuid); POWRegistrationsThisInterval::::remove(netuid); BurnRegistrationsThisInterval::::remove(netuid); - - // --- 11. AMM / price / accounting. - // SubnetTAO, SubnetAlpha{In,InProvided,Out} are already cleared during dissolve/destroy. SubnetAlphaInEmission::::remove(netuid); SubnetAlphaOutEmission::::remove(netuid); SubnetTaoInEmission::::remove(netuid); @@ -303,20 +404,14 @@ impl Pallet { SubnetExcessTao::::remove(netuid); SubnetRootSellTao::::remove(netuid); SubnetTaoProvided::::remove(netuid); - - // --- 13. Token / mechanism / registration toggles. TokenSymbol::::remove(netuid); SubnetMechanism::::remove(netuid); SubnetOwnerHotkey::::remove(netuid); NetworkRegistrationAllowed::::remove(netuid); NetworkPowRegistrationAllowed::::remove(netuid); - - // --- 14. Locks & toggles. TransferToggle::::remove(netuid); SubnetLocked::::remove(netuid); LargestLocked::::remove(netuid); - - // --- 15. Mechanism step / emissions bookkeeping. FirstEmissionBlockNumber::::remove(netuid); PendingValidatorEmission::::remove(netuid); PendingServerEmission::::remove(netuid); @@ -325,12 +420,9 @@ impl Pallet { BlocksSinceLastStep::::remove(netuid); LastMechansimStepBlock::::remove(netuid); LastAdjustmentBlock::::remove(netuid); - - // --- 16. Serving / rho / curves, and other per-net controls. ServingRateLimit::::remove(netuid); Rho::::remove(netuid); AlphaSigmoidSteepness::::remove(netuid); - MaxAllowedValidators::::remove(netuid); BondsMovingAverage::::remove(netuid); BondsPenalty::::remove(netuid); @@ -340,10 +432,8 @@ impl Pallet { ScalingLawPower::::remove(netuid); TargetRegistrationsPerInterval::::remove(netuid); CommitRevealWeightsEnabled::::remove(netuid); - BurnHalfLife::::remove(netuid); BurnIncreaseMult::::remove(netuid); - Burn::::remove(netuid); MinBurn::::remove(netuid); MaxBurn::::remove(netuid); @@ -354,139 +444,407 @@ impl Pallet { RAORecycledForRegistration::::remove(netuid); MaxRegistrationsPerBlock::::remove(netuid); WeightsVersionKey::::remove(netuid); - - // --- 17. Subtoken / feature flags. LiquidAlphaOn::::remove(netuid); Yuma3On::::remove(netuid); AlphaValues::::remove(netuid); SubtokenEnabled::::remove(netuid); ImmuneOwnerUidsLimit::::remove(netuid); - - // --- 18. Consensus aux vectors. StakeWeight::::remove(netuid); LoadedEmission::::remove(netuid); + if SubnetIdentitiesV3::::contains_key(netuid) { + SubnetIdentitiesV3::::remove(netuid); + Self::deposit_event(Event::SubnetIdentityRemoved(netuid)); + } + true + } - // --- 19. DMAPs where netuid is the FIRST key: clear by prefix. - let _ = BlockAtRegistration::::clear_prefix(netuid, u32::MAX, None); - let _ = Axons::::clear_prefix(netuid, u32::MAX, None); - let _ = NeuronCertificates::::clear_prefix(netuid, u32::MAX, None); - let _ = Prometheus::::clear_prefix(netuid, u32::MAX, None); - let _ = AlphaDividendsPerSubnet::::clear_prefix(netuid, u32::MAX, None); - let _ = PendingChildKeys::::clear_prefix(netuid, u32::MAX, None); - let _ = AssociatedEvmAddress::::clear_prefix(netuid, u32::MAX, None); + pub fn remove_network_is_network_member( + netuid: NetUid, + weight_meter: &mut WeightMeter, + ) -> bool { + let r = T::DbWeight::get().reads(1); + let w = T::DbWeight::get().writes(1); + let mut read_all = true; + + let mut to_rm: sp_std::vec::Vec = sp_std::vec::Vec::new(); + let iter = match LastKeptRawKey::::get() { + Some(raw_key) => Keys::::iter_from(raw_key), + None => Keys::::iter(), + }; + for (nu, uid, hotkey) in iter { + if !weight_meter.can_consume(r) { + read_all = false; + LastKeptRawKey::::set(Some(Keys::::hashed_key_for(nu, uid))); + break; + } + weight_meter.consume(r); + if nu == netuid { + if !weight_meter.can_consume(w) { + read_all = false; + LastKeptRawKey::::set(Some(Keys::::hashed_key_for(nu, uid))); + break; + } + weight_meter.consume(w); + to_rm.push(hotkey); + } + } + if read_all { + LastKeptRawKey::::set(None); + } - // Commit-reveal / weights commits (all per-net prefixes): - let mechanisms: u8 = MechanismCountCurrent::::get(netuid).into(); - for subid in 0..mechanisms { - let netuid_index = Self::get_mechanism_storage_index(netuid, subid.into()); - LastUpdate::::remove(netuid_index); - Incentive::::remove(netuid_index); - let _ = WeightCommits::::clear_prefix(netuid_index, u32::MAX, None); - let _ = TimelockedWeightCommits::::clear_prefix(netuid_index, u32::MAX, None); - let _ = CRV3WeightCommits::::clear_prefix(netuid_index, u32::MAX, None); - let _ = CRV3WeightCommitsV2::::clear_prefix(netuid_index, u32::MAX, None); - let _ = Bonds::::clear_prefix(netuid_index, u32::MAX, None); - let _ = Weights::::clear_prefix(netuid_index, u32::MAX, None); + for hot in to_rm { + IsNetworkMember::::remove(&hot, netuid); } - RevealPeriodEpochs::::remove(netuid); - MechanismCountCurrent::::remove(netuid); - MechanismEmissionSplit::::remove(netuid); + read_all + } - // Last hotkey swap (DMAP where netuid is FIRST key → easy) - let _ = LastHotkeySwapOnNetuid::::clear_prefix(netuid, u32::MAX, None); + pub fn remove_network_update_weights_on_root( + netuid: NetUid, + weight_meter: &mut WeightMeter, + ) -> bool { + let mut map = BTreeMap::new(); + let mut read_all = true; + let netuid_u16 = u16::from(netuid); + + let root = NetUidStorageIndex::ROOT; + let iter = match LastKeptRawKey::::get() { + Some(raw_key) => Weights::::iter_prefix_from(root, raw_key), + None => Weights::::iter_prefix(root), + }; + + // --- Iterate over stored weights and zero root weights pointing at this netuid. + for (uid_i, weights_i) in iter { + let can_consume = weight_meter.can_consume(T::DbWeight::get().reads(1)); + weight_meter.consume(T::DbWeight::get().reads(1)); + if !can_consume { + read_all = false; + LastKeptRawKey::::set(Some(Weights::::hashed_key_for(root, uid_i))); + break; + } - // --- 20. Identity maps across versions (netuid-scoped). - if SubnetIdentitiesV3::::contains_key(netuid) { - SubnetIdentitiesV3::::remove(netuid); - Self::deposit_event(Event::SubnetIdentityRemoved(netuid)); - } + // Create a new vector to hold modified weights. + let mut modified_weights = weights_i.clone(); + let mut need_update = false; + for (subnet_id, weight) in modified_weights.iter_mut() { + // If the root network had a weight pointing to this netuid, set it to 0 + if *subnet_id == netuid_u16 { + if *weight != 0 { + need_update = true; + } - // --- 21. DMAP / NMAP where netuid is NOT the first key → iterate & remove. + *weight = 0; + } + } - // ChildkeyTake: (hot, netuid) → u16 - { - let to_rm: sp_std::vec::Vec = ChildkeyTake::::iter() - .filter_map(|(hot, n, _)| if n == netuid { Some(hot) } else { None }) - .collect(); - for hot in to_rm { - ChildkeyTake::::remove(&hot, netuid); + if need_update { + let can_consume = weight_meter.can_consume(T::DbWeight::get().writes(1)); + if !can_consume { + read_all = false; + LastKeptRawKey::::set(Some(Weights::::hashed_key_for(root, uid_i))); + break; + } + weight_meter.consume(T::DbWeight::get().writes(1)); + map.insert(uid_i, modified_weights); } } - // ChildKeys: (parent, netuid) → Vec<...> - { - let to_rm: sp_std::vec::Vec = ChildKeys::::iter() - .filter_map(|(parent, n, _)| if n == netuid { Some(parent) } else { None }) - .collect(); - for parent in to_rm { - ChildKeys::::remove(&parent, netuid); + + if read_all { + LastKeptRawKey::::set(None); + } + + for (uid_i, weights_i) in map.iter() { + Weights::::insert(NetUidStorageIndex::ROOT, uid_i, weights_i.clone()); + } + read_all + } + + pub fn remove_network_childkey_take(netuid: NetUid, weight_meter: &mut WeightMeter) -> bool { + let r = T::DbWeight::get().reads(1); + let w = T::DbWeight::get().writes(1); + let mut read_all = true; + + let mut to_rm: sp_std::vec::Vec = sp_std::vec::Vec::new(); + let iter = match LastKeptRawKey::::get() { + Some(raw_key) => ChildkeyTake::::iter_from(raw_key), + None => ChildkeyTake::::iter(), + }; + for (hot, nu, _) in iter { + if !weight_meter.can_consume(r) { + read_all = false; + LastKeptRawKey::::set(Some(ChildkeyTake::::hashed_key_for(&hot, nu))); + break; } + weight_meter.consume(r); + if nu == netuid { + if !weight_meter.can_consume(w) { + read_all = false; + LastKeptRawKey::::set(Some(ChildkeyTake::::hashed_key_for(&hot, nu))); + break; + } + weight_meter.consume(w); + to_rm.push(hot); + } + } + if read_all { + LastKeptRawKey::::set(None); + } + + for hot in to_rm { + ChildkeyTake::::remove(&hot, netuid); } - // ParentKeys: (child, netuid) → Vec<...> - { - let to_rm: sp_std::vec::Vec = ParentKeys::::iter() - .filter_map(|(child, n, _)| if n == netuid { Some(child) } else { None }) - .collect(); - for child in to_rm { - ParentKeys::::remove(&child, netuid); + read_all + } + + pub fn remove_network_childkeys(netuid: NetUid, weight_meter: &mut WeightMeter) -> bool { + let r = T::DbWeight::get().reads(1); + let w = T::DbWeight::get().writes(1); + let mut read_all = true; + + let mut to_rm: sp_std::vec::Vec = sp_std::vec::Vec::new(); + let iter = match LastKeptRawKey::::get() { + Some(raw_key) => ChildKeys::::iter_from(raw_key), + None => ChildKeys::::iter(), + }; + for (hot, nu, _) in iter { + if !weight_meter.can_consume(r) { + read_all = false; + LastKeptRawKey::::set(Some(ChildKeys::::hashed_key_for(&hot, nu))); + break; } + weight_meter.consume(r); + if nu == netuid { + if !weight_meter.can_consume(w) { + read_all = false; + LastKeptRawKey::::set(Some(ChildKeys::::hashed_key_for(&hot, nu))); + break; + } + weight_meter.consume(w); + to_rm.push(hot); + } + } + if read_all { + LastKeptRawKey::::set(None); } - // LastHotkeyEmissionOnNetuid: (hot, netuid) → α - { - let to_rm: sp_std::vec::Vec = LastHotkeyEmissionOnNetuid::::iter() - .filter_map(|(hot, n, _)| if n == netuid { Some(hot) } else { None }) - .collect(); - for hot in to_rm { - LastHotkeyEmissionOnNetuid::::remove(&hot, netuid); + + for hot in to_rm { + ChildKeys::::remove(&hot, netuid); + } + read_all + } + + pub fn remove_network_parentkeys(netuid: NetUid, weight_meter: &mut WeightMeter) -> bool { + let r = T::DbWeight::get().reads(1); + let w = T::DbWeight::get().writes(1); + let mut read_all = true; + + let mut to_rm: sp_std::vec::Vec = sp_std::vec::Vec::new(); + let iter = match LastKeptRawKey::::get() { + Some(raw_key) => ParentKeys::::iter_from(raw_key), + None => ParentKeys::::iter(), + }; + for (hot, nu, _) in iter { + if !weight_meter.can_consume(r) { + read_all = false; + LastKeptRawKey::::set(Some(ParentKeys::::hashed_key_for(&hot, nu))); + break; + } + weight_meter.consume(r); + if nu == netuid { + if !weight_meter.can_consume(w) { + read_all = false; + LastKeptRawKey::::set(Some(ParentKeys::::hashed_key_for(&hot, nu))); + break; + } + weight_meter.consume(w); + to_rm.push(hot); } } - // TotalHotkeyAlphaLastEpoch: (hot, netuid) → ... - // (TotalHotkeyAlpha and TotalHotkeyShares were already removed during dissolve.) - { - let to_rm_alpha_last: sp_std::vec::Vec = - TotalHotkeyAlphaLastEpoch::::iter() - .filter_map(|(hot, n, _)| if n == netuid { Some(hot) } else { None }) - .collect(); - for hot in to_rm_alpha_last { - TotalHotkeyAlphaLastEpoch::::remove(&hot, netuid); + if read_all { + LastKeptRawKey::::set(None); + } + + for hot in to_rm { + ParentKeys::::remove(&hot, netuid); + } + read_all + } + + pub fn remove_network_last_hotkey_emission_on_netuid( + netuid: NetUid, + weight_meter: &mut WeightMeter, + ) -> bool { + let r = T::DbWeight::get().reads(1); + let w = T::DbWeight::get().writes(1); + let mut read_all = true; + + let mut to_rm: sp_std::vec::Vec = sp_std::vec::Vec::new(); + let iter = match LastKeptRawKey::::get() { + Some(raw_key) => LastHotkeyEmissionOnNetuid::::iter_from(raw_key), + None => LastHotkeyEmissionOnNetuid::::iter(), + }; + for (hot, nu, _) in iter { + if !weight_meter.can_consume(r) { + read_all = false; + LastKeptRawKey::::set(Some(LastHotkeyEmissionOnNetuid::::hashed_key_for( + &hot, nu, + ))); + break; } + weight_meter.consume(r); + if nu == netuid { + if !weight_meter.can_consume(w) { + read_all = false; + LastKeptRawKey::::set(Some( + LastHotkeyEmissionOnNetuid::::hashed_key_for(&hot, nu), + )); + break; + } + weight_meter.consume(w); + to_rm.push(hot); + } + } + if read_all { + LastKeptRawKey::::set(None); + } + + for hot in to_rm { + LastHotkeyEmissionOnNetuid::::remove(&hot, netuid); } - // TransactionKeyLastBlock NMAP: (hot, netuid, name) → u64 - { - let to_rm: sp_std::vec::Vec<(T::AccountId, u16)> = TransactionKeyLastBlock::::iter() - .filter_map( - |((hot, n, name), _)| if n == netuid { Some((hot, name)) } else { None }, - ) - .collect(); - for (hot, name) in to_rm { - TransactionKeyLastBlock::::remove((hot, netuid, name)); + read_all + } + + pub fn remove_network_total_hotkey_alpha_last_epoch( + netuid: NetUid, + weight_meter: &mut WeightMeter, + ) -> bool { + let r = T::DbWeight::get().reads(1); + let w = T::DbWeight::get().writes(1); + let mut read_all = true; + + let mut to_rm: sp_std::vec::Vec = sp_std::vec::Vec::new(); + let iter = match LastKeptRawKey::::get() { + Some(raw_key) => TotalHotkeyAlphaLastEpoch::::iter_from(raw_key), + None => TotalHotkeyAlphaLastEpoch::::iter(), + }; + + for (hot, nu, _) in iter { + if !weight_meter.can_consume(r) { + read_all = false; + LastKeptRawKey::::set(Some(TotalHotkeyAlphaLastEpoch::::hashed_key_for( + &hot, nu, + ))); + break; + } + weight_meter.consume(r); + if nu == netuid { + if !weight_meter.can_consume(w) { + read_all = false; + LastKeptRawKey::::set(Some(TotalHotkeyAlphaLastEpoch::::hashed_key_for( + &hot, nu, + ))); + break; + } + weight_meter.consume(w); + to_rm.push(hot); } } - // StakingOperationRateLimiter NMAP: (hot, cold, netuid) → bool - { - let to_rm: sp_std::vec::Vec<(T::AccountId, T::AccountId)> = - StakingOperationRateLimiter::::iter() - .filter_map( - |((hot, cold, n), _)| { - if n == netuid { Some((hot, cold)) } else { None } - }, - ) - .collect(); - for (hot, cold) in to_rm { - StakingOperationRateLimiter::::remove((hot, cold, netuid)); + + if read_all { + LastKeptRawKey::::set(None); + } + + for hot in to_rm { + TotalHotkeyAlphaLastEpoch::::remove(&hot, netuid); + } + read_all + } + + pub fn remove_network_transaction_key_last_block( + netuid: NetUid, + weight_meter: &mut WeightMeter, + ) -> bool { + let r = T::DbWeight::get().reads(1); + let w = T::DbWeight::get().writes(1); + let mut read_all = true; + + let mut to_rm: sp_std::vec::Vec<(T::AccountId, u16)> = sp_std::vec::Vec::new(); + let iter = match LastKeptRawKey::::get() { + Some(raw_key) => TransactionKeyLastBlock::::iter_from(raw_key), + None => TransactionKeyLastBlock::::iter(), + }; + for ((hot, nu, name), _) in iter { + if !weight_meter.can_consume(r) { + read_all = false; + LastKeptRawKey::::set(Some(TransactionKeyLastBlock::::hashed_key_for(( + &hot, nu, name, + )))); + break; } + weight_meter.consume(r); + if nu == netuid { + if !weight_meter.can_consume(w) { + read_all = false; + LastKeptRawKey::::set(Some(TransactionKeyLastBlock::::hashed_key_for(( + &hot, nu, name, + )))); + break; + } + weight_meter.consume(w); + to_rm.push((hot, name)); + } + } + if read_all { + LastKeptRawKey::::set(None); } - // --- 22. Subnet leasing: remove mapping and any lease-scoped state linked to this netuid. - if let Some(lease_id) = SubnetUidToLeaseId::::take(netuid) { - SubnetLeases::::remove(lease_id); - let _ = SubnetLeaseShares::::clear_prefix(lease_id, u32::MAX, None); - AccumulatedLeaseDividends::::remove(lease_id); + for (hot, name) in to_rm { + TransactionKeyLastBlock::::remove((hot, netuid, name)); } + read_all + } - // --- Final removal logging. - log::debug!( - "remove_network: netuid={netuid}, owner={owner_coldkey:?} removed successfully" - ); + pub fn remove_network_staking_operation_rate_limiter( + netuid: NetUid, + weight_meter: &mut WeightMeter, + ) -> bool { + let r = T::DbWeight::get().reads(1); + let w = T::DbWeight::get().writes(1); + let mut read_all = true; + + let mut to_rm: sp_std::vec::Vec<(T::AccountId, T::AccountId)> = sp_std::vec::Vec::new(); + let iter = match LastKeptRawKey::::get() { + Some(raw_key) => StakingOperationRateLimiter::::iter_from(raw_key), + None => StakingOperationRateLimiter::::iter(), + }; + for ((hot, cold, nu), _) in iter { + if !weight_meter.can_consume(r) { + read_all = false; + LastKeptRawKey::::set(Some(StakingOperationRateLimiter::::hashed_key_for(( + &hot, &cold, nu, + )))); + break; + } + weight_meter.consume(r); + if nu == netuid { + if !weight_meter.can_consume(w) { + read_all = false; + LastKeptRawKey::::set(Some( + StakingOperationRateLimiter::::hashed_key_for((&hot, &cold, nu)), + )); + break; + } + weight_meter.consume(w); + to_rm.push((hot, cold)); + } + } + if read_all { + LastKeptRawKey::::set(None); + } + + for (hot, cold) in to_rm { + StakingOperationRateLimiter::::remove((hot, cold, netuid)); + } + read_all } #[allow(clippy::arithmetic_side_effects)] diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 75735c7471..250fbffa6e 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -16,6 +16,7 @@ use frame_support::{ pallet_macros::import_section, pallet_prelude::*, traits::tokens::fungible, + weights::WeightMeter, }; use pallet_balances::Call as BalancesCall; // use pallet_scheduler as Scheduler; @@ -23,7 +24,10 @@ use scale_info::TypeInfo; use sp_core::Get; use sp_runtime::DispatchError; use sp_std::marker::PhantomData; -use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token, TokenReserve}; +use subtensor_runtime_common::{ + AlphaBalance, LoopRemovePrefixWithWeightMeter, NetUid, TaoBalance, Token, TokenReserve, + WeightMeterWrapper, +}; // ============================ // ==== Benchmark Imports ===== @@ -83,6 +87,7 @@ pub mod pallet { use crate::RateLimitKey; use crate::migrations; use crate::subnets::leasing::{LeaseId, SubnetLeaseOf}; + use crate::weights::WeightInfo; use frame_support::Twox64Concat; use frame_support::{ BoundedVec, @@ -91,6 +96,7 @@ pub mod pallet { traits::{ OriginTrait, QueryPreimage, StorePreimage, UnfilteredDispatchable, tokens::fungible, }, + weights::Weight, }; use frame_system::pallet_prelude::*; use pallet_drand::types::RoundNumber; @@ -348,6 +354,55 @@ pub mod pallet { subnets: BTreeSet, }, } + /// Enum for the dissolved networks cleanup phase. + #[derive( + Encode, Decode, Default, TypeInfo, Clone, PartialEq, Eq, Debug, DecodeWithMemTracking, + )] + pub enum DissolvedNetworksCleanupPhaseEnum { + #[default] + /// Phase 1.1: Remove root dividend claimable entries for the subnet. + CleanSubnetRootDividendsRootClaimable, + /// Phase 1.2: Remove root dividend claimed entries for the subnet. + CleanSubnetRootDividendsRootClaimed, + /// Phase 2.1: Get the total alpha value for the subnet. + DestroyAlphaInOutStakesGetTotalAlphaValue, + /// Phase 2.2: Destroy alpha in and out stakes for the subnet. + DestroyAlphaInOutStakesSettleStakes, + /// Phase 2.3: Clean alpha entries for the subnet. + DestroyAlphaInOutStakesCleanAlpha, + /// Phase 2.4: Clear hotkey totals for the subnet. + DestroyAlphaInOutStakesClearHotkeyTotals, + /// Phase 2.5: Clear locks for the subnet. + DestroyAlphaInOutStakesClearLocks, + /// Phase 2.6: Destroy alpha in and out stakes for the subnet. + DestroyAlphaInOutStakes, + /// Phase 3: Clear protocol liquidity for the subnet on the swap layer. + ClearProtocolLiquidity, + /// 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. + RemoveNetworkIsNetworkMember, + /// Phase 5.2: Recovery / legacy: scalar `Network*` removal; the hook advances to map cleanup like `PurgeNetuid` after `remove_network_parameters` completes. + RemoveNetworkParameters, + /// Phase 5.3: Remove map-backed subnet storage (keys, axons, per-mechanism weights, etc.). + RemoveNetworkMapParameters, + /// Phase 5.4: Clear root-network weight entries referencing this netuid. + RemoveNetworkUpdateWeightsOnRoot, + /// Phase 5.5: Remove childkey take entries for this netuid. + RemoveNetworkChildkeyTake, + /// Phase 5.6: Remove child key bindings for this netuid. + RemoveNetworkChildkeys, + /// Phase 5.7: Remove parent key bindings for this netuid. + RemoveNetworkParentkeys, + /// Phase 5.8: Remove last hotkey emission records for this netuid. + RemoveNetworkLastHotkeyEmissionOnNetuid, + /// Phase 5.9: Remove total hotkey alpha last epoch entries for this netuid. + RemoveNetworkTotalHotkeyAlphaLastEpoch, + /// Phase 5.10: Remove transaction key last-block rate limit entries for this netuid. + RemoveNetworkTransactionKeyLastBlock, + /// Phase 5.11: Remove staking operation rate limiter entries for this netuid. + RemoveNetworkStakingOperationRateLimiter, + } /// The Max Burn HalfLife Settable #[pallet::type_value] @@ -2054,6 +2109,30 @@ pub mod pallet { pub type SubtokenEnabled = StorageMap<_, Identity, NetUid, bool, ValueQuery, DefaultFalse>; + /// --- ITEM ( dissolved_networks ) Networks dissolved but some storage not removed yet + #[pallet::storage] + pub type DissolvedNetworks = StorageValue<_, Vec, ValueQuery>; + + /// --- ITEM ( dissolved_networks_cleanup_phase ) Networks dissolved data cleanup phase. + #[pallet::storage] + pub type DissolvedNetworksCleanupPhase = + StorageValue<_, DissolvedNetworksCleanupPhaseEnum, OptionQuery>; + + /// --- ITEM ( last_kept_raw_key ) Last kept raw key for the next iteration. + /// It is only used during clean the data for dissolved networks. + #[pallet::storage] + pub type LastKeptRawKey = StorageValue<_, Vec, OptionQuery>; + + /// --- ITEM ( dissolved_subnet_total_alpha_value ) Total alpha value for the dissolved subnet. + /// It is only used during clean the data for dissolved networks. + #[pallet::storage] + pub type DissolvedSubnetTotalAlphaValue = StorageValue<_, u128, OptionQuery>; + + /// --- ITEM ( dissolved_subnet_settled_alpha_value ) Settled alpha value for the dissolved subnet. + /// It is only used during clean the data for dissolved networks. + #[pallet::storage] + pub type DissolvedSubnetSettledAlphaValue = StorageValue<_, u128, OptionQuery>; + // ======================================= // ==== VotingPower Storage ==== // ======================================= @@ -2855,5 +2934,5 @@ impl ProxyInterface for () { /// Pallets that hold per-subnet commitments implement this to purge all state for `netuid`. pub trait CommitmentsInterface { - fn purge_netuid(netuid: NetUid); + fn purge_netuid(netuid: NetUid, weight_meter: &mut WeightMeter) -> bool; } diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index a98578d813..3f5914d118 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -5,7 +5,7 @@ use frame_support::pallet_macros::pallet_section; /// This can later be imported into the pallet using [`import_section`]. #[pallet_section] mod dispatches { - use crate::weights::WeightInfo; + use frame_support::pallet_prelude::DispatchResultWithPostInfo; use frame_support::traits::schedule::v3::Anon as ScheduleAnon; use frame_system::pallet_prelude::BlockNumberFor; use sp_core::ecdsa::Signature; @@ -1231,9 +1231,9 @@ mod dispatches { /// Remove a user's subnetwork /// The caller must be the owner of the network #[pallet::call_index(61)] - #[pallet::weight(Weight::from_parts(119_000_000, 0) - .saturating_add(T::DbWeight::get().reads(6)) - .saturating_add(T::DbWeight::get().writes(31)))] + #[pallet::weight(Weight::from_parts(28_560_000, 0) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(3)))] pub fn dissolve_network( origin: OriginFor, _coldkey: T::AccountId, diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index cb120b56b5..0ab7aab4bd 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -281,6 +281,10 @@ mod errors { InvalidVotingPowerEmaAlpha, /// Deprecated call. Deprecated, + /// Subnet buyback exceeded the operation rate limit + SubnetBuybackRateLimitExceeded, + /// Network already dissolved + NetworkAlreadyDissolved, /// "Add stake and burn" exceeded the operation rate limit AddStakeBurnRateLimitExceeded, /// A coldkey swap has been announced for this account. diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index cdb37bb0dd..e1d34964e0 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -529,6 +529,12 @@ mod events { alpha: AlphaBalance, }, + /// data for a dissolved network has been cleaned up. + DissolvedNetworkDataCleaned { + /// The subnet ID + netuid: NetUid, + }, + /// A coldkey swap announcement has been cleared. ColdkeySwapCleared { /// The account ID of the coldkey that cleared the announcement. diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index ecd8d4212a..ab53c7b4f0 100644 --- a/pallets/subtensor/src/macros/hooks.rs +++ b/pallets/subtensor/src/macros/hooks.rs @@ -1,5 +1,6 @@ use frame_support::pallet_macros::pallet_section; - +// use subtensor_commitments_interface::CommitmentsHandler; +// use subtensor_swap_interface::SwapHandler; /// A [`pallet_section`] that defines the events for a pallet. /// This can later be imported into the pallet using [`import_section`]. #[pallet_section] @@ -185,12 +186,20 @@ mod hooks { // Self::check_total_stake()?; Ok(()) } + + fn on_idle(_block: BlockNumberFor, limit: Weight) -> Weight { + let dissolved_networks = DissolvedNetworks::::get(); + match dissolved_networks.get(0) { + Some(netuid) => Self::remove_data_for_dissolved_networks(limit, netuid), + None => Weight::from_parts(0, 0), + } + } } 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 - fn clean_up_hotkey_swap_records(block_number: BlockNumberFor) -> Weight { + pub(crate) fn clean_up_hotkey_swap_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) @@ -224,5 +233,319 @@ mod hooks { } weight } + + // Cleans data for a dissolved network within the available block weight. + // + // The cleanup runs one stored phase at a time. `DissolvedNetworksCleanupPhase` is a + // single `StorageValue` that tracks progress for the head of `DissolvedNetworks` + // (the `netuid` passed here must be that head). If a phase completes, the next phase + // is stored. Once all phases complete, the subnet is removed from `DissolvedNetworks` + // and `DissolvedNetworkDataCleaned` is emitted. + // + // # Args: + // * 'remaining_weight': (Weight): + // - The weight available for this cleanup step. + // * 'netuid': (&NetUid): + // - The subnet to clean dissolved-network data for. + // + // # Returns: + // * 'Weight': The weight used for the cleanup step. + // + fn remove_data_for_dissolved_networks(remaining_weight: Weight, netuid: &NetUid) -> Weight { + let mut weight_meter = + frame_support::weights::WeightMeter::with_limit(remaining_weight); + + // if no phase is set, set the first phase + if DissolvedNetworksCleanupPhase::::get().is_none() { + DissolvedNetworksCleanupPhase::::set(Some( + DissolvedNetworksCleanupPhaseEnum::CleanSubnetRootDividendsRootClaimable, + )); + } + + // if one phase is done or exit because of weight limit + let mut phase_done = true; + // only reason for phase_done to be false is if the weight limit is reached + while phase_done { + if let Some(phase) = DissolvedNetworksCleanupPhase::::get() { + log::debug!( + "dissolved_networks phase: {:?} for netuid: {:?}", + phase, + netuid + ); + let done = match phase { + DissolvedNetworksCleanupPhaseEnum::CleanSubnetRootDividendsRootClaimable => { + let done = + Self::clean_up_root_claimable_for_subnet(*netuid, &mut weight_meter); + + if done { + DissolvedNetworksCleanupPhase::::set(Some( + DissolvedNetworksCleanupPhaseEnum::CleanSubnetRootDividendsRootClaimed, + )); + } + done + } + + DissolvedNetworksCleanupPhaseEnum::CleanSubnetRootDividendsRootClaimed => { + let done = + Self::clean_up_root_claimed_for_subnet(*netuid, &mut weight_meter); + + if done { + DissolvedNetworksCleanupPhase::::set(Some( + DissolvedNetworksCleanupPhaseEnum::DestroyAlphaInOutStakesGetTotalAlphaValue, + )); + } + done + } + + DissolvedNetworksCleanupPhaseEnum::DestroyAlphaInOutStakesGetTotalAlphaValue => { + let done = Self::destroy_alpha_in_out_stakes_get_total_alpha_value( + *netuid, + &mut weight_meter, + ); + if done { + DissolvedNetworksCleanupPhase::::set(Some( + DissolvedNetworksCleanupPhaseEnum::DestroyAlphaInOutStakesSettleStakes, + )); + } + done + } + + DissolvedNetworksCleanupPhaseEnum::DestroyAlphaInOutStakesSettleStakes => { + let done = Self::destroy_alpha_in_out_stakes_settle_stakes( + *netuid, + &mut weight_meter, + ); + if done { + DissolvedNetworksCleanupPhase::::set(Some( + DissolvedNetworksCleanupPhaseEnum::DestroyAlphaInOutStakesCleanAlpha, + )); + } + done + } + + DissolvedNetworksCleanupPhaseEnum::DestroyAlphaInOutStakesCleanAlpha => { + let done = Self::destroy_alpha_in_out_stakes_clean_alpha( + *netuid, + &mut weight_meter, + ); + if done { + DissolvedNetworksCleanupPhase::::set(Some( + DissolvedNetworksCleanupPhaseEnum::DestroyAlphaInOutStakesClearHotkeyTotals, + )); + } + done + } + + DissolvedNetworksCleanupPhaseEnum::DestroyAlphaInOutStakesClearHotkeyTotals => { + let done = Self::destroy_alpha_in_out_stakes_clear_hotkey_totals( + *netuid, + &mut weight_meter, + ); + + if done { + DissolvedNetworksCleanupPhase::::set(Some( + DissolvedNetworksCleanupPhaseEnum::DestroyAlphaInOutStakesClearLocks, + )); + } + done + } + + DissolvedNetworksCleanupPhaseEnum::DestroyAlphaInOutStakesClearLocks => { + let done = Self::destroy_alpha_in_out_stakes_clear_locks( + *netuid, + &mut weight_meter, + ); + if done { + DissolvedNetworksCleanupPhase::::set(Some( + DissolvedNetworksCleanupPhaseEnum::DestroyAlphaInOutStakes, + )); + } + done + } + + DissolvedNetworksCleanupPhaseEnum::DestroyAlphaInOutStakes => { + let done = Self::destroy_alpha_in_out_stakes( + *netuid, + &mut weight_meter, + ); + if done { + DissolvedNetworksCleanupPhase::::set(Some( + DissolvedNetworksCleanupPhaseEnum::ClearProtocolLiquidity, + )); + } + done + } + + DissolvedNetworksCleanupPhaseEnum::ClearProtocolLiquidity => { + let done = + T::SwapInterface::clear_protocol_liquidity(*netuid, &mut weight_meter); + + if done { + DissolvedNetworksCleanupPhase::::set(Some( + DissolvedNetworksCleanupPhaseEnum::PurgeNetuid, + )); + } + done + } + + DissolvedNetworksCleanupPhaseEnum::PurgeNetuid => { + let done = + T::CommitmentsInterface::purge_netuid(*netuid, &mut weight_meter); + + if done { + DissolvedNetworksCleanupPhase::::set(Some( + DissolvedNetworksCleanupPhaseEnum::RemoveNetworkIsNetworkMember, + )); + } + done + } + DissolvedNetworksCleanupPhaseEnum::RemoveNetworkIsNetworkMember => { + let done = + Self::remove_network_is_network_member(*netuid, &mut weight_meter); + + if done { + DissolvedNetworksCleanupPhase::::set(Some( + DissolvedNetworksCleanupPhaseEnum::RemoveNetworkParameters, + )); + } + done + } + DissolvedNetworksCleanupPhaseEnum::RemoveNetworkParameters => { + let done = + Self::remove_network_parameters(*netuid, &mut weight_meter); + + if done { + DissolvedNetworksCleanupPhase::::set(Some( + DissolvedNetworksCleanupPhaseEnum::RemoveNetworkMapParameters, + )); + } + done + } + DissolvedNetworksCleanupPhaseEnum::RemoveNetworkMapParameters => { + let done = + Self::remove_network_map_parameters(*netuid, &mut weight_meter); + + if done { + DissolvedNetworksCleanupPhase::::set(Some( + DissolvedNetworksCleanupPhaseEnum::RemoveNetworkUpdateWeightsOnRoot, + )); + } + done + } + DissolvedNetworksCleanupPhaseEnum::RemoveNetworkUpdateWeightsOnRoot => { + let done = + Self::remove_network_update_weights_on_root(*netuid, &mut weight_meter); + + if done { + DissolvedNetworksCleanupPhase::::set(Some( + DissolvedNetworksCleanupPhaseEnum::RemoveNetworkChildkeyTake, + )); + } + done + } + DissolvedNetworksCleanupPhaseEnum::RemoveNetworkChildkeyTake => { + let done = + Self::remove_network_childkey_take(*netuid, &mut weight_meter); + + if done { + DissolvedNetworksCleanupPhase::::set(Some( + DissolvedNetworksCleanupPhaseEnum::RemoveNetworkChildkeys, + )); + } + done + } + DissolvedNetworksCleanupPhaseEnum::RemoveNetworkChildkeys => { + let done = + Self::remove_network_childkeys(*netuid, &mut weight_meter); + + if done { + DissolvedNetworksCleanupPhase::::set(Some( + DissolvedNetworksCleanupPhaseEnum::RemoveNetworkParentkeys, + )); + } + done + } + DissolvedNetworksCleanupPhaseEnum::RemoveNetworkParentkeys => { + let done = + Self::remove_network_parentkeys(*netuid, &mut weight_meter); + + if done { + DissolvedNetworksCleanupPhase::::set(Some( + DissolvedNetworksCleanupPhaseEnum::RemoveNetworkLastHotkeyEmissionOnNetuid, + )); + } + done + } + DissolvedNetworksCleanupPhaseEnum::RemoveNetworkLastHotkeyEmissionOnNetuid => { + let done = + Self::remove_network_last_hotkey_emission_on_netuid( + *netuid, + &mut weight_meter, + ); + + if done { + DissolvedNetworksCleanupPhase::::set(Some( + DissolvedNetworksCleanupPhaseEnum::RemoveNetworkTotalHotkeyAlphaLastEpoch, + )); + } + done + } + DissolvedNetworksCleanupPhaseEnum::RemoveNetworkTotalHotkeyAlphaLastEpoch => { + let done = + Self::remove_network_total_hotkey_alpha_last_epoch( + *netuid, + &mut weight_meter, + ); + + if done { + DissolvedNetworksCleanupPhase::::set(Some( + DissolvedNetworksCleanupPhaseEnum::RemoveNetworkTransactionKeyLastBlock, + )); + } + done + } + DissolvedNetworksCleanupPhaseEnum::RemoveNetworkTransactionKeyLastBlock => { + let done = + Self::remove_network_transaction_key_last_block( + *netuid, + &mut weight_meter, + ); + if done { + DissolvedNetworksCleanupPhase::::set(Some( + DissolvedNetworksCleanupPhaseEnum::RemoveNetworkStakingOperationRateLimiter, + )); + } + done + } + DissolvedNetworksCleanupPhaseEnum::RemoveNetworkStakingOperationRateLimiter => { + let done = + Self::remove_network_staking_operation_rate_limiter( + *netuid, + &mut weight_meter, + ); + + // if all phases are done, remove the network from the dissolved networks list and emit the event + if done { + DissolvedNetworksCleanupPhase::::set(None); + DissolvedNetworks::::mutate(|networks| { + networks.retain(|n| *n != *netuid) + }); + Self::deposit_event(Event::DissolvedNetworkDataCleaned { netuid: *netuid }); + } + done + } + }; + + phase_done = done; + + // if phase is cleared, break since all phases are done + if DissolvedNetworksCleanupPhase::::get().is_none() { + break; + } + } + } + + weight_meter.consumed() + } } } 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 9ecf7b73d7..380232e499 100644 --- a/pallets/subtensor/src/migrations/migrate_fix_bad_hk_swap.rs +++ b/pallets/subtensor/src/migrations/migrate_fix_bad_hk_swap.rs @@ -29,7 +29,7 @@ pub fn try_restore_shares() -> Weight { let effected_netuid = 59.into(); #[rustfmt::skip] - let diffs: [(&str, i64); 112] = [ + let diffs: [(&str, i64); 112] = [ ("5Fn9SqQhx5bhDua7AGgkKxxk3gfZ75WWBGCMPeKH1WBgPaMQ", -2375685930981_i64), ("5Fnhtm7cpxEbZaChnRZ8yWoF8MXVxmobkmLRehh5bkYtyZA9", -4090996138227), ("5C7j3w2zz1SVejRuFrb2zFWHXT7UfG7eWA87KXL1WyV5KLVR", -607494031), diff --git a/pallets/subtensor/src/staking/claim_root.rs b/pallets/subtensor/src/staking/claim_root.rs index 304eb37e5b..490d3fc4a9 100644 --- a/pallets/subtensor/src/staking/claim_root.rs +++ b/pallets/subtensor/src/staking/claim_root.rs @@ -1,6 +1,7 @@ use super::*; -use frame_support::weights::Weight; +use frame_support::weights::{Weight, WeightMeter}; use sp_core::Get; +use sp_std::collections::btree_map::BTreeMap; use sp_std::collections::btree_set::BTreeSet; use substrate_fixed::types::I96F32; use subtensor_swap_interface::SwapHandler; @@ -131,6 +132,10 @@ impl Pallet { root_claim_type: RootClaimTypeEnum, ignore_minimum_condition: bool, ) { + if DissolvedNetworks::::get().contains(&netuid) { + log::debug!("root claim on subnet {netuid} is skipped, network is dissolved"); + return; + } // Subtract the root claimed. let owed: I96F32 = Self::get_root_owed_for_hotkey_coldkey_float(hotkey, coldkey, netuid); @@ -422,15 +427,66 @@ impl Pallet { } /// Claim all root dividends for subnet and remove all associated data. - pub fn finalize_all_subnet_root_dividends(netuid: NetUid) { - let hotkeys = RootClaimable::::iter_keys().collect::>(); + pub fn clean_up_root_claimable_for_subnet( + netuid: NetUid, + weight_meter: &mut WeightMeter, + ) -> bool { + let mut to_remove_map = BTreeMap::>::new(); + + let mut read_all = true; + + let iter = match LastKeptRawKey::::get() { + Some(raw_key) => RootClaimable::::iter_from(raw_key), + None => RootClaimable::::iter(), + }; + + // Iterate directly without collecting to avoid unnecessary allocation + for (hotkey, _) in iter { + let can_consume = weight_meter.can_consume(T::DbWeight::get().reads(2)); + if !can_consume { + read_all = false; + LastKeptRawKey::::set(Some(RootClaimable::::hashed_key_for(&hotkey))); + break; + } + weight_meter.consume(T::DbWeight::get().reads(2)); + + let mut claimable = RootClaimable::::get(&hotkey); + if claimable.contains_key(&netuid) { + let can_consume = weight_meter.can_consume(T::DbWeight::get().writes(1)); + if !can_consume { + read_all = false; + LastKeptRawKey::::set(Some(RootClaimable::::hashed_key_for(&hotkey))); + break; + } - for hotkey in hotkeys.iter() { - RootClaimable::::mutate(hotkey, |claimable| { claimable.remove(&netuid); - }); + to_remove_map.insert(hotkey.clone(), claimable); + } + } + + if read_all { + LastKeptRawKey::::set(None); + } + + // write weight already consumed in advance + for (hotkey, claimable) in to_remove_map.iter() { + RootClaimable::::insert(hotkey, claimable); } - let _ = RootClaimed::::clear_prefix((netuid,), u32::MAX, None); + read_all + } + + pub fn clean_up_root_claimed_for_subnet( + netuid: NetUid, + weight_meter: &mut WeightMeter, + ) -> bool { + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + RootClaimed::, + (netuid,) + ); + + true } } diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index f2d07189a4..4c75057dcf 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -1,4 +1,5 @@ use super::*; +use frame_support::weights::WeightMeter; use substrate_fixed::types::U96F32; use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; use subtensor_swap_interface::{Order, SwapHandler}; @@ -425,16 +426,22 @@ impl Pallet { } } - pub fn destroy_alpha_in_out_stakes(netuid: NetUid) -> DispatchResult { - // 1) Ensure the subnet exists. - ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + pub fn destroy_alpha_in_out_stakes(netuid: NetUid, weight_meter: &mut WeightMeter) -> bool { + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + HotkeyLock, + netuid + ); // 2) Owner / lock cost. + WeightMeterWrapper!(weight_meter, T::DbWeight::get().reads(4)); let owner_coldkey: T::AccountId = SubnetOwner::::get(netuid); let lock_cost: TaoBalance = Self::get_subnet_locked_balance(netuid); // Determine if this subnet is eligible for a lock refund (legacy). let reg_at: u64 = NetworkRegisteredAt::::get(netuid); + let start_block: u64 = NetworkRegistrationStartBlock::::get(); let should_refund_owner: bool = reg_at < start_block; @@ -445,9 +452,11 @@ impl Pallet { // - price that α using a *simulated* AMM swap. let mut owner_emission_tao = TaoBalance::ZERO; if should_refund_owner && !lock_cost.is_zero() { + WeightMeterWrapper!(weight_meter, T::DbWeight::get().reads(1)); let total_emitted_alpha_u128: u128 = Self::get_alpha_issuance(netuid).to_u64() as u128; if total_emitted_alpha_u128 > 0 { + WeightMeterWrapper!(weight_meter, T::DbWeight::get().reads(1)); let owner_fraction: U96F32 = Self::get_float_subnet_owner_cut(); let owner_alpha_u64 = U96F32::from_num(total_emitted_alpha_u128) .saturating_mul(owner_fraction) @@ -455,6 +464,8 @@ impl Pallet { .saturating_to_num::(); owner_emission_tao = if owner_alpha_u64 > 0 { + // Need max 3 reads for current_alpha_price + WeightMeterWrapper!(weight_meter, T::DbWeight::get().reads(3)); let cur_price: U96F32 = T::SwapInterface::current_alpha_price(netuid.into()); let val_u64 = U96F32::from_num(owner_alpha_u64) .saturating_mul(cur_price) @@ -467,25 +478,99 @@ impl Pallet { } } - // 4) Enumerate all α entries on this subnet to build distribution weights and cleanup lists. - // - collect keys to remove, - // - per (hot,cold) α VALUE (not shares) with fallback to raw share if pool uninitialized, - // - track hotkeys to clear pool totals. - let mut keys_to_remove: Vec<(T::AccountId, T::AccountId)> = Vec::new(); - let mut stakers: Vec<(T::AccountId, T::AccountId, u128)> = Vec::new(); + // 7.c) Remove α‑in/α‑out counters (fully destroyed). + WeightMeterWrapper!(weight_meter, T::DbWeight::get().writes(4)); + SubnetAlphaIn::::remove(netuid); + SubnetAlphaInProvided::::remove(netuid); + SubnetAlphaOut::::remove(netuid); + + // Clear the locked balance on the subnet. + Self::set_subnet_locked_balance(netuid, TaoBalance::ZERO); + + // 8) Finalize lock handling: + // - Legacy subnets (registered before NetworkRegistrationStartBlock) receive: + // refund = max(0, lock_cost(τ) − owner_received_emission_in_τ). + // - New subnets: no refund. + let refund: TaoBalance = if should_refund_owner { + lock_cost.saturating_sub(owner_emission_tao) + } else { + TaoBalance::ZERO + }; + + if !refund.is_zero() { + WeightMeterWrapper!(weight_meter, T::DbWeight::get().reads_writes(1, 1)); + let _ = Self::transfer_tao_from_subnet(netuid, &owner_coldkey, refund); + } + + true + } + + /// This function calculates the total alpha value for a subnet. + /// It iterates through all hotkeys in the subnet and calculates the total alpha value. + /// It returns true if all hotkeys are iterated, otherwise false. + /// + /// # Args: + /// * 'netuid' (NetUid): + /// - The subnet to calculate the total alpha value for. + /// + /// * 'weight_meter' (WeightMeter): + /// - The weight meter to consume the weight for the operation. + /// + /// # Returns: + /// * 'bool': + /// - True if all hotkeys are iterated, otherwise false. + /// + pub fn destroy_alpha_in_out_stakes_get_total_alpha_value( + netuid: NetUid, + weight_meter: &mut WeightMeter, + ) -> bool { + let r = T::DbWeight::get().reads(1); + let mut read_all = true; let mut total_alpha_value_u128: u128 = 0; - let hotkeys_in_subnet: Vec = TotalHotkeyAlpha::::iter_keys() - .filter(|(_, this_netuid)| *this_netuid == netuid) - .map(|(hot, _)| hot.clone()) - .collect::>(); + let iter = match LastKeptRawKey::::get() { + Some(key) => { + if let Some(value) = DissolvedSubnetTotalAlphaValue::::get() { + total_alpha_value_u128 = value; + } else { + log::warn!("DissolvedSubnetTotalAlphaValue not set"); + } + TotalHotkeyAlpha::::iter_from(key) + } + None => TotalHotkeyAlpha::::iter(), + }; + + for (hot, this_netuid, _) in iter { + // let mut coldkeys: Vec = Vec::new(); + if !weight_meter.can_consume(r) { + read_all = false; + LastKeptRawKey::::set(Some(TotalHotkeyAlpha::::hashed_key_for( + &hot, + this_netuid, + ))); + break; + } + weight_meter.consume(r); + + if this_netuid != netuid { + continue; + } + + let mut iterate_all = true; + for (cold, this_netuid, share_u64f64) in Self::alpha_iter_single_prefix(&hot) { + if !weight_meter.can_consume(r) { + iterate_all = false; + LastKeptRawKey::::set(Some(TotalHotkeyAlpha::::hashed_key_for( + &hot, + this_netuid, + ))); + break; + } + weight_meter.consume(r); - for hot in hotkeys_in_subnet.iter() { - for (cold, this_netuid, share_u64f64) in Self::alpha_iter_single_prefix(hot) { if this_netuid != netuid { continue; } - keys_to_remove.push((hot.clone(), cold.clone())); // Primary: actual α value via share pool. let pool = Self::get_alpha_share_pool(hot.clone(), netuid); @@ -501,33 +586,137 @@ impl Pallet { if val_u64 > 0 { let val_u128 = val_u64 as u128; total_alpha_value_u128 = total_alpha_value_u128.saturating_add(val_u128); + } + } + + if !iterate_all { + read_all = false; + break; + } + } + + //always update the status no matter if there is weight left or not + DissolvedSubnetTotalAlphaValue::::set(Some(total_alpha_value_u128)); + + if read_all { + LastKeptRawKey::::set(None); + } + + read_all + } + + pub fn destroy_alpha_in_out_stakes_settle_stakes( + netuid: NetUid, + weight_meter: &mut WeightMeter, + ) -> bool { + let r = T::DbWeight::get().reads(1); + let w = T::DbWeight::get().writes(1); + let mut read_all = true; + + let mut stakers: Vec<(T::AccountId, T::AccountId, u128)> = Vec::new(); + let total_alpha_value_u128: u128 = match DissolvedSubnetTotalAlphaValue::::get() { + Some(value) => value, + None => { + log::warn!("DissolvedSubnetTotalAlphaValue not set"); + return false; + } + }; + let mut settled_alpha_value_u128 = + DissolvedSubnetSettledAlphaValue::::get().unwrap_or(0); + + let mut hotkeys_in_subnet: Vec = Vec::new(); + + let iter = match LastKeptRawKey::::get() { + Some(key) => TotalHotkeyAlpha::::iter_from(key), + None => TotalHotkeyAlpha::::iter(), + }; + + for (hot, this_netuid, _) in iter { + // let mut coldkeys: Vec = Vec::new(); + if !weight_meter.can_consume(r) { + read_all = false; + LastKeptRawKey::::set(Some(TotalHotkeyAlpha::::hashed_key_for( + &hot, + this_netuid, + ))); + break; + } + weight_meter.consume(r); + + if this_netuid != netuid { + continue; + } + hotkeys_in_subnet.push(hot.clone()); + + let mut iterate_all = true; + + for (cold, this_netuid, share_u64f64) in Self::alpha_iter_single_prefix(&hot) { + if !weight_meter.can_consume(r.saturating_mul(2_u64)) { + iterate_all = false; + LastKeptRawKey::::set(Some(TotalHotkeyAlpha::::hashed_key_for( + &hot, + this_netuid, + ))); + break; + } + + weight_meter.consume(r.saturating_mul(2_u64)); + + if this_netuid != netuid { + continue; + } + + // Primary: actual α value via share pool. + let pool = Self::get_alpha_share_pool(hot.clone(), netuid); + let actual_val_u64 = pool.try_get_value(&cold).unwrap_or(0); + + // Fallback: if pool uninitialized, treat raw Alpha share as value. + let val_u64 = if actual_val_u64 == 0 { + u64::from(share_u64f64) + } else { + actual_val_u64 + }; + + if val_u64 > 0 { + // reserve the weight for the add_balance_to_coldkey_account function call later + if !weight_meter.can_consume(w) { + iterate_all = false; + LastKeptRawKey::::set(Some(TotalHotkeyAlpha::::hashed_key_for( + &hot, + this_netuid, + ))); + break; + } + weight_meter.consume(r.saturating_mul(2_u64)); + let val_u128 = val_u64 as u128; stakers.push((hot.clone(), cold, val_u128)); } } + + if !iterate_all { + read_all = false; + break; + } } - // 5) Determine the TAO pot and pre-adjust accounting to avoid double counting. + // total TAO in the subnet pool let pot_tao: TaoBalance = SubnetTAO::::get(netuid); let pot_u64: u64 = pot_tao.into(); - - if pot_u64 > 0 { - SubnetTAO::::remove(netuid); - TotalStake::::mutate(|total| *total = total.saturating_sub(pot_tao)); + struct Portion { + _hot: A, + cold: C, + share: u64, // TAO to credit to coldkey balance + rem: u128, // remainder for largest‑remainder method } + let mut portions: Vec> = Vec::with_capacity(stakers.len()); - // 6) Pro‑rata distribution of the pot by α value (largest‑remainder), + // Pro‑rata distribution of the pot by α value (largest‑remainder), // **credited directly to each staker's COLDKEY free balance**. if pot_u64 > 0 && total_alpha_value_u128 > 0 && !stakers.is_empty() { - struct Portion { - _hot: A, - cold: C, - share: u64, // TAO to credit to coldkey balance - rem: u128, // remainder for largest‑remainder method - } - let pot_u128: u128 = pot_u64 as u128; - let mut portions: Vec> = Vec::with_capacity(stakers.len()); + let mut distributed: u128 = 0; + let mut total_rem: u128 = 0; for (hot, cold, alpha_val) in &stakers { let prod: u128 = pot_u128.saturating_mul(*alpha_val); @@ -536,6 +725,7 @@ impl Pallet { distributed = distributed.saturating_add(u128::from(share_u64)); let rem: u128 = prod.checked_rem(total_alpha_value_u128).unwrap_or_default(); + total_rem = total_rem.saturating_add(rem); portions.push(Portion { _hot: hot.clone(), cold: cold.clone(), @@ -544,8 +734,13 @@ impl Pallet { }); } - let leftover: u128 = pot_u128.saturating_sub(distributed); + settled_alpha_value_u128 = settled_alpha_value_u128.saturating_add(distributed); + + let leftover: u128 = total_rem + .checked_div(total_alpha_value_u128) + .unwrap_or_default(); if leftover > 0 { + settled_alpha_value_u128 = settled_alpha_value_u128.saturating_add(leftover); portions.sort_by(|a, b| b.rem.cmp(&a.rem)); let give: usize = core::cmp::min(leftover, portions.len() as u128) as usize; for p in portions.iter_mut().take(give) { @@ -553,6 +748,11 @@ impl Pallet { } } + portions = portions + .into_iter() + .filter(|p| p.share > 0) + .collect::>(); + // Credit each share directly to coldkey free balance. for p in portions { if p.share > 0 { @@ -562,70 +762,217 @@ impl Pallet { } } - // 7) Destroy all α-in/α-out state for this subnet. - // 7.a) Remove every (hot, cold, netuid) α entry. - for (hot, cold) in keys_to_remove { - Alpha::::remove((hot.clone(), cold.clone(), netuid)); - AlphaV2::::remove((hot, cold, netuid)); + // ignore the weight for handling the final operation, we must set the correct status for the next run + if read_all { + if settled_alpha_value_u128 < total_alpha_value_u128 { + let final_leftover: u128 = total_alpha_value_u128 + .saturating_sub(settled_alpha_value_u128) + .checked_div(total_alpha_value_u128) + .unwrap_or_default(); + + if final_leftover > 0 { + let owner = SubnetOwner::::get(netuid); + let _ = Self::transfer_tao_from_subnet(netuid, &owner, final_leftover.into()); + } + + DissolvedSubnetSettledAlphaValue::::set(Some(settled_alpha_value_u128)); + } else { + DissolvedSubnetSettledAlphaValue::::set(None); + } + + DissolvedSubnetTotalAlphaValue::::set(None); + LastKeptRawKey::::set(None); + DissolvedSubnetSettledAlphaValue::::set(None); + if pot_u64 > 0 { + SubnetTAO::::remove(netuid); + TotalStake::::mutate(|total| *total = total.saturating_sub(pot_tao)); + } + } else { + DissolvedSubnetSettledAlphaValue::::set(Some(settled_alpha_value_u128)); } - // 7.b) Clear share‑pool totals for each hotkey on this subnet. - for hot in hotkeys_in_subnet { - TotalHotkeyAlpha::::remove(&hot, netuid); - TotalHotkeyShares::::remove(&hot, netuid); - TotalHotkeySharesV2::::remove(&hot, netuid); + + read_all + } + + pub fn destroy_alpha_in_out_stakes_clean_alpha( + netuid: NetUid, + weight_meter: &mut WeightMeter, + ) -> bool { + let r = T::DbWeight::get().reads(1); + let w = T::DbWeight::get().writes(1); + let mut read_all = true; + + let iter = match LastKeptRawKey::::get() { + Some(key) => TotalHotkeyAlpha::::iter_from(key), + None => TotalHotkeyAlpha::::iter(), + }; + + for (hot, this_netuid, _) in iter { + let mut coldkeys: Vec = Vec::new(); + if !weight_meter.can_consume(r) { + read_all = false; + LastKeptRawKey::::set(Some(TotalHotkeyAlpha::::hashed_key_for( + &hot, + this_netuid, + ))); + break; + } + weight_meter.consume(r); + + if this_netuid != netuid { + continue; + } + + let mut iterate_all = true; + for (cold, this_netuid, _) in Self::alpha_iter_single_prefix(&hot) { + if !weight_meter.can_consume(r) { + read_all = false; + LastKeptRawKey::::set(Some(TotalHotkeyAlpha::::hashed_key_for( + &hot, + this_netuid, + ))); + iterate_all = false; + break; + } + weight_meter.consume(r); + if this_netuid != netuid { + continue; + } + coldkeys.push(cold.clone()); + } + + if !iterate_all { + read_all = false; + break; + } + + let weight_for_all_remove = w.saturating_mul(coldkeys.len() as u64); + + if !weight_meter.can_consume(weight_for_all_remove) { + read_all = false; + LastKeptRawKey::::set(Some(TotalHotkeyAlpha::::hashed_key_for( + &hot, + this_netuid, + ))); + break; + } + weight_meter.consume(weight_for_all_remove); + + for cold in coldkeys { + Alpha::::remove((&hot, &cold, netuid)); + AlphaV2::::remove((&hot, &cold, netuid)); + } } - // 7.c) Remove α‑in/α‑out counters (fully destroyed). - SubnetAlphaIn::::remove(netuid); - SubnetAlphaInProvided::::remove(netuid); - SubnetAlphaOut::::remove(netuid); - // Clear the locked balance on the subnet. - Self::set_subnet_locked_balance(netuid, TaoBalance::ZERO); + if read_all { + LastKeptRawKey::::set(None); + } - // 8) Finalize lock handling: - // - Legacy subnets (registered before NetworkRegistrationStartBlock) receive: - // refund = max(0, lock_cost(τ) − owner_received_emission_in_τ). - // - New subnets: no refund. - let refund: TaoBalance = if should_refund_owner { - lock_cost.saturating_sub(owner_emission_tao) - } else { - TaoBalance::ZERO + read_all + } + + pub fn destroy_alpha_in_out_stakes_clear_hotkey_totals( + netuid: NetUid, + weight_meter: &mut WeightMeter, + ) -> bool { + let r = T::DbWeight::get().reads(1); + let w = T::DbWeight::get().writes(1); + let mut read_all = true; + let mut hotkeys_to_remove: Vec = Vec::new(); + + let iter = match LastKeptRawKey::::get() { + Some(key) => TotalHotkeyAlpha::::iter_from(key), + None => TotalHotkeyAlpha::::iter(), }; - if !refund.is_zero() - && let Some(subnet_account) = Self::get_subnet_account_id(netuid) - { - // Transfer maximum transferrable up to refund to owner - let transferrable = Self::get_coldkey_balance(&subnet_account); - // We do our best effort to refund owner to as full amount of refund as possible, but - // we cannot fail new subnet registration, so the result is ignored. - let _ = Self::transfer_tao(&subnet_account, &owner_coldkey, refund.min(transferrable)); + // get all hotkeys in the subnet + for (hotkey, nu, _) in iter { + if !weight_meter.can_consume(r) { + read_all = false; + LastKeptRawKey::::set(Some(TotalHotkeyAlpha::::hashed_key_for(&hotkey, nu))); + break; + } + weight_meter.consume(r); + if nu != netuid { + continue; + } + + let weight_for_all_remove = w.saturating_mul(3_u64); + if !weight_meter.can_consume(weight_for_all_remove) { + read_all = false; + LastKeptRawKey::::set(Some(TotalHotkeyAlpha::::hashed_key_for(&hotkey, nu))); + break; + } + weight_meter.consume(weight_for_all_remove); + + hotkeys_to_remove.push(hotkey.clone()); + } + + if read_all { + LastKeptRawKey::::set(None); + } + + for hotkey in hotkeys_to_remove { + TotalHotkeyAlpha::::remove(&hotkey, netuid); + TotalHotkeyShares::::remove(&hotkey, netuid); + TotalHotkeySharesV2::::remove(&hotkey, netuid); } - // 9) Recycle TAO remaining on the subnet account, forgive errors. - if let Some(subnet_account) = Self::get_subnet_account_id(netuid) { - let remaining_subnet_balance = Self::get_keep_alive_balance(&subnet_account); - if Self::recycle_tao(&subnet_account, remaining_subnet_balance).is_ok() { - RAORecycledForRegistration::::insert(netuid, remaining_subnet_balance); + read_all + } + + pub fn destroy_alpha_in_out_stakes_clear_locks( + netuid: NetUid, + weight_meter: &mut WeightMeter, + ) -> bool { + let r = T::DbWeight::get().reads(1); + let w = T::DbWeight::get().writes(1); + let mut keys_to_remove: Vec<(T::AccountId, T::AccountId)> = Vec::new(); + let mut read_all = true; + + let iter = match LastKeptRawKey::::get() { + Some(key) => Lock::::iter_from(key), + None => Lock::::iter(), + }; + + for ((coldkey, this_netuid, hotkey), _) in iter { + if !weight_meter.can_consume(r) { + read_all = false; + LastKeptRawKey::::set(Some(Lock::::hashed_key_for(( + &coldkey, + this_netuid, + &hotkey, + )))); + break; + } + weight_meter.consume(r); + + if this_netuid != netuid { + continue; + } + + if !weight_meter.can_consume(w) { + read_all = false; + LastKeptRawKey::::set(Some(Lock::::hashed_key_for(( + &coldkey, + this_netuid, + &hotkey, + )))); + break; } + weight_meter.consume(w); + + keys_to_remove.push((coldkey, hotkey)); } - // 9) Cleanup all subnet stake locks if any. - let lock_keys: Vec<(T::AccountId, NetUid, T::AccountId)> = Lock::::iter_keys() - .filter(|(_, this_netuid, _)| *this_netuid == netuid) - .collect(); - for (coldkey, netuid, hotkey) in lock_keys { + for (coldkey, hotkey) in keys_to_remove { Lock::::remove((coldkey, netuid, hotkey)); } - // 10) Cleanup all subnet hotkey locks if any. - let hotkey_lock_keys: Vec<(NetUid, T::AccountId)> = HotkeyLock::::iter_keys() - .filter(|(this_netuid, _)| *this_netuid == netuid) - .collect(); - for (netuid, hotkey) in hotkey_lock_keys { - HotkeyLock::::remove(netuid, hotkey); + if read_all { + LastKeptRawKey::::set(None); } - Ok(()) + read_all } } diff --git a/pallets/subtensor/src/subnets/subnet.rs b/pallets/subtensor/src/subnets/subnet.rs index e1aa5eb744..fda9013c46 100644 --- a/pallets/subtensor/src/subnets/subnet.rs +++ b/pallets/subtensor/src/subnets/subnet.rs @@ -57,6 +57,10 @@ impl Pallet { let mut next_netuid = NetUid::from(1); // do not allow creation of root let netuids = Self::get_all_subnet_netuids(); loop { + if DissolvedNetworks::::get().contains(&next_netuid) { + next_netuid = next_netuid.next(); + continue; + } if !netuids.contains(&next_netuid) { break next_netuid; } @@ -447,7 +451,10 @@ impl Pallet { } pub fn get_subnet_account_id(netuid: NetUid) -> Option { - if NetworksAdded::::contains_key(netuid) || netuid == NetUid::ROOT { + if NetworksAdded::::contains_key(netuid) + || netuid == NetUid::ROOT + || DissolvedNetworks::::get().contains(&netuid) + { Some(T::SubtensorPalletId::get().into_sub_account_truncating(u16::from(netuid))) } else { None diff --git a/pallets/subtensor/src/subnets/uids.rs b/pallets/subtensor/src/subnets/uids.rs index d21509f83d..3665b139ff 100644 --- a/pallets/subtensor/src/subnets/uids.rs +++ b/pallets/subtensor/src/subnets/uids.rs @@ -46,6 +46,8 @@ impl Pallet { } Dividends::::mutate(netuid, |v| Self::set_element_at(v, neuron_index, 0)); StakeWeight::::mutate(netuid, |v| Self::set_element_at(v, neuron_index, 0)); + ValidatorTrust::::mutate(netuid, |v| Self::set_element_at(v, neuron_index, 0)); + ValidatorPermit::::mutate(netuid, |v| Self::set_element_at(v, neuron_index, false)); } /// Replace the neuron under this uid. diff --git a/pallets/subtensor/src/subnets/weights.rs b/pallets/subtensor/src/subnets/weights.rs index 43e3c7e4ba..9770832d5d 100644 --- a/pallets/subtensor/src/subnets/weights.rs +++ b/pallets/subtensor/src/subnets/weights.rs @@ -216,43 +216,43 @@ impl Pallet { /// ---- Commits a timelocked, encrypted weight payload (Commit-Reveal v3). /// /// # Args - /// * `origin` (`::RuntimeOrigin`): + /// * `origin` (`::RuntimeOrigin`): /// The signed origin of the committing hotkey. - /// * `netuid` (`NetUid` = `u16`): + /// * `netuid` (`NetUid` = `u16`): /// Unique identifier for the subnet on which the commit is made. - /// * `commit` (`BoundedVec>`): - /// The encrypted weight payload, produced as follows: - /// 1. Build a [`WeightsPayload`] structure. - /// 2. SCALE-encode it (`parity_scale_codec::Encode`). - /// 3. Encrypt it following the steps - /// [here](https://github.com/ideal-lab5/tle/blob/f8e6019f0fb02c380ebfa6b30efb61786dede07b/timelock/src/tlock.rs#L283-L336) to - /// produce a [`TLECiphertext`]. + /// * `commit` (`BoundedVec>`): + /// The encrypted weight payload, produced as follows: + /// 1. Build a [`WeightsPayload`] structure. + /// 2. SCALE-encode it (`parity_scale_codec::Encode`). + /// 3. Encrypt it following the steps + /// [here](https://github.com/ideal-lab5/tle/blob/f8e6019f0fb02c380ebfa6b30efb61786dede07b/timelock/src/tlock.rs#L283-L336) to + /// produce a [`TLECiphertext`]. /// 4. Compress & serialise. - /// * `reveal_round` (`u64`): - /// DRAND round whose output becomes known during epoch `n + 1`; the payload + /// * `reveal_round` (`u64`): + /// DRAND round whose output becomes known during epoch `n + 1`; the payload /// must be revealed in that epoch. - /// * `commit_reveal_version` (`u16`): - /// Version tag that **must** match [`get_commit_reveal_weights_version`] for + /// * `commit_reveal_version` (`u16`): + /// Version tag that **must** match [`get_commit_reveal_weights_version`] for /// the call to succeed. Used to gate runtime upgrades. /// /// # Behaviour - /// 1. Verifies the caller’s signature and registration on `netuid`. + /// 1. Verifies the caller’s signature and registration on `netuid`. /// 2. Ensures commit-reveal is enabled **and** the supplied - /// `commit_reveal_version` is current. - /// 3. Enforces per-neuron rate-limiting via [`Pallet::check_rate_limit`]. + /// `commit_reveal_version` is current. + /// 3. Enforces per-neuron rate-limiting via [`Pallet::check_rate_limit`]. /// 4. Rejects the call when the hotkey already has ≥ 10 unrevealed commits in - /// the current epoch. - /// 5. Appends `(hotkey, commit_block, commit, reveal_round)` to - /// `TimelockedWeightCommits[netuid][epoch]`. - /// 6. Emits `TimelockedWeightsCommitted` with the Blake2 hash of `commit`. + /// the current epoch. + /// 5. Appends `(hotkey, commit_block, commit, reveal_round)` to + /// `TimelockedWeightCommits[netuid][epoch]`. + /// 6. Emits `TimelockedWeightsCommitted` with the Blake2 hash of `commit`. /// 7. Updates `LastUpdateForUid` so subsequent rate-limit checks include this /// commit. /// /// # Raises - /// * `CommitRevealDisabled` – Commit-reveal is disabled on `netuid`. - /// * `IncorrectCommitRevealVersion` – Provided version ≠ runtime version. - /// * `HotKeyNotRegisteredInSubNet` – Caller’s hotkey is not registered. - /// * `CommittingWeightsTooFast` – Caller exceeds commit-rate limit. + /// * `CommitRevealDisabled` – Commit-reveal is disabled on `netuid`. + /// * `IncorrectCommitRevealVersion` – Provided version ≠ runtime version. + /// * `HotKeyNotRegisteredInSubNet` – Caller’s hotkey is not registered. + /// * `CommittingWeightsTooFast` – Caller exceeds commit-rate limit. /// * `TooManyUnrevealedCommits` – Caller already has 10 unrevealed commits. /// /// # Events diff --git a/pallets/subtensor/src/tests/claim_root.rs b/pallets/subtensor/src/tests/claim_root.rs index bd5761f376..cf0b9f022e 100644 --- a/pallets/subtensor/src/tests/claim_root.rs +++ b/pallets/subtensor/src/tests/claim_root.rs @@ -1,14 +1,16 @@ #![allow(clippy::expect_used)] +use super::mock::run_block_idle; use crate::RootAlphaDividendsPerSubnet; use crate::tests::mock::*; use crate::{ - DefaultMinRootClaimAmount, Error, MAX_NUM_ROOT_CLAIMS, MAX_ROOT_CLAIM_THRESHOLD, NetworksAdded, - NumRootClaim, NumStakingColdkeys, PendingRootAlphaDivs, RootClaimable, RootClaimableThreshold, - StakingColdkeys, StakingColdkeysByIndex, SubnetAlphaIn, SubnetMechanism, SubnetMovingPrice, - SubnetTAO, SubnetTaoFlow, SubtokenEnabled, Tempo, pallet, + DefaultMinRootClaimAmount, DissolvedNetworks, Error, LastKeptRawKey, MAX_NUM_ROOT_CLAIMS, + MAX_ROOT_CLAIM_THRESHOLD, NetworksAdded, NumRootClaim, NumStakingColdkeys, + PendingRootAlphaDivs, RootClaimable, RootClaimableThreshold, RootClaimed, StakingColdkeys, + StakingColdkeysByIndex, SubnetAlphaIn, SubnetMechanism, SubnetMovingPrice, SubnetTAO, + SubnetTaoFlow, SubtokenEnabled, Tempo, pallet, }; -use crate::{RootClaimType, RootClaimTypeEnum, RootClaimed}; +use crate::{RootClaimType, RootClaimTypeEnum}; use approx::assert_abs_diff_eq; use frame_support::dispatch::RawOrigin; use frame_support::pallet_prelude::Weight; @@ -16,7 +18,7 @@ use frame_support::traits::Get; use frame_support::{assert_err, assert_noop, assert_ok}; use sp_core::{H256, U256}; use sp_runtime::DispatchError; -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use substrate_fixed::types::{I96F32, U64F64}; use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; use subtensor_swap_interface::SwapHandler; @@ -1374,6 +1376,10 @@ fn test_claim_root_on_network_deregistration() { assert_ok!(SubtensorModule::do_dissolve_network(netuid)); + DissolvedNetworks::::set(vec![netuid]); + + run_block_idle(); + assert!(!RootClaimed::::contains_key(( netuid, &hotkey, &coldkey, ))); @@ -1381,6 +1387,91 @@ fn test_claim_root_on_network_deregistration() { }); } +#[test] +fn root_claim_on_subnet_is_noop_when_subnet_is_dissolved_queue() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(41_001); + let owner_hotkey = U256::from(41_002); + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let coldkey = U256::from(41_003); + let hotkey = U256::from(41_004); + register_ok_neuron(netuid, hotkey, coldkey, 111); + + let mut claimable = BTreeMap::new(); + claimable.insert(netuid, I96F32::from(9_000_000i32)); + RootClaimable::::insert(hotkey, claimable); + + DissolvedNetworks::::put(vec![netuid]); + + let before = RootClaimable::::get(hotkey).clone(); + SubtensorModule::root_claim_on_subnet( + &hotkey, + &coldkey, + netuid, + RootClaimTypeEnum::Swap, + true, + ); + assert_eq!( + RootClaimable::::get(hotkey), + before, + "dissolved subnets must not process root claims during async cleanup" + ); + }); +} + +#[test] +fn clean_up_root_claimable_for_subnet_removes_only_that_netuid_per_hotkey() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(42_001); + let owner_hotkey = U256::from(42_002); + let net = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let hk1 = U256::from(42_010); + let hk2 = U256::from(42_011); + + let mut m1 = BTreeMap::new(); + m1.insert(net, I96F32::from(100i32)); + m1.insert(NetUid::ROOT, I96F32::from(50i32)); + let mut m2 = BTreeMap::new(); + m2.insert(net, I96F32::from(200i32)); + + RootClaimable::::insert(hk1, m1); + RootClaimable::::insert(hk2, m2); + LastKeptRawKey::::kill(); + + let mut weight_meter = + frame_support::weights::WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)); + let done = SubtensorModule::clean_up_root_claimable_for_subnet(net, &mut weight_meter); + assert!( + done, + "full weight should scan and update all claimable maps" + ); + + assert!(!RootClaimable::::get(hk1).contains_key(&net)); + assert!(RootClaimable::::get(hk1).contains_key(&NetUid::ROOT)); + assert!(!RootClaimable::::get(hk2).contains_key(&net)); + }); +} + +#[test] +fn clean_up_root_claimed_for_subnet_clears_claimed_nmap_prefix() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(43_001); + let owner_hotkey = U256::from(43_002); + let net = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let hk = U256::from(43_010); + let ck = U256::from(43_011); + + RootClaimed::::insert((net, hk, ck), 123u128); + assert!(RootClaimed::::contains_key((net, hk, ck))); + + let mut weight_meter = + frame_support::weights::WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)); + let done = SubtensorModule::clean_up_root_claimed_for_subnet(net, &mut weight_meter); + assert!(done); + assert!(!RootClaimed::::contains_key((net, hk, ck))); + }); +} + #[test] fn test_claim_root_threshold() { new_test_ext(1).execute_with(|| { @@ -2056,3 +2147,50 @@ fn test_claim_root_with_moved_stake() { assert_abs_diff_eq!(bob_stake_diff2, estimated_stake as u64, epsilon = 100u64,); }); } + +#[test] +fn test_clean_up_root_claimed_for_subnet_clears_target_preserves_other_netuid() { + new_test_ext(1).execute_with(|| { + let hotkey = U256::from(5001u64); + let c_a = U256::from(5002u64); + let c_b = U256::from(5003u64); + let c_other = U256::from(5004u64); + let netuid_target = NetUid::from(11u16); + let netuid_other = NetUid::from(12u16); + + RootClaimed::::insert((netuid_target, &hotkey, &c_a), 10u128); + RootClaimed::::insert((netuid_target, &hotkey, &c_b), 20u128); + RootClaimed::::insert((netuid_other, &hotkey, &c_other), 99u128); + + let mut weight_meter = + frame_support::weights::WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)); + let done = + SubtensorModule::clean_up_root_claimed_for_subnet(netuid_target, &mut weight_meter); + assert!(done, "enough weight should complete cleanup"); + + assert_eq!( + RootClaimed::::get((netuid_target, &hotkey, &c_a)), + 0u128 + ); + assert_eq!( + RootClaimed::::get((netuid_target, &hotkey, &c_b)), + 0u128 + ); + assert!(!RootClaimed::::contains_key(( + netuid_target, + &hotkey, + &c_a + ))); + assert!(!RootClaimed::::contains_key(( + netuid_target, + &hotkey, + &c_b + ))); + + assert_eq!( + RootClaimed::::get((netuid_other, &hotkey, &c_other)), + 99u128, + "other netuid must be untouched" + ); + }); +} diff --git a/pallets/subtensor/src/tests/destroy_alpha_tests.rs b/pallets/subtensor/src/tests/destroy_alpha_tests.rs new file mode 100644 index 0000000000..c615ebf840 --- /dev/null +++ b/pallets/subtensor/src/tests/destroy_alpha_tests.rs @@ -0,0 +1,260 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] + +use super::mock::*; +use crate::*; +use frame_support::{assert_err, assert_ok, weights::Weight}; +use frame_system::Config; +use sp_core::U256; +use sp_std::collections::{btree_map::BTreeMap, vec_deque::VecDeque}; +use substrate_fixed::types::{I96F32, 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) { + let w = Weight::from_parts(u64::MAX, u64::MAX); + let mut weight_meter = frame_support::weights::WeightMeter::with_limit(w); + assert!( + SubtensorModule::destroy_alpha_in_out_stakes_get_total_alpha_value( + netuid, + &mut weight_meter + ), + "destroy_alpha_in_out_stakes_get_total_alpha_value incomplete" + ); + assert!( + SubtensorModule::destroy_alpha_in_out_stakes_settle_stakes(netuid, &mut weight_meter), + "destroy_alpha_in_out_stakes_settle_stakes incomplete" + ); + assert!( + SubtensorModule::destroy_alpha_in_out_stakes_clean_alpha(netuid, &mut weight_meter), + "destroy_alpha_in_out_stakes_clean_alpha incomplete" + ); + assert!( + SubtensorModule::destroy_alpha_in_out_stakes_clear_hotkey_totals(netuid, &mut weight_meter), + "destroy_alpha_in_out_stakes_clear_hotkey_totals incomplete" + ); + assert!( + SubtensorModule::destroy_alpha_in_out_stakes_clear_locks(netuid, &mut weight_meter), + "destroy_alpha_in_out_stakes_clear_locks incomplete" + ); + assert!( + SubtensorModule::destroy_alpha_in_out_stakes(netuid, &mut weight_meter), + "destroy_alpha_in_out_stakes incomplete" + ); +} + +#[test] +fn test_destroy_alpha_in_out_stakes_get_total_alpha_value() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(1001); + let owner_hot = U256::from(1002); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + + // Add some stake to have alpha value + let stake_tao: u64 = 1000; + setup_reserves(netuid, (stake_tao * 1_000_000).into(), (stake_tao * 10_000_000).into()); + let amount: TaoBalance = (stake_tao).into(); + assert_ok!(SubtensorModule::create_account_if_non_existent(&owner_cold, &owner_hot)); + add_balance_to_coldkey_account(&owner_cold, amount); + // Stake into subnet to create some alpha + assert_ok!(SubtensorModule::stake_into_subnet( + &owner_hot, + &owner_cold, + netuid, + amount, + ::SwapInterface::max_price(), + false, + false, + )); + + // Now test the function + let w = Weight::from_parts(u64::MAX, u64::MAX); + let mut weight_meter = frame_support::weights::WeightMeter::with_limit(w); + let result = SubtensorModule::destroy_alpha_in_out_stakes_get_total_alpha_value(netuid, &mut weight_meter); + assert!(result, "destroy_alpha_in_out_stakes_get_total_alpha_value should return true when there is alpha to process"); + }); +} + +#[test] +fn test_destroy_alpha_in_out_stakes_settle_stakes() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(1001); + let owner_hot = U256::from(1002); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + + // Add some stake to have alpha value + let stake_tao: u64 = 1000; + setup_reserves(netuid, (stake_tao * 1_000_000).into(), (stake_tao * 10_000_000).into()); + let amount: TaoBalance = (stake_tao).into(); + assert_ok!(SubtensorModule::create_account_if_non_existent(&owner_cold, &owner_hot)); + add_balance_to_coldkey_account(&owner_cold, amount); + // Stake into subnet to create some alpha + assert_ok!(SubtensorModule::stake_into_subnet( + &owner_hot, + &owner_cold, + netuid, + amount, + ::SwapInterface::max_price(), + false, + false, + )); + + // First, we need to get the total alpha value (simulate the previous step) + let w = Weight::from_parts(u64::MAX, u64::MAX); + let mut weight_meter = frame_support::weights::WeightMeter::with_limit(w); + let _ = SubtensorModule::destroy_alpha_in_out_stakes_get_total_alpha_value(netuid, &mut weight_meter); + // Now test the settle_stakes function + let mut weight_meter2 = frame_support::weights::WeightMeter::with_limit(w); + let result = SubtensorModule::destroy_alpha_in_out_stakes_settle_stakes(netuid, &mut weight_meter2); + assert!(result, "destroy_alpha_in_out_stakes_settle_stakes should return true when there is alpha to settle"); + }); +} + +#[test] +fn test_destroy_alpha_in_out_stakes_clean_alpha() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(1001); + let owner_hot = U256::from(1002); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + + // Add some stake to have alpha value + let stake_tao: u64 = 1000; + setup_reserves(netuid, (stake_tao * 1_000_000).into(), (stake_tao * 10_000_000).into()); + let amount: TaoBalance = (stake_tao).into(); + assert_ok!(SubtensorModule::create_account_if_non_existent(&owner_cold, &owner_hot)); + add_balance_to_coldkey_account(&owner_cold, amount); + // Stake into subnet to create some alpha + assert_ok!(SubtensorModule::stake_into_subnet( + &owner_hot, + &owner_cold, + netuid, + amount, + ::SwapInterface::max_price(), + false, + false, + )); + + // Simulate the previous two steps: get total alpha and settle stakes + let w = Weight::from_parts(u64::MAX, u64::MAX); + let mut weight_meter = frame_support::weights::WeightMeter::with_limit(w); + let _ = SubtensorModule::destroy_alpha_in_out_stakes_get_total_alpha_value(netuid, &mut weight_meter); + let mut weight_meter2 = frame_support::weights::WeightMeter::with_limit(w); + let _ = SubtensorModule::destroy_alpha_in_out_stakes_settle_stakes(netuid, &mut weight_meter2); + // Now test the clean_alpha function + let mut weight_meter3 = frame_support::weights::WeightMeter::with_limit(w); + let result = SubtensorModule::destroy_alpha_in_out_stakes_clean_alpha(netuid, &mut weight_meter3); + assert!(result, "destroy_alpha_in_out_stakes_clean_alpha should return true when there is alpha to clean"); + }); +} + +#[test] +fn test_destroy_alpha_in_out_stakes_clear_hotkey_totals() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(1001); + let owner_hot = U256::from(1002); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + + // Add some stake to have alpha value and hotkey totals + let stake_tao: u64 = 1000; + setup_reserves(netuid, (stake_tao * 1_000_000).into(), (stake_tao * 10_000_000).into()); + let amount: TaoBalance = (stake_tao).into(); + assert_ok!(SubtensorModule::create_account_if_non_existent(&owner_cold, &owner_hot)); + add_balance_to_coldkey_account(&owner_cold, amount); + // Stake into subnet to create some alpha and hotkey totals + assert_ok!(SubtensorModule::stake_into_subnet( + &owner_hot, + &owner_cold, + netvid, + amount, + ::SwapInterface::max_price(), + false, + false, + )); + + // Simulate the previous three steps + let w = Weight::from_parts(u64::MAX, u64::MAX); + let mut weight_meter = frame_support::weights::WeightMeter::with_limit(w); + let _ = SubtensorModule::destroy_alpha_in_out_stakes_get_total_alpha_value(netuid, &mut weight_meter); + let mut weight_meter2 = frame_support::weights::WeightMeter::with_limit(w); + let _ = SubtensorModule::destroy_alpha_in_out_stakes_settle_stakes(netuid, &mut weight_meter2); + let mut weight_meter3 = frame_support::weights::WeightMeter::with_limit(w); + let _ = SubtensorModule::destroy_alpha_in_out_stakes_clean_alpha(netuid, &mut weight_meter3); + // Now test the clear_hotkey_totals function + let mut weight_meter4 = frame_support::weights::WeightMeter::with_limit(w); + let result = SubtensorModule::destroy_alpha_in_out_stakes_clear_hotkey_totals(netuid, &mut weight_meter4); + assert!(result, "destroy_alpha_in_out_stakes_clear_hotkey_totals should return true when there are hotkey totals to clear"); + }); +} + +#[test] +fn test_destroy_alpha_in_out_stakes_clear_locks() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(1001); + let owner_hot = U256::from(1002); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + + // Add some stake to have alpha value and create locks + let stake_tao: u64 = 1000; + setup_reserves(netvid, (stake_tao * 1_000_000).into(), (stake_tao * 10_000_000).into()); + let amount: TaoBalance = (stake_tao).into(); + assert_ok!(SubtensorModule::create_account_if_non_existent(&owner_cold, &owner_hot)); + add_balance_to_coldkey_account(&owner_cold, amount); + // Stake into subnet to create some alpha and locks + assert_ok!(SubtensorModule::stake_into_subnet( + &owner_hot, + &owner_cold, + netuid, + amount, + ::SwapInterface::max_price(), + false, + false, + )); + + // Simulate the previous four steps + let w = Weight::from_parts(u64::MAX, u64::MAX); + let mut weight_meter = frame_support::weights::WeightMeter::with_limit(w); + let _ = SubtensorModule::destroy_alpha_in_out_stakes_get_total_alpha_value(netvid, &mut weight_meter); + let mut weight_meter2 = frame_support::weights::WeightMeter::with_limit(w); + let _ = SubtensorModule::destroy_alpha_in_out_stakes_settle_stakes(netvid, &mut weight_meter2); + let mut weight_meter3 = frame_support::weights::WeightMeter::with_limit(w); + let _ = SubtensorModule::destroy_alpha_in_out_stakes_clean_alpha(netvid, &mut weight_meter3); + let mut weight_meter4 = frame_support::weights::WeightMeter::with_limit(w); + let _ = SubtensorModule::destroy_alpha_in_out_stakes_clear_hotkey_totals(netvid, &mut weight_meter4); + // Now test the clear_locks function + let mut weight_meter5 = frame_support::weights::WeightMeter::with_limit(w); + let result = SubtensorModule::destroy_alpha_in_out_stakes_clear_locks(netvid, &mut weight_meter5); + assert!(result, "destroy_alpha_in_out_stakes_clear_locks should return true when there are locks to clear"); + }); +} + +#[test] +fn test_destroy_alpha_in_out_stakes() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(1001); + let owner_hot = U256::from(1002); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + + // Add some stake to have alpha value and create locks, etc. + let stake_tao: u64 = 1000; + setup_reserves(netvid, (stake_tao * 1_000_000).into(), (stake_tao * 10_000_000).into()); + let amount: TaoBalance = (stake_tao).into(); + assert_ok!(SubtensorModule::create_account_if_non_existent(&owner_cold, &owner_hot)); + add_balance_to_coldkey_account(&owner_cold, amount); + // Stake into subnet to create some alpha and locks + assert_ok!(SubtensorModule::stake_into_subnet( + &owner_hot, + &owner_cold, + netuid, + amount, + ::SwapInterface::max_price(), + false, + false, + )); + + // Now test the main destroy function (which should call all the steps internally) + let w = Weight::from_parts(u64::MAX, u64::MAX); + let mut weight_meter = frame_support::weights::WeightMeter::with_limit(w); + let result = SubtensorModule::destroy_alpha_in_out_stakes(netvid, &mut weight_meter); + assert!(result, "destroy_alpha_in_out_stakes should return true when it successfully processes the netuid"); + }); +} \ No newline at end of file diff --git a/pallets/subtensor/src/tests/evm.rs b/pallets/subtensor/src/tests/evm.rs index d692a72f72..51f966e93a 100644 --- a/pallets/subtensor/src/tests/evm.rs +++ b/pallets/subtensor/src/tests/evm.rs @@ -31,6 +31,15 @@ fn sign_evm_message>(pair: &ecdsa::Pair, message: M) -> ecdsa::Si sig } +#[test] +fn test_weight_usage() { + new_test_ext(1).execute_with(|| { + let write = ::DbWeight::get().writes(1); + assert_eq!(write.ref_time(), 100_000_000); + assert_eq!(write.proof_size(), 0); + }); +} + #[test] fn test_associate_evm_key_success() { new_test_ext(1).execute_with(|| { diff --git a/pallets/subtensor/src/tests/locks.rs b/pallets/subtensor/src/tests/locks.rs index 00472bebe5..826cd51172 100644 --- a/pallets/subtensor/src/tests/locks.rs +++ b/pallets/subtensor/src/tests/locks.rs @@ -1750,6 +1750,7 @@ fn test_subnet_dissolution_orphans_locks() { // Dissolve the subnet assert_ok!(SubtensorModule::do_dissolve_network(netuid)); + run_block_idle(); // All Alpha entries are gone assert_eq!( @@ -1784,6 +1785,7 @@ fn test_subnet_dissolution_and_netuid_reuse() { // 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)); diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 8c553e3ee8..adafba2de2 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -19,8 +19,10 @@ use frame_support::{ }; use frame_system as system; use frame_system::{EnsureRoot, RawOrigin, limits, offchain::CreateTransactionBase}; +use pallet_commitments::pallet::Pallet as CommitmentsPallet; use pallet_subtensor_proxy as pallet_proxy; use pallet_subtensor_utility as pallet_utility; +use scale_info::TypeInfo; use share_pool::SafeFloat; use sp_core::{ConstU64, Get, H256, U256, offchain::KeyTypeId}; use sp_runtime::Perbill; @@ -31,9 +33,51 @@ use sp_runtime::{ use sp_std::{cell::RefCell, cmp::Ordering, sync::OnceLock}; use sp_tracing::tracing_subscriber; use substrate_fixed::types::U64F64; -use subtensor_runtime_common::{AuthorshipInfo, NetUid, TaoBalance}; +use subtensor_runtime_common::{AuthorshipInfo, ConstTao, NetUid, TaoBalance}; use subtensor_swap_interface::{Order, SwapHandler}; use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; + +// Local definitions for pallet_commitments associated types +pub struct TestMaxFields; +impl Get for TestMaxFields { + fn get() -> u32 { + 16 + } +} +impl TypeInfo for TestMaxFields { + type Identity = Self; + fn type_info() -> scale_info::Type { + scale_info::Type::builder() + .path(scale_info::Path::new("TestMaxFields", module_path!())) + .composite(scale_info::build::Fields::unit()) + } +} + +pub struct TestCanCommit; +impl pallet_commitments::CanCommit for TestCanCommit { + fn can_commit(_netuid: NetUid, _who: &U256) -> bool { + true + } +} + +pub struct MockTempoInterface; +impl pallet_commitments::GetTempoInterface for MockTempoInterface { + fn get_epoch_index(netuid: NetUid, cur_block: u64) -> u64 { + let tempo: u64 = 360; // Default tempo + let tempo_plus_one: u64 = tempo.checked_add(1).unwrap(); + let netuid_plus_one: u64 = (u16::from(netuid) as u64).checked_add(1).unwrap(); + let block_with_offset: u64 = cur_block.checked_add(netuid_plus_one).unwrap(); + + block_with_offset.checked_div(tempo_plus_one).unwrap_or(0) + } +} + +// Implement OnMetadataCommitment for U256 by creating a local wrapper type +pub struct TestOnMetadataCommitment; +impl pallet_commitments::OnMetadataCommitment for TestOnMetadataCommitment { + fn on_metadata_commitment(_netuid: NetUid, _who: &U256) {} +} + type Block = frame_system::mocking::MockBlock; // Configure a mock runtime to test the pallet. @@ -51,7 +95,8 @@ frame_support::construct_runtime!( Drand: pallet_drand = 9, Swap: pallet_subtensor_swap = 10, Crowdloan: pallet_crowdloan = 11, - Proxy: pallet_subtensor_proxy = 12, + Commitments: pallet_commitments = 12, + Proxy: pallet_subtensor_proxy = 13, } ); @@ -357,6 +402,18 @@ impl pallet_subtensor_swap::Config for Test { type BenchmarkHelper = (); } +// Implement pallet_commitments::Config for Test +impl pallet_commitments::Config for Test { + type Currency = Balances; + type WeightInfo = (); + type CanCommit = TestCanCommit; + type OnMetadataCommitment = TestOnMetadataCommitment; + type MaxFields = TestMaxFields; + type InitialDeposit = ConstTao<0>; + type FieldDeposit = ConstTao<0>; + type TempoInterface = MockTempoInterface; +} + pub struct OriginPrivilegeCmp; impl PrivilegeCmp for OriginPrivilegeCmp { @@ -367,7 +424,12 @@ impl PrivilegeCmp for OriginPrivilegeCmp { pub struct CommitmentsI; impl CommitmentsInterface for CommitmentsI { - fn purge_netuid(_netuid: NetUid) {} + fn purge_netuid( + netuid: NetUid, + weight_meter: &mut frame_support::weights::WeightMeter, + ) -> bool { + CommitmentsPallet::::purge_netuid(netuid, weight_meter) + } } parameter_types! { @@ -688,6 +750,14 @@ pub(crate) fn run_to_block_ext(n: u64, enable_events: bool) { } } +#[allow(dead_code)] +pub(crate) fn run_block_idle() { + SubtensorModule::on_idle( + System::block_number(), + Weight::from_parts(u64::MAX, u64::MAX), + ); +} + #[allow(dead_code)] pub(crate) fn next_block_no_epoch(netuid: NetUid) -> u64 { // high tempo to skip automatic epochs in on_initialize diff --git a/pallets/subtensor/src/tests/mock_high_ed.rs b/pallets/subtensor/src/tests/mock_high_ed.rs index 0f0d818c38..80b9fd86da 100644 --- a/pallets/subtensor/src/tests/mock_high_ed.rs +++ b/pallets/subtensor/src/tests/mock_high_ed.rs @@ -327,7 +327,12 @@ impl PrivilegeCmp for OriginPrivilegeCmp { pub struct CommitmentsI; impl CommitmentsInterface for CommitmentsI { - fn purge_netuid(_netuid: NetUid) {} + fn purge_netuid( + _netuid: NetUid, + _weight_meter: &mut frame_support::weights::WeightMeter, + ) -> bool { + true + } } parameter_types! { diff --git a/pallets/subtensor/src/tests/mod.rs b/pallets/subtensor/src/tests/mod.rs index f3d363ec29..6d70521188 100644 --- a/pallets/subtensor/src/tests/mod.rs +++ b/pallets/subtensor/src/tests/mod.rs @@ -22,6 +22,8 @@ mod networks; mod neuron_info; mod recycle_alpha; mod registration; +mod remove_data_tests; +mod requested_functions_tests; mod serving; mod staking; mod staking2; diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index c4efc75825..1bc6869ded 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -3,7 +3,7 @@ use super::mock::*; use crate::migrations::migrate_network_immunity_period; use crate::*; -use frame_support::{assert_err, assert_ok}; +use frame_support::{assert_err, assert_ok, weights::Weight}; use frame_system::Config; use sp_core::U256; use sp_std::collections::{btree_map::BTreeMap, vec_deque::VecDeque}; @@ -11,6 +11,39 @@ use substrate_fixed::types::{I96F32, 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) { + let w = Weight::from_parts(u64::MAX, u64::MAX); + let mut weight_meter = frame_support::weights::WeightMeter::with_limit(w); + assert!( + SubtensorModule::destroy_alpha_in_out_stakes_get_total_alpha_value( + netuid, + &mut weight_meter + ), + "destroy_alpha_in_out_stakes_get_total_alpha_value incomplete" + ); + assert!( + SubtensorModule::destroy_alpha_in_out_stakes_settle_stakes(netuid, &mut weight_meter), + "destroy_alpha_in_out_stakes_settle_stakes incomplete" + ); + assert!( + SubtensorModule::destroy_alpha_in_out_stakes_clean_alpha(netuid, &mut weight_meter), + "destroy_alpha_in_out_stakes_clean_alpha incomplete" + ); + assert!( + SubtensorModule::destroy_alpha_in_out_stakes_clear_hotkey_totals(netuid, &mut weight_meter), + "destroy_alpha_in_out_stakes_clear_hotkey_totals incomplete" + ); + assert!( + SubtensorModule::destroy_alpha_in_out_stakes_clear_locks(netuid, &mut weight_meter), + "destroy_alpha_in_out_stakes_clear_locks incomplete" + ); + assert!( + SubtensorModule::destroy_alpha_in_out_stakes(netuid, &mut weight_meter), + "destroy_alpha_in_out_stakes incomplete" + ); +} + #[test] fn test_registration_ok() { new_test_ext(1).execute_with(|| { @@ -78,6 +111,34 @@ fn dissolve_no_stakers_no_alpha_no_emission() { }); } +#[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); + + assert!(SubnetOwner::::contains_key(net)); + assert!(NetworkRegisteredAt::::contains_key(net)); + assert!(!DissolvedNetworks::::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!(DissolvedNetworks::::get().contains(&net)); + assert!(SubnetOwner::::contains_key(net)); + assert!(NetworkRegisteredAt::::contains_key(net)); + + // Cleanup happens in on_idle. + run_block_idle(); + + assert!(!SubnetOwner::::contains_key(net)); + assert!(!NetworkRegisteredAt::::contains_key(net)); + assert!(!DissolvedNetworks::::get().contains(&net)); + }); +} + #[test] fn dissolve_refunds_full_lock_cost_when_no_emission() { new_test_ext(0).execute_with(|| { @@ -96,6 +157,7 @@ fn dissolve_refunds_full_lock_cost_when_no_emission() { 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); @@ -126,6 +188,7 @@ fn dissolve_single_alpha_out_staker_gets_all_tao() { // Dissolve assert_ok!(SubtensorModule::do_dissolve_network(net)); + run_block_idle(); // Cold-key received full pot let after = SubtensorModule::get_coldkey_balance(&s_cold); @@ -198,6 +261,7 @@ fn dissolve_two_stakers_pro_rata_distribution() { // Dissolve assert_ok!(SubtensorModule::do_dissolve_network(net)); + run_block_idle(); // Cold-keys received their τ shares assert_eq!( @@ -275,12 +339,14 @@ fn dissolve_owner_cut_refund_logic() { 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 - assert_eq!( - TaoBalance::from(after), - TaoBalance::from(before) + expected_refund + 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:?}" ); }); } @@ -468,6 +534,7 @@ fn dissolve_clears_all_per_subnet_storages() { // Dissolve // ------------------------------------------------------------------ assert_ok!(SubtensorModule::do_dissolve_network(net)); + run_block_idle(); // ------------------------------------------------------------------ // Items that must be COMPLETELY REMOVED @@ -651,6 +718,7 @@ fn dissolve_alpha_out_but_zero_tao_no_rewards() { 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. @@ -707,6 +775,7 @@ fn dissolve_rounding_remainder_distribution() { // 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); @@ -778,7 +847,7 @@ fn destroy_alpha_out_multiple_stakers_pro_rata() { let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); // 7. Run the (now credit-to-coldkey) logic - assert_ok!(SubtensorModule::destroy_alpha_in_out_stakes(netuid)); + destroy_alpha_in_out_stakes_full_pipeline_for_test(netuid); // 8. Expected τ shares via largest remainder let prod1 = (tao_pot as u128) * a1; @@ -934,7 +1003,7 @@ fn destroy_alpha_out_many_stakers_complex_distribution() { let expected_refund = lock.saturating_sub(owner_emission_tao); // ── 6) run distribution (credits τ to coldkeys, wipes α state) ───── - assert_ok!(SubtensorModule::destroy_alpha_in_out_stakes(netuid)); + destroy_alpha_in_out_stakes_full_pipeline_for_test(netuid); // ── 7) post checks ────────────────────────────────────────────────── for i in 0..N { @@ -1018,7 +1087,9 @@ fn destroy_alpha_out_refund_gating_by_registration_block() { let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); // Run the path under test - assert_ok!(SubtensorModule::destroy_alpha_in_out_stakes(netuid)); + let mut weight_meter = + frame_support::weights::WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)); + SubtensorModule::destroy_alpha_in_out_stakes(netuid, &mut weight_meter); // Owner received their refund… let owner_after = SubtensorModule::get_coldkey_balance(&owner_cold); @@ -1065,7 +1136,9 @@ fn destroy_alpha_out_refund_gating_by_registration_block() { let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); // Run the path under test - assert_ok!(SubtensorModule::destroy_alpha_in_out_stakes(netuid)); + let mut weight_meter = + frame_support::weights::WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)); + SubtensorModule::destroy_alpha_in_out_stakes(netuid, &mut weight_meter); // No refund for non‑legacy let owner_after = SubtensorModule::get_coldkey_balance(&owner_cold); @@ -1100,7 +1173,9 @@ fn destroy_alpha_out_refund_gating_by_registration_block() { SubnetOwnerCut::::put(32_768u16); // ~50% let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); - assert_ok!(SubtensorModule::destroy_alpha_in_out_stakes(netuid)); + let mut weight_meter = + frame_support::weights::WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)); + SubtensorModule::destroy_alpha_in_out_stakes(netuid, &mut weight_meter); let owner_after = SubtensorModule::get_coldkey_balance(&owner_cold); // No refund possible when lock = 0 @@ -1440,6 +1515,47 @@ fn register_network_prunes_and_recycles_netuid() { }); } +#[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!(DissolvedNetworks::::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); + DissolvedNetworks::::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(|| { @@ -2007,6 +2123,7 @@ fn massive_dissolve_refund_and_reregistration_flow_is_lossless_and_cleans_state( } for &net in nets.iter() { assert_ok!(SubtensorModule::do_dissolve_network(net)); + run_block_idle(); } // ──────────────────────────────────────────────────────────────────── @@ -2221,6 +2338,7 @@ fn dissolve_clears_all_mechanism_scoped_maps_for_all_mechanisms() { // --- 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. @@ -2735,6 +2853,7 @@ fn registered_subnet_counter_survives_dissolve_and_bumps_on_reregistration() { // 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), @@ -2757,3 +2876,109 @@ fn registered_subnet_counter_survives_dissolve_and_bumps_on_reregistration() { ); }); } + +#[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!( + DissolvedNetworks::::get().contains(&net), + "dissolved netuid should be queued for on_idle cleanup" + ); + assert!( + DissolvedNetworksCleanupPhase::::get().is_none(), + "global cleanup phase is only driven from on_idle (not from do_dissolve_network)" + ); + + run_block_idle(); + + assert!( + !DissolvedNetworks::::get().contains(&net), + "idle cleanup should drain the dissolved net from the queue" + ); + assert!( + DissolvedNetworksCleanupPhase::::get().is_none(), + "when the queue is empty, global cleanup phase storage must be cleared" + ); + }); +} + +#[test] +fn dissolve_on_idle_weight_used_never_exceeds_limit() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(920); + let owner_hot = U256::from(921); + let net = add_dynamic_network(&owner_hot, &owner_cold); + assert_ok!(SubtensorModule::do_dissolve_network(net)); + + let limit = Weight::from_parts(50_000, 50_000); + let used = SubtensorModule::on_idle(0, limit); + assert!( + used.ref_time() <= limit.ref_time() && used.proof_size() <= limit.proof_size(), + "reported weight must respect the on_idle budget (used={used:?} limit={limit:?})" + ); + }); +} + +#[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::DissolvedNetworkDataCleaned { netuid: n }) + if *n == net + ) + }), + "expected DissolvedNetworkDataCleaned after async dissolve pipeline" + ); + assert!( + DissolvedNetworksCleanupPhase::::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!(DissolvedNetworks::::get(), vec![n1, n2]); + + let mut guard = 0u32; + while !DissolvedNetworks::::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!( + DissolvedNetworksCleanupPhase::::get().is_none(), + "no stale phase after queue drain" + ); + }); +} diff --git a/pallets/subtensor/src/tests/remove_data_tests.rs b/pallets/subtensor/src/tests/remove_data_tests.rs new file mode 100644 index 0000000000..0f5a5c7a20 --- /dev/null +++ b/pallets/subtensor/src/tests/remove_data_tests.rs @@ -0,0 +1,689 @@ +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] + +use super::mock::*; +use crate::*; +use frame_support::{assert_ok, weights::Weight}; +use sp_core::U256; +use subtensor_runtime_common::{AlphaBalance, TaoBalance}; +use subtensor_swap_interface::SwapHandler; + +// Import required types from the correct locations +use pallet_commitments::pallet::Pallet as CommitmentsPallet; +use pallet_commitments::{CommitmentInfo, Data}; +use sp_runtime::BoundedVec; + +/// Test the remove_data_for_dissolved_networks macro function by testing each phase individually +#[test] +fn test_remove_data_for_dissolved_networks_all_phases() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(1001); + let owner_hot = U256::from(1002); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + + let stake_tao: u64 = 1000000; + let lock_tao: u64 = 500; + 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( + &owner_cold, + &owner_hot + )); + add_balance_to_coldkey_account(&owner_cold, amount); + // Stake into subnet to create some alpha + assert_ok!(SubtensorModule::stake_into_subnet( + &owner_hot, + &owner_cold, + netuid, + amount, + ::SwapInterface::max_price(), + false, + false, + )); + + // Now add additional balance for locking + let lock_amount: TaoBalance = (lock_tao).into(); + add_balance_to_coldkey_account(&owner_cold, lock_amount); + + // Add some commitment data + assert_ok!(CommitmentsPallet::::set_commitment( + ::RuntimeOrigin::signed(owner_hot), + netuid, + Box::new( + CommitmentInfo::<::MaxFields> { + fields: BoundedVec::try_from(vec![Data::Raw( + BoundedVec::try_from(vec![1, 2, 3]).unwrap() + )]) + .unwrap(), + } + ) + )); + + // Add some lock data - balance already added above + assert_ok!(SubtensorModule::lock_stake( + ::RuntimeOrigin::signed(owner_cold), + owner_hot, + netuid, + AlphaBalance::from(lock_tao), + )); + + // Now test the full dissolution cleanup process by running on_idle multiple times + // until all phases complete + let total_weight = Weight::from_parts(u64::MAX, u64::MAX); + let mut remaining_weight = total_weight; + + // First dissolve the network + assert_ok!(SubtensorModule::do_dissolve_network(netuid)); + + // Verify it's in the dissolved networks queue + assert!(DissolvedNetworks::::get().contains(&netuid)); + + // Run cleanup phases until completion + let mut iterations = 0; + let max_iterations = 20; // Should be enough to go through all phases + + while !DissolvedNetworks::::get().is_empty() && iterations < max_iterations { + let used_weight = SubtensorModule::on_idle(0, remaining_weight); + remaining_weight = remaining_weight.saturating_sub(used_weight); + iterations += 1; + + // If we've used all weight, reset for next iteration + if remaining_weight.is_zero() { + remaining_weight = total_weight; + } + } + + // Verify the network has been fully removed + assert!(!DissolvedNetworks::::get().contains(&netuid)); + assert_eq!( + DissolvedNetworksCleanupPhase::::get(), + None, + "Cleanup phase should be None after completion" + ); + + // Verify the subnet no longer exists + assert!(!SubtensorModule::if_subnet_exist(netuid)); + }); +} + +#[test] +fn test_clean_up_root_claimable_for_subnet() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(1001); + let owner_hot = U256::from(1002); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + + // Add some stake + let stake_tao: u64 = 1000; + setup_reserves( + netuid, + (stake_tao * 1_000_000).into(), + (stake_tao * 10_000_000).into(), + ); + let amount: TaoBalance = (stake_tao).into(); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &owner_cold, + &owner_hot + )); + add_balance_to_coldkey_account(&owner_cold, amount); + assert_ok!(SubtensorModule::stake_into_subnet( + &owner_hot, + &owner_cold, + netuid, + amount, + ::SwapInterface::max_price(), + false, + false, + )); + + // Verify root dividend exists before cleanup - we'll check this by running the function + + // Test the cleanup function + let w = Weight::from_parts(u64::MAX, u64::MAX); + let mut weight_meter = frame_support::weights::WeightMeter::with_limit(w); + let result = SubtensorModule::clean_up_root_claimable_for_subnet(netuid, &mut weight_meter); + // This function should return true when it completes its work (or false if weight limited) + // In our test case with generous weight limit, it should complete + assert!( + result, + "clean_up_root_claimable_for_subnet should complete successfully" + ); + }); +} + +#[test] +fn test_clean_up_root_claimed_for_subnet() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(1001); + let owner_hot = U256::from(1002); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + + // Add some stake to have alpha value + let stake_tao: u64 = 1000; + setup_reserves( + netuid, + (stake_tao * 1_000_000).into(), + (stake_tao * 10_000_000).into(), + ); + let amount: TaoBalance = (stake_tao).into(); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &owner_cold, + &owner_hot + )); + add_balance_to_coldkey_account(&owner_cold, amount); + // Stake into subnet to create some alpha + assert_ok!(SubtensorModule::stake_into_subnet( + &owner_hot, + &owner_cold, + netuid, + amount, + ::SwapInterface::max_price(), + false, + false, + )); + + // Note: We don't need to actually create root dividend data for this test + // The cleanup function should handle the case where there's nothing to clean up + + // Test the cleanup function + let w = Weight::from_parts(u64::MAX, u64::MAX); + let mut weight_meter = frame_support::weights::WeightMeter::with_limit(w); + let result = SubtensorModule::clean_up_root_claimed_for_subnet(netuid, &mut weight_meter); + // This function should return true when it completes its work + assert!( + result, + "clean_up_root_claimed_for_subnet should complete successfully" + ); + }); +} + +#[test] +fn test_purge_netuid() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(1001); + let owner_hot = U256::from(1002); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + + // Add some commitment data + assert_ok!(SubtensorModule::create_account_if_non_existent( + &owner_cold, + &owner_hot + )); + assert_ok!(CommitmentsPallet::::set_commitment( + ::RuntimeOrigin::signed(owner_hot), + netuid, + Box::new( + CommitmentInfo::<::MaxFields> { + fields: BoundedVec::try_from(vec![Data::Raw( + BoundedVec::try_from(vec![1, 2, 3]).unwrap() + )]) + .unwrap(), + } + ) + )); + + // Verify commitment exists + assert!(CommitmentsPallet::::commitment_of(netuid, owner_hot).is_some()); + + // Test the purge function + let w = Weight::from_parts(u64::MAX, u64::MAX); + let mut weight_meter = frame_support::weights::WeightMeter::with_limit(w); + let result = + ::CommitmentsInterface::purge_netuid(netuid, &mut weight_meter); + assert!( + result, + "purge_netuid should return true when it successfully purges data" + ); + + // Verify commitment was purged + assert!(CommitmentsPallet::::commitment_of(netuid, owner_hot).is_none()); + }); +} + +#[test] +fn test_clear_protocol_liquidity() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(1001); + let owner_hot = U256::from(1002); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + + // Add some stake to have alpha value + let stake_tao: u64 = 1000; + setup_reserves( + netuid, + (stake_tao * 1_000_000).into(), + (stake_tao * 10_000_000).into(), + ); + let amount: TaoBalance = (stake_tao).into(); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &owner_cold, + &owner_hot + )); + add_balance_to_coldkey_account(&owner_cold, amount); + // Stake into subnet to create some alpha and potentially protocol liquidity + assert_ok!(SubtensorModule::stake_into_subnet( + &owner_hot, + &owner_cold, + netuid, + amount, + ::SwapInterface::max_price(), + false, + false, + )); + + // Test the clear protocol liquidity function (through swap interface) + let w = Weight::from_parts(u64::MAX, u64::MAX); + let mut weight_meter = frame_support::weights::WeightMeter::with_limit(w); + let result = + ::SwapInterface::clear_protocol_liquidity(netuid, &mut weight_meter); + // This function should return true when it completes its work + assert!( + result, + "clear_protocol_liquidity should complete successfully" + ); + }); +} + +#[test] +fn test_remove_data_for_dissolved_networks_via_on_idle() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(1001); + let owner_hot = U256::from(1002); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + + // Add some stake to have alpha value and create various data + let stake_tao: u64 = 1000; + setup_reserves( + netuid, + (stake_tao * 1_000_000).into(), + (stake_tao * 10_000_000).into(), + ); + let amount: TaoBalance = (stake_tao).into(); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &owner_cold, + &owner_hot + )); + add_balance_to_coldkey_account(&owner_cold, amount); + // Stake into subnet to create some alpha, locks, etc. + assert_ok!(SubtensorModule::stake_into_subnet( + &owner_hot, + &owner_cold, + netuid, + amount, + ::SwapInterface::max_price(), + false, + false, + )); + + // Add some commitment data + assert_ok!(CommitmentsPallet::::set_commitment( + ::RuntimeOrigin::signed(owner_hot), + netuid, + Box::new( + CommitmentInfo::<::MaxFields> { + fields: BoundedVec::try_from(vec![Data::Raw( + BoundedVec::try_from(vec![1, 2, 3]).unwrap() + )]) + .unwrap(), + } + ) + )); + + // Now test the full dissolution cleanup process by running on_idle multiple times + // until all phases complete (this indirectly tests remove_data_for_dissolved_networks) + let total_weight = Weight::from_parts(u64::MAX, u64::MAX); + let mut remaining_weight = total_weight; + + // First dissolve the network + assert_ok!(SubtensorModule::do_dissolve_network(netuid)); + + // Verify it's in the dissolved networks queue + assert!(DissolvedNetworks::::get().contains(&netuid)); + + // Run cleanup phases until completion + let mut iterations = 0; + let max_iterations = 20; // Should be enough to go through all phases + + while !DissolvedNetworks::::get().is_empty() && iterations < max_iterations { + let used_weight = SubtensorModule::on_idle(0, remaining_weight); + remaining_weight = remaining_weight.saturating_sub(used_weight); + iterations += 1; + + // If we've used all weight, reset for next iteration + if remaining_weight.is_zero() { + remaining_weight = total_weight; + } + } + + // Verify the network has been fully removed + assert!(!DissolvedNetworks::::get().contains(&netuid)); + assert_eq!( + DissolvedNetworksCleanupPhase::::get(), + None, + "Cleanup phase should be None after completion" + ); + + // Verify the subnet no longer exists + assert!(!SubtensorModule::if_subnet_exist(netuid)); + + // Verify data has been cleaned up + assert_eq!(SubtensorModule::get_subnet_owner(netuid), U256::from(0)); + assert!(CommitmentsPallet::::commitment_of(netuid, owner_hot).is_none()); + }); +} + +#[test] +fn test_destroy_alpha_in_out_stakes_get_total_alpha_value() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(1001); + let owner_hot = U256::from(1002); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + + // Add some stake to have alpha value + let stake_tao: u64 = 1000; + setup_reserves(netuid, (stake_tao * 1_000_000).into(), (stake_tao * 10_000_000).into()); + let amount: TaoBalance = (stake_tao).into(); + assert_ok!(SubtensorModule::create_account_if_non_existent(&owner_cold, &owner_hot)); + add_balance_to_coldkey_account(&owner_cold, amount); + // Stake into subnet to create some alpha + assert_ok!(SubtensorModule::stake_into_subnet( + &owner_hot, + &owner_cold, + netuid, + amount, + ::SwapInterface::max_price(), + false, + false, + )); + + // Now test the function + let w = Weight::from_parts(u64::MAX, u64::MAX); + let mut weight_meter = frame_support::weights::WeightMeter::with_limit(w); + let result = SubtensorModule::destroy_alpha_in_out_stakes_get_total_alpha_value(netuid, &mut weight_meter); + assert!(result, "destroy_alpha_in_out_stakes_get_total_alpha_value should return true when there is alpha to process"); + }); +} + +#[test] +fn test_destroy_alpha_in_out_stakes_settle_stakes() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(1001); + let owner_hot = U256::from(1002); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + + // Add some stake to have alpha value + let stake_tao: u64 = 1000; + setup_reserves(netuid, (stake_tao * 1_000_000).into(), (stake_tao * 10_000_000).into()); + let amount: TaoBalance = (stake_tao).into(); + assert_ok!(SubtensorModule::create_account_if_non_existent(&owner_cold, &owner_hot)); + add_balance_to_coldkey_account(&owner_cold, amount); + // Stake into subnet to create some alpha + assert_ok!(SubtensorModule::stake_into_subnet( + &owner_hot, + &owner_cold, + netuid, + amount, + ::SwapInterface::max_price(), + false, + false, + )); + + // First, we need to get the total alpha value (simulate the previous step) + let w = Weight::from_parts(u64::MAX, u64::MAX); + let mut weight_meter = frame_support::weights::WeightMeter::with_limit(w); + let result_get_total = SubtensorModule::destroy_alpha_in_out_stakes_get_total_alpha_value(netuid, &mut weight_meter); + assert!(result_get_total, "destroy_alpha_in_out_stakes_get_total_alpha_value should return true when there is alpha to process"); + // Now test the settle_stakes function + let mut weight_meter2 = frame_support::weights::WeightMeter::with_limit(w); + let result_settle = SubtensorModule::destroy_alpha_in_out_stakes_settle_stakes(netuid, &mut weight_meter2); + assert!(result_settle, "destroy_alpha_in_out_stakes_settle_stakes should return true when there is alpha to settle"); + }); +} + +#[test] +fn test_destroy_alpha_in_out_stakes_clean_alpha() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(1001); + let owner_hot = U256::from(1002); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + + // Add some stake to have alpha value + let stake_tao: u64 = 1000; + setup_reserves(netuid, (stake_tao * 1_000_000).into(), (stake_tao * 10_000_000).into()); + let amount: TaoBalance = (stake_tao).into(); + assert_ok!(SubtensorModule::create_account_if_non_existent(&owner_cold, &owner_hot)); + add_balance_to_coldkey_account(&owner_cold, amount); + // Stake into subnet to create some alpha + assert_ok!(SubtensorModule::stake_into_subnet( + &owner_hot, + &owner_cold, + netuid, + amount, + ::SwapInterface::max_price(), + false, + false, + )); + + // Simulate the previous two steps: get total alpha and settle stakes + let w = Weight::from_parts(u64::MAX, u64::MAX); + let mut weight_meter = frame_support::weights::WeightMeter::with_limit(w); + let _ = SubtensorModule::destroy_alpha_in_out_stakes_get_total_alpha_value(netuid, &mut weight_meter); + let mut weight_meter2 = frame_support::weights::WeightMeter::with_limit(w); + let _ = SubtensorModule::destroy_alpha_in_out_stakes_settle_stakes(netuid, &mut weight_meter2); + // Now test the clean_alpha function + let mut weight_meter3 = frame_support::weights::WeightMeter::with_limit(w); + let result = SubtensorModule::destroy_alpha_in_out_stakes_clean_alpha(netuid, &mut weight_meter3); + assert!(result, "destroy_alpha_in_out_stakes_clean_alpha should return true when there is alpha to clean"); + }); +} + +#[test] +fn test_destroy_alpha_in_out_stakes_clear_hotkey_totals() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(1001); + let owner_hot = U256::from(1002); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + + // Add some stake to have alpha value and hotkey totals + let stake_tao: u64 = 1000; + setup_reserves(netuid, (stake_tao * 1_000_000).into(), (stake_tao * 10_000_000).into()); + let amount: TaoBalance = (stake_tao).into(); + assert_ok!(SubtensorModule::create_account_if_non_existent(&owner_cold, &owner_hot)); + add_balance_to_coldkey_account(&owner_cold, amount); + // Stake into subnet to create some alpha and hotkey totals + assert_ok!(SubtensorModule::stake_into_subnet( + &owner_hot, + &owner_cold, + netuid, + amount, + ::SwapInterface::max_price(), + false, + false, + )); + + // Simulate the previous three steps + let w = Weight::from_parts(u64::MAX, u64::MAX); + let mut weight_meter = frame_support::weights::WeightMeter::with_limit(w); + let _ = SubtensorModule::destroy_alpha_in_out_stakes_get_total_alpha_value(netuid, &mut weight_meter); + let mut weight_meter2 = frame_support::weights::WeightMeter::with_limit(w); + let _ = SubtensorModule::destroy_alpha_in_out_stakes_settle_stakes(netuid, &mut weight_meter2); + let mut weight_meter3 = frame_support::weights::WeightMeter::with_limit(w); + let _ = SubtensorModule::destroy_alpha_in_out_stakes_clean_alpha(netuid, &mut weight_meter3); + // Now test the clear_hotkey_totals function + let mut weight_meter4 = frame_support::weights::WeightMeter::with_limit(w); + let result = SubtensorModule::destroy_alpha_in_out_stakes_clear_hotkey_totals(netuid, &mut weight_meter4); + assert!(result, "destroy_alpha_in_out_stakes_clear_hotkey_totals should return true when there are hotkey totals to clear"); + }); +} + +#[test] +fn test_destroy_alpha_in_out_stakes_clear_locks() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(1001); + let owner_hot = U256::from(1002); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + + // Add some stake to have alpha value and create locks + let stake_tao: u64 = 1000; + setup_reserves(netuid, (stake_tao * 1_000_000).into(), (stake_tao * 10_000_000).into()); + let amount: TaoBalance = (stake_tao).into(); + assert_ok!(SubtensorModule::create_account_if_non_existent(&owner_cold, &owner_hot)); + add_balance_to_coldkey_account(&owner_cold, amount); + // Stake into subnet to create some alpha and locks + assert_ok!(SubtensorModule::stake_into_subnet( + &owner_hot, + &owner_cold, + netuid, + amount, + ::SwapInterface::max_price(), + false, + false, + )); + + // Simulate the previous four steps + let w = Weight::from_parts(u64::MAX, u64::MAX); + let mut weight_meter = frame_support::weights::WeightMeter::with_limit(w); + let _ = SubtensorModule::destroy_alpha_in_out_stakes_get_total_alpha_value(netuid, &mut weight_meter); + let mut weight_meter2 = frame_support::weights::WeightMeter::with_limit(w); + let _ = SubtensorModule::destroy_alpha_in_out_stakes_settle_stakes(netuid, &mut weight_meter2); + let mut weight_meter3 = frame_support::weights::WeightMeter::with_limit(w); + let _ = SubtensorModule::destroy_alpha_in_out_stakes_clean_alpha(netuid, &mut weight_meter3); + let mut weight_meter4 = frame_support::weights::WeightMeter::with_limit(w); + let _ = SubtensorModule::destroy_alpha_in_out_stakes_clear_hotkey_totals(netuid, &mut weight_meter4); + // Now test the clear_locks function + let mut weight_meter5 = frame_support::weights::WeightMeter::with_limit(w); + let result = SubtensorModule::destroy_alpha_in_out_stakes_clear_locks(netuid, &mut weight_meter5); + assert!(result, "destroy_alpha_in_out_stakes_clear_locks should return true when there are locks to clear"); + }); +} + +#[test] +fn test_destroy_alpha_in_out_stakes() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(1001); + let owner_hot = U256::from(1002); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + + // Add some stake to have alpha value and create locks, etc. + let stake_tao: u64 = 1000; + setup_reserves(netuid, (stake_tao * 1_000_000).into(), (stake_tao * 10_000_000).into()); + let amount: TaoBalance = (stake_tao).into(); + assert_ok!(SubtensorModule::create_account_if_non_existent(&owner_cold, &owner_hot)); + add_balance_to_coldkey_account(&owner_cold, amount); + // Stake into subnet to create some alpha and locks + assert_ok!(SubtensorModule::stake_into_subnet( + &owner_hot, + &owner_cold, + netuid, + amount, + ::SwapInterface::max_price(), + false, + false, + )); + + // Now test the main destroy function (which should call all the steps internally) + let w = Weight::from_parts(u64::MAX, u64::MAX); + let mut weight_meter = frame_support::weights::WeightMeter::with_limit(w); + let result = SubtensorModule::destroy_alpha_in_out_stakes(netuid, &mut weight_meter); + assert!(result, "destroy_alpha_in_out_stakes should return true when it successfully processes the netuid"); + }); +} + +#[test] +fn test_clean_up_hotkey_swap_records() { + new_test_ext(0).execute_with(|| { + // Create two subnets: netuid 1 and netuid 2 + let owner_cold = U256::from(1001); + let owner_hot = U256::from(1002); + let netuid_1 = add_dynamic_network(&owner_hot, &owner_cold); + assert_eq!(netuid_1, 1.into()); + + let owner_cold_2 = U256::from(2001); + let owner_hot_2 = U256::from(2002); + let netuid_2 = add_dynamic_network(&owner_hot_2, &owner_cold_2); + assert_eq!(netuid_2, 2.into()); + + // We will choose a block number such that block_number % interval == 1 + // With the default interval of 15, we use block_number = 16 (16 % 15 = 1) + // So only netuid_1 (which is 1) will be processed because 1 % 15 == 1 + let block_number: u64 = 16; // 16 % 15 = 1 + + // Insert some hotkey swap records for netuid_1 + // We'll insert two records: one old (should be removed) and one new (should remain) + let coldkey_old = U256::from(3001); + let coldkey_new = U256::from(3002); + // Set an old swap block number: old enough to be removed + let swap_block_old: u64 = 0; // This is definitely < block_number - interval (101 - 100 = 1) + // Set a new swap block number: recent enough to remain + let swap_block_new: u64 = 101; // This is >= block_number - interval (1) so should remain + + // Insert the records + LastHotkeySwapOnNetuid::::insert(netuid_1, coldkey_old, swap_block_old); + LastHotkeySwapOnNetuid::::insert(netuid_1, coldkey_new, swap_block_new); + + // Insert some hotkey swap records for netuid_2 (should not be processed because 2 % 100 != 1) + let coldkey_other = U256::from(4001); + let swap_block_other: u64 = 0; // old, but netuid_2 won't be processed + LastHotkeySwapOnNetuid::::insert(netuid_2, coldkey_other, swap_block_other); + + // Also insert a record for netuid_2 with a new swap block number to show it remains untouched + let coldkey_other_new = U256::from(4002); + let swap_block_other_new: u64 = 101; + LastHotkeySwapOnNetuid::::insert(netuid_2, coldkey_other_new, swap_block_other_new); + + // Before calling the function, verify the records exist + assert!(LastHotkeySwapOnNetuid::::contains_key( + netuid_1, + coldkey_old + )); + assert!(LastHotkeySwapOnNetuid::::contains_key( + netuid_1, + coldkey_new + )); + assert!(LastHotkeySwapOnNetuid::::contains_key( + netuid_2, + coldkey_other + )); + assert!(LastHotkeySwapOnNetuid::::contains_key( + netuid_2, + coldkey_other_new + )); + + // Call the function and get the returned weight + let returned_weight = SubtensorModule::clean_up_hotkey_swap_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 + // (0 + 100 < 101 -> 100 < 101 -> true) + // - The new record (coldkey_new, swap_block_new) should remain because swap_block_new + interval >= block_number + // (101 + 100 >= 101 -> 201 >= 101 -> true) + assert!( + !LastHotkeySwapOnNetuid::::contains_key(netuid_1, coldkey_old), + "Old hotkey swap record for netuid_1 should have been removed" + ); + assert!( + LastHotkeySwapOnNetuid::::contains_key(netuid_1, coldkey_new), + "New hotkey swap record for netuid_1 should still exist" + ); + // For netuid_2, since it was not processed (netuid_2 % interval != block_number % interval), both records should remain + assert!( + LastHotkeySwapOnNetuid::::contains_key(netuid_2, coldkey_other), + "Hotkey swap record for netuid_2 should remain untouched" + ); + assert!( + LastHotkeySwapOnNetuid::::contains_key(netuid_2, coldkey_other_new), + "Hotkey swap record for netuid_2 should remain untouched" + ); + + // We can also check that the weight returned is reasonable (non-zero and not max) + // Note: Weight comparison is tricky, but we can at least check it's not zero + assert!( + returned_weight.ref_time() > 0, + "Returned weight should have positive ref_time" + ); + }); +} diff --git a/pallets/subtensor/src/tests/requested_functions_tests.rs b/pallets/subtensor/src/tests/requested_functions_tests.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/pallets/subtensor/src/tests/requested_functions_tests.rs @@ -0,0 +1 @@ + diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index 0fe951a29b..d2470f6047 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -4291,11 +4291,21 @@ fn test_move_stake_limit_partial() { // 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. - assert_ok!( - ::SwapInterface::clear_protocol_liquidity(origin_netuid) + 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 + ) ); - assert_ok!( - ::SwapInterface::clear_protocol_liquidity(destination_netuid) + 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, diff --git a/pallets/swap-interface/src/lib.rs b/pallets/swap-interface/src/lib.rs index a37e9e49ad..3288e0809c 100644 --- a/pallets/swap-interface/src/lib.rs +++ b/pallets/swap-interface/src/lib.rs @@ -1,7 +1,7 @@ #![cfg_attr(not(feature = "std"), no_std)] use core::ops::Neg; -use frame_support::pallet_prelude::*; +use frame_support::{pallet_prelude::*, weights::WeightMeter}; use substrate_fixed::types::U96F32; use subtensor_macros::freeze_struct; use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; @@ -39,6 +39,7 @@ pub trait SwapHandler { fn approx_fee_amount(netuid: NetUid, amount: T) -> T; fn current_alpha_price(netuid: NetUid) -> U96F32; + fn clear_protocol_liquidity(netuid: NetUid, weight_meter: &mut WeightMeter) -> bool; fn get_protocol_tao(netuid: NetUid) -> TaoBalance; fn max_price() -> C; fn min_price() -> C; @@ -46,7 +47,6 @@ pub trait SwapHandler { fn is_user_liquidity_enabled(netuid: NetUid) -> bool; fn dissolve_all_liquidity_providers(netuid: NetUid) -> DispatchResultWithPostInfo; fn toggle_user_liquidity(netuid: NetUid, enabled: bool); - fn clear_protocol_liquidity(netuid: NetUid) -> DispatchResult; fn get_alpha_amount_for_tao(netuid: NetUid, tao_amount: TaoBalance) -> AlphaBalance; } diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index 7be087c0f0..1fc87cdd55 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -1,27 +1,32 @@ +use super::pallet::*; +use super::swap_step::{BasicSwapStep, SwapStep, SwapStepAction}; use core::ops::Neg; - -use frame_support::dispatch::DispatchResultWithPostInfo; +use frame_support::pallet_prelude::DispatchResultWithPostInfo; use frame_support::storage::{TransactionOutcome, transactional}; -use frame_support::{ensure, pallet_prelude::DispatchError, traits::Get, weights::Weight}; +use frame_support::{ + BoundedVec, ensure, + pallet_prelude::DispatchError, + traits::Get, + weights::{Weight, WeightMeter}, +}; use safe_math::*; use sp_arithmetic::{helpers_128bit, traits::Zero}; -use sp_runtime::{DispatchResult, traits::AccountIdConversion}; -use substrate_fixed::types::{I64F64, U64F64, U96F32}; -use subtensor_runtime_common::{ - AlphaBalance, BalanceOps, NetUid, SubnetInfo, TaoBalance, Token, TokenReserve, -}; +use sp_std::vec::Vec; -use super::pallet::*; -use super::swap_step::{BasicSwapStep, SwapStep, SwapStepAction}; use crate::{ SqrtPrice, position::{Position, PositionId}, tick::{ActiveTickIndexManager, Tick, TickIndex}, }; +use sp_runtime::traits::AccountIdConversion; +use substrate_fixed::types::{I64F64, U64F64, U96F32}; +use subtensor_runtime_common::{ + AlphaBalance, BalanceOps, LoopRemovePrefixWithWeightMeter, NetUid, SubnetInfo, TaoBalance, + Token, TokenReserve, WeightMeterWrapper, +}; use subtensor_swap_interface::{ DefaultPriceLimit, Order as OrderT, SwapEngine, SwapHandler, SwapResult, }; - const MAX_SWAP_ITERATIONS: u16 = 1000; #[derive(Debug, PartialEq)] @@ -836,76 +841,190 @@ impl Pallet { } /// Clear **protocol-owned** liquidity and wipe all swap state for `netuid`. - pub fn do_clear_protocol_liquidity(netuid: NetUid) -> DispatchResult { - let protocol_account = Self::protocol_account_id(); - - // 1) Force-close only protocol positions, burning proceeds. - let mut burned_tao = TaoBalance::ZERO; - let mut burned_alpha = AlphaBalance::ZERO; - - // Collect protocol position IDs first to avoid mutating while iterating. - let protocol_pos_ids: sp_std::vec::Vec = Positions::::iter_prefix((netuid,)) - .filter_map(|((owner, pos_id), _)| { - if owner == protocol_account { - Some(pos_id) - } else { - None - } - }) - .collect(); - - for pos_id in protocol_pos_ids { - match Self::do_remove_liquidity(netuid, &protocol_account, pos_id) { - Ok(rm) => { - let alpha_total_from_pool: AlphaBalance = rm.alpha.saturating_add(rm.fee_alpha); - let tao_total_from_pool: TaoBalance = rm.tao.saturating_add(rm.fee_tao); + pub fn do_clear_protocol_liquidity(netuid: NetUid, weight_meter: &mut WeightMeter) -> bool { + if CleanUpPhase::::get().is_none() { + CleanUpPhase::::set(Some( + CleanUpPhaseEnum::ClearProtocolLiquidityRemoveLiquidity, + )); + } - if tao_total_from_pool > TaoBalance::ZERO { - burned_tao = burned_tao.saturating_add(tao_total_from_pool); + // if one phase is done or exit because of weight limit + let mut phase_done = true; + // only reason for phase_done to be false is if the weight limit is reached + while phase_done { + let current_phase = CleanUpPhase::::get(); + log::debug!( + "Current phase in do_clear_protocol_liquidity is: {:?}", + current_phase + ); + let done = match current_phase { + Some(CleanUpPhaseEnum::ClearProtocolLiquidityRemoveLiquidity) => { + let done = + Self::do_clear_protocol_liquidity_remove_liquidity(netuid, weight_meter); + if done { + CleanUpPhase::::set(Some( + CleanUpPhaseEnum::ClearProtocolLiquidityTickIndexBitmapWords, + )); } - if alpha_total_from_pool > AlphaBalance::ZERO { - burned_alpha = burned_alpha.saturating_add(alpha_total_from_pool); + done + } + Some(CleanUpPhaseEnum::ClearProtocolLiquidityTickIndexBitmapWords) => { + let done = Self::do_clear_tick_index_bitmap_words(netuid, weight_meter); + if done { + CleanUpPhase::::set(Some( + CleanUpPhaseEnum::ClearProtocolLiquidityParameters, + )); } - - log::debug!( - "clear_protocol_liquidity: burned protocol pos: netuid={netuid:?}, pos_id={pos_id:?}, τ={tao_total_from_pool:?}, α_total={alpha_total_from_pool:?}" - ); + done } - Err(e) => { - log::debug!( - "clear_protocol_liquidity: force-close failed: netuid={netuid:?}, pos_id={pos_id:?}, err={e:?}" - ); - continue; + Some(CleanUpPhaseEnum::ClearProtocolLiquidityParameters) => { + let done = Self::do_clear_protocol_liquidity_parameters(netuid, weight_meter); + if done { + CleanUpPhase::::set(None); + } + done } + None => break, + }; + + phase_done = done; + } + + CleanUpPhase::::get().is_none() + } + + pub fn do_clear_protocol_liquidity_remove_liquidity( + netuid: NetUid, + weight_meter: &mut WeightMeter, + ) -> bool { + let read_weight = T::DbWeight::get().reads(1); + // weights for do_remove_liquidity function + let do_remove_liquidity_weight = T::DbWeight::get().reads_writes(2, 6); + let mut read_all = true; + let mut to_remove: Vec = Vec::new(); + + WeightMeterWrapper!(weight_meter, read_weight); + let protocol_account = Self::protocol_account_id(); + + let iter = match CleanUpLastKey::::get() { + Some(raw_key) => Positions::::iter_prefix_from((netuid,), raw_key.into_inner()), + None => Positions::::iter_prefix((netuid,)), + }; + + for ((owner, pos_id), _) in iter { + if !weight_meter.can_consume(read_weight) { + read_all = false; + let key = Positions::::hashed_key_for((netuid, &owner, pos_id)); + CleanUpLastKey::::set(Some(BoundedVec::truncate_from(key))); + break; + } + weight_meter.consume(read_weight); + + if owner != protocol_account.clone() { + continue; + } + + if !weight_meter.can_consume(do_remove_liquidity_weight) { + read_all = false; + let key = Positions::::hashed_key_for((netuid, &owner, pos_id)); + CleanUpLastKey::::set(Some(BoundedVec::truncate_from(key))); + break; } + weight_meter.consume(do_remove_liquidity_weight); + + to_remove.push(pos_id); } - // 2) Clear active tick index entries, then all swap state (idempotent even if empty/non‑V3). - let active_ticks: sp_std::vec::Vec = - Ticks::::iter_prefix(netuid).map(|(ti, _)| ti).collect(); - for ti in active_ticks { - ActiveTickIndexManager::::remove(netuid, ti); + if read_all { + CleanUpLastKey::::set(None); } - let _ = Positions::::clear_prefix((netuid,), u32::MAX, None); - let _ = Ticks::::clear_prefix(netuid, u32::MAX, None); + for pos_id in to_remove { + if let Err(e) = Self::do_remove_liquidity(netuid, &protocol_account, pos_id) { + log::debug!( + "clear_protocol_liquidity: force-close failed: netuid={netuid:?}, pos_id={pos_id:?}, err={e:?}" + ); + } + } + read_all + } + + pub fn do_clear_protocol_liquidity_parameters( + netuid: NetUid, + weight_meter: &mut WeightMeter, + ) -> bool { + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + Positions::, + (netuid,) + ); + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + Ticks, + netuid + ); + LoopRemovePrefixWithWeightMeter!( + weight_meter, + T::DbWeight::get().writes(1), + TickIndexBitmapWords::, + (netuid,) + ); + + WeightMeterWrapper!(weight_meter, T::DbWeight::get().writes(8)); FeeGlobalTao::::remove(netuid); FeeGlobalAlpha::::remove(netuid); CurrentLiquidity::::remove(netuid); CurrentTick::::remove(netuid); AlphaSqrtPrice::::remove(netuid); SwapV3Initialized::::remove(netuid); - - let _ = TickIndexBitmapWords::::clear_prefix((netuid,), u32::MAX, None); FeeRate::::remove(netuid); EnabledUserLiquidity::::remove(netuid); - log::debug!( - "clear_protocol_liquidity: netuid={netuid:?}, protocol_burned: τ={burned_tao:?}, α={burned_alpha:?}; state cleared" - ); + true + } - Ok(()) + pub fn do_clear_tick_index_bitmap_words( + netuid: NetUid, + weight_meter: &mut WeightMeter, + ) -> bool { + let read_weight = T::DbWeight::get().reads(1); + // weights for remove_liquidity_at_index ActiveTickIndexManager::::remove + let remove_weight = T::DbWeight::get().reads_writes(3, 3); + let mut read_all = true; + + let iter = match CleanUpLastKey::::get() { + Some(raw_key) => Ticks::::iter_prefix_from(netuid, raw_key.into_inner()), + None => Ticks::::iter_prefix(netuid), + }; + + for (tick_index, _) in iter { + if !weight_meter.can_consume(read_weight) { + read_all = false; + let key = Ticks::::hashed_key_for(netuid, tick_index); + CleanUpLastKey::::set(Some(BoundedVec::truncate_from(key))); + break; + } + weight_meter.consume(read_weight); + + if !weight_meter.can_consume(remove_weight) { + read_all = false; + let key = Ticks::::hashed_key_for(netuid, tick_index); + CleanUpLastKey::::set(Some(BoundedVec::truncate_from(key))); + break; + } + weight_meter.consume(remove_weight); + + ActiveTickIndexManager::::remove(netuid, tick_index); + } + + if read_all { + CleanUpLastKey::::set(None); + } + + read_all } } @@ -1030,6 +1149,9 @@ impl SwapHandler for Pallet { Self::max_price_inner() } + fn clear_protocol_liquidity(netuid: NetUid, weight_meter: &mut WeightMeter) -> bool { + Self::do_clear_protocol_liquidity(netuid, weight_meter) + } fn adjust_protocol_liquidity(netuid: NetUid, tao_delta: TaoBalance, alpha_delta: AlphaBalance) { Self::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta); } @@ -1043,9 +1165,6 @@ impl SwapHandler for Pallet { fn toggle_user_liquidity(netuid: NetUid, enabled: bool) { EnabledUserLiquidity::::insert(netuid, enabled) } - fn clear_protocol_liquidity(netuid: NetUid) -> DispatchResult { - Self::do_clear_protocol_liquidity(netuid) - } /// Get the amount of Alpha that needs to be sold to get a given amount of Tao fn get_alpha_amount_for_tao(netuid: NetUid, tao_amount: TaoBalance) -> AlphaBalance { diff --git a/pallets/swap/src/pallet/mod.rs b/pallets/swap/src/pallet/mod.rs index 763d2150b2..d4d4d9ef75 100644 --- a/pallets/swap/src/pallet/mod.rs +++ b/pallets/swap/src/pallet/mod.rs @@ -26,8 +26,20 @@ mod tests; #[allow(clippy::expect_used)] mod pallet { use super::*; + use codec::{Decode, Encode, MaxEncodedLen}; use frame_system::{ensure_root, ensure_signed}; + #[derive(Encode, Decode, Default, TypeInfo, Clone, PartialEq, Eq, Debug, MaxEncodedLen)] + pub enum CleanUpPhaseEnum { + #[default] + /// Phase 1: Clear protocol liquidity remove liquidity. + ClearProtocolLiquidityRemoveLiquidity, + /// Phase 2: Clear protocol liquidity tick index bitmap words. + ClearProtocolLiquidityTickIndexBitmapWords, + /// Phase 3: Clear protocol liquidity parameters. + ClearProtocolLiquidityParameters, + } + #[pallet::pallet] pub struct Pallet(_); @@ -158,6 +170,14 @@ mod pallet { #[pallet::storage] pub type LastPositionId = StorageValue<_, u128, ValueQuery>; + /// Current clean up phase. + #[pallet::storage] + pub type CleanUpPhase = StorageValue<_, CleanUpPhaseEnum, OptionQuery>; + + /// Last raw position key visited while clearing protocol liquidity. + #[pallet::storage] + pub type CleanUpLastKey = StorageValue<_, BoundedVec>, OptionQuery>; + /// Tick index bitmap words storage #[pallet::storage] pub type TickIndexBitmapWords = StorageNMap< diff --git a/pallets/swap/src/pallet/tests.rs b/pallets/swap/src/pallet/tests.rs index 5d75c7da27..00e5079b7b 100644 --- a/pallets/swap/src/pallet/tests.rs +++ b/pallets/swap/src/pallet/tests.rs @@ -78,6 +78,23 @@ fn tick_to_price(tick: TickIndex) -> f64 { } } +struct ClearProtocolLiquidityResult { + consumed: Weight, + done: bool, +} + +fn clear_protocol_liquidity_with_meter( + netuid: NetUid, + limit: Weight, +) -> ClearProtocolLiquidityResult { + let mut weight_meter = frame_support::weights::WeightMeter::with_limit(limit); + let done = Pallet::::do_clear_protocol_liquidity(netuid, &mut weight_meter); + ClearProtocolLiquidityResult { + consumed: weight_meter.consumed(), + done, + } +} + mod dispatchables { use super::*; @@ -1964,7 +1981,10 @@ fn test_liquidate_v3_removes_positions_ticks_and_state() { // ACT: users-only liquidation then protocol clear assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); + CleanUpPhase::::set(Some( + CleanUpPhaseEnum::ClearProtocolLiquidityRemoveLiquidity, + )); + clear_protocol_liquidity_with_meter(netuid, Weight::from_parts(u64::MAX, u64::MAX)); // ASSERT: positions cleared (both user and protocol) assert_eq!( @@ -2049,7 +2069,7 @@ fn test_liquidate_v3_removes_positions_ticks_and_state() { // // Users-only dissolve, then clear protocol liquidity/state. // assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); -// assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); +// clear_protocol_liquidity_with_meter(netuid, Weight::from_parts(u64::MAX, u64::MAX)); // // ASSERT: positions & ticks gone, state reset // assert_eq!( @@ -2094,6 +2114,7 @@ fn test_liquidate_non_v3_uninitialized_ok_and_clears() { ); // ACT + clear_protocol_liquidity_with_meter(netuid, Weight::from_parts(u64::MAX, u64::MAX)); assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); // ASSERT: Defensive clears leave no residues and do not panic @@ -2150,8 +2171,10 @@ fn test_liquidate_idempotent() { assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); // Now clear protocol liquidity/state—also idempotent. - assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); + CleanUpPhase::::set(Some( + CleanUpPhaseEnum::ClearProtocolLiquidityRemoveLiquidity, + )); + clear_protocol_liquidity_with_meter(netuid, Weight::from_parts(u64::MAX, u64::MAX)); // State remains empty assert!( @@ -2230,6 +2253,9 @@ fn refund_alpha_single_provider_exact() { AlphaReserve::increase_provided(netuid.into(), alpha_needed.into()); // --- Act: users‑only dissolve. + CleanUpPhase::::set(Some( + CleanUpPhaseEnum::ClearProtocolLiquidityRemoveLiquidity, + )); assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); // --- Assert: total α conserved to owner (may be staked to validator). @@ -2244,7 +2270,7 @@ fn refund_alpha_single_provider_exact() { ); // Clear protocol liquidity and V3 state now. - assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); + clear_protocol_liquidity_with_meter(netuid, Weight::from_parts(u64::MAX, u64::MAX)); // --- State is cleared. assert!(Ticks::::iter_prefix(netuid).next().is_none()); @@ -2410,7 +2436,10 @@ fn test_clear_protocol_liquidity_green_path() { // --- Act --- // Green path: just clear protocol liquidity and wipe all V3 state. - assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); + CleanUpPhase::::set(Some( + CleanUpPhaseEnum::ClearProtocolLiquidityRemoveLiquidity, + )); + clear_protocol_liquidity_with_meter(netuid, Weight::from_parts(u64::MAX, u64::MAX)); // --- Assert: all protocol positions removed --- let prot_positions_after = @@ -2445,7 +2474,8 @@ fn test_clear_protocol_liquidity_green_path() { assert!(!EnabledUserLiquidity::::contains_key(netuid)); // --- And it's idempotent --- - assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); + clear_protocol_liquidity_with_meter(netuid, Weight::from_parts(u64::MAX, u64::MAX)); + assert!( Positions::::iter_prefix_values((netuid, protocol_id)) .next() @@ -2461,6 +2491,67 @@ fn test_clear_protocol_liquidity_green_path() { }); } +/// `do_clear_protocol_liquidity` must seed `CleanUpPhase` when unset (entry path used by subtensor dissolve). +#[test] +fn clear_protocol_liquidity_seeds_cleanup_phase_when_none() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(156); + assert_ok!(Pallet::::maybe_initialize_v3(netuid)); + CleanUpPhase::::kill(); + assert!(CleanUpPhase::::get().is_none()); + + let result = + clear_protocol_liquidity_with_meter(netuid, Weight::from_parts(u64::MAX, u64::MAX)); + assert!( + result.done, + "unbounded weight budget should finish swap cleanup for a freshly initialized v3 subnet" + ); + assert!( + CleanUpPhase::::get().is_none(), + "completed cleanup must reset CleanUpPhase" + ); + assert!(!SwapV3Initialized::::contains_key(netuid)); + }); +} + +/// Weight returned is **consumed** work, bounded by the caller's limit (used by on_idle accounting). +#[test] +fn clear_protocol_liquidity_reports_consumed_weight_within_limit() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(157); + assert_ok!(Pallet::::maybe_initialize_v3(netuid)); + CleanUpPhase::::kill(); + + let limit = Weight::from_parts(200_000_000, 200_000_000); + let result = clear_protocol_liquidity_with_meter(netuid, limit); + assert!( + result.consumed.ref_time() <= limit.ref_time() + && result.consumed.proof_size() <= limit.proof_size(), + "consumed weight must not exceed budget (consumed={:?} limit={limit:?})", + result.consumed + ); + }); +} + +/// Ensure zero-weight mock cleanup reaches a full wipe instead of stalling in prefix clears. +#[test] +fn clear_protocol_liquidity_completes_with_zero_db_weight_budget() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(158); + assert_ok!(Pallet::::maybe_initialize_v3(netuid)); + CleanUpPhase::::kill(); + + let result = clear_protocol_liquidity_with_meter(netuid, Weight::zero()); + assert!( + result.done, + "zero-weight mock cleanup should complete without stalling" + ); + assert_eq!(result.consumed, Weight::zero()); + assert!(CleanUpPhase::::get().is_none()); + assert!(!SwapV3Initialized::::contains_key(netuid)); + }); +} + fn as_tuple( (t_used, a_used, t_rem, a_rem): (TaoBalance, AlphaBalance, TaoBalance, AlphaBalance), ) -> (u64, u64, u64, u64) { diff --git a/pallets/transaction-fee/Cargo.toml b/pallets/transaction-fee/Cargo.toml index 272faf3198..e9be867cd4 100644 --- a/pallets/transaction-fee/Cargo.toml +++ b/pallets/transaction-fee/Cargo.toml @@ -82,7 +82,7 @@ runtime-benchmarks = [ "pallet-drand/runtime-benchmarks", "pallet-grandpa/runtime-benchmarks", "pallet-preimage/runtime-benchmarks", - "pallet-scheduler/runtime-benchmarks", + "pallet-scheduler/runtime-benchmarks", "pallet-subtensor/runtime-benchmarks", "pallet-subtensor-swap/runtime-benchmarks", "subtensor-runtime-common/runtime-benchmarks", diff --git a/pallets/transaction-fee/src/tests/mock.rs b/pallets/transaction-fee/src/tests/mock.rs index 3607fd3dfa..2f4f13bdf5 100644 --- a/pallets/transaction-fee/src/tests/mock.rs +++ b/pallets/transaction-fee/src/tests/mock.rs @@ -443,7 +443,12 @@ impl PrivilegeCmp for OriginPrivilegeCmp { pub struct CommitmentsI; impl pallet_subtensor::CommitmentsInterface for CommitmentsI { - fn purge_netuid(_netuid: NetUid) {} + fn purge_netuid( + _netuid: NetUid, + _weight_meter: &mut frame_support::weights::WeightMeter, + ) -> bool { + true + } } parameter_types! { diff --git a/precompiles/src/mock.rs b/precompiles/src/mock.rs index d82422bf51..d2dd125df8 100644 --- a/precompiles/src/mock.rs +++ b/precompiles/src/mock.rs @@ -411,7 +411,12 @@ impl AuthorshipInfo for MockAuthorshipProvider { pub struct CommitmentsI; impl pallet_subtensor::CommitmentsInterface for CommitmentsI { - fn purge_netuid(_netuid: NetUid) {} + fn purge_netuid( + _netuid: NetUid, + _weight_meter: &mut frame_support::weights::WeightMeter, + ) -> bool { + true + } } impl pallet_subtensor::Config for Runtime { diff --git a/precompiles/src/solidity/ed25519Verify.sol b/precompiles/src/solidity/ed25519Verify.sol index 035feb4cc4..e422dc5250 100644 --- a/precompiles/src/solidity/ed25519Verify.sol +++ b/precompiles/src/solidity/ed25519Verify.sol @@ -6,7 +6,7 @@ address constant IED25519VERIFY_ADDRESS = 0x000000000000000000000000000000000000 interface IEd25519Verify { /** * @dev Verifies Ed25519 signature using provided message and public key. - * + * * @param message The 32-byte signature payload message. * @param publicKey 32-byte public key matching to private key used to sign the message. * @param r The Ed25519 signature commitment (first 32 bytes). diff --git a/precompiles/src/solidity/leasing.sol b/precompiles/src/solidity/leasing.sol index 184b832a10..1b9a406fac 100644 --- a/precompiles/src/solidity/leasing.sol +++ b/precompiles/src/solidity/leasing.sol @@ -12,7 +12,7 @@ interface ILeasing { /** * @dev Retrieves the contributor share for a given lease id and contributor. - * The share is returned as a tuple of two uint128 values, where the first value + * The share is returned as a tuple of two uint128 values, where the first value * is the integer part and the second value is the fractional part. * @param leaseId The id of the lease to get contributor share for. * @param contributor The contributor to get share for. diff --git a/precompiles/src/solidity/metagraph.sol b/precompiles/src/solidity/metagraph.sol index 3a19281a57..4018c2f170 100644 --- a/precompiles/src/solidity/metagraph.sol +++ b/precompiles/src/solidity/metagraph.sol @@ -12,7 +12,7 @@ struct AxonInfo { } interface IMetagraph { - + /** * @dev Returns the count of unique identifiers (UIDs) associated with a given network identifier (netuid). * @param netuid The network identifier for which to retrieve the UID count. diff --git a/precompiles/src/solidity/subnet.abi b/precompiles/src/solidity/subnet.abi index 4531f59246..496f802d1e 100644 --- a/precompiles/src/solidity/subnet.abi +++ b/precompiles/src/solidity/subnet.abi @@ -1028,7 +1028,7 @@ "outputs": [], "stateMutability": "payable", "type": "function" - }, + }, { "inputs" } diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 48269f5eb5..bf9a0c44b4 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -328,7 +328,7 @@ runtime-benchmarks = [ # Smart Tx fees pallet "subtensor-transaction-fee/runtime-benchmarks", "pallet-shield/runtime-benchmarks", - + "subtensor-runtime-common/runtime-benchmarks", "subtensor-chain-extensions/runtime-benchmarks" ] diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 00d3839fa7..f0f96d5039 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -272,7 +272,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 406, + spec_version: 407, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -875,8 +875,11 @@ impl ProxyInterface for Proxier { pub struct CommitmentsI; impl CommitmentsInterface for CommitmentsI { - fn purge_netuid(netuid: NetUid) { - pallet_commitments::Pallet::::purge_netuid(netuid); + fn purge_netuid( + netuid: NetUid, + weight_meter: &mut frame_support::weights::WeightMeter, + ) -> bool { + pallet_commitments::Pallet::::purge_netuid(netuid, weight_meter) } } @@ -997,7 +1000,8 @@ pub struct AllowCommitments; impl CanCommit for AllowCommitments { #[cfg(not(feature = "runtime-benchmarks"))] fn can_commit(netuid: NetUid, address: &AccountId) -> bool { - SubtensorModule::is_hotkey_registered_on_network(netuid, address) + SubtensorModule::if_subnet_exist(netuid) + && SubtensorModule::is_hotkey_registered_on_network(netuid, address) } #[cfg(feature = "runtime-benchmarks")] diff --git a/scripts/install_rust.sh b/scripts/install_rust.sh index 753aa3245b..8f8964f839 100755 --- a/scripts/install_rust.sh +++ b/scripts/install_rust.sh @@ -14,7 +14,7 @@ if [[ "$(uname)" == "Darwin" ]]; then if ! which brew >/dev/null 2>&1; then /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" fi - + brew update brew install openssl cmake llvm elif [[ "$(uname)" == "Linux" ]]; then diff --git a/scripts/update-deps-to-path.sh b/scripts/update-deps-to-path.sh index a1eab9b99c..f567fed51b 100755 --- a/scripts/update-deps-to-path.sh +++ b/scripts/update-deps-to-path.sh @@ -26,10 +26,10 @@ typeset -A PKG_PATHS for SOURCE_PATH in "$@"; do SOURCE_PATH="$(cd "$SOURCE_PATH" && pwd)" echo "Scanning $SOURCE_PATH for packages..." >&2 - + for cargo_toml in $(find "$SOURCE_PATH" -name "Cargo.toml" -type f 2>/dev/null); do pkg_name=$(yq -p toml -o yaml '.package.name // ""' "$cargo_toml" 2>/dev/null | tr -d '"') - + if [[ -n "$pkg_name" && "$pkg_name" != "null" ]]; then pkg_dir="$(dirname "$cargo_toml")" PKG_PATHS[$pkg_name]="$pkg_dir" @@ -49,19 +49,19 @@ while IFS= read -r line; do if [[ "$line" =~ ^([a-zA-Z0-9_-]+|\"[^\"]+\")\ *=\ *\{.*git\ *=\ *\" ]]; then # Extract package name (handle both quoted and unquoted) dep_name=$(echo "$line" | sed -E 's/^"?([a-zA-Z0-9_-]+)"? *=.*/\1/') - + # Check for package alias if [[ "$line" =~ package\ *=\ *\"([^\"]+)\" ]]; then lookup_name="${match[1]}" else lookup_name="$dep_name" fi - + # Check if we have this package if [[ -n "${PKG_PATHS[$lookup_name]}" ]]; then pkg_path="${PKG_PATHS[$lookup_name]}" echo " $dep_name -> $pkg_path" >&2 - + # Extract features/default-features/package if present extras="" if [[ "$line" =~ default-features\ *=\ *false ]]; then @@ -73,7 +73,7 @@ while IFS= read -r line; do if [[ "$line" =~ features\ *=\ *\[([^\]]*)\] ]]; then extras="$extras, features = [${match[1]}]" fi - + # Output new line with just path echo "${dep_name} = { path = \"${pkg_path}\"${extras} }" else diff --git a/support/procedural-fork/src/runtime/mod.rs b/support/procedural-fork/src/runtime/mod.rs index a96b21cd19..13bc1ea4ec 100644 --- a/support/procedural-fork/src/runtime/mod.rs +++ b/support/procedural-fork/src/runtime/mod.rs @@ -44,9 +44,9 @@ //! ```ignore //! +----------+ //! | Implicit | -//! +----------+ -//! | -//! v +//! +----------+ +//! | +//! v //! +----------+ //! | Explicit | //! +----------+ @@ -101,7 +101,7 @@ //! //! #[runtime::pallet_index(0)] //! pub type System = frame_system; -//! +//! //! #[runtime::pallet_index(1)] //! pub type Balances = pallet_balances; //! }