Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions bittensor/core/async_subtensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6645,7 +6645,6 @@ async def mev_submit_encrypted(
self,
wallet: "Wallet",
call: "GenericCall",
signer_keypair: Optional["Keypair"] = None,
*,
period: Optional[int] = DEFAULT_PERIOD,
raise_error: bool = False,
Expand All @@ -6663,17 +6662,16 @@ async def mev_submit_encrypted(
Parameters:
wallet: The wallet used to sign the extrinsic (must be unlocked, coldkey will be used for signing).
call: The GenericCall object to encrypt and submit.
signer_keypair: The keypair used to sign the inner call.
period: The number of blocks during which the transaction will remain valid after it's submitted. If the
transaction is not included in a block within that number of blocks, it will expire and be rejected. You can
think of it as an expiration date for the transaction.
raise_error: Raises a relevant exception rather than returning `False` if unsuccessful.
wait_for_inclusion: Whether to wait for the inclusion of the transaction.
wait_for_finalization: Whether to wait for the finalization of the transaction.
wait_for_revealed_execution: Whether to wait for the DecryptedExecuted event, indicating that validators
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.
blocks_for_revealed_execution: Maximum number of blocks to poll for the DecryptedExecuted event after
blocks_for_revealed_execution: Maximum number of blocks to poll for the executed event after
inclusion. The function checks blocks from start_block+1 to start_block + blocks_for_revealed_execution.
Returns immediately if the event is found before the block limit is reached.

Expand All @@ -6694,7 +6692,6 @@ async def mev_submit_encrypted(
subtensor=self,
wallet=wallet,
call=call,
signer_keypair=signer_keypair,
period=period,
raise_error=raise_error,
wait_for_inclusion=wait_for_inclusion,
Expand Down
214 changes: 128 additions & 86 deletions bittensor/core/extrinsics/asyncex/mev_shield.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,92 +6,103 @@

from bittensor.core.extrinsics.pallets import MevShield
from bittensor.core.extrinsics.utils import (
get_event_attributes_by_event_name,
get_mev_commitment_and_ciphertext,
post_process_mev_response,
POST_SUBMIT_MEV_EVENTS,
MEV_SUBMITTED_EVENT,
get_event_data_by_event_name,
)
from bittensor.core.types import ExtrinsicResponse
from bittensor.utils import format_error_message
from bittensor.utils.btlogging import logging

if TYPE_CHECKING:
from bittensor.core.async_subtensor import AsyncSubtensor
from bittensor_wallet import Wallet, Keypair
from bittensor_wallet import Wallet
from scalecodec.types import GenericCall


async def find_revealed_extrinsic(
async def wait_for_extrinsic_by_hash(
subtensor: "AsyncSubtensor",
event_names: list[str],
event_hash_id: str,
start_block_hash: str,
blocks_ahead: int = 5,
) -> tuple[str, "AsyncExtrinsicReceipt"] | tuple[None, None]:
extrinsic_hash: str,
shield_id: str,
submit_block_hash: str,
timeout_blocks: int = 3,
) -> Optional["AsyncExtrinsicReceipt"]:
"""
Searches for an extrinsic containing a specific MEV Shield event in subsequent blocks.
Wait for the result of a MeV Shield encrypted extrinsic.

This function iterates through blocks starting from the specified block hash and searches for extrinsics that
contain a MEV Shield event (DecryptedExecuted or DecryptedRejected) matching the provided wrapper_id and signer. It
checks each extrinsic's triggered events to find the matching event.
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)

Parameters:
subtensor: The Subtensor instance used for blockchain queries.
event_names: The event identifiers to search for. Typically "DecryptedExecuted" or "DecryptedRejected" for MEV
Shield transactions.
event_hash_id: The wrapper_id (hash of (author, commitment, ciphertext)) to match. This uniquely identifies a
specific MEV Shield submission.
start_block_hash: The hash of the block where the search should begin. Usually the block where submit_encrypted
was included.
blocks_ahead: Maximum number of blocks to search ahead from the start block. Defaults to 5 blocks. The function
will check blocks from start_block + 1 to start_block + blocks_ahead (the start block itself is not checked,
as execute_revealed will be in subsequent blocks).
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 3).

Returns:
Tuple with event name and ExtrinsicReceipt object.
Optional ExtrinsicReceipt.
"""
start_block_number = await subtensor.substrate.get_block_number(start_block_hash)

for offset in range(1, blocks_ahead + 1):
current_block_number = start_block_number + offset
starting_block = await subtensor.substrate.get_block_number(submit_block_hash)
current_block = starting_block + 1

try:
current_block_hash = await subtensor.substrate.get_block_hash(
current_block_number
)
events = await subtensor.substrate.get_events(current_block_hash)
except Exception as e:
logging.debug(
f"Error getting extrinsics for block `{current_block_number}`: {e}"
)
continue

for event_name in event_names:
if event := get_event_attributes_by_event_name(events, event_name):
if event["attributes"]["id"] == event_hash_id:
return event_name, AsyncExtrinsicReceipt(
substrate=subtensor.substrate,
block_hash=current_block_hash,
extrinsic_idx=event["extrinsic_idx"],
)
while current_block - starting_block <= timeout_blocks:
logging.debug(
f"Waiting for MEV Protection (checking block {current_block - starting_block} of {timeout_blocks})..."
)

logging.debug(f"No {event_names} event found in block {current_block_number}.")
await subtensor.wait_for_block()

return None, None
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,
extrinsic_idx=result_idx,
)

current_block += 1

return None


async def submit_encrypted_extrinsic(
subtensor: "AsyncSubtensor",
wallet: "Wallet",
call: "GenericCall",
signer_keypair: Optional["Keypair"] = None,
sign_with: str = "coldkey",
period: Optional[int] = None,
raise_error: bool = False,
wait_for_inclusion: bool = True,
wait_for_finalization: bool = False,
wait_for_revealed_execution: bool = True,
blocks_for_revealed_execution: int = 5,
blocks_for_revealed_execution: int = 3,
) -> ExtrinsicResponse:
"""
Submits an encrypted extrinsic to the MEV Shield pallet.
Expand All @@ -103,17 +114,17 @@ async def submit_encrypted_extrinsic(
subtensor: The Subtensor client instance used for blockchain interaction.
wallet: The wallet used to sign the extrinsic (must be unlocked, coldkey will be used for signing).
call: The GenericCall object to encrypt and submit.
signer_keypair: The Keypair object used for signing the inner call.
sign_with: The keypair to use for signing the inner call/extrinsic. Can be either "coldkey" or "hotkey".
period: The number of blocks during which the transaction will remain valid after it's submitted. If the
transaction is not included in a block within that number of blocks, it will expire and be rejected. You can
think of it as an expiration date for the transaction.
raise_error: Raises a relevant exception rather than returning `False` if unsuccessful.
wait_for_inclusion: Whether to wait for the inclusion of the transaction.
wait_for_finalization: Whether to wait for the finalization of the transaction.
wait_for_revealed_execution: Whether to wait for the DecryptedExecuted event, indicating that validators have
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.
blocks_for_revealed_execution: Maximum number of blocks to poll for the DecryptedExecuted event after inclusion.
blocks_for_revealed_execution: Maximum number of blocks to poll for the executed event after inclusion.
The function checks blocks from start_block + 1 to start_block + blocks_for_revealed_execution. Returns
immediately if the event is found before the block limit is reached.

Expand All @@ -134,10 +145,12 @@ async def submit_encrypted_extrinsic(
protection.
"""
try:
if not (
unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error)
).success:
return unlocked
if sign_with not in ("coldkey", "hotkey"):
raise AttributeError(
f"'sign_with' must be either 'coldkey' or 'hotkey', not '{sign_with}'"
)

unlock_type = "both" if sign_with == "hotkey" else "coldkey"

if wait_for_revealed_execution and not wait_for_inclusion:
return ExtrinsicResponse.from_exception(
Expand All @@ -147,9 +160,12 @@ async def submit_encrypted_extrinsic(
),
)

# Use wallet.coldkey as default signer if signer_keypair is not provided
if signer_keypair is None:
signer_keypair = wallet.coldkey
if not (
unlocked := ExtrinsicResponse.unlock_wallet(
wallet, raise_error, unlock_type
)
).success:
return unlocked

ml_kem_768_public_key = await subtensor.get_mev_shield_next_key()
if ml_kem_768_public_key is None:
Expand All @@ -158,13 +174,21 @@ async def submit_encrypted_extrinsic(
error=ValueError("MEV Shield NextKey not available in storage."),
)

genesis_hash = await subtensor.get_block_hash(block=0)
inner_signing_keypair = getattr(wallet, sign_with)

era = "00" if period is None else {"period": period}

current_nonce = await subtensor.substrate.get_account_next_index(
account_address=inner_signing_keypair.ss58_address
)

signed_extrinsic = await subtensor.substrate.create_signed_extrinsic(
call=call, keypair=inner_signing_keypair, nonce=current_nonce + 1, era=era
)

mev_commitment, mev_ciphertext, payload_core, signature = (
mev_commitment, mev_ciphertext, payload_core = (
get_mev_commitment_and_ciphertext(
call=call,
signer_keypair=signer_keypair,
genesis_hash=genesis_hash,
signed_ext=signed_extrinsic,
ml_kem_768_public_key=ml_kem_768_public_key,
)
)
Expand All @@ -189,32 +213,50 @@ async def submit_encrypted_extrinsic(
"ciphertext": mev_ciphertext,
"ml_kem_768_public_key": ml_kem_768_public_key,
"payload_core": payload_core,
"signature": signature,
"submitting_id": extrinsic_call.call_hash,
"signed_extrinsic_hash": f"0x{signed_extrinsic.extrinsic_hash.hex()}",
}
if wait_for_revealed_execution:
triggered_events = await response.extrinsic_receipt.triggered_events
event = get_event_data_by_event_name(
events=triggered_events,
event_name="mevShield.EncryptedSubmitted",
)

event_hash_id = get_event_attributes_by_event_name(
events=triggered_events, event_name=MEV_SUBMITTED_EVENT
)["attributes"]["id"]
if event is None:
return ExtrinsicResponse.from_exception(
raise_error=raise_error,
error=RuntimeError("EncryptedSubmitted event not found."),
)

reveled_event, reveled_extrinsic = await find_revealed_extrinsic(
subtensor=subtensor,
event_names=POST_SUBMIT_MEV_EVENTS,
event_hash_id=event_hash_id,
start_block_hash=response.extrinsic_receipt.block_hash,
blocks_ahead=blocks_for_revealed_execution,
)
shield_id = event["attributes"]["id"]

post_process_mev_response(
response=response,
revealed_name=reveled_event,
revealed_extrinsic=reveled_extrinsic,
raise_error=raise_error,
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,
)
if response.mev_extrinsic is None:
return ExtrinsicResponse.from_exception(
raise_error=raise_error,
error=RuntimeError(
"Failed to find outcome of the shield extrinsic (The protected extrinsic wasn't decrypted)."
),
)

logging.debug("[green]Encrypted extrinsic submitted successfully.[/green]")
if not await response.mev_extrinsic.is_success:
response.message = format_error_message(
await response.mev_extrinsic.error_message
)
response.error = RuntimeError(response.message)
response.success = False
if raise_error:
raise response.error
else:
logging.debug(
"[green]Encrypted extrinsic submitted successfully.[/green]"
)
else:
logging.error(f"[red]{response.message}[/red]")

Expand Down
Loading
Loading