From 4b3246513e2e70c0af67143e5eb7c56f3ef7a0f9 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 5 Dec 2025 01:58:44 -0800 Subject: [PATCH 01/17] update encrypt_extrinsic --- .../src/bittensor/extrinsics/mev_shield.py | 166 ++---------------- 1 file changed, 15 insertions(+), 151 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index eadce8863..5e0a81f99 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -1,76 +1,48 @@ -import asyncio 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, format_error_message +from bittensor_drand import encrypt_mlkem768 +from bittensor_cli.src.bittensor.utils import format_error_message if TYPE_CHECKING: - from bittensor_wallet import Wallet - from scalecodec import GenericCall + from scalecodec import GenericCall, GenericExtrinsic from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface -async def encrypt_call( +async def encrypt_extrinsic( subtensor: "SubtensorInterface", - wallet: "Wallet", - call: "GenericCall", + signed_extrinsic: "GenericExtrinsic", ) -> "GenericCall": """ - Encrypt a call using MEV Shield. + Encrypt a signed extrinsic using MEV Shield. - Takes any call and returns a MevShield.submit_encrypted call - that can be submitted like any regular extrinsic. + Takes a pre-signed extrinsic and returns a MevShield.submit_encrypted call. + The signed extrinsic should be created with nonce = current_nonce + 1, + as it will execute after the shield wrapper extrinsic. Args: subtensor: The SubtensorInterface instance for chain queries. - wallet: The wallet whose coldkey will sign the inner payload. - call: The call to encrypt. + signed_extrinsic: The signed extrinsic to encrypt. Returns: - A MevShield.submit_encrypted call. + A MevShield.submit_encrypted call to be signed with the current nonce. Raises: ValueError: If MEV Shield NextKey is not available on chain. """ - next_key_result, genesis_hash = await asyncio.gather( - subtensor.get_mev_shield_next_key(), - subtensor.substrate.get_block_hash(0), - ) - if next_key_result is None: + ml_kem_768_public_key = await subtensor.get_mev_shield_next_key() + if ml_kem_768_public_key is None: raise ValueError("MEV Shield NextKey not available on chain") - ml_kem_768_public_key = next_key_result - - # Create payload_core: signer (32B) + next_key (32B) + SCALE(call) - signer_bytes = encode_account_id(wallet.coldkey.ss58_address) - scale_call_bytes = bytes(call.data.data) - next_key = hashlib.blake2b(next_key_result, digest_size=32).digest() - - payload_core = signer_bytes + next_key + 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 + plaintext = bytes(signed_extrinsic.data.data) # 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_hash = hashlib.blake2b(plaintext, digest_size=32).digest() commitment_hex = "0x" + commitment_hash.hex() # Create the MevShield.submit_encrypted call @@ -85,111 +57,3 @@ 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 - - -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]]: - """ - 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. - 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. - - Returns: - 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 - - starting_block = await subtensor.substrate.get_block_number(submit_block_hash) - current_block = starting_block + 1 - - while current_block - starting_block <= timeout_blocks: - if status: - status.update( - f"Waiting for :shield: MEV Protection " - f"(checking block {current_block - starting_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) - - # Find executeRevealed extrinsic & match ids - execute_revealed_index = None - for idx, extrinsic in enumerate(extrinsics): - call = extrinsic.value.get("call", {}) - call_module = call.get("call_module") - call_function = call.get("call_function") - - 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": - 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 - - if execute_revealed_index is None: - current_block += 1 - continue - - receipt = AsyncExtrinsicReceipt( - substrate=subtensor.substrate, - block_hash=block_hash, - extrinsic_idx=execute_revealed_index, - ) - - if not await receipt.is_success: - error_msg = format_error_message(await receipt.error_message) - return False, error_msg, None - - return True, None, receipt - - return False, "Timeout waiting for MEV Shield execution", None From 11e15b68a01995a6dd40c2c139c7a60598dc69ed Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 5 Dec 2025 02:30:38 -0800 Subject: [PATCH 02/17] add create_mev_protected_extrinsic --- .../src/bittensor/extrinsics/mev_shield.py | 66 +++++++++++++++---- 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index 5e0a81f99..582dc1651 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -6,6 +6,7 @@ from bittensor_cli.src.bittensor.utils import format_error_message if TYPE_CHECKING: + from bittensor_wallet import Keypair from scalecodec import GenericCall, GenericExtrinsic from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface @@ -15,21 +16,18 @@ async def encrypt_extrinsic( signed_extrinsic: "GenericExtrinsic", ) -> "GenericCall": """ - Encrypt a signed extrinsic using MEV Shield. + Encrypts a signed extrinsic using MEV Shield. - Takes a pre-signed extrinsic and returns a MevShield.submit_encrypted call. - The signed extrinsic should be created with nonce = current_nonce + 1, - as it will execute after the shield wrapper extrinsic. + Takes a pre-signed extrinsic and returns a `MevShield.submit_encrypted` call. + The inner extrinsic must be signed with `nonce = current_nonce + 1` because it + executes after the wrapper. - Args: - subtensor: The SubtensorInterface instance for chain queries. - signed_extrinsic: The signed extrinsic to encrypt. + :param subtensor: SubtensorInterface instance for chain queries. + :param signed_extrinsic: The pre-signed extrinsic to encrypt. - Returns: - A MevShield.submit_encrypted call to be signed with the current nonce. + :return: `MevShield.submit_encrypted` call to sign with the current nonce. - Raises: - ValueError: If MEV Shield NextKey is not available on chain. + :raises ValueError: If a MEV Shield `NextKey` is not available on chain. """ ml_kem_768_public_key = await subtensor.get_mev_shield_next_key() @@ -57,3 +55,49 @@ async def encrypt_extrinsic( return encrypted_call + +async def create_mev_protected_extrinsic( + subtensor: "SubtensorInterface", + keypair: "Keypair", + call: "GenericCall", + nonce: int, + era: Optional[int] = None, +) -> tuple["GenericExtrinsic", str]: + """ + Creates a MEV-protected extrinsic. + + Handles MEV Shield wrapping by signing the inner call with the future nonce, + encrypting it, and then signing the wrapper with the current nonce. + + :param subtensor: SubtensorInterface instance. + :param keypair: Keypair to sign both inner and wrapper extrinsics. + :param call: Call to protect (for example, `add_stake`). + :param nonce: Current account nonce; the wrapper uses this, the inner uses `nonce + 1`. + :param era: Optional era period for the extrinsic. + + :return: Tuple of `(signed_shield_extrinsic, inner_extrinsic_hash)`, where + `inner_extrinsic_hash` is used to track the actual extrinsic execution. + """ + + next_nonce = await subtensor.substrate.get_account_next_index(keypair.ss58_address) + + async def create_signed(call_to_sign, n): + kwargs = { + "call": call_to_sign, + "keypair": keypair, + "nonce": n, + } + if era is not None: + kwargs["era"] = {"period": era} + return await subtensor.substrate.create_signed_extrinsic(**kwargs) + + # Actual call: Sign with future nonce (current_nonce + 1) + inner_extrinsic = await create_signed(call, next_nonce) + inner_hash = f"0x{inner_extrinsic.extrinsic_hash.hex()}" + + # MeV Shield wrapper: Sign with current nonce + shield_call = await encrypt_extrinsic(subtensor, inner_extrinsic) + shield_extrinsic = await create_signed(shield_call, nonce) + + return shield_extrinsic, inner_hash + From 0ba42b628a44027c8f47154ce42b74634cbdd834 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 5 Dec 2025 02:31:02 -0800 Subject: [PATCH 03/17] wait_for_extrinsic_by_hash --- .../src/bittensor/extrinsics/mev_shield.py | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index 582dc1651..893510d7e 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -101,3 +101,116 @@ async def create_signed(call_to_sign, n): return shield_extrinsic, inner_hash + +async def extract_mev_shield_id(response: "AsyncExtrinsicReceipt") -> Optional[str]: + """ + Extracts 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. + + :param response: Extrinsic receipt from `submit_extrinsic`. + + :return: 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 + + +async def wait_for_extrinsic_by_hash( + subtensor: "SubtensorInterface", + extrinsic_hash: str, + shield_id: str, + submit_block_hash: str, + timeout_blocks: int = 2, + status=None, +) -> tuple[bool, Optional[str], Optional[AsyncExtrinsicReceipt]]: + """ + Waits for the result of a MEV Shield encrypted extrinsic. + + After `submit_encrypted` succeeds, the block author decrypts and submits the + inner extrinsic directly. This polls subsequent blocks for either the inner + extrinsic hash (success) or a `mark_decryption_failed` extrinsic for the + matching shield ID (failure). + + :param subtensor: SubtensorInterface instance. + :param extrinsic_hash: Hash of the inner extrinsic to find. + :param shield_id: Wrapper ID from the `EncryptedSubmitted` event (used to detect decryption failures). + :param submit_block_hash: Block hash where `submit_encrypted` was included. + :param timeout_blocks: Maximum blocks to wait before timing out (default 2). + :param status: Optional `rich.Status` object for progress updates. + + :return: Tuple `(success, error_message, receipt)` where: + - `success` is True if the extrinsic was found and succeeded. + - `error_message` contains the formatted failure reason, if any. + - `receipt` is the AsyncExtrinsicReceipt when available. + """ + + async def _noop(_): + return True + + starting_block = await subtensor.substrate.get_block_number(submit_block_hash) + current_block = starting_block + 1 + + while current_block - starting_block <= timeout_blocks: + if status: + status.update( + f"Waiting for :shield: MEV Protection " + f"(checking block {current_block - starting_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) + + result_idx = None + for idx, extrinsic in enumerate(extrinsics): + # Success: Inner extrinsic executed + if ( + extrinsic.extrinsic_hash + and f"0x{extrinsic.extrinsic_hash.hex()}" == extrinsic_hash + ): + result_idx = idx + break + + # Failure: Decryption failed + call = extrinsic.value.get("call", {}) + if ( + call.get("call_module") == "MevShield" + and call.get("call_function") == "mark_decryption_failed" + ): + call_args = call.get("call_args", []) + for arg in call_args: + if arg.get("name") == "id" and arg.get("value") == shield_id: + result_idx = idx + break + if result_idx is not None: + break + + if result_idx is not None: + receipt = AsyncExtrinsicReceipt( + substrate=subtensor.substrate, + block_hash=block_hash, + extrinsic_idx=result_idx, + ) + + if not await receipt.is_success: + error_msg = format_error_message(await receipt.error_message) + return False, error_msg, receipt + + return True, None, receipt + + current_block += 1 + + return ( + False, + "Failed to find outcome of the shield extrinsic (The protected extrinsic wasn't decrypted)", + None, + ) From e318a976d23c089081b7eafd952e7a224cc3633f Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 5 Dec 2025 02:32:34 -0800 Subject: [PATCH 04/17] update add stake --- bittensor_cli/src/commands/stake/add.py | 84 ++++++++++++++++--------- 1 file changed, 53 insertions(+), 31 deletions(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index d88692261..48ebf9f75 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -12,9 +12,9 @@ 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, + create_mev_protected_extrinsic, extract_mev_shield_id, - wait_for_mev_execution, + wait_for_extrinsic_by_hash, ) from bittensor_cli.src.bittensor.utils import ( console, @@ -141,13 +141,21 @@ async def safe_stake_extrinsic( ), ) 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}, - ) + extrinsic, inner_hash = await create_mev_protected_extrinsic( + subtensor=subtensor, + keypair=wallet.coldkey, + call=call, + next_nonce=next_nonce, + era=era, + ) + else: + inner_hash = None + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, + keypair=wallet.coldkey, + nonce=next_nonce, + era={"period": era}, + ) try: response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False @@ -171,15 +179,18 @@ async def safe_stake_extrinsic( 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 + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + inner_hash=inner_hash, + mev_shield_id=mev_shield_id, + block_hash=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 @@ -245,10 +256,18 @@ async def stake_extrinsic( f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid_i}" ) 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} - ) + extrinsic, inner_hash = await create_mev_protected_extrinsic( + subtensor=subtensor, + keypair=wallet.coldkey, + call=call, + next_nonce=next_nonce, + era=era, + ) + else: + inner_hash = None + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} + ) try: response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False @@ -264,15 +283,18 @@ async def stake_extrinsic( 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 + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + inner_hash=inner_hash, + mev_shield_id=mev_shield_id, + block_hash=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 From bf8b5685ccb9b609b95099ff5ff32c989ba6c05c Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 5 Dec 2025 02:34:05 -0800 Subject: [PATCH 05/17] update move cmds --- bittensor_cli/src/commands/stake/move.py | 118 +++++++++++++++-------- 1 file changed, 77 insertions(+), 41 deletions(-) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index a7f3f0782..b93130cf1 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -9,9 +9,9 @@ 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, + create_mev_protected_extrinsic, extract_mev_shield_id, - wait_for_mev_execution, + wait_for_extrinsic_by_hash, ) from bittensor_cli.src.bittensor.utils import ( console, @@ -548,13 +548,14 @@ async def move_stake( "alpha_amount": amount_to_move_as_balance.rao, }, ) - sim_swap, extrinsic_fee = await asyncio.gather( + sim_swap, extrinsic_fee, next_nonce = await asyncio.gather( subtensor.sim_swap( origin_netuid=origin_netuid, destination_netuid=destination_netuid, amount=amount_to_move_as_balance.rao, ), subtensor.get_extrinsic_fee(call, wallet.coldkeypub), + subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), ) # Display stake movement details @@ -586,24 +587,35 @@ async def move_stake( f"[blue]{destination_hotkey}[/blue] on netuid: [blue]{destination_netuid}[/blue] ..." ) as status: 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} - ) + extrinsic, inner_hash = await create_mev_protected_extrinsic( + subtensor=subtensor, + keypair=wallet.coldkey, + call=call, + next_nonce=next_nonce, + era=era, + ) + else: + inner_hash = None + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} + ) response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=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, "" + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + inner_hash=inner_hash, + mev_shield_id=mev_shield_id, + block_hash=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() @@ -752,13 +764,14 @@ async def transfer_stake( "alpha_amount": amount_to_transfer.rao, }, ) - sim_swap, extrinsic_fee = await asyncio.gather( + sim_swap, extrinsic_fee, next_nonce = await asyncio.gather( subtensor.sim_swap( origin_netuid=origin_netuid, destination_netuid=dest_netuid, amount=amount_to_transfer.rao, ), subtensor.get_extrinsic_fee(call, wallet.coldkeypub), + subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), ) # Display stake movement details @@ -788,10 +801,18 @@ async def transfer_stake( with console.status("\n:satellite: Transferring stake ...") as status: 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} - ) + extrinsic, inner_hash = await create_mev_protected_extrinsic( + subtensor=subtensor, + keypair=wallet.coldkey, + call=call, + next_nonce=next_nonce, + era=era, + ) + else: + inner_hash = None + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} + ) response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False @@ -799,14 +820,17 @@ async def transfer_stake( 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, "" + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + inner_hash=inner_hash, + mev_shield_id=mev_shield_id, + block_hash=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() @@ -939,13 +963,14 @@ async def swap_stake( "alpha_amount": amount_to_swap.rao, }, ) - sim_swap, extrinsic_fee = await asyncio.gather( + sim_swap, extrinsic_fee, next_nonce = await asyncio.gather( subtensor.sim_swap( origin_netuid=origin_netuid, destination_netuid=destination_netuid, amount=amount_to_swap.rao, ), subtensor.get_extrinsic_fee(call, wallet.coldkeypub), + subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), ) # Display stake movement details @@ -978,10 +1003,18 @@ async def swap_stake( f"to netuid [blue]{destination_netuid}[/blue]..." ) as status: 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} - ) + extrinsic, inner_hash = await create_mev_protected_extrinsic( + subtensor=subtensor, + keypair=wallet.coldkey, + call=call, + next_nonce=next_nonce, + era=era, + ) + else: + inner_hash = None + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} + ) response = await subtensor.substrate.submit_extrinsic( extrinsic, @@ -991,14 +1024,17 @@ async def swap_stake( 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, "" + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + inner_hash=inner_hash, + mev_shield_id=mev_shield_id, + block_hash=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 b812f2afcf943f376e6b5946e3247e183fd7deaf Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 5 Dec 2025 02:34:35 -0800 Subject: [PATCH 06/17] update remove cmds --- bittensor_cli/src/commands/stake/remove.py | 133 +++++++++++++-------- 1 file changed, 86 insertions(+), 47 deletions(-) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index afd9310da..99796ec5e 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -12,9 +12,9 @@ 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, + create_mev_protected_extrinsic, extract_mev_shield_id, - wait_for_mev_execution, + wait_for_extrinsic_by_hash, ) from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.utils import ( @@ -599,8 +599,9 @@ async def _unstake_extrinsic( f"\n:satellite: Unstaking {amount} from {hotkey_ss58} on netuid: {netuid} ..." ) - current_balance, call = await asyncio.gather( + current_balance, next_nonce, call = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="remove_stake", @@ -613,10 +614,18 @@ async def _unstake_extrinsic( ) 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} - ) + extrinsic, inner_hash = await create_mev_protected_extrinsic( + subtensor=subtensor, + keypair=wallet.coldkey, + call=call, + next_nonce=next_nonce, + era=era, + ) + else: + inner_hash = None + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} + ) try: response = await subtensor.substrate.submit_extrinsic( @@ -631,15 +640,18 @@ async def _unstake_extrinsic( 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 + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + inner_hash=inner_hash, + mev_shield_id=mev_shield_id, + block_hash=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) @@ -705,7 +717,7 @@ async def _safe_unstake_extrinsic( block_hash = await subtensor.substrate.get_chain_head() - current_balance, current_stake, call = await asyncio.gather( + current_balance, current_stake, next_nonce, call = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), subtensor.get_stake( hotkey_ss58=hotkey_ss58, @@ -713,6 +725,7 @@ async def _safe_unstake_extrinsic( netuid=netuid, block_hash=block_hash, ), + subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="remove_stake_limit", @@ -728,10 +741,18 @@ async def _safe_unstake_extrinsic( ) 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} - ) + extrinsic, inner_hash = await create_mev_protected_extrinsic( + subtensor=subtensor, + keypair=wallet.coldkey, + call=call, + next_nonce=next_nonce, + era=era, + ) + else: + inner_hash = None + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} + ) try: response = await subtensor.substrate.submit_extrinsic( @@ -757,15 +778,18 @@ async def _safe_unstake_extrinsic( 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 + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + inner_hash=inner_hash, + mev_shield_id=mev_shield_id, + block_hash=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() @@ -850,20 +874,32 @@ async def _unstake_all_extrinsic( previous_root_stake = None call_function = "unstake_all_alpha" if unstake_all_alpha else "unstake_all" - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function=call_function, - call_params={"hotkey": hotkey_ss58}, + call, next_nonce = await asyncio.gather( + subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function=call_function, + call_params={"hotkey": hotkey_ss58}, + ), + subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), ) if mev_protection: - call = await encrypt_call(subtensor, wallet, call) + extrinsic, inner_hash = await create_mev_protected_extrinsic( + subtensor=subtensor, + keypair=wallet.coldkey, + call=call, + next_nonce=next_nonce, + era=era, + ) + else: + inner_hash = None + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} + ) try: response = await subtensor.substrate.submit_extrinsic( - extrinsic=await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, era={"period": era} - ), + extrinsic, wait_for_inclusion=True, wait_for_finalization=False, ) @@ -877,15 +913,18 @@ async def _unstake_all_extrinsic( 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 + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + inner_hash=inner_hash, + mev_shield_id=mev_shield_id, + block_hash=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) From 70abc2163f7841004d599bb0ee74eb66a1befb72 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 5 Dec 2025 02:35:37 -0800 Subject: [PATCH 07/17] update sn creation --- bittensor_cli/src/commands/subnets/subnets.py | 53 ++++++++++++------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 14c026b01..d35a2701a 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -18,9 +18,9 @@ ) from bittensor_cli.src.bittensor.extrinsics.root import root_register_extrinsic from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( - encrypt_call, + create_mev_protected_extrinsic, extract_mev_shield_id, - wait_for_mev_execution, + wait_for_extrinsic_by_hash, ) from rich.live import Live from bittensor_cli.src.bittensor.minigraph import MiniGraph @@ -232,17 +232,27 @@ async def _find_event_attributes_in_extrinsic_receipt( with console.status(":satellite: Registering subnet...", spinner="earth") as status: substrate = subtensor.substrate - # create extrinsic call - call = await substrate.compose_call( - call_module="SubtensorModule", - call_function=call_function, - call_params=call_params, + call, next_nonce = await asyncio.gather( + substrate.compose_call( + call_module="SubtensorModule", + call_function=call_function, + call_params=call_params, + ), + substrate.get_account_next_index(wallet.coldkeypub.ss58_address), ) if mev_protection: - call = await encrypt_call(subtensor, wallet, call) - extrinsic = await substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) + extrinsic, inner_hash = await create_mev_protected_extrinsic( + subtensor=subtensor, + wallet=wallet, + call=call, + next_nonce=next_nonce, + era=None, + ) + else: + inner_hash = None + extrinsic = await substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey, nonce=next_nonce + ) response = await substrate.submit_extrinsic( extrinsic, wait_for_inclusion=wait_for_inclusion, @@ -263,16 +273,19 @@ async def _find_event_attributes_in_extrinsic_receipt( # Check for MEV shield execution 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 + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + inner_hash=inner_hash, + mev_shield_id=mev_shield_id, + block_hash=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 not mev_success: - status.stop() - err_console.print( - f":cross_mark: [red]Failed[/red]: MEV execution failed: {mev_error}" - ) - return False, None, None + return False, None, None # Successful registration, final check for membership attributes = await _find_event_attributes_in_extrinsic_receipt( From 6d5c050a027364a88a6a2ca0a49c2664c7de74a9 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 5 Dec 2025 02:43:51 -0800 Subject: [PATCH 08/17] wip --- .../src/bittensor/extrinsics/mev_shield.py | 101 ++++++++++-------- 1 file changed, 56 insertions(+), 45 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index 893510d7e..2732b8a3f 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -16,18 +16,19 @@ async def encrypt_extrinsic( signed_extrinsic: "GenericExtrinsic", ) -> "GenericCall": """ - Encrypts a signed extrinsic using MEV Shield. + Encrypt a signed extrinsic using MEV Shield. - Takes a pre-signed extrinsic and returns a `MevShield.submit_encrypted` call. - The inner extrinsic must be signed with `nonce = current_nonce + 1` because it - executes after the wrapper. + Takes a pre-signed extrinsic and returns a MevShield.submit_encrypted call. - :param subtensor: SubtensorInterface instance for chain queries. - :param signed_extrinsic: The pre-signed extrinsic to encrypt. + Args: + subtensor: The SubtensorInterface instance for chain queries. + signed_extrinsic: The signed extrinsic to encrypt. - :return: `MevShield.submit_encrypted` call to sign with the current nonce. + Returns: + A MevShield.submit_encrypted call to be signed with the current nonce. - :raises ValueError: If a MEV Shield `NextKey` is not available on chain. + Raises: + ValueError: If MEV Shield NextKey is not available on chain. """ ml_kem_768_public_key = await subtensor.get_mev_shield_next_key() @@ -64,23 +65,25 @@ async def create_mev_protected_extrinsic( era: Optional[int] = None, ) -> tuple["GenericExtrinsic", str]: """ - Creates a MEV-protected extrinsic. - - Handles MEV Shield wrapping by signing the inner call with the future nonce, - encrypting it, and then signing the wrapper with the current nonce. - - :param subtensor: SubtensorInterface instance. - :param keypair: Keypair to sign both inner and wrapper extrinsics. - :param call: Call to protect (for example, `add_stake`). - :param nonce: Current account nonce; the wrapper uses this, the inner uses `nonce + 1`. - :param era: Optional era period for the extrinsic. - - :return: Tuple of `(signed_shield_extrinsic, inner_extrinsic_hash)`, where - `inner_extrinsic_hash` is used to track the actual extrinsic execution. + Create a MEV-protected extrinsic. + + This function handles MEV Shield wrapping: + 1. Fetches a future nonce (current_nonce + 1) & signs the inner call with it + 2. Encrypts it into a wrapper call + 3. Signs the wrapper with the current nonce + + Args: + subtensor: The SubtensorInterface instance. + keypair: Keypair for signing. + call: The call to protect (e.g., add_stake). + nonce: The current account nonce (wrapper will use this, inner uses nonce+1). + era: The era period for the extrinsic. + + Returns: + Tuple of (signed_shield_extrinsic, inner_extrinsic_hash). + The inner_extrinsic_hash is used for tracking actual extrinsic. """ - next_nonce = await subtensor.substrate.get_account_next_index(keypair.ss58_address) - async def create_signed(call_to_sign, n): kwargs = { "call": call_to_sign, @@ -91,6 +94,8 @@ async def create_signed(call_to_sign, n): kwargs["era"] = {"period": era} return await subtensor.substrate.create_signed_extrinsic(**kwargs) + next_nonce = await subtensor.substrate.get_account_next_index(keypair.ss58_address) + # Actual call: Sign with future nonce (current_nonce + 1) inner_extrinsic = await create_signed(call, next_nonce) inner_hash = f"0x{inner_extrinsic.extrinsic_hash.hex()}" @@ -104,14 +109,16 @@ async def create_signed(call_to_sign, n): async def extract_mev_shield_id(response: "AsyncExtrinsicReceipt") -> Optional[str]: """ - Extracts the MEV Shield wrapper ID from an extrinsic response. + Extract the MEV Shield wrapper ID from an extrinsic response. - After submitting a MEV Shield encrypted call, the `EncryptedSubmitted` event + After submitting a MEV Shield encrypted call, the EncryptedSubmitted event contains the wrapper ID needed to track execution. - :param response: Extrinsic receipt from `submit_extrinsic`. + Args: + response: The extrinsic receipt from submit_extrinsic. - :return: Wrapper ID (hex string) or `None` if not found. + Returns: + The wrapper ID (hex string) or None if not found. """ for event in await response.triggered_events: if event["event_id"] == "EncryptedSubmitted": @@ -128,24 +135,28 @@ async def wait_for_extrinsic_by_hash( status=None, ) -> tuple[bool, Optional[str], Optional[AsyncExtrinsicReceipt]]: """ - Waits for the result of a MEV Shield encrypted extrinsic. - - After `submit_encrypted` succeeds, the block author decrypts and submits the - inner extrinsic directly. This polls subsequent blocks for either the inner - extrinsic hash (success) or a `mark_decryption_failed` extrinsic for the - matching shield ID (failure). - - :param subtensor: SubtensorInterface instance. - :param extrinsic_hash: Hash of the inner extrinsic to find. - :param shield_id: Wrapper ID from the `EncryptedSubmitted` event (used to detect decryption failures). - :param submit_block_hash: Block hash where `submit_encrypted` was included. - :param timeout_blocks: Maximum blocks to wait before timing out (default 2). - :param status: Optional `rich.Status` object for progress updates. - - :return: Tuple `(success, error_message, receipt)` where: - - `success` is True if the extrinsic was found and succeeded. - - `error_message` contains the formatted failure reason, if any. - - `receipt` is the AsyncExtrinsicReceipt when available. + Wait for the result of a MeV Shield encrypted extrinsic. + + After submit_encrypted succeeds, the block author will decrypt and submit + the inner extrinsic directly. This function polls subsequent blocks looking + for either: + - an extrinsic matching the provided hash (success) + OR + - a markDecryptionFailed extrinsic with matching shield ID (failure) + + Args: + subtensor: SubtensorInterface instance. + extrinsic_hash: The hash of the inner extrinsic to find. + shield_id: The wrapper ID from EncryptedSubmitted event (for detecting decryption failures). + submit_block_hash: Block hash where submit_encrypted was included. + timeout_blocks: Max blocks to wait (default 2). + status: Optional rich.Status object for progress updates. + + Returns: + Tuple of (success: bool, error: Optional[str], receipt: Optional[AsyncExtrinsicReceipt]). + - (True, None, receipt) if extrinsic was found and succeeded. + - (False, error_message, receipt) if extrinsic was found but failed. + - (False, "Timeout...", None) if not found within timeout. """ async def _noop(_): From fb91c5ab5d7c9160044be2c1e3e2c770e9a22322 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 5 Dec 2025 03:11:45 -0800 Subject: [PATCH 09/17] updates --- .../src/bittensor/extrinsics/mev_shield.py | 5 +--- bittensor_cli/src/commands/stake/add.py | 16 ++++++------- bittensor_cli/src/commands/stake/move.py | 24 +++++++++---------- bittensor_cli/src/commands/stake/remove.py | 24 +++++++++---------- bittensor_cli/src/commands/subnets/subnets.py | 10 ++++---- 5 files changed, 38 insertions(+), 41 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index 2732b8a3f..382d8029a 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -184,10 +184,7 @@ async def _noop(_): result_idx = None for idx, extrinsic in enumerate(extrinsics): # Success: Inner extrinsic executed - if ( - extrinsic.extrinsic_hash - and f"0x{extrinsic.extrinsic_hash.hex()}" == extrinsic_hash - ): + if f"0x{extrinsic.extrinsic_hash.hex()}" == extrinsic_hash: result_idx = idx break diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 48ebf9f75..c86738a04 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -145,7 +145,7 @@ async def safe_stake_extrinsic( subtensor=subtensor, keypair=wallet.coldkey, call=call, - next_nonce=next_nonce, + nonce=next_nonce, era=era, ) else: @@ -181,9 +181,9 @@ async def safe_stake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) mev_success, mev_error, response = await wait_for_extrinsic_by_hash( subtensor=subtensor, - inner_hash=inner_hash, - mev_shield_id=mev_shield_id, - block_hash=response.block_hash, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, status=status, ) if not mev_success: @@ -260,7 +260,7 @@ async def stake_extrinsic( subtensor=subtensor, keypair=wallet.coldkey, call=call, - next_nonce=next_nonce, + nonce=next_nonce, era=era, ) else: @@ -285,9 +285,9 @@ async def stake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) mev_success, mev_error, response = await wait_for_extrinsic_by_hash( subtensor=subtensor, - inner_hash=inner_hash, - mev_shield_id=mev_shield_id, - block_hash=response.block_hash, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, status=status, ) if not mev_success: diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index b93130cf1..dc23fd17a 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -591,7 +591,7 @@ async def move_stake( subtensor=subtensor, keypair=wallet.coldkey, call=call, - next_nonce=next_nonce, + nonce=next_nonce, era=era, ) else: @@ -607,9 +607,9 @@ async def move_stake( mev_shield_id = await extract_mev_shield_id(response) mev_success, mev_error, response = await wait_for_extrinsic_by_hash( subtensor=subtensor, - inner_hash=inner_hash, - mev_shield_id=mev_shield_id, - block_hash=response.block_hash, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, status=status, ) if not mev_success: @@ -805,7 +805,7 @@ async def transfer_stake( subtensor=subtensor, keypair=wallet.coldkey, call=call, - next_nonce=next_nonce, + nonce=next_nonce, era=era, ) else: @@ -822,9 +822,9 @@ async def transfer_stake( mev_shield_id = await extract_mev_shield_id(response) mev_success, mev_error, response = await wait_for_extrinsic_by_hash( subtensor=subtensor, - inner_hash=inner_hash, - mev_shield_id=mev_shield_id, - block_hash=response.block_hash, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, status=status, ) if not mev_success: @@ -1007,7 +1007,7 @@ async def swap_stake( subtensor=subtensor, keypair=wallet.coldkey, call=call, - next_nonce=next_nonce, + nonce=next_nonce, era=era, ) else: @@ -1026,9 +1026,9 @@ async def swap_stake( mev_shield_id = await extract_mev_shield_id(response) mev_success, mev_error, response = await wait_for_extrinsic_by_hash( subtensor=subtensor, - inner_hash=inner_hash, - mev_shield_id=mev_shield_id, - block_hash=response.block_hash, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, 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 99796ec5e..bf669765f 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -618,7 +618,7 @@ async def _unstake_extrinsic( subtensor=subtensor, keypair=wallet.coldkey, call=call, - next_nonce=next_nonce, + nonce=next_nonce, era=era, ) else: @@ -642,9 +642,9 @@ async def _unstake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) mev_success, mev_error, response = await wait_for_extrinsic_by_hash( subtensor=subtensor, - inner_hash=inner_hash, - mev_shield_id=mev_shield_id, - block_hash=response.block_hash, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, status=status, ) if not mev_success: @@ -745,7 +745,7 @@ async def _safe_unstake_extrinsic( subtensor=subtensor, keypair=wallet.coldkey, call=call, - next_nonce=next_nonce, + nonce=next_nonce, era=era, ) else: @@ -780,9 +780,9 @@ async def _safe_unstake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) mev_success, mev_error, response = await wait_for_extrinsic_by_hash( subtensor=subtensor, - inner_hash=inner_hash, - mev_shield_id=mev_shield_id, - block_hash=response.block_hash, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, status=status, ) if not mev_success: @@ -888,7 +888,7 @@ async def _unstake_all_extrinsic( subtensor=subtensor, keypair=wallet.coldkey, call=call, - next_nonce=next_nonce, + nonce=next_nonce, era=era, ) else: @@ -915,9 +915,9 @@ async def _unstake_all_extrinsic( mev_shield_id = await extract_mev_shield_id(response) mev_success, mev_error, response = await wait_for_extrinsic_by_hash( subtensor=subtensor, - inner_hash=inner_hash, - mev_shield_id=mev_shield_id, - block_hash=response.block_hash, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, status=status, ) if not mev_success: diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index d35a2701a..a7167b07e 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -243,9 +243,9 @@ async def _find_event_attributes_in_extrinsic_receipt( if mev_protection: extrinsic, inner_hash = await create_mev_protected_extrinsic( subtensor=subtensor, - wallet=wallet, + keypair=wallet.coldkey, call=call, - next_nonce=next_nonce, + nonce=next_nonce, era=None, ) else: @@ -275,9 +275,9 @@ async def _find_event_attributes_in_extrinsic_receipt( mev_shield_id = await extract_mev_shield_id(response) mev_success, mev_error, response = await wait_for_extrinsic_by_hash( subtensor=subtensor, - inner_hash=inner_hash, - mev_shield_id=mev_shield_id, - block_hash=response.block_hash, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, status=status, ) if not mev_success: From 3c55d9cb0c9babebbdbd1227e8e3bef4f8ae40e1 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 5 Dec 2025 16:38:29 +0200 Subject: [PATCH 10/17] Added block_number to AsyncExtrinsicReceipt from mev --- bittensor_cli/src/bittensor/extrinsics/mev_shield.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index 382d8029a..cfa2650df 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -206,6 +206,7 @@ async def _noop(_): receipt = AsyncExtrinsicReceipt( substrate=subtensor.substrate, block_hash=block_hash, + block_number=current_block, extrinsic_idx=result_idx, ) From 0780305f8a306f014ed1d0da2583b6ea18d8d158 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 5 Dec 2025 17:44:30 +0200 Subject: [PATCH 11/17] Include mev protection logic in sign and send extrinsic method --- .../src/bittensor/subtensor_interface.py | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 70af13cdc..e8c69d51b 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1178,6 +1178,7 @@ async def sign_and_send_extrinsic( nonce: Optional[int] = None, sign_with: Literal["coldkey", "hotkey", "coldkeypub"] = "coldkey", announce_only: bool = False, + mev_protection: bool = False, ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: """ Helper method to sign and submit an extrinsic call to chain. @@ -1192,8 +1193,24 @@ async def sign_and_send_extrinsic( :param sign_with: Determine which of the wallet's keypairs to use to sign the extrinsic call. :param announce_only: If set, makes the call as an announcement, rather than making the call. - :return: (success, error message, extrinsic receipt | None) + :return: (success, error message or inner extrinsic hash (if using mev_protection), extrinsic receipt | None) """ + + async def create_signed(call_to_sign, n): + kwargs = { + "call": call_to_sign, + "keypair": keypair, + "nonce": n, + } + if era is not None: + kwargs["era"] = {"period": era} + return await self.substrate.create_signed_extrinsic(**kwargs) + + if announce_only and mev_protection: + raise ValueError( + "Cannot use announce-only and mev-protection. Calls should be announced without mev protection," + "and executed with them." + ) if proxy is not None: if announce_only: call_to_announce = call @@ -1225,7 +1242,17 @@ async def sign_and_send_extrinsic( call_args["nonce"] = await self.substrate.get_account_next_index( keypair.ss58_address ) - extrinsic = await self.substrate.create_signed_extrinsic(**call_args) + inner_hash = "" + if mev_protection: + next_nonce = await self.substrate.get_account_next_index( + keypair.ss58_address + ) + inner_extrinsic = await create_signed(call, next_nonce) + inner_hash = f"0x{inner_extrinsic.extrinsic_hash.hex()}" + shield_call = await encrypt_extrinsic(self, inner_extrinsic) + extrinsic = await create_signed(shield_call, nonce) + else: + extrinsic = await self.substrate.create_signed_extrinsic(**call_args) try: response = await self.substrate.submit_extrinsic( extrinsic, @@ -1234,7 +1261,7 @@ async def sign_and_send_extrinsic( ) # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: - return True, "", response + return True, inner_hash, response if await response.is_success: if announce_only: block = await self.substrate.get_block_number(response.block_hash) @@ -1251,7 +1278,7 @@ async def sign_and_send_extrinsic( console.print( f"Added entry {call_to_announce.call_hash} at block {block} to your ProxyAnnouncements address book." ) - return True, "", response + return True, inner_hash, response else: return False, format_error_message(await response.error_message), None except SubstrateRequestException as e: From b1b87483ef2916250b01326a2e7081978f7cf08d Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 5 Dec 2025 18:09:33 +0200 Subject: [PATCH 12/17] Makes things work with the proxy logic --- .../src/bittensor/extrinsics/mev_shield.py | 50 ----- .../src/bittensor/subtensor_interface.py | 7 +- bittensor_cli/src/commands/stake/add.py | 51 ++--- bittensor_cli/src/commands/stake/move.py | 192 +++++++++--------- bittensor_cli/src/commands/stake/remove.py | 63 +++--- bittensor_cli/src/commands/subnets/subnets.py | 7 +- 6 files changed, 169 insertions(+), 201 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index cfa2650df..4a95dbb73 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -57,56 +57,6 @@ async def encrypt_extrinsic( return encrypted_call -async def create_mev_protected_extrinsic( - subtensor: "SubtensorInterface", - keypair: "Keypair", - call: "GenericCall", - nonce: int, - era: Optional[int] = None, -) -> tuple["GenericExtrinsic", str]: - """ - Create a MEV-protected extrinsic. - - This function handles MEV Shield wrapping: - 1. Fetches a future nonce (current_nonce + 1) & signs the inner call with it - 2. Encrypts it into a wrapper call - 3. Signs the wrapper with the current nonce - - Args: - subtensor: The SubtensorInterface instance. - keypair: Keypair for signing. - call: The call to protect (e.g., add_stake). - nonce: The current account nonce (wrapper will use this, inner uses nonce+1). - era: The era period for the extrinsic. - - Returns: - Tuple of (signed_shield_extrinsic, inner_extrinsic_hash). - The inner_extrinsic_hash is used for tracking actual extrinsic. - """ - - async def create_signed(call_to_sign, n): - kwargs = { - "call": call_to_sign, - "keypair": keypair, - "nonce": n, - } - if era is not None: - kwargs["era"] = {"period": era} - return await subtensor.substrate.create_signed_extrinsic(**kwargs) - - next_nonce = await subtensor.substrate.get_account_next_index(keypair.ss58_address) - - # Actual call: Sign with future nonce (current_nonce + 1) - inner_extrinsic = await create_signed(call, next_nonce) - inner_hash = f"0x{inner_extrinsic.extrinsic_hash.hex()}" - - # MeV Shield wrapper: Sign with current nonce - shield_call = await encrypt_extrinsic(subtensor, inner_extrinsic) - shield_extrinsic = await create_signed(shield_call, nonce) - - return shield_extrinsic, inner_hash - - async def extract_mev_shield_id(response: "AsyncExtrinsicReceipt") -> Optional[str]: """ Extract the MEV Shield wrapper ID from an extrinsic response. diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index e8c69d51b..e30772294 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -34,6 +34,7 @@ from bittensor_cli.src import DelegatesDetails from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float from bittensor_cli.src import Constants, defaults, TYPE_REGISTRY +from bittensor_cli.src.bittensor.extrinsics.mev_shield import encrypt_extrinsic from bittensor_cli.src.bittensor.utils import ( format_error_message, console, @@ -2406,6 +2407,7 @@ async def get_all_subnet_ema_tao_inflow( else: _, raw_ema_value = value ema_value = fixed_to_float(raw_ema_value) + # TODO @abe is this intentional: float passed as int for from_rao ema_map[netuid] = Balance.from_rao(ema_value) return ema_map @@ -2437,12 +2439,13 @@ async def get_subnet_ema_tao_inflow( return Balance.from_rao(0) _, raw_ema_value = value ema_value = fixed_to_float(raw_ema_value) + # TODO @abe this is a float, but we're passing it as an int for from_rao, is this intentional? return Balance.from_rao(ema_value) async def get_mev_shield_next_key( self, block_hash: Optional[str] = None, - ) -> Optional[tuple[bytes, int]]: + ) -> bytes: """ Get the next MEV Shield public key and epoch from chain storage. @@ -2470,7 +2473,7 @@ async def get_mev_shield_next_key( async def get_mev_shield_current_key( self, block_hash: Optional[str] = None, - ) -> Optional[tuple[bytes, int]]: + ) -> bytes: """ Get the current MEV Shield public key and epoch from chain storage. diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 7146ade41..126854aff 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -11,7 +11,6 @@ from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( - create_mev_protected_extrinsic, extract_mev_shield_id, wait_for_extrinsic_by_hash, ) @@ -141,14 +140,13 @@ async def safe_stake_extrinsic( }, ), ) - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) success_, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, nonce=next_nonce, era={"period": era}, proxy=proxy, + mev_protection=mev_protection, ) if not success_: if "Custom error: 8" in err_msg: @@ -164,16 +162,20 @@ async def safe_stake_extrinsic( return False, err_msg, None else: if mev_protection: + inner_hash = err_msg 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 + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=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 return True, "", response @@ -239,14 +241,13 @@ async def stake_extrinsic( failure_prelude = ( f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid_i}" ) - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) success_, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, nonce=next_nonce, era={"period": era}, proxy=proxy, + mev_protection=mev_protection, ) if not success_: err_msg = f"{failure_prelude} with error: {err_msg}" @@ -254,16 +255,20 @@ async def stake_extrinsic( return False, err_msg, None else: if mev_protection: + inner_hash = err_msg 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 + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=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 return True, "", response diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 97ec7baa6..86a5f9087 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -9,7 +9,6 @@ from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( - create_mev_protected_extrinsic, extract_mev_shield_id, wait_for_extrinsic_by_hash, ) @@ -588,28 +587,30 @@ async def move_stake( f"[blue]{origin_netuid}[/blue] \nto " f"[blue]{destination_hotkey}[/blue] on netuid: [blue]{destination_netuid}[/blue] ..." ) as status: - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) success_, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, era={"period": era}, proxy=proxy, + mev_protection=mev_protection, ) - 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() if response else "" if success_: + if mev_protection: + inner_hash = err_msg + mev_shield_id = await extract_mev_shield_id(response) + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=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, "" await print_extrinsic_id(response) if not prompt: console.print(":white_heavy_check_mark: [green]Sent[/green]") @@ -797,59 +798,62 @@ async def transfer_stake( return False, "" with console.status("\n:satellite: Transferring stake ...") as status: - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) success_, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, era={"period": era}, proxy=proxy, + mev_protection=mev_protection, ) - if success_: - 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 success_: + if mev_protection: + inner_hash = err_msg + mev_shield_id = await extract_mev_shield_id(response) + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=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, "" - await print_extrinsic_id(response) - ext_id = await response.get_extrinsic_identifier() - if not prompt: - console.print(":white_heavy_check_mark: [green]Sent[/green]") - return True, ext_id - else: - # Get and display new stake balances - new_stake, new_dest_stake = await asyncio.gather( - subtensor.get_stake( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=origin_hotkey, - netuid=origin_netuid, - ), - subtensor.get_stake( - coldkey_ss58=dest_coldkey_ss58, - hotkey_ss58=origin_hotkey, - netuid=dest_netuid, - ), - ) + await print_extrinsic_id(response) + ext_id = await response.get_extrinsic_identifier() + if not prompt: + console.print(":white_heavy_check_mark: [green]Sent[/green]") + return True, ext_id + else: + # Get and display new stake balances + new_stake, new_dest_stake = await asyncio.gather( + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=origin_hotkey, + netuid=origin_netuid, + ), + subtensor.get_stake( + coldkey_ss58=dest_coldkey_ss58, + hotkey_ss58=origin_hotkey, + netuid=dest_netuid, + ), + ) - console.print( - f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: " - f"[{COLOR_PALETTE.S.AMOUNT}]{new_stake}" - ) - console.print( - f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " - f"[{COLOR_PALETTE.S.AMOUNT}]{new_dest_stake}" - ) - return True, ext_id + console.print( + f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE.S.AMOUNT}]{new_stake}" + ) + console.print( + f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE.S.AMOUNT}]{new_dest_stake}" + ) + return True, ext_id - else: - err_console.print(f":cross_mark: [red]Failed[/red] with error: {err_msg}") - return False, "" + else: + err_console.print(f":cross_mark: [red]Failed[/red] with error: {err_msg}") + return False, "" async def swap_stake( @@ -991,8 +995,6 @@ async def swap_stake( f"\n:satellite: Swapping stake from netuid [blue]{origin_netuid}[/blue] " f"to netuid [blue]{destination_netuid}[/blue]..." ) as status: - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) success_, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, @@ -1000,51 +1002,55 @@ async def swap_stake( proxy=proxy, wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, + mev_protection=mev_protection, ) - 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 + ext_id = await response.get_extrinsic_identifier() + + if success_: + if mev_protection: + inner_hash = err_msg + mev_shield_id = await extract_mev_shield_id(response) + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=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, "" + await print_extrinsic_id(response) + if not prompt: + console.print(":white_heavy_check_mark: [green]Sent[/green]") + return True, await response.get_extrinsic_identifier() + else: + # Get and display new stake balances + new_stake, new_dest_stake = await asyncio.gather( + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=origin_netuid, + ), + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=destination_netuid, + ), + ) - ext_id = await response.get_extrinsic_identifier() + console.print( + f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE.S.AMOUNT}]{new_stake}" + ) + console.print( + f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE.S.AMOUNT}]{new_dest_stake}" + ) + return True, ext_id - if success_: - await print_extrinsic_id(response) - if not prompt: - console.print(":white_heavy_check_mark: [green]Sent[/green]") - return True, await response.get_extrinsic_identifier() else: - # Get and display new stake balances - new_stake, new_dest_stake = await asyncio.gather( - subtensor.get_stake( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - netuid=origin_netuid, - ), - subtensor.get_stake( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - netuid=destination_netuid, - ), - ) - - console.print( - f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: " - f"[{COLOR_PALETTE.S.AMOUNT}]{new_stake}" - ) - console.print( - f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " - f"[{COLOR_PALETTE.S.AMOUNT}]{new_dest_stake}" - ) - return True, ext_id - - else: - err_console.print(f":cross_mark: [red]Failed[/red] with error: {err_msg}") - return False, "" + err_console.print(f":cross_mark: [red]Failed[/red] with error: {err_msg}") + return False, "" diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 2105d94fd..098acf0e6 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -11,7 +11,6 @@ from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( - create_mev_protected_extrinsic, extract_mev_shield_id, wait_for_extrinsic_by_hash, ) @@ -626,24 +625,29 @@ async def _unstake_extrinsic( ), ) - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) - success, err_msg, response = await subtensor.sign_and_send_extrinsic( - call=call, wallet=wallet, era={"period": era}, proxy=proxy + # TODO I think this should handle announce-only + call=call, + wallet=wallet, + era={"period": era}, + proxy=proxy, + mev_protection=mev_protection, ) if success: if mev_protection: + inner_hash = err_msg 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 + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=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, None await print_extrinsic_id(response) block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( @@ -735,28 +739,29 @@ async def _safe_unstake_extrinsic( block_hash=block_hash, ), ) - - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) success, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, nonce=next_nonce, era={"period": era}, proxy=proxy, + mev_protection=mev_protection, ) if success: if mev_protection: + inner_hash = err_msg 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 + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=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, None await print_extrinsic_id(response) block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( @@ -860,16 +865,13 @@ async def _unstake_all_extrinsic( ), subtensor.substrate.get_account_next_index(coldkey_ss58), ) - - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) - try: success_, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, era={"period": era}, proxy=proxy, + mev_protection=mev_protection, ) if not success_: @@ -877,6 +879,7 @@ async def _unstake_all_extrinsic( return False, None if mev_protection: + inner_hash = err_msg mev_shield_id = await extract_mev_shield_id(response) mev_success, mev_error, response = await wait_for_extrinsic_by_hash( subtensor=subtensor, diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index b81a68344..9afed86f7 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -19,7 +19,6 @@ ) from bittensor_cli.src.bittensor.extrinsics.root import root_register_extrinsic from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( - create_mev_protected_extrinsic, extract_mev_shield_id, wait_for_extrinsic_by_hash, ) @@ -233,6 +232,8 @@ async def _find_event_attributes_in_extrinsic_receipt( if not unlock_key(wallet).success: return False, None, None + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address + with console.status(":satellite: Registering subnet...", spinner="earth") as status: substrate = subtensor.substrate call, next_nonce = await asyncio.gather( @@ -243,14 +244,13 @@ async def _find_event_attributes_in_extrinsic_receipt( ), substrate.get_account_next_index(coldkey_ss58), ) - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) success, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, proxy=proxy, + mev_protection=mev_protection, ) # We only wait here if we expect finalization. @@ -263,6 +263,7 @@ async def _find_event_attributes_in_extrinsic_receipt( else: # Check for MEV shield execution if mev_protection: + inner_hash = err_msg mev_shield_id = await extract_mev_shield_id(response) mev_success, mev_error, response = await wait_for_extrinsic_by_hash( subtensor=subtensor, From 000c5d6635e86c0325b4d2d9472c89cbd0e3e3c5 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 5 Dec 2025 18:18:13 +0200 Subject: [PATCH 13/17] Resolved todos --- bittensor_cli/src/commands/stake/move.py | 6 ++---- tests/e2e_tests/test_unstaking.py | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 86a5f9087..a52b8c5a3 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -767,8 +767,7 @@ async def transfer_stake( amount=amount_to_transfer.rao, ), subtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy), - # TODO should this be proxy or signer? - subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), + subtensor.substrate.get_account_next_index(proxy or wallet.coldkeypub.ss58_address), ) # Display stake movement details @@ -961,8 +960,7 @@ async def swap_stake( amount=amount_to_swap.rao, ), subtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy), - # TODO should this be proxy or signer? - subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), + subtensor.substrate.get_account_next_index(proxy or wallet.coldkeypub.ss58_address), ) # Display stake movement details diff --git a/tests/e2e_tests/test_unstaking.py b/tests/e2e_tests/test_unstaking.py index 8f17341a0..a13da8379 100644 --- a/tests/e2e_tests/test_unstaking.py +++ b/tests/e2e_tests/test_unstaking.py @@ -89,6 +89,7 @@ def test_unstaking(local_chain, wallet_setup): "--no-prompt", ], ) + print(result.stdout, result.stderr) assert "✅ Registered subnetwork with netuid: 2" in result.stdout assert "Your extrinsic has been included" in result.stdout, result.stdout From 116cec1b8a90b7aa09f522cd9dfadf8e033710bd Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 5 Dec 2025 18:38:09 +0200 Subject: [PATCH 14/17] Small fixes --- bittensor_cli/src/bittensor/subtensor_interface.py | 7 +++++-- bittensor_cli/src/commands/stake/move.py | 8 ++++++-- bittensor_cli/src/commands/subnets/subnets.py | 1 + 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index e30772294..f48972a9a 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1192,7 +1192,10 @@ async def sign_and_send_extrinsic( :param proxy: The real account used to create the proxy. None if not using a proxy for this call. :param nonce: The nonce used to submit this extrinsic call. :param sign_with: Determine which of the wallet's keypairs to use to sign the extrinsic call. - :param announce_only: If set, makes the call as an announcement, rather than making the call. + :param announce_only: If set, makes the call as an announcement, rather than making the call. Cannot + be used with `mev_protection=True`. + :param mev_protection: If set, uses Mev Protection on the extrinsic, thus encrypting it. Cannot be + used with `announce_only=True`. :return: (success, error message or inner extrinsic hash (if using mev_protection), extrinsic receipt | None) """ @@ -1204,7 +1207,7 @@ async def create_signed(call_to_sign, n): "nonce": n, } if era is not None: - kwargs["era"] = {"period": era} + kwargs["era"] = era return await self.substrate.create_signed_extrinsic(**kwargs) if announce_only and mev_protection: diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index a52b8c5a3..d65f67e71 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -767,7 +767,9 @@ async def transfer_stake( amount=amount_to_transfer.rao, ), subtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy), - subtensor.substrate.get_account_next_index(proxy or wallet.coldkeypub.ss58_address), + subtensor.substrate.get_account_next_index( + proxy or wallet.coldkeypub.ss58_address + ), ) # Display stake movement details @@ -960,7 +962,9 @@ async def swap_stake( amount=amount_to_swap.rao, ), subtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy), - subtensor.substrate.get_account_next_index(proxy or wallet.coldkeypub.ss58_address), + subtensor.substrate.get_account_next_index( + proxy or wallet.coldkeypub.ss58_address + ), ) # Display stake movement details diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 9afed86f7..d0c90477e 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -250,6 +250,7 @@ async def _find_event_attributes_in_extrinsic_receipt( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, proxy=proxy, + nonce=next_nonce, mev_protection=mev_protection, ) From 9dc99645a585d902adece6598bee950c3f2ee8cf Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 5 Dec 2025 18:58:08 +0200 Subject: [PATCH 15/17] Added missing nonces --- bittensor_cli/src/commands/stake/move.py | 3 +++ bittensor_cli/src/commands/stake/remove.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index d65f67e71..8f9255f70 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -593,6 +593,7 @@ async def move_stake( era={"period": era}, proxy=proxy, mev_protection=mev_protection, + nonce=next_nonce, ) ext_id = await response.get_extrinsic_identifier() if response else "" @@ -805,6 +806,7 @@ async def transfer_stake( era={"period": era}, proxy=proxy, mev_protection=mev_protection, + nonce=next_nonce, ) if success_: @@ -1005,6 +1007,7 @@ async def swap_stake( wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, mev_protection=mev_protection, + nonce=next_nonce, ) ext_id = await response.get_extrinsic_identifier() diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 098acf0e6..85ce18ff5 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -632,6 +632,7 @@ async def _unstake_extrinsic( era={"period": era}, proxy=proxy, mev_protection=mev_protection, + nonce=next_nonce, ) if success: if mev_protection: @@ -870,6 +871,7 @@ async def _unstake_all_extrinsic( call=call, wallet=wallet, era={"period": era}, + nonce=next_nonce, proxy=proxy, mev_protection=mev_protection, ) From fb9ed9b1eb61413072bfdbf25dbf7d560ac81f22 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 5 Dec 2025 19:26:06 +0200 Subject: [PATCH 16/17] Ensure we remain inside the status --- bittensor_cli/src/commands/subnets/subnets.py | 78 +++++++++---------- tests/e2e_tests/test_unstaking.py | 1 - 2 files changed, 39 insertions(+), 40 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index d0c90477e..509e36957 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -254,50 +254,50 @@ async def _find_event_attributes_in_extrinsic_receipt( mev_protection=mev_protection, ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, None, None + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True, None, None - if not success: - err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - return False, None, None - else: - # Check for MEV shield execution - if mev_protection: - inner_hash = err_msg - mev_shield_id = await extract_mev_shield_id(response) - mev_success, mev_error, response = await wait_for_extrinsic_by_hash( - subtensor=subtensor, - extrinsic_hash=inner_hash, - shield_id=mev_shield_id, - submit_block_hash=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 not success: + err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") + return False, None, None + else: + # Check for MEV shield execution + if mev_protection: + inner_hash = err_msg + mev_shield_id = await extract_mev_shield_id(response) + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=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 + # 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() - if not attributes: - console.print( - ":exclamation: [yellow]A possible error has occurred[/yellow]. The extrinsic reports success, but " - "we are unable to locate the 'NetworkAdded' event inside the extrinsic's events." - "" - ) - else: - console.print( - f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}" + attributes = await _find_event_attributes_in_extrinsic_receipt( + response, "NetworkAdded" ) - return True, int(attributes[0]), ext_id + await print_extrinsic_id(response) + ext_id = await response.get_extrinsic_identifier() + if not attributes: + console.print( + ":exclamation: [yellow]A possible error has occurred[/yellow]. The extrinsic reports success, but " + "we are unable to locate the 'NetworkAdded' event inside the extrinsic's events." + "" + ) + else: + console.print( + f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}" + ) + return True, int(attributes[0]), ext_id # commands diff --git a/tests/e2e_tests/test_unstaking.py b/tests/e2e_tests/test_unstaking.py index a13da8379..8f17341a0 100644 --- a/tests/e2e_tests/test_unstaking.py +++ b/tests/e2e_tests/test_unstaking.py @@ -89,7 +89,6 @@ def test_unstaking(local_chain, wallet_setup): "--no-prompt", ], ) - print(result.stdout, result.stderr) assert "✅ Registered subnetwork with netuid: 2" in result.stdout assert "Your extrinsic has been included" in result.stdout, result.stdout From 6b16a908e39cf507f3015e6b19c4e2a35f9f2b1a Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 5 Dec 2025 14:44:40 -0800 Subject: [PATCH 17/17] update ema calls --- bittensor_cli/src/bittensor/subtensor_interface.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index f48972a9a..10004797f 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -2409,8 +2409,7 @@ async def get_all_subnet_ema_tao_inflow( ema_map[netuid] = Balance.from_rao(0) else: _, raw_ema_value = value - ema_value = fixed_to_float(raw_ema_value) - # TODO @abe is this intentional: float passed as int for from_rao + ema_value = int(fixed_to_float(raw_ema_value)) ema_map[netuid] = Balance.from_rao(ema_value) return ema_map @@ -2441,8 +2440,7 @@ async def get_subnet_ema_tao_inflow( if not value: return Balance.from_rao(0) _, raw_ema_value = value - ema_value = fixed_to_float(raw_ema_value) - # TODO @abe this is a float, but we're passing it as an int for from_rao, is this intentional? + ema_value = int(fixed_to_float(raw_ema_value)) return Balance.from_rao(ema_value) async def get_mev_shield_next_key(