From 52d0c755801ff548162c8ff323df3973cb485676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBy=C5=BAniewski?= Date: Fri, 14 Mar 2025 16:12:45 +0100 Subject: [PATCH 1/3] e2e tests for bonds and get_vote_data (with minor refactoring) --- bittensor/core/async_subtensor.py | 5 +- .../core/chain_data/proposal_vote_data.py | 31 ++--- bittensor/core/subtensor.py | 5 +- tests/e2e_tests/test_delegate.py | 106 ++++++++++++++++++ tests/e2e_tests/test_incentive.py | 17 +++ tests/e2e_tests/utils/chain_interactions.py | 42 +++++++ tests/unit_tests/test_async_subtensor.py | 14 ++- 7 files changed, 197 insertions(+), 23 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 546be4a9d0..3ac6c6aeff 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -1909,10 +1909,11 @@ async def get_vote_data( block_hash=block_hash, reuse_block_hash=reuse_block, ) + if vote_data is None: return None - else: - return ProposalVoteData(vote_data) + + return ProposalVoteData.from_dict(vote_data) async def get_uid_for_hotkey_on_subnet( self, diff --git a/bittensor/core/chain_data/proposal_vote_data.py b/bittensor/core/chain_data/proposal_vote_data.py index ea785a7a90..3cf5439955 100644 --- a/bittensor/core/chain_data/proposal_vote_data.py +++ b/bittensor/core/chain_data/proposal_vote_data.py @@ -1,22 +1,27 @@ +from dataclasses import dataclass + +from bittensor.core.chain_data.info_base import InfoBase from bittensor.core.chain_data.utils import decode_account_id -# Senate / Proposal data -class ProposalVoteData: +@dataclass +class ProposalVoteData(InfoBase): + """ + Senate / Proposal data + """ + index: int threshold: int ayes: list[str] nays: list[str] end: int - def __init__(self, proposal_dict: dict) -> None: - self.index = proposal_dict["index"] - self.threshold = proposal_dict["threshold"] - self.ayes = self.decode_ss58_tuples(proposal_dict["ayes"]) - self.nays = self.decode_ss58_tuples(proposal_dict["nays"]) - self.end = proposal_dict["end"] - - @staticmethod - def decode_ss58_tuples(line: tuple): - """Decodes a tuple of ss58 addresses formatted as bytes tuples.""" - return [decode_account_id(line[x][0]) for x in range(len(line))] + @classmethod + def from_dict(cls, proposal_dict: dict) -> "ProposalVoteData": + return cls( + ayes=[decode_account_id(key) for key in proposal_dict["ayes"]], + end=proposal_dict["end"], + index=proposal_dict["index"], + nays=[decode_account_id(key) for key in proposal_dict["nays"]], + threshold=proposal_dict["threshold"], + ) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 244080ff51..878c8ab501 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1468,10 +1468,11 @@ def get_vote_data( params=[proposal_hash], block_hash=self.determine_block_hash(block), ) + if vote_data is None: return None - else: - return ProposalVoteData(vote_data) + + return ProposalVoteData.from_dict(vote_data) def get_uid_for_hotkey_on_subnet( self, hotkey_ss58: str, netuid: int, block: Optional[int] = None diff --git a/tests/e2e_tests/test_delegate.py b/tests/e2e_tests/test_delegate.py index c5ea365611..59f7bbbb75 100644 --- a/tests/e2e_tests/test_delegate.py +++ b/tests/e2e_tests/test_delegate.py @@ -3,11 +3,15 @@ import bittensor from bittensor.core.chain_data.chain_identity import ChainIdentity from bittensor.core.chain_data.delegate_info import DelegatedInfo, DelegateInfo +from bittensor.core.chain_data.proposal_vote_data import ProposalVoteData from bittensor.utils.balance import Balance from tests.e2e_tests.utils.chain_interactions import ( + propose, set_identity, sudo_set_admin_utils, + vote, ) +from tests.helpers.helpers import CLOSE_IN_VALUE DEFAULT_DELEGATE_TAKE = 0.179995422293431 @@ -321,3 +325,105 @@ def test_nominator_min_required_stake(local_chain, subtensor, alice_wallet, bob_ ) assert stake == Balance(0) + + +def test_get_vote_data(subtensor, alice_wallet): + """ + Tests: + - Sends Propose + - Checks existing Proposals + - Votes + - Checks Proposal is updated + """ + + subtensor.root_register(alice_wallet) + + proposals = subtensor.query_map( + "Triumvirate", + "ProposalOf", + params=[], + ) + + assert proposals.records == [] + + success, error = propose( + subtensor, + alice_wallet, + proposal=subtensor.substrate.compose_call( + call_module="Triumvirate", + call_function="set_members", + call_params={ + "new_members": [], + "prime": None, + "old_count": 0, + }, + ), + duration=1_000_000, + ) + + assert error == "" + assert success is True + + proposals = subtensor.query_map( + "Triumvirate", + "ProposalOf", + params=[], + ) + proposals = { + bytes(proposal_hash[0]): proposal.value for proposal_hash, proposal in proposals + } + + assert list(proposals.values()) == [ + { + "Triumvirate": ( + { + "set_members": { + "new_members": (), + "prime": None, + "old_count": 0, + }, + }, + ), + }, + ] + + proposal_hash = list(proposals.keys())[0] + proposal_hash = f"0x{proposal_hash.hex()}" + + proposal = subtensor.get_vote_data( + proposal_hash, + ) + + assert proposal == ProposalVoteData( + ayes=[], + end=CLOSE_IN_VALUE(1_000_000, subtensor.block), + index=0, + nays=[], + threshold=3, + ) + + success, error = vote( + subtensor, + alice_wallet, + alice_wallet.hotkey.ss58_address, + proposal_hash, + index=0, + approve=True, + ) + + assert error == "" + assert success is True + + proposal = subtensor.get_vote_data( + proposal_hash, + ) + + assert proposal == ProposalVoteData( + ayes=[ + alice_wallet.hotkey.ss58_address, + ], + end=CLOSE_IN_VALUE(1_000_000, subtensor.block), + index=0, + nays=[], + threshold=3, + ) diff --git a/tests/e2e_tests/test_incentive.py b/tests/e2e_tests/test_incentive.py index 0467a0cd81..7f01085428 100644 --- a/tests/e2e_tests/test_incentive.py +++ b/tests/e2e_tests/test_incentive.py @@ -100,9 +100,26 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa assert alice_neuron.rank < 0.5 bob_neuron = metagraph.neurons[1] + assert bob_neuron.incentive > 0.5 assert bob_neuron.consensus > 0.5 assert bob_neuron.rank > 0.5 assert bob_neuron.trust == 1 + bonds = subtensor.bonds(netuid) + + assert bonds == [ + ( + 0, + [ + (0, 65535), + (1, 65535), + ], + ), + ( + 1, + [], + ), + ] + print("✅ Passed test_incentive") diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index 2f59dfd1f7..0407f21c71 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -261,3 +261,45 @@ def set_identity( wait_for_inclusion=True, wait_for_finalization=True, ) + + +def propose(subtensor, wallet, proposal, duration): + return subtensor.sign_and_send_extrinsic( + subtensor.substrate.compose_call( + call_module="Triumvirate", + call_function="propose", + call_params={ + "proposal": proposal, + "length_bound": len(proposal.data), + "duration": duration, + }, + ), + wallet, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + + +def vote( + subtensor, + wallet, + hotkey, + proposal, + index, + approve, +): + return subtensor.sign_and_send_extrinsic( + subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="vote", + call_params={ + "approve": approve, + "hotkey": hotkey, + "index": index, + "proposal": proposal, + }, + ), + wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 3ddbf14f99..aad0cace71 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -47,17 +47,17 @@ def test_decode_ss58_tuples_in_proposal_vote_data(mocker): } # Call - async_subtensor.ProposalVoteData(fake_proposal_dict) + async_subtensor.ProposalVoteData.from_dict(fake_proposal_dict) # Asserts assert mocked_decode_account_id.call_count == len(fake_proposal_dict["ayes"]) + len( fake_proposal_dict["nays"] ) assert mocked_decode_account_id.mock_calls == [ - mocker.call("0"), - mocker.call("1"), - mocker.call("2"), - mocker.call("3"), + mocker.call("0 line"), + mocker.call("1 line"), + mocker.call("2 line"), + mocker.call("3 line"), ] @@ -2054,7 +2054,9 @@ async def test_get_vote_data_success(subtensor, mocker): mocked_proposal_vote_data = mocker.Mock() mocker.patch.object( - async_subtensor, "ProposalVoteData", return_value=mocked_proposal_vote_data + async_subtensor.ProposalVoteData, + "from_dict", + return_value=mocked_proposal_vote_data, ) # Call From 5ba140123878e447f86cc820c26c3d9cc95f30d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBy=C5=BAniewski?= Date: Fri, 14 Mar 2025 16:22:07 +0100 Subject: [PATCH 2/3] Trigger CI From e4b9cc9f8261ffb9b6581f4dbb4b72b013671314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBy=C5=BAniewski?= Date: Mon, 17 Mar 2025 12:53:49 +0100 Subject: [PATCH 3/3] e2e tests for move_stake and transfer_stake --- tests/e2e_tests/test_staking.py | 148 ++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/tests/e2e_tests/test_staking.py b/tests/e2e_tests/test_staking.py index 11b7b45054..60177eb7b1 100644 --- a/tests/e2e_tests/test_staking.py +++ b/tests/e2e_tests/test_staking.py @@ -516,3 +516,151 @@ def test_safe_swap_stake_scenarios(subtensor, alice_wallet, bob_wallet): assert dest_stake > Balance( 0 ), "Destination stake should be non-zero after successful swap" + + +def test_move_stake(subtensor, alice_wallet, bob_wallet): + """ + Tests: + - Adding stake + - Moving stake from one hotkey-subnet pair to another + """ + + subtensor.burned_register( + alice_wallet, + netuid=1, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + assert subtensor.add_stake( + alice_wallet, + alice_wallet.hotkey.ss58_address, + netuid=1, + amount=Balance.from_tao(1_000), + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + stakes = subtensor.get_stake_for_coldkey(alice_wallet.coldkey.ss58_address) + + assert stakes == [ + StakeInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=1, + stake=ANY_BALANCE, + locked=Balance(0), + emission=ANY_BALANCE, + drain=0, + is_registered=True, + ), + ] + + subtensor.register_subnet(bob_wallet) + + assert subtensor.move_stake( + alice_wallet, + origin_hotkey=alice_wallet.hotkey.ss58_address, + origin_netuid=1, + destination_hotkey=bob_wallet.hotkey.ss58_address, + destination_netuid=2, + amount=stakes[0].stake, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + + stakes = subtensor.get_stake_for_coldkey(alice_wallet.coldkey.ss58_address) + + assert stakes == [ + StakeInfo( + hotkey_ss58=bob_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=2, + stake=ANY_BALANCE, + locked=Balance(0), + emission=ANY_BALANCE, + drain=0, + is_registered=True, + ), + ] + + +def test_transfer_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): + """ + Tests: + - Adding stake + - Transfering stake from one coldkey-subnet pair to another + """ + + subtensor.burned_register( + alice_wallet, + netuid=1, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + assert subtensor.add_stake( + alice_wallet, + alice_wallet.hotkey.ss58_address, + netuid=1, + amount=Balance.from_tao(1_000), + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + alice_stakes = subtensor.get_stake_for_coldkey(alice_wallet.coldkey.ss58_address) + + assert alice_stakes == [ + StakeInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=1, + stake=ANY_BALANCE, + locked=Balance(0), + emission=ANY_BALANCE, + drain=0, + is_registered=True, + ), + ] + + bob_stakes = subtensor.get_stake_for_coldkey(bob_wallet.coldkey.ss58_address) + + assert bob_stakes == [] + + subtensor.register_subnet(dave_wallet) + subtensor.burned_register( + bob_wallet, + netuid=2, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + assert subtensor.transfer_stake( + alice_wallet, + destination_coldkey_ss58=bob_wallet.coldkey.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + origin_netuid=1, + destination_netuid=2, + amount=alice_stakes[0].stake, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + alice_stakes = subtensor.get_stake_for_coldkey(alice_wallet.coldkey.ss58_address) + + assert alice_stakes == [] + + bob_stakes = subtensor.get_stake_for_coldkey(bob_wallet.coldkey.ss58_address) + + assert bob_stakes == [ + StakeInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + coldkey_ss58=bob_wallet.coldkey.ss58_address, + netuid=2, + stake=ANY_BALANCE, + locked=Balance(0), + emission=ANY_BALANCE, + drain=0, + is_registered=False, + ), + ]