From 9a26fd022f0feb7a1278f700ce5f98bd07b67ece Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 11:23:08 -0800 Subject: [PATCH 01/47] add get_mev_shield_next_key --- .../src/bittensor/subtensor_interface.py | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 054d67f7a..2af67f2ca 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -41,7 +41,7 @@ decode_hex_identity_dict, validate_chain_endpoint, u16_normalized_float, - U16_MAX, + MEV_SHIELD_PUBLIC_KEY_SIZE, get_hotkey_pub_ss58, ) @@ -2299,6 +2299,34 @@ async def get_subnet_ema_tao_inflow( ema_value = fixed_to_float(raw_ema_value) return Balance.from_rao(ema_value) + async def get_mev_shield_next_key( + self, + block_hash: Optional[str] = None, + ) -> Optional[tuple[bytes, int]]: + """ + Get the next MEV Shield public key and epoch from chain storage. + + Args: + block_hash: Optional block hash to query at. + + Returns: + Tuple of (public_key_bytes, epoch) or None if not available. + """ + result = await self.query( + module="MevShield", + storage_function="NextKey", + block_hash=block_hash, + ) + public_key_bytes = bytes(next(iter(result))) + + if len(public_key_bytes) != MEV_SHIELD_PUBLIC_KEY_SIZE: + raise ValueError( + f"Invalid ML-KEM-768 public key size: {len(public_key_bytes)} bytes. " + f"Expected exactly {MEV_SHIELD_PUBLIC_KEY_SIZE} bytes." + ) + + return public_key_bytes + async def best_connection(networks: list[str]): """ From 33eb90abb9aea827ab3ceaeba16c1a48465eb490 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 11:40:09 -0800 Subject: [PATCH 02/47] adds encrypt_call --- .../src/bittensor/extrinsics/mev_shield.py | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 bittensor_cli/src/bittensor/extrinsics/mev_shield.py diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py new file mode 100644 index 000000000..7c48b01a6 --- /dev/null +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -0,0 +1,88 @@ +import asyncio +import hashlib +from typing import TYPE_CHECKING, Optional + +from bittensor_drand import encrypt_mlkem768, mlkem_kdf_id +from bittensor_cli.src.bittensor.utils import encode_account_id + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from scalecodec import GenericCall + from async_substrate_interface import AsyncExtrinsicReceipt + from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + + +async def encrypt_call( + subtensor: "SubtensorInterface", + wallet: "Wallet", + call: "GenericCall", +) -> "GenericCall": + """ + Encrypt a call using MEV Shield. + + Takes any call and returns a MevShield.submit_encrypted call + that can be submitted like any regular extrinsic. + + Args: + subtensor: The SubtensorInterface instance for chain queries. + wallet: The wallet whose coldkey will sign the inner payload. + call: The call to encrypt. + + Returns: + A MevShield.submit_encrypted call. + + Raises: + ValueError: If MEV Shield NextKey is not available on chain. + """ + + next_key_result, genesis_hash, nonce = await asyncio.gather( + subtensor.get_mev_shield_next_key(), + subtensor.substrate.get_block_hash(0), + subtensor.substrate.get_account_nonce(wallet.coldkey.ss58_address), + ) + if next_key_result is None: + raise ValueError("MEV Shield NextKey not available on chain") + + nonce = nonce + 1 # TODO: Update once chain is updated + ml_kem_768_public_key = next_key_result + + # Create payload_core: signer (32B) + nonce (u32 LE) + SCALE(call) + signer_bytes = encode_account_id(wallet.coldkey.ss58_address) + nonce_bytes = (nonce & 0xFFFFFFFF).to_bytes(4, byteorder="little") + scale_call_bytes = bytes(call.data.data) + + payload_core = signer_bytes + nonce_bytes + scale_call_bytes + + mev_shield_version = mlkem_kdf_id() + genesis_hash_clean = ( + genesis_hash[2:] if genesis_hash.startswith("0x") else genesis_hash + ) + genesis_hash_bytes = bytes.fromhex(genesis_hash_clean) + + # Sign: coldkey.sign(b"mev-shield:v1" + genesis_hash + payload_core) + message_to_sign = ( + b"mev-shield:" + mev_shield_version + genesis_hash_bytes + payload_core + ) + signature = wallet.coldkey.sign(message_to_sign) + + # Plaintext: payload_core + b"\x01" + signature + plaintext = payload_core + b"\x01" + signature + + # Encrypt using ML-KEM-768 + ciphertext = encrypt_mlkem768(ml_kem_768_public_key, plaintext) + + # Commitment: blake2_256(payload_core) + commitment_hash = hashlib.blake2b(payload_core, digest_size=32).digest() + commitment_hex = "0x" + commitment_hash.hex() + + # Create the MevShield.submit_encrypted call + encrypted_call = await subtensor.substrate.compose_call( + call_module="MevShield", + call_function="submit_encrypted", + call_params={ + "commitment": commitment_hex, + "ciphertext": ciphertext, + }, + ) + + return encrypted_call From a58c38deffda411a31364508ffa87a092e244d36 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 11:40:26 -0800 Subject: [PATCH 03/47] extract_mev_shield_id --- .../src/bittensor/extrinsics/mev_shield.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index 7c48b01a6..7f91d7b9b 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -86,3 +86,22 @@ async def encrypt_call( ) return encrypted_call + + +async def extract_mev_shield_id(response: "AsyncExtrinsicReceipt") -> Optional[str]: + """ + Extract the MEV Shield wrapper ID from an extrinsic response. + + After submitting a MEV Shield encrypted call, the EncryptedSubmitted event + contains the wrapper ID needed to track execution. + + Args: + response: The extrinsic receipt from submit_extrinsic. + + Returns: + The wrapper ID (hex string) or None if not found. + """ + for event in await response.triggered_events: + if event["event_id"] == "EncryptedSubmitted": + return event["attributes"]["id"] + return None From fb6dcb645b00960a2fa6d1c76560013956428521 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 11:40:36 -0800 Subject: [PATCH 04/47] wait_for_mev_execution --- .../src/bittensor/extrinsics/mev_shield.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index 7f91d7b9b..bd67c089f 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -105,3 +105,65 @@ async def extract_mev_shield_id(response: "AsyncExtrinsicReceipt") -> Optional[s if event["event_id"] == "EncryptedSubmitted": return event["attributes"]["id"] return None + + +async def wait_for_mev_execution( + subtensor: "SubtensorInterface", + wrapper_id: str, + timeout_blocks: int = 4, + status=None, +) -> tuple[bool, Optional[str]]: + """ + Wait for MEV Shield inner call execution. + + After submit_encrypted succeeds, the block author will decrypt and execute + the inner call via execute_revealed. This function polls for the + DecryptedExecuted or DecryptedRejected event. + + Args: + subtensor: SubtensorInterface instance. + wrapper_id: The ID from EncryptedSubmitted event. + timeout_blocks: Max blocks to wait (default 4). + status: Optional rich.Status object for progress updates. + + Returns: + Tuple of (success: bool, error: Optional[str]). + - (True, None) if DecryptedExecuted was found. + - (False, error_message) if DecryptedRejected or timeout. + """ + + start_block = await subtensor.substrate.get_block_number() + current_block = start_block + + while current_block - start_block <= timeout_blocks: + if status: + status.update( + f":hourglass: Waiting for MEV Shield execution " + f"(block {current_block - start_block + 1}/{timeout_blocks})..." + ) + + block_hash = await subtensor.substrate.get_block_hash(current_block) + events = await subtensor.substrate.get_events(block_hash) + + for event in events: + event_id = event.get("event_id", "") + if event_id == "DecryptedExecuted": + if event.get("attributes", {}).get("id") == wrapper_id: + return True, None + elif event_id == "DecryptedRejected": + if event.get("attributes", {}).get("id") == wrapper_id: + error = event.get("attributes", {}).get("error", "Unknown error") + return False, f"MEV Shield execution failed: {error}" + + current_block += 1 + + async def _noop(_): + return True + + await subtensor.substrate.wait_for_block( + current_block, + result_handler=_noop, + task_return=False, + ) + + return False, "Timeout waiting for MEV Shield execution" From 121ac08fa1c91d07f1f4105537e40cd92d9f4976 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 11:42:02 -0800 Subject: [PATCH 05/47] update deps --- bittensor_cli/src/bittensor/utils.py | 1 + pyproject.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index f8e322f01..1ca8325ba 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -37,6 +37,7 @@ BT_DOCS_LINK = "https://docs.learnbittensor.org" GLOBAL_MAX_SUBNET_COUNT = 4096 +MEV_SHIELD_PUBLIC_KEY_SIZE = 1184 console = Console() json_console = Console() diff --git a/pyproject.toml b/pyproject.toml index 9eefb8d4d..549a8f2bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dependencies = [ "async-substrate-interface>=1.5.2", "aiohttp~=3.13", "backoff~=2.2.1", + "bittensor-drand>=1.2.0", "GitPython>=3.0.0", "netaddr~=1.3.0", "numpy>=2.0.1,<3.0.0", From 5c5a7fcc4b0c1d36fd0eaa0f69cbcce4b695925d Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 12:50:49 -0800 Subject: [PATCH 06/47] handle inner call failure --- .../src/bittensor/extrinsics/mev_shield.py | 56 ++++++++++++++----- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index bd67c089f..5c437039b 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -43,7 +43,7 @@ async def encrypt_call( if next_key_result is None: raise ValueError("MEV Shield NextKey not available on chain") - nonce = nonce + 1 # TODO: Update once chain is updated + nonce = nonce + 1 # TODO: Update once chain is updated ml_kem_768_public_key = next_key_result # Create payload_core: signer (32B) + nonce (u32 LE) + SCALE(call) @@ -129,13 +129,13 @@ async def wait_for_mev_execution( Returns: Tuple of (success: bool, error: Optional[str]). - (True, None) if DecryptedExecuted was found. - - (False, error_message) if DecryptedRejected or timeout. + - (False, error_message) if the call failed or timeout. """ start_block = await subtensor.substrate.get_block_number() current_block = start_block - while current_block - start_block <= timeout_blocks: + while current_block - start_block < timeout_blocks: if status: status.update( f":hourglass: Waiting for MEV Shield execution " @@ -143,17 +143,45 @@ async def wait_for_mev_execution( ) block_hash = await subtensor.substrate.get_block_hash(current_block) - events = await subtensor.substrate.get_events(block_hash) - - for event in events: - event_id = event.get("event_id", "") - if event_id == "DecryptedExecuted": - if event.get("attributes", {}).get("id") == wrapper_id: - return True, None - elif event_id == "DecryptedRejected": - if event.get("attributes", {}).get("id") == wrapper_id: - error = event.get("attributes", {}).get("error", "Unknown error") - return False, f"MEV Shield execution failed: {error}" + events, extrinsics = await asyncio.gather( + subtensor.substrate.get_events(block_hash), + subtensor.substrate.get_extrinsics(block_hash), + ) + + # Look for execute_revealed extrinsic + execute_revealed_index = None + for idx, extrinsic in enumerate(extrinsics): + call = extrinsic.get("call", {}) + call_module = call.get("call_module") + call_function = call.get("call_function") + + if call_module == "MevShield" and call_function == "execute_revealed": + call_args = call.get("call_args", []) + for arg in call_args: + if arg.get("name") == "id": + extrinsic_wrapper_id = arg.get("value") + if extrinsic_wrapper_id == wrapper_id: + execute_revealed_index = idx + break + + if execute_revealed_index is not None: + break + + # Check for success or failure events in the extrinsic + if execute_revealed_index is not None: + for event in events: + event_id = event.get("event_id", "") + event_extrinsic_idx = event.get("extrinsic_idx") + + if event_extrinsic_idx == execute_revealed_index: + if event_id == "ExtrinsicSuccess": + return True, None + elif event_id == "ExtrinsicFailed": + dispatch_error = event.get("attributes", {}).get( + "dispatch_error", {} + ) + error_msg = f"{dispatch_error}" + return False, error_msg current_block += 1 From f368f42fc8e81dc4341c76f6559316d055760c38 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 12:53:45 -0800 Subject: [PATCH 07/47] mev shield in stake add ops --- bittensor_cli/src/commands/stake/add.py | 258 +++++++++++++----------- 1 file changed, 145 insertions(+), 113 deletions(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 5046981da..0e927c5f3 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -11,6 +11,11 @@ from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( + encrypt_call, + extract_mev_shield_id, + wait_for_mev_execution, +) from bittensor_cli.src.bittensor.utils import ( console, err_console, @@ -134,6 +139,7 @@ async def safe_stake_extrinsic( }, ), ) + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, @@ -160,49 +166,59 @@ async def safe_stake_extrinsic( err_msg = f"{failure_prelude} with error: {format_error_message(await response.error_message)}" err_out("\n" + err_msg) return False, err_msg, None - else: - if json_output: - # the rest of this checking is not necessary if using json_output - return True, "", response - await print_extrinsic_id(response) - block_hash = await subtensor.substrate.get_chain_head() - new_balance, new_stake = await asyncio.gather( - subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), - subtensor.get_stake( - hotkey_ss58=hotkey_ss58_, - coldkey_ss58=wallet.coldkeypub.ss58_address, - netuid=netuid_, - block_hash=block_hash, - ), - ) - console.print( - f":white_heavy_check_mark: [dark_sea_green3]Finalized. " - f"Stake added to netuid: {netuid_}[/dark_sea_green3]" - ) - console.print( - f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error = await wait_for_mev_execution( + subtensor, mev_shield_id, status=status ) + if not mev_success: + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, err_msg, None - amount_staked = current_balance - new_balance - if allow_partial_stake and (amount_staked != amount_): - console.print( - "Partial stake transaction. Staked:\n" - f" [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_staked}" - f"[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " - f"instead of " - f"[blue]{amount_}[/blue]" - ) + if json_output: + # the rest of this checking is not necessary if using json_output + return True, "", response + await print_extrinsic_id(response) + block_hash = await subtensor.substrate.get_chain_head() + new_balance, new_stake = await asyncio.gather( + subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), + subtensor.get_stake( + hotkey_ss58=hotkey_ss58_, + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=netuid_, + block_hash=block_hash, + ), + ) + console.print( + f":white_heavy_check_mark: [dark_sea_green3]Finalized. " + f"Stake added to netuid: {netuid_}[/dark_sea_green3]" + ) + console.print( + f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) + amount_staked = current_balance - new_balance + if allow_partial_stake and (amount_staked != amount_): console.print( - f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" - f"{netuid_}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " - f"Stake:\n" - f" [blue]{current_stake}[/blue] " - f":arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n" + "Partial stake transaction. Staked:\n" + f" [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_staked}" + f"[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " + f"instead of " + f"[blue]{amount_}[/blue]" ) - return True, "", response + + console.print( + f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"{netuid_}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " + f"Stake:\n" + f" [blue]{current_stake}[/blue] " + f":arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n" + ) + return True, "", response async def stake_extrinsic( netuid_i, amount_, current, staking_address_ss58, status=None @@ -224,6 +240,7 @@ async def stake_extrinsic( failure_prelude = ( f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid_i}" ) + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} ) @@ -235,45 +252,54 @@ async def stake_extrinsic( err_msg = f"{failure_prelude} with error: {format_error_message(e)}" err_out("\n" + err_msg) return False, err_msg, None - else: - if not await response.is_success: - err_msg = f"{failure_prelude} with error: {format_error_message(await response.error_message)}" + if not await response.is_success: + err_msg = f"{failure_prelude} with error: {format_error_message(await response.error_message)}" + err_out("\n" + err_msg) + return False, err_msg, None + + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error = await wait_for_mev_execution( + subtensor, mev_shield_id, status=status + ) + if not mev_success: + err_msg = f"{failure_prelude}: {mev_error}" err_out("\n" + err_msg) return False, err_msg, None - else: - if json_output: - # the rest of this is not necessary if using json_output - return True, "", response - await print_extrinsic_id(response) - new_block_hash = await subtensor.substrate.get_chain_head() - new_balance, new_stake = await asyncio.gather( - subtensor.get_balance( - wallet.coldkeypub.ss58_address, block_hash=new_block_hash - ), - subtensor.get_stake( - hotkey_ss58=staking_address_ss58, - coldkey_ss58=wallet.coldkeypub.ss58_address, - netuid=netuid_i, - block_hash=new_block_hash, - ), - ) - console.print( - f":white_heavy_check_mark: " - f"[dark_sea_green3]Finalized. Stake added to netuid: {netuid_i}[/dark_sea_green3]" - ) - console.print( - f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" - ) - console.print( - f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" - f"{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " - f"Stake:\n" - f" [blue]{current}[/blue] " - f":arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n" - ) - return True, "", response + + if json_output: + # the rest of this is not necessary if using json_output + return True, "", response + await print_extrinsic_id(response) + new_block_hash = await subtensor.substrate.get_chain_head() + new_balance, new_stake = await asyncio.gather( + subtensor.get_balance( + wallet.coldkeypub.ss58_address, block_hash=new_block_hash + ), + subtensor.get_stake( + hotkey_ss58=staking_address_ss58, + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=netuid_i, + block_hash=new_block_hash, + ), + ) + console.print( + f":white_heavy_check_mark: " + f"[dark_sea_green3]Finalized. Stake added to netuid: {netuid_i}[/dark_sea_green3]" + ) + console.print( + f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) + console.print( + f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " + f"Stake:\n" + f" [blue]{current}[/blue] " + f":arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n" + ) + return True, "", response netuids = ( netuids if netuids is not None else await subtensor.get_all_subnet_netuids() @@ -435,47 +461,53 @@ async def stake_extrinsic( if not unlock_key(wallet).success: return - if safe_staking: - stake_coroutines = {} - for i, (ni, am, curr, price_with_tolerance) in enumerate( - zip( - netuids, amounts_to_stake, current_stake_balances, prices_with_tolerance - ) - ): - for _, staking_address in hotkeys_to_stake_to: - # Regular extrinsic for root subnet - if ni == 0: - stake_coroutines[(ni, staking_address)] = stake_extrinsic( - netuid_i=ni, - amount_=am, - current=curr, - staking_address_ss58=staking_address, - ) - else: - stake_coroutines[(ni, staking_address)] = safe_stake_extrinsic( - netuid_=ni, - amount_=am, - current_stake=curr, - hotkey_ss58_=staking_address, - price_limit=price_with_tolerance, - ) - else: - stake_coroutines = { - (ni, staking_address): stake_extrinsic( - netuid_i=ni, - amount_=am, - current=curr, - staking_address_ss58=staking_address, - ) - for i, (ni, am, curr) in enumerate( - zip(netuids, amounts_to_stake, current_stake_balances) - ) - for _, staking_address in hotkeys_to_stake_to - } successes = defaultdict(dict) error_messages = defaultdict(dict) extrinsic_ids = defaultdict(dict) - with console.status(f"\n:satellite: Staking on netuid(s): {netuids} ..."): + with console.status(f"\n:satellite: Staking on netuid(s): {netuids} ...") as status: + if safe_staking: + stake_coroutines = {} + for i, (ni, am, curr, price_with_tolerance) in enumerate( + zip( + netuids, + amounts_to_stake, + current_stake_balances, + prices_with_tolerance, + ) + ): + for _, staking_address in hotkeys_to_stake_to: + # Regular extrinsic for root subnet + if ni == 0: + stake_coroutines[(ni, staking_address)] = stake_extrinsic( + netuid_i=ni, + amount_=am, + current=curr, + staking_address_ss58=staking_address, + status=status, + ) + else: + stake_coroutines[(ni, staking_address)] = safe_stake_extrinsic( + netuid_=ni, + amount_=am, + current_stake=curr, + hotkey_ss58_=staking_address, + price_limit=price_with_tolerance, + status=status, + ) + else: + stake_coroutines = { + (ni, staking_address): stake_extrinsic( + netuid_i=ni, + amount_=am, + current=curr, + staking_address_ss58=staking_address, + status=status, + ) + for i, (ni, am, curr) in enumerate( + zip(netuids, amounts_to_stake, current_stake_balances) + ) + for _, staking_address in hotkeys_to_stake_to + } # We can gather them all at once but balance reporting will be in race-condition. for (ni, staking_address), coroutine in stake_coroutines.items(): success, er_msg, ext_receipt = await coroutine From 6558a2d2c56ee645001878ca1df18eb476dcc260 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 13:13:21 -0800 Subject: [PATCH 08/47] Add shield in remove ops --- bittensor_cli/src/commands/stake/remove.py | 33 ++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 5d125cc16..b8f37905f 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -11,6 +11,11 @@ from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src import COLOR_PALETTE +from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( + encrypt_call, + extract_mev_shield_id, + wait_for_mev_execution, +) from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.utils import ( console, @@ -601,6 +606,8 @@ async def _unstake_extrinsic( }, ), ) + + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, era={"period": era} ) @@ -615,6 +622,17 @@ async def _unstake_extrinsic( f"{format_error_message(await response.error_message)}" ) return False, None + + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error = await wait_for_mev_execution( + subtensor, mev_shield_id, status=status + ) + if not mev_success: + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, None + # Fetch latest balance and stake await print_extrinsic_id(response) block_hash = await subtensor.substrate.get_chain_head() @@ -680,7 +698,6 @@ async def _safe_unstake_extrinsic( current_balance, next_nonce, current_stake, call = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), - subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), subtensor.get_stake( hotkey_ss58=hotkey_ss58, coldkey_ss58=wallet.coldkeypub.ss58_address, @@ -701,8 +718,9 @@ async def _safe_unstake_extrinsic( ), ) + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} + call=call, keypair=wallet.coldkey, era={"period": era} ) try: @@ -726,6 +744,17 @@ async def _safe_unstake_extrinsic( f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}" ) return False, None + + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error = await wait_for_mev_execution( + subtensor, mev_shield_id, status=status + ) + if not mev_success: + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, None + await print_extrinsic_id(response) block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( From b1f3770acb97064a7d164259b3aedcc98a4fdd1d Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 21:56:08 -0800 Subject: [PATCH 09/47] add error handling --- .../src/bittensor/extrinsics/mev_shield.py | 141 +++++++++++++----- 1 file changed, 103 insertions(+), 38 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index 5c437039b..395fbf636 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -2,13 +2,13 @@ import hashlib from typing import TYPE_CHECKING, Optional +from async_substrate_interface import AsyncExtrinsicReceipt from bittensor_drand import encrypt_mlkem768, mlkem_kdf_id -from bittensor_cli.src.bittensor.utils import encode_account_id +from bittensor_cli.src.bittensor.utils import encode_account_id, format_error_message if TYPE_CHECKING: from bittensor_wallet import Wallet from scalecodec import GenericCall - from async_substrate_interface import AsyncExtrinsicReceipt from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface @@ -112,7 +112,7 @@ async def wait_for_mev_execution( wrapper_id: str, timeout_blocks: int = 4, status=None, -) -> tuple[bool, Optional[str]]: +) -> tuple[bool, Optional[str], Optional[AsyncExtrinsicReceipt]]: """ Wait for MEV Shield inner call execution. @@ -127,11 +127,14 @@ async def wait_for_mev_execution( status: Optional rich.Status object for progress updates. Returns: - Tuple of (success: bool, error: Optional[str]). - - (True, None) if DecryptedExecuted was found. - - (False, error_message) if the call failed or timeout. + Tuple of (success: bool, error: Optional[str], receipt: Optional[AsyncExtrinsicReceipt]). + - (True, None, receipt) if DecryptedExecuted was found. + - (False, error_message, None) if the call failed or timeout. """ + async def _noop(_): + return True + start_block = await subtensor.substrate.get_block_number() current_block = start_block @@ -143,15 +146,12 @@ async def wait_for_mev_execution( ) block_hash = await subtensor.substrate.get_block_hash(current_block) - events, extrinsics = await asyncio.gather( - subtensor.substrate.get_events(block_hash), - subtensor.substrate.get_extrinsics(block_hash), - ) + extrinsics = await subtensor.substrate.get_extrinsics(block_hash) - # Look for execute_revealed extrinsic + # Find executeRevealed extrinsic & match ids execute_revealed_index = None for idx, extrinsic in enumerate(extrinsics): - call = extrinsic.get("call", {}) + call = extrinsic.value.get("call", {}) call_module = call.get("call_module") call_function = call.get("call_function") @@ -167,31 +167,96 @@ async def wait_for_mev_execution( if execute_revealed_index is not None: break - # Check for success or failure events in the extrinsic - if execute_revealed_index is not None: - for event in events: - event_id = event.get("event_id", "") - event_extrinsic_idx = event.get("extrinsic_idx") - - if event_extrinsic_idx == execute_revealed_index: - if event_id == "ExtrinsicSuccess": - return True, None - elif event_id == "ExtrinsicFailed": - dispatch_error = event.get("attributes", {}).get( - "dispatch_error", {} - ) - error_msg = f"{dispatch_error}" - return False, error_msg - - current_block += 1 - - async def _noop(_): - return True - - await subtensor.substrate.wait_for_block( - current_block, - result_handler=_noop, - task_return=False, + if execute_revealed_index is None: + current_block += 1 + await subtensor.substrate.wait_for_block( + current_block, + result_handler=_noop, + task_return=False, + ) + continue + + receipt = AsyncExtrinsicReceipt( + substrate=subtensor.substrate, + block_hash=block_hash, + extrinsic_idx=execute_revealed_index, ) - return False, "Timeout waiting for MEV Shield execution" + # TODO: Activate this when we update up-stream + # if not await receipt.is_success: + # error_msg = format_error_message(await receipt.error_message) + # return False, error_msg, None + + error = await check_mev_shield_error(receipt, subtensor, wrapper_id) + if error: + error_msg = format_error_message(error) + return False, error_msg, None + + return True, None, receipt + + return False, "Timeout waiting for MEV Shield execution", None + + +async def check_mev_shield_error( + receipt: AsyncExtrinsicReceipt, + subtensor: "SubtensorInterface", + wrapper_id: str, +) -> Optional[dict]: + """ + Handles & extracts error messages in the MEV Shield extrinsics. + This is a temporary implementation until we update up-stream code. + + Args: + receipt: AsyncExtrinsicReceipt for the execute_revealed extrinsic. + subtensor: SubtensorInterface instance. + wrapper_id: The wrapper ID to verify we're checking the correct event. + + Returns: + Error dict to be used with format_error_message(), or None if no error. + """ + if not await receipt.is_success: + return await receipt.error_message + + for event in await receipt.triggered_events: + event_details = event.get("event", {}) + + if ( + event_details.get("module_id") == "MevShield" + and event_details.get("event_id") == "DecryptedRejected" + ): + attributes = event_details.get("attributes", {}) + event_wrapper_id = attributes.get("id") + + if event_wrapper_id != wrapper_id: + continue + + reason = attributes.get("reason", {}) + dispatch_error = reason.get("error", {}) + + try: + if "Module" in dispatch_error: + module_index = dispatch_error["Module"]["index"] + error_index = dispatch_error["Module"]["error"] + + if isinstance(error_index, str) and error_index.startswith("0x"): + error_index = int(error_index[2:4], 16) + + runtime = await subtensor.substrate.init_runtime( + block_hash=receipt.block_hash + ) + module_error = runtime.metadata.get_module_error( + module_index=module_index, + error_index=error_index, + ) + + return { + "type": "Module", + "name": module_error.name, + "docs": module_error.docs, + } + except Exception: + return dispatch_error + + return dispatch_error + + return None From 4b324674086885b7efa970df329979a35a4d2113 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 21:56:21 -0800 Subject: [PATCH 10/47] improve waiting --- .../src/bittensor/extrinsics/mev_shield.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index 395fbf636..ea9da9ad9 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -136,15 +136,21 @@ async def _noop(_): return True start_block = await subtensor.substrate.get_block_number() - current_block = start_block + current_block = start_block + 1 - while current_block - start_block < timeout_blocks: + while current_block - start_block <= timeout_blocks: if status: status.update( - f":hourglass: Waiting for MEV Shield execution " - f"(block {current_block - start_block + 1}/{timeout_blocks})..." + f"Waiting for :shield: MEV Protection " + f"(checking block {current_block - start_block} of {timeout_blocks})..." ) + await subtensor.substrate.wait_for_block( + current_block, + result_handler=_noop, + task_return=False, + ) + block_hash = await subtensor.substrate.get_block_hash(current_block) extrinsics = await subtensor.substrate.get_extrinsics(block_hash) @@ -169,11 +175,6 @@ async def _noop(_): if execute_revealed_index is None: current_block += 1 - await subtensor.substrate.wait_for_block( - current_block, - result_handler=_noop, - task_return=False, - ) continue receipt = AsyncExtrinsicReceipt( From 2ca2ddff385a66663bed617ba405be7bc2fd8c3a Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 21:57:23 -0800 Subject: [PATCH 11/47] update add/remove --- bittensor_cli/src/commands/stake/add.py | 4 ++-- bittensor_cli/src/commands/stake/remove.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 0e927c5f3..9bcf2c858 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -169,7 +169,7 @@ async def safe_stake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: - mev_success, mev_error = await wait_for_mev_execution( + mev_success, mev_error, response = await wait_for_mev_execution( subtensor, mev_shield_id, status=status ) if not mev_success: @@ -259,7 +259,7 @@ async def stake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: - mev_success, mev_error = await wait_for_mev_execution( + mev_success, mev_error, response = await wait_for_mev_execution( subtensor, mev_shield_id, status=status ) if not mev_success: diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index b8f37905f..e6dc4b38b 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -625,7 +625,7 @@ async def _unstake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: - mev_success, mev_error = await wait_for_mev_execution( + mev_success, mev_error, response = await wait_for_mev_execution( subtensor, mev_shield_id, status=status ) if not mev_success: @@ -747,7 +747,7 @@ async def _safe_unstake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: - mev_success, mev_error = await wait_for_mev_execution( + mev_success, mev_error, response = await wait_for_mev_execution( subtensor, mev_shield_id, status=status ) if not mev_success: From 142d93a635d891cbb30046f9f08c49e3036064e6 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 21:57:56 -0800 Subject: [PATCH 12/47] add protection to move stakes --- bittensor_cli/src/commands/stake/move.py | 44 ++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 99a0b79ac..e28ecf3d0 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -8,6 +8,11 @@ from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( + encrypt_call, + extract_mev_shield_id, + wait_for_mev_execution, +) from bittensor_cli.src.bittensor.utils import ( console, err_console, @@ -578,13 +583,24 @@ async def move_stake( f"\n:satellite: Moving [blue]{amount_to_move_as_balance}[/blue] from [blue]{origin_hotkey}[/blue] on netuid: " f"[blue]{origin_netuid}[/blue] \nto " f"[blue]{destination_hotkey}[/blue] on netuid: [blue]{destination_netuid}[/blue] ..." - ): + ) as status: + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, era={"period": era} ) response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) + + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, status=status + ) + if not mev_success: + err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") + return False, "" + ext_id = await response.get_extrinsic_identifier() if not prompt: @@ -765,7 +781,8 @@ async def transfer_stake( if not unlock_key(wallet).success: return False, "" - with console.status("\n:satellite: Transferring stake ..."): + with console.status("\n:satellite: Transferring stake ...") as status: + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, era={"period": era} ) @@ -773,6 +790,16 @@ async def transfer_stake( response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) + + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, status=status + ) + if not mev_success: + err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") + return False, "" + ext_id = await response.get_extrinsic_identifier() if not prompt: @@ -940,7 +967,8 @@ async def swap_stake( with console.status( f"\n:satellite: Swapping stake from netuid [blue]{origin_netuid}[/blue] " f"to netuid [blue]{destination_netuid}[/blue]..." - ): + ) as status: + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, era={"period": era} ) @@ -950,6 +978,16 @@ async def swap_stake( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) + + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, status=status + ) + if not mev_success: + err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") + return False, "" + ext_id = await response.get_extrinsic_identifier() if not prompt: From 00d7b636aceeb8b1151d12148cb09c11dbbe9c25 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 22:04:27 -0800 Subject: [PATCH 13/47] remove statuses when in error state --- bittensor_cli/src/commands/stake/add.py | 2 ++ bittensor_cli/src/commands/stake/move.py | 3 +++ bittensor_cli/src/commands/stake/remove.py | 2 ++ 3 files changed, 7 insertions(+) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 9bcf2c858..e6d9490d5 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -173,6 +173,7 @@ async def safe_stake_extrinsic( subtensor, mev_shield_id, status=status ) if not mev_success: + status.stop() err_msg = f"{failure_prelude}: {mev_error}" err_out("\n" + err_msg) return False, err_msg, None @@ -263,6 +264,7 @@ async def stake_extrinsic( subtensor, mev_shield_id, status=status ) if not mev_success: + status.stop() err_msg = f"{failure_prelude}: {mev_error}" err_out("\n" + err_msg) return False, err_msg, None diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index e28ecf3d0..294e6a159 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -598,6 +598,7 @@ async def move_stake( subtensor, mev_shield_id, status=status ) if not mev_success: + status.stop() err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") return False, "" @@ -797,6 +798,7 @@ async def transfer_stake( subtensor, mev_shield_id, status=status ) if not mev_success: + status.stop() err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") return False, "" @@ -985,6 +987,7 @@ async def swap_stake( subtensor, mev_shield_id, status=status ) if not mev_success: + status.stop() err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") return False, "" diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index e6dc4b38b..91fdc69f8 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -629,6 +629,7 @@ async def _unstake_extrinsic( subtensor, mev_shield_id, status=status ) if not mev_success: + status.stop() err_msg = f"{failure_prelude}: {mev_error}" err_out("\n" + err_msg) return False, None @@ -751,6 +752,7 @@ async def _safe_unstake_extrinsic( subtensor, mev_shield_id, status=status ) if not mev_success: + status.stop() err_msg = f"{failure_prelude}: {mev_error}" err_out("\n" + err_msg) return False, None From 227a5aa57ebeb57b2ceb5427000400808ee18a6e Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 10:57:03 -0800 Subject: [PATCH 14/47] use submission block directly --- bittensor_cli/src/bittensor/extrinsics/mev_shield.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index ea9da9ad9..3ba60446c 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -110,6 +110,7 @@ async def extract_mev_shield_id(response: "AsyncExtrinsicReceipt") -> Optional[s async def wait_for_mev_execution( subtensor: "SubtensorInterface", wrapper_id: str, + submit_block_hash: str, timeout_blocks: int = 4, status=None, ) -> tuple[bool, Optional[str], Optional[AsyncExtrinsicReceipt]]: @@ -123,6 +124,7 @@ async def wait_for_mev_execution( Args: subtensor: SubtensorInterface instance. wrapper_id: The ID from EncryptedSubmitted event. + submit_block_number: Block number where submit_encrypted was included. timeout_blocks: Max blocks to wait (default 4). status: Optional rich.Status object for progress updates. @@ -135,14 +137,14 @@ async def wait_for_mev_execution( async def _noop(_): return True - start_block = await subtensor.substrate.get_block_number() - current_block = start_block + 1 + starting_block = await subtensor.substrate.get_block_number(submit_block_hash) + current_block = starting_block + 1 - while current_block - start_block <= timeout_blocks: + while current_block - starting_block <= timeout_blocks: if status: status.update( f"Waiting for :shield: MEV Protection " - f"(checking block {current_block - start_block} of {timeout_blocks})..." + f"(checking block {current_block - starting_block} of {timeout_blocks})..." ) await subtensor.substrate.wait_for_block( From 0378be3786c5852b4ca63f607c5ae9c2ef01c43d Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 10:57:38 -0800 Subject: [PATCH 15/47] handle edge case --- bittensor_cli/src/bittensor/subtensor_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 2af67f2ca..a733ed63e 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -2175,8 +2175,8 @@ async def get_claimable_stakes_for_coldkey( root_stake: Balance claimable_stake: Balance for hotkey, netuid in target_pairs: - root_stake = root_stakes[hotkey] - rate = claimable_rates[hotkey].get(netuid, 0.0) + root_stake = root_stakes.get(hotkey, Balance(0)) + rate = claimable_rates.get(hotkey, {}).get(netuid, 0.0) claimable_stake = rate * root_stake already_claimed = claimed_amounts.get((hotkey, netuid), Balance(0)) net_claimable = max(claimable_stake - already_claimed, Balance(0)) From 11d8228f9003f2b75073304a510b52032555561f Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 10:58:25 -0800 Subject: [PATCH 16/47] update mev shield calls --- bittensor_cli/src/commands/stake/add.py | 4 ++-- bittensor_cli/src/commands/stake/move.py | 6 +++--- bittensor_cli/src/commands/stake/remove.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index e6d9490d5..c97743d9b 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -170,7 +170,7 @@ async def safe_stake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, status=status + subtensor, mev_shield_id, response.block_hash, status=status ) if not mev_success: status.stop() @@ -261,7 +261,7 @@ async def stake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, status=status + subtensor, mev_shield_id, response.block_hash, status=status ) if not mev_success: status.stop() diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 294e6a159..95303873e 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -595,7 +595,7 @@ async def move_stake( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, status=status + subtensor, mev_shield_id, response.block_hash, status=status ) if not mev_success: status.stop() @@ -795,7 +795,7 @@ async def transfer_stake( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, status=status + subtensor, mev_shield_id, response.block_hash, status=status ) if not mev_success: status.stop() @@ -984,7 +984,7 @@ async def swap_stake( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, status=status + subtensor, mev_shield_id, response.block_hash, status=status ) if not mev_success: status.stop() diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 91fdc69f8..0531c97ee 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -626,7 +626,7 @@ async def _unstake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, status=status + subtensor, mev_shield_id, response.block_hash, status=status ) if not mev_success: status.stop() @@ -697,7 +697,7 @@ async def _safe_unstake_extrinsic( block_hash = await subtensor.substrate.get_chain_head() - current_balance, next_nonce, current_stake, call = await asyncio.gather( + current_balance, current_stake, call = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), subtensor.get_stake( hotkey_ss58=hotkey_ss58, @@ -749,7 +749,7 @@ async def _safe_unstake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, status=status + subtensor, mev_shield_id, response.block_hash, status=status ) if not mev_success: status.stop() From 55ea2d39036cab784adc8f04ba5ed0030055495e Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 10:59:01 -0800 Subject: [PATCH 17/47] update e2e tests --- tests/e2e_tests/test_staking_sudo.py | 80 ++++++++++++++-------------- tests/e2e_tests/test_unstaking.py | 3 +- 2 files changed, 43 insertions(+), 40 deletions(-) diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 6bcaa60cc..719063d3e 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -1,6 +1,7 @@ import asyncio import json import re +import pytest from typing import Union from bittensor_cli.src.bittensor.balances import Balance @@ -23,7 +24,7 @@ * btcli sudo get """ - +@pytest.mark.parametrize("local_chain", [False], indirect=True) def test_staking(local_chain, wallet_setup): """ Test staking & sudo commands and inspect their output @@ -402,7 +403,7 @@ def test_staking(local_chain, wallet_setup): for line in show_stake_adding_single.stdout.splitlines() ] stake_added = cleaned_stake[8].split("โ”‚")[3].strip().split()[0] - assert Balance.from_tao(float(stake_added)) >= Balance.from_tao(90) + assert Balance.from_tao(float(stake_added)) >= Balance.from_tao(87) show_stake_json = exec_command_alice( command="stake", @@ -419,7 +420,7 @@ def test_staking(local_chain, wallet_setup): ) show_stake_json_output = json.loads(show_stake_json.stdout) alice_stake = show_stake_json_output["stake_info"][keypair_alice.ss58_address][0] - assert Balance.from_tao(alice_stake["stake_value"]) > Balance.from_tao(90.0) + assert Balance.from_tao(alice_stake["stake_value"]) >= Balance.from_tao(87.0) # Execute remove_stake command and remove all alpha stakes from Alice remove_stake = exec_command_alice( @@ -451,42 +452,43 @@ def test_staking(local_chain, wallet_setup): remove_stake.stdout ) - add_stake_multiple = exec_command_alice( - command="stake", - sub_command="add", - extra_args=[ - "--netuids", - ",".join(str(x) for x in multiple_netuids), - "--wallet-path", - wallet_path_alice, - "--wallet-name", - wallet_alice.name, - "--hotkey", - wallet_alice.hotkey_str, - "--chain", - "ws://127.0.0.1:9945", - "--amount", - "100", - "--tolerance", - "0.1", - "--partial", - "--no-prompt", - "--era", - "144", - "--json-output", - ], - ) - add_stake_multiple_output = json.loads(add_stake_multiple.stdout) - for netuid_ in multiple_netuids: - - def line(key: str) -> Union[str, bool]: - return add_stake_multiple_output[key][str(netuid_)][ - wallet_alice.hotkey.ss58_address - ] - - assert line("staking_success") is True - assert line("error_messages") == "" - assert isinstance(line("extrinsic_ids"), str) + # TODO: Add back when nonce stuff is updated in mev shield + # add_stake_multiple = exec_command_alice( + # command="stake", + # sub_command="add", + # extra_args=[ + # "--netuids", + # ",".join(str(x) for x in multiple_netuids), + # "--wallet-path", + # wallet_path_alice, + # "--wallet-name", + # wallet_alice.name, + # "--hotkey", + # wallet_alice.hotkey_str, + # "--chain", + # "ws://127.0.0.1:9945", + # "--amount", + # "100", + # "--tolerance", + # "0.1", + # "--partial", + # "--no-prompt", + # "--era", + # "32", + # "--json-output", + # ], + # ) + # add_stake_multiple_output = json.loads(add_stake_multiple.stdout) + # for netuid_ in multiple_netuids: + + # def line(key: str) -> Union[str, bool]: + # return add_stake_multiple_output[key][str(netuid_)][ + # wallet_alice.hotkey.ss58_address + # ] + + # assert line("staking_success") is True + # assert line("error_messages") == "" + # assert isinstance(line("extrinsic_ids"), str) # Fetch the hyperparameters of the subnet hyperparams = exec_command_alice( diff --git a/tests/e2e_tests/test_unstaking.py b/tests/e2e_tests/test_unstaking.py index f3173b5a7..072df278c 100644 --- a/tests/e2e_tests/test_unstaking.py +++ b/tests/e2e_tests/test_unstaking.py @@ -1,12 +1,13 @@ import asyncio import json import re - +import pytest from bittensor_cli.src.bittensor.balances import Balance from .utils import set_storage_extrinsic +@pytest.mark.parametrize("local_chain", [False], indirect=True) def test_unstaking(local_chain, wallet_setup): """ Test various unstaking scenarios including partial unstake, unstake all alpha, and unstake all. From 5c03f28930dcc5fe21bc11f0006ca867cb61b242 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 13:53:31 -0800 Subject: [PATCH 18/47] update mev shield working --- bittensor_cli/src/bittensor/extrinsics/mev_shield.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index 3ba60446c..54ab666c4 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -35,23 +35,21 @@ async def encrypt_call( ValueError: If MEV Shield NextKey is not available on chain. """ - next_key_result, genesis_hash, nonce = await asyncio.gather( + next_key_result, genesis_hash = await asyncio.gather( subtensor.get_mev_shield_next_key(), subtensor.substrate.get_block_hash(0), - subtensor.substrate.get_account_nonce(wallet.coldkey.ss58_address), ) if next_key_result is None: raise ValueError("MEV Shield NextKey not available on chain") - nonce = nonce + 1 # TODO: Update once chain is updated ml_kem_768_public_key = next_key_result - # Create payload_core: signer (32B) + nonce (u32 LE) + SCALE(call) + # Create payload_core: signer (32B) + next_key (32B) + SCALE(call) signer_bytes = encode_account_id(wallet.coldkey.ss58_address) - nonce_bytes = (nonce & 0xFFFFFFFF).to_bytes(4, byteorder="little") scale_call_bytes = bytes(call.data.data) + next_key = hashlib.blake2b(next_key_result, digest_size=32).digest() - payload_core = signer_bytes + nonce_bytes + scale_call_bytes + payload_core = signer_bytes + next_key + scale_call_bytes mev_shield_version = mlkem_kdf_id() genesis_hash_clean = ( From c669b1ebf7798ca091d9458d911e888b5a27c637 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 13:53:44 -0800 Subject: [PATCH 19/47] uncomment bulk staking test --- tests/e2e_tests/test_staking_sudo.py | 74 ++++++++++++++-------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 719063d3e..9ebe03688 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -24,6 +24,7 @@ * btcli sudo get """ + @pytest.mark.parametrize("local_chain", [False], indirect=True) def test_staking(local_chain, wallet_setup): """ @@ -452,43 +453,42 @@ def test_staking(local_chain, wallet_setup): remove_stake.stdout ) - # TODO: Add back when nonce stuff is updated in mev shield - # add_stake_multiple = exec_command_alice( - # command="stake", - # sub_command="add", - # extra_args=[ - # "--netuids", - # ",".join(str(x) for x in multiple_netuids), - # "--wallet-path", - # wallet_path_alice, - # "--wallet-name", - # wallet_alice.name, - # "--hotkey", - # wallet_alice.hotkey_str, - # "--chain", - # "ws://127.0.0.1:9945", - # "--amount", - # "100", - # "--tolerance", - # "0.1", - # "--partial", - # "--no-prompt", - # "--era", - # "32", - # "--json-output", - # ], - # ) - # add_stake_multiple_output = json.loads(add_stake_multiple.stdout) - # for netuid_ in multiple_netuids: - - # def line(key: str) -> Union[str, bool]: - # return add_stake_multiple_output[key][str(netuid_)][ - # wallet_alice.hotkey.ss58_address - # ] - - # assert line("staking_success") is True - # assert line("error_messages") == "" - # assert isinstance(line("extrinsic_ids"), str) + add_stake_multiple = exec_command_alice( + command="stake", + sub_command="add", + extra_args=[ + "--netuids", + ",".join(str(x) for x in multiple_netuids), + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--amount", + "100", + "--tolerance", + "0.1", + "--partial", + "--no-prompt", + "--era", + "32", + "--json-output", + ], + ) + add_stake_multiple_output = json.loads(add_stake_multiple.stdout) + for netuid_ in multiple_netuids: + + def line(key: str) -> Union[str, bool]: + return add_stake_multiple_output[key][str(netuid_)][ + wallet_alice.hotkey.ss58_address + ] + + assert line("staking_success") is True + assert line("error_messages") == "" + assert isinstance(line("extrinsic_ids"), str) # Fetch the hyperparameters of the subnet hyperparams = exec_command_alice( From 0ef2e767c839e0e5e193ddf01de30248fc9aac8f Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 14:32:23 -0800 Subject: [PATCH 20/47] dummy commit --- tests/e2e_tests/test_staking_sudo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 9ebe03688..4f5346207 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -423,7 +423,7 @@ def test_staking(local_chain, wallet_setup): alice_stake = show_stake_json_output["stake_info"][keypair_alice.ss58_address][0] assert Balance.from_tao(alice_stake["stake_value"]) >= Balance.from_tao(87.0) - # Execute remove_stake command and remove all alpha stakes from Alice + # Execute remove_stake command and remove all alpha stakes from Alice's wallet remove_stake = exec_command_alice( command="stake", sub_command="remove", From c84afe94f17ad0d33bcb044f09fc7da66f32d1f9 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 15:24:49 -0800 Subject: [PATCH 21/47] bump asi dep --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 549a8f2bc..5fe945fa8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ classifiers = [ ] dependencies = [ "wheel", - "async-substrate-interface>=1.5.2", + "async-substrate-interface>=1.5.13", "aiohttp~=3.13", "backoff~=2.2.1", "bittensor-drand>=1.2.0", From 752051dbae38c7fbd25b39db7e129a76c88bc7b2 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 15:44:38 -0800 Subject: [PATCH 22/47] update --- bittensor_cli/src/bittensor/extrinsics/mev_shield.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index 54ab666c4..ff99466af 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -183,14 +183,8 @@ async def _noop(_): extrinsic_idx=execute_revealed_index, ) - # TODO: Activate this when we update up-stream - # if not await receipt.is_success: - # error_msg = format_error_message(await receipt.error_message) - # return False, error_msg, None - - error = await check_mev_shield_error(receipt, subtensor, wrapper_id) - if error: - error_msg = format_error_message(error) + if not await receipt.is_success: + error_msg = format_error_message(await receipt.error_message) return False, error_msg, None return True, None, receipt From 47dcdaf28c9db10f48229ac97c2d2ceaaaa73018 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 16:27:18 -0800 Subject: [PATCH 23/47] update liquidity test --- tests/e2e_tests/test_liquidity.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index 7a210f0a1..4e1232248 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -1,9 +1,8 @@ +import pytest import asyncio import json -import re import time -from bittensor_cli.src.bittensor.balances import Balance from .utils import turn_off_hyperparam_freeze_window """ @@ -15,7 +14,7 @@ * btcli liquidity remove """ - +@pytest.mark.parametrize("local_chain", [False], indirect=True) def test_liquidity(local_chain, wallet_setup): wallet_path_alice = "//Alice" netuid = 2 From 14504f76a99a6de684b84b51d439937d73b144c3 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 16:29:10 -0800 Subject: [PATCH 24/47] ruff --- tests/e2e_tests/test_liquidity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index 4e1232248..eb10a7138 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -14,6 +14,7 @@ * btcli liquidity remove """ + @pytest.mark.parametrize("local_chain", [False], indirect=True) def test_liquidity(local_chain, wallet_setup): wallet_path_alice = "//Alice" From aa2f2e4f3bdc4d81823dee546c890cf72d173086 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 16:40:43 -0800 Subject: [PATCH 25/47] wip tests --- tests/e2e_tests/test_liquidity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index eb10a7138..55e4f9531 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -92,6 +92,7 @@ def test_liquidity(local_chain, wallet_setup): assert result_output["success"] is False assert f"Subnet with netuid: {netuid} is not active" in result_output["err_msg"] assert result_output["positions"] == [] + time.sleep(20) # start emissions schedule start_subnet_emissions = exec_command_alice( From 34e21219cd9795dd4067e2e732387c417e27c7a0 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 18:08:09 -0800 Subject: [PATCH 26/47] add shield to subnet creation --- bittensor_cli/src/commands/subnets/subnets.py | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index c2346880b..2ffdfd810 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -10,13 +10,18 @@ from rich.table import Column, Table from rich import box -from bittensor_cli.src import COLOR_PALETTE, Constants +from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.extrinsics.registration import ( register_extrinsic, burned_register_extrinsic, ) from bittensor_cli.src.bittensor.extrinsics.root import root_register_extrinsic +from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( + encrypt_call, + extract_mev_shield_id, + wait_for_mev_execution, +) from rich.live import Live from bittensor_cli.src.bittensor.minigraph import MiniGraph from bittensor_cli.src.commands.wallets import set_id, get_id @@ -174,6 +179,7 @@ async def _find_event_attributes_in_extrinsic_receipt( call_function=call_function, call_params=call_params, ) + call = await encrypt_call(subtensor, wallet, call) extrinsic = await substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey ) @@ -194,17 +200,28 @@ async def _find_event_attributes_in_extrinsic_receipt( await asyncio.sleep(0.5) return False, None, None - # Successful registration, final check for membership - else: - attributes = await _find_event_attributes_in_extrinsic_receipt( - response, "NetworkAdded" - ) - await print_extrinsic_id(response) - ext_id = await response.get_extrinsic_identifier() - console.print( - f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}" + # Check for MEV shield execution + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash ) - return True, int(attributes[0]), ext_id + if not mev_success: + err_console.print( + f":cross_mark: [red]Failed[/red]: MEV execution failed: {mev_error}" + ) + return False, None, None + + # Successful registration, final check for membership + attributes = await _find_event_attributes_in_extrinsic_receipt( + response, "NetworkAdded" + ) + await print_extrinsic_id(response) + ext_id = await response.get_extrinsic_identifier() + console.print( + f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}" + ) + return True, int(attributes[0]), ext_id # commands From 9ad5f2eb68543ebc0a5356dba29e2dfd09a3c82d Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 18:37:47 -0800 Subject: [PATCH 27/47] wip --- tests/e2e_tests/test_liquidity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index 55e4f9531..84829d5a2 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -92,7 +92,7 @@ def test_liquidity(local_chain, wallet_setup): assert result_output["success"] is False assert f"Subnet with netuid: {netuid} is not active" in result_output["err_msg"] assert result_output["positions"] == [] - time.sleep(20) + time.sleep(40) # start emissions schedule start_subnet_emissions = exec_command_alice( From 4ed0bfe2933a1dbbb50afed88441066858f44ebc Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 18:40:00 -0800 Subject: [PATCH 28/47] update both tests --- tests/e2e_tests/test_hyperparams_setting.py | 3 ++- tests/e2e_tests/test_set_identity.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/e2e_tests/test_hyperparams_setting.py b/tests/e2e_tests/test_hyperparams_setting.py index c336f6615..c21008bb5 100644 --- a/tests/e2e_tests/test_hyperparams_setting.py +++ b/tests/e2e_tests/test_hyperparams_setting.py @@ -1,6 +1,6 @@ import asyncio import json - +import pytest from bittensor_cli.src import HYPERPARAMS, RootSudoOnly from .utils import turn_off_hyperparam_freeze_window @@ -13,6 +13,7 @@ """ +@pytest.mark.parametrize("local_chain", [False], indirect=True) def test_hyperparams_setting(local_chain, wallet_setup): netuid = 2 wallet_path_alice = "//Alice" diff --git a/tests/e2e_tests/test_set_identity.py b/tests/e2e_tests/test_set_identity.py index 9c008cdcd..4e5311af5 100644 --- a/tests/e2e_tests/test_set_identity.py +++ b/tests/e2e_tests/test_set_identity.py @@ -1,4 +1,5 @@ import json +import pytest from unittest.mock import MagicMock, AsyncMock, patch @@ -10,6 +11,7 @@ """ +@pytest.mark.parametrize("local_chain", [False], indirect=True) def test_set_id(local_chain, wallet_setup): """ Tests that the user is prompted to confirm that the incorrect text/html URL is From 7a96cd5a4fa1bf8bf8402c7a4248cbdf1158ec54 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 19:26:19 -0800 Subject: [PATCH 29/47] update tests + statuses --- bittensor_cli/src/commands/subnets/subnets.py | 5 +++-- tests/e2e_tests/test_liquidity.py | 2 +- tests/e2e_tests/test_set_identity.py | 2 ++ tests/e2e_tests/test_wallet_interactions.py | 3 +++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 2ffdfd810..398d9fdcf 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -171,7 +171,7 @@ async def _find_event_attributes_in_extrinsic_receipt( if not unlock_key(wallet).success: return False, None, None - with console.status(":satellite: Registering subnet...", spinner="earth"): + with console.status(":satellite: Registering subnet...", spinner="earth") as status: substrate = subtensor.substrate # create extrinsic call call = await substrate.compose_call( @@ -204,9 +204,10 @@ async def _find_event_attributes_in_extrinsic_receipt( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash + subtensor, mev_shield_id, response.block_hash, status=status ) if not mev_success: + status.stop() err_console.print( f":cross_mark: [red]Failed[/red]: MEV execution failed: {mev_error}" ) diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index 84829d5a2..0f674ee01 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -30,7 +30,7 @@ def test_liquidity(local_chain, wallet_setup): print( "Skipping turning off hyperparams freeze window. This indicates the call does not exist on the chain you are testing." ) - time.sleep(10) + time.sleep(50) # Register a subnet with sudo as Alice result = exec_command_alice( diff --git a/tests/e2e_tests/test_set_identity.py b/tests/e2e_tests/test_set_identity.py index 4e5311af5..89d6b8531 100644 --- a/tests/e2e_tests/test_set_identity.py +++ b/tests/e2e_tests/test_set_identity.py @@ -1,3 +1,4 @@ +import time import json import pytest from unittest.mock import MagicMock, AsyncMock, patch @@ -25,6 +26,7 @@ def test_set_id(local_chain, wallet_setup): keypair_alice, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup( wallet_path_alice ) + time.sleep(50) # Register a subnet with sudo as Alice result = exec_command_alice( command="subnets", diff --git a/tests/e2e_tests/test_wallet_interactions.py b/tests/e2e_tests/test_wallet_interactions.py index 3b92c4965..e80a00e41 100644 --- a/tests/e2e_tests/test_wallet_interactions.py +++ b/tests/e2e_tests/test_wallet_interactions.py @@ -1,3 +1,4 @@ +import pytest from time import sleep from bittensor_cli.src.bittensor.balances import Balance @@ -24,6 +25,7 @@ """ +@pytest.mark.parametrize("local_chain", [False], indirect=True) def test_wallet_overview_inspect(local_chain, wallet_setup): """ Test the overview and inspect commands of the wallet by interaction with subnets @@ -43,6 +45,7 @@ def test_wallet_overview_inspect(local_chain, wallet_setup): # Create wallet for Alice keypair, wallet, wallet_path, exec_command = wallet_setup(wallet_path_name) + sleep(50) # Register a subnet with sudo as Alice result = exec_command( From 244da8f29cec386b553a01307ffc4d848130b19c Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 19:40:05 -0800 Subject: [PATCH 30/47] testing --- tests/e2e_tests/test_liquidity.py | 2 +- tests/e2e_tests/test_wallet_interactions.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index 0f674ee01..1a93e45ae 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -92,7 +92,7 @@ def test_liquidity(local_chain, wallet_setup): assert result_output["success"] is False assert f"Subnet with netuid: {netuid} is not active" in result_output["err_msg"] assert result_output["positions"] == [] - time.sleep(40) + time.sleep(60) # start emissions schedule start_subnet_emissions = exec_command_alice( diff --git a/tests/e2e_tests/test_wallet_interactions.py b/tests/e2e_tests/test_wallet_interactions.py index e80a00e41..eb9038218 100644 --- a/tests/e2e_tests/test_wallet_interactions.py +++ b/tests/e2e_tests/test_wallet_interactions.py @@ -45,7 +45,7 @@ def test_wallet_overview_inspect(local_chain, wallet_setup): # Create wallet for Alice keypair, wallet, wallet_path, exec_command = wallet_setup(wallet_path_name) - sleep(50) + sleep(70) # Register a subnet with sudo as Alice result = exec_command( From 121a48d354a27dcd5ea0c9b6e59402d27da3433e Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 2 Dec 2025 14:03:29 -0800 Subject: [PATCH 31/47] bump version to 9.16.0rc1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5fe945fa8..4cac3ea69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi" [project] name = "bittensor-cli" -version = "9.15.3" +version = "9.16.0rc1" description = "Bittensor CLI" readme = "README.md" authors = [ From 82915d89560c0f2b432889717ef41d32628f8a33 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 2 Dec 2025 15:44:50 -0800 Subject: [PATCH 32/47] add mev protection arg --- bittensor_cli/cli.py | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 9ad6c89df..7206b612f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -346,6 +346,12 @@ def edit_help(cls, option_name: str, help_text: str): "--dashboard.path", help="Path to save the dashboard HTML file. For example: `~/.bittensor/dashboard`.", ) + mev_protection = typer.Option( + True, + "--mev-protection/--no-mev-protection", + show_default=False, + help="Enable or disable MEV protection [dim](default: enabled)[/dim].", + ) json_output = typer.Option( False, "--json-output", @@ -3972,6 +3978,7 @@ def stake_add( rate_tolerance: Optional[float] = Options.rate_tolerance, safe_staking: Optional[bool] = Options.safe_staking, allow_partial_stake: Optional[bool] = Options.allow_partial_stake, + mev_protection: bool = Options.mev_protection, period: int = Options.period, prompt: bool = Options.prompt, quiet: bool = Options.quiet, @@ -4201,6 +4208,7 @@ def stake_add( f"rate_tolerance: {rate_tolerance}\n" f"allow_partial_stake: {allow_partial_stake}\n" f"period: {period}\n" + f"mev_protection: {mev_protection}\n" ) return self._run_command( add_stake.stake_add( @@ -4218,6 +4226,7 @@ def stake_add( allow_partial_stake, json_output, period, + mev_protection, ) ) @@ -4270,6 +4279,7 @@ def stake_remove( safe_staking: Optional[bool] = Options.safe_staking, allow_partial_stake: Optional[bool] = Options.allow_partial_stake, period: int = Options.period, + mev_protection: bool = Options.mev_protection, prompt: bool = Options.prompt, interactive: bool = typer.Option( False, @@ -4478,7 +4488,8 @@ def stake_remove( f"all_hotkeys: {all_hotkeys}\n" f"include_hotkeys: {include_hotkeys}\n" f"exclude_hotkeys: {exclude_hotkeys}\n" - f"era: {period}" + f"era: {period}\n" + f"mev_protection: {mev_protection}" ) return self._run_command( remove_stake.unstake_all( @@ -4492,6 +4503,7 @@ def stake_remove( prompt=prompt, json_output=json_output, era=period, + mev_protection=mev_protection, ) ) elif ( @@ -4544,7 +4556,8 @@ def stake_remove( f"safe_staking: {safe_staking}\n" f"rate_tolerance: {rate_tolerance}\n" f"allow_partial_stake: {allow_partial_stake}\n" - f"era: {period}" + f"era: {period}\n" + f"mev_protection: {mev_protection}\n" ) return self._run_command( @@ -4564,6 +4577,7 @@ def stake_remove( allow_partial_stake=allow_partial_stake, json_output=json_output, era=period, + mev_protection=mev_protection, ) ) @@ -4609,6 +4623,7 @@ def stake_move( False, "--stake-all", "--all", help="Stake all", prompt=False ), period: int = Options.period, + mev_protection: bool = Options.mev_protection, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -4747,6 +4762,7 @@ def stake_move( f"era: {period}\n" f"interactive_selection: {interactive_selection}\n" f"prompt: {prompt}\n" + f"mev_protection: {mev_protection}\n" ) result, ext_id = self._run_command( move_stake.move_stake( @@ -4761,6 +4777,7 @@ def stake_move( era=period, interactive_selection=interactive_selection, prompt=prompt, + mev_protection=mev_protection, ) ) if json_output: @@ -4801,6 +4818,7 @@ def stake_transfer( stake_all: bool = typer.Option( False, "--stake-all", "--all", help="Stake all", prompt=False ), + mev_protection: bool = Options.mev_protection, period: int = Options.period, prompt: bool = Options.prompt, quiet: bool = Options.quiet, @@ -4933,7 +4951,8 @@ def stake_transfer( f"dest_coldkey_ss58: {dest_ss58}\n" f"amount: {amount}\n" f"era: {period}\n" - f"stake_all: {stake_all}" + f"stake_all: {stake_all}\n" + f"mev_protection: {mev_protection}" ) result, ext_id = self._run_command( move_stake.transfer_stake( @@ -4948,6 +4967,7 @@ def stake_transfer( interactive_selection=interactive_selection, stake_all=stake_all, prompt=prompt, + mev_protection=mev_protection, ) ) if json_output: @@ -4992,6 +5012,7 @@ def stake_swap( prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, + mev_protection: bool = Options.mev_protection, quiet: bool = Options.quiet, verbose: bool = Options.verbose, json_output: bool = Options.json_output, @@ -5056,6 +5077,7 @@ def stake_swap( f"prompt: {prompt}\n" f"wait_for_inclusion: {wait_for_inclusion}\n" f"wait_for_finalization: {wait_for_finalization}\n" + f"mev_protection: {mev_protection}\n" ) result, ext_id = self._run_command( move_stake.swap_stake( @@ -5070,6 +5092,7 @@ def stake_swap( prompt=prompt, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + mev_protection=mev_protection, ) ) if json_output: @@ -5085,6 +5108,7 @@ def stake_wizard( wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, period: int = Options.period, + mev_protection: bool = Options.mev_protection, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -5150,6 +5174,7 @@ def stake_wizard( era=period, interactive_selection=False, prompt=prompt, + mev_protection=mev_protection, ) ) elif operation == "transfer": @@ -5179,6 +5204,7 @@ def stake_wizard( era=period, interactive_selection=False, prompt=prompt, + mev_protection=mev_protection, ) ) elif operation == "swap": @@ -5194,6 +5220,7 @@ def stake_wizard( era=period, interactive_selection=False, prompt=prompt, + mev_protection=mev_protection, ) ) else: @@ -6427,6 +6454,7 @@ def subnets_create( additional_info: Optional[str] = typer.Option( None, "--additional-info", help="Additional information" ), + mev_protection: bool = Options.mev_protection, json_output: bool = Options.json_output, prompt: bool = Options.prompt, quiet: bool = Options.quiet, @@ -6472,7 +6500,12 @@ def subnets_create( logger.debug(f"args:\nnetwork: {network}\nidentity: {identity}\n") self._run_command( subnets.create( - wallet, self.initialize_chain(network), identity, json_output, prompt + wallet, + self.initialize_chain(network), + identity, + json_output, + prompt, + mev_protection, ) ) From 79a7682bdf686853ebebf9eb53c6cbb19ff56d37 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 2 Dec 2025 15:51:17 -0800 Subject: [PATCH 33/47] add optional arg to add & rem stake --- bittensor_cli/src/commands/stake/add.py | 49 +++++++------- bittensor_cli/src/commands/stake/remove.py | 74 +++++++++++++++------- 2 files changed, 77 insertions(+), 46 deletions(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index c97743d9b..d88692261 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -51,6 +51,7 @@ async def stake_add( allow_partial_stake: bool, json_output: bool, era: int, + mev_protection: bool, ): """ Args: @@ -139,7 +140,8 @@ async def safe_stake_extrinsic( }, ), ) - call = await encrypt_call(subtensor, wallet, call) + if mev_protection: + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, @@ -167,16 +169,17 @@ async def safe_stake_extrinsic( err_out("\n" + err_msg) return False, err_msg, None - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, err_msg, None + if mev_protection: + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash, status=status + ) + if not mev_success: + status.stop() + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, err_msg, None if json_output: # the rest of this checking is not necessary if using json_output @@ -241,7 +244,8 @@ async def stake_extrinsic( failure_prelude = ( f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid_i}" ) - call = await encrypt_call(subtensor, wallet, call) + if mev_protection: + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} ) @@ -258,16 +262,17 @@ async def stake_extrinsic( err_out("\n" + err_msg) return False, err_msg, None - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, err_msg, None + if mev_protection: + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash, status=status + ) + if not mev_success: + status.stop() + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, err_msg, None if json_output: # the rest of this is not necessary if using json_output diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 0531c97ee..afd9310da 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -53,6 +53,7 @@ async def unstake( allow_partial_stake: bool, json_output: bool, era: int, + mev_protection: bool, ): """Unstake from hotkey(s).""" @@ -331,6 +332,7 @@ async def unstake( "hotkey_ss58": op["hotkey_ss58"], "status": status, "era": era, + "mev_protection": mev_protection, } if safe_staking and op["netuid"] != 0: @@ -375,6 +377,7 @@ async def unstake_all( era: int = 3, prompt: bool = True, json_output: bool = False, + mev_protection: bool = True, ) -> None: """Unstakes all stakes from all hotkeys in all subnets.""" include_hotkeys = include_hotkeys or [] @@ -551,6 +554,7 @@ async def unstake_all( unstake_all_alpha=unstake_all_alpha, status=status, era=era, + mev_protection=mev_protection, ) ext_id = await ext_receipt.get_extrinsic_identifier() if success else None successes[hotkey_ss58] = { @@ -571,6 +575,7 @@ async def _unstake_extrinsic( hotkey_ss58: str, status=None, era: int = 3, + mev_protection: bool = True, ) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """Execute a standard unstake extrinsic. @@ -607,7 +612,8 @@ async def _unstake_extrinsic( ), ) - call = await encrypt_call(subtensor, wallet, call) + if mev_protection: + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, era={"period": era} ) @@ -623,16 +629,17 @@ async def _unstake_extrinsic( ) return False, None - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, None + if mev_protection: + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash, status=status + ) + if not mev_success: + status.stop() + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, None # Fetch latest balance and stake await print_extrinsic_id(response) @@ -672,6 +679,7 @@ async def _safe_unstake_extrinsic( allow_partial_stake: bool, status=None, era: int = 3, + mev_protection: bool = True, ) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """Execute a safe unstake extrinsic with price limit. @@ -719,7 +727,8 @@ async def _safe_unstake_extrinsic( ), ) - call = await encrypt_call(subtensor, wallet, call) + if mev_protection: + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, era={"period": era} ) @@ -746,16 +755,17 @@ async def _safe_unstake_extrinsic( ) return False, None - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, None + if mev_protection: + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash, status=status + ) + if not mev_success: + status.stop() + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, None await print_extrinsic_id(response) block_hash = await subtensor.substrate.get_chain_head() @@ -798,6 +808,7 @@ async def _unstake_all_extrinsic( unstake_all_alpha: bool, status=None, era: int = 3, + mev_protection: bool = True, ) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """Execute an unstake all extrinsic. @@ -845,6 +856,9 @@ async def _unstake_all_extrinsic( call_params={"hotkey": hotkey_ss58}, ) + if mev_protection: + call = await encrypt_call(subtensor, wallet, call) + try: response = await subtensor.substrate.submit_extrinsic( extrinsic=await subtensor.substrate.create_signed_extrinsic( @@ -860,8 +874,20 @@ async def _unstake_all_extrinsic( f"{format_error_message(await response.error_message)}" ) return False, None - else: - await print_extrinsic_id(response) + + if mev_protection: + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash, status=status + ) + if not mev_success: + status.stop() + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, None + + await print_extrinsic_id(response) # Fetch latest balance and stake block_hash = await subtensor.substrate.get_chain_head() From 341140a9488e2dc3547afc9d7e3fd9666efb359f Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 2 Dec 2025 15:54:08 -0800 Subject: [PATCH 34/47] update movement cmds --- bittensor_cli/src/commands/stake/move.py | 69 +++++++++++++----------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 95303873e..a7f3f0782 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -463,6 +463,7 @@ async def move_stake( era: int, interactive_selection: bool = False, prompt: bool = True, + mev_protection: bool = True, ) -> tuple[bool, str]: if interactive_selection: try: @@ -584,7 +585,8 @@ async def move_stake( f"[blue]{origin_netuid}[/blue] \nto " f"[blue]{destination_hotkey}[/blue] on netuid: [blue]{destination_netuid}[/blue] ..." ) as status: - call = await encrypt_call(subtensor, wallet, call) + if mev_protection: + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, era={"period": era} ) @@ -592,15 +594,16 @@ async def move_stake( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") - return False, "" + if mev_protection: + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash, status=status + ) + if not mev_success: + status.stop() + err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") + return False, "" ext_id = await response.get_extrinsic_identifier() @@ -661,6 +664,7 @@ async def transfer_stake( interactive_selection: bool = False, stake_all: bool = False, prompt: bool = True, + mev_protection: bool = True, ) -> tuple[bool, str]: """Transfers stake from one network to another. @@ -783,7 +787,8 @@ async def transfer_stake( return False, "" with console.status("\n:satellite: Transferring stake ...") as status: - call = await encrypt_call(subtensor, wallet, call) + if mev_protection: + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, era={"period": era} ) @@ -792,15 +797,16 @@ async def transfer_stake( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") - return False, "" + if mev_protection: + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash, status=status + ) + if not mev_success: + status.stop() + err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") + return False, "" ext_id = await response.get_extrinsic_identifier() @@ -852,6 +858,7 @@ async def swap_stake( prompt: bool = True, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + mev_protection: bool = True, ) -> tuple[bool, str]: """Swaps stake between subnets while keeping the same coldkey-hotkey pair ownership. @@ -970,7 +977,8 @@ async def swap_stake( f"\n:satellite: Swapping stake from netuid [blue]{origin_netuid}[/blue] " f"to netuid [blue]{destination_netuid}[/blue]..." ) as status: - call = await encrypt_call(subtensor, wallet, call) + if mev_protection: + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, era={"period": era} ) @@ -981,15 +989,16 @@ async def swap_stake( wait_for_finalization=wait_for_finalization, ) - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") - return False, "" + if mev_protection: + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash, status=status + ) + if not mev_success: + status.stop() + err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") + return False, "" ext_id = await response.get_extrinsic_identifier() From 57035e90a9ef3165e495f2e8f6ced72f015d71d7 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 2 Dec 2025 15:54:55 -0800 Subject: [PATCH 35/47] update sn creation --- bittensor_cli/src/commands/subnets/subnets.py | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 398d9fdcf..e2cc7bbd7 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -61,6 +61,7 @@ async def register_subnetwork_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = False, + mev_protection: bool = True, ) -> tuple[bool, Optional[int], Optional[str]]: """Registers a new subnetwork. @@ -179,7 +180,8 @@ async def _find_event_attributes_in_extrinsic_receipt( call_function=call_function, call_params=call_params, ) - call = await encrypt_call(subtensor, wallet, call) + if mev_protection: + call = await encrypt_call(subtensor, wallet, call) extrinsic = await substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey ) @@ -201,17 +203,18 @@ async def _find_event_attributes_in_extrinsic_receipt( return False, None, None # Check for MEV shield execution - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_console.print( - f":cross_mark: [red]Failed[/red]: MEV execution failed: {mev_error}" + if mev_protection: + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash, status=status ) - return False, None, None + if not mev_success: + status.stop() + err_console.print( + f":cross_mark: [red]Failed[/red]: MEV execution failed: {mev_error}" + ) + return False, None, None # Successful registration, final check for membership attributes = await _find_event_attributes_in_extrinsic_receipt( @@ -1640,12 +1643,13 @@ async def create( subnet_identity: dict, json_output: bool, prompt: bool, + mev_protection: bool = True, ): """Register a subnetwork""" # Call register command. success, netuid, ext_id = await register_subnetwork_extrinsic( - subtensor, wallet, subnet_identity, prompt=prompt + subtensor, wallet, subnet_identity, prompt=prompt, mev_protection=mev_protection ) if json_output: # technically, netuid can be `None`, but only if not wait for finalization/inclusion. However, as of present From 7f2bcae017f9f8692a17480d3d64a5f7e481dcac Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 2 Dec 2025 18:59:22 -0800 Subject: [PATCH 36/47] update tests to use fastblocks --- tests/e2e_tests/test_liquidity.py | 7 +++---- tests/e2e_tests/test_set_identity.py | 6 ++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index 1a93e45ae..e97e1b6b4 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -1,4 +1,3 @@ -import pytest import asyncio import json import time @@ -15,7 +14,6 @@ """ -@pytest.mark.parametrize("local_chain", [False], indirect=True) def test_liquidity(local_chain, wallet_setup): wallet_path_alice = "//Alice" netuid = 2 @@ -30,7 +28,6 @@ def test_liquidity(local_chain, wallet_setup): print( "Skipping turning off hyperparams freeze window. This indicates the call does not exist on the chain you are testing." ) - time.sleep(50) # Register a subnet with sudo as Alice result = exec_command_alice( @@ -63,6 +60,7 @@ def test_liquidity(local_chain, wallet_setup): "https://testsubnet.com/logo.png", "--no-prompt", "--json-output", + "--no-mev-protection", ], ) result_output = json.loads(result.stdout) @@ -92,7 +90,7 @@ def test_liquidity(local_chain, wallet_setup): assert result_output["success"] is False assert f"Subnet with netuid: {netuid} is not active" in result_output["err_msg"] assert result_output["positions"] == [] - time.sleep(60) + time.sleep(40) # start emissions schedule start_subnet_emissions = exec_command_alice( @@ -138,6 +136,7 @@ def test_liquidity(local_chain, wallet_setup): "--no-prompt", "--era", "144", + "--no-mev-protection", ], ) assert "โœ… Finalized" in stake_to_enable_v3.stdout, stake_to_enable_v3.stderr diff --git a/tests/e2e_tests/test_set_identity.py b/tests/e2e_tests/test_set_identity.py index 89d6b8531..345927785 100644 --- a/tests/e2e_tests/test_set_identity.py +++ b/tests/e2e_tests/test_set_identity.py @@ -1,6 +1,4 @@ -import time import json -import pytest from unittest.mock import MagicMock, AsyncMock, patch @@ -12,7 +10,6 @@ """ -@pytest.mark.parametrize("local_chain", [False], indirect=True) def test_set_id(local_chain, wallet_setup): """ Tests that the user is prompted to confirm that the incorrect text/html URL is @@ -26,7 +23,7 @@ def test_set_id(local_chain, wallet_setup): keypair_alice, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup( wallet_path_alice ) - time.sleep(50) + # Register a subnet with sudo as Alice result = exec_command_alice( command="subnets", @@ -58,6 +55,7 @@ def test_set_id(local_chain, wallet_setup): "https://testsubnet.com/logo.png", "--no-prompt", "--json-output", + "--no-mev-protection", ], ) result_output = json.loads(result.stdout) From 6076bdf7f8191574549fe5ba1dea887d46ceb18e Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 3 Dec 2025 15:05:01 -0800 Subject: [PATCH 37/47] init setup --- tests/e2e_tests/test_stake_movement.py | 168 +++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 tests/e2e_tests/test_stake_movement.py diff --git a/tests/e2e_tests/test_stake_movement.py b/tests/e2e_tests/test_stake_movement.py new file mode 100644 index 000000000..e35a524f0 --- /dev/null +++ b/tests/e2e_tests/test_stake_movement.py @@ -0,0 +1,168 @@ +import asyncio +import json +import pytest + +from .utils import set_storage_extrinsic + + +@pytest.mark.parametrize("local_chain", [False], indirect=True) +def test_stake_movement(local_chain, wallet_setup): + """ + Exercise stake move, transfer, and swap flows across subnets using Alice and Bob. + + Steps: + 0. Initial setup: Make alice own SN 0, create SN2, SN3, SN4, start emissions on all subnets. + 1. Activation: Register Bob on subnets 2 and 3; add initial stake for V3 activation. + 2. Move: Move stake from Alice's hotkey on netuid 2 to Bob's hotkey on netuid 3. + 3. Transfer: Transfer all root (netuid 0) stake from Alice's coldkey to Bob's coldkey. + 4. Swap: Swap Alice's stake from netuid 4 to the root netuid. + + Note: + - All movement commands executed with mev shield + - Stake commands executed without shield to speed up tests + - Shield for stake commands is already covered in its own test + """ + print("Testing stake movement commands ๐Ÿงช") + + wallet_path_alice = "//Alice" + wallet_path_bob = "//Bob" + + keypair_alice, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup( + wallet_path_alice + ) + keypair_bob, wallet_bob, wallet_path_bob, exec_command_bob = wallet_setup( + wallet_path_bob + ) + + # Force Alice to own SN0 by setting storage + sn0_owner_storage_items = [ + ( + bytes.fromhex( + "658faa385070e074c85bf6b568cf055536e3e82152c8758267395fe524fbbd160000" + ), + bytes.fromhex( + "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + ), + ) + ] + asyncio.run( + set_storage_extrinsic( + local_chain, + wallet=wallet_alice, + items=sn0_owner_storage_items, + ) + ) + + # Create SN2, SN3, SN4 for move/transfer/swap checks + subnets_to_create = [2, 3, 4] + for netuid in subnets_to_create: + create_subnet_result = exec_command_alice( + command="subnets", + sub_command="create", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--subnet-name", + "Test Subnet", + "--repo", + "https://github.com/username/repo", + "--contact", + "alice@opentensor.dev", + "--url", + "https://testsubnet.com", + "--discord", + "alice#1234", + "--description", + "A test subnet for e2e testing", + "--additional-info", + "Created by Alice", + "--logo-url", + "https://testsubnet.com/logo.png", + "--no-prompt", + "--json-output", + ], + ) + create_subnet_payload = json.loads(create_subnet_result.stdout) + assert create_subnet_payload["success"] is True + assert create_subnet_payload["netuid"] == netuid + + # Start emission schedule for subnets (including root netuid 0) + for netuid in [0] + subnets_to_create: + start_emission_result = exec_command_alice( + command="subnets", + sub_command="start", + extra_args=[ + "--netuid", + str(netuid), + "--wallet-name", + wallet_alice.name, + "--no-prompt", + "--chain", + "ws://127.0.0.1:9945", + "--wallet-path", + wallet_path_alice, + ], + ) + assert ( + f"Successfully started subnet {netuid}'s emission schedule." + in start_emission_result.stdout + ) + + # Alice is already registered - register Bob on the two non-root subnets + for netuid in [2, 3]: + register_bob_result = exec_command_bob( + command="subnets", + sub_command="register", + extra_args=[ + "--netuid", + str(netuid), + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--hotkey", + wallet_bob.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + ], + ) + assert "โœ… Registered" in register_bob_result.stdout, register_bob_result.stderr + assert "Your extrinsic has been included" in register_bob_result.stdout, ( + register_bob_result.stdout + ) + + # Add initial stake to enable V3 (1 TAO) on all created subnets + for netuid in [2, 3, 4]: + add_initial_stake_result = exec_command_alice( + command="stake", + sub_command="add", + extra_args=[ + "--netuid", + str(netuid), + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--amount", + "1", + "--unsafe", + "--no-prompt", + "--era", + "144", + "--no-mev-protection", + ], + ) + assert "โœ… Finalized" in add_initial_stake_result.stdout, ( + add_initial_stake_result.stderr + ) From d642f414944c5d4f91b2ebe0f2c678ec42fc87a7 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 3 Dec 2025 15:11:46 -0800 Subject: [PATCH 38/47] move stake test --- tests/e2e_tests/test_stake_movement.py | 109 +++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/tests/e2e_tests/test_stake_movement.py b/tests/e2e_tests/test_stake_movement.py index e35a524f0..b27be4ee4 100644 --- a/tests/e2e_tests/test_stake_movement.py +++ b/tests/e2e_tests/test_stake_movement.py @@ -166,3 +166,112 @@ def test_stake_movement(local_chain, wallet_setup): assert "โœ… Finalized" in add_initial_stake_result.stdout, ( add_initial_stake_result.stderr ) + + ############################ + # TEST 1: Move stake command + # Move stake between hotkeys while keeping the same coldkey + ############################ + + # Add 25 TAO stake for move test for Alice + add_move_stake_result = exec_command_alice( + command="stake", + sub_command="add", + extra_args=[ + "--netuid", + "2", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--amount", + "25", + "--no-prompt", + "--era", + "144", + "--unsafe", + "--no-mev-protection", + ], + ) + assert "โœ… Finalized" in add_move_stake_result.stdout, add_move_stake_result.stderr + + # List Alice's stakes prior to the move + alice_stake_before_move = exec_command_alice( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + + # Check Alice's stakes before move to ensure sufficient stake on netuid 2 + alice_stake_list_before_move = json.loads(alice_stake_before_move.stdout) + alice_stakes_before_move = alice_stake_list_before_move.get("stake_info", {}) + for hotkey_ss58, stakes in alice_stakes_before_move.items(): + if hotkey_ss58 == wallet_alice.hotkey.ss58_address: + for stake in stakes: + if stake["netuid"] == 2: + assert stake["stake_value"] >= int(20) + + # Move stake from Alice's hotkey on netuid 2 -> Bob's hotkey on netuid 3 + move_amount = 20 + move_result = exec_command_alice( + command="stake", + sub_command="move", + extra_args=[ + "--origin-netuid", + "2", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--dest-netuid", + "3", + "--dest", + wallet_bob.hotkey.ss58_address, + "--amount", + move_amount, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + ], + ) + assert "โœ… Sent" in move_result.stdout + + # Check Alice's stakes after move + alice_stake_after_move = exec_command_alice( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + # Assert stake was moved from Alice's hotkey on netuid 2 -> Bob's hotkey on netuid 3 + alice_stake_list_after_move = json.loads(alice_stake_after_move.stdout) + alice_stakes_after_move = alice_stake_list_after_move.get("stake_info", {}) + for hotkey_ss58, stakes in alice_stakes_after_move.items(): + if hotkey_ss58 == wallet_bob.hotkey.ss58_address: + for stake in stakes: + if stake["netuid"] == 3: + assert stake["stake_value"] >= int(move_amount) From 8ff6266c6a3b081eeb372eea8787f55b5443b3d7 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 3 Dec 2025 15:12:06 -0800 Subject: [PATCH 39/47] test transfer stake --- tests/e2e_tests/test_stake_movement.py | 123 +++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/tests/e2e_tests/test_stake_movement.py b/tests/e2e_tests/test_stake_movement.py index b27be4ee4..684e664e2 100644 --- a/tests/e2e_tests/test_stake_movement.py +++ b/tests/e2e_tests/test_stake_movement.py @@ -275,3 +275,126 @@ def test_stake_movement(local_chain, wallet_setup): for stake in stakes: if stake["netuid"] == 3: assert stake["stake_value"] >= int(move_amount) + + ################################ + # TEST 2: Transfer stake command + # Transfer stake between coldkeys while keeping the same hotkey + ################################ + + transfer_amount = 20 + transfer_fund_root_result = exec_command_alice( + command="stake", + sub_command="add", + extra_args=[ + "--netuid", + "0", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--amount", + transfer_amount, + "--no-prompt", + "--era", + "144", + "--unsafe", + "--no-mev-protection", + ], + ) + assert "โœ… Finalized" in transfer_fund_root_result.stdout, ( + transfer_fund_root_result.stderr + ) + + # Ensure Bob doesn't have any stake in root netuid before transfer + bob_stake_list_before_transfer = exec_command_bob( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + assert bob_stake_list_before_transfer.stdout == "" + + # Transfer stake from Alice's coldkey on netuid 0 -> Bob's coldkey on netuid 0 + transfer_result = exec_command_alice( + command="stake", + sub_command="transfer", + extra_args=[ + "--origin-netuid", + "0", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--dest-netuid", + "0", + "--dest", + wallet_bob.coldkeypub.ss58_address, + "--all", + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + ], + ) + assert "โœ… Sent" in transfer_result.stdout + + # Check Bob's stakes after transfer + bob_stake_list_after_transfer = exec_command_bob( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + bob_stake_list_after_transfer = json.loads(bob_stake_list_after_transfer.stdout) + bob_stakes_after_transfer = bob_stake_list_after_transfer.get("stake_info", {}) + for hotkey_ss58, stakes in bob_stakes_after_transfer.items(): + for stake in stakes: + if stake["netuid"] == 0: + assert stake["stake_value"] >= int(transfer_amount) + + # Check Alice's stakes after transfer + alice_stake_list_after_transfer = exec_command_alice( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + + alice_stake_list_after_transfer = json.loads(alice_stake_list_after_transfer.stdout) + alice_stakes_after_transfer = alice_stake_list_after_transfer.get("stake_info", {}) + for hotkey_ss58, stakes in alice_stakes_after_transfer.items(): + for stake in stakes: + if stake["netuid"] == 0: + pytest.fail("Stake found in root netuid after transfer") From 34c384f4704f465bf26bdd89d3c79f41ac3b89b5 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 3 Dec 2025 15:12:15 -0800 Subject: [PATCH 40/47] test swap stake --- tests/e2e_tests/test_stake_movement.py | 109 +++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/tests/e2e_tests/test_stake_movement.py b/tests/e2e_tests/test_stake_movement.py index 684e664e2..b2660fadb 100644 --- a/tests/e2e_tests/test_stake_movement.py +++ b/tests/e2e_tests/test_stake_movement.py @@ -398,3 +398,112 @@ def test_stake_movement(local_chain, wallet_setup): for stake in stakes: if stake["netuid"] == 0: pytest.fail("Stake found in root netuid after transfer") + + ################################ + # TEST 3: Swap stake command + # Swap stake between subnets while keeping the same coldkey-hotkey pair + ################################ + + swap_seed_stake_result = exec_command_alice( + command="stake", + sub_command="add", + extra_args=[ + "--netuid", + "4", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--amount", + "25", + "--no-prompt", + "--era", + "144", + "--unsafe", + "--no-mev-protection", + ], + ) + assert "โœ… Finalized" in swap_seed_stake_result.stdout, ( + swap_seed_stake_result.stderr + ) + + # Ensure stake was added to Alice's hotkey on netuid 4 + alice_stake_list_before_swap_cmd = exec_command_alice( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + + alice_stake_list_before_swap = json.loads(alice_stake_list_before_swap_cmd.stdout) + alice_stakes_before_swap = alice_stake_list_before_swap.get("stake_info", {}) + found_stake_in_netuid_4 = False + for hotkey_ss58, stakes in alice_stakes_before_swap.items(): + for stake in stakes: + if stake["netuid"] == 4: + found_stake_in_netuid_4 = True + break + if not found_stake_in_netuid_4: + pytest.fail("Stake not found in netuid 4 before swap") + + # Swap stake from Alice's hotkey on netuid 4 -> Bob's hotkey on netuid 0 + swap_result = exec_command_alice( + command="stake", + sub_command="swap", + extra_args=[ + "--origin-netuid", + "4", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--dest-netuid", + "0", + "--all", + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + ], + ) + assert "โœ… Sent" in swap_result.stdout, swap_result.stderr + + # Check Alice's stakes after swap + alice_stake_list_after_swap_cmd = exec_command_alice( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + + alice_stake_list_after_swap = json.loads(alice_stake_list_after_swap_cmd.stdout) + alice_stakes_after_swap = alice_stake_list_after_swap.get("stake_info", {}) + for hotkey_ss58, stakes in alice_stakes_after_swap.items(): + for stake in stakes: + if stake["netuid"] == 4: + pytest.fail("Stake found in netuid 4 after swap") + + print("Passed stake movement commands") From 31ffd5eb8e8f3b9b82058b7b7d225a82237ea6a6 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 3 Dec 2025 16:19:22 -0800 Subject: [PATCH 41/47] improvement --- tests/e2e_tests/test_stake_movement.py | 74 +++++++++++++------------- tests/e2e_tests/utils.py | 28 ++++++++++ 2 files changed, 66 insertions(+), 36 deletions(-) diff --git a/tests/e2e_tests/test_stake_movement.py b/tests/e2e_tests/test_stake_movement.py index b2660fadb..eb9a22b0e 100644 --- a/tests/e2e_tests/test_stake_movement.py +++ b/tests/e2e_tests/test_stake_movement.py @@ -2,7 +2,7 @@ import json import pytest -from .utils import set_storage_extrinsic +from .utils import find_stake_entries, set_storage_extrinsic @pytest.mark.parametrize("local_chain", [False], indirect=True) @@ -217,12 +217,13 @@ def test_stake_movement(local_chain, wallet_setup): # Check Alice's stakes before move to ensure sufficient stake on netuid 2 alice_stake_list_before_move = json.loads(alice_stake_before_move.stdout) - alice_stakes_before_move = alice_stake_list_before_move.get("stake_info", {}) - for hotkey_ss58, stakes in alice_stakes_before_move.items(): - if hotkey_ss58 == wallet_alice.hotkey.ss58_address: - for stake in stakes: - if stake["netuid"] == 2: - assert stake["stake_value"] >= int(20) + alice_stakes_before_move = find_stake_entries( + alice_stake_list_before_move, + netuid=2, + hotkey_ss58=wallet_alice.hotkey.ss58_address, + ) + for stake in alice_stakes_before_move: + assert stake["stake_value"] >= int(20) # Move stake from Alice's hotkey on netuid 2 -> Bob's hotkey on netuid 3 move_amount = 20 @@ -269,12 +270,13 @@ def test_stake_movement(local_chain, wallet_setup): ) # Assert stake was moved from Alice's hotkey on netuid 2 -> Bob's hotkey on netuid 3 alice_stake_list_after_move = json.loads(alice_stake_after_move.stdout) - alice_stakes_after_move = alice_stake_list_after_move.get("stake_info", {}) - for hotkey_ss58, stakes in alice_stakes_after_move.items(): - if hotkey_ss58 == wallet_bob.hotkey.ss58_address: - for stake in stakes: - if stake["netuid"] == 3: - assert stake["stake_value"] >= int(move_amount) + bob_stakes_after_move = find_stake_entries( + alice_stake_list_after_move, + netuid=3, + hotkey_ss58=wallet_bob.hotkey.ss58_address, + ) + for stake in bob_stakes_after_move: + assert stake["stake_value"] >= move_amount ################################ # TEST 2: Transfer stake command @@ -369,11 +371,12 @@ def test_stake_movement(local_chain, wallet_setup): ], ) bob_stake_list_after_transfer = json.loads(bob_stake_list_after_transfer.stdout) - bob_stakes_after_transfer = bob_stake_list_after_transfer.get("stake_info", {}) - for hotkey_ss58, stakes in bob_stakes_after_transfer.items(): - for stake in stakes: - if stake["netuid"] == 0: - assert stake["stake_value"] >= int(transfer_amount) + bob_stakes_after_transfer = find_stake_entries( + bob_stake_list_after_transfer, + netuid=0, + ) + for stake in bob_stakes_after_transfer: + assert stake["stake_value"] >= transfer_amount # Check Alice's stakes after transfer alice_stake_list_after_transfer = exec_command_alice( @@ -393,11 +396,12 @@ def test_stake_movement(local_chain, wallet_setup): ) alice_stake_list_after_transfer = json.loads(alice_stake_list_after_transfer.stdout) - alice_stakes_after_transfer = alice_stake_list_after_transfer.get("stake_info", {}) - for hotkey_ss58, stakes in alice_stakes_after_transfer.items(): - for stake in stakes: - if stake["netuid"] == 0: - pytest.fail("Stake found in root netuid after transfer") + alice_stakes_after_transfer = find_stake_entries( + alice_stake_list_after_transfer, + netuid=0, + ) + if alice_stakes_after_transfer: + pytest.fail("Stake found in root netuid after transfer") ################################ # TEST 3: Swap stake command @@ -449,14 +453,11 @@ def test_stake_movement(local_chain, wallet_setup): ) alice_stake_list_before_swap = json.loads(alice_stake_list_before_swap_cmd.stdout) - alice_stakes_before_swap = alice_stake_list_before_swap.get("stake_info", {}) - found_stake_in_netuid_4 = False - for hotkey_ss58, stakes in alice_stakes_before_swap.items(): - for stake in stakes: - if stake["netuid"] == 4: - found_stake_in_netuid_4 = True - break - if not found_stake_in_netuid_4: + alice_stakes_before_swap = find_stake_entries( + alice_stake_list_before_swap, + netuid=4, + ) + if not alice_stakes_before_swap: pytest.fail("Stake not found in netuid 4 before swap") # Swap stake from Alice's hotkey on netuid 4 -> Bob's hotkey on netuid 0 @@ -500,10 +501,11 @@ def test_stake_movement(local_chain, wallet_setup): ) alice_stake_list_after_swap = json.loads(alice_stake_list_after_swap_cmd.stdout) - alice_stakes_after_swap = alice_stake_list_after_swap.get("stake_info", {}) - for hotkey_ss58, stakes in alice_stakes_after_swap.items(): - for stake in stakes: - if stake["netuid"] == 4: - pytest.fail("Stake found in netuid 4 after swap") + alice_stakes_after_swap = find_stake_entries( + alice_stake_list_after_swap, + netuid=4, + ) + if alice_stakes_after_swap: + pytest.fail("Stake found in netuid 4 after swap") print("Passed stake movement commands") diff --git a/tests/e2e_tests/utils.py b/tests/e2e_tests/utils.py index 323797356..effd4cef9 100644 --- a/tests/e2e_tests/utils.py +++ b/tests/e2e_tests/utils.py @@ -132,6 +132,34 @@ def extract_coldkey_balance( } +def find_stake_entries( + stake_payload: dict, netuid: int, hotkey_ss58: str | None = None +) -> list[dict]: + """ + Return stake entries matching a given netuid, optionally scoped to a specific hotkey. + Requires json payload using `--json-output` flag. + + Args: + stake_payload: Parsed JSON payload containing `stake_info`. + netuid: The subnet identifier to filter on. + hotkey_ss58: Optional hotkey address to further narrow results. + + Returns: + A list of stake dicts matching the criteria (may be empty). + """ + stake_info = stake_payload.get("stake_info", {}) or {} + matching_stakes: list[dict] = [] + + for stake_hotkey, stakes in stake_info.items(): + if hotkey_ss58 and stake_hotkey != hotkey_ss58: + continue + for stake in stakes or []: + if stake.get("netuid") == netuid: + matching_stakes.append(stake) + + return matching_stakes + + def verify_subnet_entry(output_text: str, netuid: str, ss58_address: str) -> bool: """ Verifies the presence of a specific subnet entry subnets list output. From 6799a229e996323b99753ef3ee44bd9ceedab07c Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 3 Dec 2025 16:19:40 -0800 Subject: [PATCH 42/47] test_wallet_overview_inspect non fast-blocks --- tests/e2e_tests/test_wallet_interactions.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/e2e_tests/test_wallet_interactions.py b/tests/e2e_tests/test_wallet_interactions.py index eb9038218..ae23c5b0e 100644 --- a/tests/e2e_tests/test_wallet_interactions.py +++ b/tests/e2e_tests/test_wallet_interactions.py @@ -1,4 +1,3 @@ -import pytest from time import sleep from bittensor_cli.src.bittensor.balances import Balance @@ -25,7 +24,6 @@ """ -@pytest.mark.parametrize("local_chain", [False], indirect=True) def test_wallet_overview_inspect(local_chain, wallet_setup): """ Test the overview and inspect commands of the wallet by interaction with subnets @@ -45,7 +43,6 @@ def test_wallet_overview_inspect(local_chain, wallet_setup): # Create wallet for Alice keypair, wallet, wallet_path, exec_command = wallet_setup(wallet_path_name) - sleep(70) # Register a subnet with sudo as Alice result = exec_command( @@ -77,6 +74,7 @@ def test_wallet_overview_inspect(local_chain, wallet_setup): "--additional-info", "Test subnet", "--no-prompt", + "--no-mev-protection", ], ) assert f"โœ… Registered subnetwork with netuid: {netuid}" in result.stdout From 6a40b71df36b20a20c7bdf0706958907f966c2af Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 4 Dec 2025 16:06:39 -0800 Subject: [PATCH 43/47] add get_mev_shield_current_key --- .../src/bittensor/subtensor_interface.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index a733ed63e..0566f14e8 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -2327,6 +2327,34 @@ async def get_mev_shield_next_key( return public_key_bytes + async def get_mev_shield_current_key( + self, + block_hash: Optional[str] = None, + ) -> Optional[tuple[bytes, int]]: + """ + Get the current MEV Shield public key and epoch from chain storage. + + Args: + block_hash: Optional block hash to query at. + + Returns: + Tuple of (public_key_bytes, epoch) or None if not available. + """ + result = await self.query( + module="MevShield", + storage_function="CurrentKey", + block_hash=block_hash, + ) + public_key_bytes = bytes(next(iter(result))) + + if len(public_key_bytes) != MEV_SHIELD_PUBLIC_KEY_SIZE: + raise ValueError( + f"Invalid ML-KEM-768 public key size: {len(public_key_bytes)} bytes. " + f"Expected exactly {MEV_SHIELD_PUBLIC_KEY_SIZE} bytes." + ) + + return public_key_bytes + async def best_connection(networks: list[str]): """ From e7b181234b19dd0613ffb267da4e8b3a13249c18 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 4 Dec 2025 16:10:14 -0800 Subject: [PATCH 44/47] add supp for mark_decryption_failed --- .../src/bittensor/extrinsics/mev_shield.py | 70 ++----------------- 1 file changed, 4 insertions(+), 66 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index ff99466af..eadce8863 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -161,7 +161,10 @@ async def _noop(_): call_module = call.get("call_module") call_function = call.get("call_function") - if call_module == "MevShield" and call_function == "execute_revealed": + if call_module == "MevShield" and call_function in ( + "execute_revealed", + "mark_decryption_failed", + ): call_args = call.get("call_args", []) for arg in call_args: if arg.get("name") == "id": @@ -190,68 +193,3 @@ async def _noop(_): return True, None, receipt return False, "Timeout waiting for MEV Shield execution", None - - -async def check_mev_shield_error( - receipt: AsyncExtrinsicReceipt, - subtensor: "SubtensorInterface", - wrapper_id: str, -) -> Optional[dict]: - """ - Handles & extracts error messages in the MEV Shield extrinsics. - This is a temporary implementation until we update up-stream code. - - Args: - receipt: AsyncExtrinsicReceipt for the execute_revealed extrinsic. - subtensor: SubtensorInterface instance. - wrapper_id: The wrapper ID to verify we're checking the correct event. - - Returns: - Error dict to be used with format_error_message(), or None if no error. - """ - if not await receipt.is_success: - return await receipt.error_message - - for event in await receipt.triggered_events: - event_details = event.get("event", {}) - - if ( - event_details.get("module_id") == "MevShield" - and event_details.get("event_id") == "DecryptedRejected" - ): - attributes = event_details.get("attributes", {}) - event_wrapper_id = attributes.get("id") - - if event_wrapper_id != wrapper_id: - continue - - reason = attributes.get("reason", {}) - dispatch_error = reason.get("error", {}) - - try: - if "Module" in dispatch_error: - module_index = dispatch_error["Module"]["index"] - error_index = dispatch_error["Module"]["error"] - - if isinstance(error_index, str) and error_index.startswith("0x"): - error_index = int(error_index[2:4], 16) - - runtime = await subtensor.substrate.init_runtime( - block_hash=receipt.block_hash - ) - module_error = runtime.metadata.get_module_error( - module_index=module_index, - error_index=error_index, - ) - - return { - "type": "Module", - "name": module_error.name, - "docs": module_error.docs, - } - except Exception: - return dispatch_error - - return dispatch_error - - return None From 3d40ede4f7d9954415ed537b75e3d98bc087caab Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 4 Dec 2025 16:49:39 -0800 Subject: [PATCH 45/47] no mev prot --- tests/e2e_tests/test_wallet_interactions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e_tests/test_wallet_interactions.py b/tests/e2e_tests/test_wallet_interactions.py index ae23c5b0e..7ed705b65 100644 --- a/tests/e2e_tests/test_wallet_interactions.py +++ b/tests/e2e_tests/test_wallet_interactions.py @@ -394,6 +394,7 @@ def test_wallet_identities(local_chain, wallet_setup): "--logo-url", "https://testsubnet.com/logo.png", "--no-prompt", + "--no-mev-protection", ], ) assert f"โœ… Registered subnetwork with netuid: {netuid}" in result.stdout From bd621e9e7f075027fe8cfa4a72f996c687a162d7 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 4 Dec 2025 18:05:31 -0800 Subject: [PATCH 46/47] bump ASI --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4cac3ea69..fbe506980 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ classifiers = [ ] dependencies = [ "wheel", - "async-substrate-interface>=1.5.13", + "async-substrate-interface>=1.5.14", "aiohttp~=3.13", "backoff~=2.2.1", "bittensor-drand>=1.2.0", From 42055a33d936b3751998c956a4382f288fca4164 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 4 Dec 2025 18:12:52 -0800 Subject: [PATCH 47/47] improve example cmds --- bittensor_cli/cli.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 7206b612f..8d911ffa6 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4013,6 +4013,9 @@ def stake_add( 7. Stake the same amount to multiple subnets: [green]$[/green] btcli stake add --amount 100 --netuids 4,5,6 + 8. Stake without MEV protection: + [green]$[/green] btcli stake add --amount 100 --netuid 1 --no-mev-protection + [bold]Safe Staking Parameters:[/bold] โ€ข [blue]--safe[/blue]: Enables rate tolerance checks โ€ข [blue]--tolerance[/blue]: Maximum % rate change allowed (0.05 = 5%) @@ -4316,6 +4319,9 @@ def stake_remove( 6. Unstake all Alpha from a hotkey and stake to Root: [green]$[/green] btcli stake remove --all-alpha + 7. Unstake without MEV protection: + [green]$[/green] btcli stake remove --amount 100 --netuid 1 --no-mev-protection + [bold]Safe Staking Parameters:[/bold] โ€ข [blue]--safe[/blue]: Enables rate tolerance checks during unstaking โ€ข [blue]--tolerance[/blue]: Max allowed rate change (0.05 = 5%) @@ -4645,9 +4651,13 @@ def stake_move( If no arguments are provided, an interactive selection menu will be shown. - EXAMPLE + EXAMPLES + + 1. Interactive move (guided prompts): + [green]$[/green] btcli stake move - [green]$[/green] btcli stake move + 2. Move stake without MEV protection: + [green]$[/green] btcli stake move --no-mev-protection """ self.verbosity_handler(quiet, verbose, json_output) if prompt: @@ -4856,6 +4866,9 @@ def stake_transfer( Transfer all available stake from origin hotkey: [green]$[/green] btcli stake transfer --all --origin-netuid 1 --dest-netuid 2 + + Transfer stake without MEV protection: + [green]$[/green] btcli stake transfer --origin-netuid 1 --dest-netuid 2 --amount 100 --no-mev-protection """ self.verbosity_handler(quiet, verbose, json_output) if prompt: @@ -5032,10 +5045,13 @@ def stake_swap( If no arguments are provided, an interactive selection menu will be shown. - EXAMPLE + EXAMPLES - Swap 100 TAO from subnet 1 to subnet 2: - [green]$[/green] btcli stake swap --wallet-name default --wallet-hotkey default --origin-netuid 1 --dest-netuid 2 --amount 100 + 1. Swap 100 TAO from subnet 1 to subnet 2: + [green]$[/green] btcli stake swap --wallet-name default --wallet-hotkey default --origin-netuid 1 --dest-netuid 2 --amount 100 + + 2. Swap stake without MEV protection: + [green]$[/green] btcli stake swap --origin-netuid 1 --dest-netuid 2 --amount 100 --no-mev-protection """ self.verbosity_handler(quiet, verbose, json_output) console.print( @@ -6473,6 +6489,9 @@ def subnets_create( 2. Create with GitHub repo and contact email: [green]$[/green] btcli subnets create --subnet-name MySubnet --github-repo https://github.com/myorg/mysubnet --subnet-contact team@mysubnet.net + + 3. Create subnet without MEV protection: + [green]$[/green] btcli subnets create --no-mev-protection """ self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask(