diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a10bf9c0..fbeeba36 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: ETH_PRIVATE_KEY: ${{ secrets.ETH_PRIVATE_KEY }} ENDPOINT: ${{ secrets.ENDPOINT }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - MANAGER_TAG: "1.8.2-develop.57" + MANAGER_TAG: "1.9.0-develop.1" steps: - uses: actions/checkout@v2 with: diff --git a/setup.py b/setup.py index c6c5a5ef..53afd6a5 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ setup( name='skale.py', - version='5.6', + version='5.7', description='SKALE client tools', long_description_markdown_filename='README.md', author='SKALE Labs', diff --git a/skale/contracts/manager/manager.py b/skale/contracts/manager/manager.py index ada38f7f..3522844c 100644 --- a/skale/contracts/manager/manager.py +++ b/skale/contracts/manager/manager.py @@ -27,6 +27,9 @@ from skale.contracts.base_contract import BaseContract, transaction_method from skale.utils import helper from skale.transactions.result import TxRes +from skale.dataclasses.schain_options import ( + SchainOptions, get_default_schain_options +) logger = logging.getLogger(__name__) @@ -62,15 +65,34 @@ def create_default_schain(self, name): wait_for=True) @transaction_method - def create_schain(self, lifetime, type_of_nodes, deposit, name): + def create_schain( + self, + lifetime: int, + type_of_nodes: int, + deposit: str, + name: str, + schain_originator: str = None, + options: SchainOptions = None + ): logger.info( f'create_schain: type_of_nodes: {type_of_nodes}, name: {name}') skale_nonce = helper.generate_nonce() + + if schain_originator is None: + schain_originator = self.skale.wallet.address + if not options: + options = get_default_schain_options() + tx_data = encode_abi( - ['uint', 'uint8', 'uint16', 'string'], - [lifetime, type_of_nodes, skale_nonce, name] + ['(uint,uint8,uint16,string,address,(string,bytes)[])'], + [(lifetime, type_of_nodes, skale_nonce, name, schain_originator, options.to_tuples())] + ) + + return self.skale.token.contract.functions.send( + self.address, + deposit, + tx_data ) - return self.skale.token.contract.functions.send(self.address, deposit, tx_data) @transaction_method def get_bounty(self, node_id): diff --git a/skale/contracts/manager/nodes.py b/skale/contracts/manager/nodes.py index bafd32fc..e66f2258 100644 --- a/skale/contracts/manager/nodes.py +++ b/skale/contracts/manager/nodes.py @@ -163,3 +163,7 @@ def node_manager_role(self): def compliance_role(self): return self.contract.functions.COMPLIANCE_ROLE().call() + + @transaction_method + def init_exit(self, node_id: int) -> TxRes: + return self.contract.functions.initExit(node_id) diff --git a/skale/contracts/manager/schains.py b/skale/contracts/manager/schains.py index 7fc3b4ed..74e4cb58 100644 --- a/skale/contracts/manager/schains.py +++ b/skale/contracts/manager/schains.py @@ -19,18 +19,22 @@ """ Schains.sol functions """ import functools -from dataclasses import dataclass +from dataclasses import dataclass, asdict from Crypto.Hash import keccak from skale.contracts.base_contract import BaseContract, transaction_method from skale.transactions.result import TxRes from skale.utils.helper import format_fields +from skale.dataclasses.schain_options import ( + SchainOptions, get_default_schain_options, parse_schain_options +) FIELDS = [ 'name', 'mainnetOwner', 'indexInOwnerList', 'partOfNode', 'lifetime', 'startDate', 'startBlock', - 'deposit', 'index', 'generation', 'originator', 'chainId' + 'deposit', 'index', 'generation', 'originator', 'chainId', 'multitransactionMode', + 'thresholdEncryption' ] @@ -48,6 +52,7 @@ class SchainStructure: generation: int originator: str chain_id: int + options: SchainOptions class SChains(BaseContract): @@ -72,18 +77,17 @@ def get(self, id_, obj=False): hash_obj = keccak.new(data=res[0].encode("utf8"), digest_bits=256) hash_str = "0x" + hash_obj.hexdigest()[:13] res.append(hash_str) + options = self.get_options(id_) if obj: # TODO: temporary solution for backwards compatibility - return SchainStructure(*res) + return SchainStructure(*res, options=options) + else: + res += asdict(options).values() return res @format_fields(FIELDS) - def get_by_name(self, name): + def get_by_name(self, name, obj=False): id_ = self.name_to_id(name) - res = self.schains_internal.get_raw(id_) - hash_obj = keccak.new(data=res[0].encode("utf8"), digest_bits=256) - hash_str = "0x" + hash_obj.hexdigest()[:13] - res.append(hash_str) - return res + return self.get(id_, obj=obj) def get_schains_for_owner(self, account): schains = [] @@ -132,21 +136,30 @@ def get_schain_price(self, index_of_type, lifetime): @transaction_method def add_schain_by_foundation( - self, - lifetime: int, - type_of_nodes: int, - nonce: int, - name: str, - schain_owner=None, - schain_originator=None + self, + lifetime: int, + type_of_nodes: int, + nonce: int, + name: str, + options: SchainOptions = None, + schain_owner=None, + schain_originator=None ) -> TxRes: if schain_owner is None: schain_owner = self.skale.wallet.address if schain_originator is None: schain_originator = self.skale.wallet.address + if not options: + options = get_default_schain_options() return self.contract.functions.addSchainByFoundation( - lifetime, type_of_nodes, nonce, name, schain_owner, schain_originator + lifetime, + type_of_nodes, + nonce, + name, + schain_owner, + schain_originator, + options.to_tuples() ) @transaction_method @@ -155,3 +168,15 @@ def grant_role(self, role: bytes, owner: str) -> TxRes: def schain_creator_role(self): return self.contract.functions.SCHAIN_CREATOR_ROLE().call() + + def __raw_get_options(self, schain_id: str) -> list: + return self.contract.functions.getOptions(schain_id).call() + + def get_options(self, schain_id: str) -> SchainOptions: + return parse_schain_options( + raw_options=self.__raw_get_options(schain_id) + ) + + def get_options_by_name(self, name: str) -> SchainOptions: + id_ = self.name_to_id(name) + return self.get_options(id_) diff --git a/skale/dataclasses/schain_options.py b/skale/dataclasses/schain_options.py new file mode 100644 index 00000000..32d74357 --- /dev/null +++ b/skale/dataclasses/schain_options.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# +# This file is part of SKALE.py +# +# Copyright (C) 2021 SKALE Labs +# +# SKALE.py is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SKALE.py is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with SKALE.py. If not, see . + +from dataclasses import dataclass + + +@dataclass +class SchainOptions: + multitransaction_mode: bool + threshold_encryption: bool + + def to_tuples(self) -> list: + return [ + ('multitr', bool_to_bytes(self.multitransaction_mode)), + ('encrypt', bool_to_bytes(self.threshold_encryption)) + ] + + +def parse_schain_options(raw_options: list) -> SchainOptions: + """ + Parses raw sChain options from smart contracts (list of tuples). + Returns default values if nothing is set on contracts. + """ + if len(raw_options) == 0: + return get_default_schain_options() + return SchainOptions( + multitransaction_mode=bytes_to_bool(raw_options[0][1]), + threshold_encryption=bytes_to_bool(raw_options[1][1]) + ) + + +def get_default_schain_options() -> SchainOptions: + return SchainOptions( + multitransaction_mode=False, + threshold_encryption=False + ) + + +def bool_to_bytes(bool_value: bool) -> bytes: + return bool_value.to_bytes(1, byteorder='big') + + +def bytes_to_bool(bytes_value: bytes) -> bool: + return bool(int.from_bytes(bytes_value, 'big')) diff --git a/skale/utils/contracts_provision/main.py b/skale/utils/contracts_provision/main.py index 853187c4..cff2b519 100644 --- a/skale/utils/contracts_provision/main.py +++ b/skale/utils/contracts_provision/main.py @@ -132,6 +132,7 @@ def cleanup_nodes_schains(skale): if schain_name is not None: skale.manager.delete_schain_by_root(schain_name, wait_for=True) for node_id in skale.nodes.get_active_node_ids(): + skale.nodes.init_exit(node_id, wait_for=True) skale.manager.node_exit(node_id, wait_for=True) @@ -255,7 +256,13 @@ def create_nodes(skale, names=()): ) -def create_schain(skale, schain_name=DEFAULT_SCHAIN_NAME, schain_type=None, random_name=False): +def create_schain( + skale, + schain_name=DEFAULT_SCHAIN_NAME, + schain_type=None, + random_name=False, + schain_options=None +): print('Creating schain') # create 1 s-chain type_of_nodes, lifetime_seconds, name = generate_random_schain_data(skale) @@ -271,6 +278,7 @@ def create_schain(skale, schain_name=DEFAULT_SCHAIN_NAME, schain_type=None, rand schain_type, 0, schain_name, + options=schain_options, wait_for=True, value=TEST_SRW_FUND_VALUE ) diff --git a/tests/manager/manager_test.py b/tests/manager/manager_test.py index 4463ca85..93f0a3ee 100644 --- a/tests/manager/manager_test.py +++ b/tests/manager/manager_test.py @@ -122,10 +122,18 @@ def test_create_delete_schain(skale): schains_ids = skale.schains_internal.get_all_schains_ids() type_of_nodes, lifetime_seconds, name = generate_random_schain_data(skale) - price_in_wei = skale.schains.get_schain_price(type_of_nodes, - lifetime_seconds) - tx_res = skale.manager.create_schain(lifetime_seconds, type_of_nodes, - price_in_wei, name, wait_for=True) + price_in_wei = skale.schains.get_schain_price( + type_of_nodes, + lifetime_seconds + ) + tx_res = skale.manager.create_schain( + lifetime_seconds, + type_of_nodes, + price_in_wei, + name, + wait_for=True + ) + assert tx_res.receipt['status'] == 1 schains_ids_number_after = skale.schains_internal.get_schains_number() @@ -221,6 +229,7 @@ def test_empty_node_exit(skale): ip, public_ip, port, name = generate_random_node_data() skale.manager.create_node(ip, port, name, public_ip, wait_for=True) node_idx = skale.nodes.node_name_to_index(name) + skale.nodes.init_exit(node_idx, wait_for=True) tx_res = skale.manager.node_exit(node_idx, wait_for=True) assert tx_res.receipt['status'] == 1 assert skale.nodes.get_node_status(node_idx) == 2 diff --git a/tests/manager/schains_internal_test.py b/tests/manager/schains_internal_test.py index 2b380de8..6acc9369 100644 --- a/tests/manager/schains_internal_test.py +++ b/tests/manager/schains_internal_test.py @@ -8,7 +8,7 @@ def test_get_raw(skale): schain_arr = skale.schains_internal.get_raw(DEFAULT_SCHAIN_ID) - assert len(FIELDS) == len(schain_arr) + 1 # +1 for chainId + assert len(FIELDS) == len(schain_arr) + 3 # +1 for chainId + options def test_get_raw_not_exist(skale): diff --git a/tests/manager/schains_test.py b/tests/manager/schains_test.py index 09c6f5e8..8aed0314 100644 --- a/tests/manager/schains_test.py +++ b/tests/manager/schains_test.py @@ -2,6 +2,7 @@ from hexbytes import HexBytes +from skale.dataclasses.schain_options import SchainOptions from skale.contracts.manager.schains import FIELDS, SchainStructure from skale.utils.contracts_provision.fake_multisig_contract import FAKE_MULTISIG_DATA_PATH from skale.utils.contracts_provision.main import create_clean_schain @@ -23,6 +24,7 @@ def test_get(skale): def test_get_object(skale): schain = skale.schains.get(DEFAULT_SCHAIN_ID, obj=True) assert isinstance(schain, SchainStructure) + assert isinstance(schain.options, SchainOptions) def test_get_by_name(skale): @@ -105,13 +107,34 @@ def test_add_schain_by_foundation(skale): assert name not in schains_names +def test_add_schain_by_foundation_with_options(skale): + skale.schains.grant_role(skale.schains.schain_creator_role(), + skale.wallet.address) + type_of_nodes, lifetime_seconds, name = generate_random_schain_data(skale) + skale.schains.add_schain_by_foundation( + lifetime_seconds, + type_of_nodes, + 0, + name, + options=SchainOptions( + multitransaction_mode=True, + threshold_encryption=False + ), + wait_for=True + ) + schain = skale.schains.get_by_name(name, obj=True) + + assert schain.options.multitransaction_mode is True + assert schain.options.threshold_encryption is False + + def test_add_schain_by_foundation_custom_owner(skale): skale.schains.grant_role(skale.schains.schain_creator_role(), skale.wallet.address) type_of_nodes, lifetime_seconds, name = generate_random_schain_data(skale) custom_wallet = generate_wallet(skale.web3) skale.schains.add_schain_by_foundation( - lifetime_seconds, type_of_nodes, 0, name, custom_wallet.address, wait_for=True + lifetime_seconds, type_of_nodes, 0, name, schain_owner=custom_wallet.address, wait_for=True ) new_schain = skale.schains.get_by_name(name) @@ -162,9 +185,9 @@ def test_add_schain_by_foundation_custom_originator(skale): def test_get_active_schains_for_node(skale): - create_schain(skale, 'test1') - create_schain(skale, 'test2') - skale.manager.delete_schain('test1', wait_for=True) + name = create_schain(skale, random_name=True) + create_schain(skale, random_name=True) + skale.manager.delete_schain(name, wait_for=True) node_id = skale.nodes.node_name_to_index(DEFAULT_NODE_NAME) active_schains = skale.schains.get_active_schains_for_node(node_id) @@ -178,3 +201,29 @@ def test_name_to_group_id(skale): name = 'TEST' gid = skale.schains.name_to_group_id(name) assert gid == HexBytes('0x852daa74cc3c31fe64542bb9b8764cfb91cc30f9acf9389071ffb44a9eefde46') # noqa + + +def test_raw_get_options(skale): + schain_options = SchainOptions( + multitransaction_mode=True, + threshold_encryption=False + ) + name = create_schain(skale, random_name=True, schain_options=schain_options) + id_ = skale.schains.name_to_id(name) + raw_options = skale.schains._SChains__raw_get_options(id_) + assert raw_options == [('multitr', b'\x01'), ('encrypt', b'\x00')] + + +def test_get_options(skale): + schain_options = SchainOptions( + multitransaction_mode=False, + threshold_encryption=True + ) + name = create_schain(skale, random_name=True, schain_options=schain_options) + + id_ = skale.schains.name_to_id(name) + options = skale.schains.get_options(id_) + assert options == schain_options + + options = skale.schains.get_options_by_name(name) + assert options == schain_options diff --git a/tests/rotation_history/utils.py b/tests/rotation_history/utils.py index fd06ffc1..d1945e2b 100644 --- a/tests/rotation_history/utils.py +++ b/tests/rotation_history/utils.py @@ -135,6 +135,7 @@ def send_complaint(nodes, skale_instances, group_index, failed_node_index): def rotate_node(skale, group_index, nodes, skale_instances, exiting_node_index, do_dkg=True): new_nodes, new_skale_instances = set_up_nodes(skale, 1) + skale.nodes.init_exit(nodes[exiting_node_index]['node_id']) skale_instances[exiting_node_index].manager.node_exit(nodes[exiting_node_index]['node_id']) nodes[exiting_node_index] = new_nodes[0] skale_instances[exiting_node_index] = new_skale_instances[0]