diff --git a/docs/btcli/btcli-permissions.md b/docs/btcli/btcli-permissions.md index 718c035dd..ca029349f 100644 --- a/docs/btcli/btcli-permissions.md +++ b/docs/btcli/btcli-permissions.md @@ -8,7 +8,7 @@ This page details the requirements for all of the `btcli` commands. See also the `btcli` permissions guides for specific Bittensor personas: -- [Staker's Guide to `BTCLI`](../staking-and-delegation/stakers-btcli-guide) +- [Managing Your Stakes](../staking-and-delegation/managing-stake-sdk) - [Miner's Guide to `BTCLI`](../miners/miners-btcli-guide) - [Validator's Guide to `BTCLI`](../validators/validators-btcli-guide) - [Subnet Creator's Guide to `BTCLI`](../subnets/subnet-creators-btcli-guide) diff --git a/docs/btcli/overview.md b/docs/btcli/overview.md index d15782ddf..27964d6ba 100644 --- a/docs/btcli/overview.md +++ b/docs/btcli/overview.md @@ -9,5 +9,5 @@ The Bittensor command line interface (CLI), `btcli`, provides the simplest way t See: - [Install `btcli`](../getting-started/install-btcli) -- [Managing Stake with BTCLI](../staking-and-delegation/managing-stake-btcli.md) +- [Managing Your Stakes](../staking-and-delegation/managing-stake-sdk.md) - [`btcli reference document`](./btcli.md) diff --git a/docs/concepts/inspecting-the-chain.md b/docs/concepts/inspecting-the-chain.md new file mode 100644 index 000000000..a58f05352 --- /dev/null +++ b/docs/concepts/inspecting-the-chain.md @@ -0,0 +1,85 @@ +--- +title: "Inspecting the Chain with Polkadot.js" +--- + +# Inspecting the Chain with Polkadot.js + + +The [Polkadot.js Blockchain Explorer Browser App: `https://polkadot.js.org/apps/`](https://polkadot.js.org/apps/) offers a way to connect directly to Bittensor's blockchain layer (Subtensor), and query chain state, submit extrinsics, and inspect runtime metadata without installing any software. This page covers the features most relevant to Bittensor users. + +## Connecting to Bittensor + +Use these pre-configured links: + +- **Mainnet (Finney):** [polkadot.js/apps/?rpc=wss://entrypoint-finney.opentensor.ai:443](https://polkadot.js.org/apps/?rpc=wss://entrypoint-finney.opentensor.ai:443) +- **Testnet:** [polkadot.js/apps/?rpc=wss://test.finney.opentensor.ai:443](https://polkadot.js.org/apps/?rpc=wss://test.finney.opentensor.ai:443) + +Or click the network selector in the top-left corner and enter a custom WebSocket endpoint under **Development → Custom**. + +## Chain state (storage queries) + +**Developer → Chain state → Storage** + +This is where you read on-chain parameters and account data. Select a pallet (e.g. `subtensorModule`, `proxy`, `balances`) and a storage item, then click **+** to query. + +Example queries: + +| Query | What it returns | +|---|---| +| `subtensorModule.networkRateLimit()` | Blocks between subnet registrations | +| `subtensorModule.minStake()` | Minimum transaction amount for staking operations | +| `subtensorModule.immunityPeriod(netuid)` | Immunity period in blocks for a subnet | +| `proxy.announcements(account)` | Pending proxy announcements | + + +### Storage maps vs storage values + +Some storage items are **values** (global constants, no parameters needed). Others are **maps** keyed by account, netuid, or other identifiers. For maps, fill in the key field before querying. + +## Constants + +**Developer → Chain state → Constants** + +Runtime constants are values baked into the chain code that don't change without a runtime upgrade. Select a pallet and constant name to view. + +| Constant | What it returns | +|---|---| +| `balances.existentialDeposit` | Minimum account balance (500 RAO) | +| `proxy.maxProxies` | Maximum proxy relationships per account | +| `proxy.maxPending` | Maximum pending announcements per delegate | +| `proxy.announcementDepositBase` | Base deposit for proxy announcements | + +## Runtime calls + +**Developer → Runtime calls** + +Runtime calls execute read-only functions that may involve computation (not just storage reads). Useful for derived values like the current subnet registration cost. + +| Call | What it returns | +|---|---| +| `SubnetInfoRuntimeApi.get_subnet_info(netuid)` | Full subnet info including price, emission, reserves | +| `TransactionPaymentApi.query_info(uxt, len)` | Fee estimate for an extrinsic | + +## Extrinsics + +**Developer → Extrinsics** + +Submit transactions directly. This is useful for operations not yet supported by `btcli`, or when signing with Polkadot Vault (QR code signing from an air-gapped device). + +To submit an extrinsic: +1. Select the signing account. +2. Choose the pallet and call. +3. Fill in parameters. +4. Click **Submit Transaction**. + +For Polkadot Vault users, the app will display a QR code to scan with the Vault device for air-gapped signing. + +## Block explorer + +**Network → Explorer** + +Browse recent blocks and their extrinsics. Click any block number to see the extrinsics it contains and their events. + +- Querying at a specific block: In Chain state, toggle "include option" and enter a block hash to query historical state. +- Decoding call data: Paste raw call data under Developer → Decode to see the human-readable extrinsic. +- Metadata updates: If you're using Polkadot Vault, you must re-load chain metadata after each Bittensor runtime upgrade. See [Coldkey and Hotkey Workstation Security](../keys/coldkey-hotkey-security#hardware-solution-polkadot-vault). diff --git a/docs/dynamic-tao/_dtao-faq.md b/docs/dynamic-tao/_dtao-faq.md index a33a03942..784cf3e09 100644 --- a/docs/dynamic-tao/_dtao-faq.md +++ b/docs/dynamic-tao/_dtao-faq.md @@ -41,7 +41,7 @@ Held stake (alpha tokens) may increase or decrease in TAO value as the price of It is up to the subnet creator, and is configured using the `TransferToggle` hyperparameter. -When enabled, a holder of alpha stake can transfer its ownership to another coldkey/wallet using [`btcli stake transfer`](../staking-and-delegation/managing-stake-btcli#transferring-stake) or [`transfer_stake`](pathname:///python-api/html/autoapi/bittensor/core/async_subtensor/index.html#bittensor.core.async_subtensor.AsyncSubtensor.transfer_stake). +When enabled, a holder of alpha stake can transfer its ownership to another coldkey/wallet using [`btcli stake transfer`](../staking-and-delegation/managing-stake-sdk#transfer-stake-ownership) or [`transfer_stake`](pathname:///python-api/html/autoapi/bittensor/core/async_subtensor/index.html#bittensor.core.async_subtensor.AsyncSubtensor.transfer_stake). ### How will Dynamic TAO affect governance of the network? diff --git a/docs/dynamic-tao/sdk-cheat-sheet.md.bak b/docs/dynamic-tao/sdk-cheat-sheet.md.bak index 806ce3eb4..38e8b3440 100644 --- a/docs/dynamic-tao/sdk-cheat-sheet.md.bak +++ b/docs/dynamic-tao/sdk-cheat-sheet.md.bak @@ -39,7 +39,7 @@ Or the following configuration for synchronous calls to Bittensor mainnet ('finn ```python import bittensor as bt - sub = bt.Subtensor(network="finney") + sub = bt.Subtensor(network="test") ``` diff --git a/docs/errors/custom.md b/docs/errors/custom.md index 835d1379a..4a0e7c82c 100644 --- a/docs/errors/custom.md +++ b/docs/errors/custom.md @@ -35,6 +35,13 @@ Related: **Description**: The amount you are staking/unstaking/moving is below the minimum TAO equivalent. **Minimum**: 500,000 RAO (0.0005 TAO) +
+Check current value on-chain + +To verify the current minimum, open the [Polkadot.js app](https://polkadot.js.org/apps/?rpc=wss://entrypoint-finney.opentensor.ai:443#/chainstate) connected to Finney. Under **Developer → Chain state → Storage**, query `subtensorModule.minStake()`. See [Inspecting the Chain](../concepts/inspecting-the-chain). + +
+ ### Error Code 2 **Error**: `BalanceTooLow` diff --git a/docs/keys/_proxy-warning.mdx b/docs/keys/_proxy-warning.mdx new file mode 100644 index 000000000..ec9216be9 --- /dev/null +++ b/docs/keys/_proxy-warning.mdx @@ -0,0 +1,9 @@ +import Admonition from '@theme/Admonition'; + +export const ProxyColdkeyWarning = () => ( + +

+ The operations on this page require a coldkey. Your primary coldkey should remain in cold storage (hardware wallet) and never be loaded onto a machine running btcli or the Bittensor SDK. Use a scoped, delayed proxy coldkey to perform these operations via btcli or the SDK. See Coldkey and Hotkey Workstation Security and Proxies. +

+
+); diff --git a/docs/keys/coldkey-hotkey-security.md b/docs/keys/coldkey-hotkey-security.md index 749828056..6b4855fc4 100644 --- a/docs/keys/coldkey-hotkey-security.md +++ b/docs/keys/coldkey-hotkey-security.md @@ -6,7 +6,7 @@ import { SecurityWarning } from "./\_security-warning.mdx"; # Coldkey and Hotkey Workstation Security -This page goes into detail of security concerns for working with coldkeys and hotkeys in Bittensor. + See also: @@ -14,176 +14,196 @@ See also: - [Bittensor CLI: Permissions Guide](../btcli/btcli-permissions) - [Handle your Seed Phrase/Mnemonic Securely](./handle-seed-phrase) -Interacting with Bittensor generally falls into one of three levels of security, depending on whether you need to use your coldkey private key, hotkey private key, or neither. +## Security model -The workstations you use to do this work can be referred to as a permissionless workstation (requiring neither private key), a coldkey workstation or a hotkey workstation, depending on which private key is provisioned. +Bittensor operations fall into three security tiers based on which key is required: -- [Permisionless workstation](#permissionless-workstation) -- [Coldkey workstation](#permissionless-workstation) -- [Hotkey workstation](#permissionless-workstation) +| Tier | Key required | Recommended environment | +|---|---|---| +| **Permissionless** | Public key only | Any device | +| **Hotkey operational** | Hotkey | Mining/validation server | +| **Coldkey operational** | Scoped proxy coldkey | Internet-connected workstation | +| **Custody** | Primary coldkey | Hardware wallet only | - ## Permissionless workstation -You can check public information about Bittensor wallets (including your TAO and alpha stake balances), subnets, validators, and more _without_ using a (coldkey or hotkey) private key. This is because transaction information is public on the Bittensor blockchain, with parties being identified by their wallet's coldkey public key. +You can check public information about Bittensor wallets (including your TAO and alpha stake balances), subnets, validators, and more _without_ using a private key. Transaction information is public on the Bittensor blockchain, with parties identified by their coldkey public key. + +Whenever possible, do read-only work on a device that does _not_ have any private key loaded. -When you use a website and apps with _only your public key_, this is considered "permissionless" work. Whenever possible, you should do permissionless work on a **permissionless workstation**, meaning a device (laptop or desktop computer, mobile phone, tablet, etc.) that does _not_ have your coldkey private key loaded into it. +To use `btcli` as a permissionless workstation, import only your coldkey **public key**: -In other words, don't use your coldkey private key when you don't have to, and avoiding loading it into devices unnecessarily. Every device that _does_ have your coldkey private key loaded into it is a **coldkey workstation**, and should be used with security precautions. +```shell +btcli w regen-coldkeypub --ss58 +``` -When you just want to read/check the state of the blockchain (balances, emissions, token prices, etc.) and you don't need to use your coldkey to _change_ anything (for exmaple, to transfer TAO or move stake), it is preferable to use a permissionless workstation. +Then view balances, stakes, and blockchain state: -To use the Bittensor CLI `btcli` as a permissionless workstation: +```shell +btcli view dashboard +``` -1. Importing your coldkey **_public key_** (not private key) with: +Websites that offer permissionless browsing of Bittensor data: - ```shell - btcli w regen-coldkeypub --ss58 - ``` +- [bittensor.com/scan](https://bittensor.com/scan) +- [TAO.app (without loading a private key)](https://tao.app) -1. View your balances and stakes, as well as information about the Bittensor blockchain, subnets, miners, validators, etc., simply by running: - ```shell - btcli view dashboard - ``` +## Cold custody: hardware wallets -Websites that offer permissionless browsing of Bittensor data include: +Your primary coldkey is the ultimate authority over your Bittensor wallet. It controls all TAO and alpha balances. **This key should live in a hardware wallet and never be exported to any machine.** -- [bittensor.com/scan](https://bittensor.com/scan) -- [TAO.app (without using the browser extention to load private key)](https://tao.app) +Hardware wallets keep the private key inside the device. Signing happens on the device itself; the key is never exposed to the host machine's operating system. -## Coldkey workstation -Your coldkey private key, accessible with your recovery [seed phrase](./wallets#the-seed-phrase-aka-mnemonic), is the complete representation of your identity to Bittensor. In otherwords, holding the coldkey or seed phrase is the ultimate authority over your Bittensor wallet. If your coldkey key is leaked or stolen, it allows an attacker holder to transfer (steal) your TAO, redelegate your stakes, or take other actions that can’t be reversed. Conversely, without your coldkey private key or the seed phrase, there is no possible way to recover access to your wallet. -Because of these high stakes, best practices should be diligently followed. Always prioritize confidentiality and integrity over convenience when handling coldkeys. -### Isolation of coldkey operations -The first principle is to isolate coldkey operations from day-to-day or internet-exposed systems. This means using a dedicated machine that is minimally connected to the internet, protected with full disk encryption, and has only highly trusted software installed to minimize the risk of malware or keyloggers intercepting your coldkey. -In short, you should approach all operations involving your coldkey management as high-value, mission-critical, and laden with inherent risk. +### Operations requiring the primary coldkey -Ensure a clear boundary between coldkey operations and the working environment you use to carry them out, and everything else. +Only two operations strictly require the primary coldkey: -:::warning Do not mine with coldkeys +1. **Initial proxy setup**: creating the very first proxy relationship on a new coldkey, since no proxy exists yet to act on its behalf. +2. **Coldkey rotation**: swapping to a new coldkey. -Miners will need coldkeys to manage their TAO and alpha currency, as well as hotkeys to serve requests. Miners must ensure that there is a clear boundary—the coldkey should **never** be on an environment with untrusted ML code from containers, frameworks, or libraries that might exfiltrate secrets. -::: +All other operations, including rejecting proxy announcements, can be done through a `NonTransfer` proxy. -### Coldkey mobile device +For these, use your hardware wallet (Ledger or Polkadot Vault). Both support proxy creation and coldkey rotation. -You can use the Bittensor mobile wallet app: [bittensor.com/wallet](https://bittensor.com/wallet). If so, it is recommended to use a dedicated mobile phone for the purpose that you do not install other software on, to minimize the risk of the coldkey or seed phrase being leaked. +**After the initial proxy is in place, use it to manage all subsequent proxy relationships.** A `NonTransfer` proxy can create and remove other proxies, and perform batch operations so the primary coldkey never needs to leave cold storage again. The recommended pattern: -This option is suitable for alpha staking and TAO balance management. +1. From your hardware wallet, create a single `NonTransfer` proxy with a non-zero delay. +2. Use that proxy to create narrower, scoped proxies (`Staking`, `Registration`, etc.) as needed. +3. Use those scoped proxies for day-to-day operations. +4. Use the `NonTransfer` proxy to revoke scoped proxies when they're no longer needed. -### Coldkey laptop +If you find yourself needing to load your primary coldkey onto a machine to perform an operation, that is a signal to reconsider the approach. -This is required for using `btcli` or the Bittensor Python SDK for advanced use cases such as hotkey management and scripting. - +### Hardware Solution: Ledger -### Operational Hygiene +Ledger hardware wallets, used with a compatible wallet app, support TAO transfers, staking, unstaking, and proxy creation. Compatible wallet apps include the Bittensor mobile wallet app ([bittensor.com/wallet](https://bittensor.com/wallet)), [Crucible](https://crucible.bittensor.com/), [Talisman](https://www.talisman.xyz/), [Nova Wallet](https://novawallet.io/), and [SubWallet](https://www.subwallet.app/). -Even on a minimal or air-gapped machine, follow standard security hygiene: +See [Using Ledger Hardware Wallet](../staking-and-delegation/using-ledger-hw-wallet). + +### Hardware Solution: Polkadot Vault + +[Polkadot Vault](https://vault.novasama.io/) (formerly Parity Signer) turns a dedicated offline smartphone into a cold-signing device. The private key is generated on the device, which is then kept in airplane mode permanently. Transactions pass between the hot and cold device exclusively via QR code — the key never leaves the air-gapped phone. + +Unlike Ledger wallet apps, which expose a limited set of supported operations, Polkadot Vault can decode and sign **any Subtensor extrinsic**. -- Always [Handle your Seed Phrase/Mnemonic Securely](./handle-seed-phrase). -- Use strong passwords for your encryption passphrases. -- Do not reuse credentials across different environments. -- Keep your workstation’s operating system and critical software updated with the latest security patches. -- Disable all network services (SSH, RDP, or anything else) that are not strictly needed. -- Maintain logs of important oprations. ### Rotating your coldkey -If you suspect your coldkey may have been leaked, you can request to swap it out of your wallet, using an extrinsic blockchain transaction. This operation has a 5 day waiting period, during which your coldkey will be locked. The cost of this coldkey swap transaction is 0.1 TAO. +If you suspect the primary coldkey has been compromised, you can swap it out using an on-chain extrinsic. This operation has a 5-day waiting period during which the coldkey is locked. The cost is 0.1 TAO. + +See [Rotate/Swap your Coldkey](./coldkey-swap). + +If a proxy coldkey is comporomised it may be easier, and is certainly quicker, to revoke its proxy status and purge any references to it from your system. -See [Rotate/Swap your Coldkey](./coldkey-swap) -Effectively, this transfers all of your TAO and alpha stake balances, as well as your `sudo` control over any subnets you have created: +## Using BTCLI and the SDK with proxy coldkeys -- For each hotkey owned by the old coldkey, its stake and block transfer to the new coldkey. -- For each subnet, if the old coldkey is the owner, ownership transfers to the new coldkey. -- For each hotkey staking for the old coldkey, transfer its stake to the new coldkey. -- Total stake transfers from the old coldkey to the new coldkey. -- The list of staking hotkeys transfers from the old coldkey to the new coldkey. -- For each hotkey owned by the old coldkey, ownership transfers to the new coldkey. The list of owned hotkeys for both old and new coldkeys updates. -- Any remaining balances transfer from the old coldkey to the new coldkey. +`btcli` and the Bittensor SDK run on internet-connected machines. Any coldkey loaded onto such a machine is exposed to network risk regardless of how the machine is configured. -### Proxy wallets for coldkey protection -**Proxies are one of the most effective tools for protecting your coldkey** while maintaining operational flexibility. By setting up proxy relationships, you can perform routine operations like staking without exposing your coldkey to any online environment. +Proxies can be set to act with a requried delay, allowing a window to reject unauthorized transactions. A properly maintained, adequately monitored system of scoped, delayed proxies offers the best way to securely conduct operations that require a coldkey for advanced functionality requiring the SDK or `btcli`, such as managing subnets or hotkeys. -Key benefits: +### Recommended proxy configuration -- **Least-privilege permissions**: Configure proxies with only the specific permissions needed (e.g., `Staking` type for stake management only) -- **Time-delayed operations**: Set a non-zero delay so you have time to reject unauthorized transactions if a proxy is compromised -- **Coldkey stays in cold storage**: Your high-value coldkey never needs to leave secure offline storage for day-to-day operations +Configure proxies with: -:::warning Zero-delay proxies -A proxy with `delay: 0` and `ProxyType: Any` offers **no additional security** over direct coldkey access. Always use the narrowest `ProxyType` possible and consider adding delays for high-value operations. +- **Least-privileged proxy type**: use only the permission level the operation requires: + - `Staking`: stake and unstake only, no transfers + - `SmallTransfer`: TAO and alpha transfers below 0.5 TAO/alpha per transaction only + - `Transfer`: unlimited transfers, including all of your TAO to someone else in one quick step. + - `Registration`: hotkey registration only + - `Owner`: subnet owner operations only + +- **Non-zero delay**: a delayed proxy must announce its intent on-chain and wait a specified number of blocks before the call executes. During this window, you can reject and veto the transaction using a `NonTransfer` proxy. + + +:::warning Transfer proxy type +A proxy with `Transfer` permissions and zero delay provides little protection over direct coldkey access. It can drain your entire TAO balance in a single transaction. If transfer capability is needed, prefer `SmallTransfer`. ::: -See: +:::danger Zero-delay proxies +A proxy with `delay: 0` executes immediately with no veto window. Always set a non-zero delay for proxies that control financial operations. +::: + + +Set up proxies from your hardware wallet so the primary coldkey is never involved in day-to-day operations. See: - [Proxies: Overview](./proxies/index.md) - [Working with Proxies](./proxies/working-with-proxies.md) -- [Staking with a Proxy](./proxies/staking-with-proxy.md) +- [Managing Your Stakes](../staking-and-delegation/managing-stake-sdk.md) -### Hardware Wallets and Hardware Security Modules (HSMs) +### Proxy lifecycle -Ledger can be integrated with the Bittensor Chrome Extension. This may be a good option for managing stake and TAO balances, but does not allow for advanced functions such as hotkey management, subnet configuration, and governance. +The safest pattern for any `btcli` or SDK operation requiring a coldkey: -See [Using Ledger Hardware Wallet](../staking-and-delegation/using-ledger-hw-wallet). +1. **Create** a proxy with the narrowest type and a delay sufficient to let you detect and cancel misuse. +2. **Use** it for the specific operation. +3. **Revoke** it after you are no longer monitoring it for announcements. + +Keeping long-lived proxies with broad permissions around indefinitely defeats the purpose. The goal is to minimize the window during which a leaked key can cause damage. - +**Requirements:** -### Signing Policy and Governance +- Check for pending announcements on a schedule **shorter than your configured delay period**. A 100-block delay (~20 minutes) requires checks more frequent than that. +- Revoke any proxy relationship you are not actively monitoring. A dormant delayed proxy with no observer is no safer than a zero-delay proxy. +- Be ready to reject unexpected announcements using your `NonTransfer` proxy immediately. -If you work within a team or DAO environment that collectively manages a coldkey, consider implementing measures such as a multisig to avoid a compromise of a single individuals's keys from compromising the protected key. +See [Monitor and Reject Announcements](./proxies/working-with-proxies#monitor-and-reject-proxy-announcements) for how to query pending announcements, run a monitoring script, and reject. - +:::warning Do not mine with primary coldkeys +Miners need coldkeys for currency management and hotkeys for serving requests. The no coldkey should be present in an environment running mining code. +::: -### Periodic Security Assessments +### Team and multi-signature setups -Maintain a secure software environment: +If a team collectively manages a coldkey, you can use a multisig to prevent a single compromised team member from acting unilaterally. -- Keep an eye on newly discovered OS or hardware vulnerabilities. -- Run vulnerability scans on any machine that touches your coldkey. -- Conduct red team exercises and penetration testing to identify weaknesses in your setup. +See [Multi-signature wallets](./multisig). ## Hotkey workstation -Hotkeys in Bittensor serve as the operational keys for mining, validation, and weight commits, which require moderately high availability. Because these keys do not control direct movement of TAO balances, they pose a lower risk if compromised. Nonetheless, a malicious actor who gains control of your hotkey can damage your reputation, submit invalid weights (if you are a validator) or serve malicious responses to requests as a miner. +Hotkeys in Bittensor serve as the operational keys for mining, validation, and weight commits, requiring moderately high availability. Because hotkeys do not control direct movement of TAO balances, they pose a lower risk if compromised. Nonetheless, a malicious actor who gains control of a hotkey can damage your reputation, submit invalid weights (if you are a validator), or serve malicious responses to requests as a miner. -Overall, a hotkey workstation can be considered an “operational” environment. Losing a hotkey is less of a direct financial loss than losing a coldkey, but the reputational and operational risks can be serious. Use general best practices for managing secrets when handling your hotkeys. Include continuous monitoring of activity associated with your hotkey and have a rapid mitigation strategy in place in case your hotkey is compromised. +A hotkey workstation is an operational environment. Losing a hotkey is less of a direct financial loss than losing a coldkey, but the reputational and operational risks are serious. Use general best practices for managing secrets, monitor hotkey activity continuously, and have a rapid mitigation strategy ready if a hotkey is compromised. -### Secrets managements +### Secrets management -Bittensor miners must handle hotkeys in MLOps workflows. Hotkeys must be created in coldkey workstation environments and then provisioned to the mining/hotkey workstation environment, i.e. a server that will handle requests from validators, for example by querying an AI model to generate a response (a generated image or text response) to a text prompt from a user. +Hotkeys must be created in coldkey workstation environments and then provisioned to the mining or validation server. Options: -- Secure secrets management solution (like [HashiCorp Vault](https://www.vaultproject.io/), [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/), or [GCP Secret Manager](https://cloud.google.com/secret-manager)) to provision the hotkey private key or seedphrase to the mining server. -- Use ephemeral secret injection (CI/CD pipelines like GitLab or GitHub Actions allow storing secrets and injecting them at runtime). -- Never put keys in code repositories +- A secrets management solution such as [HashiCorp Vault](https://www.vaultproject.io/), [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/), or [GCP Secret Manager](https://cloud.google.com/secret-manager) to provision the hotkey to the server at runtime. +- Ephemeral secret injection via CI/CD pipelines (GitLab, GitHub Actions) to inject secrets at runtime without storing them on the server. +- Never put keys in code repositories. ### Hotkey rotation -If you suspect that a hotkey (but not a coldkey) has been leaked, rotate it as soon as possible using `btcli wallet swap-hotkey`. This moves the registration to a newly created hotkey owned by the same coldkey, including all of the stake delegated by other users. +If you suspect a hotkey has been leaked, rotate it as soon as possible: -Note that this operation incurs a $1 \tau$ recycling fee. +```shell +btcli wallet swap-hotkey +``` + +This moves the subnet registration to a newly created hotkey owned by the same coldkey, including all stake delegated by other users. The operation incurs a 1 TAO recycling fee. ### Minimize dependency risk -Bittensor nodes often run complex software stacks with many dependencies. Take steps to reduce risk: +Bittensor nodes often run complex software stacks with many dependencies. Steps to reduce risk: - Keep your Python environment or Docker images updated with the latest patches. -- Avoid installing unnecessary packages that might contain vulnerabilities. -- Consider sandboxing the ML library if possible, using solutions like [PyPy sandboxing](https://doc.pypy.org/en/latest/sandbox.html) or custom Docker seccomp profiles. +- Avoid installing unnecessary packages. +- Pin exact package versions and verify SHA-256 hashes with `pip install --require-hashes`. +- Consider sandboxing ML libraries using solutions like custom Docker seccomp profiles. + +For an additional layer of defense against supply chain attacks, configure network egress control — a host-level firewall that restricts outbound connections to an explicit allowlist. Even if a malicious package executes, it cannot exfiltrate key material if it cannot reach attacker-controlled infrastructure. diff --git a/docs/keys/coldkey-swap.md b/docs/keys/coldkey-swap.md index 24f4e7a52..ab6feec54 100644 --- a/docs/keys/coldkey-swap.md +++ b/docs/keys/coldkey-swap.md @@ -41,6 +41,13 @@ At this initiation step, the coldkey owner provides the destination wallet addre Next, a pending or lock-out period must elapse, during which the swap can be disputed but not finalized. Currently, the waiting/locked period is **36,000 blocks** (~ **5 days**). +
+Check current value on-chain + +To verify the current swap duration, open the [Polkadot.js app](https://polkadot.js.org/apps/?rpc=wss://entrypoint-finney.opentensor.ai:443#/chainstate) connected to Finney. Under **Developer → Chain state → Storage**, query `subtensorModule.coldkeySwapScheduleDuration()`. See [Inspecting the Chain](../concepts/inspecting-the-chain). + +
+ 3. **Disputation or Finalization** 1. [Disputing a coldkey swap](#dispute-a-coldkey-swap) prevents the execution of the swap and completely blocks the coldkey from performing any operations. At this point, the triumvirate is required to resolve the dispute. The coldkey private key is required to dispute a swap. 2. If the Pending Period expires without the swap being disputed, the coldkey owner must finalize the swap by providing the destination coldkey. It will be checked against the on-chain coldkey hash provided during announcement before proceeding. diff --git a/docs/keys/handle-seed-phrase.md b/docs/keys/handle-seed-phrase.md index 3d22cf132..8a680344b 100644 --- a/docs/keys/handle-seed-phrase.md +++ b/docs/keys/handle-seed-phrase.md @@ -98,7 +98,6 @@ Cons: Only to be used in addition to backups of the seed phrase. ::: -Here’s a concise, on-brand section you can drop into your doc, matching the tone and structure of the others: ### Mobile phone vault (e.g. Polkadot Vault) @@ -120,27 +119,3 @@ Cons: :::tip Use only a repurposed device kept permanently offline. ::: - -### Shamir's Secret Sharing - -[Shamir’s Secret Sharing (SSS)](https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing) is a cryptographic method for securely splitting a secret—like your seed phrase—into multiple pieces or “shares.” A minimum number of these shares must be recombined to reconstruct the original secret. This offers strong protection against both loss and leak. - -For example, you might split a seed phrase into 5 shares, requiring any 3 to restore the secret. These can be stored separately or given to different custodians. - -Pros: - -- Extremely resistant to both single-point loss and leakage: - - The leak of any share does not compromise your wallet. - - The loss of any share does not result in loss of the wallet. -- Shares can be safely distributed across multiple locations or people. - -Cons: - -- Imposes additional operational complexity. -- Stored secret is no longer human readable. Can be remedied with [slip39](https://github.com/satoshilabs/slips/blob/master/slip-0039.md). - -Tools: - -- [`sssa-golang`](https://github.com/SSSaaS/sssa-golang): An implementation of Shamir's Secret Sharing Algorithm in Go. -- [Banana Split](https://github.com/paritytech/banana_split): Open source tool that uses a variation of SSS to split a seed phrase into QR codes. -- [PyCryptodome SSS](https://pycryptodome.readthedocs.io/en/latest/src/protocol/ss.html): A Python-based implementation of the Shamir scheme. diff --git a/docs/keys/proxies/index.md b/docs/keys/proxies/index.md index e81caf626..83e441b20 100644 --- a/docs/keys/proxies/index.md +++ b/docs/keys/proxies/index.md @@ -4,15 +4,22 @@ title: "Proxies: Overview" # Proxies: Overview -This page introduces the theory and use of proxy wallets for enhanced security in Bittensor. +This page introduces proxy wallets, a critical security feature in Bittensor. -See [Working with Proxies](./proxies/working-with-proxies) +Operational details are covered in [Working with Proxies](../../keys/proxies/working-with-proxies) ## Introduction: What is a proxy? -Proxies allow one wallet to perform Bittensor operations on behalf of another. Used correctly, this allows you to add a strong layer of additional protection for your most important wallets and the valuable assets they control, such as large TAO or alpha holdings, or subnet ownership. Proxy relationships are useful both for one person managing their own coldkey security, and also for allowing one person to on behalf of another person or an organization. +Proxies allow one wallet to perform sign blockchains transactions on behalf of another. Used correctly, this allows you to add a strong layer of additional protection for your most important wallets and the valuable assets they control, such as large TAO or alpha holdings, or subnet ownership. + +The private key and seed phrase for a highly valuable wallet's coldkey should be kept offline in cold storage, and, and only used to sign transactions via a secure connection to a hardware wallet. + +See [Workstation Security](../../keys/coldkey-hotkey-security.md). + +By allowing one coldkey to serve as a _proxy_ or stand-in for another, the "real account" or "safe wallet", we add an additional layer of security for the safe wallet by leaving it in cold storage and using the proxy instead. + +Proxy relationships are useful both for one person managing their own coldkey security, and also for allowing one person to on behalf of another person or an organization. -The private key and seed phrase for a highly valuable wallet's coldkey should be kept offline as much as possible, and only used via a dedicated, highly secure [coldkey workstation](../coldkey-hotkey-security.md). By allowing one coldkey to serve as a _proxy_ or stand-in for another, the "real account" or "safe wallet", we add an additional layer of security for the safe wallet by leaving it in cold storage and using the proxy instead. ### Common use cases @@ -20,17 +27,29 @@ Proxies are useful in many situations where the permissions of one coldkey shoul - **Staking operations**: Keep your coldkey secure in cold storage while using a proxy to manage staking operations. - See [Staking with a Proxy](../../keys/proxies/staking-with-proxy.md). + See [Managing Your Stakes](../../staking-and-delegation/managing-stake-sdk.md). - **Operational delegation**: run subnet operations tasks like setting hyperparameters from a designated operations wallet, allowing the owner wallet to remain in maximum-security deep storage. - **Least-privilege permissions**: allow an employee or other designated operator to perform a constrained set of calls on a project-owned wallet. +- **Nearly *all* operations:** you can even manage proxies with a proxy, so other than making the first proxy, you should ideally not perform any operations with your primary coldkey. + ### Scope and Delays -The power of proxies as a security tool comes from the two ways proxies can be limited: in the scope of their permissions, and by requiring a delay with announcement before they can perform operations. It's critical to note that without using these constraints properly, proxies don't necessarily give any security benefit. +The power of proxies as a security tool comes from the two ways proxies can be limited: in the scope of their permissions, and by requiring a delay with announcement before they can perform operations. **Without using these constraints properly, proxies don't necessarily give any security benefit.** - The proxy can be constrained to specific operations. The permission scope is determined by the `ProxyType` call filter. -- The proxy can be constrained by a **delay** with a public **announcement**, giving the safe wallet holder time to reject a call made by they proxy (for example, if a key has been compromised). +- The proxy can be constrained by a **delay** with a public **announcement**, giving the safe wallet holder time to reject a call made by the proxy (for example, if a key has been compromised). + +:::danger Zero-delay proxies provide little protection + +A zero-delay proxy allows an attacker to act repeatedly and opportunistically. + +Without a delay, even a staking proxy can use `swap_stake` to repeatedly move a victim's stake through low-liquidity subnet AMMs, extracting value through slippage on each round trip. + +See: [Avoid Staking Proxy Attacks](../learn/avoid-staking-proxy-attacks) + +::: ### Terminology and parameters @@ -89,10 +108,19 @@ The following table shows the available `ProxyType` options and their descriptio When setting up and using proxies, it's important to follow practices that reduce security risks and operational overhead. The following guidelines highlight how to map permissions correctly, manage delays, and keep accounts secure while making proxy usage efficient: +- Always set a non-zero delay for proxies that control financial operations. The delay creates a veto window during which you can reject unauthorized announcements from your hardware wallet. + +- If you have a delayed proxy, monitor announcements on a schedule shorter than your delay period. See [Monitor and Reject Announcements](../../keys/proxies/working-with-proxies#monitor-and-reject-announcements). + +- Clear announcements you don't plan to execute. + +- If you are not monitoring a proxy, revoke it. Every proxy relationship is a potential attack vector. + - Map your operational needs to a minimal `ProxyType`. If a type seems overly broad, consider whether a more restrictive variant exists. -- Use non-zero delays for high-risk actions; monitor announcements. -- Track deposits and limits; batch or clear announcements to avoid dangling deposits. -- Favor maximum security over convenience when protecting your real account coldkey, using a more convenient but less protected mode of access to your proxy wallet for day for operations. + +- An `Any` proxy at zero delay is an equal risk to the primary coldkey it should be protecting, so by creating one you actually increase your risk (since either of two keys could leak), rather than reducing it. + +- Understand and practice [Coldkey and Hotkey Workstation Security](../../keys/coldkey-hotkey-security). ### Choosing the Right `ProxyType` @@ -109,5 +137,12 @@ Only use the unrestricted `Any` type when no other option fits. If a proxy call To ensure scalability and prevent abuse, proxy usage is subject to certain limits as shown: -- **`MaxProxies`**: This refers to the maximum number of delegate accounts that can be linked to a single real account. Each account can register up to 20 proxies in total. See [source code: MaxProxies configuration](https://github.com/opentensor/subtensor/blob/main/runtime/src/lib.rs#L670). -- **`MaxPending`**: This refers to the maximum number of pending announcements that a delegate account can have. This limit helps prevent excessive queuing. Each account can have up to 75 pending announcements at a time. See [source code: MaxPending configuration](https://github.com/opentensor/subtensor/blob/main/runtime/src/lib.rs#L671). +- **`MaxProxies`**: The maximum number of delegate accounts that can be linked to a single real account. Each account can register up to 20 proxies in total. See [source code: MaxProxies configuration](https://github.com/opentensor/subtensor/blob/main/runtime/src/lib.rs#L670). +- **`MaxPending`**: The maximum number of pending announcements that a delegate account can have. Each account can have up to 75 pending announcements at a time. See [source code: MaxPending configuration](https://github.com/opentensor/subtensor/blob/main/runtime/src/lib.rs#L671). + +
+Check current values on-chain + +To verify these limits, open the [Polkadot.js app](https://polkadot.js.org/apps/?rpc=wss://entrypoint-finney.opentensor.ai:443#/chainstate) connected to Finney. Under **Developer → Chain state → Constants**, select `proxy.maxProxies` and `proxy.maxPending`. See [Inspecting the Chain](../../concepts/inspecting-the-chain). + +
diff --git a/docs/keys/proxies/working-with-proxies.md b/docs/keys/proxies/working-with-proxies.md index 35f1ec635..a93efd176 100644 --- a/docs/keys/proxies/working-with-proxies.md +++ b/docs/keys/proxies/working-with-proxies.md @@ -1,6 +1,5 @@ --- title: "Working with Proxies" -# toc_max_heading_level: 2 --- import Tabs from '@theme/Tabs'; @@ -12,9 +11,11 @@ import { SdkVersion } from "../../sdk/\_sdk-version.mdx"; This page covers each step in the use of proxy wallets as a security feature for Bittensor operations: - Creating proxy relationships between existing wallets -- Executing transactions with 0-day proxy wallets. +- Executing transactions with zero-delay proxy wallets. - Announcing and then executing transactions with a non-zero delay period. -- Removing proxy relationships +- Removing proxy relationships. +- Monitoring pending announcements on your proxy accounts. +- Rejecting unauthorized announcements. See: @@ -29,10 +30,37 @@ Proxy wallets are a powerful security feature, but to get the full benefits, it Generally, the safe wallet should be given the maximum security possible, whereas the proxy wallet (if it is carefully limited in its permissions), can be handled in a more convenient, less secure way. For example, a proxy might be loaded into a less trusted compute runtime, whereas the safe wallet's coldkey private key/seed phrase should _never_ be loaded into any but the most absolutely secure device). However, depending on the proxy's configuration, compromise of a proxy wallet's coldkey can still be disastrous. For example, a proxy with `ProxyType`:`any` and `delay`:`0` can immediately perform any operation on behalf of the safe wallet, so leaking such a proxy key is just as bad as leaking the safe wallet key. + +:::warning mind your proxies! + +A non-zero delay creates a window to cancel transactions that implement attacks, but if you are not checking for announcements regularly, an attacker who has stolen a proxy key can announce a call and wait for the delay to expire without any intervention. The delay protects you only if you are watching. + +Two rules follow from this: + +1. Revoke any proxy relationship you are not actively monitoring. A dormant delayed proxy with no one watching it is little safer than a zero-delay proxy. +2. Check for pending announcements on a schedule shorter than your configured delay. If your delay is 100 blocks (~20 minutes), you must check more frequently than that to have any realistic veto window. + +See also: [Coldkey and Hotkey Workstation Security: Monitor proxy announcements](../coldkey-hotkey-security#monitor-proxy-announcements). + +::: + Before executing any operations with any coldkeys holding TAO on Bittensor main network, carefully think through the desired end result and the steps required to achieve it. See: [Coldkey and Hotkey Workstation Security](../coldkey-hotkey-security). +### Hardware wallet requirements for initial proxy setup + +The first proxy relationship on a coldkey must be created by the primary coldkey itself. This is the one operation where you cannot use a proxy, since none exists yet. To do this without exposing your primary coldkey to a hot machine, you need a hardware wallet solution that supports arbitrary Subtensor extrinsics: + +- **Polkadot Vault + Polkadot.js Apps**: Polkadot Vault loads full chain metadata and can sign any extrinsic, including `proxy.addProxy`. Transactions are passed between the air-gapped device and [polkadot.js/apps](https://polkadot.js.org/apps/) via QR code. This is the most flexible option. +- **Ledger + Talisman/SubWallet**: Ledger hardware wallets support proxy creation through compatible wallet apps like [Talisman](https://www.talisman.xyz/) and [SubWallet](https://www.subwallet.app/). + +After creating the initial `NonTransfer` proxy from your hardware wallet, all subsequent proxy management (creating scoped proxies, revoking proxies, rejecting announcements) can be done through that proxy. The primary coldkey never needs to leave cold storage again. + +If you need `btcli` or the SDK for any operation, use a proxy key, not your primary coldkey. And remember to use scope limitations, delays, and prompt revocation to limit the risk exposure of your proxies. + +See: [Coldkey and Hotkey Workstation Security](../coldkey-hotkey-security). + ### Prerequisites #### Practice/Dev @@ -50,31 +78,28 @@ Once you have practiced on a local or test chain, and you are ready to execute t - The proxy wallet, which will act on behalf of the safe wallet. :::warning fee -The delegate account must hold enough funds to cover transaction fees, which are approximately 25 Rao (0.000025 TAO). - -See: [Fees](../../learn/fees) +The delegate account must hold enough funds to cover transaction fees (fees are dynamic and weight-based; see [Transaction Fees](../../learn/fees) and [Estimating Fees](../../learn/fees#estimating-fees)). ::: ## Add a Proxy Relationship Add a proxy record on the blockchain to designate a proxy wallet for your safe wallet. -:::note consider security! +:::note coldkey security! Note that this operation requires the safe wallet's coldkey private key, which is a maximally sensitive and valuable cryptographic secret. -For any wallet with real-value TAO (i.e. TAO on Bittensor's main network, `finney`), coldkey private keys and seed phrases should be handled with utmost care, only on dedicated coldkey workstations. - See: [Coldkey and Hotkey Workstation Security](../coldkey-hotkey-security). ::: :::info Multiple proxy relationships can exist between a pair of wallets, as long as each proxy entry uses a different `ProxyType`. Attempting to register a duplicate entry with the same delegate and `ProxyType` will result in a `proxy.Duplicate` error. ::: +### Add the on-chain proxy relationship -### Add the on-chain proxy relationship + Run `btcli proxy add` to create a proxy relationship between existing wallets on-chain. @@ -99,7 +124,7 @@ For our example, we'll use two wallets called `PracticeSafeWallet` and `Practice - **`PracticeSafeWallet`**: `5CS9x5NsPHpb2THeS92zBYCSSk4MFoQjjx76DB8bEzeJTTSt` - **`PracticeProxy`**: `5CZmB94iEG4Ld7JkejAWToAw7NKEfV3YZHX7FYaqPGh7isXe` -To give the `PracticeProxy` account the ability to order small transfers from the `PracticeSafeWallet` wallet's balance immediately (with 0 delay), we'll use the following comand: +To give the `PracticeProxy` account the ability to order small transfers from the `PracticeSafeWallet` wallet's balance immediately (with 0 delay), we'll use the following command: ```bash btcli proxy add \ @@ -146,7 +171,7 @@ btcli config proxies import bittensor as bt from bittensor.core.chain_data.proxy import ProxyType -subtensor = bt.Subtensor() +subtensor = bt.Subtensor("test") real_account = bt.Wallet(name="WALLET_NAME") # Your real account delegate_address = "DELEGATE_ADDRESS" # Your delegate wallet address @@ -181,6 +206,43 @@ The proxy type can be provided either by importing and using the `ProxyType` enu +### Manage proxies through a NonTransfer proxy + +After initial setup, you should never need your primary coldkey to manage proxies. A `NonTransfer` proxy can create and remove other proxy relationships on behalf of the real account ([`is_superset` in `runtime/src/lib.rs:815-828`](https://github.com/opentensor/subtensor/blob/devnet-ready/runtime/src/lib.rs#L815-L828) defines which proxy types a `NonTransfer` proxy can manage: everything except `Transfer` and `SmallTransfer`). + +This means your primary coldkey stays in cold storage. Use it once to create the initial `NonTransfer` proxy from your hardware wallet, then use that proxy for all subsequent proxy management. + +```python +import asyncio +import bittensor as bt +from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.extrinsics.pallets import Proxy + +nontransfer_proxy_wallet = bt.Wallet(name="YOUR_NONTRANSFER_PROXY") # replace with your NonTransfer proxy wallet name +real_account_ss58 = "YOUR_REAL_ACCOUNT_SS58" # replace with your real account SS58 +new_delegate_ss58 = "NEW_DELEGATE_SS58" # replace with the SS58 of the new proxy to add + +async def main(): + async with bt.AsyncSubtensor(network="test") as subtensor: + # Create a Staking proxy via the NonTransfer proxy + add_proxy_call = await Proxy(subtensor).add_proxy( + delegate=new_delegate_ss58, + proxy_type="Staking", + delay=0, + ) + response = await subtensor.proxy( + wallet=nontransfer_proxy_wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=ProxyType.NonTransfer, + call=add_proxy_call, + ) + print(response) + +asyncio.run(main()) +``` + +To remove a proxy through the same path, use `Proxy(subtensor).remove_proxy()` with the same parameters. + ### Check an Account’s Proxies You can check which proxies are associated with an account to see their delegate addresses, proxy types, and any configured delays. To do this: @@ -197,7 +259,7 @@ btcli config proxies This displays all proxies you've saved to your local address book. -:::info On-chain proxy query +:::warning No BTCLI Coverage BTCLI does not currently provide a command to query on-chain proxy state directly. To view all proxies registered on-chain for an account, use the SDK's `get_proxies_for_real_account()` method or query via Polkadot.js Apps. ::: @@ -231,8 +293,10 @@ This returns all the proxies associated to the account and their information—` A proxy wallet that is set up with a delay of 0 can execute transactions allowed by its proxy type simply by declaring which real account they are acting as proxy for. -:::note consider security! -This operation will be run in a coldkey workstation that is set up for the _proxy wallet_, not the _safe wallet/real account_. For main network (`finney`) wallets, the safe wallet's coldkey private key should _never_ be loaded onto the proxy workstation, otherwise we undermine the security advantage of the proxy relationship. The safe wallet's coldkey private key/seed phrase should be kept in cold storage as muchas possible, and should only be loaded into dedicated, highly secure, code environments provisioned specifically for that purpose. +:::danger consider security! +This operation will be run in a coldkey workstation that is set up for the _proxy wallet_, not the _safe wallet/real account_. For main network (`finney`) wallets, the safe wallet's coldkey private key should _never_ be loaded onto the proxy workstation, otherwise we undermine the security advantage of the proxy relationship. The safe wallet's coldkey private key/seed phrase should be kept in cold storage as much as possible, and should only be loaded into dedicated, highly secure, code environments provisioned specifically for that purpose. + +However, 0-delay proxies are high-risk keys, since their compromise allows a would-be-attacker to act immediately, repeatedly and opportunistically. ::: @@ -307,7 +371,7 @@ import bittensor as bt from bittensor.core.chain_data.proxy import ProxyType from bittensor.core.extrinsics.pallets import Balances -subtensor = bt.Subtensor() +subtensor = bt.Subtensor("test") real_account = "REAL_ACCOUNT_ADDRESS" # address of the real account delegate_address = bt.Wallet(name="PROXY_WALLET") # name of the proxy wallet @@ -351,7 +415,7 @@ Balances(subtensor).transfer_keep_alive(...) ::: :::warning -The delegate account must hold enough funds to cover transaction fees, which are approximately 25 µTAO (0.000025 TAO). +The delegate account must hold enough funds to cover transaction fees (fees are dynamic and weight-based; see [Transaction Fees](../../learn/fees) and [Estimating Fees](../../learn/fees#estimating-fees)). ::: @@ -382,6 +446,12 @@ After submitting the transaction, check the Polkadot.JS web app's **Explorer** p Removing a proxy revokes the delegate’s permission to act on behalf of the primary account, effectively ending the proxy relationship on-chain. To remove a proxy: +:::note coldkey security! +Note that this operation requires the safe wallet's coldkey private key, which is a maximally sensitive and valuable cryptographic secret. + +See: [Coldkey and Hotkey Workstation Security](../coldkey-hotkey-security). +::: + @@ -429,7 +499,7 @@ Unlike delayed execution, removing a proxy takes effect immediately, regardless import bittensor as bt from bittensor.core.chain_data.proxy import ProxyType -subtensor = bt.Subtensor() +subtensor = bt.Subtensor("test") real_account = bt.Wallet(name="WALLET_NAME") delegate_address = "DELEGATE_ADDRESS" @@ -451,10 +521,10 @@ print(response) 2. Under “using the selected account”, pick the delegator account. 3. Under "submit the following extrinsic", choose the `proxy` pallet and call `removeProxy(delegate, proxyType, delay)`. 4. Fill the parameters: - - `delegate`: select the imported delegate account from the _Accounts_ dropdown. - - `proxyType`: select `SmallTransfer`; this should allow us to transfer amounts that do not exceed 0.5τ. - - `delay`: Optionally, include a delay in blocks. -5. Click **Submit Transaction** and sign with the _delegator_ account. + - `delegate`: select the delegate account to remove. + - `proxyType`: must match the proxy type used when the proxy was added. + - `delay`: must match the delay value used when the proxy was added. +5. Click **Submit Transaction** and sign with the _delegator_ (real) account. @@ -466,6 +536,11 @@ The `delegate_ss58`, `proxy_type`, and `delay` parameters must exactly match tho ### Remove all proxies Use this to remove all proxies associated with an account. +:::note coldkey security! +Note that this operation requires the safe wallet's coldkey private key, which is a maximally sensitive and valuable cryptographic secret. + +See: [Coldkey and Hotkey Workstation Security](../coldkey-hotkey-security). +::: @@ -484,9 +559,9 @@ To remove all proxies in one operation, use the SDK's `remove_proxies()` method. ```python import bittensor as bt -subtensor = bt.Subtensor(network="local") +subtensor = bt.Subtensor(network="test") -real_account = bt.Wallet(name="sn-creator") +real_account = bt.Wallet(name="WALLET_NAME") response = subtensor.remove_proxies(wallet=real_account) @@ -533,7 +608,7 @@ Added proxy delegatee '5CZmB94iEG4Ld7JkejAWToAw7NKEfV3YZHX7FYaqPGh7isXe' from de import bittensor as bt from bittensor.core.chain_data.proxy import ProxyType -subtensor = bt.Subtensor() +subtensor = bt.Subtensor("test") real_account = bt.Wallet(name="WALLET_NAME") # Your real account delegate_address = "DELEGATE_ADDRESS" # Your delegate wallet address @@ -570,7 +645,7 @@ When you run a BTCLI command with the `--announce-only` flag, BTCLI automaticall import bittensor as bt from bittensor.core.extrinsics.pallets import Balances -subtensor = bt.Subtensor() +subtensor = bt.Subtensor("test") recipient_address = "RECIPIENT_WALLET" @@ -584,7 +659,7 @@ transfer_call = Balances(subtensor).transfer_keep_alive( # Get the call hash call_hash = "0x" + transfer_call.call_hash.hex() -print(response) +print(call_hash) ``` @@ -649,7 +724,7 @@ Block Hash: 0x1c6378ee38b8c27f161b646125ec301f1aa52bffd63b090ec0c0876c9cc56ba5 Balance: 98.4409 τ ➡ 98.4409 τ -```` +``` **What this does:** @@ -703,7 +778,7 @@ response = subtensor.announce_proxy( print(response) # Save the call_hash - you'll need it to execute after the delay -```` +``` :::info Next, wait for the duration of the configured delay before executing the call. During the waiting period, the delegate can cancel the announcement—`subtensor.remove_proxy_announcement()`, while the real account retains final authority to reject it—`subtensor.reject_proxy_announcement()`. @@ -831,6 +906,264 @@ The call you execute **must have the exact same parameters** as the call you ann - Once a delayed proxy call is executed, its announcement is cleared. To execute another proxy with the same details, you must create a new announcement and wait for the waiting period to pass. ::: +## Monitor and Reject Proxy Announcements + +### Check pending announcements + +The `Proxy.Announcements` chain state is keyed by **delegate (proxy) account**. To find all pending announcements against your coldkey, query each of your configured proxy delegates and filter for entries where your coldkey is the real account. + +:::tip +No coldkey is required for this operation, like all chain reads. +::: + + + + +:::warning No BTCLI command +BTCLI does not currently have a command to list pending on-chain announcements. Use the SDK or Polkadot.js Apps. +::: + + + + + + +```python +import asyncio +import bittensor as bt + +MY_COLDKEY_SS58 = "" # replace with your coldkey SS58 +BLOCK_TIME_SECONDS = 12 + +async def check_announcements(): + async with bt.AsyncSubtensor(network="test") as subtensor: + # Get your configured proxy relationships to know delegate addresses and delays + proxies, _ = await subtensor.get_proxies_for_real_account( + real_account_ss58=MY_COLDKEY_SS58 + ) + if not proxies: + print("No proxy relationships configured.") + return + print(proxies) + + current_block = await subtensor.get_current_block() + for proxy_info in proxies: + if proxy_info.delay == 0: + continue # 0-delay proxies have no announcement mechanism + + announcements = await subtensor.get_proxy_announcement(proxy_info.delegate) + for ann in announcements: + if ann.real != MY_COLDKEY_SS58: + continue + blocks_elapsed = current_block - ann.height + blocks_remaining = proxy_info.delay - blocks_elapsed + time_remaining_s = blocks_remaining * BLOCK_TIME_SECONDS + print( + f"PENDING ANNOUNCEMENT\n" + f" delegate: {proxy_info.delegate}\n" + f" proxy_type: {proxy_info.proxy_type}\n" + f" call_hash: {ann.call_hash}\n" + f" announced: block {ann.height} ({blocks_elapsed} blocks ago)\n" + f" veto window: {max(0, blocks_remaining)} blocks remaining " + f"({max(0, time_remaining_s):.0f} s)\n" + ) + +asyncio.run(check_announcements()) +``` + + + + +1. Go to **Developer** → **Chain state** → **Storage**. +2. Select the `proxy` pallet and `announcements` storage function. +3. Enter the **delegate (proxy) account** SS58 address. +4. Click **+** to run the query. + +This returns all pending announcements for that delegate, each with the real account, call hash, and block height. + + + + + +### Reject an announcement + +If you find an unexpected announcement, reject it immediately. Rejection cancels the announcement on-chain and prevents execution. You can reject through a `NonTransfer` proxy (the proxy pallet sets the origin to the real account, satisfying the on-chain check), so your primary coldkey can stay in cold storage. + + + + +:::warning No BTCLI command +BTCLI does not currently have a command to reject proxy announcements. Use the SDK. +::: + + + + + + +Use a `NonTransfer` proxy to reject announcements on behalf of the real account. + +```python +import asyncio +import bittensor as bt +from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.extrinsics.pallets import Proxy + +async def main(): + async with bt.AsyncSubtensor(network="test") as subtensor: + nontransfer_proxy_wallet = bt.Wallet(name="YOUR_NONTRANSFER_PROXY") # replace with your NonTransfer proxy wallet name + real_account_ss58 = "YOUR_REAL_ACCOUNT_SS58" # replace with your real account SS58 + delegate_ss58 = "DELEGATE_SS58" # proxy account that made the announcement + + reject_call = await Proxy(subtensor).reject_announcement( + delegate=delegate_ss58, + call_hash="0x...", # call_hash from the announcement + ) + response = await subtensor.proxy( + wallet=nontransfer_proxy_wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=ProxyType.NonTransfer, + call=reject_call, + ) + print(response) + +asyncio.run(main()) +``` + + + + +With Polkadot.js you can reject directly from the real account (signing with Polkadot Vault keeps the primary coldkey air-gapped), or through a `NonTransfer` proxy like the SDK examples above. + +**Direct rejection from the real account:** + +1. Go to **Developer** → **Extrinsics**. +2. Under "using the selected account", choose the **real account**. +3. Select the `proxy` pallet and call `rejectAnnouncement(delegate, callHash)`. +4. Fill the parameters: + - `delegate`: the delegate (proxy) account that made the announcement. + - `callHash`: the call hash from the announcement. +5. Click **Submit Transaction** and sign with Polkadot Vault (QR code). + +**Through a NonTransfer proxy:** + +1. Go to **Developer** → **Extrinsics**. +2. Under "using the selected account", choose your `NonTransfer` proxy account. +3. Select the `proxy` pallet and call `proxy(real, forceProxyType, call)` where the inner call is `rejectAnnouncement(delegate, callHash)`. +4. Fill the parameters: + - `real`: your real account SS58 address. + - `forceProxyType`: `NonTransfer`. + - `delegate`: the delegate (proxy) account that made the announcement. + - `callHash`: the call hash from the announcement. +5. Click **Submit Transaction** and sign from the `NonTransfer` proxy account. + + + + +:::tip Use a NonTransfer proxy for rejections +A `NonTransfer` proxy can reject announcements on behalf of the real account, so your primary coldkey stays in cold storage even during incident response. Set up a `NonTransfer` proxy with zero delay specifically for monitoring and rejection. +::: + +### Reject all pending announcements + +If you need to clear all pending announcements from a delegate at once — for example, after aborting a staking run or if you suspect your proxy key is compromised — use this script to fetch and reject every pending announcement as a single atomic [batch transaction](../../learn/batch-transactions). + +```python +import asyncio +import bittensor as bt +from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.extrinsics.pallets import Proxy + +async def main(): + async with bt.AsyncSubtensor(network="test") as subtensor: + nontransfer_proxy_wallet = bt.Wallet(name="YOUR_NONTRANSFER_PROXY") # replace with your NonTransfer proxy wallet name + real_account_ss58 = "YOUR_REAL_ACCOUNT_SS58" # replace with your real account SS58 + delegate_ss58 = "DELEGATE_SS58" # replace with your proxy account SS58 address + + # Fetch all pending announcements for this delegate + announcements = await subtensor.get_proxy_announcement(delegate_ss58) + + if not announcements: + print("No pending announcements found.") + return + + print(f"Found {len(announcements)} pending announcements. Batch rejecting...") + + # Build a reject call for each announcement + reject_calls = [] + for ann in announcements: + print(f" Will reject: {ann.call_hash}") + call = await Proxy(subtensor).reject_announcement( + delegate=delegate_ss58, + call_hash=ann.call_hash, + ) + reject_calls.append(call) + + # Wrap in batch_all — reverts all rejections atomically on any failure + batch_call = await subtensor.compose_call( + call_module="Utility", + call_function="batch_all", + call_params={"calls": reject_calls}, + ) + + # Submit via NonTransfer proxy — primary coldkey stays in cold storage + response = await subtensor.proxy( + wallet=nontransfer_proxy_wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=ProxyType.NonTransfer, + call=batch_call, + ) + print(response) + +asyncio.run(main()) +``` +## Register on a subnet with a proxy + +Use a `Registration` proxy to register a hotkey on a subnet. The proxy coldkey signs the transaction; your primary coldkey never needs to be present on the machine. + +```python +import os +import bittensor as bt +from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.extrinsics.pallets import SubtensorModule + +sub = bt.Subtensor(network="test") + +proxy_wallet = bt.Wallet(name=os.environ['BT_PROXY_WALLET_NAME']) +real_account_ss58 = os.environ['BT_REAL_ACCOUNT_SS58'] + +hotkey_wallet = bt.Wallet( + name="ExampleWalletName", + hotkey="ExampleHotkey", +) + +burned_register_call = SubtensorModule(sub).burned_register( + netuid=3, + hotkey=hotkey_wallet.hotkey.ss58_address, +) + +response = sub.proxy( + wallet=proxy_wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=ProxyType.Registration, + call=burned_register_call, +) +print(response) +``` + + +Querying which subnets a hotkey is registered on is a permissionless operation — only the public key is needed: + +```python +import bittensor as bt +sub = bt.Subtensor(network="test") +wallet = bt.Wallet( + name="ExampleWalletName", + hotkey="ExampleHotkey", +) +netuids = sub.get_netuids_for_hotkey(wallet.hotkey.ss58_address) +print(netuids) +``` ## Troubleshooting - `proxy.Duplicate`: A proxy with the same configuration already exists on the real account. See [source code: `Duplicate` error](https://github.com/opentensor/subtensor/blob/main/pallets/proxy/src/lib.rs#L739). diff --git a/docs/keys/wallets.md b/docs/keys/wallets.md index d9ec4deb5..0137efa0e 100644 --- a/docs/keys/wallets.md +++ b/docs/keys/wallets.md @@ -22,7 +22,6 @@ See [Proxies: Overview](./proxies/index.md) to learn how to protect your coldkey ## What are wallets and keys? -The core of your wallet is one or more cryptographic key-pairs, referred to as **coldkey** and **hotkey**. Your wallet essentially consists of the records associated with your key-pairs on the blockchain, including your balances of TAO and alpha currencies, and your history of transactions and interactions with subnets and other wallets (such as mining or validating). Each coldkey or hotkey is actually a cryptographic [key-pair](https://en.wikipedia.org/wiki/Public-key_cryptography)with a private and a public key. The public key is mathematically derived from the private key. @@ -41,17 +40,47 @@ An existential deposit is the minumum required TAO in a wallet (i.e., in a coldk If a wallet balance goes below the existential deposit, then this wallet account is deactivated and the remaining TAO in it is destroyed. **This is set to 500 RAO for any Bittensor wallet**. +
+Check current value on-chain + +To verify, open the [Polkadot.js app](https://polkadot.js.org/apps/?rpc=wss://entrypoint-finney.opentensor.ai:443#/chainstate) connected to Finney. Under **Developer → Chain state → Constants**, select `balances.existentialDeposit`. See [Inspecting the Chain](../concepts/inspecting-the-chain). + +
+ See also [What is the Existential Deposit?](https://support.polkadot.network/support/solutions/articles/65000168651-what-is-the-existential-deposit-). ::: -## Wallets and wallet applications +## Cryptographic, hardware, and software wallets + +We must be careful to distinguish two senses of the term 'wallet' that can otherwise be confusing, the **cryptographic wallet (the cryptographic key needed to sign transactions)**, a **hardware wallet (secure device designed to hold cryptographic keys)**, and **wallet applications/software wallets**, the software designed to perform blockchain operations (using the cryptographic wallet). + +- The **cryptographic wallet** defines a person or organization's identity in Bittensor. It consists of one or more cryptographic key pairs that allow a person to sign transactions or be referred to in transactions signed by others. In this sense, the wallet is more or less synonymous with the unique **coldkey** that controls access to your assets and serves as your public identity, and connects you to any hotkeys you control. Your on-chain identity consists of the records associated with your key-pairs, including your balances of TAO and alpha currencies, and your history of transactions and interactions with subnets and other wallets. + + Every Bittensor user has one or more cryptographic wallets, i.e. one or more coldkey. Any cryptographic wallet can be loaded into any number of wallet applications. Even every wallet application that has been initialized with your cryptographic wallet (i.e. signed into with your coldkey private key) is closed, logged out, etc., and the device incinerated, your cryptographic wallet exists on the blockchain, and can be recovered with your _seed phrase_. + + The **_seed phrase_** (a.k.a. 'menemonic' or 'recovery phrase') is a series of (at least 12) words that is generated together with your wallet's cryptographic key pair, and which can be used to recover the coldkey private key. This seed phrase is therefore a human-usable way to save access to the cryptographic wallet offline, and to import the cryptographic wallet into a hardware or software wallet. -We must be careful to distinguish two senses of the term 'wallet' that can otherwise be confusing: + The most important operational goal when handling Bittensor wallets is to avoid losing or leaking your seed phrase. Make sure you [Handle your Seed Phrase/Mnemonic Securely](./handle-seed-phrase). + +- A **hardware wallet** is a device designed to hold the private key for a cryptographic wallet and use it to sign transactions across a secure device connection that protects the private key. + - The Ledger wallet is a supported, special purpose signing wallet device compatible with several Bittensor-compatible wallet applications you can run on your laptop. It connects via a secure usb connection using a protocol that does not expose the private key + - Using [Polkadot Vault](https://vault.novasama.io/), you can turn a dedicated mobile phone into an offline secure hardware wallet. + - A laptop acts effectively acts as a hardware wallet when you use a coldkey with either the Bittensor command line interface, `BTCLI`, or the Bittensor Python SDK. This is necessary to perform certain operations, such as managing hotkeys (necessary for mining and validating), or managing a subnet + +- The **wallet application** is software that runs on your device and allows you to interact with the blockchain by entering your keys, as described below. + :::tip + Different wallet applications have different levels of functionality, and represent different levels of operational risk. Some minimize risk by allowing you to compose transactions and then sign them securely across a device boundary that protects the key from the possibility of being leaked (an 'airgap'). + + The fullest-featured clients (BTCLI and the SDK) do not support hardware wallet signing, and therefore should be used with proxy keys to bolster security. + ::: + + +## Wallet applications -- The **cryptographic wallet** is one or more cryptographic key pairs that comprise an identity, and allow a person to sign transactions or be referred to in transactions signed by others. In this sense, the wallet is more or less synonymous with the unique **coldkey** that controls access to your assets and serves as your public identity. +There are many different applications that can interact with your public and/or private keys in some way. -- The **wallet application** is software that runs on your device and allows you to interact with the blockchain by entering your keys. There are several officially supported Bittensor wallet applications: +There are several officially supported Bittensor wallet applications: - The Bittensor wallet app for mobile: [bittensor.com/wallet](https://bittensor.com/wallet) - [The Crucible wallet](https://cruciblelabs.com) a Tao wallet featuring an auto-allocator for dynamic TAO staking across subnets, with full Ledger integration. - [The Polkadot browser extension](https://polkadot.js.org/extension/) which can be used with Polkadot Vault. @@ -63,9 +92,6 @@ We must be careful to distinguish two senses of the term 'wallet' that can other - The Bittensor Python SDK, which includes the secure [Bittensor Wallet module](https://docs.bittensor.com/btwallet-api/html/autoapi/btwallet/wallet/index.html). - The Bittensor CLI, `btcli`, which uses the Bittensor Wallet module under the hood. -Every Bittensor user has one or more cryptographic wallets, i.e. one or more coldkey. Any cryptographic wallet can be loaded into any number of wallet applications. If every wallet application that has been initialized with your cryptographic wallet (i.e. signed into with your coldkey private key) is closed, logged out, etc., and the device incinerated, your cryptographic wallet exists on the blockchain, and can be recovered with your _seed phrase_. - -Different wallet applications have different levels of functionality: - The mobile app and Chrome extension allow for staking and transfer of TAO balalnces, but do not include any hotkey management or advanced functionality. @@ -74,23 +100,12 @@ Different wallet applications have different levels of functionality: - The mobile app depends on using a secure phone as a [coldkey workstation](./coldkey-hotkey-security). - `btcli` and the SDK allow for hotkey management and other advanced functionality. These require a laptop as a [coldkey workstation](./coldkey-hotkey-security). + :::tip + Note that you can also check balances on an unsecure device without entering your coldkey private key. For example, using [https://bittensor.com/scan](https://bittensor.com/scan). These website can be considered permissionless wallet applications. -:::tip -Note that you can also check balances on an unsecure device without entering your coldkey private key. For example, using [https://bittensor.com/scan](https://bittensor.com/scan). These website can be considered permissionless wallet applications. - -See [Coldkey and Hotkey Workstation Security: Permissionless workstation](./coldkey-hotkey-security#permissionless-workstation) -::: - -## The seed phrase a.k.a. mnemonic - -The **_seed phrase_** (a.k.a. 'menemonic' or 'recovery phrase') is a series of (at least 12) words that is generated together with your wallet's cryptographic key pair, and which can be used to recover the coldkey private key. This seed phrase is therefore a human-usable way to save access to the cryptographic wallet offline, and to import the cryptographic wallet into a wallet application. - -Arguably the most important operational goal when handling Bittensor wallets is to avoid losing or leaking your seed phrase. Make sure you [Handle your Seed Phrase/Mnemonic Securely](./handle-seed-phrase). - -## Wallet applications - -There are many different applications that can interact with your public and/or private keys in some way. - + See [Coldkey and Hotkey Workstation Security: Permissionless workstation](./coldkey-hotkey-security#permissionless-workstation) + ::: + ### Permissionless wallet apps You can visit [bittensor.com/scan](https://bittensor.com/scan) and enter a coldkey public key to view public information about any wallet. @@ -155,7 +170,7 @@ Hotkeys are used to register on a subnet as a miner or validator. **Relationship to coldkey**: You can create multiple hotkeys paired to your single coldkey. However, when you are validating or mining in a subnet, you are identified by a hotkey in that subnet, so that your coldkey is not exposed. -You can’t use the same hotkey for multiple UIDs within a single subnet — each UID in a subnet requires its own hotkey. However, you can reuse the same hotkey for UIDs that belong to different subnets. +You can’t use the same hotkey for multiple UIDs within a single subnet. However, you can reuse the same hotkey for UIDs that belong to different subnets. **Purpose**: Hotkeys are used for regular operational tasks in the Bittensor network, such as those described below: diff --git a/docs/keys/working-with-keys.md b/docs/keys/working-with-keys.md index 7311e6017..40492ad88 100644 --- a/docs/keys/working-with-keys.md +++ b/docs/keys/working-with-keys.md @@ -25,7 +25,7 @@ The most critical operational goal when handling Bittensor wallets is to avoid l ::: :::tip Use proxies for regular operations -Once you've created your wallet, consider setting up **proxy wallets** for any operations you'll perform regularly. Proxies let you keep your coldkey in secure cold storage while still managing stake, making transfers, or performing other operations through a less-privileged proxy account. +Once you've created your wallet, set up **proxy wallets** for any operations you'll perform regularly. Proxies let you keep your coldkey in secure cold storage while still managing stake, making transfers, or performing other operations through a less-privileged proxy account. See [Proxies: Overview](./proxies/index.md) to learn more. ::: diff --git a/docs/learn/avoid-staking-proxy-attacks.md b/docs/learn/avoid-staking-proxy-attacks.md new file mode 100644 index 000000000..141ed1bab --- /dev/null +++ b/docs/learn/avoid-staking-proxy-attacks.md @@ -0,0 +1,47 @@ +--- +title: "Avoid Staking Proxy Attacks" +--- + +import { SecurityWarning } from "../keys/_security-warning.mdx"; + +# Avoid Staking Proxy Attacks + +A `Staking` proxy relationship allows one wallet to stake on behalf of another. A typical way to use this is to keep your TAO in a 'safe' wallet, and to use another wallet that is a staking proxy of the first only to stake and unstake on behalf of the other. This allows the first wallet to remain in 'cold storage' without ever having its key cryptographic material loaded onto a 'hot' device connected to the internet. + +Start with: [Proxies: Overview](../keys/proxies/index.md). + +It might seem like leaking a staking proxy key would be not so bad; unlike a transfer proxy, an attacker can't use it to just steal all of your TAO in one step. + +However, an attacker that gains your staking proxy can still drain your token balance by repeatedly setting you up for maximized multi-transaction sandwich attacks (bypassing MEV-shield because of unfolding over multiple transaction), where you make unfavorable trades, losing value each time as the attacker gains. + +## How the attack works + +1. The attacker stakes some of their own TAO into a subnet (first leg). +2. Using the stolen `Staking` proxy, they stake a large amount of the victim's TAO into the same subnet and hotkey (second leg), moving the pool further. +3. The attacker unstakes their alpha (third leg). +4. Using the proxy again, they unstake the victim's alpha (fourth leg). + +The attack may be obscure to the victim, in that no transaction links their account directly to a the attackers. The loss shows up only as unexpected transactions that lose value to high slippage. + +See [Slippage](./slippage.md). + +## Protect yourself: non-zero delay + monitoring + +A non-zero delay forces the delegate to announce a call and wait for a number of blocks before execution. During that window, the real account (for example from a hardware wallet) can reject the announcement. With zero delay, there is no such window: a leaked delegate key can act as fast as the chain accepts extrinsics. + +Whether delay helps you in practice depends on whether you actually check for announcements on a schedule shorter than the delay. If you never look, a long delay only helps after the fact in forensics, not prevention. + +See [Monitor and Reject Proxy Announcements](../keys/proxies/working-with-proxies#monitor-and-reject-proxy-announcements) + +- Prefer scoped proxy types and non-zero delay for any delegate that can touch meaningful stake. See [Proxies: Overview](../keys/proxies/index.md) and [Working with Proxies](../keys/proxies/working-with-proxies.md). +- Treat a compromised `Staking` proxy (especially one with 0-delay) as a real operational risk, not "low impact": rotate or remove the proxy, and assume stake-moving activity until you verify otherwise. +- Keep the custody coldkey off hot workstations; follow the best practices described in [Coldkey and Hotkey Workstation Security](../keys/coldkey-hotkey-security.md). +- Revoke proxies you are not actively using if you do not want ongoing monitoring burden. + +## Learn more + +- [Proxies: Overview](../keys/proxies/index.md) (main proxy documentation) +- [Working with Proxies](../keys/proxies/working-with-proxies.md) +- [Coldkey and Hotkey Workstation Security](../keys/coldkey-hotkey-security.md) +- [Slippage](./slippage.md) +- [Price protection](./price-protection.md) diff --git a/docs/learn/batch-transactions.md b/docs/learn/batch-transactions.md index 92a3960fd..d91b24dc7 100644 --- a/docs/learn/batch-transactions.md +++ b/docs/learn/batch-transactions.md @@ -2,6 +2,8 @@ title: "Batch Transactions" --- +import { ProxyColdkeyWarning } from "../keys/_proxy-warning.mdx"; + # Batch Transactions The Bittensor runtime's utility pallet exposes three extrinsics — `batch`, `batch_all`, and `force_batch` — that let you submit multiple calls as a single on-chain transaction. This is useful when you want to stake to multiple hotkeys, perform multiple operations atomically, or reduce the number of round-trips to the chain. @@ -18,67 +20,58 @@ The three variants differ only in how they handle errors. Choose based on whethe | `batch_all` | Reverts all calls atomically on any failure. | | `force_batch` | Continues past failures; failed calls are skipped. | -:::info -Use `batch_all` when all inner calls must succeed or none should. Use `batch` if partial success is acceptable, or `force_batch` to continue past failures. -::: +:::info +Use `batch_all` when all inner calls must succeed or none should. Use `batch` if partial success is acceptable, or `force_batch` to continue past failures. +::: **Source code:** `batch` [`pallets/utility/src/lib.rs:197–201`](https://github.com/opentensor/subtensor/blob/main/pallets/utility/src/lib.rs#L197-L201), `batch_all` [`pallets/utility/src/lib.rs:309–313`](https://github.com/opentensor/subtensor/blob/main/pallets/utility/src/lib.rs#L309-L313), `force_batch` [`pallets/utility/src/lib.rs:408–412`](https://github.com/opentensor/subtensor/blob/main/pallets/utility/src/lib.rs#L408-L412). ## Using batch calls with the SDK + The SDK's `add_stake_multiple` and `unstake_multiple` send individual extrinsics sequentially, not a single batch extrinsic. -To submit a true batch (one extrinsic on-chain), use the low-level `compose_call` + `sign_and_send_extrinsic` path directly. +To submit multiple stake actions as an atomic batch (one extrinsic on-chain), use the low-level pallet builder + `proxy` path. The batch call is wrapped in a proxy extrinsic signed by your proxy wallet. +:::note Proxy type for batch calls +The `Staking` proxy type is an allowlist of specific staking extrinsics. It does not permit `Utility::batch_all` as the outer call, so batch staking via a `Staking` proxy will fail with `CallFiltered`. Use a `NonTransfer` proxy for batch staking. `NonTransfer` blocks only balance transfers and coldkey swaps, allowing everything else including batch wrappers. `NonCritical` also works but is more permissive than most users need. +::: ```python +import os import bittensor as bt +from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.extrinsics.pallets import SubtensorModule -sub = bt.Subtensor(network="finney") -wallet = bt.Wallet(name="my_wallet", hotkey="my_hotkey") -wallet.unlock_coldkey() +sub = bt.Subtensor(network="test") +proxy_wallet = bt.Wallet(name=os.environ['BT_PROXY_WALLET_NAME']) +real_account_ss58 = os.environ['BT_REAL_ACCOUNT_SS58'] hotkey_1 = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" hotkey_2 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" netuid = 1 amount = bt.Balance.from_tao(10) -# Compose each inner call individually -call_1 = sub.compose_call( - call_module="SubtensorModule", - call_function="add_stake", - call_params={ - "hotkey": hotkey_1, - "netuid": netuid, - "amount_staked": amount.rao, - }, -) -call_2 = sub.compose_call( - call_module="SubtensorModule", - call_function="add_stake", - call_params={ - "hotkey": hotkey_2, - "netuid": netuid, - "amount_staked": amount.rao, - }, -) +pallet = SubtensorModule(sub) +call_1 = pallet.add_stake(netuid=netuid, hotkey=hotkey_1, amount_staked=amount.rao) +call_2 = pallet.add_stake(netuid=netuid, hotkey=hotkey_2, amount_staked=amount.rao) -# Wrap in a Utility.batch_all — reverts all calls atomically on any failure +# Wrap in Utility.batch_all — reverts all calls atomically on any failure batch_call = sub.compose_call( call_module="Utility", call_function="batch_all", call_params={"calls": [call_1, call_2]}, ) -# Submit the batch as a single extrinsic -success, error_message = sub.sign_and_send_extrinsic( +# Submit via proxy — Staking proxy type cannot wrap batch calls, use NonTransfer +response = sub.proxy( + wallet=proxy_wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=ProxyType.NonTransfer, call=batch_call, - wallet=wallet, - wait_for_inclusion=True, - wait_for_finalization=False, ) -print(f"Success: {success}" if success else f"Failed: {error_message}") +print(response) ``` :::note `add_stake_multiple` is not a batch extrinsic diff --git a/docs/learn/chain-rate-limits.md b/docs/learn/chain-rate-limits.md index b3674a839..1e4590b77 100644 --- a/docs/learn/chain-rate-limits.md +++ b/docs/learn/chain-rate-limits.md @@ -62,10 +62,17 @@ This rate limit controls how frequently subnet owners can trim UIDs on their sub This rate limit prevents frequent creation of new subnets. -- Rate Limit: 28,800 blocks (4 days) +- Rate Limit: 14,400 blocks (~2 days) - Chain State Variable: `NetworkRateLimit` - Error message: [`NetworkTxRateLimitExceeded`](../errors/subtensor.md#networktxratelimitexceeded) +
+Check current value on-chain + +To verify, open the [Polkadot.js app](https://polkadot.js.org/apps/?rpc=wss://entrypoint-finney.opentensor.ai:443#/chainstate) connected to Finney. Under **Developer → Chain state → Storage**, query `subtensorModule.networkRateLimit()`. See [Inspecting the Chain](../concepts/inspecting-the-chain) for more on using Polkadot.js. + +
+ ### Owner hyperparameter rate limit This rate limit controls how frequently subnet owners can modify hyperparameters. The limit is enforced independently per hyperparameter, so updating one parameter does not block updating a different one during the same window. diff --git a/docs/learn/emissions.md b/docs/learn/emissions.md index acc2b9e0b..5d6a2275a 100644 --- a/docs/learn/emissions.md +++ b/docs/learn/emissions.md @@ -91,7 +91,14 @@ The flow-based model uses an Exponential Moving Average (EMA) of net TAO flows ( 5. **Final TAO injection**: Multiply the share by total block emission to get actual TAO amount: $$\Delta\tau_i = \Delta\bar{\tau} \times \text{share}(i)$$ - This converts the proportions into actual TAO amounts. Currently, the total block emission $\Delta\bar{\tau}$ is 0.5 TAO per block. + This converts the proportions into actual TAO amounts. Currently, the total block emission $\Delta\bar{\tau}$ is 0.5 TAO per block (this will decrease with future [halvings](../concepts/halving)). + +
+ Check current value on-chain + + To verify the current block emission, open the [Polkadot.js app](https://polkadot.js.org/apps/?rpc=wss://entrypoint-finney.opentensor.ai:443#/chainstate) connected to Finney. Under **Developer → Chain state → Storage**, query `subtensorModule.blockEmission()`. See [Inspecting the Chain](../concepts/inspecting-the-chain). + +
With the default $p = 1$ ([source](https://github.com/opentensor/subtensor/blob/main/pallets/subtensor/src/lib.rs#L1293-L1295)), this creates **linear/proportional distribution**: a subnet with 2× the flow receives exactly 2× the emissions. The parameter can be adjusted to create winner-takes-more dynamics if desired (e.g., with $p = 1.5$, a subnet with 2× flow would get 2.83× emissions). diff --git a/docs/learn/fees.md b/docs/learn/fees.md index a549fbed6..f930e2f5d 100644 --- a/docs/learn/fees.md +++ b/docs/learn/fees.md @@ -267,7 +267,7 @@ These values are runtime constants (compiled into the WASM blob) and can only ch ```python import bittensor as bt -sub = bt.Subtensor(network="finney") +sub = bt.Subtensor(network="test") s = sub.substrate for name in [ @@ -316,7 +316,7 @@ The **SimSwap Runtime API** simulates a swap and returns the liquidity fee and e :::note Full cost for a stake operation -For `add_stake`, `remove_stake`, `move_stake`, or `swap_stake`: +For `add_stake`, `remove_stake`, `move_stake`, or `swap_stake`: 1. **Swap fee:** `sim_swap(origin_netuid, destination_netuid, amount)` → use `tao_fee` or `alpha_fee` as appropriate (and same-subnet moves have no swap fee). 2. **Transaction fee:** Compose the extrinsic, then `get_payment_info(call, keypair)` or `get_extrinsic_fee(call, keypair)` → `partial_fee`. @@ -389,7 +389,7 @@ This example estimates swap fee and output for add_stake (TAO → alpha for SN 1 ```python import bittensor as bt -sub = bt.Subtensor(network="finney") +sub = bt.Subtensor(network="test") netuid = 14 amount_stake = bt.Balance.from_tao(0.1) @@ -409,7 +409,7 @@ This example estimates swap fee and output for remove_stake (alpha for SN 14 → ```python import bittensor as bt -sub = bt.Subtensor(network="finney") +sub = bt.Subtensor(network="test") netuid = 14 amount_stake = bt.Balance.from_tao(0.1) @@ -431,7 +431,7 @@ This example estimates swap for a cross-subnet move (subnet 14 → subnet 5): ```python import bittensor as bt -sub = bt.Subtensor(network="finney") +sub = bt.Subtensor(network="test") amount_alpha = bt.Balance.from_tao(1).set_unit(5) @@ -506,7 +506,6 @@ import bittensor as bt sub = bt.Subtensor(network="test") wallet = bt.Wallet(name="my_wallet", hotkey="my_hotkey") -wallet.unlock_coldkey() netuid = 1 hotkey_ss58 = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" @@ -522,7 +521,7 @@ call = sub.compose_call( "amount_staked": amount.rao, }, ) -# Keypair is the account that pays the fee (coldkey) +# Fee estimation only needs the public key — no coldkey unlock required fee = sub.get_extrinsic_fee(call=call, keypair=wallet.coldkeypub) print(f"Estimated transaction fee: {fee}") ``` @@ -549,9 +548,8 @@ Using the SDK: ```python import bittensor as bt -sub = bt.Subtensor(network="finney") +sub = bt.Subtensor(network="test") wallet = bt.Wallet(name="my_wallet", hotkey="my_hotkey") -wallet.unlock_coldkey() hotkey_1 = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" hotkey_2 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" @@ -575,7 +573,7 @@ batch_call = sub.compose_call( call_params={"calls": [call_1, call_2]}, ) -# Estimate the transaction fee before sending +# Fee estimation only needs the public key — no coldkey unlock required fee = sub.get_extrinsic_fee(call=batch_call, keypair=wallet.coldkeypub) print(f"Estimated transaction fee: {fee}") ``` diff --git a/docs/learn/price-protection.md b/docs/learn/price-protection.md index 6ca966517..76935a6f1 100644 --- a/docs/learn/price-protection.md +++ b/docs/learn/price-protection.md @@ -102,387 +102,84 @@ btcli stake add --amount 1000 --netuid 1 --safe --tolerance 0.02 --partial btcli stake add --amount 300 --netuid 1 --unsafe ``` -## Managing Price Protection with SDK +## Managing Price Protection with the SDK -The Bittensor SDK provides price protection through method parameters: - -### Parameters + -:::warning -Unlike the `btcli`, the SDK's default behavior is _Unsafe_ mode. -You must explicitly configure price protection when using the SDK's staking/unstaking functionality. +:::warning SDK default is unsafe +Unlike `btcli`, the SDK does **not** enable price protection by default, it must be explicitly configured. ::: -**`safe_staking`** (bool): +When using the SDK with proxies (the recommended approach for mainnet), price protection is configured through the pallet-level calls `add_stake_limit` and `remove_stake_limit`. These accept two parameters that control protection behavior: -- **Default**: False -- **Purpose**: Enables price protection +- **`limit_price`** (int): The worst acceptable price in RAO per one alpha. For staking this is a ceiling (max you'll pay); for unstaking it's a floor (min you'll accept). +- **`allow_partial`** (bool): If `False` (strict mode), the transaction is rejected entirely when the price exceeds the limit. If `True` (partial mode), the maximum amount that stays within the limit is executed. -**`allow_partial_stake`** (bool): +To calculate `limit_price` from a tolerance percentage: -- **Default**: False -- **Purpose**: Enables partial execution mode +```python +pool = await subtensor.subnet(netuid=netuid) -**`rate_tolerance`** (float): +# For staking (price goes up as you buy alpha, so set a ceiling): +limit_price = bt.Balance.from_tao(pool.price.tao * (1 + rate_tolerance)).rao -- **Default**: 0.005 (0.5%) -- **Range**: 0.0 to 1.0 -- **Purpose**: Maximum allowed final price deviation from submission price +# For unstaking (price goes down as you sell alpha, so set a floor): +limit_price = bt.Balance.from_tao(pool.price.tao * (1 - rate_tolerance)).rao +``` ### SDK Examples - - -See [Price Protection Simulation](#price-protection-simulation) for an extended example. +See [Managing Your Stakes](../staking-and-delegation/managing-stake-sdk) for complete proxy staking workflows. -#### Safe Mode (reject if price moves beyond tolerance) +#### Strict Mode (reject if price moves beyond tolerance) ```python +import asyncio import bittensor as bt +from bittensor.core.extrinsics.pallets import SubtensorModule + +async def main(): + async with bt.AsyncSubtensor(network="test") as subtensor: + pool = await subtensor.subnet(netuid=1) + limit_price = bt.Balance.from_tao(pool.price.tao * 1.02).rao # 2% tolerance + + call = await SubtensorModule(subtensor).add_stake_limit( + netuid=1, + hotkey="5F...", + amount_staked=bt.Balance.from_tao(100).rao, + limit_price=limit_price, + allow_partial=False, # Reject entirely if price exceeds limit + ) + # Submit via proxy (see Managing Your Stakes for full proxy examples) -subtensor = bt.Subtensor() -wallet = bt.Wallet("my_wallet") - -response = subtensor.add_stake( - wallet=wallet, - hotkey_ss58="5F...", - netuid=1, - amount=bt.Balance.from_tao(100), - safe_staking=True, # Enable protection - rate_tolerance=0.02, # 2% price tolerance - allow_partial_stake=False # Reject if exceeds tolerance -) +asyncio.run(main()) ``` #### Partial Mode (execute what fits within tolerance) ```python -response = subtensor.add_stake( - wallet=wallet, - hotkey_ss58="5F...", +call = await SubtensorModule(subtensor).add_stake_limit( netuid=1, - amount=bt.Balance.from_tao(1000), - safe_staking=True, # Enable protection - rate_tolerance=0.02, # 2% price tolerance - allow_partial_stake=True # Execute partial amount within tolerance + hotkey="5F...", + amount_staked=bt.Balance.from_tao(1000).rao, + limit_price=limit_price, + allow_partial=True, # Execute maximum amount within price limit ) ``` -#### Unsafe Mode (ignore price protection) +#### Unsafe Mode (no price protection) + +For testnet or development use only. Uses `add_stake` instead of `add_stake_limit`: ```python -response = subtensor.add_stake( - wallet=wallet, - hotkey_ss58="5F...", +call = await SubtensorModule(subtensor).add_stake( netuid=1, - amount=bt.Balance.from_tao(100), - safe_staking=False # Disable protection; Unnecessary as this is the default setting + hotkey="5F...", + amount_staked=bt.Balance.from_tao(100).rao, ) ``` -## Price Protection Simulation - -The following script runs through several different stake and unstake operations with different price protection modes, to demonstrate the different behaviors contingent on price. - -Prerequisites: - -- [Run a Local Bittensor Blockchain Instance](../local-build/deploy) -- [Create a subnet on a local blockchain](../local-build/create-subnet) - -:::tip troubleshooting tip -If you see a `Custom error: 14` or a `SubtokenDisabled(Module)` error, you may need to start emissions on your subnet with the following command: - -```shell -btcli s start -``` - +:::tip High-level API (testnet convenience) +The SDK also provides `subtensor.add_stake(safe_staking=True)` and `subtensor.unstake(safe_unstaking=True)` which auto-compute the limit price. However, these methods sign directly from the wallet's coldkey and do not support proxies, so they are only suitable for testnet or development use. On mainnet, use the pallet-level calls shown above with `subtensor.proxy()`. ::: -```python -import bittensor as bt - -def display_balances_and_stakes(subtensor, wallet, target_hotkey, netuid, label): - """Display current balances and stakes for the simulation.""" - print(f"\n--- {label} ---") - balance = subtensor.get_balance(wallet.coldkey.ss58_address) - stakes = subtensor.get_stake_info_for_coldkey(wallet.coldkey.ss58_address) - - print(f"Coldkey balance: {balance}") - - # Find stake for our target hotkey and netuid - target_stake = None - for stake_info in stakes: - if stake_info.hotkey_ss58 == target_hotkey and stake_info.netuid == netuid: - target_stake = stake_info.stake - break - - if target_stake: - print(f"Stake on {target_hotkey[:8]}... (netuid {netuid}): {target_stake}") - else: - print(f"No stake found on {target_hotkey[:8]}... (netuid {netuid})") - -def show_current_price_and_protection(subtensor, netuid, tolerance, label): - """Show current subnet price and calculate protection thresholds.""" - print(f"\n{label} Price Analysis:") - subnet_info = subtensor.subnet(netuid=netuid) - current_price = subnet_info.price - print(f"Current price: {current_price}") - - # Calculate protection thresholds - price_floor = current_price.tao * (1 - tolerance) - price_ceiling = current_price.tao * (1 + tolerance) - - print(f"Price protection with {tolerance:.2%} tolerance:") - print(f" • Price floor (unstaking): {price_floor:.6f} TAO/α") - print(f" • Price ceiling (staking): {price_ceiling:.6f} TAO/α") - print(f" • Protection range: {price_floor:.6f} - {price_ceiling:.6f} TAO/α") - - return subnet_info - -def demonstrate_protection_modes(): - """Comprehensive demonstration of all three price protection modes.""" - - print("=== Bittensor Price Protection Mode Simulation ===\n") - - # Connect to local network - subtensor = bt.Subtensor("ws://127.0.0.1:9945") - - # Get subnet information - netuid = 2 - subnet_info = subtensor.subnet(netuid=netuid) - if subnet_info is None: - print(f"Error: Could not connect to subnet {netuid}. Is the local node running?") - return False - - print(f"Connected to subnet {netuid}") - print(f"Alpha in reserve: {subnet_info.alpha_in}") - print(f"TAO in reserve: {subnet_info.tao_in}") - - # Initialize wallet - wallet = bt.Wallet(name="Alice") - - try: - wallet.unlock_coldkey() - except Exception as e: - print(f"Error: Could not unlock wallet. Make sure 'Alice' wallet exists and is unlocked. {e}") - return False - - # Get registered hotkeys for the subnet - metagraph = subtensor.metagraph(netuid=netuid) - registered_hotkeys = metagraph.hotkeys - - if not registered_hotkeys: - print(f"Error: No registered hotkeys found on subnet {netuid}.") - return False - - target_hotkey = registered_hotkeys[0] - print(f"Using registered hotkey: {target_hotkey[:8]}...") - - # Display initial state - display_balances_and_stakes(subtensor, wallet, target_hotkey, netuid, "Initial State") - - print("\n" + "="*60) - print("SIMULATION: Testing price protection modes") - print("="*60) - - # Test amounts - stake_amount = 5.0 # TAO - - # Mode 1: UNSAFE MODE (No Protection) - print(f"\n{'='*20} MODE 1: UNSAFE (No Protection) {'='*20}") - print("Executes transaction regardless of price movements") - - subnet_info = show_current_price_and_protection(subtensor, netuid, 0.0, "Pre-Unsafe") - - try: - print(f"\nStaking {stake_amount} TAO with NO protection...") - response = subtensor.add_stake( - wallet=wallet, - hotkey_ss58=target_hotkey, - netuid=netuid, - amount=bt.Balance.from_tao(stake_amount), - safe_staking=False # No protection - ) - - print(response) - - except Exception as e: - print(f"❌ Unsafe staking failed: {e}") - - # Show price after unsafe transaction - show_current_price_and_protection(subtensor, netuid, 0.0, "Post-Unsafe") - - display_balances_and_stakes(subtensor, wallet, target_hotkey, netuid, "After Unsafe Staking") - - # Mode 2: SAFE MODE with VERY strict tolerance (should fail) - print(f"\n{'='*20} MODE 2: SAFE with STRICT Tolerance {'='*20}") - print("Rejects transaction if price moves beyond tolerance") - - strict_tolerance = 0.001 # 0.1% tolerance - very strict - large_stake_amount = 20.0 # Larger amount to trigger protection - - subnet_info = show_current_price_and_protection(subtensor, netuid, strict_tolerance, "Pre-Safe-Strict") - pre_safe_price = subnet_info.price.tao - price_ceiling = pre_safe_price * (1 + strict_tolerance) - - try: - print(f"\nStaking {large_stake_amount} TAO with SAFE protection (tolerance: {strict_tolerance:.2%})...") - print(f"Transaction should FAIL if final price > {price_ceiling:.6f} TAO/α") - - response = subtensor.add_stake( - wallet=wallet, - hotkey_ss58=target_hotkey, - netuid=netuid, - amount=bt.Balance.from_tao(large_stake_amount), - safe_staking=True, - rate_tolerance=strict_tolerance, - allow_partial_stake=False - ) - - print(response) - - except Exception as e: - if "Price exceeded tolerance limit" in str(e) or "exceeded tolerance" in str(e) or "tolerance" in str(e).lower(): - print("🛡️ EXPECTED: Transaction rejected - price protection activated!") - else: - print(f"❌ Safe staking failed with unexpected error: {e}") - - # Show price after safe transaction - show_current_price_and_protection(subtensor, netuid, strict_tolerance, "Post-Safe-Strict") - - display_balances_and_stakes(subtensor, wallet, target_hotkey, netuid, "After Strict Safe Staking") - - # Mode 3: SAFE MODE with reasonable tolerance (should succeed) - print(f"\n{'='*20} MODE 3: SAFE with Reasonable Tolerance {'='*20}") - print("Demonstrating normal safe staking that succeeds") - - reasonable_tolerance = 0.05 # 5% tolerance - normal_amount = 5.0 # Normal amount - - subnet_info = show_current_price_and_protection(subtensor, netuid, reasonable_tolerance, "Pre-Safe-Normal") - - try: - print(f"\nStaking {normal_amount} TAO with SAFE protection (tolerance: {reasonable_tolerance:.2%})...") - - response = subtensor.add_stake( - wallet=wallet, - hotkey_ss58=target_hotkey, - netuid=netuid, - amount=bt.Balance.from_tao(normal_amount), - safe_staking=True, - rate_tolerance=reasonable_tolerance, - allow_partial_stake=False - ) - - print(response) - - except Exception as e: - print(f"❌ Safe staking failed: {e}") - - display_balances_and_stakes(subtensor, wallet, target_hotkey, netuid, "After Normal Safe Staking") - - # Mode 4: PARTIAL MODE with strict tolerance (should execute partially) - print(f"\n{'='*20} MODE 4: PARTIAL with STRICT Tolerance {'='*20}") - print("Should execute maximum amount within tolerance") - - partial_strict_tolerance = 0.002 # 0.2% tolerance - very strict for partial - very_large_amount = 50.0 # Very large amount to force partial execution - - subnet_info = show_current_price_and_protection(subtensor, netuid, partial_strict_tolerance, "Pre-Partial-Strict") - - print(f"\nUsing very strict tolerance ({partial_strict_tolerance:.2%}) with large amount ({very_large_amount} TAO)") - print(f"Should execute PARTIAL amount to stay within tolerance") - - # Record balance before to see actual amount executed - balance_before = subtensor.get_balance(wallet.coldkey.ss58_address) - - try: - print(f"\nStaking {very_large_amount} TAO with PARTIAL protection (tolerance: {partial_strict_tolerance:.2%})...") - response = subtensor.add_stake( - wallet=wallet, - hotkey_ss58=target_hotkey, - netuid=netuid, - amount=bt.Balance.from_tao(very_large_amount), - safe_staking=True, - rate_tolerance=partial_strict_tolerance, - allow_partial_stake=True # Allow partial execution - ) - - # Check actual amount executed - balance_after = subtensor.get_balance(wallet.coldkey.ss58_address) - actual_amount_executed = balance_before.tao - balance_after.tao - - print(response) - - if response.success: - print(f"Amount requested: {very_large_amount} TAO") - print(f"Amount actually executed: {actual_amount_executed:.3f} TAO") - execution_percentage = (actual_amount_executed / very_large_amount) * 100 - print(f"Execution percentage: {execution_percentage:.1f}%") - - if actual_amount_executed < very_large_amount * 0.95: # Less than 95% executed - print(f"🎯 SUCCESS: PARTIAL execution detected! Only {execution_percentage:.1f}% executed due to price protection") - else: - print(f"🤔 Unexpected: Near-full execution despite strict tolerance") - - except Exception as e: - print(f"❌ Partial staking failed: {e}") - - # Show price after partial to see impact - show_current_price_and_protection(subtensor, netuid, partial_strict_tolerance, "Post-Partial-Strict") - - display_balances_and_stakes(subtensor, wallet, target_hotkey, netuid, "After Partial Staking") - - # Demonstrate unstaking with protection - print(f"\n{'='*20} UNSTAKING WITH PROTECTION {'='*20}") - print("Demonstrating unstaking with price protection") - - # Find current stake to unstake from - stakes = subtensor.get_stake_info_for_coldkey(wallet.coldkey.ss58_address) - current_stake = None - for stake_info in stakes: - if stake_info.hotkey_ss58 == target_hotkey and stake_info.netuid == netuid: - current_stake = stake_info.stake - break - - if current_stake and current_stake.rao > 0: - unstake_tolerance = 0.05 # 5% tolerance for unstaking - subnet_info = show_current_price_and_protection(subtensor, netuid, unstake_tolerance, "Pre-Unstake") - - # Unstake a portion with protection - unstake_amount_rao = min(current_stake.rao // 4, int(50 * 1e9)) - unstake_balance = bt.Balance.from_rao(unstake_amount_rao).set_unit(netuid=netuid) - - print(f"Current stake: {current_stake}") - print(f"Attempting to unstake: {unstake_balance}") - - try: - print(f"\nUnstaking with SAFE protection (tolerance: {unstake_tolerance:.2%})...") - response = subtensor.unstake( - wallet=wallet, - hotkey_ss58=target_hotkey, - netuid=netuid, - amount=unstake_balance, - safe_unstaking=True, - rate_tolerance=unstake_tolerance, - allow_partial_stake=False - ) - - print(response) - - except Exception as e: - if "Price exceeded tolerance limit" in str(e) or "exceeded tolerance" in str(e): - print("🛡️ Unstaking rejected - price moved beyond tolerance") - else: - print(f"❌ Protected unstaking failed: {e}") - else: - print("No stake available to unstake") - - display_balances_and_stakes(subtensor, wallet, target_hotkey, netuid, "Final State") - show_current_price_and_protection(subtensor, netuid, 0.0, "Final") - - return True - -if __name__ == "__main__": - demonstrate_protection_modes() -``` diff --git a/docs/miners/autostaking.md b/docs/miners/autostaking.md index cabfd2a79..7a6e1bc28 100644 --- a/docs/miners/autostaking.md +++ b/docs/miners/autostaking.md @@ -107,9 +107,15 @@ netuid 2: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY Sets the destination hotkey for your coldkey on a specific subnet. +:::warning Use a proxy coldkey for this operation on mainnet +`set_coldkey_auto_stake_hotkey` has no scoped proxy type, but a `NonTransfer` proxy permits it. Your primary coldkey should remain in cold storage. See [Coldkey and Hotkey Workstation Security](../keys/coldkey-hotkey-security) and [Working with Proxies](../keys/proxies/working-with-proxies). +::: + +On mainnet, run with `--wallet.name PROXY_WALLET --proxy REAL_COLDKEY_SS58` (using a `NonTransfer` proxy): + ```bash btcli stake set-auto --wallet.name --netuid ``` @@ -154,25 +160,29 @@ Set this auto-stake destination? [y/n] (y): y ```python -import asyncio +import asyncio, os import bittensor as bt +from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.extrinsics.pallets import SubtensorModule async def main(): - async with bt.async_subtensor(network="local") as subtensor: - wallet = bt.Wallet( - name="Alice", - ) - wallet.unlock_coldkey() + proxy_wallet = bt.Wallet(name=os.environ['BT_PROXY_WALLET_NAME']) + real_account_ss58 = os.environ['BT_REAL_ACCOUNT_SS58'] - netuid = 2 # subnet to configure - hotkey_ss58 = "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" # validator hotkey to auto-stake to + netuid = 2 # subnet to configure + hotkey_ss58 = "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" # validator hotkey to auto-stake to - response = await subtensor.set_auto_stake( - wallet=wallet, + async with bt.AsyncSubtensor(network="test") as subtensor: + set_auto_call = SubtensorModule(subtensor).set_coldkey_auto_stake_hotkey( netuid=netuid, - hotkey_ss58=hotkey_ss58, - wait_for_inclusion=True, - wait_for_finalization=False, + hotkey=hotkey_ss58, + ) + # NonTransfer is the narrowest proxy type that permits set_coldkey_auto_stake_hotkey + response = await subtensor.proxy( + wallet=proxy_wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=ProxyType.NonTransfer, + call=set_auto_call, ) print(response) diff --git a/docs/miners/index.md b/docs/miners/index.md index 8e5185352..145fee3c7 100644 --- a/docs/miners/index.md +++ b/docs/miners/index.md @@ -57,23 +57,15 @@ btcli subnet register --netuid 1 --wallet.name test-coldkey --wallet.hotkey test ## Miner deregistration -A miner can be deregistered if it earns low emissions due to receiving low weights (ratings) from validators. Typical subnets have 256 UID slots per subnet, of which a maximum of 64 subnet can be occupied by validators. Each tempo, the lowest ranked slot is deregistered from the hotkey that holds it and assigned to a new registrant. +A miner can be deregistered if it earns low emissions due to receiving low weights (ratings) from validators. Typical subnets have 256 UID slots per subnet, of which a maximum of 64 can be occupied by validators. This leaves 192 UIDs for miners, though if there are fewer than 64 eligible validators on a subnet, miners can occupy available slots. -- Every subnet has an `immunity_period` hyperparameter expressed in a number of blocks. - :::tip See - See [`immunity_period`](../subnets/subnet-hyperparameters.md#immunityperiod). - ::: -- A subnet miner or validator at a UID (in that subnet) has a defined number of blocks to improve its performance. This is known as `immunity_period`. When the `immunity_period` expires, that miner or validator can be deregistered if it has the lowest performance in the subnet and a new registration arrives. -- A neuron's `immunity_period` starts when the miner or validator is registered into the subnet. - Validators as well as miners can be deregistered if their emissions are low; either role requires a UID. +If all UID slots are occupied, a new registration will cause the lowest ranked slot deregistered from the hotkey that holds it and assigned to a new registrant. -Typically, subnets have 256 UID slots, with a maximum of 64 slots capable of serving as validators by default. This leaves 192 UIDs for miners, though if there are fewer than 64 eligible validators on a subnet, miners can occupy available slots. +Every subnet has an immunity period, during which newly registered miners cannot be deregistered. See [`immunity_period`](../subnets/subnet-hyperparameters.md#immunityperiod). When the immunity period expires, that miner or validator can be deregistered if it has the lowest performance in the subnet and a new registration arrives. -:::info -Deregistration only occurs on subnets where all 256 UID slots are occupied. If a new registration occurs in a subnet with available UID slots, the registered neuron occupies one of the available UID slots. -::: +If a new registration occurs in a subnet with available UID slots, the registered neuron occupies one of the available UID slots. -Each tempo, the '[neuron](../learn/neurons)' (miner _or_ validator node) with the lowest 'pruning score' (based solely on emissions), and that is no longer within its [immunity period](../subnets/subnet-hyperparameters.md#immunityperiod), risks being replaced by a newly registered neuron, who takes over that UID. +that UID. :::info Deregistration is based on emissions The subnet does not distinguish between miners and validators for the purpose of deregistration. The chain only looks at emissions (represented as 'pruning score'). Whenever a new registration occurs in the subnet, the neuron with the lowest emissions will get deregistered. @@ -81,11 +73,6 @@ The subnet does not distinguish between miners and validators for the purpose of ### Immunity period -Every subnet has an `immunity_period` hyperparameter expressed in a number of blocks. A neuron's `immunity_period` starts when the miner or validator registers into the subnet. For more information, see [`immunity_period`](../subnets/subnet-hyperparameters.md#immunityperiod). - -A subnet neuron (miner or validator) at a UID (in that subnet) has `immunity_period` blocks to improve its performance. When `immunity_period` expires, that miner or validator can be deregistered if it has the lowest performance in the subnet and a new registration arrives. - -**Implementation Details:** Immunity status is calculated dynamically using the formula `is_immune = (current_block - registered_at) < immunity_period`, where: @@ -93,6 +80,13 @@ Immunity status is calculated dynamically using the formula `is_immune = (curren - `registered_at` is the block number when the neuron was registered - `immunity_period` is the configured protection period for the subnet (default: 4096 blocks ≈ 13.7 hours) +
+Check current value on-chain + +Immunity period is per-subnet. To check, open the [Polkadot.js app](https://polkadot.js.org/apps/?rpc=wss://entrypoint-finney.opentensor.ai:443#/chainstate) connected to Finney. Under **Developer → Chain state → Storage**, query `subtensorModule.immunityPeriod(netuid)`. See [Inspecting the Chain](../concepts/inspecting-the-chain). + +
+ **Code References:** - [`subtensor/pallets/subtensor/src/utils/misc.rs:442-448`](https://github.com/opentensor/subtensor/blob/main/pallets/subtensor/src/utils/misc.rs#L442-448) - Immunity status calculation @@ -105,7 +99,7 @@ Immunity status is calculated dynamically using the formula `is_immune = (curren - In cases where two or more nodes have the lowest "pruning score", the older node gets deregistered first. - The subnet owner's hotkey has permanent immunity from deregistration. - ::: +::: ### Registration flow diagram diff --git a/docs/miners/miners-btcli-guide.md b/docs/miners/miners-btcli-guide.md index ffb8371fd..4b0fe3e30 100644 --- a/docs/miners/miners-btcli-guide.md +++ b/docs/miners/miners-btcli-guide.md @@ -24,7 +24,7 @@ Miners must also manage their own TAO and alpha stake (to exit the emissions tha See: - [Staking/Delegation Overview](../staking-and-delegation/delegation) -- [Staker's Guide to `BTCLI`](../staking-and-delegation/stakers-btcli-guide) +- [Managing Your Stakes](../staking-and-delegation/managing-stake-sdk) Creating hotkeys requires a coldkey private key, and should be done on a secure [coldkey workstation](../keys/coldkey-hotkey-security#coldkey-workstation). However, using hotkeys for signing requests when mining does not require a coldkey, which should never be present on a mining server, i.e. a hotkey workstation. The coldkey should not be placed on a machine used for mining because the software dependencies for mining should not be considered safe/trusted code to the standards of a coldkey workstation. diff --git a/docs/resources/glossary.md b/docs/resources/glossary.md index ac44c43f4..17f8201d7 100644 --- a/docs/resources/glossary.md +++ b/docs/resources/glossary.md @@ -160,19 +160,19 @@ Network Security Properties: A subnet validator that receives staked TAO tokens from delegators and performs validation tasks in one or more subnets. -**See also:** [Delegation](../staking-and-delegation/delegation.md), [Managing Stake with btcli](../staking-and-delegation/managing-stake-btcli.md) +**See also:** [Delegation](../staking-and-delegation/delegation.md), [Managing Your Stakes](../staking-and-delegation/managing-stake-sdk.md) ### Delegate Stake The amount of TAO staked by the delegate themselves. -**See also:** [Managing Stake with btcli](../staking-and-delegation/managing-stake-btcli.md), [Managing Stake with SDK](../staking-and-delegation/managing-stake-sdk.md) +**See also:** [Managing Your Stakes](../staking-and-delegation/managing-stake-sdk.md), [Managing Stake with SDK](../staking-and-delegation/managing-stake-sdk.md) ### Delegation Also known as staking, delegating TAO to a validator (who is thereby the delegate), increases the validator's stake and secure a validator permit. -**See also:** [Delegation](../staking-and-delegation/delegation.md), [Managing Stake with btcli](../staking-and-delegation/managing-stake-btcli.md) +**See also:** [Delegation](../staking-and-delegation/delegation.md), [Managing Your Stakes](../staking-and-delegation/managing-stake-sdk.md) ### Dendrite @@ -217,7 +217,7 @@ A cryptographic algorithm used to generate public and private key pairs for cold The total staked TAO amount of a delegate, including their own TAO tokens and those delegated by nominators. -**See also:** [Managing Stake with btcli](../staking-and-delegation/managing-stake-btcli.md), [Managing Stake with SDK](../staking-and-delegation/managing-stake-sdk.md) +**See also:** [Managing Your Stakes](../staking-and-delegation/managing-stake-sdk.md), [Managing Stake with SDK](../staking-and-delegation/managing-stake-sdk.md) ### Emission @@ -256,7 +256,7 @@ from bittensor.core.async_subtensor import AsyncSubtensor from bittensor.utils.balance import Balance async def main(): - async with AsyncSubtensor(network="finney") as subtensor: + async with AsyncSubtensor(network="test") as subtensor: deposit = await subtensor.get_existential_deposit() print(f"Existential deposit: {deposit.tao} TAO") asyncio.run(main()) @@ -658,6 +658,16 @@ Subnet Zero a.k.a. the root subnet is a special subnet. No miners can register o ## S +### Seed phrase + +The **_seed phrase_** (a.k.a. 'menemonic' or 'recovery phrase') is a series of (at least 12) words that is generated together with your wallet's cryptographic key pair, and which can be used to recover the coldkey private key. This seed phrase is therefore a human-usable way to save access to the cryptographic wallet offline, and to import the cryptographic wallet into a hardware or software wallet. + +The most important operational goal when handling Bittensor wallets is to avoid losing or leaking your seed phrase. + +See: +- [Handle your Seed Phrase/Mnemonic Securely](../keys/handle-seed-phrase) +- [Wallets, Coldkeys and Hotkeys in Bittensor](../keys/wallets) + ### SS58 Encoded A compact representation of public keys corresponding to the wallet's coldkey and hotkey, used as wallet addresses for secure TAO transfers. @@ -686,7 +696,7 @@ The amount of currency tokens delegated to a validator UID in a subnet. Includes Stake determines a validator's weight in consensus as well as their emissions. -**See also:** [Managing Stake with btcli](../staking-and-delegation/managing-stake-btcli.md), [Managing Stake with SDK](../staking-and-delegation/managing-stake-sdk.md), [Delegation](../staking-and-delegation/delegation.md) +**See also:** [Managing Your Stakes](../staking-and-delegation/managing-stake-sdk.md), [Managing Stake with SDK](../staking-and-delegation/managing-stake-sdk.md), [Delegation](../staking-and-delegation/delegation.md) ### Stake Weight @@ -732,7 +742,7 @@ The process of attaching TAO to a validator hotkey, i.e., locking TAO to a subne **See also:** -- [Managing Stake with btcli](../staking-and-delegation/managing-stake-btcli.md) +- [Managing Your Stakes](../staking-and-delegation/managing-stake-sdk.md) - [Managing Stake with SDK](../staking-and-delegation/managing-stake-sdk.md) - [Delegation](../staking-and-delegation/delegation.md) - [Browse validators on TAO.app](https://www.tao.app/validators) @@ -910,8 +920,7 @@ Unstaking incurs blockchain transaction fees, which are recycled back into the T **See also:** - [Staking/Delegation overview](../staking-and-delegation/delegation.md#unstaking) -- [Managing Stake with btcli](../staking-and-delegation/managing-stake-btcli.md#unstaking-with-btcli) -- [Managing Stake with SDK](../staking-and-delegation/managing-stake-sdk.md#unstaking-from-a-validator) +- [Managing Your Stakes](../staking-and-delegation/managing-stake-sdk.md#unstake-from-a-validator) - [Understanding Pricing and Anticipating Slippage](../learn/slippage.md) - [Price Protection When Staking](../learn/price-protection.md) - [Transaction Fees](../learn/fees.md) diff --git a/docs/sdk/call.md b/docs/sdk/call.md index e7d447b83..af2b10825 100644 --- a/docs/sdk/call.md +++ b/docs/sdk/call.md @@ -74,7 +74,7 @@ The SDK provides `subtensor.compose_call()` with enhanced functionality: ```python import bittensor as bt -subtensor = bt.Subtensor(network="finney") +subtensor = bt.Subtensor(network="test") # Creating a call with automatic validation call = subtensor.compose_call( @@ -96,7 +96,7 @@ call = subtensor.compose_call( ```python import bittensor as bt -subtensor = bt.Subtensor(network="finney") +subtensor = bt.Subtensor(network="test") # Example 1: Creating a call to add stake stake_call = subtensor.compose_call( @@ -169,7 +169,7 @@ from bittensor.core.extrinsics.pallets import ( ```python import bittensor as bt -subtensor = bt.Subtensor(network="finney") +subtensor = bt.Subtensor(network="test") # Create GenericCall for Proxy.add_proxy function from bittensor.core.extrinsics.pallets import Proxy @@ -188,7 +188,7 @@ call = Proxy(subtensor).add_proxy( ```python import bittensor as bt -async_subtensor = bt.AsyncSubtensor(network="finney") +async_subtensor = bt.AsyncSubtensor(network="test") # Create GenericCall (need await for async) from bittensor.core.extrinsics.pallets import Proxy @@ -207,7 +207,7 @@ call = await Proxy(async_subtensor).add_proxy( ```python import bittensor as bt -subtensor = bt.Subtensor(network="finney") +subtensor = bt.Subtensor(network="test") # Even if the method is not explicitly defined in SubtensorModule class, # CallBuilder will automatically create GenericCall for this function @@ -235,7 +235,7 @@ Proxy functionality frequently requires passing `call` as an argument. Here are ```python import bittensor as bt -subtensor = bt.Subtensor(network="finney") +subtensor = bt.Subtensor(network="test") wallet = bt.Wallet(name="alice") # Create a call to add stake @@ -261,7 +261,7 @@ response = subtensor.proxy( ```python import bittensor as bt -subtensor = bt.Subtensor(network="finney") +subtensor = bt.Subtensor(network="test") wallet = bt.Wallet(name="alice") # Create a call to remove stake @@ -287,7 +287,7 @@ response = subtensor.proxy( ```python import bittensor as bt -subtensor = bt.Subtensor(network="finney") +subtensor = bt.Subtensor(network="test") wallet = bt.Wallet(name="alice") # Create a call to transfer stake between subnets @@ -315,7 +315,7 @@ response = subtensor.proxy( ```python import bittensor as bt -subtensor = bt.Subtensor(network="finney") +subtensor = bt.Subtensor(network="test") wallet = bt.Wallet(name="alice") # Create a call to move stake @@ -343,7 +343,7 @@ response = subtensor.proxy( ```python import bittensor as bt -subtensor = bt.Subtensor(network="finney") +subtensor = bt.Subtensor(network="test") wallet = bt.Wallet(name="alice") # Create a call to set mechanism weights @@ -371,7 +371,7 @@ response = subtensor.proxy( ```python import bittensor as bt -subtensor = bt.Subtensor(network="finney") +subtensor = bt.Subtensor(network="test") wallet = bt.Wallet(name="alice") # Create a call to register network @@ -395,7 +395,7 @@ response = subtensor.proxy( ```python import bittensor as bt -subtensor = bt.Subtensor(network="finney") +subtensor = bt.Subtensor(network="test") wallet = bt.Wallet(name="alice") # Create a call to set subnet identity @@ -427,7 +427,7 @@ response = subtensor.proxy( ```python import bittensor as bt -subtensor = bt.Subtensor(network="finney") +subtensor = bt.Subtensor(network="test") wallet = bt.Wallet(name="alice") # Create a call that was previously announced @@ -454,7 +454,7 @@ response = subtensor.proxy_announced( ```python import bittensor as bt -subtensor = bt.Subtensor(network="finney") +subtensor = bt.Subtensor(network="test") wallet = bt.Wallet(name="alice") # Create a call to execute upon successful crowdloan completion @@ -480,7 +480,7 @@ response = subtensor.create_crowdloan( ```python import bittensor as bt -subtensor = bt.Subtensor(network="finney") +subtensor = bt.Subtensor(network="test") wallet = bt.Wallet(name="alice") # Create a call to encrypt @@ -507,7 +507,7 @@ response = subtensor.mev_submit_encrypted( ```python import bittensor as bt -subtensor = bt.Subtensor(network="finney") +subtensor = bt.Subtensor(network="test") wallet = bt.Wallet(name="alice") # Create a call @@ -546,7 +546,7 @@ import bittensor as bt from bittensor.core.extrinsics.staking import add_stake_extrinsic from bittensor.core.extrinsics.proxy import proxy_extrinsic -subtensor = bt.Subtensor(network="finney") +subtensor = bt.Subtensor(network="test") wallet = bt.Wallet(name="alice") # Example 1: Direct extrinsic call to add stake diff --git a/docs/sdk/index.md b/docs/sdk/index.md index 2fb858c33..897768029 100644 --- a/docs/sdk/index.md +++ b/docs/sdk/index.md @@ -44,11 +44,11 @@ Use proxies to delegate account permissions securely while keeping your coldkey - [Create and Manage Proxies](../keys/proxies/working-with-proxies.md): Set up standard proxy relationships - [Pure Proxies](../keys/proxies/pure-proxies.md): Create and use keyless pure proxy accounts -- [Staking with a Proxy](../keys/proxies/staking-with-proxy.md): Perform staking operations through a proxy +- [Managing Your Stakes](../staking-and-delegation/managing-stake-sdk.md): Staking operations through a proxy **By proxy type:** -- **Proxy staking**: See [Staking with a Proxy](../keys/proxies/staking-with-proxy.md) +- **Proxy staking**: See [Managing Your Stakes](../staking-and-delegation/managing-stake-sdk.md) - **Other proxy operations**: Execute any permitted call through a proxy (see [Working with Proxies](../keys/proxies/working-with-proxies.md)) ## Advanced: Working with Blockchain Calls diff --git a/docs/staking-and-delegation/delegation.md b/docs/staking-and-delegation/delegation.md index 94d02c795..bf120704c 100644 --- a/docs/staking-and-delegation/delegation.md +++ b/docs/staking-and-delegation/delegation.md @@ -16,9 +16,7 @@ Staking and unstaking operations incur transaction fees. See [Transaction Fees i See also: - [Browse validators on TAO.app](https://www.tao.app/validators), with on-chain identities, stake distributions, validator take percentages, etc. -- [Managing Stake with 'btcli'](./managing-stake-btcli.md) -- [Managing Stake with the Python SDK](./managing-stake-sdk.md) -- [Staking with a Proxy](../keys/proxies/staking-with-proxy): Keep your coldkey secure while managing staking operations +- [Managing Your Stakes](./managing-stake-sdk.md): Complete guide to staking operations with btcli and the Python SDK :::tip tips Validators/delegates can configure their take. The default value is 18%. See [`btcli sudo set-take`](../btcli#btcli-sudo-set-take). diff --git a/docs/staking-and-delegation/managing-stake-btcli.md b/docs/staking-and-delegation/managing-stake-btcli.md index 524b3f88c..b908d37ab 100644 --- a/docs/staking-and-delegation/managing-stake-btcli.md +++ b/docs/staking-and-delegation/managing-stake-btcli.md @@ -2,6 +2,8 @@ title: "Managing Stake with BTCLI" --- +import { ProxyColdkeyWarning } from "../keys/_proxy-warning.mdx"; + # Managing stake with `btcli` This page demonstrates usage of `btcli`, the Bittensor CLI, for managing stake. @@ -14,6 +16,8 @@ Likewise, TAO holders can **unstake** to withdraw their delegated tokens from va Staking and unstaking operations incur transaction fees for the underlying blockchain transactions they trigger. See [Transaction Fees in Bittensor](../learn/fees.md) for details. ::: + + See also: - [Staking/delegation overview](./delegation) @@ -22,13 +26,7 @@ See also: - [Staking with a proxy](../keys/proxies/staking-with-proxy) :::tip -Minimum transaction amount for stake/unstake/move/transfer: 500,000 RAO or 0.0005 TAO. -::: - -:::warning Keep your coldkey secure -Staking is a regular operation for most TAO holders. Every time you stake or unstake directly, you must decrypt and use your coldkey—exposing it to potential compromise. - -**For better security, use a [Staking Proxy](../keys/proxies/staking-with-proxy)**. With a `Staking` proxy configured with a delay, you can manage your stake without ever exposing your coldkey. If the proxy is compromised, the delay gives you time to reject unauthorized unstaking attempts. +Minimum transaction amount for stake/unstake/move/transfer: 500,000 RAO or 0.0005 TAO. To verify, query `subtensorModule.minStake()` on the [Polkadot.js app](https://polkadot.js.org/apps/?rpc=wss://entrypoint-finney.opentensor.ai:443#/chainstate). See [Inspecting the Chain](../concepts/inspecting-the-chain). ::: ## Pre-requisite: Create a wallet @@ -135,10 +133,10 @@ Using the specified network test from config ## Stake to a validator -Use `btcli stake add` to stake to a validator on a subnet. You'll be prompted to choose a subnet and validator, as well as specify an amount of TAO to stake into the validator's hotkey as alpha. +Use `btcli stake add` to stake to a validator on a subnet. Specify your proxy wallet with `--wallet.name` and the real account SS58 with `--proxy`. You'll be prompted to choose a subnet and validator, as well as specify an amount of TAO to stake. ```shell - btcli stake add +btcli stake add --wallet.name PROXY_WALLET --proxy REAL_COLDKEY_SS58 ``` ```console @@ -208,16 +206,16 @@ If you confirm, the staking operation will execute. ### Staking to multiple validators -You can add stake to multiple validators at once by running the following command: +You can add stake to multiple validators at once by including a comma-separated list of subnet netuids: ```shell -btcli stake add -n 4,14,70 +btcli stake add -n 4,14,70 --wallet.name PROXY_WALLET --proxy REAL_COLDKEY_SS58 ``` -The command accepts a comma-separated list of the subnets you wish to stake into. If you want to stake the same amount of TAO into all subnets, you can include the `--amount` flag as shown: +If you want to stake the same amount of TAO into all subnets, include the `--amount` flag: ```shell -btcli stake add -n 4,14,70 --amount 100 +btcli stake add -n 4,14,70 --amount 100 --wallet.name PROXY_WALLET --proxy REAL_COLDKEY_SS58 ``` ## View your current stakes @@ -257,10 +255,10 @@ Unstaking is the process of withdrawing your staked TAO from validators, convert ### Remove stake from a validator -Use `btcli stake remove` to unstake from a specific validator. You'll be prompted to select the subnet and validator, then specify the amount to unstake. +Use `btcli stake remove` to unstake from a specific validator. Specify your proxy wallet with `--wallet.name` and the real account SS58 with `--proxy`. You'll be prompted to select the subnet and validator, then specify the amount to unstake. ```shell -btcli stake remove +btcli stake remove --wallet.name PROXY_WALLET --proxy REAL_COLDKEY_SS58 ``` You'll see a confirmation screen showing: @@ -340,10 +338,10 @@ Do you want to proceed with unstaking everything? [y/n]: y ### Unstake all from a validator -To unstake all your stake from a specific validator, or from all validators use the `--all` flag: +To unstake all your stake from a specific validator, or from all validators, use the `--all` flag: ```shell -btcli stake remove --all +btcli stake remove --all --wallet.name PROXY_WALLET --proxy REAL_COLDKEY_SS58 ``` Either specify the hotkey, to remove all stake on all subnets, or `all` for all stake on all subnets for all validator hotkeys. @@ -376,7 +374,7 @@ Do you want to proceed with unstaking everything? [y/n]: ## Transferring stake -The `btcli stake transfer` command is used to transfer ownership of stake from one wallet (coldkey) to another. +The `btcli stake transfer` command transfers ownership of stake from one wallet (coldkey) to another. This operation uses the `transfer_stake` extrinsic, which requires a `Transfer` (or `SmallTransfer` for amounts under 0.5 TAO) proxy — not a `Staking` proxy. Pass your proxy wallet with `--wallet.name` and the real account SS58 with `--proxy`. :::tip Don't confuse this with `btcli stake move`, which does not transfer ownership to another wallet/coldkey, but moves stake between validators or subnets, effectively unstaking and restaking it in a single operation. @@ -389,7 +387,7 @@ This operation effectively comprises a series of operations, which occur as an a - the recipient then automatically stakes the newly received TAO into the subnet, receiving the alpha tokens in return ``` -btcli stake transfer +btcli stake transfer --wallet.name PROXY_WALLET --proxy REAL_COLDKEY_SS58 This command transfers stake from one coldkey to another while keeping the same hotkey. @@ -463,8 +461,12 @@ Wallet: ## Moving stake -The `btcli stake move` command is used to moves stake between validators or subnets, effectively unstaking and restaking it in a single operation. It does not change ownership of the stake, which remains with the same wallet/coldkey. +The `btcli stake move` command moves stake between validators or subnets in a single operation. It does not change ownership; stake remains with the same coldkey. This uses the `move_stake` extrinsic, which is permitted by the `Staking` proxy type. :::tip -Don't confuse this with `btcli stake transfer`, which is used to transfer ownership of stake from one wallet (coldkey) to another. +Don't confuse this with `btcli stake transfer`, which transfers ownership of stake from one coldkey to another (requires a `Transfer` proxy). ::: + +```shell +btcli stake move --wallet.name PROXY_WALLET --proxy REAL_COLDKEY_SS58 +``` diff --git a/docs/staking-and-delegation/managing-stake-sdk.md b/docs/staking-and-delegation/managing-stake-sdk.md index 8b7d963dc..357688478 100644 --- a/docs/staking-and-delegation/managing-stake-sdk.md +++ b/docs/staking-and-delegation/managing-stake-sdk.md @@ -1,59 +1,145 @@ --- -title: "Managing stake with Bittensor Python SDK" +title: "Managing Your Stakes" --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; import { SdkVersion } from "../sdk/\_sdk-version.mdx"; +import { ProxyColdkeyWarning } from "../keys/_proxy-warning.mdx"; -# Managing Stake with Bittensor Python SDK +# Managing Your Stakes -This page demonstrates usage of the Bittensor SDK for Python for managing stake. +This guide covers staking operations in Bittensor. TAO holders can **stake** any amount of the liquidity they hold to a validator. Also known as **delegation**, staking supports validators, because their total stake in the subnet, including stake delegated to them by others, determines their consensus power and their share of emissions. After the validator/delegate extracts their **take** the remaining emissions are credited back to the stakers/delegators in proportion to their stake with that validator. -Likewise, TAO holders can **unstake** from a subnet by converting subnet-specific alpha tokens back to TAO through the subnet's automated market maker (AMM). +Subsequently, TAO holders **unstake** from a subnet by converting subnet-specific alpha tokens back to TAO through the subnet's automated market maker (AMM). + See also: - [Staking/delegation overview](./delegation) +- [Working with Proxies](../keys/proxies/working-with-proxies) - [Understanding pricing and anticipating slippage](../learn/slippage) - [Price protection when staking](../learn/price-protection) - [MEV Shield Protection](../sdk/mev-protection.md) -- [Staking with a Proxy](../keys/proxies/staking-with-proxy) +- [Batch Transactions](../learn/batch-transactions) + + +## Best practices for staking security + +When staking real-value liquidity (especially on mainnet), three tools can significantly reduce your exposure to loss, if properly used: proxies, price protection, and MEV shield. + +### Proxies + +A proxy allows a separate key to sign staking transactions on behalf of your primary coldkey, which stays in cold storage. For mainnet staking, always use a proxy with a non-zero delay so that transactions must be announced on-chain before execution, giving you a window to detect and reject unauthorized activity. + +See [Prerequisites: For mainnet](#for-mainnet) for the recommended proxy setup, and [Proxies: Overview](../keys/proxies/) and [Coldkey and Hotkey Workstation Security](../keys/coldkey-hotkey-security) for the full security model. + +### Price protection (safe staking) + +Every stake or unstake operation trades tokens through the subnet's AMM, which means your transaction itself moves the price. Without price protection, you're exposed to: + +- **Slippage**: Large transactions push the price against you. +- **Organic volatility**: The price may move between when you submit and when your transaction lands. + +Price protection sets a hard limit on the worst price you'll accept. If the price moves beyond your limit, the transaction is rejected (strict mode) or partially filled (partial mode). + +See: [Understand Price Protection](../learn/price-protection) -:::tip -Minimum transaction amount for stake/unstake/move/transfer: 500,000 RAO or 0.0005 TAO. -::: -:::warning Keep your coldkey secure -Staking is a regular operation for most TAO holders. Every time you stake or unstake directly, you must decrypt and use your coldkey—exposing it to potential compromise. +
+ SDK Price Protection details + + The SDK provides **price protection** through `add_stake_limit` and `remove_stake_limit` extrinsics, which accept a `limit_price` (the worst acceptable price in RAO per alpha) and an `allow_partial` flag: + +- **Strict mode** (`allow_partial=False`): Transaction is rejected entirely if the final price would exceed your limit. +- **Partial mode** (`allow_partial=True`): Executes the maximum amount that stays within your price limit. + +To calculate `limit_price` from a tolerance percentage: -**For better security, use a [Staking Proxy](../keys/proxies/staking-with-proxy)**. With a `Staking` proxy configured with a delay, you can manage your stake without ever exposing your coldkey. If the proxy is compromised, the delay gives you time to reject unauthorized unstaking attempts. +```python +# For staking (price goes up as you buy alpha): +pool = await subtensor.subnet(netuid=netuid) +limit_price = bt.Balance.from_tao(pool.price.tao * (1 + rate_tolerance)).rao + +# For unstaking (price goes down as you sell alpha): +limit_price = bt.Balance.from_tao(pool.price.tao * (1 - rate_tolerance)).rao +``` + +:::warning SDK default is unsafe +Unlike `btcli`, the SDK does **not** enable price protection by default, it must be explicitly configured. ::: +
-## Check your TAO balance +### MEV shield + +MEV (maximal extractable value) attacks exploit the fact that pending transactions are visible in the mempool before they land in a block. An attacker who sees your staking transaction can front-run it (buying alpha before you, driving the price up) or sandwich it (buying before and selling after, extracting value from your trade). Price protection limits your losses from these attacks, but MEV shield prevents them entirely by encrypting your transaction so its contents are hidden until it is included in a block. + +MEV shield encrypts the signed extrinsic before submission. The chain decrypts and executes it only after block inclusion, so no one — including block producers — can see or front-run the transaction while it is pending. + +To enable MEV shield, add `mev_protection=True` to any supported SDK extrinsic call (or enable it globally): + +```python +response = await subtensor.add_stake_extrinsic( + wallet=wallet, + hotkey_ss58=hotkey, + netuid=netuid, + amount=amount, + mev_protection=True, +) +``` + +MEV shield and price protection are complementary — use both for maximum safety. Price protection caps your worst-case price; MEV shield prevents adversaries from manipulating that price in the first place. + +See: [MEV Shield](../concepts/mev-shield/), [Using MEV Shield with the SDK](../sdk/mev-protection) + +## Prerequisites + +### For testing/practice + +- [Install btcli](../getting-started/install-btcli) and/or the [Bittensor Python SDK](../getting-started/installation) +- [Create a wallet](../keys/working-with-keys#creating-a-wallet-with-btcli) +- Get some test TAO: ask in [Discord](https://discord.com/channels/799672011265015819/1107738550373454028/threads/1331693251589312553), or [run a local blockchain](../local-build/deploy.md) + +### For mainnet + +Everything above, plus: + +- **A hardware wallet** (Ledger or [Polkadot Vault](../keys/coldkey-hotkey-security#hardware-solution-polkadot-vault)) for your primary coldkey. Your primary coldkey should never be loaded onto an internet-connected machine. +- **A `NonTransfer` proxy** created from your hardware wallet. This proxy manages all other proxies so the primary coldkey stays in cold storage permanently. See [Add a Proxy Relationship](../keys/proxies/working-with-proxies#add-a-proxy-relationship) and [Manage proxies through a NonTransfer proxy](../keys/proxies/working-with-proxies#manage-proxies-through-a-nontransfer-proxy). +- **A `Staking` proxy with a non-zero delay**, created through your `NonTransfer` proxy. This is the key you will use for day-to-day staking operations on this page. The delay creates a veto window during which you can [reject unauthorized announcements](../keys/proxies/working-with-proxies#reject-an-announcement). +- **Sufficient TAO** in the proxy wallet to cover transaction fees (fees are dynamic and weight-based; see [Transaction Fees](../learn/fees) and [Estimating Fees](../learn/fees#estimating-fees)). + +See [Coldkey and Hotkey Workstation Security](../keys/coldkey-hotkey-security) for the full security model. -To stake, you'll first need some TAO. Inquire in [Discord](https://discord.com/channels/799672011265015819/1107738550373454028/threads/1331693251589312553) to obtain TAO on Bittensor test network. Alternatively, you can [run a local Bittensor blockchain instance](../local-build/deploy.md). +## Check your TAO balance -:::danger -The funds in a crypto wallet are only as secure as your private key and/or seed phrase, and the devices that have access to these. +Checking a balance is a permissionless operation — only the public key (SS58 address) is needed: -Test network tokens have no real value. Before managing liquidity on Bittensor mainnet, carefully consider all aspects of secrets management and endpoint security! -::: + + + +```bash +btcli wallet balance +``` + + + ```python import bittensor as bt sub = bt.Subtensor(network="test") -wallet = bt.Wallet( - name="PracticeKey!", - hotkey="stakinkey1", -) -wallet.unlock_coldkey() -balance = sub.get_balance(wallet.coldkey.ss58_address) +wallet = bt.Wallet(name="my_coldkey") +balance = sub.get_balance(wallet.coldkeypub.ss58_address) print(balance) ``` + + + ## View exchange rates The following script displays exchange rates for a subnet alpha token, with and without slippage. @@ -80,6 +166,20 @@ print("alpha_to_tao", subnet.alpha_to_tao(alpha_amount)) Use the metagraph to view validators and their stakes within a subnet. This helps you identify top validators before deciding where to stake. + + + +```bash +# View validators on a specific subnet +btcli subnet show --netuid 14 + +# Or view the full metagraph +btcli subnets metagraph --netuid 14 +``` + + + + ```python import bittensor as bt @@ -89,14 +189,11 @@ netuid = 14 # Change to your desired subnet # Fetch the metagraph for the subnet metagraph = sub.metagraph(netuid=netuid) -# Get validator hotkeys and their stakes -validators = [] -for uid in range(len(metagraph.hotkeys)): - hotkey = metagraph.hotkeys[uid] - stake = metagraph.stake[uid] - validators.append((uid, hotkey, stake)) - -# Sort by stake (highest first) and show top 10 +# Pair each UID with its hotkey and stake, then sort by stake +validators = [ + (uid, hk, stake) + for uid, (hk, stake) in enumerate(zip(metagraph.hotkeys, metagraph.stake)) +] top_validators = sorted(validators, key=lambda x: x[2], reverse=True)[:10] print(f"Top 10 Validators in Subnet {netuid}:") @@ -104,361 +201,1105 @@ for rank, (uid, hotkey, stake) in enumerate(top_validators, start=1): print(f" {rank}. UID {uid} | Stake: {stake:.4f} | Hotkey: {hotkey}") ``` -## Register on a subnet + + + -You can register your hotkey on a subnet using the `burned_register` method. This is necessary for staking, mining or validating. + +## Stake without a proxy (insecure) + +:::danger Do not use this on mainnet +Staking without a proxy requires your coldkey private key on the machine. This is acceptable for testing on testnet but is a serious security risk on mainnet. For mainnet, always use a proxy. See [Best practices](#best-practices-for-staking-security). +::: + + + + +```bash +btcli stake add --wallet.name my_coldkey --netuid 14 --amount 1.0 +``` + + + ```python import bittensor as bt -logging = bt.logging -logging.set_info() + sub = bt.Subtensor(network="test") -wallet = bt.Wallet( - name="ExampleWalletName", - hotkey="ExampleHotkey", +wallet = bt.Wallet(name="my_coldkey") + +response = sub.add_stake( + wallet=wallet, + hotkey_ss58="VALIDATOR_HOTKEY_SS58", # replace with validator hotkey + netuid=14, + amount=bt.Balance.from_tao(1.0), ) -wallet.unlock_coldkey() -reg = sub.burned_register(wallet=wallet, netuid=3) +print(response) +``` + + + + +## Stake to a specific validator with a time-delay proxy + +Stake to a single validator on a specific subnet using a time-delay `Staking` proxy with [price protection](#price-protection-safe-staking). The process has three steps: announce the transaction, monitor for unauthorized announcements, then execute after the delay. + +### Step 1. Announce + +Build the staking call and announce its hash on-chain. Record the hash for monitoring. + + + + +```bash +btcli stake add \ + --wallet.name PROXY_WALLET \ + --proxy REAL_COLDKEY_SS58 \ + --netuid 14 \ + --hotkey VALIDATOR_HOTKEY \ + --amount 100.0 \ + --announce-only ``` -## View your registered subnets +Note the call hash from the output. + + + + + + ```python +import asyncio, json import bittensor as bt -sub = bt.Subtensor(network="test") -wallet = bt.Wallet( - name="ExampleWalletName", - hotkey="ExampleHotkey", -) -wallet.unlock_coldkey() -netuids = sub.get_netuids_for_hotkey(wallet.hotkey.ss58_address) -print(netuids) +from bittensor.core.extrinsics.pallets import SubtensorModule + +proxy_wallet = bt.Wallet(name="PROXY_WALLET") # replace with your proxy wallet name +real_account_ss58 = "REAL_COLDKEY_SS58" # replace with your real account SS58 + +async def main(): + async with bt.AsyncSubtensor(network='test') as subtensor: + netuid = 14 + hotkey = "VALIDATOR_HOTKEY" # replace with validator hotkey + amount = bt.Balance.from_tao(100) + + # Build the call with price protection + pool = await subtensor.subnet(netuid=netuid) + rate_tolerance = 0.02 # 2% + limit_price = bt.Balance.from_tao(pool.price.tao * (1 + rate_tolerance)).rao + + add_stake_call = await SubtensorModule(subtensor).add_stake_limit( + netuid=netuid, + hotkey=hotkey, + amount_staked=amount.rao, + limit_price=limit_price, + allow_partial=False, + ) + + call_hash = "0x" + add_stake_call.call_hash.hex() + print(f"Announcing: {call_hash}") + announce_result = await subtensor.announce_proxy( + wallet=proxy_wallet, + real_account_ss58=real_account_ss58, + call_hash=call_hash, + mev_protection=True, + ) + print(announce_result) + + # Save exact parameters so the execute step can rebuild the identical call. + save_data = { + "netuid": netuid, + "hotkey": hotkey, + "call_hash": call_hash, + "amount_staked_rao": amount.rao, + "limit_price_rao": limit_price, + "allow_partial": False, + } + with open("announced_stake.json", "w") as f: + json.dump(save_data, f, indent=2) + print(f"Saved announcement data to announced_stake.json") + +asyncio.run(main()) +``` + + + + +### Step 2. Monitor + +During the delay window, query the chain for pending announcements and verify the only pending hash is the one you announced. This script checks all delayed proxy delegates for your real account, cross-references against `announced_stake.json` from Step 1, and gives a clear verdict. + +btcli cannot query on-chain announcements, so use the SDK or [Polkadot.js](../concepts/inspecting-the-chain). + +:::warning Run this more than once +An attacker could announce after your first check. Run this at least twice: once shortly after announcing, and again immediately before executing Step 3. +::: + +```python +import asyncio, json, sys +import bittensor as bt + +REAL_ACCOUNT_SS58 = "REAL_COLDKEY_SS58" # replace with your coldkey SS58 +ANNOUNCED_FILE = "announced_stake.json" # file saved by Step 1 +BLOCK_TIME_SECONDS = 12 + +async def monitor(): + async with bt.AsyncSubtensor(network="test") as subtensor: + with open(ANNOUNCED_FILE) as f: + data = json.load(f) + # Single-stake file is a dict, not a list + expected_hashes = {data["call_hash"]} if isinstance(data, dict) else {a["call_hash"] for a in data} + + proxies, _ = await subtensor.get_proxies_for_real_account(REAL_ACCOUNT_SS58) + if not proxies: + sys.exit("No proxy relationships found for this account.") + + # Warn about 0-delay proxies — these can act immediately with no announcement + zero_delay = [p for p in proxies if p.delay == 0] + delayed_proxies = [p for p in proxies if p.delay > 0] + + if zero_delay: + print(f"WARNING: {len(zero_delay)} zero-delay proxy relationship(s) detected.") + print(" These proxies can execute instantly with no announcement or veto window:") + for p in zero_delay: + print(f" delegate={p.delegate} type={p.proxy_type}") + print(" If you do not recognize these, revoke them immediately.\n") + + if not delayed_proxies: + sys.exit("No delayed proxy relationships found. Nothing to monitor.") + + current_block = await subtensor.get_current_block() + on_chain_hashes = set() + + for proxy_info in delayed_proxies: + announcements = await subtensor.get_proxy_announcement(proxy_info.delegate) + for ann in announcements: + if ann.real != REAL_ACCOUNT_SS58: + continue + on_chain_hashes.add(ann.call_hash) + blocks_elapsed = current_block - ann.height + blocks_remaining = max(0, proxy_info.delay - blocks_elapsed) + is_ours = ann.call_hash in expected_hashes + executable_now = blocks_remaining == 0 + + if is_ours: + status = "EXPECTED" + elif executable_now: + status = "EXECUTABLE NOW — REJECT IMMEDIATELY" + else: + status = "UNEXPECTED — NOT IN " + ANNOUNCED_FILE + + print( + f"PENDING ANNOUNCEMENT\n" + f" delegate: {proxy_info.delegate}\n" + f" proxy_type: {proxy_info.proxy_type}\n" + f" call_hash: {ann.call_hash}\n" + f" announced: block {ann.height} ({blocks_elapsed} blocks ago)\n" + f" veto window: {blocks_remaining} blocks ({blocks_remaining * BLOCK_TIME_SECONDS}s)\n" + f" status: {status}\n" + ) + + unexpected = on_chain_hashes - expected_hashes + if unexpected: + print("UNAUTHORIZED announcements detected! Reject immediately and rotate keys.") + for h in unexpected: + print(f" {h}") + else: + print("All on-chain announcements match expected hashes. Safe to proceed to Step 3.") + +asyncio.run(monitor()) +``` + +If you see an unexpected hash, [reject it](../keys/proxies/working-with-proxies#reject-an-announcement) immediately. To batch-reject all pending announcements, see [Reject all pending announcements](../keys/proxies/working-with-proxies#reject-all-pending-announcements). To reject a single announcement: + +```python +import asyncio +import bittensor as bt +from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.extrinsics.pallets import Proxy + +async def main(): + async with bt.AsyncSubtensor(network="test") as subtensor: + nontransfer_proxy_wallet = bt.Wallet(name="YOUR_NONTRANSFER_PROXY") # replace + real_account_ss58 = "REAL_COLDKEY_SS58" # replace + delegate_ss58 = "DELEGATE_SS58" # the proxy that made the announcement + + reject_call = await Proxy(subtensor).reject_announcement( + delegate=delegate_ss58, + call_hash="0xSUSPICIOUS_HASH", # replace with the hash to reject + ) + response = await subtensor.proxy( + wallet=nontransfer_proxy_wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=ProxyType.NonTransfer, + call=reject_call, + mev_protection=True, + ) + print(response) + +asyncio.run(main()) ``` -## Asynchronously stake to top subnets/validators +### Step 3. Execute + +After the delay has passed and you have confirmed that only your announced hash is pending, execute the call. You must rebuild the call with exactly the same parameters so the hash matches. + + + + +```bash +btcli proxy execute \ + --wallet.name PROXY_WALLET \ + --call-hash 0x... +``` + + + + +```python +import asyncio, json +import bittensor as bt +from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.extrinsics.pallets import SubtensorModule -The following script incrementally stakes a user-defined amount of TAO in each of the user-defined number of the top subnets. +proxy_wallet = bt.Wallet(name="PROXY_WALLET") # replace +real_account_ss58 = "REAL_COLDKEY_SS58" # replace +PROXY_DELAY_BLOCKS = 100 # must match delay configured when the proxy was created -Note that it uses asynchronous calls to the Bittensor blockchain via the `async_subtensor` module, employing the `await asyncio.gather(*tasks)` pattern. +async def main(): + async with bt.AsyncSubtensor(network='test') as subtensor: + # Load the exact parameters saved during announcement. + with open("announced_stake.json") as f: + data = json.load(f) + + add_stake_call = await SubtensorModule(subtensor).add_stake_limit( + netuid=data["netuid"], + hotkey=data["hotkey"], + amount_staked=data["amount_staked_rao"], + limit_price=data["limit_price_rao"], + allow_partial=data["allow_partial"], + ) + + # Verify the hash matches what was announced. + call_hash = "0x" + add_stake_call.call_hash.hex() + assert call_hash == data["call_hash"], ( + f"Hash mismatch: rebuilt {call_hash} != announced {data['call_hash']}. " + f"Parameters must be identical to announcement." + ) + + current_block = await subtensor.get_current_block() + target_block = current_block + PROXY_DELAY_BLOCKS + print(f"Waiting for block {target_block}...") + await subtensor.wait_for_block(target_block) + + result = await subtensor.proxy_announced( + wallet=proxy_wallet, + delegate_ss58=proxy_wallet.coldkey.ss58_address, + real_account_ss58=real_account_ss58, + force_proxy_type=ProxyType.Staking, + call=add_stake_call, + mev_protection=True, + ) + print(result) + +asyncio.run(main()) +``` -`AsyncSubtensor` methods like `add_stake()`, `unstake()`, `metagraph()`, and `move_stake()` are designed as asynchronous methods, meaning that, unlike their `Subtensor` module equivalents, they return coroutine objects that must be awaizted within an event loop. + + -See [Working with Concurrency](/subnets/asyncio). -:::info -You must create some environment variables before running the following script. To do this, paste the following in your Python environment: +## Stake to top subnets and validators with a time-delay proxy + +This script automatically stakes across the top validators in the top subnets by emission, using a **time-delay proxy**. The process is split into three separate steps with a mandatory human verification step between them: + +1. **Announce**: Build all staking calls and announce their hashes on-chain. Record every hash. +2. **Monitor**: Before doing anything else, verify that **all and only** the hashes you announced are pending. This is the critical security step — if an attacker has compromised your proxy key, unauthorized announcements will appear here. If you see any hash you don't recognize, reject it immediately and rotate your keys. +3. **Execute**: After the delay has passed and you have confirmed the announcements are legitimate, execute them. + +The delay is configured when the proxy relationship is created — see [Add a Proxy Relationship](../keys/proxies/working-with-proxies#add-a-proxy-relationship) and [Announce and Execute a Delayed Proxy Call](../keys/proxies/working-with-proxies#announce-and-execute-a-delayed-proxy-call). This example assumes a delay of 100 blocks (~20 minutes). For high-value accounts, this delay is almost always worth the extra latency. + +### Step 1: Announce + +This script builds all the staking calls, announces each one on-chain, and prints the call hashes, as well as saving them to file. You will need them to verify that no unauthorized announcements were injected before you execute. + +Set up the required environment variables before running: ```python import os -os.environ['BT_WALLET_NAME'] = 'STAKE_WALLET' +os.environ['BT_PROXY_WALLET_NAME'] = 'PROXY_WALLET' # proxy wallet name +os.environ['BT_REAL_ACCOUNT_SS58'] = 'YOUR_COLDKEY_SS58' # primary coldkey SS58 (no private key needed) os.environ['TOTAL_TAO_TO_STAKE'] = '1' os.environ['NUM_SUBNETS_TO_STAKE_IN'] = '3' os.environ['NUM_VALIDATORS_PER_SUBNET'] = '3' ``` -Replace `STAKE_WALLET` with the name of the funded wallet you intend to use. -::: - ```python -import os, sys, asyncio +import os, sys, asyncio, json import bittensor as bt -import time -from bittensor import tao - -# Load environmental variables -wallet_name=os.environ.get('BT_WALLET_NAME') -total_to_stake=os.environ.get('TOTAL_TAO_TO_STAKE') -num_subnets= os.environ.get('NUM_SUBNETS_TO_STAKE_IN') -validators_per_subnet = os.environ.get('NUM_VALIDATORS_PER_SUBNET') - -# Validate inputs -if wallet_name is None: - sys.exit("❌ WALLET not specified. Usage: `WALLET=my-wallet TOTAL_TAO_TO_STAKE=1 NUM_SUBNETS_TO_STAKE_IN=3 NUM_VALIDATORS_PER_SUBNET=3 python script.py`") - -if total_to_stake is None: - print("⚠️ TOTAL_TAO_TO_STAKE not specified. Defaulting to 1 TAO.") - total_to_stake = 1.0 -else: - try: - total_to_stake = float(total_to_stake) - except: - sys.exit("❌ Invalid TOTAL_TAO_TO_STAKE amount.") +from bittensor.core.extrinsics.pallets import SubtensorModule -if num_subnets is None: - num_subnets = 3 -else: - try: - num_subnets = int(num_subnets) - except: - sys.exit("❌ Invalid NUM_SUBNETS_TO_STAKE_IN.") +proxy_wallet_name = os.environ.get('BT_PROXY_WALLET_NAME') +real_account_ss58 = os.environ.get('BT_REAL_ACCOUNT_SS58') -if validators_per_subnet is None: - validators_per_subnet = 3 -else: - try: - validators_per_subnet = int(validators_per_subnet) - except: - sys.exit("❌ Invalid NUM_VALIDATORS_PER_SUBNET.") +if not proxy_wallet_name: + sys.exit("❌ BT_PROXY_WALLET_NAME not specified.") +if not real_account_ss58: + sys.exit("❌ BT_REAL_ACCOUNT_SS58 not specified.") + +try: + total_to_stake = float(os.environ.get('TOTAL_TAO_TO_STAKE', '1')) +except ValueError: + sys.exit("❌ TOTAL_TAO_TO_STAKE must be a number.") -print(f"\n🔓 Using wallet: {wallet_name}") +try: + num_subnets = int(os.environ.get('NUM_SUBNETS_TO_STAKE_IN', '3')) +except ValueError: + sys.exit("❌ NUM_SUBNETS_TO_STAKE_IN must be an integer.") + +try: + validators_per_subnet = int(os.environ.get('NUM_VALIDATORS_PER_SUBNET', '3')) +except ValueError: + sys.exit("❌ NUM_VALIDATORS_PER_SUBNET must be an integer.") + +print(f"\n🔓 Using proxy wallet: {proxy_wallet_name}") +print(f" Staking on behalf of: {real_account_ss58[:12]}...") print(f" Dividing {total_to_stake} TAO across top {validators_per_subnet} validators in each of top {num_subnets} subnets.") -wallet = bt.Wallet(wallet_name) +proxy_wallet = bt.Wallet(proxy_wallet_name) + +# Price protection settings +RATE_TOLERANCE = 0.02 # 2% price tolerance +ALLOW_PARTIAL = False # Strict mode: reject if price exceeds tolerance -# Initialize the subtensor connection within a block scope to ensure it is garbage collected -async def stake_batch(subtensor, netuid, top_validators, amount_to_stake): - for hk in top_validators: - print(f" Staking {amount_to_stake} to {hk} on subnet {netuid}...") +# Time-delay proxy settings +# Must match the delay configured when the proxy relationship was created. +PROXY_DELAY_BLOCKS = 100 # ~20 minutes at 12 seconds per block + +async def announce_stake(subtensor, netuid, hotkey_ss58, amount_to_stake): + """Build a staking call and announce its hash on-chain. Returns the call and hash for later execution.""" + print(f" Announcing stake of {amount_to_stake} to {hotkey_ss58} on subnet {netuid}...") try: - results = await asyncio.gather(*[ subtensor.add_stake(wallet=wallet, netuid=netuid, hotkey_ss58=hk, amount=amount_to_stake) for hk in top_validators ] ) - print(results) + pool = await subtensor.subnet(netuid=netuid) + limit_price = bt.Balance.from_tao(pool.price.tao * (1 + RATE_TOLERANCE)).rao + + add_stake_call = await SubtensorModule(subtensor).add_stake_limit( + netuid=netuid, + hotkey=hotkey_ss58, + amount_staked=amount_to_stake.rao, + limit_price=limit_price, + allow_partial=ALLOW_PARTIAL, + ) + + # GenericCall objects expose a .call_hash property (blake2-256 of the SCALE-encoded call). + call_hash = "0x" + add_stake_call.call_hash.hex() + print(f" Call hash: {call_hash}") + + announce_result = await subtensor.announce_proxy( + wallet=proxy_wallet, + real_account_ss58=real_account_ss58, + call_hash=call_hash, + mev_protection=True, + ) + if not announce_result.success: + print(f" ❌ Announce failed: {announce_result.message}") + return None + + print(f" ✅ Announced successfully") + return { + "netuid": netuid, + "hotkey": hotkey_ss58, + "call_hash": call_hash, + "amount_staked_rao": amount_to_stake.rao, + "limit_price_rao": limit_price, + "allow_partial": ALLOW_PARTIAL, + } except Exception as e: - print(f"❌ Failed to stake to {hk} on subnet {netuid}: {e}") + print(f" ❌ Failed: {e}") + return None -async def find_top_three_valis(subtensor,subnet): +async def find_top_validators(subtensor, subnet): + """Fetch metagraph and return top validators by stake. Read-only, safe to run concurrently.""" netuid = subnet.netuid print(f"\n Subnet {netuid} had {subnet.tao_in_emission} emissions!") - print(f"\n Fetching metagraph for subnet {netuid}...") - - start_time = time.time() metagraph = await subtensor.metagraph(netuid) - print(f"✅ Retrieved metagraph for subnet {netuid} in {time.time() - start_time:.2f} seconds.") - # Extract validators and their stake amounts - hk_stake_pairs = [(metagraph.hotkeys[index], metagraph.stake[index]) for index in range(len(metagraph.stake))] + hk_stake_pairs = list(zip(metagraph.hotkeys, metagraph.stake)) + top_validators = sorted(hk_stake_pairs, key=lambda x: x[1], reverse=True)[:validators_per_subnet] - # Sort validators by stake in descending order - top_validators = sorted(hk_stake_pairs, key=lambda x: x[1], reverse=True)[0:3] + print(f" Top {validators_per_subnet} Validators for Subnet {netuid}:") + for rank, (hk, stake) in enumerate(top_validators, start=1): + print(f" {rank}. {hk} - Stake: {stake}") - # Print the top 3 validators for this subnet - print(f"\n Top 3 Validators for Subnet {netuid}:") - for rank, (index, stake) in enumerate(top_validators, start=1): - print(f" {rank}. Validator index {index} - Stake: {stake}") - - return { - "netuid": netuid, - "metagraph": metagraph, - "validators": top_validators - } + return {"netuid": netuid, "validators": top_validators} async def main(): async with bt.AsyncSubtensor(network='test') as subtensor: - print("Fetching information on top subnets by TAO emissions") - # get subnets and sort by tao emissions sorted_subnets = sorted(list(await subtensor.all_subnets()), key=lambda subnet: subnet.tao_in_emission, reverse=True) - top_subnets = sorted_subnets[0:3] - amount_to_stake = bt.Balance.from_tao(total_to_stake/9) + top_subnets = sorted_subnets[:num_subnets] + amount_to_stake = bt.Balance.from_tao(total_to_stake / (num_subnets * validators_per_subnet)) - # find the top 3 validators in each subnet - top_vali_dicts = await asyncio.gather(*[find_top_three_valis(subtensor, subnet) for subnet in top_subnets]) + # Fetch metagraphs concurrently (read-only, no nonce needed) + top_vali_dicts = await asyncio.gather(*[find_top_validators(subtensor, subnet) for subnet in top_subnets]) top_validators_per_subnet = {} for d in top_vali_dicts: netuid = d['netuid'] - for v in d['validators']: - hk = v[0] - if netuid in top_validators_per_subnet: - top_validators_per_subnet[netuid].append(hk) + top_validators_per_subnet[netuid] = [hk for hk, _ in d['validators']] + + # Announce all stakes sequentially (each announcement needs its own nonce) + announced = [] + for netuid, top_validators in top_validators_per_subnet.items(): + for hk in top_validators: + result = await announce_stake(subtensor, netuid, hk, amount_to_stake) + if result: + announced.append(result) + + # Save the announced hashes for later verification and execution. + # Record these hashes — you will cross-reference them in the monitoring step. + print(f"\n{'='*60}") + print(f"ANNOUNCED {len(announced)} STAKING TRANSACTIONS") + print(f"{'='*60}") + for a in announced: + print(f" Subnet {a['netuid']} → {a['hotkey'][:16]}...") + print(f" Hash: {a['call_hash']}") + print(f"{'='*60}") + print(f"\n⏳ Delay period: {PROXY_DELAY_BLOCKS} blocks (~{PROXY_DELAY_BLOCKS * 12 // 60} minutes)") + print(f" STOP HERE. Do not execute until you have monitored your") + print(f" announcements and confirmed that all and only the above") + print(f" hashes are pending. See Step 2.") + + # Save announced call data so Step 3 can rebuild the exact same calls. + # The limit_price and amount_staked RAO values must be preserved exactly — + # the chain re-hashes the call to verify it matches the announcement. + with open("announced_stakes.json", "w") as f: + json.dump(announced, f, indent=2) + print(f"\n Saved announcement data to announced_stakes.json") + +asyncio.run(main()) +``` +```shell +🔓 Using proxy wallet: zingo + Staking on behalf of: 5ECaCSR1tEzc... + Dividing 1.0 TAO across top 3 validators in each of top 3 subnets. +Fetching information on top subnets by TAO emissions + + Subnet 31 had τ0.043267221 emissions! + + Subnet 119 had τ0.017303911 emissions! + + Subnet 26 had τ0.014446833 emissions! + Top 3 Validators for Subnet 31: + 1. 5H9iGnmydhRKbVNtC6tDr9ZbbEhHAKUE5xuLWZ1wJWsUw49z - Stake: 1130.682861328125 + 2. 5CAbcrX6dDoCLYZrXzNCU9csL8JctBxhi9oZcvtc8hqz5Pri - Stake: 695.8594970703125 + 3. 5DM5o384xnjsohycyLxX9umWKybCJLVoSjwzYBZ8NUwy5zXj - Stake: 83.06707763671875 + Top 3 Validators for Subnet 26: + 1. 5FZijBVEXfmCqhJH8V6aXhSujVMMTPKGb76AiG4QfWVG6fvM - Stake: 2606.20849609375 + 2. 5GYi8aRkGCqQH8YScK4yYDkfZx6DtLVz3G5WJigwwbennZz8 - Stake: 689.8923950195312 + 3. 5DoRe6Zic5PUfnPUno3z8MngQEHvgqEMWhfFMEXB7wug9HsV - Stake: 106.76640319824219 + Top 3 Validators for Subnet 119: + 1. 5FRxKzKrBDX3cCGqXFjYb6zCNC7GMTEaam1FWtsE8Nbr1EQJ - Stake: 993573.125 + 2. 5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT - Stake: 490129.15625 + 3. 5Ckdcm5X2EMe8q3V5EH3A6bhNpUEd8ZM61dwfxECjuJLfMUV - Stake: 297071.78125 + Announcing stake of τ0.111111111 to 5H9iGnmydhRKbVNtC6tDr9ZbbEhHAKUE5xuLWZ1wJWsUw49z on subnet 31... + Call hash: 0x976e9001af2a194cc174ca7f6b9d073f3070c880ee5338ffc9babee1f9f2152f +Enter your password: +Decrypting... + ✅ Announced successfully + Announcing stake of τ0.111111111 to 5CAbcrX6dDoCLYZrXzNCU9csL8JctBxhi9oZcvtc8hqz5Pri on subnet 31... + Call hash: 0xd7932a9330761d28c9715e0f479588c265ed75e58d588d3e90235723535768e8 + ✅ Announced successfully + Announcing stake of τ0.111111111 to 5DM5o384xnjsohycyLxX9umWKybCJLVoSjwzYBZ8NUwy5zXj on subnet 31... + Call hash: 0x16dbf773da71e8783ca70cc25a327550e0e7f0cfe7197bb9df6ca0ef8e80051c + ✅ Announced successfully + Announcing stake of τ0.111111111 to 5FRxKzKrBDX3cCGqXFjYb6zCNC7GMTEaam1FWtsE8Nbr1EQJ on subnet 119... + Call hash: 0x634e5dc1e2c8064d003b55811a3c944a758c977c6d04c2b4c719b5042049aea6 + ✅ Announced successfully + Announcing stake of τ0.111111111 to 5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT on subnet 119... + Call hash: 0x9b3ca2fa9e8674dd56a93079221c99510e1392f2c9ce898fbf28b69a3a60345e + ✅ Announced successfully + Announcing stake of τ0.111111111 to 5Ckdcm5X2EMe8q3V5EH3A6bhNpUEd8ZM61dwfxECjuJLfMUV on subnet 119... + Call hash: 0x6723384df7f9b865165ce1271bdd7cdf29dfc31b1e807eaabf670f17c013b067 + ✅ Announced successfully + Announcing stake of τ0.111111111 to 5FZijBVEXfmCqhJH8V6aXhSujVMMTPKGb76AiG4QfWVG6fvM on subnet 26... + Call hash: 0x47389244e6ed9dcd5b16ae10d2360313db960f87564fce0627603a46fd52d06e + ✅ Announced successfully + Announcing stake of τ0.111111111 to 5GYi8aRkGCqQH8YScK4yYDkfZx6DtLVz3G5WJigwwbennZz8 on subnet 26... + Call hash: 0xc18db3dc6d6260ef995243711a98ee73884914e125d7c35c47def0c190b6dd96 + ✅ Announced successfully + Announcing stake of τ0.111111111 to 5DoRe6Zic5PUfnPUno3z8MngQEHvgqEMWhfFMEXB7wug9HsV on subnet 26... + Call hash: 0xf4671b0f0febcfd4e5d6c32b6817b4241aeff6ea07c6c3001c205be1301f0bc0 + ✅ Announced successfully + +============================================================ +ANNOUNCED 9 STAKING TRANSACTIONS +============================================================ + Subnet 31 → 5H9iGnmydhRKbVNt... + Hash: 0x976e9001af2a194cc174ca7f6b9d073f3070c880ee5338ffc9babee1f9f2152f + Subnet 31 → 5CAbcrX6dDoCLYZr... + Hash: 0xd7932a9330761d28c9715e0f479588c265ed75e58d588d3e90235723535768e8 + Subnet 31 → 5DM5o384xnjsohyc... + Hash: 0x16dbf773da71e8783ca70cc25a327550e0e7f0cfe7197bb9df6ca0ef8e80051c + Subnet 119 → 5FRxKzKrBDX3cCGq... + Hash: 0x634e5dc1e2c8064d003b55811a3c944a758c977c6d04c2b4c719b5042049aea6 + Subnet 119 → 5FCPTnjevGqAuTtt... + Hash: 0x9b3ca2fa9e8674dd56a93079221c99510e1392f2c9ce898fbf28b69a3a60345e + Subnet 119 → 5Ckdcm5X2EMe8q3V... + Hash: 0x6723384df7f9b865165ce1271bdd7cdf29dfc31b1e807eaabf670f17c013b067 + Subnet 26 → 5FZijBVEXfmCqhJH... + Hash: 0x47389244e6ed9dcd5b16ae10d2360313db960f87564fce0627603a46fd52d06e + Subnet 26 → 5GYi8aRkGCqQH8YS... + Hash: 0xc18db3dc6d6260ef995243711a98ee73884914e125d7c35c47def0c190b6dd96 + Subnet 26 → 5DoRe6Zic5PUfnPU... + Hash: 0xf4671b0f0febcfd4e5d6c32b6817b4241aeff6ea07c6c3001c205be1301f0bc0 +============================================================ + +⏳ Delay period: 100 blocks (~20 minutes) + STOP HERE. Do not execute until you have monitored your + announcements and confirmed that all and only the above + hashes are pending. See Step 2. + +``` +### Step 2: Monitor announcements + +:::danger monitoring is not optional +The entire security value of a time-delay proxy depends on monitoring. If you skip this step, a compromised proxy key can drain your account during the delay window by submitting its own announcements. **Always verify that the pending announcements match exactly what you announced**. +::: + +During the delay window, run this script to cross-reference on-chain announcements against the `announced_stakes.json` file saved in Step 1. It checks **all** proxy delegates for your real account (not just the one you used), filters for announcements targeting your coldkey, and flags anything you didn't create. + +:::warning Run this more than once +A single check is not sufficient. An attacker could announce *after* your first check. Run this script at least twice: once shortly after announcing, and again immediately before executing Step 3. +::: + +```python +import asyncio, json, sys +import bittensor as bt + +REAL_ACCOUNT_SS58 = "YOUR_COLDKEY_SS58" # replace with your real account SS58 +ANNOUNCED_FILE = "announced_stakes.json" # file saved by Step 1 +BLOCK_TIME_SECONDS = 12 + +async def monitor(): + async with bt.AsyncSubtensor(network="test") as subtensor: + # Load the hashes we expect from Step 1 + with open(ANNOUNCED_FILE) as f: + expected = json.load(f) + expected_hashes = {a["call_hash"] for a in expected} + + # Get ALL proxy relationships for this real account + proxies, _ = await subtensor.get_proxies_for_real_account(REAL_ACCOUNT_SS58) + if not proxies: + sys.exit("No proxy relationships found for this account.") + + # Warn about 0-delay proxies — these can act immediately with no announcement + zero_delay = [p for p in proxies if p.delay == 0] + delayed_proxies = [p for p in proxies if p.delay > 0] + + if zero_delay: + print(f"WARNING: {len(zero_delay)} zero-delay proxy relationship(s) detected.") + print(" These proxies can execute instantly with no announcement or veto window:") + for p in zero_delay: + print(f" delegate={p.delegate} type={p.proxy_type}") + print(" If you do not recognize these, revoke them immediately.\n") + + if not delayed_proxies: + sys.exit("No delayed proxy relationships found. Nothing to monitor.") + + print(f"Checking {len(delayed_proxies)} delayed proxy delegate(s) for {REAL_ACCOUNT_SS58[:16]}...") + current_block = await subtensor.get_current_block() + on_chain_hashes = set() + + print(f"\n{'='*60}") + print(f"PENDING ANNOUNCEMENTS (block {current_block})") + print(f"{'='*60}") + + found_any = False + for proxy_info in delayed_proxies: + announcements = await subtensor.get_proxy_announcement(proxy_info.delegate) + for ann in announcements: + # Only look at announcements targeting our real account + if ann.real != REAL_ACCOUNT_SS58: + continue + found_any = True + on_chain_hashes.add(ann.call_hash) + blocks_elapsed = current_block - ann.height + blocks_remaining = max(0, proxy_info.delay - blocks_elapsed) + is_ours = ann.call_hash in expected_hashes + executable_now = blocks_remaining == 0 + + if is_ours: + status = "EXPECTED" + elif executable_now: + status = "EXECUTABLE NOW — REJECT IMMEDIATELY" else: - top_validators_per_subnet[netuid] = [hk] + status = "UNEXPECTED — NOT IN " + ANNOUNCED_FILE + + print(f"\n call_hash: {ann.call_hash}") + print(f" delegate: {proxy_info.delegate}") + print(f" proxy_type: {proxy_info.proxy_type}") + print(f" announced: block {ann.height} ({blocks_elapsed} blocks ago)") + print(f" veto window: {blocks_remaining} blocks ({blocks_remaining * BLOCK_TIME_SECONDS}s)") + print(f" status: {status}") + + if not found_any: + print("\n (no pending announcements found for this account)") + + # Cross-reference + missing = expected_hashes - on_chain_hashes + unexpected = on_chain_hashes - expected_hashes + + print(f"\n{'='*60}") + print(f"SUMMARY") + print(f"{'='*60}") + print(f" Expected: {len(expected_hashes)}") + print(f" On-chain: {len(on_chain_hashes)}") + print(f" Matched: {len(expected_hashes & on_chain_hashes)}") + + if missing: + print(f"\n MISSING announcements (expected but not on-chain):") + for h in missing: + print(f" {h}") + print(f" These may not have been finalized yet. Re-check shortly.") + + if unexpected: + print(f"\n UNAUTHORIZED announcements detected!") + print(f" Your proxy key may be compromised.") + print(f" Reject these immediately and rotate your keys.") + for h in unexpected: + print(f" {h}") + return False + else: + print(f"\n All on-chain announcements match expected hashes. Safe to proceed to Step 3.") + return True + +asyncio.run(monitor()) +``` + + +If you see an **unexpected hash**, [reject it](../keys/proxies/working-with-proxies#reject-an-announcement) immediately. To batch-reject all pending announcements at once, see [Reject all pending announcements](../keys/proxies/working-with-proxies#reject-all-pending-announcements). To reject a single announcement: - # Stake to each top 3 validators in each top 3 subnets - start_time = time.time() - await asyncio.gather(*[stake_batch(subtensor, netuid,top_validators, amount_to_stake) for netuid, top_validators in top_validators_per_subnet.items()]) - print(f"Staking completed in {time.time() - start_time:.2f}s") +```python +import asyncio +import bittensor as bt +from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.extrinsics.pallets import Proxy + +async def main(): + async with bt.AsyncSubtensor(network="test") as subtensor: + nontransfer_proxy_wallet = bt.Wallet(name="YOUR_NONTRANSFER_PROXY") # replace + real_account_ss58 = "YOUR_REAL_ACCOUNT_SS58" # replace + delegate_ss58 = "COMPROMISED_DELEGATE_SS58" # the proxy that made the announcement + + reject_call = await Proxy(subtensor).reject_announcement( + delegate=delegate_ss58, + call_hash="0xSUSPICIOUS_HASH_HERE", # replace with the hash to reject + ) + response = await subtensor.proxy( + wallet=nontransfer_proxy_wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=ProxyType.NonTransfer, + call=reject_call, + mev_protection=True, + ) + print(response) asyncio.run(main()) ``` -
- Show sample response! +### Step 3: Execute -```console - Using wallet: PracticeKey! -Staking total not specified, dividing 1 TAO across top 3 validators in each of top 3 subnets by default. - Usage: `TOTAL_TAO_TO STAKE=1 WALLET=my-wallet-name ./stakerscript.py` -Fetching information on top subnets by TAO emissions +After the delay has passed and you have confirmed in Step 2 that all pending announcements are legitimate, execute them. - Subnet 277 had τ0.415595173 emissions! +`proxy_announced` requires the full `GenericCall` object, not just the hash — the chain re-hashes the call you submit and verifies it matches what was announced. This means you must rebuild each call with **exactly the same parameters** used in Step 1. This is why Step 1 saves `amount_staked_rao`, `limit_price_rao`, and `allow_partial` to `announced_stakes.json` — if any parameter differs (e.g. because you recomputed `limit_price` from a newer pool price), the hash won't match and execution will fail. - Fetching metagraph for subnet 277... +```python +import os, sys, asyncio, json +import bittensor as bt +from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.extrinsics.pallets import SubtensorModule - Subnet 3 had τ0.170148635 emissions! +proxy_wallet_name = os.environ.get('BT_PROXY_WALLET_NAME') +real_account_ss58 = os.environ.get('BT_REAL_ACCOUNT_SS58') - Fetching metagraph for subnet 3... +if proxy_wallet_name is None: + sys.exit("❌ BT_PROXY_WALLET_NAME not specified.") +if real_account_ss58 is None: + sys.exit("❌ BT_REAL_ACCOUNT_SS58 not specified.") - Subnet 119 had τ0.137442127 emissions! +proxy_wallet = bt.Wallet(proxy_wallet_name) - Fetching metagraph for subnet 119... -✅ Retrieved metagraph for subnet 277 in 1.60 seconds. +# Load the announcement data saved in Step 1. +# This contains the exact parameter values needed to rebuild identical calls. +with open("announced_stakes.json") as f: + announced = json.load(f) - Top 3 Validators for Subnet 277: - 1. Validator index 5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT - Stake: 550446.75 - 2. Validator index 5EFtEvPcgZHheW36jGXMPMrDETzbngziR3DPPVVp5L5Gt7Wo - Stake: 123175.8515625 - 3. Validator index 5GNyf1SotvL34mEx86C2cvEGJ563hYiPZWazXUueJ5uu16EK - Stake: 54379.609375 -✅ Retrieved metagraph for subnet 119 in 1.97 seconds. +print(f"Loaded {len(announced)} announcements to execute.") - Top 3 Validators for Subnet 119: - 1. Validator index 5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT - Stake: 231810.8125 - 2. Validator index 5FRxKzKrBDX3cCGqXFjYb6zCNC7GMTEaam1FWtsE8Nbr1EQJ - Stake: 118400.6328125 - 3. Validator index 5CFZ9xDaFQVLA9ERsTs9S3i6jp1VDydvjQH5RDsyWCCJkTM4 - Stake: 30794.974609375 -✅ Retrieved metagraph for subnet 3 in 2.00 seconds. - - Top 3 Validators for Subnet 3: - 1. Validator index 5EHammhTy9rV9FhDdYeFY98YTMvU8Vz9Zv2FuFQQQyMTptc6 - Stake: 285393.71875 - 2. Validator index 5FupG35rCCMghVEAzdYuxxb4SWHU7HtpKeveDmSoyCN8vHyb - Stake: 190750.453125 - 3. Validator index 5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT - Stake: 57048.80859375 - Staking τ0.111111111 to 5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT on subnet 277... - Staking τ0.111111111 to 5EFtEvPcgZHheW36jGXMPMrDETzbngziR3DPPVVp5L5Gt7Wo on subnet 277... - Staking τ0.111111111 to 5GNyf1SotvL34mEx86C2cvEGJ563hYiPZWazXUueJ5uu16EK on subnet 277... -Enter your password: -Decrypting... -[True, True, True] - Staking 0.111111111इ to 5EHammhTy9rV9FhDdYeFY98YTMvU8Vz9Zv2FuFQQQyMTptc6 on subnet 3... - Staking 0.111111111इ to 5FupG35rCCMghVEAzdYuxxb4SWHU7HtpKeveDmSoyCN8vHyb on subnet 3... - Staking 0.111111111इ to 5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT on subnet 3... -[True, True, True] - Staking 0.111111111γ to 5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT on subnet 119... - Staking 0.111111111γ to 5FRxKzKrBDX3cCGqXFjYb6zCNC7GMTEaam1FWtsE8Nbr1EQJ on subnet 119... - Staking 0.111111111γ to 5CFZ9xDaFQVLA9ERsTs9S3i6jp1VDydvjQH5RDsyWCCJkTM4 on subnet 119... -[True, True, True] +async def main(): + async with bt.AsyncSubtensor(network='test') as subtensor: + for a in announced: + netuid = a["netuid"] + hotkey = a["hotkey"] + expected_hash = a["call_hash"] + + # Rebuild the call using the exact parameters saved in Step 1. + # Do NOT recompute limit_price from the current pool price — + # the hash must match what was announced. + add_stake_call = await SubtensorModule(subtensor).add_stake_limit( + netuid=netuid, + hotkey=hotkey, + amount_staked=a["amount_staked_rao"], + limit_price=a["limit_price_rao"], + allow_partial=a["allow_partial"], + ) + + # Sanity check: verify the rebuilt call matches the announced hash + rebuilt_hash = "0x" + add_stake_call.call_hash.hex() + if rebuilt_hash != expected_hash: + print(f" ❌ Hash mismatch for subnet {netuid} → {hotkey[:16]}...") + print(f" Expected: {expected_hash}") + print(f" Got: {rebuilt_hash}") + print(f" Skipping — do NOT execute mismatched calls.") + continue + + print(f" Executing: subnet {netuid} → {hotkey[:16]}... (hash: {expected_hash[:18]}...)") + result = await subtensor.proxy_announced( + wallet=proxy_wallet, + delegate_ss58=proxy_wallet.coldkey.ss58_address, + real_account_ss58=real_account_ss58, + force_proxy_type=ProxyType.Staking, + call=add_stake_call, + mev_protection=True, + ) + print(result) + +asyncio.run(main()) ``` -
+## Unstake from a validator + +Unstake from a specific validator on a specific subnet. The `limit_price` for unstaking is computed as `price * (1 - tolerance)` since selling alpha pushes the price down — you're setting a floor on the worst price you'll accept. + + + + +```bash +btcli stake remove \ + --wallet.name PROXY_WALLET \ + --proxy REAL_COLDKEY_SS58 \ + --netuid 14 \ + --hotkey VALIDATOR_HOTKEY \ + --amount 25.0 +``` -## Unstaking From a Validator +To unstake all stake from a validator: -Unstaking is the process of withdrawing your staked TAO from validators, converting subnet-specific alpha tokens back to TAO through the subnet's AMM. When you unstake, slippage applies similar to staking operations—your transaction affects pool prices, with larger amounts experiencing more slippage. +```bash +btcli stake remove --all \ + --wallet.name PROXY_WALLET \ + --proxy REAL_COLDKEY_SS58 +``` -Here's a basic example of unstaking from a specific validator on a specific subnet. + + -`amount` specifies the amount of alpha to unstake. +Replace the placeholder values with your proxy wallet name, real account SS58, target subnet, validator hotkey, and amount. ```python import asyncio import bittensor as bt -async def main(): - async with bt.AsyncSubtensor(network='test') as subtensor: - wallet = bt.Wallet(name="PracticeKey!") - wallet.unlock_coldkey() +from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.extrinsics.pallets import SubtensorModule +proxy_wallet = bt.Wallet(name="PROXY_WALLET") # replace with your proxy wallet name +real_account_ss58 = "REAL_COLDKEY_SS58" # replace with your real account SS58 - result = await subtensor.unstake( - wallet=wallet, - netuid=17, - hotkey_ss58="5FvC...", - amount=bt.Balance.from_tao(10), +async def main(): + async with bt.AsyncSubtensor(network='test') as subtensor: + netuid = 17 + hotkey = "VALIDATOR_HOTKEY" # replace with validator hotkey + amount = bt.Balance.from_tao(10) + + # Compute limit price for unstaking (price floor). + # Unstaking sells alpha for TAO, pushing the price down. + pool = await subtensor.subnet(netuid=netuid) + rate_tolerance = 0.02 # 2% + limit_price = bt.Balance.from_tao(pool.price.tao * (1 - rate_tolerance)).rao + + remove_stake_call = await SubtensorModule(subtensor).remove_stake_limit( + netuid=netuid, + hotkey=hotkey, + amount_unstaked=amount.rao, + limit_price=limit_price, + allow_partial=False, + ) + result = await subtensor.proxy( + wallet=proxy_wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=ProxyType.Staking, + call=remove_stake_call, + mev_protection=True, wait_for_inclusion=True, wait_for_finalization=False, ) - print(result) + asyncio.run(main()) ``` -To unstake all your stake from a specific validator on a subnet: + + + + +## Unstake with a time-delay proxy + +Unstake from a validator using a time-delay `Staking` proxy. This follows the same announce → monitor → execute pattern as [staking with a time-delay proxy](#stake-to-top-subnets-and-validators-with-a-time-delay-proxy), adapted for `remove_stake_limit`. The limit price is a **floor** (worst acceptable price when selling alpha for TAO) rather than a ceiling. + +### Step 1. Announce + +Build the unstaking call and announce its hash on-chain. Save the exact parameters to a file — you will need them to rebuild the identical call in Step 3. + + + + +```bash +btcli stake remove \ + --wallet.name PROXY_WALLET \ + --proxy REAL_COLDKEY_SS58 \ + --netuid 14 \ + --hotkey VALIDATOR_HOTKEY \ + --amount 25.0 \ + --announce-only +``` + +Note the call hash from the output. + + + ```python -import asyncio +import asyncio, json import bittensor as bt +from bittensor.core.extrinsics.pallets import SubtensorModule + +proxy_wallet = bt.Wallet(name="PROXY_WALLET") # replace with your proxy wallet name +real_account_ss58 = "REAL_COLDKEY_SS58" # replace with your real account SS58 async def main(): - async with bt.async_subtensor(network='test') as subtensor: - wallet = bt.Wallet(name="ExampleWallet") - wallet.unlock_coldkey() - - # Unstake all from this validator - result = await subtensor.unstake_all( - wallet=wallet, - netuid=1, - hotkey_ss58="5FCP...", - rate_tolerance=0.005, - wait_for_inclusion=True, - wait_for_finalization=True, + async with bt.AsyncSubtensor(network='test') as subtensor: + netuid = 14 + hotkey = "VALIDATOR_HOTKEY" # replace with validator hotkey + amount = bt.Balance.from_tao(25) + + # Compute limit price for unstaking (price floor). + pool = await subtensor.subnet(netuid=netuid) + rate_tolerance = 0.02 # 2% + limit_price = bt.Balance.from_tao(pool.price.tao * (1 - rate_tolerance)).rao + + remove_stake_call = await SubtensorModule(subtensor).remove_stake_limit( + netuid=netuid, + hotkey=hotkey, + amount_unstaked=amount.rao, + limit_price=limit_price, + allow_partial=False, ) - print(result) + call_hash = "0x" + remove_stake_call.call_hash.hex() + print(f"Announcing: {call_hash}") + announce_result = await subtensor.announce_proxy( + wallet=proxy_wallet, + real_account_ss58=real_account_ss58, + call_hash=call_hash, + mev_protection=True, + ) + print(announce_result) + + # Save parameters so Step 3 can rebuild the exact same call. + save_data = { + "netuid": netuid, + "hotkey": hotkey, + "call_hash": call_hash, + "amount_unstaked_rao": amount.rao, + "limit_price_rao": limit_price, + "allow_partial": False, + } + with open("announced_unstake.json", "w") as f: + json.dump(save_data, f, indent=2) + print(f"Saved announcement data to announced_unstake.json") asyncio.run(main()) ``` -
- Show sample response! -```console -Enter your password: -Decrypting... -ExtrinsicResponse: - success: True - message: Success - extrinsic_function: unstake_all_extrinsic - extrinsic: {'account_id': '0xb0fec20486c9cf366c90bf1c93ad1bbc6b50596653f8832ee6c40483aa73d851', 'signature': {'Sr25519': '0xee6ee6315786232da41a7cc79e583a41179dd28357843ba1d017c11e8d1d52185586bacd5aad5ac52d205a9bcaa49725a572784b1f2a45dbbd8e10d16a0a1e82'}, 'call_function': 'remove_stake_full_limit', 'call_module': 'SubtensorModule', 'call_args': {'hotkey': '5FupG35rCCMghVEAzdYuxxb4SWHU7HtpKeveDmSoyCN8vHyb', 'netuid': 3, 'limit_price': τ0.110584903}, 'nonce': 629, 'era': {'period': 128, 'current': 5844829}, 'tip': 0, 'asset_id': {'tip': 0, 'asset_id': None}, 'mode': 'Disabled', 'signature_version': 1, 'address': '0xb0fec20486c9cf366c90bf1c93ad1bbc6b50596653f8832ee6c40483aa73d851', 'call': {'call_function': 'remove_stake_full_limit', 'call_module': 'SubtensorModule', 'call_args': {'hotkey': '5FupG35rCCMghVEAzdYuxxb4SWHU7HtpKeveDmSoyCN8vHyb', 'netuid': 3, 'limit_price': τ0.110584903}}} - extrinsic_fee: τ0.000136575 - extrinsic_receipt: ExtrinsicReceipt - transaction_tao_fee: None - transaction_alpha_fee: None - data: None - error: None + + + +### Step 2. Monitor + +:::danger Monitoring is not optional +The entire security value of a time-delay proxy depends on monitoring. If you skip this step, a compromised proxy key can drain your account during the delay window by submitting its own announcements. **Always verify that the pending announcements match exactly what you announced**. +::: + +During the delay window, run the [monitoring script from the staking section](#step-2-monitor-announcements) to cross-reference on-chain announcements against your saved data. If you see any hash you didn't create, [reject it](../keys/proxies/working-with-proxies#reject-an-announcement) immediately. + +### Step 3. Execute + +After the delay has passed and you have confirmed that only your announced hash is pending, rebuild the call with the exact same parameters and execute it. + + + + +```bash +btcli proxy execute \ + --wallet.name PROXY_WALLET \ + --call-hash 0x... ``` -
-## Asynchronously unstake from low-emissions validators +
+ -The more advanced script below will unstake from the delegations (stakes) to validators on particular subnets that have yielded the least emissions in the last tempo. +```python +import asyncio, json +import bittensor as bt +from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.extrinsics.pallets import SubtensorModule -:::info -You must create some environment variables before running the following script. To do this, paste the following in your Python environment: +proxy_wallet = bt.Wallet(name="PROXY_WALLET") # replace +real_account_ss58 = "REAL_COLDKEY_SS58" # replace +PROXY_DELAY_BLOCKS = 100 # must match delay configured when the proxy was created + +async def main(): + async with bt.AsyncSubtensor(network='test') as subtensor: + # Load the exact parameters saved during announcement. + with open("announced_unstake.json") as f: + data = json.load(f) + + remove_stake_call = await SubtensorModule(subtensor).remove_stake_limit( + netuid=data["netuid"], + hotkey=data["hotkey"], + amount_unstaked=data["amount_unstaked_rao"], + limit_price=data["limit_price_rao"], + allow_partial=data["allow_partial"], + ) + + # Verify the hash matches what was announced. + call_hash = "0x" + remove_stake_call.call_hash.hex() + assert call_hash == data["call_hash"], ( + f"Hash mismatch: rebuilt {call_hash} != announced {data['call_hash']}. " + f"Parameters must be identical to announcement." + ) + + current_block = await subtensor.get_current_block() + target_block = current_block + PROXY_DELAY_BLOCKS + print(f"Waiting for block {target_block}...") + await subtensor.wait_for_block(target_block) + + result = await subtensor.proxy_announced( + wallet=proxy_wallet, + delegate_ss58=proxy_wallet.coldkey.ss58_address, + real_account_ss58=real_account_ss58, + force_proxy_type=ProxyType.Staking, + call=remove_stake_call, + mev_protection=True, + ) + print(result) + +asyncio.run(main()) +``` + + +
+ +## Unstake from low-emissions validators + +The script below unstakes from the delegations to validators on particular subnets that have yielded the least emissions in the last tempo. + +Set up the required environment variables: ```python import os -os.environ['WALLET'] = 'STAKE_WALLET' -os.environ['TOTAL_TAO_TO_STAKE'] = '1' -os.environ['NUM_SUBNETS_TO_STAKE_IN'] = '3' -os.environ['NUM_VALIDATORS_PER_SUBNET'] = '3' +os.environ['BT_PROXY_WALLET_NAME'] = 'PROXY_WALLET' +os.environ['BT_REAL_ACCOUNT_SS58'] = 'YOUR_COLDKEY_SS58' +os.environ['TOTAL_TAO_TO_UNSTAKE'] = '1' +os.environ['MAX_STAKES_TO_UNSTAKE'] = '10' ``` -Replace `STAKE_WALLET` with the name of the funded wallet you intend to use. -::: - ```python -import os, sys, asyncio, time +import os, sys, asyncio import bittensor as bt -import bittensor_wallet -from bittensor import tao +from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.extrinsics.pallets import SubtensorModule + +proxy_wallet_name = os.environ.get('BT_PROXY_WALLET_NAME') +real_account_ss58 = os.environ.get('BT_REAL_ACCOUNT_SS58') + +if not proxy_wallet_name: + sys.exit("BT_PROXY_WALLET_NAME not specified.") +if not real_account_ss58: + sys.exit("BT_REAL_ACCOUNT_SS58 not specified.") + +try: + total_to_unstake = float(os.environ.get('TOTAL_TAO_TO_UNSTAKE', '1')) +except ValueError: + sys.exit("TOTAL_TAO_TO_UNSTAKE must be a number.") + +try: + max_stakes_to_unstake = int(os.environ.get('MAX_STAKES_TO_UNSTAKE', '10')) +except ValueError: + sys.exit("MAX_STAKES_TO_UNSTAKE must be an integer.") + +print(f" Using proxy wallet: {proxy_wallet_name}") +print(f" Unstaking on behalf of: {real_account_ss58[:12]}...") +print(f" Unstaking a total of {total_to_unstake} TAO across up to {max_stakes_to_unstake} lowest-emission validators") + +total_to_unstake = bt.Balance.from_tao(total_to_unstake) +proxy_wallet = bt.Wallet(proxy_wallet_name) +unstake_minimum = 0.0005 # TAO + +# Price protection settings +RATE_TOLERANCE = 0.02 # 2% +ALLOW_PARTIAL = False # Strict mode async def perform_unstake(subtensor, stake, amount): try: print(f"⏳ Attempting to unstake {amount} from {stake.hotkey_ss58} on subnet {stake.netuid}") - start = time.time() - result = await subtensor.unstake( - wallet, hotkey_ss58=stake.hotkey_ss58, netuid=stake.netuid, amount=amount + + # Compute limit price for this subnet (price floor) + pool = await subtensor.subnet(netuid=stake.netuid) + limit_price = bt.Balance.from_tao(pool.price.tao * (1 - RATE_TOLERANCE)).rao + + remove_stake_call = await SubtensorModule(subtensor).remove_stake_limit( + netuid=stake.netuid, + hotkey=stake.hotkey_ss58, + amount_unstaked=amount.rao, + limit_price=limit_price, + allow_partial=ALLOW_PARTIAL, + ) + result = await subtensor.proxy( + wallet=proxy_wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=ProxyType.Staking, + call=remove_stake_call, + mev_protection=True, ) - elapsed = time.time() - start print(result) - print(f"Time elapsed: {elapsed:.2f}s") return result.success except Exception as e: print(f"❌ Error during unstake from {stake.hotkey_ss58} on subnet {stake.netuid}: {e}") return False - async def main(): async with bt.AsyncSubtensor(network='test') as subtensor: try: - # Retrieve all active active stakes asscociated with the coldkey - stakes = await subtensor.get_stake_info_for_coldkey(wallet_ck) + stakes = await subtensor.get_stake_info_for_coldkey(real_account_ss58) except Exception as e: sys.exit(f"❌ Failed to get stake info: {e}") - # Filter and sort - # Remove small stakes that are under the minimum threshold - stakes = list(filter(lambda s: float(s.stake.tao) > unstake_minimum, stakes)) - # Sort by emission rate (lowest emission first) - stakes = sorted(stakes, key=lambda s: s.emission.tao) - # Limit to the N lowest emission validators - stakes = stakes[:max_stakes_to_unstake] + stakes = [s for s in stakes if float(s.stake.tao) > unstake_minimum] + stakes = sorted(stakes, key=lambda s: s.emission.tao)[:max_stakes_to_unstake] if not stakes: sys.exit("❌ No eligible stakes found to unstake.") @@ -467,175 +1308,438 @@ async def main(): for s in stakes: print(f"Validator: {s.hotkey_ss58}\n NetUID: {s.netuid}\n Stake: {s.stake.tao}\n Emission: {s.emission}\n-----------") - # Determine how much TAO to unstake per validator amount_per_stake = total_to_unstake.tao / len(stakes) - # Prepare concurrent unstake tasks, then execute as a batch - tasks = [ - perform_unstake(subtensor, stake, bt.Balance.from_tao(min(amount_per_stake, stake.stake.tao)).set_unit(stake.netuid)) - for stake in stakes - ] - results = await asyncio.gather(*tasks) - - # Count successes and print final report - success_count = sum(results) + # Unstake sequentially to avoid nonce collisions + success_count = 0 + for stake in stakes: + amount = bt.Balance.from_tao(min(amount_per_stake, stake.stake.tao)).set_unit(stake.netuid) + success = await perform_unstake(subtensor, stake, amount) + if success: + success_count += 1 print(f"\n Unstake complete. Success: {success_count}/{len(stakes)}") -wallet_name = os.environ.get('BT_WALLET_NAME') -total_to_unstake = os.environ.get('TOTAL_TAO_TO_UNSTAKE') -max_stakes_to_unstake = os.environ.get('MAX_STAKES_TO_UNSTAKE') +asyncio.run(main()) +``` -if wallet_name is None: - sys.exit("wallet name not specified. Usage: `TOTAL_TAO_TO_UNSTAKE=1 MAX_STAKES_TO_UNSTAKE=10 WALLET=my-wallet-name ./unstakerscript.py`") +## Move stake -if total_to_unstake is None: - print("Unstaking total not specified, defaulting to 1 TAO.") - total_to_unstake = 1 -else: - try: - total_to_unstake = float(total_to_unstake) - except: - sys.exit("invalid TAO amount!") +Move stake from one validator/subnet to another using a `Staking` proxy. Moving stake converts alpha on the origin subnet to TAO (via the AMM), then converts TAO to alpha on the destination subnet. Both conversions incur slippage. -if max_stakes_to_unstake is None: - max_stakes_to_unstake = 10 -else: - try: - max_stakes_to_unstake = int(max_stakes_to_unstake) - except: - sys.exit("invalid number for MAX_STAKES_TO_UNSTAKE") + + -print(f" Using wallet: {wallet_name}") -print(f" Unstaking a total of {total_to_unstake} TAO across up to {max_stakes_to_unstake} lowest-emission validators") +```bash +btcli stake move \ + --wallet.name PROXY_WALLET \ + --proxy REAL_COLDKEY_SS58 \ + --origin-netuid 5 \ + --origin-hotkey ORIGIN_VALIDATOR_HOTKEY \ + --dest-netuid 18 \ + --dest-hotkey DEST_VALIDATOR_HOTKEY \ + --amount 50.0 +``` -total_to_unstake = bt.Balance.from_tao(total_to_unstake) -wallet = bt.Wallet(wallet_name) -wallet_ck = wallet.coldkeypub.ss58_address + + + +```python +import asyncio +import bittensor as bt +from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.extrinsics.pallets import SubtensorModule + +proxy_wallet = bt.Wallet(name="PROXY_WALLET") # replace with your proxy wallet name +real_account_ss58 = "REAL_COLDKEY_SS58" # replace with your real account SS58 + +async def main(): + async with bt.AsyncSubtensor("test") as subtensor: + move_stake_call = await SubtensorModule(subtensor).move_stake( + origin_netuid=5, + origin_hotkey_ss58="ORIGIN_VALIDATOR_HOTKEY", # replace + destination_netuid=18, + destination_hotkey_ss58="DEST_VALIDATOR_HOTKEY", # replace + alpha_amount=bt.Balance.from_tao(50).rao, + ) + result = await subtensor.proxy( + wallet=proxy_wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=ProxyType.Staking, + call=move_stake_call, + mev_protection=True, + wait_for_inclusion=True, + wait_for_finalization=False, + ) + print(result) -unstake_minimum = 0.0005 # TAO asyncio.run(main()) +``` + + + + +## Move stake with a time-delay proxy + +Move stake between validators/subnets using a time-delay `Staking` proxy. This follows the same announce → monitor → execute pattern as [staking with a time-delay proxy](#stake-to-top-subnets-and-validators-with-a-time-delay-proxy), adapted for `move_stake`. + +### Step 1. Announce +Build the move-stake call and announce its hash on-chain. Save the exact parameters to a file — you will need them to rebuild the identical call in Step 3. + + + + +```bash +btcli stake move \ + --wallet.name PROXY_WALLET \ + --proxy REAL_COLDKEY_SS58 \ + --origin-netuid 5 \ + --origin-hotkey ORIGIN_VALIDATOR_HOTKEY \ + --dest-netuid 18 \ + --dest-hotkey DEST_VALIDATOR_HOTKEY \ + --amount 50.0 \ + --announce-only ``` -
- Show sample response! - -```console -Unstaking total not specified, defaulting to 1 TAO. - Using wallet: PracticeKey! - Unstaking a total of 1 TAO across up to 10 lowest-emission validators - - Preparing to unstake from 10 validators: - -Validator: 5GEXJdUXxLVmrkaHBfkFmoodXrCSUMFSgPXULbnrRicEt1kK - NetUID: 119 - Stake: 229.212349960Ⲃ - Emission: 0.000000000Ⲃ ------------ -Validator: 5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT - NetUID: 119 - Stake: 19.766958098Ⲃ - Emission: 0.000000000Ⲃ ------------ -Validator: 5FRxKzKrBDX3cCGqXFjYb6zCNC7GMTEaam1FWtsE8Nbr1EQJ - NetUID: 119 - Stake: 18.475227001Ⲃ - Emission: 0.000000000Ⲃ ------------ -Validator: 5Gwz1AQmkya4UkiiXc9HASKYLc5dsQ9qzrgqCfSvjtbrbQp6 - NetUID: 3 - Stake: 44.463571197γ - Emission: 0.005925040γ ------------ -Validator: 5EscZNs55FCTfbpgFFDTbiSE7GgwSwqmdivfPikdqTyDiegb - NetUID: 3 - Stake: 786.209456613γ - Emission: 0.102145233γ ------------ -Validator: 5GNyf1SotvL34mEx86C2cvEGJ563hYiPZWazXUueJ5uu16EK - NetUID: 277 - Stake: 5.058595339इ - Emission: 4.550549887इ ------------ -Validator: 5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT - NetUID: 3 - Stake: 11.654577962γ - Emission: 5.429011017γ ------------ -Validator: 5EFtEvPcgZHheW36jGXMPMrDETzbngziR3DPPVVp5L5Gt7Wo - NetUID: 277 - Stake: 5.258687558इ - Emission: 11.038508585इ ------------ -Validator: 5CFZ9xDaFQVLA9ERsTs9S3i6jp1VDydvjQH5RDsyWCCJkTM4 - NetUID: 119 - Stake: 20.942357630Ⲃ - Emission: 16.662837489Ⲃ ------------ -Validator: 5FupG35rCCMghVEAzdYuxxb4SWHU7HtpKeveDmSoyCN8vHyb - NetUID: 3 - Stake: 87.243220111γ - Emission: 22.063085545γ ------------ -⏳ Attempting to unstake τ0.100000000 from 5GEXJdUXxLVmrkaHBfkFmoodXrCSUMFSgPXULbnrRicEt1kK on subnet 119 -Enter your password: -Decrypting... -⏳ Attempting to unstake τ0.100000000 from 5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT on subnet 119 -⏳ Attempting to unstake τ0.100000000 from 5FRxKzKrBDX3cCGqXFjYb6zCNC7GMTEaam1FWtsE8Nbr1EQJ on subnet 119 -⏳ Attempting to unstake τ0.100000000 from 5Gwz1AQmkya4UkiiXc9HASKYLc5dsQ9qzrgqCfSvjtbrbQp6 on subnet 3 -⏳ Attempting to unstake τ0.100000000 from 5EscZNs55FCTfbpgFFDTbiSE7GgwSwqmdivfPikdqTyDiegb on subnet 3 -⏳ Attempting to unstake τ0.100000000 from 5GNyf1SotvL34mEx86C2cvEGJ563hYiPZWazXUueJ5uu16EK on subnet 277 -⏳ Attempting to unstake τ0.100000000 from 5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT on subnet 3 -⏳ Attempting to unstake τ0.100000000 from 5EFtEvPcgZHheW36jGXMPMrDETzbngziR3DPPVVp5L5Gt7Wo on subnet 277 -⏳ Attempting to unstake τ0.100000000 from 5CFZ9xDaFQVLA9ERsTs9S3i6jp1VDydvjQH5RDsyWCCJkTM4 on subnet 119 -⏳ Attempting to unstake τ0.100000000 from 5FupG35rCCMghVEAzdYuxxb4SWHU7HtpKeveDmSoyCN8vHyb on subnet 3 -✅ Successfully unstaked 0.100000000इ from 5Gwz1AQmkya4UkiiXc9HASKYLc5dsQ9qzrgqCfSvjtbrbQp6 on subnet 3 in 10.78s -✅ Successfully unstaked 0.100000000इ from 5FRxKzKrBDX3cCGqXFjYb6zCNC7GMTEaam1FWtsE8Nbr1EQJ on subnet 119 in 10.78s -✅ Successfully unstaked 0.100000000इ from 5GEXJdUXxLVmrkaHBfkFmoodXrCSUMFSgPXULbnrRicEt1kK on subnet 119 in 15.23s -✅ Successfully unstaked 0.100000000इ from 5FupG35rCCMghVEAzdYuxxb4SWHU7HtpKeveDmSoyCN8vHyb on subnet 3 in 10.78s -✅ Successfully unstaked 0.100000000इ from 5EFtEvPcgZHheW36jGXMPMrDETzbngziR3DPPVVp5L5Gt7Wo on subnet 277 in 10.79s -✅ Successfully unstaked 0.100000000इ from 5EscZNs55FCTfbpgFFDTbiSE7GgwSwqmdivfPikdqTyDiegb on subnet 3 in 10.79s -✅ Successfully unstaked 0.100000000इ from 5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT on subnet 3 in 10.83s -✅ Successfully unstaked 0.100000000इ from 5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT on subnet 119 in 10.83s -✅ Successfully unstaked 0.100000000इ from 5GNyf1SotvL34mEx86C2cvEGJ563hYiPZWazXUueJ5uu16EK on subnet 277 in 10.84s -✅ Successfully unstaked 0.100000000इ from 5CFZ9xDaFQVLA9ERsTs9S3i6jp1VDydvjQH5RDsyWCCJkTM4 on subnet 119 in 10.89s - - Unstake complete. Success: 10/10 +Note the call hash from the output. + + + + +```python +import asyncio, json +import bittensor as bt +from bittensor.core.extrinsics.pallets import SubtensorModule + +proxy_wallet = bt.Wallet(name="PROXY_WALLET") # replace with your proxy wallet name +real_account_ss58 = "REAL_COLDKEY_SS58" # replace with your real account SS58 + +async def main(): + async with bt.AsyncSubtensor(network='test') as subtensor: + origin_netuid = 5 + origin_hotkey = "ORIGIN_VALIDATOR_HOTKEY" # replace + destination_netuid = 18 + destination_hotkey = "DEST_VALIDATOR_HOTKEY" # replace + alpha_amount = bt.Balance.from_tao(50).rao + + move_stake_call = await SubtensorModule(subtensor).move_stake( + origin_netuid=origin_netuid, + origin_hotkey_ss58=origin_hotkey, + destination_netuid=destination_netuid, + destination_hotkey_ss58=destination_hotkey, + alpha_amount=alpha_amount, + ) + + call_hash = "0x" + move_stake_call.call_hash.hex() + print(f"Announcing: {call_hash}") + announce_result = await subtensor.announce_proxy( + wallet=proxy_wallet, + real_account_ss58=real_account_ss58, + call_hash=call_hash, + mev_protection=True, + ) + print(announce_result) + + # Save parameters so Step 3 can rebuild the exact same call. + save_data = { + "origin_netuid": origin_netuid, + "origin_hotkey": origin_hotkey, + "destination_netuid": destination_netuid, + "destination_hotkey": destination_hotkey, + "call_hash": call_hash, + "alpha_amount_rao": alpha_amount, + } + with open("announced_move_stake.json", "w") as f: + json.dump(save_data, f, indent=2) + print(f"Saved announcement data to announced_move_stake.json") + +asyncio.run(main()) ``` -
+
+
-## Move stake +### Step 2. Monitor + +:::danger Monitoring is not optional +The entire security value of a time-delay proxy depends on monitoring. If you skip this step, a compromised proxy key can drain your account during the delay window by submitting its own announcements. **Always verify that the pending announcements match exactly what you announced**. +::: + +During the delay window, run the [monitoring script from the staking section](#step-2-monitor-announcements) to cross-reference on-chain announcements against your saved data. If you see any hash you didn't create, [reject it](../keys/proxies/working-with-proxies#reject-an-announcement) immediately. -This operation moves stake from one delegate to another. +### Step 3. Execute + +After the delay has passed and you have confirmed that only your announced hash is pending, rebuild the call with the exact same parameters and execute it. + + + + +```bash +btcli proxy execute \ + --wallet.name PROXY_WALLET \ + --call-hash 0x... +``` + + + + +```python +import asyncio, json +import bittensor as bt +from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.extrinsics.pallets import SubtensorModule + +proxy_wallet = bt.Wallet(name="PROXY_WALLET") # replace +real_account_ss58 = "REAL_COLDKEY_SS58" # replace +PROXY_DELAY_BLOCKS = 100 # must match delay configured when the proxy was created + +async def main(): + async with bt.AsyncSubtensor(network='test') as subtensor: + # Load the exact parameters saved during announcement. + with open("announced_move_stake.json") as f: + data = json.load(f) + + move_stake_call = await SubtensorModule(subtensor).move_stake( + origin_netuid=data["origin_netuid"], + origin_hotkey_ss58=data["origin_hotkey"], + destination_netuid=data["destination_netuid"], + destination_hotkey_ss58=data["destination_hotkey"], + alpha_amount=data["alpha_amount_rao"], + ) + + # Verify the hash matches what was announced. + call_hash = "0x" + move_stake_call.call_hash.hex() + assert call_hash == data["call_hash"], ( + f"Hash mismatch: rebuilt {call_hash} != announced {data['call_hash']}. " + f"Parameters must be identical to announcement." + ) + + current_block = await subtensor.get_current_block() + target_block = current_block + PROXY_DELAY_BLOCKS + print(f"Waiting for block {target_block}...") + await subtensor.wait_for_block(target_block) + + result = await subtensor.proxy_announced( + wallet=proxy_wallet, + delegate_ss58=proxy_wallet.coldkey.ss58_address, + real_account_ss58=real_account_ss58, + force_proxy_type=ProxyType.Staking, + call=move_stake_call, + mev_protection=True, + ) + print(result) + +asyncio.run(main()) +``` + + + + +## Transfer stake ownership + +Transfer the ownership of staked alpha from one coldkey to another. The stake stays with the same validator on the same subnet, but the controlling coldkey changes. This requires a `Transfer` or `SmallTransfer` proxy type (not `Staking`). + +:::warning Transfer vs Move +**Transfer stake** changes which coldkey owns the stake. **Move stake** changes which validator/subnet the stake is on. They are different operations. +::: + + + + +```bash +btcli stake transfer \ + --wallet.name PROXY_WALLET \ + --proxy REAL_COLDKEY_SS58 +``` + +btcli will interactively prompt for the destination coldkey, hotkey, subnet, and amount. + + + ```python import asyncio -from concurrent.futures import ThreadPoolExecutor import bittensor as bt -from bittensor.core.subtensor import Subtensor -from bittensor.core.async_subtensor import AsyncSubtensor +from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.extrinsics.pallets import SubtensorModule + +proxy_wallet = bt.Wallet(name="PROXY_WALLET") # replace with your Transfer proxy wallet name +real_account_ss58 = "REAL_COLDKEY_SS58" # replace with your real account SS58 async def main(): - async with AsyncSubtensor("test") as subtensor: - wallet = bt.Wallet() - wallet.unlock_coldkey() - amount = bt.Balance.from_tao(1.0).set_unit(5) # set amount in origin subnet - result = await subtensor.move_stake(wallet = wallet, - origin_hotkey_ss58 = "5DyHnV9Wz6cnefGfczeBkQCzHZ5fJcVgy7x1eKVh8otMEd31", - origin_netuid = 5, - destination_hotkey_ss58 = "5HidY9Danh9NhNPHL2pfrf97Zboew3v7yz4abuibZszcKEMv", - destination_netuid = 18, - amount = amount, - wait_for_inclusion = True, - wait_for_finalization = False, + async with bt.AsyncSubtensor("test") as subtensor: + transfer_stake_call = await SubtensorModule(subtensor).transfer_stake( + destination_coldkey="DEST_COLDKEY_SS58", # replace with destination coldkey + hotkey="VALIDATOR_HOTKEY", # replace with the validator hotkey + origin_netuid=14, + destination_netuid=14, + alpha_amount=bt.Balance.from_tao(50).rao, + ) + result = await subtensor.proxy( + wallet=proxy_wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=ProxyType.Transfer, + call=transfer_stake_call, + mev_protection=True, + wait_for_inclusion=True, + wait_for_finalization=False, ) print(result) -# Because move_stake is asynchronous, we run it in an event loop: + asyncio.run(main()) +``` + + + + +## Transfer stake ownership with a time-delay proxy + +Transfer stake ownership using a time-delay proxy. Because this operation **permanently changes which coldkey controls the stake**, using a time-delay proxy is strongly recommended. This uses a `Transfer` (or `SmallTransfer`) proxy type. + +### Step 1. Announce +Build the transfer-stake call and announce its hash on-chain. Save the exact parameters to a file — you will need them to rebuild the identical call in Step 3. + + + + +```bash +btcli stake transfer \ + --wallet.name PROXY_WALLET \ + --proxy REAL_COLDKEY_SS58 \ + --announce-only ``` -:::info -Replace `WALLET_NAME` with the name of the funded wallet you intend to use. +btcli will interactively prompt for the destination coldkey, hotkey, subnet, and amount. Note the call hash from the output. + + + + +```python +import asyncio, json +import bittensor as bt +from bittensor.core.extrinsics.pallets import SubtensorModule + +proxy_wallet = bt.Wallet(name="PROXY_WALLET") # replace with your Transfer proxy wallet name +real_account_ss58 = "REAL_COLDKEY_SS58" # replace with your real account SS58 + +async def main(): + async with bt.AsyncSubtensor(network='test') as subtensor: + destination_coldkey = "DEST_COLDKEY_SS58" # replace with destination coldkey + hotkey = "VALIDATOR_HOTKEY" # replace with the validator hotkey + origin_netuid = 14 + destination_netuid = 14 + alpha_amount = bt.Balance.from_tao(50).rao + + transfer_stake_call = await SubtensorModule(subtensor).transfer_stake( + destination_coldkey=destination_coldkey, + hotkey=hotkey, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + alpha_amount=alpha_amount, + ) + + call_hash = "0x" + transfer_stake_call.call_hash.hex() + print(f"Announcing: {call_hash}") + announce_result = await subtensor.announce_proxy( + wallet=proxy_wallet, + real_account_ss58=real_account_ss58, + call_hash=call_hash, + mev_protection=True, + ) + print(announce_result) + + # Save parameters so Step 3 can rebuild the exact same call. + save_data = { + "destination_coldkey": destination_coldkey, + "hotkey": hotkey, + "origin_netuid": origin_netuid, + "destination_netuid": destination_netuid, + "call_hash": call_hash, + "alpha_amount_rao": alpha_amount, + } + with open("announced_transfer_stake.json", "w") as f: + json.dump(save_data, f, indent=2) + print(f"Saved announcement data to announced_transfer_stake.json") + +asyncio.run(main()) +``` + + + + +### Step 2. Monitor + +:::danger Monitoring is not optional +The entire security value of a time-delay proxy depends on monitoring. A compromised Transfer proxy key could redirect your stake to an attacker-controlled coldkey. **Always verify that the pending announcements match exactly what you announced** — especially the `destination_coldkey`. ::: + +During the delay window, run the [monitoring script from the staking section](#step-2-monitor-announcements) to cross-reference on-chain announcements against your saved data. If you see any hash you didn't create, [reject it](../keys/proxies/working-with-proxies#reject-an-announcement) immediately. + +### Step 3. Execute + +After the delay has passed and you have confirmed that only your announced hash is pending, rebuild the call with the exact same parameters and execute it. + + + + +```bash +btcli proxy execute \ + --wallet.name PROXY_WALLET \ + --call-hash 0x... +``` + + + + +```python +import asyncio, json +import bittensor as bt +from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.extrinsics.pallets import SubtensorModule + +proxy_wallet = bt.Wallet(name="PROXY_WALLET") # replace +real_account_ss58 = "REAL_COLDKEY_SS58" # replace +PROXY_DELAY_BLOCKS = 100 # must match delay configured when the proxy was created + +async def main(): + async with bt.AsyncSubtensor(network='test') as subtensor: + # Load the exact parameters saved during announcement. + with open("announced_transfer_stake.json") as f: + data = json.load(f) + + transfer_stake_call = await SubtensorModule(subtensor).transfer_stake( + destination_coldkey=data["destination_coldkey"], + hotkey=data["hotkey"], + origin_netuid=data["origin_netuid"], + destination_netuid=data["destination_netuid"], + alpha_amount=data["alpha_amount_rao"], + ) + + # Verify the hash matches what was announced. + call_hash = "0x" + transfer_stake_call.call_hash.hex() + assert call_hash == data["call_hash"], ( + f"Hash mismatch: rebuilt {call_hash} != announced {data['call_hash']}. " + f"Parameters must be identical to announcement." + ) + + current_block = await subtensor.get_current_block() + target_block = current_block + PROXY_DELAY_BLOCKS + print(f"Waiting for block {target_block}...") + await subtensor.wait_for_block(target_block) + + result = await subtensor.proxy_announced( + wallet=proxy_wallet, + delegate_ss58=proxy_wallet.coldkey.ss58_address, + real_account_ss58=real_account_ss58, + force_proxy_type=ProxyType.Transfer, + call=transfer_stake_call, + mev_protection=True, + ) + print(result) + +asyncio.run(main()) +``` + + + diff --git a/docs/staking-and-delegation/root-claims/managing-root-claims.md b/docs/staking-and-delegation/root-claims/managing-root-claims.md index 13c051298..dfb012df4 100644 --- a/docs/staking-and-delegation/root-claims/managing-root-claims.md +++ b/docs/staking-and-delegation/root-claims/managing-root-claims.md @@ -5,11 +5,14 @@ title: "Managing Root Claims" import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import { SdkVersion } from "../../sdk/_sdk-version.mdx"; +import { ProxyColdkeyWarning } from "../../keys/_proxy-warning.mdx"; # Managing Root Claims This page covers how to configure, monitor, and claim root dividends, i.e. dividends from staking to validators on the Root Subnet. See [Root Claim](./) + + ## Prerequisites - A coldkey with TAO staked on the root network (subnet 0). @@ -30,10 +33,10 @@ Your claim type determines what happens to your root dividends when they're clai -Use the `btcli stake set-claim` command to set your root claim type: +Use the `btcli stake set-claim` command to set your root claim type. The `set_root_claim_type` extrinsic is included in the `Staking` proxy type's allowed operations, so a `Staking` proxy is sufficient: ```bash -btcli stake set-claim +btcli stake set-claim --wallet.name PROXY_WALLET --proxy REAL_COLDKEY_SS58 ``` The command will display your current setting and prompt for changes. @@ -68,38 +71,35 @@ Decrypting... -Use the `set_root_claim_type()` method to set your root claim type: +Use the `set_root_claim_type()` pallet builder with a `Staking` proxy. `set_root_claim_type` is included in the `Staking` proxy type's allowed operations: ```python -import asyncio +import asyncio, os +import bittensor as bt from bittensor_wallet import Wallet from bittensor.core.async_subtensor import AsyncSubtensor +from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.extrinsics.pallets import SubtensorModule async def main(): - # Initialize wallet and subtensor - wallet = Wallet(name="validator", hotkey="default") - async with AsyncSubtensor(network="local") as subtensor: - # Set claim type to 'Keep' to retain Alpha tokens - response = await subtensor.set_root_claim_type( - wallet=wallet, + proxy_wallet = Wallet(name=os.environ['BT_PROXY_WALLET_NAME']) + real_account_ss58 = os.environ['BT_REAL_ACCOUNT_SS58'] + + async with AsyncSubtensor(network="test") as subtensor: + set_claim_call = SubtensorModule(subtensor).set_root_claim_type( new_root_claim_type="Keep", # or "Swap" for TAO accumulation - wait_for_finalization=True ) - + response = await subtensor.proxy( + wallet=proxy_wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=ProxyType.Staking, + call=set_claim_call, + ) print(response) - if response.extrinsic_receipt: - print(f"Transaction hash: {response.extrinsic_receipt.extrinsic_hash}") asyncio.run(main()) ``` -``` -Enter your password: -Decrypting... -✅ Successfully set root claim type to 'Keep' -Transaction hash: 0xe3a387589b0ae6abfd7172088cc7853224f304e0bc4c3688b335a6f6e8f9a508 -``` - You can also query the current claim type: ```python @@ -109,7 +109,7 @@ from bittensor.core.async_subtensor import AsyncSubtensor async def main(): wallet = Wallet(name="validator", hotkey="default") - async with AsyncSubtensor(network="finney") as subtensor: + async with AsyncSubtensor(network="test") as subtensor: claim_type = await subtensor.get_root_claim_type( coldkey_ss58=wallet.coldkeypub.ss58_address ) @@ -271,10 +271,10 @@ To manually trigger a claim: -Use the `btcli stake process-claim` command to manually claim your accumulated root network emissions: +Use the `btcli stake process-claim` command to manually claim your accumulated root network emissions. Use a `RootClaim` proxy — it is scoped specifically to `claim_root` operations: ```console -btcli st process-claim --verbose +btcli st process-claim --verbose --wallet.name PROXY_WALLET --proxy REAL_COLDKEY_SS58 ```
@@ -310,41 +310,33 @@ Do you want to proceed? [y/n]: -Use the `claim_root()` method to manually claim your accumulated root network emissions: +Use the `claim_root()` pallet builder with a `RootClaim` proxy to manually claim your accumulated root network emissions. The `RootClaim` proxy type is scoped specifically to `claim_root` operations: ```python -import asyncio +import asyncio, os from bittensor_wallet import Wallet from bittensor.core.async_subtensor import AsyncSubtensor +from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.extrinsics.pallets import SubtensorModule async def main(): - # Initialize wallet and subtensor - wallet = Wallet(name="validator", hotkey="default") - async with AsyncSubtensor(network="local") as subtensor: - # Specify the subnets to claim from (up to 5 at once) - netuids = [1, 2, 3] - - # Claim root emissions - response = await subtensor.claim_root( - wallet=wallet, - netuids=netuids, - wait_for_finalization=True + proxy_wallet = Wallet(name=os.environ['BT_PROXY_WALLET_NAME']) + real_account_ss58 = os.environ['BT_REAL_ACCOUNT_SS58'] + + async with AsyncSubtensor(network="test") as subtensor: + # Specify subnets to claim from (up to 5 at once) + claim_call = SubtensorModule(subtensor).claim_root(subnets=[1, 2, 3]) + response = await subtensor.proxy( + wallet=proxy_wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=ProxyType.RootClaim, + call=claim_call, ) - print(response) - if response.extrinsic_receipt: - print(f"Transaction hash: {response.extrinsic_receipt.extrinsic_hash}") asyncio.run(main()) ``` -```console -Enter your password: -Decrypting... -✅ Successfully claimed root emissions from subnets [1, 2, 3] -Transaction hash: 0x0e153ac52f63dde1be1854f00daf09f643d1491c6e6b4103cdd5b04591921e3f -``` - You can also check claimable amounts before claiming: ```python @@ -354,7 +346,7 @@ from bittensor.core.async_subtensor import AsyncSubtensor async def main(): wallet = Wallet(name="validator", hotkey="default") - async with AsyncSubtensor(network="finney") as subtensor: + async with AsyncSubtensor(network="test") as subtensor: # Get stake info which includes claimable amounts stake_info = await subtensor.get_stake_info_for_coldkey( coldkey_ss58=wallet.coldkeypub.ss58_address diff --git a/docs/subnets/asyncio.md b/docs/subnets/asyncio.md index f57bf24e5..88aa2703a 100644 --- a/docs/subnets/asyncio.md +++ b/docs/subnets/asyncio.md @@ -184,8 +184,8 @@ COLDKEY_PUBS = [ ] async def main(): # define a coroutine with `async def` - sync_sub = Subtensor(network="finney") - async with AsyncSubtensor(network="finney") as async_subtensor: + sync_sub = Subtensor(network="test") + async with AsyncSubtensor(network="test") as async_subtensor: sync_balance: Balance = sync_sub.get_balance(COLDKEY_PUB) print(f"Sync balance: {sync_balance}") @@ -213,7 +213,7 @@ async def main(): # define a coroutine with `async def` # We can even chain these together quite dramatically, such as this example in btcli wallets: """ async def main(): - async with AsyncSubtensor(network="finney") as subtensor: + async with AsyncSubtensor(network="test") as subtensor: # Get current block hash for consistency block_hash = await subtensor.get_block_hash() diff --git a/docs/subnets/create-a-subnet.md b/docs/subnets/create-a-subnet.md index 81d7da78c..6a66e4a37 100644 --- a/docs/subnets/create-a-subnet.md +++ b/docs/subnets/create-a-subnet.md @@ -52,9 +52,16 @@ You must meet the same [requirements for validation](../validators#requirements- One option for subnet owners is to ask one of the root network (subnet 0) validators to parent your validator hotkey as a childkey of theirs. This will lend their stake to your validator, and can help you ensure that your validator maintains a sufficient stake to effectively participate in consensus as well as resist deregistration. See the [Child Hotkeys](../validators/child-hotkeys) documentation for more detail. -### Subnet creation rate limits +### Subnet creation rate limit -Subnet creations are limited to **one subnet creation per 14400 blocks** (approximately one every two days). The cost to register a new subnet is also dynamic. For these reason, picking the right time to create your subnet requires planning. +Subnet creations are limited to **one subnet creation per 14400 blocks** (approximately one every two days), and the cost to register a new subnet is dynamic. + +
+Query rate limit on-chain + +To check the current rate limit on the blockchain, navigate to the [Polkadot.js browser app](https://polkadot.js.org/apps/?rpc=wss://entrypoint-finney.opentensor.ai:443#/chainstate) connected to Finney. Under **Developer → Chain state → Storage**, and query `subtensorModule.networkRateLimit()`. + +
## Prerequisites @@ -65,6 +72,15 @@ Subnet creations are limited to **one subnet creation per 14400 blocks** (approx - To create a subnet on test chain, your wallet must have sufficient test net TAO. Inquire in [Discord](https://discord.com/channels/799672011265015819/1107738550373454028/threads/1331693251589312553) to obtain TAO on Bittensor test network. - To create a subnet on main network (finney) requires a substantial investment of TAO, depending on current registration cost for new subnets. +:::warning Coldkey required — use a hardware wallet +Creating a subnet requires your primary coldkey. There is no scoped proxy type for subnet creation. Perform this operation from a hardware wallet: + +- **Polkadot Vault** (recommended): An air-gapped phone that can sign any Subtensor extrinsic via QR code. See [Coldkey and Hotkey Workstation Security](../keys/coldkey-hotkey-security). +- **Ledger via Crucible**: Supports a limited set of operations — verify subnet creation is supported before relying on this path. + +If you must use `btcli` directly, load your primary coldkey only on a dedicated, offline-after-use machine. Do not load it onto any machine that is regularly connected to the internet. +::: + ## Creating a subnet on testchain Create your new subnet on the testchain using the test TAO you received from the previous step. This will create a new subnet on the testchain and give you its owner permissions. diff --git a/docs/subnets/metagraph.md b/docs/subnets/metagraph.md index 6f51f32e1..79b0306e6 100644 --- a/docs/subnets/metagraph.md +++ b/docs/subnets/metagraph.md @@ -68,10 +68,10 @@ The Bittensor Python SDK [Metagraph module](pathname:///python-api/html/autoapi/ from bittensor.core.metagraph import Metagraph # Initialize metagraph for subnet 14 (lite mode - excludes weights/bonds) -m = Metagraph(netuid=14, network="finney", sync=True) +m = Metagraph(netuid=14, network="test", sync=True) # Initialize metagraph with full data including weights and bonds -m = Metagraph(netuid=14, network="finney", lite=False, sync=True) +m = Metagraph(netuid=14, network="test", lite=False, sync=True) ``` ### Mechanism-aware metagraphs (multiple incentive mechanisms) @@ -85,13 +85,13 @@ Subnets can run multiple incentive mechanisms. The SDK supports selecting a mech from bittensor.core.metagraph import Metagraph # Default mechanism (0) -meta = Metagraph(netuid=14, network="finney", sync=True) +meta = Metagraph(netuid=14, network="test", sync=True) print(meta.mechid) # 0 print(meta.mechanism_count) # e.g., 2 print(meta.emissions_split) # e.g., [60, 40] # Specific mechanism (1) -mech_meta = Metagraph(netuid=14, network="finney", sync=True, lite=False) +mech_meta = Metagraph(netuid=14, network="test", sync=True, lite=False) mech_meta.mechid = 1 mech_meta.sync() # or re-init with mechid in helper constructors ``` @@ -372,7 +372,7 @@ from bittensor.core.metagraph import Metagraph def main(): # Initialize metagraph for subnet 1 print("Initializing metagraph for subnet 1...") - metagraph = Metagraph(netuid=1, network="finney", sync=True) + metagraph = Metagraph(netuid=1, network="test", sync=True) # Get basic metagraph metadata print("\n=== Basic Metagraph Metadata ===") @@ -406,7 +406,7 @@ from bittensor.core.metagraph import Metagraph def main(): # Initialize metagraph for subnet 1 print("Initializing metagraph for subnet 1...") - metagraph = Metagraph(netuid=1, network="finney", sync=True) + metagraph = Metagraph(netuid=1, network="test", sync=True) # Get all neuron UIDs uids = metagraph.uids @@ -447,7 +447,7 @@ from bittensor.core.metagraph import Metagraph def main(): # Initialize metagraph for subnet 1 print("Initializing metagraph for subnet 1...") - metagraph = Metagraph(netuid=1, network="finney", sync=True) + metagraph = Metagraph(netuid=1, network="test", sync=True) # Get performance metrics ranks = metagraph.R # Performance ranks @@ -502,7 +502,7 @@ from bittensor.core.metagraph import Metagraph def main(): # Initialize metagraph for subnet 1 print("Initializing metagraph for subnet 1...") - metagraph = Metagraph(netuid=1, network="finney", sync=True) + metagraph = Metagraph(netuid=1, network="test", sync=True) # Get economic metrics incentives = metagraph.I # Incentive scores @@ -548,7 +548,7 @@ from bittensor.core.metagraph import Metagraph def main(): # Initialize metagraph for subnet 1 print("Initializing metagraph for subnet 1...") - metagraph = Metagraph(netuid=1, network="finney", sync=True) + metagraph = Metagraph(netuid=1, network="test", sync=True) # Get network information axons = metagraph.axons @@ -601,7 +601,7 @@ from bittensor.core.metagraph import Metagraph def main(): # Initialize metagraph for subnet 1 with full sync (not lite) print("Initializing metagraph for subnet 1 (full sync)...") - metagraph = Metagraph(netuid=1, network="finney", lite=False, sync=True) + metagraph = Metagraph(netuid=1, network="test", lite=False, sync=True) uids = metagraph.uids @@ -659,7 +659,7 @@ from bittensor.core.metagraph import Metagraph def main(): # Initialize metagraph for subnet 1 with full sync (not lite) print("Initializing metagraph for subnet 1 (full sync)...") - metagraph = Metagraph(netuid=1, network="finney", lite=False, sync=True) + metagraph = Metagraph(netuid=1, network="test", lite=False, sync=True) uids = metagraph.uids @@ -699,7 +699,7 @@ from bittensor.core.metagraph import Metagraph def main(): # Initialize metagraph for subnet 1 print("Initializing metagraph for subnet 1...") - metagraph = Metagraph(netuid=1, network="finney", sync=True) + metagraph = Metagraph(netuid=1, network="test", sync=True) # Get activity information active = metagraph.active # Activity status @@ -749,7 +749,7 @@ from bittensor.core.metagraph import Metagraph def main(): # Initialize metagraph for subnet 1 print("Initializing metagraph for subnet 1...") - metagraph = Metagraph(netuid=1, network="finney", sync=True) + metagraph = Metagraph(netuid=1, network="test", sync=True) # Get subnet hyperparameters hparams = metagraph.hparams @@ -805,7 +805,7 @@ from bittensor.core.metagraph import Metagraph def main(): # Initialize metagraph for subnet 1 print("Initializing metagraph for subnet 1...") - metagraph = Metagraph(netuid=1, network="finney", sync=True) + metagraph = Metagraph(netuid=1, network="test", sync=True) # Get basic metrics stakes = metagraph.S @@ -880,12 +880,12 @@ from bittensor.core.metagraph import async_metagraph async def analyze_metagraph(): # Create async subtensor first - async with AsyncSubtensor(network="finney") as subtensor: + async with AsyncSubtensor(network="test") as subtensor: # Create async metagraph with subtensor print("Creating async metagraph...") metagraph = await async_metagraph( netuid=1, - network="finney", + network="test", lite=False, subtensor=subtensor # Pass the subtensor ) @@ -919,7 +919,7 @@ from bittensor.core.metagraph import Metagraph def main(): # Initialize metagraph for subnet 1 print("Initializing metagraph for subnet 1...") - metagraph = Metagraph(netuid=1, network="finney", sync=True, lite=False) + metagraph = Metagraph(netuid=1, network="test", sync=True, lite=False) # Get complete neuron information for first 5 neurons print("=== Complete Neuron Information (First 5 Neurons) ===") diff --git a/docs/subtensor-nodes/subtensor-storage-query-examples.md b/docs/subtensor-nodes/subtensor-storage-query-examples.md index 70a518322..9866e71d9 100644 --- a/docs/subtensor-nodes/subtensor-storage-query-examples.md +++ b/docs/subtensor-nodes/subtensor-storage-query-examples.md @@ -2758,7 +2758,7 @@ import { SdkVersion } from "../sdk/_sdk-version.mdx"; ``` ## 172. TransferToggle -- **Description**: Storage for TransferToggle. When enabled, a holder of alpha stake can transfer its ownership to another coldkey/wallet using [`btcli stake transfer`](../staking-and-delegation/managing-stake-btcli#transferring-stake) or [`transfer_stake`](pathname:///python-api/html/autoapi/bittensor/core/async_subtensor/index.html#bittensor.core.async_subtensor.AsyncSubtensor.transfer_stake). +- **Description**: Storage for TransferToggle. When enabled, a holder of alpha stake can transfer its ownership to another coldkey/wallet using [`btcli stake transfer`](../staking-and-delegation/managing-stake-sdk#transfer-stake-ownership) or [`transfer_stake`](pathname:///python-api/html/autoapi/bittensor/core/async_subtensor/index.html#bittensor.core.async_subtensor.AsyncSubtensor.transfer_stake). - **Query Type**: `u16 -> unknown` - **Parameters**: - `netuid`: `u16` diff --git a/docusaurus.config.js b/docusaurus.config.js index 8d8505d36..8df8908ae 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -77,6 +77,18 @@ const config = { to: "/keys/proxies/working-with-proxies", from: "/keys/proxies/create-proxy", }, + { + from: "/keys/proxies/staking-with-proxy", + to: "/staking-and-delegation/managing-stake-sdk", + }, + { + from: "/staking-and-delegation/managing-stake-btcli", + to: "/staking-and-delegation/managing-stake-sdk", + }, + { + from: "/staking-and-delegation/stakers-btcli-guide", + to: "/staking-and-delegation/managing-stake-sdk", + }, { to: "/subnets/understanding-multiple-mech-subnets", from: "/subnets/understanding-sub-subnets", diff --git a/sidebars.js b/sidebars.js index f6d36ab69..640e81a30 100644 --- a/sidebars.js +++ b/sidebars.js @@ -95,6 +95,7 @@ const sidebars = { "keys/proxies/index", "keys/proxies/working-with-proxies", "keys/proxies/pure-proxies", + "learn/avoid-staking-proxy-attacks", ], }, "keys/multisig", @@ -109,10 +110,11 @@ const sidebars = { collapsed: true, items: [ "staking-and-delegation/delegation", - "staking-and-delegation/stakers-btcli-guide", - "staking-and-delegation/managing-stake-btcli", - "staking-and-delegation/managing-stake-sdk", - "keys/proxies/staking-with-proxy", + { + type: "doc", + id: "staking-and-delegation/managing-stake-sdk", + label: "Managing Your Stakes", + }, { type: "category", label: "Root claims", @@ -124,7 +126,7 @@ const sidebars = { ], }, "learn/price-protection", - "learn/slippage", + "learn/slippage", "staking-and-delegation/staking-polkadot-js", "staking-and-delegation/using-ledger-hw-wallet", ], @@ -235,6 +237,7 @@ const sidebars = { "concepts/commit-reveal", "concepts/stake-burn", "concepts/consensus-based-weights", + "concepts/inspecting-the-chain", "concepts/bt-logging-levels", "resources/utilities", ],