diff --git a/bittensor/commands/stake/revoke_children.py b/bittensor/commands/stake/revoke_children.py index 74bbe35f6d..030f6205f2 100644 --- a/bittensor/commands/stake/revoke_children.py +++ b/bittensor/commands/stake/revoke_children.py @@ -18,13 +18,12 @@ import argparse import re -from typing import Union, Tuple -import numpy as np -from numpy.typing import NDArray +from typing import Tuple, List from rich.prompt import Confirm, Prompt import bittensor from .. import defaults, GetChildrenCommand +from ...utils import is_valid_ss58_address console = bittensor.__console__ @@ -84,9 +83,13 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Parse from strings netuid = cli.config.netuid - children = np.array( - [str(x) for x in re.split(r"[ ,]+", cli.config.children)], dtype=str - ) + children = re.split(r"[ ,]+", cli.config.children.strip()) + + # Validate children SS58 addresses + for child in children: + if not is_valid_ss58_address(child): + console.print(f":cross_mark:[red] Invalid SS58 address: {child}[/red]") + return success, message = RevokeChildrenCommand.do_revoke_children_multiple( subtensor=subtensor, @@ -152,7 +155,7 @@ def do_revoke_children_multiple( subtensor: "bittensor.subtensor", wallet: "bittensor.wallet", hotkey: str, - children: Union[NDArray[str], list], + children: List[str], netuid: int, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, @@ -168,7 +171,7 @@ def do_revoke_children_multiple( Bittensor wallet object. hotkey (str): Parent hotkey. - children (np.ndarray): + children (List[str]): Children hotkeys. netuid (int): Unique identifier of for the subnet. diff --git a/bittensor/commands/stake/set_child.py b/bittensor/commands/stake/set_child.py index 111b44cf79..28b9a149ae 100644 --- a/bittensor/commands/stake/set_child.py +++ b/bittensor/commands/stake/set_child.py @@ -23,7 +23,7 @@ import bittensor from .. import defaults, GetChildrenCommand # type: ignore -from ...utils.formatting import float_to_u64 +from ...utils.formatting import float_to_u64, normalize_u64_values console = bittensor.__console__ @@ -221,6 +221,7 @@ def do_set_child_singular( try: # prepare values for emmit proportion = float_to_u64(proportion) + proportion = normalize_u64_values([proportion])[0] call_module = "SubtensorModule" call_function = "set_child_singular" diff --git a/bittensor/commands/stake/set_children.py b/bittensor/commands/stake/set_children.py index eaecfc12b5..0389a1c1c3 100644 --- a/bittensor/commands/stake/set_children.py +++ b/bittensor/commands/stake/set_children.py @@ -17,7 +17,7 @@ import argparse import re -from typing import Union +from typing import Union, List from rich.prompt import Confirm from numpy.typing import NDArray import numpy as np @@ -25,7 +25,11 @@ from typing import Tuple import bittensor from .. import defaults, GetChildrenCommand # type: ignore -from ...utils.formatting import float_to_u64 +from ...utils.formatting import ( + float_to_u64, + normalize_u64_values, + is_valid_ss58_address, +) console = bittensor.__console__ @@ -93,15 +97,16 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Parse from strings netuid = cli.config.netuid - proportions = np.array( - [float(x) for x in re.split(r"[ ,]+", cli.config.proportions)], - dtype=np.float32, - ) - children = np.array( - [str(x) for x in re.split(r"[ ,]+", cli.config.children)], dtype=str - ) + proportions = [float(x) for x in re.split(r"[ ,]+", cli.config.proportions)] + children = [str(x) for x in re.split(r"[ ,]+", cli.config.children)] - total_proposed = np.sum(proportions) + current_proportions + # Validate children SS58 addresses + for child in children: + if not is_valid_ss58_address(child): + console.print(f":cross_mark:[red] Invalid SS58 address: {child}[/red]") + return + + total_proposed = sum(proportions) + current_proportions if total_proposed > 1: raise ValueError( f":cross_mark:[red] The sum of all proportions cannot be greater than 1. Proposed sum of proportions is {total_proposed}[/red]" @@ -181,7 +186,7 @@ def do_set_children_multiple( subtensor: "bittensor.subtensor", wallet: "bittensor.wallet", hotkey: str, - children: Union[NDArray[str], list], + children: List[str], netuid: int, proportions: Union[NDArray[np.float32], list], wait_for_inclusion: bool = True, @@ -198,7 +203,7 @@ def do_set_children_multiple( Bittensor wallet object. hotkey (str): Parent hotkey. - children (np.ndarray): + children (List[str]): Children hotkeys. netuid (int): Unique identifier of for the subnet. @@ -241,11 +246,14 @@ def do_set_children_multiple( else proportions ) - # Convert each proportion value to u16 + # Convert each proportion value to u64 proportions_val = [ float_to_u64(proportion) for proportion in proportions_val ] + # Normalize the u64 values to ensure their sum equals u64::MAX + proportions_val = normalize_u64_values(proportions_val) + children_with_proportions = list(zip(children, proportions_val)) call_module = "SubtensorModule" diff --git a/bittensor/utils/formatting.py b/bittensor/utils/formatting.py index cfd6eb1112..86769559d8 100644 --- a/bittensor/utils/formatting.py +++ b/bittensor/utils/formatting.py @@ -1,4 +1,7 @@ import math +from typing import List + +import bittensor def get_human_readable(num, suffix="H"): @@ -42,23 +45,71 @@ def u16_to_float(value): return value / u16_max -def float_to_u64(value): +def float_to_u64(value: float) -> int: # Ensure the input is within the expected range if not (0 <= value < 1): raise ValueError("Input value must be between 0 and 1") - # Calculate the u64 representation - u64_max = 18446744073709551615 # 2^64 - 1 - return int(value * u64_max) + # Convert the float to a u64 value + return int(value * (2**64 - 1)) def u64_to_float(value): - # Ensure the input is within the expected range - if not (0 <= value < 18446744073709551615): + u64_max = 2**64 - 1 + # Allow for a small margin of error (e.g., 1) to account for potential rounding issues + if not (0 <= value <= u64_max + 1): raise ValueError( - "Input value must be between 0 and 18446744073709551615 (2^64 - 1)" + f"Input value ({value}) must be between 0 and {u64_max} (2^64 - 1)" ) - - # Calculate the float representation - u64_max = 18446744073709551615 - return value / u64_max + return min(value / u64_max, 1.0) # Ensure the result is never greater than 1.0 + + +def normalize_u64_values(values: List[int]) -> List[int]: + """ + Normalize a list of u64 values so that their sum equals u64::MAX (2^64 - 1). + """ + if not values: + raise ValueError("Input list cannot be empty") + + if any(v < 0 for v in values): + raise ValueError("Input values must be non-negative") + + total = sum(values) + if total == 0: + raise ValueError("Sum of input values cannot be zero") + + u64_max = 2**64 - 1 + normalized = [int((v / total) * u64_max) for v in values] + + # Adjust values to ensure sum is exactly u64::MAX + current_sum = sum(normalized) + diff = u64_max - current_sum + + for i in range(abs(diff)): + if diff > 0: + normalized[i % len(normalized)] += 1 + else: + normalized[i % len(normalized)] = max( + 0, normalized[i % len(normalized)] - 1 + ) + + # Final check and adjustment + final_sum = sum(normalized) + if final_sum > u64_max: + normalized[-1] -= final_sum - u64_max + + assert ( + sum(normalized) == u64_max + ), f"Sum of normalized values ({sum(normalized)}) is not equal to u64::MAX ({u64_max})" + + return normalized + + +def is_valid_ss58_address(address: str) -> bool: + """ + Validate that the hotkey address input str is a valid ss58 address. + """ + try: + return bittensor.utils.ss58.is_valid_ss58_address(address) + except: + return False diff --git a/tests/e2e_tests/subcommands/stake/test_set_revoke_child_hotkeys.py b/tests/e2e_tests/subcommands/stake/test_set_revoke_child_hotkeys.py index 4ac0488984..189349ce4c 100644 --- a/tests/e2e_tests/subcommands/stake/test_set_revoke_child_hotkeys.py +++ b/tests/e2e_tests/subcommands/stake/test_set_revoke_child_hotkeys.py @@ -96,7 +96,9 @@ def test_set_revoke_child(local_chain, capsys): ) subtensor = bittensor.subtensor(network="ws://localhost:9945") - assert len(subtensor.get_children_info(netuid=1)) == 1, "failed to set child hotkey" + assert ( + len(subtensor.get_children_info(netuid=1)[alice_keypair.ss58_address]) == 1 + ), "failed to set child hotkey" output = capsys.readouterr().out assert "✅ Finalized" in output @@ -223,7 +225,7 @@ def test_set_revoke_children(local_chain, capsys): subtensor = bittensor.subtensor(network="ws://localhost:9945") assert ( - len(subtensor.get_children_info(netuid=1)) == 2 + len(subtensor.get_children_info(netuid=1)[alice_keypair.ss58_address]) == 2 ), "failed to set children hotkeys" output = capsys.readouterr().out