Skip to content

Commit 6950464

Browse files
authored
Merge pull request #2906 from opentensor/feat/thewhaleking/add-parent-hotkeys
Add method to fetch parents for child hotkeys
2 parents 80717d3 + 31c8406 commit 6950464

File tree

7 files changed

+228
-1
lines changed

7 files changed

+228
-1
lines changed

bittensor/core/async_subtensor.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,47 @@ async def get_block_hash(self, block: Optional[int] = None) -> str:
973973
else:
974974
return await self.substrate.get_chain_head()
975975

976+
async def get_parents(
977+
self,
978+
hotkey: str,
979+
netuid: int,
980+
block: Optional[int] = None,
981+
block_hash: Optional[str] = None,
982+
reuse_block: bool = False,
983+
) -> list[tuple[float, str]]:
984+
"""
985+
This method retrieves the parent of a given hotkey and netuid. It queries the SubtensorModule's ParentKeys
986+
storage function to get the children and formats them before returning as a tuple.
987+
988+
Arguments:
989+
hotkey: The child hotkey SS58.
990+
netuid: The netuid value.
991+
block: The block number for which the children are to be retrieved.
992+
block_hash: The hash of the block to retrieve the subnet unique identifiers from.
993+
reuse_block: Whether to reuse the last-used block hash.
994+
995+
Returns:
996+
A list of formatted parents [(proportion, parent)]
997+
"""
998+
block_hash = await self.determine_block_hash(block, block_hash, reuse_block)
999+
parents = await self.substrate.query(
1000+
module="SubtensorModule",
1001+
storage_function="ParentKeys",
1002+
params=[hotkey, netuid],
1003+
block_hash=block_hash,
1004+
reuse_block_hash=reuse_block,
1005+
)
1006+
if parents:
1007+
formatted_parents = []
1008+
for proportion, parent in parents.value:
1009+
# Convert U64 to int
1010+
formatted_child = decode_account_id(parent[0])
1011+
normalized_proportion = u64_normalized_float(proportion)
1012+
formatted_parents.append((normalized_proportion, formatted_child))
1013+
return formatted_parents
1014+
1015+
return []
1016+
9761017
async def get_children(
9771018
self,
9781019
hotkey: str,

bittensor/core/subtensor.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,38 @@ def get_hyperparameter(
751751

752752
return getattr(result, "value", result)
753753

754+
def get_parents(
755+
self, hotkey: str, netuid: int, block: Optional[int] = None
756+
) -> list[tuple[float, str]]:
757+
"""
758+
This method retrieves the parent of a given hotkey and netuid. It queries the SubtensorModule's ParentKeys
759+
storage function to get the children and formats them before returning as a tuple.
760+
761+
Arguments:
762+
hotkey: The child hotkey SS58.
763+
netuid: The netuid.
764+
block: The block number for which the children are to be retrieved.
765+
766+
Returns:
767+
A list of formatted parents [(proportion, parent)]
768+
"""
769+
parents = self.substrate.query(
770+
module="SubtensorModule",
771+
storage_function="ParentKeys",
772+
params=[hotkey, netuid],
773+
block_hash=self.determine_block_hash(block),
774+
)
775+
if parents:
776+
formatted_parents = []
777+
for proportion, parent in parents.value:
778+
# Convert U64 to int
779+
formatted_child = decode_account_id(parent[0])
780+
normalized_proportion = u64_normalized_float(proportion)
781+
formatted_parents.append((normalized_proportion, formatted_child))
782+
return formatted_parents
783+
784+
return []
785+
754786
def get_children(
755787
self, hotkey: str, netuid: int, block: Optional[int] = None
756788
) -> tuple[bool, list[tuple[float, str]], str]:

bittensor/core/subtensor_api/subnets.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]):
1414
self.bonds = subtensor.bonds
1515
self.difficulty = subtensor.difficulty
1616
self.get_all_subnets_info = subtensor.get_all_subnets_info
17+
self.get_parents = subtensor.get_parents
1718
self.get_children = subtensor.get_children
1819
self.get_children_pending = subtensor.get_children_pending
1920
self.get_current_weight_commit_info = subtensor.get_current_weight_commit_info

bittensor/core/subtensor_api/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def add_legacy_methods(subtensor: "SubtensorApi"):
3636
subtensor.get_balance = subtensor._subtensor.get_balance
3737
subtensor.get_balances = subtensor._subtensor.get_balances
3838
subtensor.get_block_hash = subtensor._subtensor.get_block_hash
39+
subtensor.get_parents = subtensor._subtensor.get_parents
3940
subtensor.get_children = subtensor._subtensor.get_children
4041
subtensor.get_children_pending = subtensor._subtensor.get_children_pending
4142
subtensor.get_commitment = subtensor._subtensor.get_commitment

tests/e2e_tests/test_hotkeys.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def test_hotkeys(subtensor, alice_wallet, dave_wallet):
6969
)
7070
is True
7171
)
72-
logging.console.success(f"✅ Test [green]test_hotkeys[/green] passed")
72+
logging.console.success("✅ Test [green]test_hotkeys[/green] passed")
7373

7474

7575
@pytest.mark.asyncio
@@ -276,6 +276,10 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_w
276276
assert success is True
277277
assert children == [(1.0, bob_wallet.hotkey.ss58_address)]
278278

279+
parent_ = subtensor.get_parents(bob_wallet.hotkey.ss58_address, dave_subnet_netuid)
280+
281+
assert parent_ == [(1.0, alice_wallet.hotkey.ss58_address)]
282+
279283
# pending queue is empty
280284
pending, cooldown = subtensor.get_children_pending(
281285
alice_wallet.hotkey.ss58_address,

tests/unit_tests/test_async_subtensor.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1981,6 +1981,90 @@ async def test_get_children_substrate_request_exception(subtensor, mocker):
19811981
assert result == (False, [], "Formatted error message")
19821982

19831983

1984+
@pytest.mark.asyncio
1985+
async def test_get_parents_success(subtensor, mocker):
1986+
"""Tests get_parents when parents are successfully retrieved and formatted."""
1987+
# Preps
1988+
fake_hotkey = "valid_hotkey"
1989+
fake_netuid = 1
1990+
fake_parents = mocker.Mock(
1991+
value=[
1992+
(1000, ["parent_key_1"]),
1993+
(2000, ["parent_key_2"]),
1994+
]
1995+
)
1996+
1997+
mocked_query = mocker.AsyncMock(return_value=fake_parents)
1998+
subtensor.substrate.query = mocked_query
1999+
2000+
mocked_decode_account_id = mocker.Mock(
2001+
side_effect=["decoded_parent_key_1", "decoded_parent_key_2"]
2002+
)
2003+
mocker.patch.object(async_subtensor, "decode_account_id", mocked_decode_account_id)
2004+
2005+
expected_formatted_parents = [
2006+
(u64_normalized_float(1000), "decoded_parent_key_1"),
2007+
(u64_normalized_float(2000), "decoded_parent_key_2"),
2008+
]
2009+
2010+
# Call
2011+
result = await subtensor.get_parents(hotkey=fake_hotkey, netuid=fake_netuid)
2012+
2013+
# Asserts
2014+
mocked_query.assert_called_once_with(
2015+
block_hash=None,
2016+
module="SubtensorModule",
2017+
storage_function="ParentKeys",
2018+
params=[fake_hotkey, fake_netuid],
2019+
reuse_block_hash=False,
2020+
)
2021+
mocked_decode_account_id.assert_has_calls(
2022+
[mocker.call("parent_key_1"), mocker.call("parent_key_2")]
2023+
)
2024+
assert result == expected_formatted_parents
2025+
2026+
2027+
@pytest.mark.asyncio
2028+
async def test_get_parents_no_parents(subtensor, mocker):
2029+
"""Tests get_parents when there are no parents to retrieve."""
2030+
# Preps
2031+
fake_hotkey = "valid_hotkey"
2032+
fake_netuid = 1
2033+
fake_parents = []
2034+
2035+
mocked_query = mocker.AsyncMock(return_value=fake_parents)
2036+
subtensor.substrate.query = mocked_query
2037+
2038+
# Call
2039+
result = await subtensor.get_parents(hotkey=fake_hotkey, netuid=fake_netuid)
2040+
2041+
# Asserts
2042+
mocked_query.assert_called_once_with(
2043+
block_hash=None,
2044+
module="SubtensorModule",
2045+
storage_function="ParentKeys",
2046+
params=[fake_hotkey, fake_netuid],
2047+
reuse_block_hash=False,
2048+
)
2049+
assert result == []
2050+
2051+
2052+
@pytest.mark.asyncio
2053+
async def test_get_parents_substrate_request_exception(subtensor, mocker):
2054+
"""Tests get_parents when SubstrateRequestException is raised."""
2055+
# Preps
2056+
fake_hotkey = "valid_hotkey"
2057+
fake_netuid = 1
2058+
fake_exception = async_subtensor.SubstrateRequestException("Test Exception")
2059+
2060+
mocked_query = mocker.AsyncMock(side_effect=fake_exception)
2061+
subtensor.substrate.query = mocked_query
2062+
2063+
# Call
2064+
with pytest.raises(async_subtensor.SubstrateRequestException):
2065+
await subtensor.get_parents(hotkey=fake_hotkey, netuid=fake_netuid)
2066+
2067+
19842068
@pytest.mark.asyncio
19852069
async def test_get_children_pending(mock_substrate, subtensor):
19862070
mock_substrate.query.return_value.value = [

tests/unit_tests/test_subtensor.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3746,3 +3746,67 @@ def test_get_next_epoch_start_block(mocker, subtensor, call_return, expected):
37463746
)
37473747
subtensor.tempo.assert_called_once_with(netuid=netuid, block=block)
37483748
assert result == expected
3749+
3750+
3751+
def test_get_parents_success(subtensor, mocker):
3752+
"""Tests get_parents when parents are successfully retrieved and formatted."""
3753+
# Preps
3754+
fake_hotkey = "valid_hotkey"
3755+
fake_netuid = 1
3756+
fake_parents = mocker.Mock(
3757+
value=[
3758+
(1000, ["parent_key_1"]),
3759+
(2000, ["parent_key_2"]),
3760+
]
3761+
)
3762+
3763+
mocked_query = mocker.MagicMock(return_value=fake_parents)
3764+
subtensor.substrate.query = mocked_query
3765+
3766+
mocked_decode_account_id = mocker.Mock(
3767+
side_effect=["decoded_parent_key_1", "decoded_parent_key_2"]
3768+
)
3769+
mocker.patch.object(subtensor_module, "decode_account_id", mocked_decode_account_id)
3770+
3771+
expected_formatted_parents = [
3772+
(u64_normalized_float(1000), "decoded_parent_key_1"),
3773+
(u64_normalized_float(2000), "decoded_parent_key_2"),
3774+
]
3775+
3776+
# Call
3777+
result = subtensor.get_parents(hotkey=fake_hotkey, netuid=fake_netuid)
3778+
3779+
# Asserts
3780+
mocked_query.assert_called_once_with(
3781+
block_hash=None,
3782+
module="SubtensorModule",
3783+
storage_function="ParentKeys",
3784+
params=[fake_hotkey, fake_netuid],
3785+
)
3786+
mocked_decode_account_id.assert_has_calls(
3787+
[mocker.call("parent_key_1"), mocker.call("parent_key_2")]
3788+
)
3789+
assert result == expected_formatted_parents
3790+
3791+
3792+
def test_get_parents_no_parents(subtensor, mocker):
3793+
"""Tests get_parents when there are no parents to retrieve."""
3794+
# Preps
3795+
fake_hotkey = "valid_hotkey"
3796+
fake_netuid = 1
3797+
fake_parents = []
3798+
3799+
mocked_query = mocker.MagicMock(return_value=fake_parents)
3800+
subtensor.substrate.query = mocked_query
3801+
3802+
# Call
3803+
result = subtensor.get_parents(hotkey=fake_hotkey, netuid=fake_netuid)
3804+
3805+
# Asserts
3806+
mocked_query.assert_called_once_with(
3807+
block_hash=None,
3808+
module="SubtensorModule",
3809+
storage_function="ParentKeys",
3810+
params=[fake_hotkey, fake_netuid],
3811+
)
3812+
assert result == []

0 commit comments

Comments
 (0)