diff --git a/MIGRATION.md b/MIGRATION.md index 15d12c02cc..eae73d0174 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -70,7 +70,8 @@ rename this variable in documentation. 12. ✅ The SDK is dropping support for `Python 3.9` starting with this release. 13. ✅ Remove `Default is` and `Default to` in docstrings bc parameters enough. 14. ✅ `camfairchild`: TODO, but we should have a grab_metadata if we don't already. Maybe don't decode, but can have a call that removes the Raw prefix, and another just doing grab_metadata_raw (no decoding). `get_commitment_metadata` added. -15. Find and process all `TODOs` across the entire code base. If in doubt, discuss each one with the team separately. SDK has 29 TODOs. +15. Solve the issue when a script using SDK receives the `--config` cli parameter. Disable `argparse` processing by default and enable it only when using SOME? a local environment variable. +16. Find and process all `TODOs` across the entire code base. If in doubt, discuss each one with the team separately. SDK has 29 TODOs. ## New features 1. ✅ Unify extrinsic return values by introducing an ExtrinsicResponse class. Extrinsics currently return either a boolean or a tuple. @@ -81,7 +82,7 @@ rename this variable in documentation. - Opportunity to expand the content of the extrinsic's response at any time upon community request or based on new technical requirements any time. 2. ✅ Add `bittensor.utils.hex_to_ss58` function. SDK still doesn't have it. (Probably inner import `from scalecodec import ss58_encode, ss58_decode`) 3. ✅ Implement Sub-subnets logic. Subtensor PR https://github.com/opentensor/subtensor/pull/1984 -4. Implement classes for `BlockInfo` objects that will contain the following fields: +4. ✅ Implement classes for `BlockInfo` objects that will contain the following fields: - number (int) - hash (str) - timestamp (datetime) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 956356c8c3..44a0699837 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -85,8 +85,13 @@ set_weights_extrinsic, ) from bittensor.core.metagraph import AsyncMetagraph -from bittensor.core.settings import version_as_int, TYPE_REGISTRY +from bittensor.core.settings import ( + version_as_int, + TYPE_REGISTRY, + TAO_APP_BLOCK_EXPLORER, +) from bittensor.core.types import ( + BlockInfo, ExtrinsicResponse, ParamWithTypes, Salt, @@ -1361,6 +1366,53 @@ async def get_block_hash(self, block: Optional[int] = None) -> str: else: return await self.substrate.get_chain_head() + async def get_block_info( + self, + block: Optional[int] = None, + block_hash: Optional[str] = None, + ) -> Optional[BlockInfo]: + """ + Retrieve complete information about a specific block from the Subtensor chain. + + This method aggregates multiple low-level RPC calls into a single structured response, returning both the raw + on-chain data and high-level decoded metadata for the given block. + + Args: + block: The block number for which the hash is to be retrieved. + block_hash: The hash of the block to retrieve the block from. + + Returns: + BlockInfo instance: + A dataclass containing all available information about the specified block, including: + - number: The block number. + - hash: The corresponding block hash. + - timestamp: The timestamp of the block (based on the `Timestamp.Now` extrinsic). + - header: The raw block header returned by the node RPC. + - extrinsics: The list of decoded extrinsics included in the block. + - explorer: The link to block explorer service. Always related with finney block data. + """ + block_info = await self.substrate.get_block( + block_number=block, block_hash=block_hash, ignore_decoding_errors=True + ) + if isinstance(block_info, dict) and (header := block_info.get("header")): + block = block or header.get("number", None) + block_hash = block_hash or header.get("hash", None) + extrinsics = cast(list, block_info.get("extrinsics")) + timestamp = None + for ext in extrinsics: + if ext.value_serialized["call"]["call_module"] == "Timestamp": + timestamp = ext.value_serialized["call"]["call_args"][0]["value"] + break + return BlockInfo( + number=block, + hash=block_hash, + timestamp=timestamp, + header=header, + extrinsics=extrinsics, + explorer=f"{TAO_APP_BLOCK_EXPLORER}{block}", + ) + return None + async def get_parents( self, hotkey_ss58: str, diff --git a/bittensor/core/chain_data/scheduled_coldkey_swap_info.py b/bittensor/core/chain_data/scheduled_coldkey_swap_info.py index 991665a77e..361a366c37 100644 --- a/bittensor/core/chain_data/scheduled_coldkey_swap_info.py +++ b/bittensor/core/chain_data/scheduled_coldkey_swap_info.py @@ -1,11 +1,11 @@ from dataclasses import dataclass from typing import Optional +from bittensor_wallet.utils import SS58_FORMAT from scalecodec.utils.ss58 import ss58_encode from bittensor.core.chain_data.info_base import InfoBase from bittensor.core.chain_data.utils import from_scale_encoding, ChainDataType -from bittensor.core.settings import SS58_FORMAT @dataclass diff --git a/bittensor/core/chain_data/utils.py b/bittensor/core/chain_data/utils.py index 621477ad21..5374652d8c 100644 --- a/bittensor/core/chain_data/utils.py +++ b/bittensor/core/chain_data/utils.py @@ -3,12 +3,12 @@ from enum import Enum from typing import Optional, Union, TYPE_CHECKING -from scalecodec.base import RuntimeConfiguration, ScaleBytes from async_substrate_interface.types import ScaleObj +from bittensor_wallet.utils import SS58_FORMAT +from scalecodec.base import RuntimeConfiguration, ScaleBytes from scalecodec.type_registry import load_type_registry_preset from scalecodec.utils.ss58 import ss58_encode -from bittensor.core.settings import SS58_FORMAT from bittensor.utils.balance import Balance if TYPE_CHECKING: diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index 0407e80001..29a744f33e 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -14,6 +14,8 @@ WALLETS_DIR = USER_BITTENSOR_DIR / "wallets" MINERS_DIR = USER_BITTENSOR_DIR / "miners" +TAO_APP_BLOCK_EXPLORER = "https://www.tao.app/block/" + __version__ = importlib.metadata.version("bittensor") @@ -64,9 +66,6 @@ # Substrate chain block time (seconds). BLOCKTIME = 12 -# Substrate ss58_format -SS58_FORMAT = 42 - # Wallet ss58 address length SS58_ADDRESS_LENGTH = 48 diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 8f26da5e62..cfb2da2cdb 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -84,12 +84,14 @@ set_weights_extrinsic, ) from bittensor.core.metagraph import Metagraph +from bittensor_wallet.utils import SS58_FORMAT from bittensor.core.settings import ( version_as_int, - SS58_FORMAT, + TAO_APP_BLOCK_EXPLORER, TYPE_REGISTRY, ) from bittensor.core.types import ( + BlockInfo, ExtrinsicResponse, ParamWithTypes, Salt, @@ -801,6 +803,53 @@ def get_block_hash(self, block: Optional[int] = None) -> str: else: return self.substrate.get_chain_head() + def get_block_info( + self, + block: Optional[int] = None, + block_hash: Optional[str] = None, + ) -> Optional[BlockInfo]: + """ + Retrieve complete information about a specific block from the Subtensor chain. + + This method aggregates multiple low-level RPC calls into a single structured response, returning both the raw + on-chain data and high-level decoded metadata for the given block. + + Args: + block: The block number for which the hash is to be retrieved. + block_hash: The hash of the block to retrieve the block from. + + Returns: + BlockInfo instance: + A dataclass containing all available information about the specified block, including: + - number: The block number. + - hash: The corresponding block hash. + - timestamp: The timestamp of the block (based on the `Timestamp.Now` extrinsic). + - header: The raw block header returned by the node RPC. + - extrinsics: The list of decoded extrinsics included in the block. + - explorer: The link to block explorer service. Always related with finney block data. + """ + block_info = self.substrate.get_block( + block_number=block, block_hash=block_hash, ignore_decoding_errors=True + ) + if isinstance(block_info, dict) and (header := block_info.get("header")): + block = block or header.get("number", None) + block_hash = block_hash or header.get("hash", None) + extrinsics = cast(list, block_info.get("extrinsics")) + timestamp = None + for ext in extrinsics: + if ext.value_serialized["call"]["call_module"] == "Timestamp": + timestamp = ext.value_serialized["call"]["call_args"][0]["value"] + break + return BlockInfo( + number=block, + hash=block_hash, + timestamp=timestamp, + header=header, + extrinsics=extrinsics, + explorer=f"{TAO_APP_BLOCK_EXPLORER}{block}", + ) + return None + def determine_block_hash(self, block: Optional[int]) -> Optional[str]: if block is None: return None diff --git a/bittensor/core/types.py b/bittensor/core/types.py index 74e5d54787..08280c6d2a 100644 --- a/bittensor/core/types.py +++ b/bittensor/core/types.py @@ -518,3 +518,28 @@ def with_log( if self.message: getattr(logging, level)(self.message) return self + + +@dataclass +class BlockInfo: + """ + Class that holds information about a blockchain block. + + This class encapsulates all relevant information about a block in the blockchain, including its number, hash, + timestamp, and contents. + + Attributes: + number: The block number. + hash: The corresponding block hash. + timestamp: The timestamp of the block (based on the `Timestamp.Now` extrinsic). + header: The raw block header returned by the node RPC. + extrinsics: The list of extrinsics included in the block. + explorer: The link to block explorer service. + """ + + number: int + hash: str + timestamp: Optional[int] + header: dict + extrinsics: list + explorer: str diff --git a/bittensor/extras/subtensor_api/chain.py b/bittensor/extras/subtensor_api/chain.py index 8a45169169..dfebb034a6 100644 --- a/bittensor/extras/subtensor_api/chain.py +++ b/bittensor/extras/subtensor_api/chain.py @@ -9,6 +9,7 @@ class Chain: def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.get_admin_freeze_window = subtensor.get_admin_freeze_window self.get_block_hash = subtensor.get_block_hash + self.get_block_info = subtensor.get_block_info self.get_current_block = subtensor.get_current_block self.get_delegate_identities = subtensor.get_delegate_identities self.get_existential_deposit = subtensor.get_existential_deposit diff --git a/bittensor/extras/subtensor_api/utils.py b/bittensor/extras/subtensor_api/utils.py index 28354430a4..8f4c500801 100644 --- a/bittensor/extras/subtensor_api/utils.py +++ b/bittensor/extras/subtensor_api/utils.py @@ -44,6 +44,7 @@ def add_legacy_methods(subtensor: "SubtensorApi"): subtensor.get_balance = subtensor.inner_subtensor.get_balance subtensor.get_balances = subtensor.inner_subtensor.get_balances subtensor.get_block_hash = subtensor.inner_subtensor.get_block_hash + subtensor.get_block_info = subtensor.inner_subtensor.get_block_info subtensor.get_children = subtensor.inner_subtensor.get_children subtensor.get_children_pending = subtensor.inner_subtensor.get_children_pending subtensor.get_commitment = subtensor.inner_subtensor.get_commitment diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 7d58d94de3..f821928842 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -20,7 +20,7 @@ ) from bittensor.core import settings -from bittensor.core.settings import SS58_FORMAT +from bittensor_wallet.utils import SS58_FORMAT from bittensor.utils.btlogging import logging from .registration import torch, use_torch from .version import check_version, VersionCheckError diff --git a/tests/e2e_tests/test_metagraph.py b/tests/e2e_tests/test_metagraph.py index 133eebd23f..4a82aa4b4c 100644 --- a/tests/e2e_tests/test_metagraph.py +++ b/tests/e2e_tests/test_metagraph.py @@ -1307,44 +1307,3 @@ async def test_metagraph_info_with_indexes_async( logging.console.info( "✅ Passed [blue]test_metagraph_info_with_indexes_async[/blue]" ) - - -def test_blocks(subtensor): - """ - Tests: - - Get current block - - Get block hash - - Wait for block - """ - get_current_block = subtensor.chain.get_current_block() - block = subtensor.block - - # Several random tests fell during the block finalization period. Fast blocks of 0.25 seconds (very fast) - assert get_current_block in [block, block + 1] - - block_hash = subtensor.chain.get_block_hash(block) - assert re.match("0x[a-z0-9]{64}", block_hash) - - subtensor.wait_for_block(block + 10) - assert subtensor.chain.get_current_block() == block + 10 - - logging.console.info("✅ Passed [blue]test_blocks[/blue]") - - -@pytest.mark.asyncio -async def test_blocks_async(subtensor): - """ - Async tests: - - Get current block - - Get block hash - - Wait for block - """ - block = subtensor.chain.get_current_block() - assert block == subtensor.block - - block_hash = subtensor.chain.get_block_hash(block) - assert re.match("0x[a-z0-9]{64}", block_hash) - - subtensor.wait_for_block(block + 10) - assert subtensor.chain.get_current_block() in [block + 10, block + 11] - logging.console.info("✅ Passed [blue]test_blocks_async[/blue]") diff --git a/tests/e2e_tests/test_subtensor_functions.py b/tests/e2e_tests/test_subtensor_functions.py index 85f77848d0..b236f9bd53 100644 --- a/tests/e2e_tests/test_subtensor_functions.py +++ b/tests/e2e_tests/test_subtensor_functions.py @@ -1,5 +1,5 @@ import asyncio - +import re import pytest from bittensor.core.extrinsics.asyncex.utils import ( @@ -372,3 +372,91 @@ async def test_subtensor_extrinsics_async( assert actual_owner == expected_owner, ( f"Expected owner {expected_owner}, but found {actual_owner}" ) + + +def test_blocks(subtensor): + """ + Tests: + - Get current block + - Get block hash + - Wait for block + """ + get_current_block = subtensor.chain.get_current_block() + block = subtensor.block + + # Several random tests fail during the block finalization period. Fast blocks of 0.25 seconds (very fast) + assert get_current_block in [block, block + 1] + + block_hash = subtensor.chain.get_block_hash(block) + assert re.match("0x[a-z0-9]{64}", block_hash) + + subtensor.wait_for_block(block + 10) + assert subtensor.chain.get_current_block() == block + 10 + + logging.console.info("✅ Passed [blue]test_blocks[/blue]") + + +@pytest.mark.asyncio +async def test_blocks_async(subtensor): + """ + Async tests: + - Get current block + - Get block hash + - Wait for block + """ + block = subtensor.chain.get_current_block() + assert block == subtensor.block + + block_hash = subtensor.chain.get_block_hash(block) + assert re.match("0x[a-z0-9]{64}", block_hash) + + subtensor.wait_for_block(block + 10) + assert subtensor.chain.get_current_block() in [block + 10, block + 11] + logging.console.info("✅ Passed [blue]test_blocks_async[/blue]") + + +@pytest.mark.parametrize( + "block, block_hash, result", + [ + (None, None, True), + (1, None, True), + (None, "SOME_HASH", True), + (1, "SOME_HASH", False) + ] +) +def test_block_info(subtensor, block, block_hash, result): + """Tests sync get_block_info.""" + if block_hash: + block_hash = subtensor.chain.get_block_hash() + + subtensor.wait_for_block(2) + + try: + res = subtensor.chain.get_block_info(block=block, block_hash=block_hash) + assert (res is not None) == result + except Exception as e: + assert "Either block_hash or block_number should be set" in str(e) + + +@pytest.mark.parametrize( + "block, block_hash, result", + [ + (None, None, True), + (1, None, True), + (None, "SOME_HASH", True), + (1, "SOME_HASH", False) + ] +) +@pytest.mark.asyncio +async def test_block_info(async_subtensor, block, block_hash, result): + """Tests async get_block_info.""" + if block_hash: + block_hash = await async_subtensor.chain.get_block_hash() + + await async_subtensor.wait_for_block(2) + + try: + res = await async_subtensor.chain.get_block_info(block=block, block_hash=block_hash) + assert (res is not None) == result + except Exception as e: + assert "Either block_hash or block_number should be set" in str(e) diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 7da45d6750..bfad7cb33a 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -1,12 +1,12 @@ import datetime import unittest.mock as mock -from bittensor.core.errors import BalanceTypeError + import pytest from async_substrate_interface.types import ScaleObj from bittensor_wallet import Wallet from bittensor import u64_normalized_float -from bittensor.core import async_subtensor +from bittensor.core import async_subtensor, settings from bittensor.core.async_subtensor import AsyncSubtensor from bittensor.core.chain_data import ( proposal_vote_data, @@ -15,6 +15,7 @@ StakeInfo, SelectiveMetagraphIndex, ) +from bittensor.core.errors import BalanceTypeError from bittensor.core.types import ExtrinsicResponse from bittensor.utils import U64_MAX, get_function_name from bittensor.utils.balance import Balance @@ -4138,3 +4139,51 @@ async def test_set_auto_stake(subtensor, mocker): ) assert result == mocked_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_get_block_info(subtensor, mocker): + """Tests that `get_block_info` calls proper methods and returns the correct value.""" + # Preps + fake_block = mocker.Mock(spec=int) + fake_hash = mocker.Mock(spec=str) + fake_timestamp = mocker.Mock(spec=int) + fake_decoded = mocker.Mock( + value_serialized={ + "call": { + "call_module": "Timestamp", + "call_args": [{"value": fake_timestamp}], + } + } + ) + fake_substrate_block = { + "header": { + "number": fake_block, + "hash": fake_hash, + }, + "extrinsics": [ + fake_decoded, + ] + + } + mocked_get_block = mocker.patch.object(subtensor.substrate, "get_block", return_value=fake_substrate_block) + mocked_BlockInfo = mocker.patch.object(async_subtensor, "BlockInfo") + + # Call + result = await subtensor.get_block_info() + + # Asserts + mocked_get_block.assert_awaited_once_with( + block_hash=None, + block_number=None, + ignore_decoding_errors=True, + ) + mocked_BlockInfo.assert_called_once_with( + number=fake_block, + hash=fake_hash, + timestamp=fake_timestamp, + header=fake_substrate_block.get("header"), + extrinsics=fake_substrate_block.get("extrinsics"), + explorer=f"{settings.TAO_APP_BLOCK_EXPLORER}{fake_block}" + ) + assert result == mocked_BlockInfo.return_value diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 57051f9b27..ab35d422bb 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -4310,3 +4310,50 @@ def test_set_auto_stake(subtensor, mocker): ) assert result == mocked_extrinsic.return_value + + +def test_get_block_info(subtensor, mocker): + """Tests that `get_block_info` calls proper methods and returns the correct value.""" + # Preps + fake_block = mocker.Mock(spec=int) + fake_hash = mocker.Mock(spec=str) + fake_timestamp = mocker.Mock(spec=int) + fake_decoded = mocker.Mock( + value_serialized={ + "call": { + "call_module": "Timestamp", + "call_args": [{"value": fake_timestamp}], + } + } + ) + fake_substrate_block = { + "header": { + "number": fake_block, + "hash": fake_hash, + }, + "extrinsics": [ + fake_decoded, + ] + + } + mocked_get_block = mocker.patch.object(subtensor.substrate, "get_block", return_value=fake_substrate_block) + mocked_BlockInfo = mocker.patch.object(subtensor_module, "BlockInfo") + + # Call + result = subtensor.get_block_info() + + # Asserts + mocked_get_block.assert_called_once_with( + block_hash=None, + block_number=None, + ignore_decoding_errors=True, + ) + mocked_BlockInfo.assert_called_once_with( + number=fake_block, + hash=fake_hash, + timestamp=fake_timestamp, + header=fake_substrate_block.get("header"), + extrinsics=fake_substrate_block.get("extrinsics"), + explorer=f"{settings.TAO_APP_BLOCK_EXPLORER}{fake_block}" + ) + assert result == mocked_BlockInfo.return_value diff --git a/tests/unit_tests/utils/test_utils.py b/tests/unit_tests/utils/test_utils.py index 9a6527d643..d7e25d6c93 100644 --- a/tests/unit_tests/utils/test_utils.py +++ b/tests/unit_tests/utils/test_utils.py @@ -1,7 +1,7 @@ import pytest from bittensor import warnings, __getattr__, version_split, logging, trace, debug, utils -from bittensor.core.settings import SS58_FORMAT +from bittensor_wallet.utils import SS58_FORMAT def test_getattr_version_split():