Skip to content

Commit 2cbaf3d

Browse files
authored
Merge pull request opentensor#731 from opentensor/feat/thewhaleking/updated-mev-shield
Fixes: updated mev shield
2 parents 43e7919 + 6b16a90 commit 2cbaf3d

File tree

6 files changed

+348
-296
lines changed

6 files changed

+348
-296
lines changed
Lines changed: 63 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,47 @@
1-
import asyncio
21
import hashlib
32
from typing import TYPE_CHECKING, Optional
43

54
from async_substrate_interface import AsyncExtrinsicReceipt
6-
from bittensor_drand import encrypt_mlkem768, mlkem_kdf_id
7-
from bittensor_cli.src.bittensor.utils import encode_account_id, format_error_message
5+
from bittensor_drand import encrypt_mlkem768
6+
from bittensor_cli.src.bittensor.utils import format_error_message
87

98
if TYPE_CHECKING:
10-
from bittensor_wallet import Wallet
11-
from scalecodec import GenericCall
9+
from bittensor_wallet import Keypair
10+
from scalecodec import GenericCall, GenericExtrinsic
1211
from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface
1312

1413

15-
async def encrypt_call(
14+
async def encrypt_extrinsic(
1615
subtensor: "SubtensorInterface",
17-
wallet: "Wallet",
18-
call: "GenericCall",
16+
signed_extrinsic: "GenericExtrinsic",
1917
) -> "GenericCall":
2018
"""
21-
Encrypt a call using MEV Shield.
19+
Encrypt a signed extrinsic using MEV Shield.
2220
23-
Takes any call and returns a MevShield.submit_encrypted call
24-
that can be submitted like any regular extrinsic.
21+
Takes a pre-signed extrinsic and returns a MevShield.submit_encrypted call.
2522
2623
Args:
2724
subtensor: The SubtensorInterface instance for chain queries.
28-
wallet: The wallet whose coldkey will sign the inner payload.
29-
call: The call to encrypt.
25+
signed_extrinsic: The signed extrinsic to encrypt.
3026
3127
Returns:
32-
A MevShield.submit_encrypted call.
28+
A MevShield.submit_encrypted call to be signed with the current nonce.
3329
3430
Raises:
3531
ValueError: If MEV Shield NextKey is not available on chain.
3632
"""
3733

38-
next_key_result, genesis_hash = await asyncio.gather(
39-
subtensor.get_mev_shield_next_key(),
40-
subtensor.substrate.get_block_hash(0),
41-
)
42-
if next_key_result is None:
34+
ml_kem_768_public_key = await subtensor.get_mev_shield_next_key()
35+
if ml_kem_768_public_key is None:
4336
raise ValueError("MEV Shield NextKey not available on chain")
4437

45-
ml_kem_768_public_key = next_key_result
46-
47-
# Create payload_core: signer (32B) + next_key (32B) + SCALE(call)
48-
signer_bytes = encode_account_id(wallet.coldkey.ss58_address)
49-
scale_call_bytes = bytes(call.data.data)
50-
next_key = hashlib.blake2b(next_key_result, digest_size=32).digest()
51-
52-
payload_core = signer_bytes + next_key + scale_call_bytes
53-
54-
mev_shield_version = mlkem_kdf_id()
55-
genesis_hash_clean = (
56-
genesis_hash[2:] if genesis_hash.startswith("0x") else genesis_hash
57-
)
58-
genesis_hash_bytes = bytes.fromhex(genesis_hash_clean)
59-
60-
# Sign: coldkey.sign(b"mev-shield:v1" + genesis_hash + payload_core)
61-
message_to_sign = (
62-
b"mev-shield:" + mev_shield_version + genesis_hash_bytes + payload_core
63-
)
64-
signature = wallet.coldkey.sign(message_to_sign)
65-
66-
# Plaintext: payload_core + b"\x01" + signature
67-
plaintext = payload_core + b"\x01" + signature
38+
plaintext = bytes(signed_extrinsic.data.data)
6839

6940
# Encrypt using ML-KEM-768
7041
ciphertext = encrypt_mlkem768(ml_kem_768_public_key, plaintext)
7142

7243
# Commitment: blake2_256(payload_core)
73-
commitment_hash = hashlib.blake2b(payload_core, digest_size=32).digest()
44+
commitment_hash = hashlib.blake2b(plaintext, digest_size=32).digest()
7445
commitment_hex = "0x" + commitment_hash.hex()
7546

7647
# Create the MevShield.submit_encrypted call
@@ -105,31 +76,37 @@ async def extract_mev_shield_id(response: "AsyncExtrinsicReceipt") -> Optional[s
10576
return None
10677

10778

108-
async def wait_for_mev_execution(
79+
async def wait_for_extrinsic_by_hash(
10980
subtensor: "SubtensorInterface",
110-
wrapper_id: str,
81+
extrinsic_hash: str,
82+
shield_id: str,
11183
submit_block_hash: str,
112-
timeout_blocks: int = 4,
84+
timeout_blocks: int = 2,
11385
status=None,
11486
) -> tuple[bool, Optional[str], Optional[AsyncExtrinsicReceipt]]:
11587
"""
116-
Wait for MEV Shield inner call execution.
88+
Wait for the result of a MeV Shield encrypted extrinsic.
11789
118-
After submit_encrypted succeeds, the block author will decrypt and execute
119-
the inner call via execute_revealed. This function polls for the
120-
DecryptedExecuted or DecryptedRejected event.
90+
After submit_encrypted succeeds, the block author will decrypt and submit
91+
the inner extrinsic directly. This function polls subsequent blocks looking
92+
for either:
93+
- an extrinsic matching the provided hash (success)
94+
OR
95+
- a markDecryptionFailed extrinsic with matching shield ID (failure)
12196
12297
Args:
12398
subtensor: SubtensorInterface instance.
124-
wrapper_id: The ID from EncryptedSubmitted event.
125-
submit_block_number: Block number where submit_encrypted was included.
126-
timeout_blocks: Max blocks to wait (default 4).
99+
extrinsic_hash: The hash of the inner extrinsic to find.
100+
shield_id: The wrapper ID from EncryptedSubmitted event (for detecting decryption failures).
101+
submit_block_hash: Block hash where submit_encrypted was included.
102+
timeout_blocks: Max blocks to wait (default 2).
127103
status: Optional rich.Status object for progress updates.
128104
129105
Returns:
130106
Tuple of (success: bool, error: Optional[str], receipt: Optional[AsyncExtrinsicReceipt]).
131-
- (True, None, receipt) if DecryptedExecuted was found.
132-
- (False, error_message, None) if the call failed or timeout.
107+
- (True, None, receipt) if extrinsic was found and succeeded.
108+
- (False, error_message, receipt) if extrinsic was found but failed.
109+
- (False, "Timeout...", None) if not found within timeout.
133110
"""
134111

135112
async def _noop(_):
@@ -154,42 +131,45 @@ async def _noop(_):
154131
block_hash = await subtensor.substrate.get_block_hash(current_block)
155132
extrinsics = await subtensor.substrate.get_extrinsics(block_hash)
156133

157-
# Find executeRevealed extrinsic & match ids
158-
execute_revealed_index = None
134+
result_idx = None
159135
for idx, extrinsic in enumerate(extrinsics):
160-
call = extrinsic.value.get("call", {})
161-
call_module = call.get("call_module")
162-
call_function = call.get("call_function")
136+
# Success: Inner extrinsic executed
137+
if f"0x{extrinsic.extrinsic_hash.hex()}" == extrinsic_hash:
138+
result_idx = idx
139+
break
163140

164-
if call_module == "MevShield" and call_function in (
165-
"execute_revealed",
166-
"mark_decryption_failed",
141+
# Failure: Decryption failed
142+
call = extrinsic.value.get("call", {})
143+
if (
144+
call.get("call_module") == "MevShield"
145+
and call.get("call_function") == "mark_decryption_failed"
167146
):
168147
call_args = call.get("call_args", [])
169148
for arg in call_args:
170-
if arg.get("name") == "id":
171-
extrinsic_wrapper_id = arg.get("value")
172-
if extrinsic_wrapper_id == wrapper_id:
173-
execute_revealed_index = idx
174-
break
175-
176-
if execute_revealed_index is not None:
149+
if arg.get("name") == "id" and arg.get("value") == shield_id:
150+
result_idx = idx
151+
break
152+
if result_idx is not None:
177153
break
178154

179-
if execute_revealed_index is None:
180-
current_block += 1
181-
continue
155+
if result_idx is not None:
156+
receipt = AsyncExtrinsicReceipt(
157+
substrate=subtensor.substrate,
158+
block_hash=block_hash,
159+
block_number=current_block,
160+
extrinsic_idx=result_idx,
161+
)
182162

183-
receipt = AsyncExtrinsicReceipt(
184-
substrate=subtensor.substrate,
185-
block_hash=block_hash,
186-
extrinsic_idx=execute_revealed_index,
187-
)
163+
if not await receipt.is_success:
164+
error_msg = format_error_message(await receipt.error_message)
165+
return False, error_msg, receipt
188166

189-
if not await receipt.is_success:
190-
error_msg = format_error_message(await receipt.error_message)
191-
return False, error_msg, None
167+
return True, None, receipt
192168

193-
return True, None, receipt
169+
current_block += 1
194170

195-
return False, "Timeout waiting for MEV Shield execution", None
171+
return (
172+
False,
173+
"Failed to find outcome of the shield extrinsic (The protected extrinsic wasn't decrypted)",
174+
None,
175+
)

bittensor_cli/src/bittensor/subtensor_interface.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from bittensor_cli.src import DelegatesDetails
3535
from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float
3636
from bittensor_cli.src import Constants, defaults, TYPE_REGISTRY
37+
from bittensor_cli.src.bittensor.extrinsics.mev_shield import encrypt_extrinsic
3738
from bittensor_cli.src.bittensor.utils import (
3839
format_error_message,
3940
console,
@@ -1178,6 +1179,7 @@ async def sign_and_send_extrinsic(
11781179
nonce: Optional[int] = None,
11791180
sign_with: Literal["coldkey", "hotkey", "coldkeypub"] = "coldkey",
11801181
announce_only: bool = False,
1182+
mev_protection: bool = False,
11811183
) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]:
11821184
"""
11831185
Helper method to sign and submit an extrinsic call to chain.
@@ -1190,10 +1192,29 @@ async def sign_and_send_extrinsic(
11901192
:param proxy: The real account used to create the proxy. None if not using a proxy for this call.
11911193
:param nonce: The nonce used to submit this extrinsic call.
11921194
:param sign_with: Determine which of the wallet's keypairs to use to sign the extrinsic call.
1193-
:param announce_only: If set, makes the call as an announcement, rather than making the call.
1195+
:param announce_only: If set, makes the call as an announcement, rather than making the call. Cannot
1196+
be used with `mev_protection=True`.
1197+
:param mev_protection: If set, uses Mev Protection on the extrinsic, thus encrypting it. Cannot be
1198+
used with `announce_only=True`.
11941199
1195-
:return: (success, error message, extrinsic receipt | None)
1200+
:return: (success, error message or inner extrinsic hash (if using mev_protection), extrinsic receipt | None)
11961201
"""
1202+
1203+
async def create_signed(call_to_sign, n):
1204+
kwargs = {
1205+
"call": call_to_sign,
1206+
"keypair": keypair,
1207+
"nonce": n,
1208+
}
1209+
if era is not None:
1210+
kwargs["era"] = era
1211+
return await self.substrate.create_signed_extrinsic(**kwargs)
1212+
1213+
if announce_only and mev_protection:
1214+
raise ValueError(
1215+
"Cannot use announce-only and mev-protection. Calls should be announced without mev protection,"
1216+
"and executed with them."
1217+
)
11971218
if proxy is not None:
11981219
if announce_only:
11991220
call_to_announce = call
@@ -1225,7 +1246,17 @@ async def sign_and_send_extrinsic(
12251246
call_args["nonce"] = await self.substrate.get_account_next_index(
12261247
keypair.ss58_address
12271248
)
1228-
extrinsic = await self.substrate.create_signed_extrinsic(**call_args)
1249+
inner_hash = ""
1250+
if mev_protection:
1251+
next_nonce = await self.substrate.get_account_next_index(
1252+
keypair.ss58_address
1253+
)
1254+
inner_extrinsic = await create_signed(call, next_nonce)
1255+
inner_hash = f"0x{inner_extrinsic.extrinsic_hash.hex()}"
1256+
shield_call = await encrypt_extrinsic(self, inner_extrinsic)
1257+
extrinsic = await create_signed(shield_call, nonce)
1258+
else:
1259+
extrinsic = await self.substrate.create_signed_extrinsic(**call_args)
12291260
try:
12301261
response = await self.substrate.submit_extrinsic(
12311262
extrinsic,
@@ -1234,7 +1265,7 @@ async def sign_and_send_extrinsic(
12341265
)
12351266
# We only wait here if we expect finalization.
12361267
if not wait_for_finalization and not wait_for_inclusion:
1237-
return True, "", response
1268+
return True, inner_hash, response
12381269
if await response.is_success:
12391270
if announce_only:
12401271
block = await self.substrate.get_block_number(response.block_hash)
@@ -1251,7 +1282,7 @@ async def sign_and_send_extrinsic(
12511282
console.print(
12521283
f"Added entry {call_to_announce.call_hash} at block {block} to your ProxyAnnouncements address book."
12531284
)
1254-
return True, "", response
1285+
return True, inner_hash, response
12551286
else:
12561287
return False, format_error_message(await response.error_message), None
12571288
except SubstrateRequestException as e:
@@ -2378,7 +2409,7 @@ async def get_all_subnet_ema_tao_inflow(
23782409
ema_map[netuid] = Balance.from_rao(0)
23792410
else:
23802411
_, raw_ema_value = value
2381-
ema_value = fixed_to_float(raw_ema_value)
2412+
ema_value = int(fixed_to_float(raw_ema_value))
23822413
ema_map[netuid] = Balance.from_rao(ema_value)
23832414
return ema_map
23842415

@@ -2409,13 +2440,13 @@ async def get_subnet_ema_tao_inflow(
24092440
if not value:
24102441
return Balance.from_rao(0)
24112442
_, raw_ema_value = value
2412-
ema_value = fixed_to_float(raw_ema_value)
2443+
ema_value = int(fixed_to_float(raw_ema_value))
24132444
return Balance.from_rao(ema_value)
24142445

24152446
async def get_mev_shield_next_key(
24162447
self,
24172448
block_hash: Optional[str] = None,
2418-
) -> Optional[tuple[bytes, int]]:
2449+
) -> bytes:
24192450
"""
24202451
Get the next MEV Shield public key and epoch from chain storage.
24212452
@@ -2443,7 +2474,7 @@ async def get_mev_shield_next_key(
24432474
async def get_mev_shield_current_key(
24442475
self,
24452476
block_hash: Optional[str] = None,
2446-
) -> Optional[tuple[bytes, int]]:
2477+
) -> bytes:
24472478
"""
24482479
Get the current MEV Shield public key and epoch from chain storage.
24492480

0 commit comments

Comments
 (0)