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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand Down
54 changes: 53 additions & 1 deletion bittensor/core/async_subtensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion bittensor/core/chain_data/scheduled_coldkey_swap_info.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions bittensor/core/chain_data/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 2 additions & 3 deletions bittensor/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")


Expand Down Expand Up @@ -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

Expand Down
51 changes: 50 additions & 1 deletion bittensor/core/subtensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions bittensor/core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions bittensor/extras/subtensor_api/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions bittensor/extras/subtensor_api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion bittensor/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 0 additions & 41 deletions tests/e2e_tests/test_metagraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]")
Loading