diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 2141055cca..43a0ff9252 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -22,6 +22,7 @@ SubnetHyperparameters, decode_account_id, ) +from bittensor.core.extrinsics.async_commit_reveal import commit_reveal_v3_extrinsic from bittensor.core.extrinsics.async_registration import register_extrinsic from bittensor.core.extrinsics.async_root import ( set_root_weights_extrinsic, @@ -1596,18 +1597,44 @@ async def set_weights( This function is crucial in shaping the network's collective intelligence, where each neuron's learning and contribution are influenced by the weights it sets towards others【81†source】. """ + retries = 0 + success = False + if ( + uid := await self.get_uid_for_hotkey_on_subnet( + wallet.hotkey.ss58_address, netuid + ) + ) is None: + return ( + False, + f"Hotkey {wallet.hotkey.ss58_address} not registered in subnet {netuid}", + ) + if (await self.commit_reveal_enabled(netuid=netuid)) is True: # go with `commit reveal v3` extrinsic - raise NotImplementedError( - "Not implemented yet for AsyncSubtensor. Coming soon." - ) + message = "No attempt made. Perhaps it is too soon to commit weights!" + while ( + await self.blocks_since_last_update(netuid, uid) + > await self.weights_rate_limit(netuid) + and retries < max_retries + and success is False + ): + logging.info( + f"Committing weights for subnet #{netuid}. Attempt {retries + 1} of {max_retries}." + ) + success, message = await commit_reveal_v3_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + uids=uids, + weights=weights, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + retries += 1 + return success, message else: # go with classic `set weights extrinsic` - uid = await self.get_uid_for_hotkey_on_subnet( - wallet.hotkey.ss58_address, netuid - ) - retries = 0 - success = False message = "No attempt made. Perhaps it is too soon to set weights!" while ( retries < max_retries diff --git a/bittensor/core/extrinsics/async_commit_reveal.py b/bittensor/core/extrinsics/async_commit_reveal.py new file mode 100644 index 0000000000..b1c2bea094 --- /dev/null +++ b/bittensor/core/extrinsics/async_commit_reveal.py @@ -0,0 +1,152 @@ +from typing import Optional, Union, TYPE_CHECKING + +import numpy as np +from bittensor_commit_reveal import get_encrypted_commit +from numpy.typing import NDArray + +from bittensor.core.settings import version_as_int +from bittensor.utils import format_error_message +from bittensor.utils.btlogging import logging +from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.async_subtensor import AsyncSubtensor + from bittensor.utils.registration import torch + + +async def _do_commit_reveal_v3( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + commit: bytes, + reveal_round: int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, +) -> tuple[bool, Optional[str]]: + """ + Executes the commit-reveal phase 3 for a given netuid and commit, and optionally waits for extrinsic inclusion or finalization. + + Arguments: + subtensor: An instance of the Subtensor class. + wallet: Wallet An instance of the Wallet class containing the user's keypair. + netuid: int The network unique identifier. + commit bytes The commit data in bytes format. + reveal_round: int The round number for the reveal phase. + wait_for_inclusion: bool, optional Flag indicating whether to wait for the extrinsic to be included in a block. + wait_for_finalization: bool, optional Flag indicating whether to wait for the extrinsic to be finalized. + + Returns: + A tuple where the first element is a boolean indicating success or failure, and the second element is an optional string containing error message if any. + """ + logging.info( + f"Committing weights hash [blue]{commit.hex()}[/blue] for subnet #[blue]{netuid}[/blue] with " + f"reveal round [blue]{reveal_round}[/blue]..." + ) + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="commit_crv3_weights", + call_params={ + "netuid": netuid, + "commit": commit, + "reveal_round": reveal_round, + }, + ) + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, + keypair=wallet.hotkey, + ) + + response = await subtensor.substrate.submit_extrinsic( + subtensor=subtensor, + extrinsic=extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True, "Not waiting for finalization or inclusion." + + if await response.is_success: + return True, None + + return False, format_error_message(await response.error_message) + + +async def commit_reveal_v3_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + uids: Union[NDArray[np.int64], "torch.LongTensor", list], + weights: Union[NDArray[np.float32], "torch.FloatTensor", list], + version_key: int = version_as_int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, +) -> tuple[bool, str]: + """ + Commits and reveals weights for given subtensor and wallet with provided uids and weights. + + Arguments: + subtensor: The Subtensor instance. + wallet: The wallet to use for committing and revealing. + netuid: The id of the network. + uids: The uids to commit. + weights: The weights associated with the uids. + version_key: The version key to use for committing and revealing. Default is version_as_int. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. Default is False. + wait_for_finalization: Whether to wait for the finalization of the transaction. Default is False. + + Returns: + tuple[bool, str]: A tuple where the first element is a boolean indicating success or failure, and the second element is a message associated with the result. + """ + try: + # Convert uids and weights + if isinstance(uids, list): + uids = np.array(uids, dtype=np.int64) + if isinstance(weights, list): + weights = np.array(weights, dtype=np.float32) + + # Reformat and normalize. + uids, weights = convert_weights_and_uids_for_emit(uids, weights) + + current_block = await subtensor.substrate.get_block_number(None) + subnet_hyperparameters = await subtensor.get_subnet_hyperparameters(netuid) + tempo = subnet_hyperparameters.tempo + subnet_reveal_period_epochs = ( + subnet_hyperparameters.commit_reveal_weights_interval + ) + + # Encrypt `commit_hash` with t-lock and `get reveal_round` + commit_for_reveal, reveal_round = get_encrypted_commit( + uids=uids, + weights=weights, + version_key=version_key, + tempo=tempo, + current_block=current_block, + netuid=netuid, + subnet_reveal_period_epochs=subnet_reveal_period_epochs, + ) + + success, message = await _do_commit_reveal_v3( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + commit=commit_for_reveal, + reveal_round=reveal_round, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if success is not True: + logging.error(message) + return False, message + + logging.success( + f"[green]Finalized![/green] Weights commited with reveal round [blue]{reveal_round}[/blue]." + ) + return True, f"reveal_round:{reveal_round}" + + except Exception as e: + logging.error(f":cross_mark: [red]Failed. Error:[/red] {e}") + return False, str(e) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index f9705415dd..c2e85352e2 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1836,7 +1836,13 @@ def set_weights( """ retries = 0 success = False - uid = self.get_uid_for_hotkey_on_subnet(wallet.hotkey.ss58_address, netuid) + if ( + uid := self.get_uid_for_hotkey_on_subnet(wallet.hotkey.ss58_address, netuid) + ) is None: + return ( + False, + f"Hotkey {wallet.hotkey.ss58_address} not registered in subnet {netuid}", + ) if self.commit_reveal_enabled(netuid=netuid) is True: # go with `commit reveal v3` extrinsic diff --git a/tests/unit_tests/extrinsics/test_async_commit_reveal.py b/tests/unit_tests/extrinsics/test_async_commit_reveal.py new file mode 100644 index 0000000000..ce4a5ccbfa --- /dev/null +++ b/tests/unit_tests/extrinsics/test_async_commit_reveal.py @@ -0,0 +1,373 @@ +from bittensor.core import async_subtensor as subtensor_module +from bittensor.core.chain_data import SubnetHyperparameters +from bittensor.core.async_subtensor import AsyncSubtensor +from bittensor.core.extrinsics import async_commit_reveal +import pytest +import torch +import numpy as np + + +@pytest.fixture +def subtensor(mocker): + fake_substrate = mocker.AsyncMock() + fake_substrate.websocket.sock.getsockopt.return_value = 0 + mocker.patch.object( + subtensor_module, "AsyncSubstrateInterface", return_value=fake_substrate + ) + yield AsyncSubtensor() + + +@pytest.fixture +def hyperparams(): + yield SubnetHyperparameters( + rho=0, + kappa=0, + immunity_period=0, + min_allowed_weights=0, + max_weight_limit=0.0, + tempo=0, + min_difficulty=0, + max_difficulty=0, + weights_version=0, + weights_rate_limit=0, + adjustment_interval=0, + activity_cutoff=0, + registration_allowed=False, + target_regs_per_interval=0, + min_burn=0, + max_burn=0, + bonds_moving_avg=0, + max_regs_per_block=0, + serving_rate_limit=0, + max_validators=0, + adjustment_alpha=0, + difficulty=0, + commit_reveal_weights_interval=0, + commit_reveal_weights_enabled=True, + alpha_high=0, + alpha_low=0, + liquid_alpha_enabled=False, + ) + + +@pytest.mark.asyncio +async def test_do_commit_reveal_v3_success(mocker, subtensor): + """Test successful commit-reveal with wait for finalization.""" + # Preps + fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) + fake_netuid = 1 + fake_commit = b"fake_commit" + fake_reveal_round = 1 + + mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_create_signed_extrinsic = mocker.patch.object( + subtensor.substrate, "create_signed_extrinsic" + ) + mocked_submit_extrinsic = mocker.patch.object( + subtensor.substrate, "submit_extrinsic" + ) + + # Call + result = await async_commit_reveal._do_commit_reveal_v3( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + commit=fake_commit, + reveal_round=fake_reveal_round, + ) + + # Asserts + mocked_compose_call.assert_awaited_once_with( + call_module="SubtensorModule", + call_function="commit_crv3_weights", + call_params={ + "netuid": fake_netuid, + "commit": fake_commit, + "reveal_round": fake_reveal_round, + }, + ) + mocked_create_signed_extrinsic.assert_awaited_once_with( + call=mocked_compose_call.return_value, keypair=fake_wallet.hotkey + ) + mocked_submit_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + extrinsic=mocked_create_signed_extrinsic.return_value, + wait_for_inclusion=False, + wait_for_finalization=False, + ) + assert result == (True, "Not waiting for finalization or inclusion.") + + +@pytest.mark.asyncio +async def test_do_commit_reveal_v3_failure_due_to_error(mocker, subtensor): + """Test commit-reveal fails due to an error in submission.""" + # Preps + fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) + fake_netuid = 1 + fake_commit = b"fake_commit" + fake_reveal_round = 1 + + mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_create_signed_extrinsic = mocker.patch.object( + subtensor.substrate, "create_signed_extrinsic" + ) + mocked_submit_extrinsic = mocker.patch.object( + subtensor.substrate, + "submit_extrinsic", + return_value=mocker.Mock( + is_success=mocker.AsyncMock(return_value=False)(), + error_message=mocker.AsyncMock(return_value="Mocked error")(), + ), + ) + + mocked_format_error_message = mocker.patch.object( + async_commit_reveal, "format_error_message", return_value="Formatted error" + ) + + # Call + result = await async_commit_reveal._do_commit_reveal_v3( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + commit=fake_commit, + reveal_round=fake_reveal_round, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + # Asserts + mocked_compose_call.assert_awaited_once_with( + call_module="SubtensorModule", + call_function="commit_crv3_weights", + call_params={ + "netuid": fake_netuid, + "commit": fake_commit, + "reveal_round": fake_reveal_round, + }, + ) + mocked_create_signed_extrinsic.assert_awaited_once_with( + call=mocked_compose_call.return_value, keypair=fake_wallet.hotkey + ) + mocked_submit_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + extrinsic=mocked_create_signed_extrinsic.return_value, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + mocked_format_error_message.assert_called_once_with("Mocked error") + assert result == (False, "Formatted error") + + +@pytest.mark.asyncio +async def test_commit_reveal_v3_extrinsic_success_with_torch( + mocker, subtensor, hyperparams +): + """Test successful commit-reveal with torch tensors.""" + # Preps + fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) + fake_netuid = 1 + fake_uids = torch.tensor([1, 2, 3], dtype=torch.int64) + fake_weights = torch.tensor([0.1, 0.2, 0.7], dtype=torch.float32) + fake_commit_for_reveal = b"mock_commit_for_reveal" + fake_reveal_round = 1 + + # Mocks + + mocked_uids = mocker.Mock() + mocked_weights = mocker.Mock() + mocked_convert_weights_and_uids_for_emit = mocker.patch.object( + async_commit_reveal, + "convert_weights_and_uids_for_emit", + return_value=(mocked_uids, mocked_weights), + ) + mocked_get_subnet_reveal_period_epochs = mocker.patch.object( + subtensor, "get_subnet_reveal_period_epochs" + ) + mocked_get_encrypted_commit = mocker.patch.object( + async_commit_reveal, + "get_encrypted_commit", + return_value=(fake_commit_for_reveal, fake_reveal_round), + ) + mock_do_commit_reveal_v3 = mocker.patch.object( + async_commit_reveal, "_do_commit_reveal_v3", return_value=(True, "Success") + ) + mock_block = mocker.patch.object( + subtensor.substrate, "get_block_number", return_value=1 + ) + mock_hyperparams = mocker.patch.object( + subtensor, + "get_subnet_hyperparameters", + return_value=hyperparams, + ) + + # Call + success, message = await async_commit_reveal.commit_reveal_v3_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + # Asserts + assert success is True + assert message == "reveal_round:1" + mocked_convert_weights_and_uids_for_emit.assert_called_once_with( + fake_uids, fake_weights + ) + mocked_get_encrypted_commit.assert_called_once_with( + uids=mocked_uids, + weights=mocked_weights, + subnet_reveal_period_epochs=mock_hyperparams.return_value.commit_reveal_weights_interval, + version_key=async_commit_reveal.version_as_int, + tempo=mock_hyperparams.return_value.tempo, + netuid=fake_netuid, + current_block=mock_block.return_value, + ) + mock_do_commit_reveal_v3.assert_awaited_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + commit=fake_commit_for_reveal, + reveal_round=fake_reveal_round, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + +@pytest.mark.asyncio +async def test_commit_reveal_v3_extrinsic_success_with_numpy( + mocker, subtensor, hyperparams +): + """Test successful commit-reveal with numpy arrays.""" + # Preps + fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) + fake_netuid = 1 + fake_uids = np.array([1, 2, 3], dtype=np.int64) + fake_weights = np.array([0.1, 0.2, 0.7], dtype=np.float32) + + mock_convert = mocker.patch.object( + async_commit_reveal, + "convert_weights_and_uids_for_emit", + return_value=(fake_uids, fake_weights), + ) + mock_encode_drand = mocker.patch.object( + async_commit_reveal, "get_encrypted_commit", return_value=(b"commit", 0) + ) + mock_do_commit = mocker.patch.object( + async_commit_reveal, "_do_commit_reveal_v3", return_value=(True, "Committed!") + ) + mocker.patch.object(subtensor.substrate, "get_block_number", return_value=1) + mocker.patch.object( + subtensor, + "get_subnet_hyperparameters", + return_value=hyperparams, + ) + + # Call + success, message = await async_commit_reveal.commit_reveal_v3_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + wait_for_inclusion=False, + wait_for_finalization=False, + ) + + # Asserts + assert success is True + assert message == "reveal_round:0" + mock_convert.assert_called_once_with(fake_uids, fake_weights) + mock_encode_drand.assert_called_once() + mock_do_commit.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_commit_reveal_v3_extrinsic_response_false( + mocker, subtensor, hyperparams +): + """Test unsuccessful commit-reveal with torch.""" + # Preps + fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) + fake_netuid = 1 + fake_uids = torch.tensor([1, 2, 3], dtype=torch.int64) + fake_weights = torch.tensor([0.1, 0.2, 0.7], dtype=torch.float32) + fake_commit_for_reveal = b"mock_commit_for_reveal" + fake_reveal_round = 1 + + # Mocks + mocker.patch.object( + async_commit_reveal, + "convert_weights_and_uids_for_emit", + return_value=(fake_uids, fake_weights), + ) + mocker.patch.object( + async_commit_reveal, + "get_encrypted_commit", + return_value=(fake_commit_for_reveal, fake_reveal_round), + ) + mock_do_commit_reveal_v3 = mocker.patch.object( + async_commit_reveal, "_do_commit_reveal_v3", return_value=(False, "Failed") + ) + mocker.patch.object(subtensor.substrate, "get_block_number", return_value=1) + mocker.patch.object( + subtensor, + "get_subnet_hyperparameters", + return_value=hyperparams, + ) + + # Call + success, message = await async_commit_reveal.commit_reveal_v3_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + # Asserts + assert success is False + assert message == "Failed" + mock_do_commit_reveal_v3.assert_awaited_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + commit=fake_commit_for_reveal, + reveal_round=fake_reveal_round, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + +@pytest.mark.asyncio +async def test_commit_reveal_v3_extrinsic_exception(mocker, subtensor): + """Test exception handling in commit-reveal.""" + # Preps + fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) + fake_netuid = 1 + fake_uids = [1, 2, 3] + fake_weights = [0.1, 0.2, 0.7] + + mocker.patch.object( + async_commit_reveal, + "convert_weights_and_uids_for_emit", + side_effect=Exception("Test Error"), + ) + + # Call + success, message = await async_commit_reveal.commit_reveal_v3_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + ) + + # Asserts + assert success is False + assert "Test Error" in message diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index d5b89c8d99..bf672beb56 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -2614,7 +2614,9 @@ async def test_set_weights_success(subtensor, mocker): fake_weights = [0.3, 0.5, 0.2] max_retries = 1 - mocked_get_uid_for_hotkey_on_subnet = mocker.AsyncMock(return_value=fake_netuid) + mocked_get_uid_for_hotkey_on_subnet = mocker.patch.object( + subtensor, "get_uid_for_hotkey_on_subnet" + ) subtensor.get_uid_for_hotkey_on_subnet = mocked_get_uid_for_hotkey_on_subnet mocked_blocks_since_last_update = mocker.AsyncMock(return_value=2)