diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 2a5fe6d3d9..bdf5319394 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -3361,125 +3361,6 @@ async def get_mev_shield_next_key( return public_key_bytes - async def get_mev_shield_submission( - self, - submission_id: str, - block: Optional[int] = None, - block_hash: Optional[str] = None, - reuse_block: bool = False, - ) -> Optional[dict[str, str | int | bytes]]: - """ - Retrieves Submission from the MevShield pallet storage. - - If submission_id is provided, returns a single submission. If submission_id is None, returns all submissions from - the storage map. - - Parameters: - submission_id: The hash ID of the submission. Can be a hex string with "0x" prefix or bytes. If None, - returns all submissions. - block: The blockchain block number for the query. - block_hash: The hash of the block to retrieve the stake from. Do not specify if using block or reuse_block. - reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. - - Returns: - If submission_id is provided: A dictionary containing the submission data if found, None otherwise. The - dictionary contains: - - author: The SS58 address of the account that submitted the encrypted extrinsic - - commitment: The blake2_256 hash of the payload_core (as hex string with "0x" prefix) - - ciphertext: The encrypted blob as bytes (format: [u16 kem_len][kem_ct][nonce24][aead_ct]) - - submitted_in: The block number when the submission was created - - If submission_id is None: A dictionary mapping submission IDs (as hex strings) to submission dictionaries. - - Note: - If a specific submission does not exist in storage, this function returns None. If querying all submissions - and none exist, returns an empty dictionary. - """ - block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - submission_id = ( - submission_id[2:] if submission_id.startswith("0x") else submission_id - ) - submission_id_bytes = bytes.fromhex(submission_id) - - query = await self.substrate.query( - module="MevShield", - storage_function="Submissions", - params=[submission_id_bytes], - block_hash=block_hash, - ) - - query_value = getattr(query, "value", query) - if query_value is None or not isinstance(query_value, dict): - return None - - author_raw = cast(Union[bytes, str], query_value.get("author")) - commitment_raw = cast(list[bytes], query_value.get("commitment")) - ciphertext_raw = cast(list[bytes], query_value.get("ciphertext")) - submitted_in = cast(int, query_value.get("submitted_in")) - - autor = decode_account_id(author_raw) - commitment = bytes(commitment_raw[0]) - ciphertext = bytes(ciphertext_raw[0]) - - return { - "author": autor, - "commitment": commitment, - "ciphertext": ciphertext, - "submitted_in": submitted_in, - } - - async def get_mev_shield_submissions( - self, - block: Optional[int] = None, - block_hash: Optional[str] = None, - reuse_block: bool = False, - ) -> Optional[dict[str, dict[str, str | int]]]: - """ - Retrieves all encrypted submissions from the MevShield pallet storage. - - This function queries the MevShield.Submissions storage map and returns all pending encrypted submissions that - have been submitted via submit_encrypted but not yet executed via execute_revealed. - - Parameters: - block: The blockchain block number for the query. If None, uses the current block. - block_hash: The hash of the block to retrieve the submissions from. Do not specify if using block or reuse_block. - reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. - - Returns: - A dictionary mapping wrapper_id (as hex string with "0x" prefix) to submission data dictionaries. Each - submission dictionary contains: - - author: The SS58 address of the account that submitted the encrypted extrinsic - - commitment: The blake2_256 hash of the payload_core as bytes (32 bytes) - - ciphertext: The encrypted blob as bytes (format: [u16 kem_len][kem_ct][nonce24][aead_ct]) - - submitted_in: The block number when the submission was created - - Returns None if no submissions exist in storage at the specified block. - - Note: - Submissions are automatically pruned after KEY_EPOCH_HISTORY blocks (100 blocks) by the pallet's - on_initialize hook. Only submissions that have been submitted but not yet executed will be present in - storage. - """ - block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - query = await self.substrate.query_map( - module="MevShield", - storage_function="Submissions", - block_hash=block_hash, - ) - - result = {} - async for q in query: - key, value = q - value = value.value - result["0x" + bytes(key[0]).hex()] = { - "author": decode_account_id(value.get("author")), - "commitment": bytes(value.get("commitment")[0]), - "ciphertext": bytes(value.get("ciphertext")[0]), - "submitted_in": value.get("submitted_in"), - } - - return result if result else None - async def get_minimum_required_stake(self): """Returns the minimum required stake threshold for nominator cleanup operations. diff --git a/bittensor/core/errors.py b/bittensor/core/errors.py index 438faab0b4..53e2f982ce 100644 --- a/bittensor/core/errors.py +++ b/bittensor/core/errors.py @@ -57,6 +57,8 @@ "TxRateLimitExceeded", "UnknownSynapseError", "UnstakeError", + "SHIELD_VALIDATION_ERRORS", + "map_shield_error", ] @@ -263,3 +265,32 @@ def __init__( ): self.message = message super().__init__(self.message, synapse) + + +SHIELD_VALIDATION_ERRORS = { + "Custom error: 23": ( + "Failed to parse shielded transaction: the ciphertext has an invalid format." + ), + "Custom error: 24": ( + "Invalid encryption key: the key_hash in the ciphertext does not match any known key. " + "The key may have rotated between reading NextKey and submitting the transaction." + ), +} + + +def map_shield_error(raw_message: str) -> str: + """Map a raw shield validation error to a human-readable description. + + Checks the message against known Custom error codes from CheckShieldedTxValidity, + then falls back to detecting a generic ``"invalid"`` subscription status. + Returns the original message unchanged if nothing matches. + """ + for marker, description in SHIELD_VALIDATION_ERRORS.items(): + if marker in raw_message: + return description + if "'result': 'invalid'" in raw_message.lower(): + return ( + "MEV Shield extrinsic rejected as invalid. " + "The key may have rotated between reading NextKey and submission." + ) + return raw_message diff --git a/bittensor/core/extrinsics/asyncex/mev_shield.py b/bittensor/core/extrinsics/asyncex/mev_shield.py index 8aa8dd7043..b0cd644e12 100644 --- a/bittensor/core/extrinsics/asyncex/mev_shield.py +++ b/bittensor/core/extrinsics/asyncex/mev_shield.py @@ -3,10 +3,12 @@ from typing import TYPE_CHECKING, Optional from async_substrate_interface import AsyncExtrinsicReceipt +from async_substrate_interface.errors import SubstrateRequestException +from bittensor.core.errors import map_shield_error from bittensor.core.extrinsics.pallets import MevShield from bittensor.core.extrinsics.utils import ( - get_mev_commitment_and_ciphertext, + get_mev_shielded_ciphertext, get_event_data_by_event_name, ) from bittensor.core.types import ExtrinsicResponse @@ -22,7 +24,6 @@ async def wait_for_extrinsic_by_hash( subtensor: "AsyncSubtensor", extrinsic_hash: str, - shield_id: str, submit_block_hash: str, timeout_blocks: int = 3, ) -> Optional["AsyncExtrinsicReceipt"]: @@ -30,15 +31,11 @@ async def wait_for_extrinsic_by_hash( 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) + function polls subsequent blocks looking for an extrinsic matching the provided hash. 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. @@ -59,34 +56,14 @@ async def wait_for_extrinsic_by_hash( 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 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: - return AsyncExtrinsicReceipt( - substrate=subtensor.substrate, - block_hash=block_hash, - block_number=current_block, - extrinsic_idx=result_idx, - ) + return AsyncExtrinsicReceipt( + substrate=subtensor.substrate, + block_hash=block_hash, + block_number=current_block, + extrinsic_idx=idx, + ) current_block += 1 @@ -125,7 +102,7 @@ async def submit_encrypted_extrinsic( wait_for_finalization: Whether to wait for the finalization of the transaction. wait_for_revealed_execution: Whether to wait for the executed event, indicating that validators have successfully decrypted and executed the inner call. If True, the function will poll subsequent blocks for - the event matching this submission's commitment. + the extrinsic matching this submission. blocks_for_revealed_execution: Maximum number of blocks to poll for the executed event after inclusion. The function checks blocks from start_block to start_block + blocks_for_revealed_execution. Returns immediately if the event is found before the block limit is reached. @@ -138,13 +115,8 @@ async def submit_encrypted_extrinsic( SubstrateRequestException: If the extrinsic fails to be submitted or included. Note: - The encryption uses the public key from NextKey storage, which rotates every block. The payload structure is: - payload_core = signer_bytes (32B) + key_hash (32B Blake2-256 hash of NextKey) + SCALE(call) - plaintext = payload_core + b"\\x01" + signature (64B for sr25519) - commitment = blake2_256(payload_core) - - The key_hash binds the transaction to the key epoch at submission time and replaces nonce-based replay - protection. + The encryption uses the public key from NextKey storage, which rotates every block. The ciphertext wire format + is: [key_hash(16)][u16 kem_len LE][kem_ct][nonce24][aead_ct], where key_hash = twox_128(NextKey). """ try: if sign_with not in ("coldkey", "hotkey"): @@ -186,15 +158,12 @@ async def submit_encrypted_extrinsic( call=call, keypair=inner_signing_keypair, nonce=next_nonce, era=era ) - mev_commitment, mev_ciphertext, payload_core = ( - get_mev_commitment_and_ciphertext( - signed_ext=signed_extrinsic, - ml_kem_768_public_key=ml_kem_768_public_key, - ) + mev_ciphertext = get_mev_shielded_ciphertext( + signed_ext=signed_extrinsic, + ml_kem_768_public_key=ml_kem_768_public_key, ) extrinsic_call = await MevShield(subtensor).submit_encrypted( - commitment=mev_commitment, ciphertext=mev_ciphertext, ) @@ -211,10 +180,8 @@ async def submit_encrypted_extrinsic( if response.success: response.data = { - "commitment": mev_commitment, "ciphertext": mev_ciphertext, "ml_kem_768_public_key": ml_kem_768_public_key, - "payload_core": payload_core, "signed_extrinsic_hash": f"0x{signed_extrinsic.extrinsic_hash.hex()}", } if wait_for_revealed_execution: @@ -230,12 +197,9 @@ async def submit_encrypted_extrinsic( error=RuntimeError("EncryptedSubmitted event not found."), ) - shield_id = event["attributes"]["id"] - response.mev_extrinsic = await wait_for_extrinsic_by_hash( subtensor=subtensor, extrinsic_hash=f"0x{signed_extrinsic.extrinsic_hash.hex()}", - shield_id=shield_id, submit_block_hash=response.extrinsic_receipt.block_hash, timeout_blocks=blocks_for_revealed_execution, ) @@ -251,7 +215,7 @@ async def submit_encrypted_extrinsic( response.message = format_error_message( await response.mev_extrinsic.error_message ) - response.error = RuntimeError(response.message) + response.error = SubstrateRequestException(response.message) response.success = False if raise_error: raise response.error @@ -260,6 +224,10 @@ async def submit_encrypted_extrinsic( "[green]Encrypted extrinsic submitted successfully.[/green]" ) else: + response.message = map_shield_error(str(response.message)) + response.error = SubstrateRequestException(response.message) + if raise_error: + raise response.error logging.error(f"[red]{response.message}[/red]") return response diff --git a/bittensor/core/extrinsics/mev_shield.py b/bittensor/core/extrinsics/mev_shield.py index a21a397270..ab34ae07cf 100644 --- a/bittensor/core/extrinsics/mev_shield.py +++ b/bittensor/core/extrinsics/mev_shield.py @@ -3,10 +3,13 @@ from typing import TYPE_CHECKING, Optional from async_substrate_interface import ExtrinsicReceipt +from async_substrate_interface.errors import SubstrateRequestException + from bittensor.utils import format_error_message +from bittensor.core.errors import map_shield_error from bittensor.core.extrinsics.pallets import MevShield from bittensor.core.extrinsics.utils import ( - get_mev_commitment_and_ciphertext, + get_mev_shielded_ciphertext, get_event_data_by_event_name, ) from bittensor.core.types import ExtrinsicResponse @@ -21,7 +24,6 @@ def wait_for_extrinsic_by_hash( subtensor: "Subtensor", extrinsic_hash: str, - shield_id: str, submit_block_hash: str, timeout_blocks: int = 3, ) -> Optional["ExtrinsicReceipt"]: @@ -29,15 +31,11 @@ def wait_for_extrinsic_by_hash( 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) + function polls subsequent blocks looking for an extrinsic matching the provided hash. 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. @@ -58,34 +56,14 @@ def wait_for_extrinsic_by_hash( block_hash = subtensor.substrate.get_block_hash(current_block) extrinsics = subtensor.substrate.get_extrinsics(block_hash) - result_idx = None for idx, extrinsic in enumerate(extrinsics): - # Success: Inner extrinsic executed if 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: - return ExtrinsicReceipt( - substrate=subtensor.substrate, - block_hash=block_hash, - block_number=current_block, - extrinsic_idx=result_idx, - ) + return ExtrinsicReceipt( + substrate=subtensor.substrate, + block_hash=block_hash, + block_number=current_block, + extrinsic_idx=idx, + ) current_block += 1 @@ -124,7 +102,7 @@ def submit_encrypted_extrinsic( wait_for_finalization: Whether to wait for the finalization of the transaction. wait_for_revealed_execution: Whether to wait for the executed event, indicating that validators have successfully decrypted and executed the inner call. If True, the function will poll subsequent blocks for - the event matching this submission's commitment. + the extrinsic matching this submission. blocks_for_revealed_execution: Maximum number of blocks to poll for the executed event after inclusion. The function checks blocks from start_block to start_block + blocks_for_revealed_execution. Returns immediately if the event is found before the block limit is reached. @@ -137,13 +115,8 @@ def submit_encrypted_extrinsic( SubstrateRequestException: If the extrinsic fails to be submitted or included. Note: - The encryption uses the public key from NextKey storage, which rotates every block. The payload structure is: - payload_core = signer_bytes (32B) + key_hash (32B Blake2-256 hash of NextKey) + SCALE(call) - plaintext = payload_core + b"\\x01" + signature (64B for sr25519) - commitment = blake2_256(payload_core) - - The key_hash binds the transaction to the key epoch at submission time and replaces nonce-based replay - protection. + The encryption uses the public key from NextKey storage, which rotates every block. The ciphertext wire format + is: [key_hash(16)][u16 kem_len LE][kem_ct][nonce24][aead_ct], where key_hash = twox_128(NextKey). """ try: if sign_with not in ["coldkey", "hotkey"]: @@ -183,15 +156,12 @@ def submit_encrypted_extrinsic( call=call, keypair=inner_signing_keypair, nonce=next_nonce, era=era ) - mev_commitment, mev_ciphertext, payload_core = ( - get_mev_commitment_and_ciphertext( - signed_ext=signed_extrinsic, - ml_kem_768_public_key=ml_kem_768_public_key, - ) + mev_ciphertext = get_mev_shielded_ciphertext( + signed_ext=signed_extrinsic, + ml_kem_768_public_key=ml_kem_768_public_key, ) extrinsic_call = MevShield(subtensor).submit_encrypted( - commitment=mev_commitment, ciphertext=mev_ciphertext, ) @@ -208,10 +178,8 @@ def submit_encrypted_extrinsic( if response.success: response.data = { - "commitment": mev_commitment, "ciphertext": mev_ciphertext, "ml_kem_768_public_key": ml_kem_768_public_key, - "payload_core": payload_core, "signed_extrinsic_hash": f"0x{signed_extrinsic.extrinsic_hash.hex()}", } @@ -228,12 +196,9 @@ def submit_encrypted_extrinsic( error=RuntimeError("EncryptedSubmitted event not found."), ) - shield_id = event["attributes"]["id"] - response.mev_extrinsic = wait_for_extrinsic_by_hash( subtensor=subtensor, extrinsic_hash=f"0x{signed_extrinsic.extrinsic_hash.hex()}", - shield_id=shield_id, submit_block_hash=response.extrinsic_receipt.block_hash, timeout_blocks=blocks_for_revealed_execution, ) @@ -249,7 +214,7 @@ def submit_encrypted_extrinsic( response.message = format_error_message( response.mev_extrinsic.error_message # type: ignore ) - response.error = RuntimeError(response.message) + response.error = SubstrateRequestException(response.message) response.success = False if raise_error: raise response.error @@ -258,6 +223,10 @@ def submit_encrypted_extrinsic( "[green]Encrypted extrinsic submitted successfully.[/green]" ) else: + response.message = map_shield_error(str(response.message)) + response.error = SubstrateRequestException(response.message) + if raise_error: + raise response.error logging.error(f"[red]{response.message}[/red]") return response diff --git a/bittensor/core/extrinsics/pallets/mev_shield.py b/bittensor/core/extrinsics/pallets/mev_shield.py index cef4995bc8..ee2a5f8a8a 100644 --- a/bittensor/core/extrinsics/pallets/mev_shield.py +++ b/bittensor/core/extrinsics/pallets/mev_shield.py @@ -15,14 +15,12 @@ class MevShield(_BasePallet): Example: # Sync usage call = MevShield(subtensor).submit_encrypted( - commitment="0x1234...", ciphertext=b"encrypted_data..." ) response = subtensor.sign_and_send_extrinsic(call=call, ...) # Async usage call = await MevShield(async_subtensor).submit_encrypted( - commitment="0x1234...", ciphertext=b"encrypted_data..." ) response = await async_subtensor.sign_and_send_extrinsic(call=call, ...) @@ -30,7 +28,6 @@ class MevShield(_BasePallet): def submit_encrypted( self, - commitment: str, ciphertext: bytes, ) -> Call: """Returns GenericCall instance for MevShield function submit_encrypted. @@ -39,21 +36,18 @@ def submit_encrypted( transaction pool until it is included in a block and decrypted by validators. Parameters: - commitment: The blake2_256 hash of the payload_core (signer + nonce + SCALE(call)). Must be a hex string - with "0x" prefix. ciphertext: The encrypted blob containing the payload and signature. - Format: [u16 kem_len LE][kem_ct][nonce24][aead_ct] + Format: [key_hash(16)][u16 kem_len LE][kem_ct][nonce24][aead_ct] Maximum size: 8192 bytes. Returns: GenericCall instance ready for extrinsic submission. Note: - The commitment is used to verify the ciphertext's content at decryption time. The ciphertext is encrypted - using ML-KEM-768 + XChaCha20Poly1305 with the public key from the NextKey storage item, which rotates every - block. + The ciphertext is encrypted using ML-KEM-768 + XChaCha20Poly1305 with the public key from the NextKey + storage item, which rotates every block. The key_hash prefix (twox_128 of the public key) is validated + on-chain by CheckShieldedTxValidity. """ return self.create_composed_call( - commitment=commitment, ciphertext=ciphertext, ) diff --git a/bittensor/core/extrinsics/utils.py b/bittensor/core/extrinsics/utils.py index 5139c35ff8..6244635efe 100644 --- a/bittensor/core/extrinsics/utils.py +++ b/bittensor/core/extrinsics/utils.py @@ -223,40 +223,25 @@ def apply_pure_proxy_data( return response.with_log("warning") -def get_mev_commitment_and_ciphertext( +def get_mev_shielded_ciphertext( signed_ext: "GenericExtrinsic", ml_kem_768_public_key: bytes, -) -> tuple[str, bytes, bytes]: +) -> bytes: """ - Builds MEV Shield payload and encrypts it using ML-KEM-768 + XChaCha20Poly1305. + Encrypts a signed extrinsic for MEV Shield submission. - This function constructs the payload structure required for MEV Shield encryption and performs the encryption - process. The payload binds the transaction to a specific key epoch using the key_hash, which replaces nonce-based - replay protection. + This function extracts the raw extrinsic bytes and encrypts them using ML-KEM-768 + XChaCha20Poly1305 with the + twox_128 key hash prepended for on-chain validation. Parameters: signed_ext: The signed GenericExtrinsic object representing the inner call to be encrypted and executed. - ml_kem_768_public_key: The ML-KEM-768 public key bytes (1184 bytes) from NextKey storage. This key is used for - encryption and its hash binds the transaction to the key epoch. + ml_kem_768_public_key: The ML-KEM-768 public key bytes (1184 bytes) from NextKey storage. Returns: - A tuple containing: - - commitment_hex: Hex string of the Blake2-256 hash of payload_core (32 bytes). - - ciphertext: Encrypted blob containing plaintext. - - payload_core: Raw payload bytes before encryption. + The encrypted ciphertext bytes in wire format: [key_hash(16)][u16 kem_len LE][kem_ct][nonce24][aead_ct] """ - payload_core = signed_ext.data.data - - plaintext = bytes(payload_core) - - # Getting ciphertext (encrypting plaintext using ML-KEM-768) - ciphertext = encrypt_mlkem768(ml_kem_768_public_key, plaintext) - - # Compute commitment: blake2_256(payload_core) - commitment_hash = hashlib.blake2b(payload_core, digest_size=32).digest() - commitment_hex = "0x" + commitment_hash.hex() - - return commitment_hex, ciphertext, payload_core + plaintext = bytes(signed_ext.data.data) + return encrypt_mlkem768(ml_kem_768_public_key, plaintext, include_key_hash=True) def get_event_data_by_event_name(events: list, event_name: str) -> Optional[dict]: diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 9e586ef1be..1f3d38de71 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -2766,117 +2766,6 @@ def get_mev_shield_next_key(self, block: Optional[int] = None) -> Optional[bytes return public_key_bytes - def get_mev_shield_submission( - self, - submission_id: str, - block: Optional[int] = None, - ) -> Optional[dict[str, str | int | bytes]]: - """ - Retrieves Submission from the MevShield pallet storage. - - If submission_id is provided, returns a single submission. If submission_id is None, returns all submissions from - the storage map. - - Parameters: - submission_id: The hash ID of the submission. Can be a hex string with "0x" prefix or bytes. If None, - returns all submissions. - block: The blockchain block number at which to perform the query. If None, uses the current block. - - Returns: - If submission_id is provided: A dictionary containing the submission data if found, None otherwise. The - dictionary contains: - - author: The SS58 address of the account that submitted the encrypted extrinsic - - commitment: The blake2_256 hash of the payload_core (as hex string with "0x" prefix) - - ciphertext: The encrypted blob as bytes (format: [u16 kem_len][kem_ct][nonce24][aead_ct]) - - submitted_in: The block number when the submission was created - - If submission_id is None: A dictionary mapping submission IDs (as hex strings) to submission dictionaries. - - Note: - If a specific submission does not exist in storage, this function returns None. If querying all submissions - and none exist, returns an empty dictionary. - """ - block_hash = self.determine_block_hash(block=block) - submission_id = ( - submission_id[2:] if submission_id.startswith("0x") else submission_id - ) - submission_id_bytes = bytes.fromhex(submission_id) - - query = self.substrate.query( - module="MevShield", - storage_function="Submissions", - params=[submission_id_bytes], - block_hash=block_hash, - ) - - query_value = getattr(query, "value", query) - if query_value is None or not isinstance(query_value, dict): - return None - - author_raw = cast(Union[bytes, str], query_value.get("author")) - commitment_raw = cast(list[bytes], query_value.get("commitment")) - ciphertext_raw = cast(list[bytes], query_value.get("ciphertext")) - submitted_in = cast(int, query_value.get("submitted_in")) - - autor = decode_account_id(author_raw) - commitment = bytes(commitment_raw[0]) - ciphertext = bytes(ciphertext_raw[0]) - - return { - "author": autor, - "commitment": commitment, - "ciphertext": ciphertext, - "submitted_in": submitted_in, - } - - def get_mev_shield_submissions( - self, - block: Optional[int] = None, - ) -> Optional[dict[str, dict[str, str | int]]]: - """ - Retrieves all encrypted submissions from the MevShield pallet storage. - - This function queries the MevShield.Submissions storage map and returns all pending encrypted submissions that - have been submitted via submit_encrypted but not yet executed via execute_revealed. - - Parameters: - block: The blockchain block number for the query. If None, uses the current block. - - Returns: - A dictionary mapping wrapper_id (as hex string with "0x" prefix) to submission data dictionaries. Each - submission dictionary contains: - - author: The SS58 address of the account that submitted the encrypted extrinsic - - commitment: The blake2_256 hash of the payload_core as bytes (32 bytes) - - ciphertext: The encrypted blob as bytes (format: [u16 kem_len][kem_ct][nonce24][aead_ct]) - - submitted_in: The block number when the submission was created - - Returns None if no submissions exist in storage at the specified block. - - Note: - Submissions are automatically pruned after KEY_EPOCH_HISTORY blocks (100 blocks) by the pallet's - on_initialize hook. Only submissions that have been submitted but not yet executed will be present in - storage. - """ - block_hash = self.determine_block_hash(block=block) - query = self.substrate.query_map( - module="MevShield", - storage_function="Submissions", - block_hash=block_hash, - ) - - result = {} - for q in query: - key, value = q - value = value.value - result["0x" + bytes(key[0]).hex()] = { - "author": decode_account_id(value.get("author")), - "commitment": bytes(value.get("commitment")[0]), - "ciphertext": bytes(value.get("ciphertext")[0]), - "submitted_in": value.get("submitted_in"), - } - - return result if result else None - def get_minimum_required_stake(self) -> Balance: """Returns the minimum required stake threshold for nominator cleanup operations. diff --git a/bittensor/extras/subtensor_api/mev_shield.py b/bittensor/extras/subtensor_api/mev_shield.py index 8484e1a766..5d8e2b396d 100644 --- a/bittensor/extras/subtensor_api/mev_shield.py +++ b/bittensor/extras/subtensor_api/mev_shield.py @@ -11,8 +11,6 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): # Storage queries self.get_mev_shield_current_key = subtensor.get_mev_shield_current_key self.get_mev_shield_next_key = subtensor.get_mev_shield_next_key - self.get_mev_shield_submission = subtensor.get_mev_shield_submission - self.get_mev_shield_submissions = subtensor.get_mev_shield_submissions # Extrinsics self.mev_submit_encrypted = subtensor.mev_submit_encrypted diff --git a/pyproject.toml b/pyproject.toml index bb9aa401c5..db98e1d102 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor" -version = "10.1.0" +version = "10.2.0" description = "Bittensor SDK" readme = "README.md" authors = [ @@ -32,9 +32,9 @@ dependencies = [ "pydantic>=2.3,<3", "scalecodec==1.2.12", "uvicorn", - "bittensor-drand>=1.2.0,<2.0.0", + "bittensor-drand>=1.3.0,<2.0.0", "bittensor-wallet>=4.0.0,<5.0", - "async-substrate-interface>=1.5.15" + "async-substrate-interface>=1.6.2" ] [project.optional-dependencies] diff --git a/tests/e2e_tests/test_mev_shield.py b/tests/e2e_tests/test_mev_shield.py index 05889bbf61..97a8790900 100644 --- a/tests/e2e_tests/test_mev_shield.py +++ b/tests/e2e_tests/test_mev_shield.py @@ -169,3 +169,42 @@ async def test_mev_shield_happy_path_async( ) logging.console.info(f"Stake after: {stake_after}") assert stake_after > stake_before + + +@pytest.mark.parametrize("local_chain", [False], indirect=True) +def test_mev_shield_invalid_ciphertext(subtensor, alice_wallet, local_chain): + """Submitting garbage bytes as ciphertext should be rejected by CheckShieldedTxValidity (Custom(23)).""" + extrinsic_call = pallets.MevShield(subtensor.inner_subtensor).submit_encrypted( + ciphertext=b"\x00", + ) + + logging.console.info(f"Extrinsic call: {extrinsic_call}") + + response = subtensor.sign_and_send_extrinsic( + wallet=alice_wallet, + call=extrinsic_call, + raise_error=False, + ) + assert not response.success + + +@pytest.mark.parametrize("local_chain", [False], indirect=True) +@pytest.mark.asyncio +async def test_mev_shield_invalid_ciphertext_async( + async_subtensor, alice_wallet, local_chain +): + """Async: submitting garbage ciphertext should be rejected by CheckShieldedTxValidity (Custom(23)).""" + extrinsic_call = await pallets.MevShield( + async_subtensor.inner_subtensor + ).submit_encrypted( + ciphertext=b"\x00", + ) + + logging.console.info(f"Extrinsic call: {extrinsic_call}") + + response = await async_subtensor.sign_and_send_extrinsic( + wallet=alice_wallet, + call=extrinsic_call, + raise_error=False, + ) + assert not response.success diff --git a/tests/unit_tests/extrinsics/asyncex/test_mev_shield.py b/tests/unit_tests/extrinsics/asyncex/test_mev_shield.py index 4143252636..057484603a 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_mev_shield.py +++ b/tests/unit_tests/extrinsics/asyncex/test_mev_shield.py @@ -1,7 +1,7 @@ import pytest -from bittensor_wallet import Wallet -from scalecodec.types import GenericCall from async_substrate_interface import AsyncExtrinsicReceipt +from async_substrate_interface.errors import SubstrateRequestException +from scalecodec.types import GenericCall from bittensor.core.extrinsics.asyncex import mev_shield from bittensor.core.types import ExtrinsicResponse @@ -12,7 +12,6 @@ async def test_wait_for_extrinsic_by_hash_success(subtensor, mocker): """Verify that wait_for_extrinsic_by_hash finds the extrinsic by hash.""" # Preps extrinsic_hash = "0x1234567890abcdef" - shield_id = "shield_id_123" submit_block_hash = "0xblockhash" starting_block = 100 current_block = 100 @@ -49,74 +48,6 @@ async def test_wait_for_extrinsic_by_hash_success(subtensor, mocker): result = await mev_shield.wait_for_extrinsic_by_hash( subtensor=subtensor, extrinsic_hash=extrinsic_hash, - shield_id=shield_id, - submit_block_hash=submit_block_hash, - timeout_blocks=3, - ) - - # Asserts - mocked_get_block_number.assert_awaited_once_with(submit_block_hash) - mocked_wait_for_block.assert_awaited_once() - mocked_get_block_hash.assert_awaited_once_with(current_block) - mocked_get_extrinsics.assert_awaited_once_with("0xblockhash101") - mocked_extrinsic_receipt.assert_called_once_with( - substrate=subtensor.substrate, - block_hash="0xblockhash101", - block_number=current_block, - extrinsic_idx=0, - ) - assert result == mock_receipt - - -@pytest.mark.asyncio -async def test_wait_for_extrinsic_by_hash_decryption_failed(subtensor, mocker): - """Verify that wait_for_extrinsic_by_hash finds mark_decryption_failed extrinsic.""" - # Preps - extrinsic_hash = "0x1234567890abcdef" - shield_id = "shield_id_123" - submit_block_hash = "0xblockhash" - starting_block = 100 - current_block = 100 - - mocked_get_block_number = mocker.patch.object( - subtensor.substrate, - "get_block_number", - new=mocker.AsyncMock(return_value=starting_block), - ) - mocked_wait_for_block = mocker.patch.object( - subtensor, "wait_for_block", new=mocker.AsyncMock() - ) - mocked_get_block_hash = mocker.patch.object( - subtensor.substrate, - "get_block_hash", - new=mocker.AsyncMock(return_value="0xblockhash101"), - ) - - mock_extrinsic = mocker.MagicMock() - mock_extrinsic.value = { - "call": { - "call_module": "MevShield", - "call_function": "mark_decryption_failed", - "call_args": [{"name": "id", "value": shield_id}], - } - } - mocked_get_extrinsics = mocker.patch.object( - subtensor.substrate, - "get_extrinsics", - new=mocker.AsyncMock(return_value=[mock_extrinsic]), - ) - - mock_receipt = mocker.MagicMock(spec=AsyncExtrinsicReceipt) - mocked_extrinsic_receipt = mocker.patch( - "bittensor.core.extrinsics.asyncex.mev_shield.AsyncExtrinsicReceipt", - return_value=mock_receipt, - ) - - # Call - result = await mev_shield.wait_for_extrinsic_by_hash( - subtensor=subtensor, - extrinsic_hash=extrinsic_hash, - shield_id=shield_id, submit_block_hash=submit_block_hash, timeout_blocks=3, ) @@ -140,7 +71,6 @@ async def test_wait_for_extrinsic_by_hash_timeout(subtensor, mocker): """Verify that wait_for_extrinsic_by_hash returns None on timeout.""" # Preps extrinsic_hash = "0x1234567890abcdef" - shield_id = "shield_id_123" submit_block_hash = "0xblockhash" starting_block = 100 @@ -167,7 +97,6 @@ async def test_wait_for_extrinsic_by_hash_timeout(subtensor, mocker): result = await mev_shield.wait_for_extrinsic_by_hash( subtensor=subtensor, extrinsic_hash=extrinsic_hash, - shield_id=shield_id, submit_block_hash=submit_block_hash, timeout_blocks=3, ) @@ -192,14 +121,11 @@ async def test_submit_encrypted_extrinsic_success_with_revealed_execution( ) ml_kem_768_public_key = b"fake_ml_kem_key" * 74 # 1184 bytes - mev_commitment = "0xcommitment" mev_ciphertext = b"fake_ciphertext" - payload_core = b"fake_payload" signed_extrinsic_hash_hex = "abcdef123456" signed_extrinsic_hash = f"0x{signed_extrinsic_hash_hex}" current_nonce = 5 next_nonce = 6 - shield_id = "shield_id_123" block_hash = "0xblockhash" mocked_unlock_wallet = mocker.patch.object( @@ -224,9 +150,9 @@ async def test_submit_encrypted_extrinsic_success_with_revealed_execution( "create_signed_extrinsic", new=mocker.AsyncMock(return_value=mock_signed_extrinsic), ) - mocked_get_mev_commitment = mocker.patch( - "bittensor.core.extrinsics.asyncex.mev_shield.get_mev_commitment_and_ciphertext", - return_value=(mev_commitment, mev_ciphertext, payload_core), + mocked_get_mev_shielded_ciphertext = mocker.patch( + "bittensor.core.extrinsics.asyncex.mev_shield.get_mev_shielded_ciphertext", + return_value=mev_ciphertext, ) mocked_mev_shield = mocker.patch( "bittensor.core.extrinsics.asyncex.mev_shield.MevShield" @@ -242,7 +168,7 @@ async def test_submit_encrypted_extrinsic_success_with_revealed_execution( { "module_id": "mevShield", "event_id": "EncryptedSubmitted", - "attributes": {"id": shield_id}, + "attributes": {"id": "shield_id_123"}, } ] mock_response = mocker.MagicMock(spec=ExtrinsicResponse) @@ -264,7 +190,7 @@ async def test_submit_encrypted_extrinsic_success_with_revealed_execution( return_value={ "module_id": "mevShield", "event_id": "EncryptedSubmitted", - "attributes": {"id": shield_id}, + "attributes": {"id": "shield_id_123"}, }, ) @@ -296,13 +222,12 @@ async def test_submit_encrypted_extrinsic_success_with_revealed_execution( nonce=next_nonce, era="00", ) - mocked_get_mev_commitment.assert_called_once_with( + mocked_get_mev_shielded_ciphertext.assert_called_once_with( signed_ext=mock_signed_extrinsic, ml_kem_768_public_key=ml_kem_768_public_key, ) mocked_mev_shield.assert_called_once_with(subtensor) mock_mev_shield_instance.submit_encrypted.assert_awaited_once_with( - commitment=mev_commitment, ciphertext=mev_ciphertext, ) mocked_sign_and_send_extrinsic.assert_awaited_once_with( @@ -322,7 +247,6 @@ async def test_submit_encrypted_extrinsic_success_with_revealed_execution( mocked_wait_for_extrinsic.assert_awaited_once_with( subtensor=subtensor, extrinsic_hash=signed_extrinsic_hash, - shield_id=shield_id, submit_block_hash=block_hash, timeout_blocks=3, ) @@ -342,9 +266,7 @@ async def test_submit_encrypted_extrinsic_success_without_revealed_execution( ) ml_kem_768_public_key = b"fake_ml_kem_key" * 74 - mev_commitment = "0xcommitment" mev_ciphertext = b"fake_ciphertext" - payload_core = b"fake_payload" signed_extrinsic_hash_hex = "abcdef123456" signed_extrinsic_hash = f"0x{signed_extrinsic_hash_hex}" current_nonce = 5 @@ -372,9 +294,9 @@ async def test_submit_encrypted_extrinsic_success_without_revealed_execution( "create_signed_extrinsic", new=mocker.AsyncMock(return_value=mock_signed_extrinsic), ) - mocked_get_mev_commitment = mocker.patch( - "bittensor.core.extrinsics.asyncex.mev_shield.get_mev_commitment_and_ciphertext", - return_value=(mev_commitment, mev_ciphertext, payload_core), + mocked_get_mev_shielded_ciphertext = mocker.patch( + "bittensor.core.extrinsics.asyncex.mev_shield.get_mev_shielded_ciphertext", + return_value=mev_ciphertext, ) mocked_mev_shield = mocker.patch( "bittensor.core.extrinsics.asyncex.mev_shield.MevShield" @@ -417,13 +339,12 @@ async def test_submit_encrypted_extrinsic_success_without_revealed_execution( nonce=next_nonce, era="00", ) - mocked_get_mev_commitment.assert_called_once_with( + mocked_get_mev_shielded_ciphertext.assert_called_once_with( signed_ext=mock_signed_extrinsic, ml_kem_768_public_key=ml_kem_768_public_key, ) mocked_mev_shield.assert_called_once_with(subtensor) mock_mev_shield_instance.submit_encrypted.assert_awaited_once_with( - commitment=mev_commitment, ciphertext=mev_ciphertext, ) mocked_sign_and_send_extrinsic.assert_awaited_once_with( @@ -437,10 +358,8 @@ async def test_submit_encrypted_extrinsic_success_without_revealed_execution( wait_for_finalization=False, ) assert result == mock_response - assert result.data["commitment"] == mev_commitment assert result.data["ciphertext"] == mev_ciphertext assert result.data["ml_kem_768_public_key"] == ml_kem_768_public_key - assert result.data["payload_core"] == payload_core assert result.data["signed_extrinsic_hash"] == signed_extrinsic_hash @@ -561,9 +480,7 @@ async def test_submit_encrypted_extrinsic_encrypted_submitted_event_not_found( ) ml_kem_768_public_key = b"fake_ml_kem_key" * 74 - mev_commitment = "0xcommitment" mev_ciphertext = b"fake_ciphertext" - payload_core = b"fake_payload" current_nonce = 5 next_nonce = 6 @@ -589,9 +506,9 @@ async def test_submit_encrypted_extrinsic_encrypted_submitted_event_not_found( "create_signed_extrinsic", new=mocker.AsyncMock(return_value=mock_signed_extrinsic), ) - mocked_get_mev_commitment = mocker.patch( - "bittensor.core.extrinsics.asyncex.mev_shield.get_mev_commitment_and_ciphertext", - return_value=(mev_commitment, mev_ciphertext, payload_core), + mocked_get_mev_shielded_ciphertext = mocker.patch( + "bittensor.core.extrinsics.asyncex.mev_shield.get_mev_shielded_ciphertext", + return_value=mev_ciphertext, ) mocked_mev_shield = mocker.patch( "bittensor.core.extrinsics.asyncex.mev_shield.MevShield" @@ -642,13 +559,12 @@ async def test_submit_encrypted_extrinsic_encrypted_submitted_event_not_found( nonce=next_nonce, era="00", ) - mocked_get_mev_commitment.assert_called_once_with( + mocked_get_mev_shielded_ciphertext.assert_called_once_with( signed_ext=mock_signed_extrinsic, ml_kem_768_public_key=ml_kem_768_public_key, ) mocked_mev_shield.assert_called_once_with(subtensor) mock_mev_shield_instance.submit_encrypted.assert_awaited_once_with( - commitment=mev_commitment, ciphertext=mev_ciphertext, ) mocked_sign_and_send_extrinsic.assert_awaited_once_with( @@ -682,14 +598,11 @@ async def test_submit_encrypted_extrinsic_failed_to_find_outcome( ) ml_kem_768_public_key = b"fake_ml_kem_key" * 74 - mev_commitment = "0xcommitment" mev_ciphertext = b"fake_ciphertext" - payload_core = b"fake_payload" signed_extrinsic_hash_hex = "abcdef123456" signed_extrinsic_hash = f"0x{signed_extrinsic_hash_hex}" current_nonce = 5 next_nonce = 6 - shield_id = "shield_id_123" block_hash = "0xblockhash" mocked_unlock_wallet = mocker.patch.object( @@ -714,9 +627,9 @@ async def test_submit_encrypted_extrinsic_failed_to_find_outcome( "create_signed_extrinsic", new=mocker.AsyncMock(return_value=mock_signed_extrinsic), ) - mocked_get_mev_commitment = mocker.patch( - "bittensor.core.extrinsics.asyncex.mev_shield.get_mev_commitment_and_ciphertext", - return_value=(mev_commitment, mev_ciphertext, payload_core), + mocked_get_mev_shielded_ciphertext = mocker.patch( + "bittensor.core.extrinsics.asyncex.mev_shield.get_mev_shielded_ciphertext", + return_value=mev_ciphertext, ) mocked_mev_shield = mocker.patch( "bittensor.core.extrinsics.asyncex.mev_shield.MevShield" @@ -732,7 +645,7 @@ async def test_submit_encrypted_extrinsic_failed_to_find_outcome( { "module_id": "mevShield", "event_id": "EncryptedSubmitted", - "attributes": {"id": shield_id}, + "attributes": {"id": "shield_id_123"}, } ] mock_response = mocker.MagicMock(spec=ExtrinsicResponse) @@ -748,7 +661,7 @@ async def test_submit_encrypted_extrinsic_failed_to_find_outcome( return_value={ "module_id": "mevShield", "event_id": "EncryptedSubmitted", - "attributes": {"id": shield_id}, + "attributes": {"id": "shield_id_123"}, }, ) mocked_wait_for_extrinsic = mocker.patch( @@ -783,13 +696,12 @@ async def test_submit_encrypted_extrinsic_failed_to_find_outcome( nonce=next_nonce, era="00", ) - mocked_get_mev_commitment.assert_called_once_with( + mocked_get_mev_shielded_ciphertext.assert_called_once_with( signed_ext=mock_signed_extrinsic, ml_kem_768_public_key=ml_kem_768_public_key, ) mocked_mev_shield.assert_called_once_with(subtensor) mock_mev_shield_instance.submit_encrypted.assert_awaited_once_with( - commitment=mev_commitment, ciphertext=mev_ciphertext, ) mocked_sign_and_send_extrinsic.assert_awaited_once_with( @@ -809,7 +721,6 @@ async def test_submit_encrypted_extrinsic_failed_to_find_outcome( mocked_wait_for_extrinsic.assert_awaited_once_with( subtensor=subtensor, extrinsic_hash=signed_extrinsic_hash, - shield_id=shield_id, submit_block_hash=block_hash, timeout_blocks=3, ) @@ -830,14 +741,11 @@ async def test_submit_encrypted_extrinsic_execution_failure( ) ml_kem_768_public_key = b"fake_ml_kem_key" * 74 - mev_commitment = "0xcommitment" mev_ciphertext = b"fake_ciphertext" - payload_core = b"fake_payload" signed_extrinsic_hash_hex = "abcdef123456" signed_extrinsic_hash = f"0x{signed_extrinsic_hash_hex}" current_nonce = 5 next_nonce = 6 - shield_id = "shield_id_123" block_hash = "0xblockhash" error_message = "Execution failed" formatted_error = "Formatted error: Execution failed" @@ -864,9 +772,9 @@ async def test_submit_encrypted_extrinsic_execution_failure( "create_signed_extrinsic", new=mocker.AsyncMock(return_value=mock_signed_extrinsic), ) - mocked_get_mev_commitment = mocker.patch( - "bittensor.core.extrinsics.asyncex.mev_shield.get_mev_commitment_and_ciphertext", - return_value=(mev_commitment, mev_ciphertext, payload_core), + mocked_get_mev_shielded_ciphertext = mocker.patch( + "bittensor.core.extrinsics.asyncex.mev_shield.get_mev_shielded_ciphertext", + return_value=mev_ciphertext, ) mocked_mev_shield = mocker.patch( "bittensor.core.extrinsics.asyncex.mev_shield.MevShield" @@ -882,7 +790,7 @@ async def test_submit_encrypted_extrinsic_execution_failure( { "module_id": "mevShield", "event_id": "EncryptedSubmitted", - "attributes": {"id": shield_id}, + "attributes": {"id": "shield_id_123"}, } ] mock_response = mocker.MagicMock(spec=ExtrinsicResponse) @@ -905,7 +813,7 @@ async def test_submit_encrypted_extrinsic_execution_failure( return_value={ "module_id": "mevShield", "event_id": "EncryptedSubmitted", - "attributes": {"id": shield_id}, + "attributes": {"id": "shield_id_123"}, }, ) mocked_format_error_message = mocker.patch( @@ -940,13 +848,12 @@ async def test_submit_encrypted_extrinsic_execution_failure( nonce=next_nonce, era="00", ) - mocked_get_mev_commitment.assert_called_once_with( + mocked_get_mev_shielded_ciphertext.assert_called_once_with( signed_ext=mock_signed_extrinsic, ml_kem_768_public_key=ml_kem_768_public_key, ) mocked_mev_shield.assert_called_once_with(subtensor) mock_mev_shield_instance.submit_encrypted.assert_awaited_once_with( - commitment=mev_commitment, ciphertext=mev_ciphertext, ) mocked_sign_and_send_extrinsic.assert_awaited_once_with( @@ -966,13 +873,12 @@ async def test_submit_encrypted_extrinsic_execution_failure( mocked_wait_for_extrinsic.assert_awaited_once_with( subtensor=subtensor, extrinsic_hash=signed_extrinsic_hash, - shield_id=shield_id, submit_block_hash=block_hash, timeout_blocks=3, ) mocked_format_error_message.assert_called_once_with(error_message) assert result.success is False - assert isinstance(result.error, RuntimeError) + assert isinstance(result.error, SubstrateRequestException) assert result.message == formatted_error @@ -988,9 +894,7 @@ async def test_submit_encrypted_extrinsic_sign_and_send_failure( ) ml_kem_768_public_key = b"fake_ml_kem_key" * 74 - mev_commitment = "0xcommitment" mev_ciphertext = b"fake_ciphertext" - payload_core = b"fake_payload" current_nonce = 5 next_nonce = 6 @@ -1016,9 +920,9 @@ async def test_submit_encrypted_extrinsic_sign_and_send_failure( "create_signed_extrinsic", new=mocker.AsyncMock(return_value=mock_signed_extrinsic), ) - mocked_get_mev_commitment = mocker.patch( - "bittensor.core.extrinsics.asyncex.mev_shield.get_mev_commitment_and_ciphertext", - return_value=(mev_commitment, mev_ciphertext, payload_core), + mocked_get_mev_shielded_ciphertext = mocker.patch( + "bittensor.core.extrinsics.asyncex.mev_shield.get_mev_shielded_ciphertext", + return_value=mev_ciphertext, ) mocked_mev_shield = mocker.patch( "bittensor.core.extrinsics.asyncex.mev_shield.MevShield" @@ -1060,13 +964,12 @@ async def test_submit_encrypted_extrinsic_sign_and_send_failure( nonce=next_nonce, era="00", ) - mocked_get_mev_commitment.assert_called_once_with( + mocked_get_mev_shielded_ciphertext.assert_called_once_with( signed_ext=mock_signed_extrinsic, ml_kem_768_public_key=ml_kem_768_public_key, ) mocked_mev_shield.assert_called_once_with(subtensor) mock_mev_shield_instance.submit_encrypted.assert_awaited_once_with( - commitment=mev_commitment, ciphertext=mev_ciphertext, ) mocked_sign_and_send_extrinsic.assert_awaited_once_with( @@ -1092,9 +995,7 @@ async def test_submit_encrypted_extrinsic_with_hotkey(subtensor, fake_wallet, mo fake_wallet.hotkey.ss58_address = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" ml_kem_768_public_key = b"fake_ml_kem_key" * 74 - mev_commitment = "0xcommitment" mev_ciphertext = b"fake_ciphertext" - payload_core = b"fake_payload" current_nonce = 5 next_nonce = 6 @@ -1120,9 +1021,9 @@ async def test_submit_encrypted_extrinsic_with_hotkey(subtensor, fake_wallet, mo "create_signed_extrinsic", new=mocker.AsyncMock(return_value=mock_signed_extrinsic), ) - mocked_get_mev_commitment = mocker.patch( - "bittensor.core.extrinsics.asyncex.mev_shield.get_mev_commitment_and_ciphertext", - return_value=(mev_commitment, mev_ciphertext, payload_core), + mocked_get_mev_shielded_ciphertext = mocker.patch( + "bittensor.core.extrinsics.asyncex.mev_shield.get_mev_shielded_ciphertext", + return_value=mev_ciphertext, ) mocked_mev_shield = mocker.patch( "bittensor.core.extrinsics.asyncex.mev_shield.MevShield" @@ -1165,13 +1066,12 @@ async def test_submit_encrypted_extrinsic_with_hotkey(subtensor, fake_wallet, mo nonce=next_nonce, era="00", ) - mocked_get_mev_commitment.assert_called_once_with( + mocked_get_mev_shielded_ciphertext.assert_called_once_with( signed_ext=mock_signed_extrinsic, ml_kem_768_public_key=ml_kem_768_public_key, ) mocked_mev_shield.assert_called_once_with(subtensor) mock_mev_shield_instance.submit_encrypted.assert_awaited_once_with( - commitment=mev_commitment, ciphertext=mev_ciphertext, ) mocked_sign_and_send_extrinsic.assert_awaited_once_with( @@ -1197,9 +1097,7 @@ async def test_submit_encrypted_extrinsic_with_period(subtensor, fake_wallet, mo ) ml_kem_768_public_key = b"fake_ml_kem_key" * 74 - mev_commitment = "0xcommitment" mev_ciphertext = b"fake_ciphertext" - payload_core = b"fake_payload" current_nonce = 5 next_nonce = 6 period = 64 @@ -1226,9 +1124,9 @@ async def test_submit_encrypted_extrinsic_with_period(subtensor, fake_wallet, mo "create_signed_extrinsic", new=mocker.AsyncMock(return_value=mock_signed_extrinsic), ) - mocked_get_mev_commitment = mocker.patch( - "bittensor.core.extrinsics.asyncex.mev_shield.get_mev_commitment_and_ciphertext", - return_value=(mev_commitment, mev_ciphertext, payload_core), + mocked_get_mev_shielded_ciphertext = mocker.patch( + "bittensor.core.extrinsics.asyncex.mev_shield.get_mev_shielded_ciphertext", + return_value=mev_ciphertext, ) mocked_mev_shield = mocker.patch( "bittensor.core.extrinsics.asyncex.mev_shield.MevShield" @@ -1271,13 +1169,12 @@ async def test_submit_encrypted_extrinsic_with_period(subtensor, fake_wallet, mo nonce=next_nonce, era={"period": period}, ) - mocked_get_mev_commitment.assert_called_once_with( + mocked_get_mev_shielded_ciphertext.assert_called_once_with( signed_ext=mock_signed_extrinsic, ml_kem_768_public_key=ml_kem_768_public_key, ) mocked_mev_shield.assert_called_once_with(subtensor) mock_mev_shield_instance.submit_encrypted.assert_awaited_once_with( - commitment=mev_commitment, ciphertext=mev_ciphertext, ) mocked_sign_and_send_extrinsic.assert_awaited_once_with( diff --git a/tests/unit_tests/extrinsics/test_mev_shield.py b/tests/unit_tests/extrinsics/test_mev_shield.py index 44b7cec7a9..a2642b0764 100644 --- a/tests/unit_tests/extrinsics/test_mev_shield.py +++ b/tests/unit_tests/extrinsics/test_mev_shield.py @@ -1,6 +1,7 @@ from bittensor_wallet import Wallet from scalecodec.types import GenericCall from async_substrate_interface import ExtrinsicReceipt +from async_substrate_interface.errors import SubstrateRequestException from bittensor.core.extrinsics import mev_shield from bittensor.core.types import ExtrinsicResponse @@ -10,7 +11,6 @@ def test_wait_for_extrinsic_by_hash_success(subtensor, mocker): """Verify that wait_for_extrinsic_by_hash finds the extrinsic by hash.""" # Preps extrinsic_hash = "0x1234567890abcdef" - shield_id = "shield_id_123" submit_block_hash = "0xblockhash" starting_block = 100 current_block = 100 @@ -41,67 +41,6 @@ def test_wait_for_extrinsic_by_hash_success(subtensor, mocker): result = mev_shield.wait_for_extrinsic_by_hash( subtensor=subtensor, extrinsic_hash=extrinsic_hash, - shield_id=shield_id, - submit_block_hash=submit_block_hash, - timeout_blocks=3, - ) - - # Asserts - mocked_get_block_number.assert_called_once_with(submit_block_hash) - mocked_wait_for_block.assert_called_once() - mocked_get_block_hash.assert_called_once_with(current_block) - mocked_get_extrinsics.assert_called_once_with("0xblockhash101") - mocked_extrinsic_receipt.assert_called_once_with( - substrate=subtensor.substrate, - block_hash="0xblockhash101", - block_number=current_block, - extrinsic_idx=0, - ) - assert result == mock_receipt - - -def test_wait_for_extrinsic_by_hash_decryption_failed(subtensor, mocker): - """Verify that wait_for_extrinsic_by_hash finds mark_decryption_failed extrinsic.""" - # Preps - extrinsic_hash = "0x1234567890abcdef" - shield_id = "shield_id_123" - submit_block_hash = "0xblockhash" - starting_block = 100 - current_block = 100 - - mocked_get_block_number = mocker.patch.object( - subtensor.substrate, "get_block_number", return_value=starting_block - ) - mocked_wait_for_block = mocker.patch.object(subtensor, "wait_for_block") - mocked_get_block_hash = mocker.patch.object( - subtensor.substrate, "get_block_hash", return_value="0xblockhash101" - ) - - mock_extrinsic = mocker.MagicMock() - mock_extrinsic.value = { - "call": { - "call_module": "MevShield", - "call_function": "mark_decryption_failed", - "call_args": [{"name": "id", "value": shield_id}], - } - } - mocked_get_extrinsics = mocker.patch.object( - subtensor.substrate, - "get_extrinsics", - return_value=[mock_extrinsic], - ) - - mock_receipt = mocker.MagicMock(spec=ExtrinsicReceipt) - mocked_extrinsic_receipt = mocker.patch( - "bittensor.core.extrinsics.mev_shield.ExtrinsicReceipt", - return_value=mock_receipt, - ) - - # Call - result = mev_shield.wait_for_extrinsic_by_hash( - subtensor=subtensor, - extrinsic_hash=extrinsic_hash, - shield_id=shield_id, submit_block_hash=submit_block_hash, timeout_blocks=3, ) @@ -124,7 +63,6 @@ def test_wait_for_extrinsic_by_hash_timeout(subtensor, mocker): """Verify that wait_for_extrinsic_by_hash returns None on timeout.""" # Preps extrinsic_hash = "0x1234567890abcdef" - shield_id = "shield_id_123" submit_block_hash = "0xblockhash" starting_block = 100 @@ -145,7 +83,6 @@ def test_wait_for_extrinsic_by_hash_timeout(subtensor, mocker): result = mev_shield.wait_for_extrinsic_by_hash( subtensor=subtensor, extrinsic_hash=extrinsic_hash, - shield_id=shield_id, submit_block_hash=submit_block_hash, timeout_blocks=3, ) @@ -169,14 +106,11 @@ def test_submit_encrypted_extrinsic_success_with_revealed_execution( ) ml_kem_768_public_key = b"fake_ml_kem_key" * 74 # 1184 bytes - mev_commitment = "0xcommitment" mev_ciphertext = b"fake_ciphertext" - payload_core = b"fake_payload" signed_extrinsic_hash_hex = "abcdef123456" signed_extrinsic_hash = f"0x{signed_extrinsic_hash_hex}" current_nonce = 5 next_nonce = 6 - shield_id = "shield_id_123" block_hash = "0xblockhash" mocked_unlock_wallet = mocker.patch.object( @@ -199,9 +133,9 @@ def test_submit_encrypted_extrinsic_success_with_revealed_execution( "create_signed_extrinsic", return_value=mock_signed_extrinsic, ) - mocked_get_mev_commitment = mocker.patch( - "bittensor.core.extrinsics.mev_shield.get_mev_commitment_and_ciphertext", - return_value=(mev_commitment, mev_ciphertext, payload_core), + mocked_get_mev_shielded_ciphertext = mocker.patch( + "bittensor.core.extrinsics.mev_shield.get_mev_shielded_ciphertext", + return_value=mev_ciphertext, ) mocked_mev_shield = mocker.patch("bittensor.core.extrinsics.mev_shield.MevShield") mock_mev_shield_instance = mocker.MagicMock() @@ -217,7 +151,7 @@ def test_submit_encrypted_extrinsic_success_with_revealed_execution( { "module_id": "mevShield", "event_id": "EncryptedSubmitted", - "attributes": {"id": shield_id}, + "attributes": {"id": "shield_id_123"}, } ] @@ -232,7 +166,7 @@ def test_submit_encrypted_extrinsic_success_with_revealed_execution( return_value={ "module_id": "mevShield", "event_id": "EncryptedSubmitted", - "attributes": {"id": shield_id}, + "attributes": {"id": "shield_id_123"}, }, ) @@ -264,13 +198,12 @@ def test_submit_encrypted_extrinsic_success_with_revealed_execution( nonce=next_nonce, era="00", ) - mocked_get_mev_commitment.assert_called_once_with( + mocked_get_mev_shielded_ciphertext.assert_called_once_with( signed_ext=mock_signed_extrinsic, ml_kem_768_public_key=ml_kem_768_public_key, ) mocked_mev_shield.assert_called_once_with(subtensor) mock_mev_shield_instance.submit_encrypted.assert_called_once_with( - commitment=mev_commitment, ciphertext=mev_ciphertext, ) mocked_sign_and_send_extrinsic.assert_called_once_with( @@ -290,16 +223,13 @@ def test_submit_encrypted_extrinsic_success_with_revealed_execution( mocked_wait_for_extrinsic.assert_called_once_with( subtensor=subtensor, extrinsic_hash=signed_extrinsic_hash, - shield_id=shield_id, submit_block_hash=block_hash, timeout_blocks=3, ) assert result == mock_response assert result.mev_extrinsic == mock_mev_extrinsic - assert result.data["commitment"] == mev_commitment assert result.data["ciphertext"] == mev_ciphertext assert result.data["ml_kem_768_public_key"] == ml_kem_768_public_key - assert result.data["payload_core"] == payload_core assert result.data["signed_extrinsic_hash"] == signed_extrinsic_hash @@ -314,9 +244,7 @@ def test_submit_encrypted_extrinsic_success_without_revealed_execution( ) ml_kem_768_public_key = b"fake_ml_kem_key" * 74 - mev_commitment = "0xcommitment" mev_ciphertext = b"fake_ciphertext" - payload_core = b"fake_payload" signed_extrinsic_hash_hex = "abcdef123456" signed_extrinsic_hash = f"0x{signed_extrinsic_hash_hex}" current_nonce = 5 @@ -342,9 +270,9 @@ def test_submit_encrypted_extrinsic_success_without_revealed_execution( "create_signed_extrinsic", return_value=mock_signed_extrinsic, ) - mocked_get_mev_commitment = mocker.patch( - "bittensor.core.extrinsics.mev_shield.get_mev_commitment_and_ciphertext", - return_value=(mev_commitment, mev_ciphertext, payload_core), + mocked_get_mev_shielded_ciphertext = mocker.patch( + "bittensor.core.extrinsics.mev_shield.get_mev_shielded_ciphertext", + return_value=mev_ciphertext, ) mocked_mev_shield = mocker.patch("bittensor.core.extrinsics.mev_shield.MevShield") mock_mev_shield_instance = mocker.MagicMock() @@ -382,13 +310,12 @@ def test_submit_encrypted_extrinsic_success_without_revealed_execution( nonce=next_nonce, era="00", ) - mocked_get_mev_commitment.assert_called_once_with( + mocked_get_mev_shielded_ciphertext.assert_called_once_with( signed_ext=mock_signed_extrinsic, ml_kem_768_public_key=ml_kem_768_public_key, ) mocked_mev_shield.assert_called_once_with(subtensor) mock_mev_shield_instance.submit_encrypted.assert_called_once_with( - commitment=mev_commitment, ciphertext=mev_ciphertext, ) mocked_sign_and_send_extrinsic.assert_called_once_with( @@ -402,10 +329,8 @@ def test_submit_encrypted_extrinsic_success_without_revealed_execution( wait_for_finalization=False, ) assert result == mock_response - assert result.data["commitment"] == mev_commitment assert result.data["ciphertext"] == mev_ciphertext assert result.data["ml_kem_768_public_key"] == ml_kem_768_public_key - assert result.data["payload_core"] == payload_core assert result.data["signed_extrinsic_hash"] == signed_extrinsic_hash @@ -517,9 +442,7 @@ def test_submit_encrypted_extrinsic_encrypted_submitted_event_not_found( ) ml_kem_768_public_key = b"fake_ml_kem_key" * 74 - mev_commitment = "0xcommitment" mev_ciphertext = b"fake_ciphertext" - payload_core = b"fake_payload" current_nonce = 5 next_nonce = 6 @@ -543,9 +466,9 @@ def test_submit_encrypted_extrinsic_encrypted_submitted_event_not_found( "create_signed_extrinsic", return_value=mock_signed_extrinsic, ) - mocked_get_mev_commitment = mocker.patch( - "bittensor.core.extrinsics.mev_shield.get_mev_commitment_and_ciphertext", - return_value=(mev_commitment, mev_ciphertext, payload_core), + mocked_get_mev_shielded_ciphertext = mocker.patch( + "bittensor.core.extrinsics.mev_shield.get_mev_shielded_ciphertext", + return_value=mev_ciphertext, ) mocked_mev_shield = mocker.patch("bittensor.core.extrinsics.mev_shield.MevShield") mock_mev_shield_instance = mocker.MagicMock() @@ -589,13 +512,12 @@ def test_submit_encrypted_extrinsic_encrypted_submitted_event_not_found( nonce=next_nonce, era="00", ) - mocked_get_mev_commitment.assert_called_once_with( + mocked_get_mev_shielded_ciphertext.assert_called_once_with( signed_ext=mock_signed_extrinsic, ml_kem_768_public_key=ml_kem_768_public_key, ) mocked_mev_shield.assert_called_once_with(subtensor) mock_mev_shield_instance.submit_encrypted.assert_called_once_with( - commitment=mev_commitment, ciphertext=mev_ciphertext, ) mocked_sign_and_send_extrinsic.assert_called_once_with( @@ -628,14 +550,11 @@ def test_submit_encrypted_extrinsic_failed_to_find_outcome( ) ml_kem_768_public_key = b"fake_ml_kem_key" * 74 - mev_commitment = "0xcommitment" mev_ciphertext = b"fake_ciphertext" - payload_core = b"fake_payload" signed_extrinsic_hash_hex = "abcdef123456" signed_extrinsic_hash = f"0x{signed_extrinsic_hash_hex}" current_nonce = 5 next_nonce = 6 - shield_id = "shield_id_123" block_hash = "0xblockhash" mocked_unlock_wallet = mocker.patch.object( @@ -658,9 +577,9 @@ def test_submit_encrypted_extrinsic_failed_to_find_outcome( "create_signed_extrinsic", return_value=mock_signed_extrinsic, ) - mocked_get_mev_commitment = mocker.patch( - "bittensor.core.extrinsics.mev_shield.get_mev_commitment_and_ciphertext", - return_value=(mev_commitment, mev_ciphertext, payload_core), + mocked_get_mev_shielded_ciphertext = mocker.patch( + "bittensor.core.extrinsics.mev_shield.get_mev_shielded_ciphertext", + return_value=mev_ciphertext, ) mocked_mev_shield = mocker.patch("bittensor.core.extrinsics.mev_shield.MevShield") mock_mev_shield_instance = mocker.MagicMock() @@ -676,7 +595,7 @@ def test_submit_encrypted_extrinsic_failed_to_find_outcome( { "module_id": "mevShield", "event_id": "EncryptedSubmitted", - "attributes": {"id": shield_id}, + "attributes": {"id": "shield_id_123"}, } ] @@ -685,7 +604,7 @@ def test_submit_encrypted_extrinsic_failed_to_find_outcome( return_value={ "module_id": "mevShield", "event_id": "EncryptedSubmitted", - "attributes": {"id": shield_id}, + "attributes": {"id": "shield_id_123"}, }, ) mocked_wait_for_extrinsic = mocker.patch( @@ -719,13 +638,12 @@ def test_submit_encrypted_extrinsic_failed_to_find_outcome( nonce=next_nonce, era="00", ) - mocked_get_mev_commitment.assert_called_once_with( + mocked_get_mev_shielded_ciphertext.assert_called_once_with( signed_ext=mock_signed_extrinsic, ml_kem_768_public_key=ml_kem_768_public_key, ) mocked_mev_shield.assert_called_once_with(subtensor) mock_mev_shield_instance.submit_encrypted.assert_called_once_with( - commitment=mev_commitment, ciphertext=mev_ciphertext, ) mocked_sign_and_send_extrinsic.assert_called_once_with( @@ -745,7 +663,6 @@ def test_submit_encrypted_extrinsic_failed_to_find_outcome( mocked_wait_for_extrinsic.assert_called_once_with( subtensor=subtensor, extrinsic_hash=signed_extrinsic_hash, - shield_id=shield_id, submit_block_hash=block_hash, timeout_blocks=3, ) @@ -763,14 +680,11 @@ def test_submit_encrypted_extrinsic_execution_failure(subtensor, fake_wallet, mo ) ml_kem_768_public_key = b"fake_ml_kem_key" * 74 - mev_commitment = "0xcommitment" mev_ciphertext = b"fake_ciphertext" - payload_core = b"fake_payload" signed_extrinsic_hash_hex = "abcdef123456" signed_extrinsic_hash = f"0x{signed_extrinsic_hash_hex}" current_nonce = 5 next_nonce = 6 - shield_id = "shield_id_123" block_hash = "0xblockhash" error_message = "Execution failed" formatted_error = "Formatted error: Execution failed" @@ -795,9 +709,9 @@ def test_submit_encrypted_extrinsic_execution_failure(subtensor, fake_wallet, mo "create_signed_extrinsic", return_value=mock_signed_extrinsic, ) - mocked_get_mev_commitment = mocker.patch( - "bittensor.core.extrinsics.mev_shield.get_mev_commitment_and_ciphertext", - return_value=(mev_commitment, mev_ciphertext, payload_core), + mocked_get_mev_shielded_ciphertext = mocker.patch( + "bittensor.core.extrinsics.mev_shield.get_mev_shielded_ciphertext", + return_value=mev_ciphertext, ) mocked_mev_shield = mocker.patch("bittensor.core.extrinsics.mev_shield.MevShield") mock_mev_shield_instance = mocker.MagicMock() @@ -813,7 +727,7 @@ def test_submit_encrypted_extrinsic_execution_failure(subtensor, fake_wallet, mo { "module_id": "mevShield", "event_id": "EncryptedSubmitted", - "attributes": {"id": shield_id}, + "attributes": {"id": "shield_id_123"}, } ] @@ -829,7 +743,7 @@ def test_submit_encrypted_extrinsic_execution_failure(subtensor, fake_wallet, mo return_value={ "module_id": "mevShield", "event_id": "EncryptedSubmitted", - "attributes": {"id": shield_id}, + "attributes": {"id": "shield_id_123"}, }, ) mocked_format_error_message = mocker.patch( @@ -863,13 +777,12 @@ def test_submit_encrypted_extrinsic_execution_failure(subtensor, fake_wallet, mo nonce=next_nonce, era="00", ) - mocked_get_mev_commitment.assert_called_once_with( + mocked_get_mev_shielded_ciphertext.assert_called_once_with( signed_ext=mock_signed_extrinsic, ml_kem_768_public_key=ml_kem_768_public_key, ) mocked_mev_shield.assert_called_once_with(subtensor) mock_mev_shield_instance.submit_encrypted.assert_called_once_with( - commitment=mev_commitment, ciphertext=mev_ciphertext, ) mocked_sign_and_send_extrinsic.assert_called_once_with( @@ -889,13 +802,12 @@ def test_submit_encrypted_extrinsic_execution_failure(subtensor, fake_wallet, mo mocked_wait_for_extrinsic.assert_called_once_with( subtensor=subtensor, extrinsic_hash=signed_extrinsic_hash, - shield_id=shield_id, submit_block_hash=block_hash, timeout_blocks=3, ) mocked_format_error_message.assert_called_once_with(error_message) assert result.success is False - assert isinstance(result.error, RuntimeError) + assert isinstance(result.error, SubstrateRequestException) assert result.message == formatted_error @@ -910,9 +822,7 @@ def test_submit_encrypted_extrinsic_sign_and_send_failure( ) ml_kem_768_public_key = b"fake_ml_kem_key" * 74 - mev_commitment = "0xcommitment" mev_ciphertext = b"fake_ciphertext" - payload_core = b"fake_payload" current_nonce = 5 next_nonce = 6 @@ -936,9 +846,9 @@ def test_submit_encrypted_extrinsic_sign_and_send_failure( "create_signed_extrinsic", return_value=mock_signed_extrinsic, ) - mocked_get_mev_commitment = mocker.patch( - "bittensor.core.extrinsics.mev_shield.get_mev_commitment_and_ciphertext", - return_value=(mev_commitment, mev_ciphertext, payload_core), + mocked_get_mev_shielded_ciphertext = mocker.patch( + "bittensor.core.extrinsics.mev_shield.get_mev_shielded_ciphertext", + return_value=mev_ciphertext, ) mocked_mev_shield = mocker.patch("bittensor.core.extrinsics.mev_shield.MevShield") mock_mev_shield_instance = mocker.MagicMock() @@ -975,13 +885,12 @@ def test_submit_encrypted_extrinsic_sign_and_send_failure( nonce=next_nonce, era="00", ) - mocked_get_mev_commitment.assert_called_once_with( + mocked_get_mev_shielded_ciphertext.assert_called_once_with( signed_ext=mock_signed_extrinsic, ml_kem_768_public_key=ml_kem_768_public_key, ) mocked_mev_shield.assert_called_once_with(subtensor) mock_mev_shield_instance.submit_encrypted.assert_called_once_with( - commitment=mev_commitment, ciphertext=mev_ciphertext, ) mocked_sign_and_send_extrinsic.assert_called_once_with( @@ -1006,9 +915,7 @@ def test_submit_encrypted_extrinsic_with_hotkey(subtensor, fake_wallet, mocker): fake_wallet.hotkey.ss58_address = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" ml_kem_768_public_key = b"fake_ml_kem_key" * 74 - mev_commitment = "0xcommitment" mev_ciphertext = b"fake_ciphertext" - payload_core = b"fake_payload" current_nonce = 5 next_nonce = 6 @@ -1032,9 +939,9 @@ def test_submit_encrypted_extrinsic_with_hotkey(subtensor, fake_wallet, mocker): "create_signed_extrinsic", return_value=mock_signed_extrinsic, ) - mocked_get_mev_commitment = mocker.patch( - "bittensor.core.extrinsics.mev_shield.get_mev_commitment_and_ciphertext", - return_value=(mev_commitment, mev_ciphertext, payload_core), + mocked_get_mev_shielded_ciphertext = mocker.patch( + "bittensor.core.extrinsics.mev_shield.get_mev_shielded_ciphertext", + return_value=mev_ciphertext, ) mocked_mev_shield = mocker.patch("bittensor.core.extrinsics.mev_shield.MevShield") mock_mev_shield_instance = mocker.MagicMock() @@ -1072,13 +979,12 @@ def test_submit_encrypted_extrinsic_with_hotkey(subtensor, fake_wallet, mocker): nonce=next_nonce, era="00", ) - mocked_get_mev_commitment.assert_called_once_with( + mocked_get_mev_shielded_ciphertext.assert_called_once_with( signed_ext=mock_signed_extrinsic, ml_kem_768_public_key=ml_kem_768_public_key, ) mocked_mev_shield.assert_called_once_with(subtensor) mock_mev_shield_instance.submit_encrypted.assert_called_once_with( - commitment=mev_commitment, ciphertext=mev_ciphertext, ) mocked_sign_and_send_extrinsic.assert_called_once_with( @@ -1103,9 +1009,7 @@ def test_submit_encrypted_extrinsic_with_period(subtensor, fake_wallet, mocker): ) ml_kem_768_public_key = b"fake_ml_kem_key" * 74 - mev_commitment = "0xcommitment" mev_ciphertext = b"fake_ciphertext" - payload_core = b"fake_payload" current_nonce = 5 next_nonce = 6 period = 64 @@ -1130,9 +1034,9 @@ def test_submit_encrypted_extrinsic_with_period(subtensor, fake_wallet, mocker): "create_signed_extrinsic", return_value=mock_signed_extrinsic, ) - mocked_get_mev_commitment = mocker.patch( - "bittensor.core.extrinsics.mev_shield.get_mev_commitment_and_ciphertext", - return_value=(mev_commitment, mev_ciphertext, payload_core), + mocked_get_mev_shielded_ciphertext = mocker.patch( + "bittensor.core.extrinsics.mev_shield.get_mev_shielded_ciphertext", + return_value=mev_ciphertext, ) mocked_mev_shield = mocker.patch("bittensor.core.extrinsics.mev_shield.MevShield") mock_mev_shield_instance = mocker.MagicMock() @@ -1170,13 +1074,12 @@ def test_submit_encrypted_extrinsic_with_period(subtensor, fake_wallet, mocker): nonce=next_nonce, era={"period": period}, ) - mocked_get_mev_commitment.assert_called_once_with( + mocked_get_mev_shielded_ciphertext.assert_called_once_with( signed_ext=mock_signed_extrinsic, ml_kem_768_public_key=ml_kem_768_public_key, ) mocked_mev_shield.assert_called_once_with(subtensor) mock_mev_shield_instance.submit_encrypted.assert_called_once_with( - commitment=mev_commitment, ciphertext=mev_ciphertext, ) mocked_sign_and_send_extrinsic.assert_called_once_with( diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 132582af55..3b31a3ef34 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -6145,231 +6145,6 @@ async def test_get_mev_shield_next_key_invalid_size(subtensor, mocker): ) -@pytest.mark.asyncio -async def test_get_mev_shield_submission_success(subtensor, mocker): - """Test get_mev_shield_submission returns correct submission when found.""" - # Prep - fake_submission_id = "0x1234567890abcdef" - fake_block = 123 - fake_block_hash = "0x123abc" - fake_author = b"\x01" * 32 - fake_commitment = b"\x02" * 32 - fake_ciphertext = b"\x03" * 100 - fake_submitted_in = 100 - - fake_query_result = { - "author": [fake_author], - "commitment": [fake_commitment], - "ciphertext": [fake_ciphertext], - "submitted_in": fake_submitted_in, - } - - mocked_determine_block_hash = mocker.AsyncMock(return_value=fake_block_hash) - mocker.patch.object(subtensor, "determine_block_hash", mocked_determine_block_hash) - mocked_query = mocker.AsyncMock(return_value=fake_query_result) - subtensor.substrate.query = mocked_query - mocked_decode_account_id = mocker.patch.object( - async_subtensor, - "decode_account_id", - return_value="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", - ) - - # Call - result = await subtensor.get_mev_shield_submission( - submission_id=fake_submission_id, block=fake_block - ) - - # Asserts - mocked_determine_block_hash.assert_awaited_once_with(fake_block, None, False) - mocked_query.assert_awaited_once_with( - module="MevShield", - storage_function="Submissions", - params=[bytes.fromhex("1234567890abcdef")], - block_hash=fake_block_hash, - ) - mocked_decode_account_id.assert_called_once_with([fake_author]) - assert result == { - "author": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", - "commitment": fake_commitment, - "ciphertext": fake_ciphertext, - "submitted_in": fake_submitted_in, - } - - -@pytest.mark.asyncio -async def test_get_mev_shield_submission_without_0x_prefix(subtensor, mocker): - """Test get_mev_shield_submission handles submission_id without 0x prefix.""" - # Prep - fake_submission_id = "1234567890abcdef" - fake_block = 123 - fake_block_hash = "0x123abc" - fake_query_result = { - "author": [b"\x01" * 32], - "commitment": [b"\x02" * 32], - "ciphertext": [b"\x03" * 100], - "submitted_in": 100, - } - - mocked_determine_block_hash = mocker.AsyncMock(return_value=fake_block_hash) - mocker.patch.object(subtensor, "determine_block_hash", mocked_determine_block_hash) - mocked_query = mocker.AsyncMock(return_value=fake_query_result) - subtensor.substrate.query = mocked_query - mocked_decode_account_id = mocker.patch.object( - async_subtensor, - "decode_account_id", - return_value="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", - ) - - # Call - result = await subtensor.get_mev_shield_submission( - submission_id=fake_submission_id, block=fake_block - ) - - # Asserts - mocked_determine_block_hash.assert_awaited_once_with(fake_block, None, False) - mocked_query.assert_awaited_once_with( - module="MevShield", - storage_function="Submissions", - params=[bytes.fromhex("1234567890abcdef")], - block_hash=fake_block_hash, - ) - mocked_decode_account_id.assert_called_once_with([b"\x01" * 32]) - assert result is not None - - -@pytest.mark.asyncio -async def test_get_mev_shield_submission_none(subtensor, mocker): - """Test get_mev_shield_submission returns None when submission not found.""" - # Prep - fake_submission_id = "0x1234567890abcdef" - fake_block = 123 - fake_block_hash = "0x123abc" - - mocked_determine_block_hash = mocker.AsyncMock(return_value=fake_block_hash) - mocker.patch.object(subtensor, "determine_block_hash", mocked_determine_block_hash) - mocked_query = mocker.AsyncMock(return_value=None) - subtensor.substrate.query = mocked_query - - # Call - result = await subtensor.get_mev_shield_submission( - submission_id=fake_submission_id, block=fake_block - ) - - # Asserts - mocked_determine_block_hash.assert_awaited_once_with(fake_block, None, False) - mocked_query.assert_awaited_once_with( - module="MevShield", - storage_function="Submissions", - params=[bytes.fromhex("1234567890abcdef")], - block_hash=fake_block_hash, - ) - assert result is None - - -@pytest.mark.asyncio -async def test_get_mev_shield_submissions_success(subtensor, mocker): - """Test get_mev_shield_submissions returns all submissions when found.""" - # Prep - fake_block = 123 - fake_block_hash = "0x123abc" - fake_submission_id_1 = b"\x01" * 32 - fake_submission_id_2 = b"\x02" * 32 - fake_author_1 = b"\x03" * 32 - fake_author_2 = b"\x04" * 32 - fake_commitment_1 = b"\x05" * 32 - fake_commitment_2 = b"\x06" * 32 - fake_ciphertext_1 = b"\x07" * 100 - fake_ciphertext_2 = b"\x08" * 100 - - fake_query_result = mocker.AsyncMock() - fake_query_result.__aiter__.return_value = iter( - [ - ( - [fake_submission_id_1], - mocker.MagicMock( - value={ - "author": [fake_author_1], - "commitment": [fake_commitment_1], - "ciphertext": [fake_ciphertext_1], - "submitted_in": 100, - } - ), - ), - ( - [fake_submission_id_2], - mocker.MagicMock( - value={ - "author": [fake_author_2], - "commitment": [fake_commitment_2], - "ciphertext": [fake_ciphertext_2], - "submitted_in": 101, - } - ), - ), - ] - ) - - mocked_determine_block_hash = mocker.AsyncMock(return_value=fake_block_hash) - mocker.patch.object(subtensor, "determine_block_hash", mocked_determine_block_hash) - mocked_query_map = mocker.AsyncMock(return_value=fake_query_result) - subtensor.substrate.query_map = mocked_query_map - mocked_decode_account_id = mocker.patch.object( - async_subtensor, - "decode_account_id", - side_effect=[ - "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", - "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", - ], - ) - - # Call - result = await subtensor.get_mev_shield_submissions(block=fake_block) - - # Asserts - mocked_determine_block_hash.assert_awaited_once_with(fake_block, None, False) - mocked_query_map.assert_awaited_once_with( - module="MevShield", - storage_function="Submissions", - block_hash=fake_block_hash, - ) - assert result is not None - assert len(result) == 2 - assert "0x" + fake_submission_id_1.hex() in result - assert "0x" + fake_submission_id_2.hex() in result - assert result["0x" + fake_submission_id_1.hex()]["submitted_in"] == 100 - assert result["0x" + fake_submission_id_2.hex()]["submitted_in"] == 101 - # Verify decode_account_id was called for both submissions - assert mocked_decode_account_id.call_count == 2 - - -@pytest.mark.asyncio -async def test_get_mev_shield_submissions_none(subtensor, mocker): - """Test get_mev_shield_submissions returns None when no submissions found.""" - # Prep - fake_block = 123 - fake_block_hash = "0x123abc" - - fake_query_result = mocker.AsyncMock() - fake_query_result.__aiter__.return_value = iter([]) - - mocked_determine_block_hash = mocker.AsyncMock(return_value=fake_block_hash) - mocker.patch.object(subtensor, "determine_block_hash", mocked_determine_block_hash) - mocked_query_map = mocker.AsyncMock(return_value=fake_query_result) - subtensor.substrate.query_map = mocked_query_map - - # Call - result = await subtensor.get_mev_shield_submissions(block=fake_block) - - # Asserts - mocked_determine_block_hash.assert_awaited_once_with(fake_block, None, False) - mocked_query_map.assert_awaited_once_with( - module="MevShield", - storage_function="Submissions", - block_hash=fake_block_hash, - ) - assert result is None - - @pytest.mark.asyncio async def test_mev_submit_encrypted_success(subtensor, fake_wallet, mocker): """Test mev_submit_encrypted calls submit_encrypted_extrinsic correctly.""" diff --git a/tests/unit_tests/test_errors.py b/tests/unit_tests/test_errors.py index 8a556b22e4..8b09b892e0 100644 --- a/tests/unit_tests/test_errors.py +++ b/tests/unit_tests/test_errors.py @@ -1,6 +1,7 @@ from bittensor.core.errors import ( ChainError, HotKeyAccountNotExists, + map_shield_error, ) @@ -47,3 +48,29 @@ class NewException(ChainError): exception = ChainError.from_error(error) assert isinstance(exception, NewException) + + +class TestMapShieldError: + def test_custom_error_23_maps_to_parsing_failure(self): + msg = "Subtensor returned `SubstrateRequestException(Verification Error)` error. This means: `Custom error: 23 | Please consult docs`." + result = map_shield_error(msg) + assert ( + result + == "Failed to parse shielded transaction: the ciphertext has an invalid format." + ) + + def test_custom_error_24_maps_to_invalid_key_hash(self): + msg = "Subtensor returned `SubstrateRequestException(Verification Error)` error. This means: `Custom error: 24 | Please consult docs`." + result = map_shield_error(msg) + assert "key_hash" in result + assert "does not match any known key" in result + + def test_generic_invalid_status_maps_to_catchall(self): + msg = "Subtensor returned: Subscription abc123 invalid: {'jsonrpc': '2.0', 'params': {'result': 'invalid'}}" + result = map_shield_error(msg) + assert "MEV Shield extrinsic rejected as invalid" in result + + def test_unrelated_error_returned_unchanged(self): + msg = "Something completely unrelated went wrong" + result = map_shield_error(msg) + assert result == msg diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 1e8c401333..e5c3dad2c7 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -6256,234 +6256,6 @@ def test_get_mev_shield_next_key_invalid_size(subtensor, mocker): ) -def test_get_mev_shield_submission_success(subtensor, mocker): - """Test get_mev_shield_submission returns correct submission when found.""" - # Prep - fake_submission_id = "0x1234567890abcdef" - fake_block = 123 - fake_block_hash = "0x123abc" - fake_author = b"\x01" * 32 - fake_commitment = b"\x02" * 32 - fake_ciphertext = b"\x03" * 100 - fake_submitted_in = 100 - - fake_query_result = { - "author": [fake_author], - "commitment": [fake_commitment], - "ciphertext": [fake_ciphertext], - "submitted_in": fake_submitted_in, - } - - mocked_determine_block_hash = mocker.patch.object( - subtensor, "determine_block_hash", return_value=fake_block_hash - ) - mocked_query = mocker.patch.object( - subtensor.substrate, "query", return_value=fake_query_result - ) - mocked_decode_account_id = mocker.patch.object( - subtensor_module, - "decode_account_id", - return_value="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", - ) - - # Call - result = subtensor.get_mev_shield_submission( - submission_id=fake_submission_id, block=fake_block - ) - - # Asserts - mocked_determine_block_hash.assert_called_once_with(block=fake_block) - mocked_query.assert_called_once_with( - module="MevShield", - storage_function="Submissions", - params=[bytes.fromhex("1234567890abcdef")], - block_hash=fake_block_hash, - ) - mocked_decode_account_id.assert_called_once_with([fake_author]) - assert result == { - "author": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", - "commitment": fake_commitment, - "ciphertext": fake_ciphertext, - "submitted_in": fake_submitted_in, - } - - -def test_get_mev_shield_submission_without_0x_prefix(subtensor, mocker): - """Test get_mev_shield_submission handles submission_id without 0x prefix.""" - # Prep - fake_submission_id = "1234567890abcdef" - fake_block = 123 - fake_block_hash = "0x123abc" - fake_query_result = { - "author": [b"\x01" * 32], - "commitment": [b"\x02" * 32], - "ciphertext": [b"\x03" * 100], - "submitted_in": 100, - } - - mocked_determine_block_hash = mocker.patch.object( - subtensor, "determine_block_hash", return_value=fake_block_hash - ) - mocked_query = mocker.patch.object( - subtensor.substrate, "query", return_value=fake_query_result - ) - mocked_decode_account_id = mocker.patch.object( - subtensor_module, - "decode_account_id", - return_value="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", - ) - - # Call - result = subtensor.get_mev_shield_submission( - submission_id=fake_submission_id, block=fake_block - ) - - # Asserts - mocked_determine_block_hash.assert_called_once_with(block=fake_block) - mocked_query.assert_called_once_with( - module="MevShield", - storage_function="Submissions", - params=[bytes.fromhex("1234567890abcdef")], - block_hash=fake_block_hash, - ) - mocked_decode_account_id.assert_called_once_with([b"\x01" * 32]) - assert result is not None - - -def test_get_mev_shield_submission_none(subtensor, mocker): - """Test get_mev_shield_submission returns None when submission not found.""" - # Prep - fake_submission_id = "0x1234567890abcdef" - fake_block = 123 - fake_block_hash = "0x123abc" - - mocked_determine_block_hash = mocker.patch.object( - subtensor, "determine_block_hash", return_value=fake_block_hash - ) - mocked_query = mocker.patch.object(subtensor.substrate, "query", return_value=None) - - # Call - result = subtensor.get_mev_shield_submission( - submission_id=fake_submission_id, block=fake_block - ) - - # Asserts - mocked_determine_block_hash.assert_called_once_with(block=fake_block) - mocked_query.assert_called_once_with( - module="MevShield", - storage_function="Submissions", - params=[bytes.fromhex("1234567890abcdef")], - block_hash=fake_block_hash, - ) - assert result is None - - -def test_get_mev_shield_submissions_success(subtensor, mocker): - """Test get_mev_shield_submissions returns all submissions when found.""" - # Prep - fake_block = 123 - fake_block_hash = "0x123abc" - fake_submission_id_1 = b"\x01" * 32 - fake_submission_id_2 = b"\x02" * 32 - fake_author_1 = b"\x03" * 32 - fake_author_2 = b"\x04" * 32 - fake_commitment_1 = b"\x05" * 32 - fake_commitment_2 = b"\x06" * 32 - fake_ciphertext_1 = b"\x07" * 100 - fake_ciphertext_2 = b"\x08" * 100 - - fake_query_result = mocker.MagicMock() - fake_query_result.__iter__.return_value = iter( - [ - ( - [fake_submission_id_1], - mocker.MagicMock( - value={ - "author": [fake_author_1], - "commitment": [fake_commitment_1], - "ciphertext": [fake_ciphertext_1], - "submitted_in": 100, - } - ), - ), - ( - [fake_submission_id_2], - mocker.MagicMock( - value={ - "author": [fake_author_2], - "commitment": [fake_commitment_2], - "ciphertext": [fake_ciphertext_2], - "submitted_in": 101, - } - ), - ), - ] - ) - - mocked_determine_block_hash = mocker.patch.object( - subtensor, "determine_block_hash", return_value=fake_block_hash - ) - mocked_query_map = mocker.patch.object( - subtensor.substrate, "query_map", return_value=fake_query_result - ) - mocked_decode_account_id = mocker.patch.object( - subtensor_module, - "decode_account_id", - side_effect=[ - "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", - "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", - ], - ) - - # Call - result = subtensor.get_mev_shield_submissions(block=fake_block) - - # Asserts - mocked_determine_block_hash.assert_called_once_with(block=fake_block) - mocked_query_map.assert_called_once_with( - module="MevShield", - storage_function="Submissions", - block_hash=fake_block_hash, - ) - assert result is not None - assert len(result) == 2 - assert "0x" + fake_submission_id_1.hex() in result - assert "0x" + fake_submission_id_2.hex() in result - assert result["0x" + fake_submission_id_1.hex()]["submitted_in"] == 100 - assert result["0x" + fake_submission_id_2.hex()]["submitted_in"] == 101 - # Verify decode_account_id was called for both submissions - assert mocked_decode_account_id.call_count == 2 - - -def test_get_mev_shield_submissions_none(subtensor, mocker): - """Test get_mev_shield_submissions returns None when no submissions found.""" - # Prep - fake_block = 123 - fake_block_hash = "0x123abc" - - fake_query_result = mocker.MagicMock() - fake_query_result.__iter__.return_value = iter([]) - - mocked_determine_block_hash = mocker.patch.object( - subtensor, "determine_block_hash", return_value=fake_block_hash - ) - mocked_query_map = mocker.patch.object( - subtensor.substrate, "query_map", return_value=fake_query_result - ) - - # Call - result = subtensor.get_mev_shield_submissions(block=fake_block) - - # Asserts - mocked_determine_block_hash.assert_called_once_with(block=fake_block) - mocked_query_map.assert_called_once_with( - module="MevShield", - storage_function="Submissions", - block_hash=fake_block_hash, - ) - assert result is None - - def test_mev_submit_encrypted_success(subtensor, fake_wallet, mocker): """Test mev_submit_encrypted calls submit_encrypted_extrinsic correctly.""" # Prep