-
Notifications
You must be signed in to change notification settings - Fork 310
Emission suppression via root validator voting #2420
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
c00f12e
Add EmissionSuppression storage + share zeroing
ppolewicz 3a6c098
Add EmissionSuppressionOverride (root-only)
ppolewicz b7982b8
Add voting extrinsic + on-epoch vote collection
ppolewicz 24b53af
Add emission suppression tests
ppolewicz f4fda36
Add KeepRootSellPressureOnSuppressedSubnets
ppolewicz 6694b52
Address code review: events, root guard, coldkey swap conflict
ppolewicz 11b0d5f
Add 8 additional test scenarios from code review
ppolewicz 68748b9
Add /fix and /ship Claude Code skills for automated lint, test, and s…
ppolewicz c3283ab
Address code review: R-1 fail-fast validation, R-2 weight fix, R-5 Ow…
ppolewicz df8f8f4
Fix unused variable warnings in emission_suppression tests
ppolewicz b9dbe95
Fix skills location
ppolewicz bf0b574
Replace KeepRootSellPressure bool with RootSellPressureOnSuppressedSu…
ppolewicz 0cc440f
cargo fmt
ppolewicz 884ac91
Fix recycle mode tests: use add_dynamic_network, add tao_outflow trac…
ppolewicz 6d8ca77
Address code review: R-1 swap fallback, R-3 cache suppression, R-8 co…
ppolewicz 684169a
Add basic CLAUDE.md
ppolewicz ce3a4e9
Merge remote-tracking branch 'origin/devnet-ready' into emission_supp…
ppolewicz 136c1a4
Address code review: Disable mode recycles root alpha to validators, …
ppolewicz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| --- | ||
| name: fix | ||
| description: Commit changes, run Rust fix tools, run tests, and amend with any fixes | ||
| --- | ||
|
|
||
| # Fix Skill | ||
|
|
||
| Commit current changes with a descriptive message, then run Rust fix tools one by one, amending the commit after each tool if it produced changes, then run unit tests and fix any failures. | ||
|
|
||
| ## Steps | ||
|
|
||
| 1. **Initial commit**: Stage all changes and create a commit with a descriptive message summarizing the changes (use `git add -A && git commit -m "<descriptive message>"`). If there are no changes to commit, create no commit but still proceed with the fix tools below. | ||
|
|
||
| 2. **Run each fix tool in order**. After EACH tool, check `git status --porcelain` for changes. If there are changes, stage them and amend the commit (`git add -A && git commit --amend --no-edit`). | ||
|
|
||
| The tools to run in order: | ||
|
|
||
| a. `cargo check --workspace` | ||
| b. `cargo clippy --fix --workspace --all-features --all-targets --allow-dirty` | ||
| c. `cargo fix --workspace --all-features --all-targets --allow-dirty` | ||
| d. `cargo fmt --all` | ||
|
|
||
| 3. **Run unit tests in a Sonnet subagent**: Launch a Task subagent (subagent_type: `general-purpose`, model: `sonnet`) that runs: | ||
| ``` | ||
| cargo test -p pallet-subtensor --lib | ||
| ``` | ||
| The subagent must: | ||
| - Run the test command and capture full output. | ||
| - If all tests pass, report success and return. | ||
| - If any tests fail, analyze the failures: read the failing test code AND the source code it tests, determine the root cause, apply fixes using Edit tools, and re-run the tests to confirm the fix works. | ||
| - After fixing, if there are further failures, repeat (up to 3 fix-and-retest cycles). | ||
| - Return a summary of: which tests failed, what was fixed, and whether all tests pass now. | ||
|
|
||
| 4. **Amend commit with test fixes**: After the subagent returns, if any code changes were made (check `git status --porcelain`), stage and amend the commit (`git add -A && git commit --amend --no-edit`). Then re-run the fix tools from step 2 (since code changes from test fixes may need formatting/clippy cleanup), amending after each if there are changes. | ||
|
|
||
| 5. **Final output**: Show `git log --oneline -1` so the user can see the resulting commit. | ||
|
|
||
| ## Important | ||
|
|
||
| - Use `--allow-dirty` flags on clippy --fix and cargo fix since the working tree may have unstaged changes between steps. | ||
| - If a fix tool fails (step 2/4), stop and report the error to the user rather than continuing. | ||
| - Do NOT run `scripts/fix_rust.sh` itself — run the individual commands listed above instead. | ||
| - Do NOT skip any step. Run all four fix tools even if earlier ones produced no changes. | ||
| - The test subagent must fix source code to make tests pass, NOT modify tests to make them pass (unless the test itself is clearly wrong). | ||
| - If the test subagent cannot fix all failures after 3 cycles, it must return the remaining failures so the main agent can report them to the user. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| --- | ||
| name: ship | ||
| description: Ship the current branch: fix, push, create PR, watch CI, fix failures, code review | ||
| --- | ||
|
|
||
| # Ship Skill | ||
|
|
||
| Ship the current branch: fix, push, create PR if needed, watch CI, fix failures, and perform code review. | ||
|
|
||
| ## Phase 1: Fix and Push | ||
|
|
||
| 1. **Run `/fix`** — invoke the fix skill to commit, lint, and format. | ||
| 2. **Push the branch** to origin: `git push -u origin HEAD`. | ||
| 3. **Create a PR if none exists**: | ||
| - Check: `gh pr view --json number 2>/dev/null` — if it fails, no PR exists yet. | ||
| - If no PR exists, create one: | ||
| - Use `git log main..HEAD --oneline` to understand all commits on the branch. | ||
| - Read the changed files with `git diff main...HEAD --stat` to understand scope. | ||
| - Create the PR with `gh pr create --title "<concise title>" --body "<detailed markdown description>" --label "skip-cargo-audit"`. | ||
| - The description must include: a **Summary** section (bullet points of what changed and why), a **Changes** section (key files/modules affected), and a **Test plan** section. | ||
| - If a PR already exists, just note its number/URL. | ||
|
|
||
| ## Phase 2: Watch CI and Fix Failures | ||
|
|
||
| 4. **Poll CI status** in a loop: | ||
| - Run: `gh pr checks --json name,state,conclusion,link --watch --fail-fast 2>/dev/null || gh pr checks` | ||
| - If `--watch` is not available, poll manually every 90 seconds using `gh pr checks --json name,state,conclusion,link` until all checks have completed (no checks with state "pending" or conclusion ""). | ||
| - **Ignore these known-flaky/irrelevant checks** — treat them as passing even if they fail: | ||
| - `validate-benchmarks` (benchmark CI — not relevant) | ||
| - Any `Contract E2E Tests` check that failed only due to a timeout (look for timeout in the failure link/logs) | ||
| - `cargo-audit` (we already added the skip label) | ||
| - Also ignore any checks related to `check-spec-version` and `e2e` tests — these are environment-dependent and not fixable from code. | ||
|
|
||
| 5. **If there are real CI failures** (failures NOT in the ignore list above): | ||
| - For EACH distinct failing check, launch a **separate Task subagent** (subagent_type: `general-purpose`, model: `sonnet`) in parallel. Each subagent must: | ||
| - Fetch the failed check's logs: use `gh run view <run-id> --log-failed` or the check link to get failure details. | ||
| - Investigate the root cause by reading relevant source files. | ||
| - Return a **fix plan**: a description of what needs to change and in which files, with specific code snippets showing the fix. | ||
| - **Wait for all subagents** to return their fix plans. | ||
|
|
||
| 6. **Aggregate and apply fixes**: | ||
| - Review all returned fix plans for conflicts or overlaps. | ||
| - Apply the fixes using Edit/Write tools. | ||
| - Run `/fix` again (invoke the fix skill) to commit, lint, and format the fixes. | ||
| - Push: `git push`. | ||
|
|
||
| 7. **Re-check CI**: Go back to step 4 and poll again. Repeat the fix cycle up to **3 times**. If CI still fails after 3 rounds, report the remaining failures to the user and stop. | ||
|
|
||
| ## Phase 3: Code Review | ||
|
|
||
| 8. **Once CI is green** (or only ignored checks are failing), perform a thorough code review. | ||
|
|
||
| 9. **Launch a single Opus subagent** (subagent_type: `general-purpose`, model: `opus`) for the review: | ||
| - It must get the full PR diff: `git diff main...HEAD`. | ||
| - It must read every changed file in full. | ||
| - It must produce a numbered list of **issues** found, where each issue has: | ||
| - A unique sequential ID (e.g., `R-1`, `R-2`, ...). | ||
| - **Severity**: critical / major / minor / nit. | ||
| - **File and line(s)** affected. | ||
| - **Description** of the problem. | ||
| - The review must check for: correctness, safety (no panics, no unchecked arithmetic, no indexing), edge cases, naming, documentation gaps, test coverage, and adherence to Substrate/Rust best practices. | ||
| - Return the full list of issues. | ||
|
|
||
| 10. **For each issue**, launch TWO subagents **in parallel**: | ||
| - **Fix designer** (subagent_type: `general-purpose`, model: `sonnet`): Given the issue description and relevant code context, design a concrete proposed fix with exact code changes (old code -> new code). Return the fix as a structured plan. | ||
| - **Fix reviewer** (subagent_type: `general-purpose`, model: `opus`): Given the issue description, the relevant code context, and the proposed fix (once the fix designer returns — so the reviewer runs AFTER the designer, but reviewers for different issues run in parallel with each other). The reviewer must check: | ||
| - Does the fix actually solve the issue? | ||
| - Does it introduce new problems? | ||
| - Is it the simplest correct fix? | ||
| - Return: approved / rejected with reasoning. | ||
|
|
||
| Implementation note: For each issue, first launch the fix designer. Once the fix designer for that issue returns, launch the fix reviewer for that issue. But all issues should be processed in parallel — i.e., launch all fix designers at once, then as each designer returns, launch its corresponding reviewer. You may batch reviewers if designers finish close together. | ||
|
|
||
| 11. **Report to user**: Present a formatted summary: | ||
| ``` | ||
| ## Code Review Results | ||
|
|
||
| ### R-1: <title> [severity] | ||
| **File**: path/to/file.rs:42 | ||
| **Issue**: <description> | ||
| **Proposed fix**: <summary of fix> | ||
| **Review**: Approved / Rejected — <reasoning> | ||
|
|
||
| ### R-2: ... | ||
| ``` | ||
| Ask the user which fixes to apply (all approved ones, specific ones by ID, or none). | ||
|
|
||
| ## Important Rules | ||
|
|
||
| - Never force-push. Always use regular `git push`. | ||
| - All CI polling must have a maximum total wall-clock timeout of 45 minutes. If CI hasn't finished by then, report current status and stop waiting. | ||
| - When fetching CI logs, if `gh run view` output is very long, focus on the failed step output only. | ||
| - Do NOT apply code review fixes automatically — always present them for user approval first. | ||
| - Use HEREDOC syntax for PR body and commit messages to preserve formatting. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| - never use slice indexing like `arr[n..]` or `arr[i]`; use `.get(n..)`, `.get(i)` etc. instead to avoid panics (clippy::indexing_slicing) | ||
| - never use `*`, `+`, `-`, `/` for arithmetic; use `.saturating_mul()`, `.saturating_add()`, `.saturating_sub()`, `.saturating_div()` or checked variants instead (clippy::arithmetic_side_effects) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,10 @@ use safe_math::FixedExt; | |
| use substrate_fixed::transcendental::{exp, ln}; | ||
| use substrate_fixed::types::{I32F32, I64F64, U64F64, U96F32}; | ||
|
|
||
| /// Emission suppression threshold (50%). Subnets with suppression fraction | ||
| /// above this value are considered emission-suppressed. | ||
| const EMISSION_SUPPRESSION_THRESHOLD: f64 = 0.5; | ||
|
|
||
| impl<T: Config> Pallet<T> { | ||
| pub fn get_subnets_to_emit_to(subnets: &[NetUid]) -> Vec<NetUid> { | ||
| // Filter out root subnet. | ||
|
|
@@ -27,7 +31,8 @@ impl<T: Config> Pallet<T> { | |
| block_emission: U96F32, | ||
| ) -> BTreeMap<NetUid, U96F32> { | ||
| // Get subnet TAO emissions. | ||
| let shares = Self::get_shares(subnets_to_emit_to); | ||
| let mut shares = Self::get_shares(subnets_to_emit_to); | ||
| Self::apply_emission_suppression(&mut shares); | ||
| log::debug!("Subnet emission shares = {shares:?}"); | ||
|
|
||
| shares | ||
|
|
@@ -246,4 +251,78 @@ impl<T: Config> Pallet<T> { | |
| }) | ||
| .collect::<BTreeMap<NetUid, U64F64>>() | ||
| } | ||
|
|
||
| /// Normalize shares so they sum to 1.0. | ||
| pub(crate) fn normalize_shares(shares: &mut BTreeMap<NetUid, U64F64>) { | ||
| let sum: U64F64 = shares | ||
| .values() | ||
| .copied() | ||
| .fold(U64F64::saturating_from_num(0), |acc, v| { | ||
| acc.saturating_add(v) | ||
| }); | ||
| if sum > U64F64::saturating_from_num(0) { | ||
| for s in shares.values_mut() { | ||
| *s = s.safe_div(sum); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Check if a subnet is currently emission-suppressed, considering override first. | ||
| pub(crate) fn is_subnet_emission_suppressed(netuid: NetUid) -> bool { | ||
| match EmissionSuppressionOverride::<T>::get(netuid) { | ||
| Some(true) => true, | ||
| Some(false) => false, | ||
| None => { | ||
| EmissionSuppression::<T>::get(netuid) | ||
| > U64F64::saturating_from_num(EMISSION_SUPPRESSION_THRESHOLD) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Zero the emission share of any subnet whose suppression fraction exceeds 50% | ||
| /// (or is force-suppressed via override), then re-normalize the remaining shares. | ||
| pub(crate) fn apply_emission_suppression(shares: &mut BTreeMap<NetUid, U64F64>) { | ||
| let zero = U64F64::saturating_from_num(0); | ||
| let mut any_zeroed = false; | ||
| for (netuid, share) in shares.iter_mut() { | ||
| if Self::is_subnet_emission_suppressed(*netuid) { | ||
| *share = zero; | ||
| any_zeroed = true; | ||
| } | ||
| } | ||
| if any_zeroed { | ||
| Self::normalize_shares(shares); | ||
| } | ||
| } | ||
|
|
||
| /// Collect emission suppression votes from root validators for a subnet | ||
| /// and update the EmissionSuppression storage. | ||
| /// Called once per subnet per epoch. No-op for root subnet. | ||
| pub(crate) fn collect_emission_suppression_votes(netuid: NetUid) { | ||
| if netuid.is_root() { | ||
| return; | ||
| } | ||
| let root_n = SubnetworkN::<T>::get(NetUid::ROOT); | ||
| let mut suppress_stake = U64F64::saturating_from_num(0u64); | ||
| let mut total_root_stake = U64F64::saturating_from_num(0u64); | ||
|
|
||
| for uid in 0..root_n { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can iter_prefix on Keys to avoid this additional reading: |
||
| let hotkey = Keys::<T>::get(NetUid::ROOT, uid); | ||
| let root_stake = Self::get_stake_for_hotkey_on_subnet(&hotkey, NetUid::ROOT); | ||
| let stake_u64f64 = U64F64::saturating_from_num(u64::from(root_stake)); | ||
| total_root_stake = total_root_stake.saturating_add(stake_u64f64); | ||
|
|
||
| let coldkey = Owner::<T>::get(&hotkey); | ||
| if let Some(true) = EmissionSuppressionVote::<T>::get(netuid, &coldkey) { | ||
| suppress_stake = suppress_stake.saturating_add(stake_u64f64); | ||
| } | ||
| } | ||
|
|
||
| let suppression = if total_root_stake > U64F64::saturating_from_num(0u64) { | ||
| suppress_stake.safe_div(total_root_stake) | ||
| } else { | ||
| U64F64::saturating_from_num(0u64) | ||
| }; | ||
| EmissionSuppression::<T>::insert(netuid, suppression); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The f64 is non-deterministic, we don't use it in runtime. This needs to be done like: