meshtensor-cli 9.18.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. meshtensor_cli/__init__.py +22 -0
  2. meshtensor_cli/cli.py +10742 -0
  3. meshtensor_cli/doc_generation_helper.py +4 -0
  4. meshtensor_cli/src/__init__.py +1085 -0
  5. meshtensor_cli/src/commands/__init__.py +0 -0
  6. meshtensor_cli/src/commands/axon/__init__.py +0 -0
  7. meshtensor_cli/src/commands/axon/axon.py +132 -0
  8. meshtensor_cli/src/commands/crowd/__init__.py +0 -0
  9. meshtensor_cli/src/commands/crowd/contribute.py +621 -0
  10. meshtensor_cli/src/commands/crowd/contributors.py +200 -0
  11. meshtensor_cli/src/commands/crowd/create.py +783 -0
  12. meshtensor_cli/src/commands/crowd/dissolve.py +219 -0
  13. meshtensor_cli/src/commands/crowd/refund.py +233 -0
  14. meshtensor_cli/src/commands/crowd/update.py +418 -0
  15. meshtensor_cli/src/commands/crowd/utils.py +124 -0
  16. meshtensor_cli/src/commands/crowd/view.py +991 -0
  17. meshtensor_cli/src/commands/governance/__init__.py +0 -0
  18. meshtensor_cli/src/commands/governance/governance.py +794 -0
  19. meshtensor_cli/src/commands/liquidity/__init__.py +0 -0
  20. meshtensor_cli/src/commands/liquidity/liquidity.py +699 -0
  21. meshtensor_cli/src/commands/liquidity/utils.py +202 -0
  22. meshtensor_cli/src/commands/proxy.py +700 -0
  23. meshtensor_cli/src/commands/stake/__init__.py +0 -0
  24. meshtensor_cli/src/commands/stake/add.py +799 -0
  25. meshtensor_cli/src/commands/stake/auto_staking.py +306 -0
  26. meshtensor_cli/src/commands/stake/children_hotkeys.py +865 -0
  27. meshtensor_cli/src/commands/stake/claim.py +770 -0
  28. meshtensor_cli/src/commands/stake/list.py +738 -0
  29. meshtensor_cli/src/commands/stake/move.py +1211 -0
  30. meshtensor_cli/src/commands/stake/remove.py +1466 -0
  31. meshtensor_cli/src/commands/stake/wizard.py +323 -0
  32. meshtensor_cli/src/commands/subnets/__init__.py +0 -0
  33. meshtensor_cli/src/commands/subnets/mechanisms.py +515 -0
  34. meshtensor_cli/src/commands/subnets/price.py +733 -0
  35. meshtensor_cli/src/commands/subnets/subnets.py +2908 -0
  36. meshtensor_cli/src/commands/sudo.py +1294 -0
  37. meshtensor_cli/src/commands/tc/__init__.py +0 -0
  38. meshtensor_cli/src/commands/tc/tc.py +190 -0
  39. meshtensor_cli/src/commands/treasury/__init__.py +0 -0
  40. meshtensor_cli/src/commands/treasury/treasury.py +194 -0
  41. meshtensor_cli/src/commands/view.py +354 -0
  42. meshtensor_cli/src/commands/wallets.py +2311 -0
  43. meshtensor_cli/src/commands/weights.py +467 -0
  44. meshtensor_cli/src/meshtensor/__init__.py +0 -0
  45. meshtensor_cli/src/meshtensor/balances.py +313 -0
  46. meshtensor_cli/src/meshtensor/chain_data.py +1263 -0
  47. meshtensor_cli/src/meshtensor/extrinsics/__init__.py +0 -0
  48. meshtensor_cli/src/meshtensor/extrinsics/mev_shield.py +174 -0
  49. meshtensor_cli/src/meshtensor/extrinsics/registration.py +1861 -0
  50. meshtensor_cli/src/meshtensor/extrinsics/root.py +550 -0
  51. meshtensor_cli/src/meshtensor/extrinsics/serving.py +255 -0
  52. meshtensor_cli/src/meshtensor/extrinsics/transfer.py +239 -0
  53. meshtensor_cli/src/meshtensor/meshtensor_interface.py +2598 -0
  54. meshtensor_cli/src/meshtensor/minigraph.py +254 -0
  55. meshtensor_cli/src/meshtensor/networking.py +12 -0
  56. meshtensor_cli/src/meshtensor/templates/main-filters.j2 +24 -0
  57. meshtensor_cli/src/meshtensor/templates/main-header.j2 +36 -0
  58. meshtensor_cli/src/meshtensor/templates/neuron-details.j2 +111 -0
  59. meshtensor_cli/src/meshtensor/templates/price-multi.j2 +113 -0
  60. meshtensor_cli/src/meshtensor/templates/price-single.j2 +99 -0
  61. meshtensor_cli/src/meshtensor/templates/subnet-details-header.j2 +49 -0
  62. meshtensor_cli/src/meshtensor/templates/subnet-details.j2 +32 -0
  63. meshtensor_cli/src/meshtensor/templates/subnet-metrics.j2 +57 -0
  64. meshtensor_cli/src/meshtensor/templates/subnets-table.j2 +28 -0
  65. meshtensor_cli/src/meshtensor/templates/table.j2 +267 -0
  66. meshtensor_cli/src/meshtensor/templates/view.css +1058 -0
  67. meshtensor_cli/src/meshtensor/templates/view.j2 +43 -0
  68. meshtensor_cli/src/meshtensor/templates/view.js +1053 -0
  69. meshtensor_cli/src/meshtensor/utils.py +2007 -0
  70. meshtensor_cli/version.py +23 -0
  71. meshtensor_cli-9.18.1.dist-info/METADATA +261 -0
  72. meshtensor_cli-9.18.1.dist-info/RECORD +74 -0
  73. meshtensor_cli-9.18.1.dist-info/WHEEL +4 -0
  74. meshtensor_cli-9.18.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,1861 @@
1
+ import asyncio
2
+ import binascii
3
+ from contextlib import redirect_stdout
4
+ from dataclasses import dataclass
5
+ from datetime import timedelta
6
+ import functools
7
+ import hashlib
8
+ import io
9
+ import math
10
+ import multiprocessing as mp
11
+ from multiprocessing.queues import Queue as Queue_Type
12
+ from multiprocessing import Process, Event, Lock, Array, Value, Queue
13
+ import os
14
+ from queue import Empty, Full
15
+ import random
16
+ import time
17
+ import typing
18
+ from typing import Optional
19
+ import subprocess
20
+
21
+ from async_substrate_interface import AsyncExtrinsicReceipt
22
+ from meshtensor_wallet import Wallet
23
+ from Crypto.Hash import keccak
24
+ import numpy as np
25
+ from rich.console import Console
26
+ from rich.status import Status
27
+ from async_substrate_interface.errors import SubstrateRequestException
28
+
29
+ from meshtensor_cli.src import COLOR_PALETTE
30
+ from meshtensor_cli.src.meshtensor.chain_data import NeuronInfo
31
+ from meshtensor_cli.src.meshtensor.balances import Balance
32
+ from meshtensor_cli.src.meshtensor.utils import (
33
+ confirm_action,
34
+ console,
35
+ print_error,
36
+ print_success,
37
+ format_error_message,
38
+ millify,
39
+ get_human_readable,
40
+ print_verbose,
41
+ print_error,
42
+ unlock_key,
43
+ hex_to_bytes,
44
+ get_hotkey_pub_ss58,
45
+ print_extrinsic_id,
46
+ )
47
+
48
+ if typing.TYPE_CHECKING:
49
+ from meshtensor_cli.src.meshtensor.meshtensor_interface import MeshtensorInterface
50
+
51
+
52
+ def use_torch() -> bool:
53
+ """Force the use of torch over numpy for certain operations."""
54
+ return True if os.getenv("USE_TORCH") == "1" else False
55
+
56
+
57
+ def legacy_torch_api_compat(func: typing.Callable):
58
+ """
59
+ Convert function operating on numpy Input&Output to legacy torch Input&Output API if `use_torch()` is True.
60
+
61
+ :param func: Function with numpy Input/Output to be decorated.
62
+
63
+ :return: Decorated function
64
+ """
65
+
66
+ @functools.wraps(func)
67
+ def decorated(*args, **kwargs):
68
+ if use_torch():
69
+ # if argument is a Torch tensor, convert it to numpy
70
+ args = [
71
+ arg.cpu().numpy() if isinstance(arg, torch.Tensor) else arg
72
+ for arg in args
73
+ ]
74
+ kwargs = {
75
+ key: value.cpu().numpy() if isinstance(value, torch.Tensor) else value
76
+ for key, value in kwargs.items()
77
+ }
78
+ ret = func(*args, **kwargs)
79
+ if use_torch():
80
+ # if return value is a numpy array, convert it to Torch tensor
81
+ if isinstance(ret, np.ndarray):
82
+ ret = torch.from_numpy(ret)
83
+ return ret
84
+
85
+ return decorated
86
+
87
+
88
+ @functools.cache
89
+ def _get_real_torch():
90
+ try:
91
+ import torch as _real_torch
92
+ except ImportError:
93
+ _real_torch = None
94
+ return _real_torch
95
+
96
+
97
+ def log_no_torch_error():
98
+ print_error(
99
+ "This command requires torch. You can install torch"
100
+ " with `pip install torch` and run the command again."
101
+ )
102
+
103
+
104
+ @dataclass
105
+ class POWSolution:
106
+ """A solution to the registration PoW problem."""
107
+
108
+ nonce: int
109
+ block_number: int
110
+ difficulty: int
111
+ seal: bytes
112
+
113
+ async def is_stale(self, meshtensor: "MeshtensorInterface") -> bool:
114
+ """Returns True if the POW is stale.
115
+ This means the block the POW is solved for is within 3 blocks of the current block.
116
+ """
117
+ current_block = await meshtensor.substrate.get_block_number(None)
118
+ return self.block_number < current_block - 3
119
+
120
+
121
+ @dataclass
122
+ class RegistrationStatistics:
123
+ """Statistics for a registration."""
124
+
125
+ time_spent_total: float
126
+ rounds_total: int
127
+ time_average: float
128
+ time_spent: float
129
+ hash_rate_perpetual: float
130
+ hash_rate: float
131
+ difficulty: int
132
+ block_number: int
133
+ block_hash: str
134
+
135
+
136
+ class RegistrationStatisticsLogger:
137
+ """Logs statistics for a registration."""
138
+
139
+ console: Console
140
+ status: Optional[Status]
141
+
142
+ def __init__(self, console_: Console, output_in_place: bool = True) -> None:
143
+ self.console = console_
144
+
145
+ if output_in_place:
146
+ self.status = self.console.status("Solving")
147
+ else:
148
+ self.status = None
149
+
150
+ def start(self) -> None:
151
+ if self.status is not None:
152
+ self.status.start()
153
+
154
+ def stop(self) -> None:
155
+ if self.status is not None:
156
+ self.status.stop()
157
+
158
+ @classmethod
159
+ def get_status_message(
160
+ cls, stats: RegistrationStatistics, verbose: bool = False
161
+ ) -> str:
162
+ """
163
+ Provides a message of the current status of the block solving as a str for a logger or stdout
164
+ """
165
+ message = (
166
+ "Solving\n"
167
+ + f"Time Spent (total): [bold white]{timedelta(seconds=stats.time_spent_total)}[/bold white]\n"
168
+ + (
169
+ f"Time Spent This Round: {timedelta(seconds=stats.time_spent)}\n"
170
+ + f"Time Spent Average: {timedelta(seconds=stats.time_average)}\n"
171
+ if verbose
172
+ else ""
173
+ )
174
+ + f"Registration Difficulty: [bold white]{millify(stats.difficulty)}[/bold white]\n"
175
+ + f"Iters (Inst/Perp): [bold white]{get_human_readable(stats.hash_rate, 'H')}/s / "
176
+ + f"{get_human_readable(stats.hash_rate_perpetual, 'H')}/s[/bold white]\n"
177
+ + f"Block Number: [bold white]{stats.block_number}[/bold white]\n"
178
+ + f"Block Hash: [bold white]{stats.block_hash.encode('utf-8')}[/bold white]\n"
179
+ )
180
+ return message
181
+
182
+ def update(self, stats: RegistrationStatistics, verbose: bool = False) -> None:
183
+ """
184
+ Passes the current status to the logger
185
+ """
186
+ if self.status is not None:
187
+ self.status.update(self.get_status_message(stats, verbose=verbose))
188
+ else:
189
+ self.console.log(self.get_status_message(stats, verbose=verbose))
190
+
191
+
192
+ class _SolverBase(Process):
193
+ """
194
+ A process that solves the registration PoW problem.
195
+
196
+ :param proc_num: The number of the process being created.
197
+ :param num_proc: The total number of processes running.
198
+ :param update_interval: The number of nonces to try to solve before checking for a new block.
199
+ :param finished_queue: The queue to put the process number when a process finishes each update_interval.
200
+ Used for calculating the average time per update_interval across all processes.
201
+ :param solution_queue: The queue to put the solution the process has found during the pow solve.
202
+ :param stop_event: The event to set by the main process when all the solver processes should stop.
203
+ The solver process will check for the event after each update_interval.
204
+ The solver process will stop when the event is set.
205
+ Used to stop the solver processes when a solution is found.
206
+ :param curr_block: The array containing this process's current block hash.
207
+ The main process will set the array to the new block hash when a new block is finalized in the
208
+ network. The solver process will get the new block hash from this array when newBlockEvent is set
209
+ :param curr_block_num: The value containing this process's current block number.
210
+ The main process will set the value to the new block number when a new block is finalized in
211
+ the network. The solver process will get the new block number from this value when
212
+ new_block_event is set.
213
+ :param curr_diff: The array containing this process's current difficulty. The main process will set the array to
214
+ the new difficulty when a new block is finalized in the network. The solver process will get the
215
+ new difficulty from this array when newBlockEvent is set.
216
+ :param check_block: The lock to prevent this process from getting the new block data while the main process is
217
+ updating the data.
218
+ :param limit: The limit of the pow solve for a valid solution.
219
+
220
+ :var new_block_event: The event to set by the main process when a new block is finalized in the network.
221
+ The solver process will check for the event after each update_interval.
222
+ The solver process will get the new block hash and difficulty and start solving for a new
223
+ nonce.
224
+ """
225
+
226
+ proc_num: int
227
+ num_proc: int
228
+ update_interval: int
229
+ finished_queue: Queue_Type
230
+ solution_queue: Queue_Type
231
+ new_block_event: Event
232
+ stop_event: Event
233
+ hotkey_bytes: bytes
234
+ curr_block: Array
235
+ curr_block_num: Value
236
+ curr_diff: Array
237
+ check_block: Lock
238
+ limit: int
239
+
240
+ def __init__(
241
+ self,
242
+ proc_num,
243
+ num_proc,
244
+ update_interval,
245
+ finished_queue,
246
+ solution_queue,
247
+ stop_event,
248
+ curr_block,
249
+ curr_block_num,
250
+ curr_diff,
251
+ check_block,
252
+ limit,
253
+ ):
254
+ Process.__init__(self, daemon=True)
255
+ self.proc_num = proc_num
256
+ self.num_proc = num_proc
257
+ self.update_interval = update_interval
258
+ self.finished_queue = finished_queue
259
+ self.solution_queue = solution_queue
260
+ self.new_block_event = Event()
261
+ self.new_block_event.clear()
262
+ self.curr_block = curr_block
263
+ self.curr_block_num = curr_block_num
264
+ self.curr_diff = curr_diff
265
+ self.check_block = check_block
266
+ self.stop_event = stop_event
267
+ self.limit = limit
268
+
269
+ def run(self):
270
+ raise NotImplementedError("_SolverBase is an abstract class")
271
+
272
+ @staticmethod
273
+ def create_shared_memory() -> tuple[Array, Value, Array]:
274
+ """Creates shared memory for the solver processes to use."""
275
+ curr_block = Array("h", 32, lock=True) # byte array
276
+ curr_block_num = Value("i", 0, lock=True) # int
277
+ curr_diff = Array("Q", [0, 0], lock=True) # [high, low]
278
+
279
+ return curr_block, curr_block_num, curr_diff
280
+
281
+
282
+ class _Solver(_SolverBase):
283
+ """
284
+ Performs POW Solution
285
+ """
286
+
287
+ def run(self):
288
+ block_number: int
289
+ block_and_hotkey_hash_bytes: bytes
290
+ block_difficulty: int
291
+ nonce_limit = int(math.pow(2, 64)) - 1
292
+
293
+ # Start at random nonce
294
+ nonce_start = random.randint(0, nonce_limit)
295
+ nonce_end = nonce_start + self.update_interval
296
+ while not self.stop_event.is_set():
297
+ if self.new_block_event.is_set():
298
+ with self.check_block:
299
+ block_number = self.curr_block_num.value
300
+ block_and_hotkey_hash_bytes = bytes(self.curr_block)
301
+ block_difficulty = _registration_diff_unpack(self.curr_diff)
302
+
303
+ self.new_block_event.clear()
304
+
305
+ # Do a block of nonces
306
+ solution = _solve_for_nonce_block(
307
+ nonce_start,
308
+ nonce_end,
309
+ block_and_hotkey_hash_bytes,
310
+ block_difficulty,
311
+ self.limit,
312
+ block_number,
313
+ )
314
+ if solution is not None:
315
+ self.solution_queue.put(solution)
316
+
317
+ try:
318
+ # Send time
319
+ self.finished_queue.put_nowait(self.proc_num)
320
+ except Full:
321
+ pass
322
+
323
+ nonce_start = random.randint(0, nonce_limit)
324
+ nonce_start = nonce_start % nonce_limit
325
+ nonce_end = nonce_start + self.update_interval
326
+
327
+
328
+ class _CUDASolver(_SolverBase):
329
+ """
330
+ Performs POW Solution using CUDA
331
+ """
332
+
333
+ dev_id: int
334
+ tpb: int
335
+
336
+ def __init__(
337
+ self,
338
+ proc_num,
339
+ num_proc,
340
+ update_interval,
341
+ finished_queue,
342
+ solution_queue,
343
+ stop_event,
344
+ curr_block,
345
+ curr_block_num,
346
+ curr_diff,
347
+ check_block,
348
+ limit,
349
+ dev_id: int,
350
+ tpb: int,
351
+ ):
352
+ super().__init__(
353
+ proc_num,
354
+ num_proc,
355
+ update_interval,
356
+ finished_queue,
357
+ solution_queue,
358
+ stop_event,
359
+ curr_block,
360
+ curr_block_num,
361
+ curr_diff,
362
+ check_block,
363
+ limit,
364
+ )
365
+ self.dev_id = dev_id
366
+ self.tpb = tpb
367
+
368
+ def run(self):
369
+ block_number: int = 0 # dummy value
370
+ block_and_hotkey_hash_bytes: bytes = b"0" * 32 # dummy value
371
+ block_difficulty: int = int(math.pow(2, 64)) - 1 # dummy value
372
+ nonce_limit = int(math.pow(2, 64)) - 1 # U64MAX
373
+
374
+ # Start at random nonce
375
+ nonce_start = random.randint(0, nonce_limit)
376
+ while not self.stop_event.is_set():
377
+ if self.new_block_event.is_set():
378
+ with self.check_block:
379
+ block_number = self.curr_block_num.value
380
+ block_and_hotkey_hash_bytes = bytes(self.curr_block)
381
+ block_difficulty = _registration_diff_unpack(self.curr_diff)
382
+
383
+ self.new_block_event.clear()
384
+
385
+ # Do a block of nonces
386
+ solution = _solve_for_nonce_block_cuda(
387
+ nonce_start,
388
+ self.update_interval,
389
+ block_and_hotkey_hash_bytes,
390
+ block_difficulty,
391
+ self.limit,
392
+ block_number,
393
+ self.dev_id,
394
+ self.tpb,
395
+ )
396
+ if solution is not None:
397
+ self.solution_queue.put(solution)
398
+
399
+ try:
400
+ # Signal that a nonce_block was finished using queue
401
+ # send our proc_num
402
+ self.finished_queue.put(self.proc_num)
403
+ except Full:
404
+ pass
405
+
406
+ # increase nonce by number of nonces processed
407
+ nonce_start += self.update_interval * self.tpb
408
+ nonce_start = nonce_start % nonce_limit
409
+
410
+
411
+ class LazyLoadedTorch:
412
+ def __bool__(self):
413
+ return bool(_get_real_torch())
414
+
415
+ def __getattr__(self, name):
416
+ if real_torch := _get_real_torch():
417
+ return getattr(real_torch, name)
418
+ else:
419
+ log_no_torch_error()
420
+ raise ImportError("torch not installed")
421
+
422
+
423
+ if typing.TYPE_CHECKING:
424
+ import torch
425
+ else:
426
+ torch = LazyLoadedTorch()
427
+
428
+
429
+ class MaxSuccessException(Exception):
430
+ """
431
+ Raised when the POW Solver has reached the max number of successful solutions
432
+ """
433
+
434
+
435
+ class MaxAttemptsException(Exception):
436
+ """
437
+ Raised when the POW Solver has reached the max number of attempts
438
+ """
439
+
440
+
441
+ async def is_hotkey_registered(
442
+ meshtensor: "MeshtensorInterface", netuid: int, hotkey_ss58: str
443
+ ) -> bool:
444
+ """Checks to see if the hotkey is registered on a given netuid"""
445
+ _result = await meshtensor.query(
446
+ module="MeshtensorModule",
447
+ storage_function="Uids",
448
+ params=[netuid, hotkey_ss58],
449
+ )
450
+ if _result is not None:
451
+ return True
452
+ else:
453
+ return False
454
+
455
+
456
+ async def register_extrinsic(
457
+ meshtensor: "MeshtensorInterface",
458
+ wallet: Wallet,
459
+ netuid: int,
460
+ wait_for_inclusion: bool = False,
461
+ wait_for_finalization: bool = True,
462
+ prompt: bool = False,
463
+ decline: bool = False,
464
+ quiet: bool = False,
465
+ max_allowed_attempts: int = 3,
466
+ output_in_place: bool = True,
467
+ cuda: bool = False,
468
+ dev_id: typing.Union[list[int], int] = 0,
469
+ tpb: int = 256,
470
+ num_processes: Optional[int] = None,
471
+ update_interval: Optional[int] = None,
472
+ log_verbose: bool = False,
473
+ ) -> bool:
474
+ """Registers the wallet to the chain.
475
+
476
+ :param meshtensor: initialized MeshtensorInterface object to use for chain interactions
477
+ :param wallet: Meshtensor wallet object.
478
+ :param netuid: The ``netuid`` of the subnet to register on.
479
+ :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns
480
+ `False` if the extrinsic fails to enter the block within the timeout.
481
+ :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`,
482
+ or returns `False` if the extrinsic fails to be finalized within the timeout.
483
+ :param prompt: If `True`, the call waits for confirmation from the user before proceeding.
484
+ :param max_allowed_attempts: Maximum number of attempts to register the wallet.
485
+ :param output_in_place: Whether the POW solving should be outputted to the console as it goes along.
486
+ :param cuda: If `True`, the wallet should be registered using CUDA device(s).
487
+ :param dev_id: The CUDA device id to use, or a list of device ids.
488
+ :param tpb: The number of threads per block (CUDA).
489
+ :param num_processes: The number of processes to use to register.
490
+ :param update_interval: The number of nonces to solve between updates.
491
+ :param log_verbose: If `True`, the registration process will log more information.
492
+
493
+ :return: `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion,
494
+ the response is `True`.
495
+ """
496
+
497
+ async def get_neuron_for_pubkey_and_subnet():
498
+ uid = await meshtensor.query(
499
+ "MeshtensorModule", "Uids", [netuid, get_hotkey_pub_ss58(wallet)]
500
+ )
501
+ if uid is None:
502
+ return NeuronInfo.get_null_neuron()
503
+
504
+ result = await meshtensor.neuron_for_uid(
505
+ uid=uid,
506
+ netuid=netuid,
507
+ block_hash=meshtensor.substrate.last_block_hash,
508
+ )
509
+ return result
510
+
511
+ print_verbose("Checking subnet status")
512
+ if not await meshtensor.subnet_exists(netuid):
513
+ print_error(
514
+ f"Failed: error: [bold white]subnet:{netuid}[/bold white] does not exist."
515
+ )
516
+ return False
517
+
518
+ with console.status(
519
+ f":satellite: Checking Account on [bold]subnet:{netuid}[/bold]...",
520
+ spinner="aesthetic",
521
+ ) as status:
522
+ neuron = await get_neuron_for_pubkey_and_subnet()
523
+ if not neuron.is_null:
524
+ print_error(
525
+ f"Wallet {wallet} is already registered on subnet {neuron.netuid} with uid {neuron.uid}",
526
+ status,
527
+ )
528
+ return True
529
+
530
+ if prompt:
531
+ if not confirm_action(
532
+ f"Continue Registration?\n"
533
+ f" hotkey [{COLOR_PALETTE.G.HK}]({wallet.hotkey_str})[/{COLOR_PALETTE.G.HK}]:"
534
+ f"\t[{COLOR_PALETTE.G.HK}]{get_hotkey_pub_ss58(wallet)}[/{COLOR_PALETTE.G.HK}]\n"
535
+ f" coldkey [{COLOR_PALETTE.G.CK}]({wallet.name})[/{COLOR_PALETTE.G.CK}]:"
536
+ f"\t[{COLOR_PALETTE.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE.G.CK}]\n"
537
+ f" network:\t\t[{COLOR_PALETTE.G.LINKS}]{meshtensor.network}[/{COLOR_PALETTE.G.LINKS}]\n",
538
+ decline=decline,
539
+ quiet=quiet,
540
+ ):
541
+ return False
542
+
543
+ if not torch:
544
+ log_no_torch_error()
545
+ return False
546
+
547
+ # Attempt rolling registration.
548
+ attempts = 1
549
+ pow_result: Optional[POWSolution]
550
+ while True:
551
+ console.print(
552
+ ":satellite: Registering...({}/{})".format(attempts, max_allowed_attempts)
553
+ )
554
+ # Solve latest POW.
555
+ if cuda:
556
+ if not torch.cuda.is_available():
557
+ if prompt:
558
+ console.print("CUDA is not available.")
559
+ return False
560
+ pow_result = await create_pow(
561
+ meshtensor,
562
+ wallet,
563
+ netuid,
564
+ output_in_place,
565
+ cuda=cuda,
566
+ dev_id=dev_id,
567
+ tpb=tpb,
568
+ num_processes=num_processes,
569
+ update_interval=update_interval,
570
+ log_verbose=log_verbose,
571
+ )
572
+ else:
573
+ pow_result = await create_pow(
574
+ meshtensor,
575
+ wallet,
576
+ netuid,
577
+ output_in_place,
578
+ cuda=cuda,
579
+ num_processes=num_processes,
580
+ update_interval=update_interval,
581
+ log_verbose=log_verbose,
582
+ )
583
+
584
+ # pow failed
585
+ if not pow_result:
586
+ # might be registered already on this subnet
587
+ is_registered = await is_hotkey_registered(
588
+ meshtensor, netuid=netuid, hotkey_ss58=get_hotkey_pub_ss58(wallet)
589
+ )
590
+ if is_registered:
591
+ print_success(
592
+ f"[dark_sea_green3]Already registered on netuid:{netuid}[/dark_sea_green3]"
593
+ )
594
+ return True
595
+
596
+ # pow successful, proceed to submit pow to chain for registration
597
+ else:
598
+ with console.status(":satellite: Submitting POW..."):
599
+ # check if pow result is still valid
600
+ while not await pow_result.is_stale(meshtensor=meshtensor):
601
+ call = await meshtensor.substrate.compose_call(
602
+ call_module="MeshtensorModule",
603
+ call_function="register",
604
+ call_params={
605
+ "netuid": netuid,
606
+ "block_number": pow_result.block_number,
607
+ "nonce": pow_result.nonce,
608
+ "work": [int(byte_) for byte_ in pow_result.seal],
609
+ "hotkey": get_hotkey_pub_ss58(wallet),
610
+ "coldkey": wallet.coldkeypub.ss58_address,
611
+ },
612
+ )
613
+ extrinsic = await meshtensor.substrate.create_signed_extrinsic(
614
+ call=call, keypair=wallet.hotkey
615
+ )
616
+ response = await meshtensor.substrate.submit_extrinsic(
617
+ extrinsic,
618
+ wait_for_inclusion=wait_for_inclusion,
619
+ wait_for_finalization=wait_for_finalization,
620
+ )
621
+ if not wait_for_finalization and not wait_for_inclusion:
622
+ success, err_msg = True, ""
623
+ else:
624
+ success = await response.is_success
625
+ if not success:
626
+ success, err_msg = (
627
+ False,
628
+ format_error_message(await response.error_message),
629
+ )
630
+ # Look error here
631
+ # https://github.com/opentensor/meshtensor/blob/development/pallets/meshtensor/src/errors.rs
632
+
633
+ if "HotKeyAlreadyRegisteredInSubNet" in err_msg:
634
+ print_success(
635
+ f"[dark_sea_green3]Already Registered on "
636
+ f"[bold]subnet:{netuid}[/bold][/dark_sea_green3]"
637
+ )
638
+ return True
639
+ print_error(f"Failed: {err_msg}")
640
+ await asyncio.sleep(0.5)
641
+
642
+ # Successful registration, final check for neuron and pubkey
643
+ if success:
644
+ console.print(":satellite: Checking Registration status...")
645
+ is_registered = await is_hotkey_registered(
646
+ meshtensor,
647
+ netuid=netuid,
648
+ hotkey_ss58=get_hotkey_pub_ss58(wallet),
649
+ )
650
+ if is_registered:
651
+ print_success(
652
+ "[dark_sea_green3]Registered[/dark_sea_green3]"
653
+ )
654
+ return True
655
+ else:
656
+ # neuron not found, try again
657
+ print_error("Unknown error. Neuron not found.")
658
+ continue
659
+ else:
660
+ # Exited loop because pow is no longer valid.
661
+ print_error("POW is stale.")
662
+ # Try again.
663
+ continue
664
+
665
+ if attempts < max_allowed_attempts:
666
+ # Failed registration, retry pow
667
+ attempts += 1
668
+ print_error(
669
+ ":satellite: Failed registration, retrying pow ...({attempts}/{max_allowed_attempts})"
670
+ )
671
+ else:
672
+ # Failed to register after max attempts.
673
+ print_error("No more attempts.")
674
+ return False
675
+
676
+
677
+ async def burned_register_extrinsic(
678
+ meshtensor: "MeshtensorInterface",
679
+ wallet: Wallet,
680
+ netuid: int,
681
+ old_balance: Balance,
682
+ wait_for_inclusion: bool = True,
683
+ wait_for_finalization: bool = True,
684
+ era: Optional[int] = None,
685
+ proxy: Optional[str] = None,
686
+ ) -> tuple[bool, str, Optional[str]]:
687
+ """Registers the wallet to chain by recycling MESH.
688
+
689
+ :param meshtensor: The MeshtensorInterface object to use for the call, initialized
690
+ :param wallet: Meshtensor wallet object.
691
+ :param netuid: The `netuid` of the subnet to register on.
692
+ :param old_balance: The wallet balance prior to the registration burn.
693
+ :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns
694
+ `False` if the extrinsic fails to enter the block within the timeout.
695
+ :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`,
696
+ or returns `False` if the extrinsic fails to be finalized within the timeout.
697
+ :param era: the period (in blocks) for which the transaction should remain valid.
698
+ :param proxy: the proxy address to use for the call.
699
+
700
+ :return: (success, msg), where success is `True` if extrinsic was finalized or included in the block. If we did not
701
+ wait for finalization/inclusion, the response is `True`.
702
+ """
703
+
704
+ if not (unlock_status := unlock_key(wallet, print_out=False)).success:
705
+ return False, unlock_status.message, None
706
+
707
+ with console.status(
708
+ f":satellite: Checking Account on [bold]subnet:{netuid}[/bold]...",
709
+ spinner="aesthetic",
710
+ ) as status:
711
+ my_uid = await meshtensor.query(
712
+ "MeshtensorModule", "Uids", [netuid, get_hotkey_pub_ss58(wallet)]
713
+ )
714
+ block_hash = await meshtensor.substrate.get_chain_head()
715
+
716
+ print_verbose("Checking if already registered", status)
717
+ neuron = await meshtensor.neuron_for_uid(
718
+ uid=my_uid, netuid=netuid, block_hash=block_hash
719
+ )
720
+ if not era:
721
+ current_block, tempo, blocks_since_last_step = await asyncio.gather(
722
+ meshtensor.substrate.get_block_number(block_hash=block_hash),
723
+ meshtensor.get_hyperparameter(
724
+ "Tempo", netuid=netuid, block_hash=block_hash
725
+ ),
726
+ meshtensor.query(
727
+ "MeshtensorModule",
728
+ "BlocksSinceLastStep",
729
+ [netuid],
730
+ block_hash=block_hash,
731
+ ),
732
+ )
733
+ validity_period = tempo - blocks_since_last_step
734
+ era_ = {
735
+ "period": validity_period,
736
+ "current": current_block,
737
+ }
738
+ else:
739
+ era_ = {"period": era}
740
+
741
+ if not neuron.is_null:
742
+ print_success("[dark_sea_green3]Already Registered[/dark_sea_green3]:")
743
+ console.print(
744
+ f"uid: [{COLOR_PALETTE.G.NETUID_EXTRA}]{neuron.uid}[/{COLOR_PALETTE.G.NETUID_EXTRA}]\n"
745
+ f"netuid: [{COLOR_PALETTE.G.NETUID}]{neuron.netuid}[/{COLOR_PALETTE.G.NETUID}]\n"
746
+ f"hotkey: [{COLOR_PALETTE.G.HK}]{neuron.hotkey}[/{COLOR_PALETTE.G.HK}]\n"
747
+ f"coldkey: [{COLOR_PALETTE.G.CK}]{neuron.coldkey}[/{COLOR_PALETTE.G.CK}]"
748
+ )
749
+ return True, "Already registered", None
750
+
751
+ with console.status(
752
+ ":satellite: Recycling MESH for Registration...", spinner="aesthetic"
753
+ ):
754
+ call = await meshtensor.substrate.compose_call(
755
+ call_module="MeshtensorModule",
756
+ call_function="burned_register",
757
+ call_params={
758
+ "netuid": netuid,
759
+ "hotkey": get_hotkey_pub_ss58(wallet),
760
+ },
761
+ )
762
+ success, err_msg, ext_receipt = await meshtensor.sign_and_send_extrinsic(
763
+ call,
764
+ wallet,
765
+ wait_for_inclusion,
766
+ wait_for_finalization,
767
+ era=era_,
768
+ proxy=proxy,
769
+ )
770
+
771
+ if not success:
772
+ print_error(f"Failed: {err_msg}")
773
+ await asyncio.sleep(0.5)
774
+ return False, err_msg, None
775
+ # Successful registration, final check for neuron and pubkey
776
+ else:
777
+ ext_id = await ext_receipt.get_extrinsic_identifier()
778
+ await print_extrinsic_id(ext_receipt)
779
+ with console.status(":satellite: Checking Balance...", spinner="aesthetic"):
780
+ block_hash = await meshtensor.substrate.get_chain_head()
781
+ new_balance, netuids_for_hotkey, my_uid = await asyncio.gather(
782
+ meshtensor.get_balance(
783
+ wallet.coldkeypub.ss58_address,
784
+ block_hash=block_hash,
785
+ reuse_block=False,
786
+ ),
787
+ meshtensor.get_netuids_for_hotkey(
788
+ get_hotkey_pub_ss58(wallet), block_hash=block_hash
789
+ ),
790
+ meshtensor.query(
791
+ "MeshtensorModule", "Uids", [netuid, get_hotkey_pub_ss58(wallet)]
792
+ ),
793
+ )
794
+
795
+ console.print(
796
+ "Balance:\n"
797
+ f" [blue]{old_balance}[/blue] :arrow_right: "
798
+ f"[{COLOR_PALETTE.S.STAKE_AMOUNT}]{new_balance}[/{COLOR_PALETTE.S.STAKE_AMOUNT}]"
799
+ )
800
+
801
+ if len(netuids_for_hotkey) > 0:
802
+ print_success(f"Registered on netuid {netuid} with UID {my_uid}")
803
+ return True, f"Registered on {netuid} with UID {my_uid}", ext_id
804
+ else:
805
+ # neuron not found, try again
806
+ print_error("Unknown error. Neuron not found.")
807
+ return False, "Unknown error. Neuron not found.", ext_id
808
+
809
+
810
+ async def run_faucet_extrinsic(
811
+ meshtensor: "MeshtensorInterface",
812
+ wallet: Wallet,
813
+ wait_for_inclusion: bool = False,
814
+ wait_for_finalization: bool = True,
815
+ prompt: bool = False,
816
+ decline: bool = False,
817
+ quiet: bool = False,
818
+ max_allowed_attempts: int = 3,
819
+ output_in_place: bool = True,
820
+ cuda: bool = False,
821
+ dev_id: int = 0,
822
+ tpb: int = 256,
823
+ num_processes: Optional[int] = None,
824
+ update_interval: Optional[int] = None,
825
+ log_verbose: bool = True,
826
+ max_successes: int = 3,
827
+ ) -> tuple[bool, str]:
828
+ r"""Runs a continual POW to get a faucet of MESH on the test net.
829
+
830
+ :param meshtensor: The meshtensor interface object used to run the extrinsic
831
+ :param wallet: Meshtensor wallet object.
832
+ :param prompt: If `True`, the call waits for confirmation from the user before proceeding.
833
+ :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`,
834
+ or returns `False` if the extrinsic fails to enter the block within the timeout.
835
+ :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`,
836
+ or returns `False` if the extrinsic fails to be finalized within the timeout.
837
+ :param max_allowed_attempts: Maximum number of attempts to register the wallet.
838
+ :param output_in_place: Whether to output logging data as the process runs.
839
+ :param cuda: If `True`, the wallet should be registered using CUDA device(s).
840
+ :param dev_id: The CUDA device id to use
841
+ :param tpb: The number of threads per block (CUDA).
842
+ :param num_processes: The number of processes to use to register.
843
+ :param update_interval: The number of nonces to solve between updates.
844
+ :param log_verbose: If `True`, the registration process will log more information.
845
+ :param max_successes: The maximum number of successful faucet runs for the wallet.
846
+
847
+ :return: `True` if extrinsic was finalized or included in the block. If we did not wait for
848
+ finalization/inclusion, the response is also `True`
849
+ """
850
+ if prompt:
851
+ if not confirm_action(
852
+ "Run Faucet?\n"
853
+ f" wallet name: [bold white]{wallet.name}[/bold white]\n"
854
+ f" coldkey: [bold white]{wallet.coldkeypub.ss58_address}[/bold white]\n"
855
+ f" network: [bold white]{meshtensor}[/bold white]",
856
+ decline=decline,
857
+ quiet=quiet,
858
+ ):
859
+ return False, ""
860
+
861
+ if not torch:
862
+ log_no_torch_error()
863
+ return False, "Requires torch"
864
+
865
+ # Unlock coldkey
866
+ if not (unlock_status := unlock_key(wallet, print_out=False)).success:
867
+ return False, unlock_status.message
868
+
869
+ # Get previous balance.
870
+ old_balance = await meshtensor.get_balance(wallet.coldkeypub.ss58_address)
871
+
872
+ # Attempt rolling registration.
873
+ attempts = 1
874
+ successes = 1
875
+ pow_result: Optional[POWSolution]
876
+ while True:
877
+ try:
878
+ account_nonce = await meshtensor.substrate.get_account_nonce(
879
+ wallet.coldkey.ss58_address
880
+ )
881
+ pow_result = None
882
+ while pow_result is None or await pow_result.is_stale(meshtensor=meshtensor):
883
+ # Solve latest POW.
884
+ if cuda:
885
+ if not torch.cuda.is_available():
886
+ if prompt:
887
+ print_error("CUDA is not available.")
888
+ return False, "CUDA is not available."
889
+ pow_result = await create_pow(
890
+ meshtensor,
891
+ wallet,
892
+ -1,
893
+ output_in_place,
894
+ cuda=cuda,
895
+ dev_id=dev_id,
896
+ tpb=tpb,
897
+ num_processes=num_processes,
898
+ update_interval=update_interval,
899
+ log_verbose=log_verbose,
900
+ )
901
+ else:
902
+ pow_result = await create_pow(
903
+ meshtensor,
904
+ wallet,
905
+ -1,
906
+ output_in_place,
907
+ cuda=cuda,
908
+ num_processes=num_processes,
909
+ update_interval=update_interval,
910
+ log_verbose=log_verbose,
911
+ )
912
+ call = await meshtensor.substrate.compose_call(
913
+ call_module="MeshtensorModule",
914
+ call_function="faucet",
915
+ call_params={
916
+ "block_number": pow_result.block_number,
917
+ "nonce": pow_result.nonce,
918
+ "work": [int(byte_) for byte_ in pow_result.seal],
919
+ },
920
+ )
921
+ extrinsic = await meshtensor.substrate.create_signed_extrinsic(
922
+ call=call, keypair=wallet.coldkey, nonce=account_nonce
923
+ )
924
+ response = await meshtensor.substrate.submit_extrinsic(
925
+ extrinsic,
926
+ wait_for_inclusion=wait_for_inclusion,
927
+ wait_for_finalization=wait_for_finalization,
928
+ )
929
+
930
+ # process if registration successful, try again if pow is still valid
931
+ if not await response.is_success:
932
+ print_error(
933
+ f"Failed: {format_error_message(await response.error_message)}"
934
+ )
935
+ if attempts == max_allowed_attempts:
936
+ raise MaxAttemptsException
937
+ attempts += 1
938
+ # Wait a bit before trying again
939
+ time.sleep(1)
940
+
941
+ # Successful registration
942
+ else:
943
+ new_balance = await meshtensor.get_balance(
944
+ wallet.coldkeypub.ss58_address
945
+ )
946
+ console.print(
947
+ f"Balance: [blue]{old_balance}[/blue] :arrow_right:"
948
+ f" [green]{new_balance}[/green]"
949
+ )
950
+ old_balance = new_balance
951
+
952
+ if successes == max_successes:
953
+ raise MaxSuccessException
954
+
955
+ attempts = 1 # Reset attempts on success
956
+ successes += 1
957
+
958
+ except KeyboardInterrupt:
959
+ return True, "Done"
960
+
961
+ except MaxSuccessException:
962
+ return True, f"Max successes reached: {3}"
963
+
964
+ except MaxAttemptsException:
965
+ return False, f"Max attempts reached: {max_allowed_attempts}"
966
+
967
+
968
+ async def _check_for_newest_block_and_update(
969
+ meshtensor: "MeshtensorInterface",
970
+ netuid: int,
971
+ old_block_number: int,
972
+ hotkey_bytes: bytes,
973
+ curr_diff: Array,
974
+ curr_block: Array,
975
+ curr_block_num: Value,
976
+ update_curr_block: typing.Callable,
977
+ check_block: Lock,
978
+ solvers: list[_Solver],
979
+ curr_stats: RegistrationStatistics,
980
+ ) -> int:
981
+ """
982
+ Checks for a new block and updates the current block information if a new block is found.
983
+
984
+ :param meshtensor: The meshtensor object to use for getting the current block.
985
+ :param netuid: The netuid to use for retrieving the difficulty.
986
+ :param old_block_number: The old block number to check against.
987
+ :param hotkey_bytes: The bytes of the hotkey's pubkey.
988
+ :param curr_diff: The current difficulty as a multiprocessing array.
989
+ :param curr_block: Where the current block is stored as a multiprocessing array.
990
+ :param curr_block_num: Where the current block number is stored as a multiprocessing value.
991
+ :param update_curr_block: A function that updates the current block.
992
+ :param check_block: A mp lock that is used to check for a new block.
993
+ :param solvers: A list of solvers to update the current block for.
994
+ :param curr_stats: The current registration statistics to update.
995
+
996
+ :return: The current block number.
997
+ """
998
+ block_number = await meshtensor.substrate.get_block_number(None)
999
+ if block_number != old_block_number:
1000
+ old_block_number = block_number
1001
+ # update block information
1002
+ block_number, difficulty, block_hash = await _get_block_with_retry(
1003
+ meshtensor=meshtensor, netuid=netuid
1004
+ )
1005
+ block_bytes = hex_to_bytes(block_hash)
1006
+
1007
+ update_curr_block(
1008
+ curr_diff,
1009
+ curr_block,
1010
+ curr_block_num,
1011
+ block_number,
1012
+ block_bytes,
1013
+ difficulty,
1014
+ hotkey_bytes,
1015
+ check_block,
1016
+ )
1017
+ # Set new block events for each solver
1018
+
1019
+ for worker in solvers:
1020
+ worker.new_block_event.set()
1021
+
1022
+ # update stats
1023
+ curr_stats.block_number = block_number
1024
+ curr_stats.block_hash = block_hash
1025
+ curr_stats.difficulty = difficulty
1026
+
1027
+ return old_block_number
1028
+
1029
+
1030
+ async def _block_solver(
1031
+ meshtensor: "MeshtensorInterface",
1032
+ wallet: Wallet,
1033
+ num_processes: int,
1034
+ netuid: int,
1035
+ dev_id: list[int],
1036
+ tpb: int,
1037
+ update_interval: int,
1038
+ curr_block,
1039
+ curr_block_num,
1040
+ curr_diff,
1041
+ n_samples,
1042
+ alpha_,
1043
+ output_in_place,
1044
+ log_verbose,
1045
+ cuda: bool,
1046
+ ):
1047
+ """
1048
+ Shared code used by the Solvers to solve the POW solution
1049
+ """
1050
+ limit = int(math.pow(2, 256)) - 1
1051
+
1052
+ # Establish communication queues
1053
+ # See the _Solver class for more information on the queues.
1054
+ stop_event = Event()
1055
+ stop_event.clear()
1056
+
1057
+ solution_queue = Queue()
1058
+ if cuda:
1059
+ num_processes = len(dev_id)
1060
+
1061
+ finished_queues = [Queue() for _ in range(num_processes)]
1062
+ check_block = Lock()
1063
+
1064
+ hotkey_bytes = (
1065
+ wallet.coldkeypub.public_key if netuid == -1 else wallet.hotkey.public_key
1066
+ )
1067
+
1068
+ if cuda:
1069
+ # Create a worker per CUDA device
1070
+ solvers = [
1071
+ _CUDASolver(
1072
+ i,
1073
+ num_processes,
1074
+ update_interval,
1075
+ finished_queues[i],
1076
+ solution_queue,
1077
+ stop_event,
1078
+ curr_block,
1079
+ curr_block_num,
1080
+ curr_diff,
1081
+ check_block,
1082
+ limit,
1083
+ dev_id[i],
1084
+ tpb,
1085
+ )
1086
+ for i in range(num_processes)
1087
+ ]
1088
+ else:
1089
+ # Start consumers
1090
+ solvers = [
1091
+ _Solver(
1092
+ i,
1093
+ num_processes,
1094
+ update_interval,
1095
+ finished_queues[i],
1096
+ solution_queue,
1097
+ stop_event,
1098
+ curr_block,
1099
+ curr_block_num,
1100
+ curr_diff,
1101
+ check_block,
1102
+ limit,
1103
+ )
1104
+ for i in range(num_processes)
1105
+ ]
1106
+
1107
+ # Get first block
1108
+ block_number, difficulty, block_hash = await _get_block_with_retry(
1109
+ meshtensor=meshtensor, netuid=netuid
1110
+ )
1111
+
1112
+ block_bytes = hex_to_bytes(block_hash)
1113
+ old_block_number = block_number
1114
+ # Set to current block
1115
+ _update_curr_block(
1116
+ curr_diff,
1117
+ curr_block,
1118
+ curr_block_num,
1119
+ block_number,
1120
+ block_bytes,
1121
+ difficulty,
1122
+ hotkey_bytes,
1123
+ check_block,
1124
+ )
1125
+
1126
+ # Set new block events for each solver to start at the initial block
1127
+ for worker in solvers:
1128
+ worker.new_block_event.set()
1129
+
1130
+ for worker in solvers:
1131
+ worker.start() # start the solver processes
1132
+
1133
+ start_time = time.time() # time that the registration started
1134
+ time_last = start_time # time that the last work blocks completed
1135
+
1136
+ curr_stats = RegistrationStatistics(
1137
+ time_spent_total=0.0,
1138
+ time_average=0.0,
1139
+ rounds_total=0,
1140
+ time_spent=0.0,
1141
+ hash_rate_perpetual=0.0,
1142
+ hash_rate=0.0,
1143
+ difficulty=difficulty,
1144
+ block_number=block_number,
1145
+ block_hash=block_hash,
1146
+ )
1147
+
1148
+ start_time_perpetual = time.time()
1149
+
1150
+ logger = RegistrationStatisticsLogger(console, output_in_place)
1151
+ logger.start()
1152
+
1153
+ solution = None
1154
+
1155
+ hash_rates = [0] * n_samples # The last n true hash_rates
1156
+ weights = [alpha_**i for i in range(n_samples)] # weights decay by alpha
1157
+
1158
+ timeout = 0.15 if cuda else 0.15
1159
+ while netuid == -1 or not await is_hotkey_registered(
1160
+ meshtensor, netuid, get_hotkey_pub_ss58(wallet)
1161
+ ):
1162
+ # Wait until a solver finds a solution
1163
+ try:
1164
+ solution = solution_queue.get(block=True, timeout=timeout)
1165
+ if solution is not None:
1166
+ break
1167
+ except Empty:
1168
+ # No solution found, try again
1169
+ pass
1170
+
1171
+ # check for new block
1172
+ old_block_number = await _check_for_newest_block_and_update(
1173
+ meshtensor=meshtensor,
1174
+ netuid=netuid,
1175
+ hotkey_bytes=hotkey_bytes,
1176
+ old_block_number=old_block_number,
1177
+ curr_diff=curr_diff,
1178
+ curr_block=curr_block,
1179
+ curr_block_num=curr_block_num,
1180
+ curr_stats=curr_stats,
1181
+ update_curr_block=_update_curr_block,
1182
+ check_block=check_block,
1183
+ solvers=solvers,
1184
+ )
1185
+
1186
+ num_time = 0
1187
+ for finished_queue in finished_queues:
1188
+ try:
1189
+ finished_queue.get(timeout=0.1)
1190
+ num_time += 1
1191
+
1192
+ except Empty:
1193
+ continue
1194
+
1195
+ time_now = time.time() # get current time
1196
+ time_since_last = time_now - time_last # get time since last work block(s)
1197
+ if num_time > 0 and time_since_last > 0.0:
1198
+ # create EWMA of the hash_rate to make measure more robust
1199
+
1200
+ if cuda:
1201
+ hash_rate_ = (num_time * tpb * update_interval) / time_since_last
1202
+ else:
1203
+ hash_rate_ = (num_time * update_interval) / time_since_last
1204
+ hash_rates.append(hash_rate_)
1205
+ hash_rates.pop(0) # remove the 0th data point
1206
+ curr_stats.hash_rate = sum(
1207
+ [hash_rates[i] * weights[i] for i in range(n_samples)]
1208
+ ) / (sum(weights))
1209
+
1210
+ # update time last to now
1211
+ time_last = time_now
1212
+
1213
+ curr_stats.time_average = (
1214
+ curr_stats.time_average * curr_stats.rounds_total
1215
+ + curr_stats.time_spent
1216
+ ) / (curr_stats.rounds_total + num_time)
1217
+ curr_stats.rounds_total += num_time
1218
+
1219
+ # Update stats
1220
+ curr_stats.time_spent = time_since_last
1221
+ new_time_spent_total = time_now - start_time_perpetual
1222
+ if cuda:
1223
+ curr_stats.hash_rate_perpetual = (
1224
+ curr_stats.rounds_total * (tpb * update_interval)
1225
+ ) / new_time_spent_total
1226
+ else:
1227
+ curr_stats.hash_rate_perpetual = (
1228
+ curr_stats.rounds_total * update_interval
1229
+ ) / new_time_spent_total
1230
+ curr_stats.time_spent_total = new_time_spent_total
1231
+
1232
+ # Update the logger
1233
+ logger.update(curr_stats, verbose=log_verbose)
1234
+
1235
+ # exited while, solution contains the nonce or wallet is registered
1236
+ stop_event.set() # stop all other processes
1237
+ logger.stop()
1238
+
1239
+ # terminate and wait for all solvers to exit
1240
+ _terminate_workers_and_wait_for_exit(solvers)
1241
+
1242
+ return solution
1243
+
1244
+
1245
+ async def _solve_for_difficulty_fast_cuda(
1246
+ meshtensor: "MeshtensorInterface",
1247
+ wallet: Wallet,
1248
+ netuid: int,
1249
+ output_in_place: bool = True,
1250
+ update_interval: int = 50_000,
1251
+ tpb: int = 512,
1252
+ dev_id: typing.Union[list[int], int] = 0,
1253
+ n_samples: int = 10,
1254
+ alpha_: float = 0.80,
1255
+ log_verbose: bool = False,
1256
+ ) -> Optional[POWSolution]:
1257
+ """
1258
+ Solves the registration fast using CUDA
1259
+
1260
+ :param meshtensor: The meshtensor node to grab blocks
1261
+ :param wallet: The wallet to register
1262
+ :param netuid: The netuid of the subnet to register to.
1263
+ :param output_in_place: If true, prints the output in place, otherwise prints to new lines
1264
+ :param update_interval: The number of nonces to try before checking for more blocks
1265
+ :param tpb: The number of threads per block. CUDA param that should match the GPU capability
1266
+ :param dev_id: The CUDA device IDs to execute the registration on, either a single device or a list of devices
1267
+ :param n_samples: The number of samples of the hash_rate to keep for the EWMA
1268
+ :param alpha_: The alpha for the EWMA for the hash_rate calculation
1269
+ :param log_verbose: If true, prints more verbose logging of the registration metrics.
1270
+
1271
+ Note: The hash rate is calculated as an exponentially weighted moving average in order to make the measure more
1272
+ robust.
1273
+ """
1274
+ if isinstance(dev_id, int):
1275
+ dev_id = [dev_id]
1276
+ elif dev_id is None:
1277
+ dev_id = [0]
1278
+
1279
+ if update_interval is None:
1280
+ update_interval = 50_000
1281
+
1282
+ if not torch.cuda.is_available():
1283
+ raise Exception("CUDA not available")
1284
+
1285
+ # Set mp start to use spawn so CUDA doesn't complain
1286
+ with _UsingSpawnStartMethod(force=True):
1287
+ curr_block, curr_block_num, curr_diff = _CUDASolver.create_shared_memory()
1288
+
1289
+ solution = await _block_solver(
1290
+ meshtensor=meshtensor,
1291
+ wallet=wallet,
1292
+ num_processes=None,
1293
+ netuid=netuid,
1294
+ dev_id=dev_id,
1295
+ tpb=tpb,
1296
+ update_interval=update_interval,
1297
+ curr_block=curr_block,
1298
+ curr_block_num=curr_block_num,
1299
+ curr_diff=curr_diff,
1300
+ n_samples=n_samples,
1301
+ alpha_=alpha_,
1302
+ output_in_place=output_in_place,
1303
+ log_verbose=log_verbose,
1304
+ cuda=True,
1305
+ )
1306
+
1307
+ return solution
1308
+
1309
+
1310
+ async def _solve_for_difficulty_fast(
1311
+ meshtensor,
1312
+ wallet: Wallet,
1313
+ netuid: int,
1314
+ output_in_place: bool = True,
1315
+ num_processes: Optional[int] = None,
1316
+ update_interval: Optional[int] = None,
1317
+ n_samples: int = 10,
1318
+ alpha_: float = 0.80,
1319
+ log_verbose: bool = False,
1320
+ ) -> Optional[POWSolution]:
1321
+ """
1322
+ Solves the POW for registration using multiprocessing.
1323
+
1324
+ :param meshtensor: Meshtensor to connect to for block information and to submit.
1325
+ :param wallet: wallet to use for registration.
1326
+ :param netuid: The netuid of the subnet to register to.
1327
+ :param output_in_place: If true, prints the status in place. Otherwise, prints the status on a new line.
1328
+ :param num_processes: Number of processes to use.
1329
+ :param update_interval: Number of nonces to solve before updating block information.
1330
+ :param n_samples: The number of samples of the hash_rate to keep for the EWMA
1331
+ :param alpha_: The alpha for the EWMA for the hash_rate calculation
1332
+ :param log_verbose: If true, prints more verbose logging of the registration metrics.
1333
+
1334
+ Notes:
1335
+
1336
+ - The hash rate is calculated as an exponentially weighted moving average in order to make the measure more robust.
1337
+ - We can also modify the update interval to do smaller blocks of work, while still updating the block information
1338
+ after a different number of nonces, to increase the transparency of the process while still keeping the speed.
1339
+ """
1340
+ if not num_processes:
1341
+ # get the number of allowed processes for this process
1342
+ num_processes = min(1, get_cpu_count())
1343
+
1344
+ if update_interval is None:
1345
+ update_interval = 50_000
1346
+
1347
+ curr_block, curr_block_num, curr_diff = _Solver.create_shared_memory()
1348
+
1349
+ solution = await _block_solver(
1350
+ meshtensor=meshtensor,
1351
+ wallet=wallet,
1352
+ num_processes=num_processes,
1353
+ netuid=netuid,
1354
+ dev_id=None,
1355
+ tpb=None,
1356
+ update_interval=update_interval,
1357
+ curr_block=curr_block,
1358
+ curr_block_num=curr_block_num,
1359
+ curr_diff=curr_diff,
1360
+ n_samples=n_samples,
1361
+ alpha_=alpha_,
1362
+ output_in_place=output_in_place,
1363
+ log_verbose=log_verbose,
1364
+ cuda=False,
1365
+ )
1366
+
1367
+ return solution
1368
+
1369
+
1370
+ def _terminate_workers_and_wait_for_exit(
1371
+ workers: list[typing.Union[Process, Queue_Type]],
1372
+ ) -> None:
1373
+ for worker in workers:
1374
+ if isinstance(worker, Queue_Type):
1375
+ worker.join_thread()
1376
+ else:
1377
+ try:
1378
+ worker.join(3.0)
1379
+ except subprocess.TimeoutExpired:
1380
+ worker.terminate()
1381
+ try:
1382
+ worker.close()
1383
+ except ValueError:
1384
+ worker.terminate()
1385
+
1386
+
1387
+ async def _get_block_with_retry(
1388
+ meshtensor: "MeshtensorInterface", netuid: int
1389
+ ) -> tuple[int, int, str]:
1390
+ """
1391
+ Gets the current block number, difficulty, and block hash from the substrate node.
1392
+
1393
+ :param meshtensor: The meshtensor object to use to get the block number, difficulty, and block hash.
1394
+ :param netuid: The netuid of the network to get the block number, difficulty, and block hash from.
1395
+
1396
+ :return: The current block number, difficulty of the subnet, block hash
1397
+
1398
+ :raises Exception: If the block hash is None.
1399
+ :raises ValueError: If the difficulty is None.
1400
+ """
1401
+ block = await meshtensor.substrate.get_block()
1402
+ block_hash = block["header"]["hash"]
1403
+ block_number = block["header"]["number"]
1404
+ try:
1405
+ difficulty = (
1406
+ 1_000_000
1407
+ if netuid == -1
1408
+ else int(
1409
+ await meshtensor.get_hyperparameter(
1410
+ param_name="Difficulty", netuid=netuid, block_hash=block_hash
1411
+ )
1412
+ )
1413
+ )
1414
+ except TypeError:
1415
+ raise ValueError("Chain error. Difficulty is None")
1416
+ except SubstrateRequestException:
1417
+ raise Exception(
1418
+ "Network error. Could not connect to substrate to get block hash"
1419
+ )
1420
+ return block_number, difficulty, block_hash
1421
+
1422
+
1423
+ def _registration_diff_unpack(packed_diff: Array) -> int:
1424
+ """Unpacks the packed two 32-bit integers into one 64-bit integer. Little endian."""
1425
+ return int(packed_diff[0] << 32 | packed_diff[1])
1426
+
1427
+
1428
+ def _registration_diff_pack(diff: int, packed_diff: Array):
1429
+ """Packs the difficulty into two 32-bit integers. Little endian."""
1430
+ packed_diff[0] = diff >> 32
1431
+ packed_diff[1] = diff & 0xFFFFFFFF # low 32 bits
1432
+
1433
+
1434
+ class _UsingSpawnStartMethod:
1435
+ def __init__(self, force: bool = False):
1436
+ self._old_start_method = None
1437
+ self._force = force
1438
+
1439
+ def __enter__(self):
1440
+ self._old_start_method = mp.get_start_method(allow_none=True)
1441
+ if self._old_start_method is None:
1442
+ self._old_start_method = "spawn" # default to spawn
1443
+
1444
+ mp.set_start_method("spawn", force=self._force)
1445
+
1446
+ def __exit__(self, *args):
1447
+ # restore the old start method
1448
+ mp.set_start_method(self._old_start_method, force=True)
1449
+
1450
+
1451
+ async def create_pow(
1452
+ meshtensor: "MeshtensorInterface",
1453
+ wallet: Wallet,
1454
+ netuid: int,
1455
+ output_in_place: bool = True,
1456
+ cuda: bool = False,
1457
+ dev_id: typing.Union[list[int], int] = 0,
1458
+ tpb: int = 256,
1459
+ num_processes: int = None,
1460
+ update_interval: int = None,
1461
+ log_verbose: bool = False,
1462
+ ) -> Optional[dict[str, typing.Any]]:
1463
+ """
1464
+ Creates a proof of work for the given meshtensor and wallet.
1465
+
1466
+ :param meshtensor: The meshtensor to create a proof of work for.
1467
+ :param wallet: The wallet to create a proof of work for.
1468
+ :param netuid: The netuid for the subnet to create a proof of work for.
1469
+ :param output_in_place: If true, prints the progress of the proof of work to the console
1470
+ in-place. Meaning the progress is printed on the same lines.
1471
+ :param cuda: If true, uses CUDA to solve the proof of work.
1472
+ :param dev_id: The CUDA device id(s) to use. If cuda is true and dev_id is a list,
1473
+ then multiple CUDA devices will be used to solve the proof of work.
1474
+ :param tpb: The number of threads per block to use when solving the proof of work. Should be a multiple of 32.
1475
+ :param num_processes: The number of processes to use when solving the proof of work.
1476
+ If None, then the number of processes is equal to the number of CPU cores.
1477
+ :param update_interval: The number of nonces to run before checking for a new block.
1478
+ :param log_verbose: If true, prints the progress of the proof of work more verbosely.
1479
+
1480
+ :return: The proof of work solution or None if the wallet is already registered or there is a different error.
1481
+
1482
+ :raises ValueError: If the subnet does not exist.
1483
+ """
1484
+ if netuid != -1:
1485
+ if not await meshtensor.subnet_exists(netuid=netuid):
1486
+ raise ValueError(f"Subnet {netuid} does not exist")
1487
+
1488
+ if cuda:
1489
+ solution: Optional[POWSolution] = await _solve_for_difficulty_fast_cuda(
1490
+ meshtensor,
1491
+ wallet,
1492
+ netuid=netuid,
1493
+ output_in_place=output_in_place,
1494
+ dev_id=dev_id,
1495
+ tpb=tpb,
1496
+ update_interval=update_interval,
1497
+ log_verbose=log_verbose,
1498
+ )
1499
+ else:
1500
+ solution: Optional[POWSolution] = await _solve_for_difficulty_fast(
1501
+ meshtensor,
1502
+ wallet,
1503
+ netuid=netuid,
1504
+ output_in_place=output_in_place,
1505
+ num_processes=num_processes,
1506
+ update_interval=update_interval,
1507
+ log_verbose=log_verbose,
1508
+ )
1509
+
1510
+ return solution
1511
+
1512
+
1513
+ def _solve_for_nonce_block_cuda(
1514
+ nonce_start: int,
1515
+ update_interval: int,
1516
+ block_and_hotkey_hash_bytes: bytes,
1517
+ difficulty: int,
1518
+ limit: int,
1519
+ block_number: int,
1520
+ dev_id: int,
1521
+ tpb: int,
1522
+ ) -> Optional[POWSolution]:
1523
+ """
1524
+ Tries to solve the POW on a CUDA device for a block of nonces (nonce_start, nonce_start + update_interval * tpb
1525
+ """
1526
+ solution, seal = solve_cuda(
1527
+ nonce_start,
1528
+ update_interval,
1529
+ tpb,
1530
+ block_and_hotkey_hash_bytes,
1531
+ difficulty,
1532
+ limit,
1533
+ dev_id,
1534
+ )
1535
+
1536
+ if solution != -1:
1537
+ # Check if solution is valid (i.e. not -1)
1538
+ return POWSolution(solution, block_number, difficulty, seal)
1539
+
1540
+ return None
1541
+
1542
+
1543
+ def _solve_for_nonce_block(
1544
+ nonce_start: int,
1545
+ nonce_end: int,
1546
+ block_and_hotkey_hash_bytes: bytes,
1547
+ difficulty: int,
1548
+ limit: int,
1549
+ block_number: int,
1550
+ ) -> Optional[POWSolution]:
1551
+ """
1552
+ Tries to solve the POW for a block of nonces (nonce_start, nonce_end)
1553
+ """
1554
+ for nonce in range(nonce_start, nonce_end):
1555
+ # Create seal.
1556
+ seal = _create_seal_hash(block_and_hotkey_hash_bytes, nonce)
1557
+
1558
+ # Check if seal meets difficulty
1559
+ if _seal_meets_difficulty(seal, difficulty, limit):
1560
+ # Found a solution, save it.
1561
+ return POWSolution(nonce, block_number, difficulty, seal)
1562
+
1563
+ return None
1564
+
1565
+
1566
+ class CUDAException(Exception):
1567
+ """An exception raised when an error occurs in the CUDA environment."""
1568
+
1569
+
1570
+ def _hex_bytes_to_u8_list(hex_bytes: bytes):
1571
+ hex_chunks = [int(hex_bytes[i : i + 2], 16) for i in range(0, len(hex_bytes), 2)]
1572
+ return hex_chunks
1573
+
1574
+
1575
+ def _create_seal_hash(block_and_hotkey_hash_bytes: bytes, nonce: int) -> bytes:
1576
+ """
1577
+ Create a cryptographic seal hash from the given block and hotkey hash bytes and nonce.
1578
+
1579
+ This function generates a seal hash by combining the given block and hotkey hash bytes with a nonce.
1580
+ It first converts the nonce to a byte representation, then concatenates it with the first 64 hex
1581
+ characters of the block and hotkey hash bytes. The result is then hashed using SHA-256 followed by
1582
+ the Keccak-256 algorithm to produce the final seal hash.
1583
+
1584
+ :param block_and_hotkey_hash_bytes: The combined hash bytes of the block and hotkey.
1585
+ :param nonce: The nonce value used for hashing.
1586
+
1587
+ :return: The resulting seal hash.
1588
+ """
1589
+ nonce_bytes = binascii.hexlify(nonce.to_bytes(8, "little"))
1590
+ pre_seal = nonce_bytes + binascii.hexlify(block_and_hotkey_hash_bytes)[:64]
1591
+ seal_sh256 = hashlib.sha256(bytearray(_hex_bytes_to_u8_list(pre_seal))).digest()
1592
+ kec = keccak.new(digest_bits=256)
1593
+ seal = kec.update(seal_sh256).digest()
1594
+ return seal
1595
+
1596
+
1597
+ def _seal_meets_difficulty(seal: bytes, difficulty: int, limit: int) -> bool:
1598
+ """Determines if a seal meets the specified difficulty"""
1599
+ seal_number = int.from_bytes(seal, "big")
1600
+ product = seal_number * difficulty
1601
+ return product < limit
1602
+
1603
+
1604
+ def _hash_block_with_hotkey(block_bytes: bytes, hotkey_bytes: bytes) -> bytes:
1605
+ """Hashes the block with the hotkey using Keccak-256 to get 32 bytes"""
1606
+ kec = keccak.new(digest_bits=256)
1607
+ kec = kec.update(bytearray(block_bytes + hotkey_bytes))
1608
+ block_and_hotkey_hash_bytes = kec.digest()
1609
+ return block_and_hotkey_hash_bytes
1610
+
1611
+
1612
+ def _update_curr_block(
1613
+ curr_diff: Array,
1614
+ curr_block: Array,
1615
+ curr_block_num: Value,
1616
+ block_number: int,
1617
+ block_bytes: bytes,
1618
+ diff: int,
1619
+ hotkey_bytes: bytes,
1620
+ lock: Lock,
1621
+ ):
1622
+ """
1623
+ Update the current block data with the provided block information and difficulty.
1624
+
1625
+ This function updates the current block
1626
+ and its difficulty in a thread-safe manner. It sets the current block
1627
+ number, hashes the block with the hotkey, updates the current block bytes, and packs the difficulty.
1628
+
1629
+ :param curr_diff: Shared array to store the current difficulty.
1630
+ :param curr_block: Shared array to store the current block data.
1631
+ :param curr_block_num: Shared value to store the current block number.
1632
+ :param block_number: The block number to set as the current block number.
1633
+ :param block_bytes: The block data bytes to be hashed with the hotkey.
1634
+ :param diff: The difficulty value to be packed into the current difficulty array.
1635
+ :param hotkey_bytes: The hotkey bytes used for hashing the block.
1636
+ :param lock: A lock to ensure thread-safe updates.
1637
+ """
1638
+ with lock:
1639
+ curr_block_num.value = block_number
1640
+ # Hash the block with the hotkey
1641
+ block_and_hotkey_hash_bytes = _hash_block_with_hotkey(block_bytes, hotkey_bytes)
1642
+ for i in range(32):
1643
+ curr_block[i] = block_and_hotkey_hash_bytes[i]
1644
+ _registration_diff_pack(diff, curr_diff)
1645
+
1646
+
1647
+ def get_cpu_count() -> int:
1648
+ try:
1649
+ return len(os.sched_getaffinity(0))
1650
+ except AttributeError:
1651
+ # macOS does not have sched_getaffinity
1652
+ return os.cpu_count()
1653
+
1654
+
1655
+ @dataclass
1656
+ class RegistrationStatistics:
1657
+ """Statistics for a registration."""
1658
+
1659
+ time_spent_total: float
1660
+ rounds_total: int
1661
+ time_average: float
1662
+ time_spent: float
1663
+ hash_rate_perpetual: float
1664
+ hash_rate: float
1665
+ difficulty: int
1666
+ block_number: int
1667
+ block_hash: bytes
1668
+
1669
+
1670
+ def solve_cuda(
1671
+ nonce_start: np.int64,
1672
+ update_interval: np.int64,
1673
+ tpb: int,
1674
+ block_and_hotkey_hash_bytes: bytes,
1675
+ difficulty: int,
1676
+ limit: int,
1677
+ dev_id: int = 0,
1678
+ ) -> tuple[np.int64, bytes]:
1679
+ """
1680
+ Solves the PoW problem using CUDA.
1681
+
1682
+ :param nonce_start: Starting nonce.
1683
+ :param update_interval: Number of nonces to solve before updating block information.
1684
+ :param tpb: Threads per block.
1685
+ :param block_and_hotkey_hash_bytes: Keccak(Bytes of the block hash + bytes of the hotkey) 64 bytes.
1686
+ :param difficulty: Difficulty of the PoW problem.
1687
+ :param limit: Upper limit of the nonce.
1688
+ :param dev_id: The CUDA device ID
1689
+
1690
+ :return: (nonce, seal) corresponding to the solution. Returns -1 for nonce if no solution is found.
1691
+ """
1692
+
1693
+ try:
1694
+ import cubit
1695
+ except ImportError:
1696
+ raise ImportError("Please install cubit")
1697
+
1698
+ upper = int(limit // difficulty)
1699
+
1700
+ upper_bytes = upper.to_bytes(32, byteorder="little", signed=False)
1701
+
1702
+ # Call cython function
1703
+ # int blockSize, uint64 nonce_start, uint64 update_interval, const unsigned char[:] limit,
1704
+ # const unsigned char[:] block_bytes, int dev_id
1705
+ block_and_hotkey_hash_hex = binascii.hexlify(block_and_hotkey_hash_bytes)[:64]
1706
+
1707
+ solution = cubit.solve_cuda(
1708
+ tpb,
1709
+ nonce_start,
1710
+ update_interval,
1711
+ upper_bytes,
1712
+ block_and_hotkey_hash_hex,
1713
+ dev_id,
1714
+ ) # 0 is first GPU
1715
+ seal = None
1716
+ if solution != -1:
1717
+ seal = _create_seal_hash(block_and_hotkey_hash_hex, solution)
1718
+ if _seal_meets_difficulty(seal, difficulty, limit):
1719
+ return solution, seal
1720
+ else:
1721
+ return -1, b"\x00" * 32
1722
+
1723
+ return solution, seal
1724
+
1725
+
1726
+ def reset_cuda():
1727
+ """
1728
+ Resets the CUDA environment.
1729
+ """
1730
+ try:
1731
+ import cubit
1732
+ except ImportError:
1733
+ raise ImportError("Please install cubit")
1734
+
1735
+ cubit.reset_cuda()
1736
+
1737
+
1738
+ def log_cuda_errors() -> str:
1739
+ """
1740
+ Logs any CUDA errors.
1741
+ """
1742
+ try:
1743
+ import cubit
1744
+ except ImportError:
1745
+ raise ImportError("Please install cubit")
1746
+
1747
+ f = io.StringIO()
1748
+ with redirect_stdout(f):
1749
+ cubit.log_cuda_errors()
1750
+
1751
+ s = f.getvalue()
1752
+
1753
+ return s
1754
+
1755
+
1756
+ async def swap_hotkey_extrinsic(
1757
+ meshtensor: "MeshtensorInterface",
1758
+ wallet: Wallet,
1759
+ new_wallet: Wallet,
1760
+ netuid: Optional[int] = None,
1761
+ proxy: Optional[str] = None,
1762
+ prompt: bool = False,
1763
+ decline: bool = False,
1764
+ quiet: bool = False,
1765
+ ) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]:
1766
+ """
1767
+ Performs an extrinsic update for swapping two hotkeys on the chain
1768
+
1769
+ :return: Success
1770
+ """
1771
+ block_hash = await meshtensor.substrate.get_chain_head()
1772
+ hk_ss58 = get_hotkey_pub_ss58(wallet)
1773
+ new_hk_ss58 = get_hotkey_pub_ss58(new_wallet)
1774
+
1775
+ netuids_registered = await meshtensor.get_netuids_for_hotkey(
1776
+ hk_ss58, block_hash=block_hash
1777
+ )
1778
+ netuids_registered_new_hotkey = await meshtensor.get_netuids_for_hotkey(
1779
+ new_hk_ss58, block_hash=block_hash
1780
+ )
1781
+
1782
+ if netuid is not None and netuid not in netuids_registered:
1783
+ print_error(
1784
+ f"Failed: Original hotkey {hk_ss58} is not registered on subnet {netuid}"
1785
+ )
1786
+ return False, None
1787
+
1788
+ elif not len(netuids_registered) > 0:
1789
+ print_error(
1790
+ f"Original hotkey [dark_orange]{hk_ss58}[/dark_orange] is not registered on any subnet. "
1791
+ f"Please register and try again"
1792
+ )
1793
+ return False, None
1794
+
1795
+ if netuid is not None:
1796
+ if netuid in netuids_registered_new_hotkey:
1797
+ print_error(
1798
+ f"Failed: New hotkey {new_hk_ss58} "
1799
+ f"is already registered on subnet {netuid}"
1800
+ )
1801
+ return False, None
1802
+ else:
1803
+ if len(netuids_registered_new_hotkey) > 0:
1804
+ print_error(
1805
+ f"Failed: New hotkey {new_hk_ss58} "
1806
+ f"is already registered on subnet(s) {netuids_registered_new_hotkey}"
1807
+ )
1808
+ return False, None
1809
+
1810
+ if not unlock_key(wallet).success:
1811
+ return False, None
1812
+
1813
+ if prompt:
1814
+ # Prompt user for confirmation.
1815
+ if netuid is not None:
1816
+ confirm_message = (
1817
+ f"Do you want to swap [dark_orange]{wallet.name}[/dark_orange] hotkey \n\t"
1818
+ f"[dark_orange]{hk_ss58} ({wallet.hotkey_str})[/dark_orange] with hotkey \n\t"
1819
+ f"[dark_orange]{new_hk_ss58} ({new_wallet.hotkey_str})[/dark_orange] on subnet {netuid}\n"
1820
+ "This operation will cost [bold cyan]1 MESH (recycled)[/bold cyan]"
1821
+ )
1822
+ else:
1823
+ confirm_message = (
1824
+ f"Do you want to swap [dark_orange]{wallet.name}[/dark_orange] hotkey \n\t"
1825
+ f"[dark_orange]{hk_ss58} ({wallet.hotkey_str})[/dark_orange] with hotkey \n\t"
1826
+ f"[dark_orange]{new_hk_ss58} ({new_wallet.hotkey_str})[/dark_orange] on all subnets\n"
1827
+ "This operation will cost [bold cyan]1 MESH (recycled)[/bold cyan]"
1828
+ )
1829
+
1830
+ if not confirm_action(confirm_message, decline=decline, quiet=quiet):
1831
+ return False, None
1832
+ print_verbose(
1833
+ f"Swapping {wallet.name}'s hotkey ({hk_ss58} - {wallet.hotkey_str}) with "
1834
+ f"{new_wallet.name}'s hotkey ({new_hk_ss58} - {new_wallet.hotkey_str})"
1835
+ )
1836
+ with console.status(":satellite: Swapping hotkeys...", spinner="aesthetic"):
1837
+ call_params = {
1838
+ "hotkey": hk_ss58,
1839
+ "new_hotkey": new_hk_ss58,
1840
+ "netuid": netuid,
1841
+ }
1842
+
1843
+ call = await meshtensor.substrate.compose_call(
1844
+ call_module="MeshtensorModule",
1845
+ call_function="swap_hotkey",
1846
+ call_params=call_params,
1847
+ )
1848
+ success, err_msg, ext_receipt = await meshtensor.sign_and_send_extrinsic(
1849
+ call=call, wallet=wallet, proxy=proxy
1850
+ )
1851
+
1852
+ if success:
1853
+ console.print(
1854
+ f"Hotkey {hk_ss58} ({wallet.hotkey_str}) swapped for new hotkey: "
1855
+ f"{new_hk_ss58} ({new_wallet.hotkey_str})"
1856
+ )
1857
+ return True, ext_receipt
1858
+ else:
1859
+ print_error(f"Failed: {err_msg}")
1860
+ time.sleep(0.5)
1861
+ return False, ext_receipt