From 8e2ea85c4b6cb5ba19dd444607687849b7518fa1 Mon Sep 17 00:00:00 2001 From: Joakim Nyman Date: Thu, 7 Dec 2023 16:18:42 +0100 Subject: [PATCH 1/6] Action to find out if node is validator or collating. --- actions.yaml | 5 +++++ charmcraft.yaml | 5 ++++- src/charm.py | 10 ++++++++++ src/polkadot_rpc_wrapper.py | 20 +++++++++++++++++++- 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/actions.yaml b/actions.yaml index 9ae3f1b..e3f4136 100644 --- a/actions.yaml +++ b/actions.yaml @@ -43,6 +43,11 @@ set-node-key: type: string required: [ key ] +find-validator: + description: | + Checks if node has a session key present on-chain, using a RPC call. If it does, it means it is currently producing blocks. + Returns the validator/collator address if a session key is found. Else False. + get-node-info: description: | Gets system information about the node and its container. diff --git a/charmcraft.yaml b/charmcraft.yaml index ef8182e..e3383f9 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -18,6 +18,9 @@ bases: # This below is needed for charmhelpers 0.20.24 to build parts: charm: - charm-python-packages: [setuptools < 58] + charm-python-packages: + - setuptools < 58 + charm-binary-python-packages: + - substrate-interface prime: - files/* diff --git a/src/charm.py b/src/charm.py index 272d53e..451676a 100755 --- a/src/charm.py +++ b/src/charm.py @@ -62,6 +62,7 @@ def __init__(self, *args): self.framework.observe(self.on.start_node_service_action, self._on_start_node_service_action) self.framework.observe(self.on.stop_node_service_action, self._on_stop_node_service_action) self.framework.observe(self.on.set_node_key_action, self._on_set_node_key_action) + self.framework.observe(self.on.find_validator_action, self._on_find_validator_action) self.framework.observe(self.on.get_node_info_action, self._on_get_node_info_action) self.framework.observe(self.on.get_node_help_action, self._on_get_node_help_action) @@ -199,6 +200,15 @@ def _on_set_node_key_action(self, event: ops.ActionEvent) -> None: utils.write_node_key_file(key) utils.start_service() + def _on_find_validator_action(self, event: ActionEvent) -> None: + event.log("Checking sessions key through rpc...") + rpc_port = ServiceArgs(self._stored.service_args).rpc_port + validator = PolkadotRpcWrapper(rpc_port).find_validator() + if validator: + event.set_results(results={'validator': validator}) + else: + event.fail("This node has no session key on-chain.") + # TODO: this action is getting quite large and specialized, perhaps move all actions to an `actions.py` file? def _on_get_node_info_action(self, event: ops.ActionEvent) -> None: # Disk usage diff --git a/src/polkadot_rpc_wrapper.py b/src/polkadot_rpc_wrapper.py index ba48dfa..0b03dc3 100644 --- a/src/polkadot_rpc_wrapper.py +++ b/src/polkadot_rpc_wrapper.py @@ -4,7 +4,7 @@ import json import re from typing import Tuple - +import substrateinterface class PolkadotRpcWrapper(): @@ -107,3 +107,21 @@ def insert_key(self, mnemonic, address): """ data = '{"id": 1,"jsonrpc":"2.0", "method": "author_insertKey", "params":["aura","' + mnemonic + '","' + address + '"]}' requests.post(url=self.__server_address, headers=self.__headers, data=data) + + def find_validator(self): + """ + Check if this node is currently producing block for a validator/collator. + It does so by checking if any session key currently on-chain is present on this node. + :return: the validator/collator address or False. + """ + substrate = substrateinterface.SubstrateInterface(url=self.__server_address) + result = substrate.query("Session", "QueuedKeys").value_serialized + for validator in result: + keys = validator[1] + session_key = '0x' + for k in keys.values(): + # Some chains uses multiple keys. Before checking if it exist on the node they need to be concatenated removing preceding '0x'. + session_key += k[2:] + if self.has_session_key(session_key): + return validator[0] + return False From 84dc03c88e43c579f268ed7e51b17bf3f971e280 Mon Sep 17 00:00:00 2001 From: Joakim Nyman Date: Wed, 27 Dec 2023 22:57:24 +0100 Subject: [PATCH 2/6] updated update-status with new info. added actions. --- actions.yaml | 18 +++++++++++-- config.yaml | 6 +++++ src/charm.py | 50 ++++++++++++++++++++++++++++++----- src/polkadot_rpc_wrapper.py | 52 +++++++++++++++++++++++++++---------- 4 files changed, 103 insertions(+), 23 deletions(-) diff --git a/actions.yaml b/actions.yaml index e3f4136..97f8c2f 100644 --- a/actions.yaml +++ b/actions.yaml @@ -43,11 +43,25 @@ set-node-key: type: string required: [ key ] -find-validator: +find-validator-address: description: | - Checks if node has a session key present on-chain, using a RPC call. If it does, it means it is currently producing blocks. + Checks if this node is currently validating for any address, using a RPC call. Returns the validator/collator address if a session key is found. Else False. +is-validating-this-era: + description: | + Checks if node has a session key present on-chain this era for the validator address set in config, using a RPC call. + If it does, it means it is currently producing blocks. + Returns the session key if found. Else False. + REQUIRES validator-address config parameter to be set. + +is-validating-next-era: + description: | + Checks if node has a session key present on-chain to validate next era, using a RPC call. + If it does, it means it will start producing blocks next era. + Returns the session key if found. Else False. + REQUIRES validator-address config parameter to be set. + get-node-info: description: | Gets system information about the node and its container. diff --git a/config.yaml b/config.yaml index 91783e7..3354c0e 100644 --- a/config.yaml +++ b/config.yaml @@ -39,3 +39,9 @@ options: Extra arguments that the service should run with. '--chain=... --rpc-port=...' are required to set for the charm to run. + validator-address: + type: string + default: "" + description: | + If this is set, update-status will check if this node is currently validating for this address. + HINT: if this config parameter is set, the actions is-validating-this-era and is-validating-next-era can be used. diff --git a/src/charm.py b/src/charm.py index 451676a..ac673dd 100755 --- a/src/charm.py +++ b/src/charm.py @@ -62,7 +62,9 @@ def __init__(self, *args): self.framework.observe(self.on.start_node_service_action, self._on_start_node_service_action) self.framework.observe(self.on.stop_node_service_action, self._on_stop_node_service_action) self.framework.observe(self.on.set_node_key_action, self._on_set_node_key_action) - self.framework.observe(self.on.find_validator_action, self._on_find_validator_action) + self.framework.observe(self.on.find_validator_address_action, self._on_find_validator_address_action) + self.framework.observe(self.on.is_validating_this_era_action, self._on_is_validating_this_era_action) + self.framework.observe(self.on.is_validating_next_era_action, self._on_is_validating_next_era_action) self.framework.observe(self.on.get_node_info_action, self._on_get_node_info_action) self.framework.observe(self.on.get_node_help_action, self._on_get_node_help_action) @@ -126,9 +128,17 @@ def update_status(self, connection_attempts: int = 4) -> None: for i in range(connection_attempts): time.sleep(5) try: - self.unit.status = ops.ActiveStatus("Syncing: {}, Validating: {}".format( - str(PolkadotRpcWrapper(rpc_port).is_syncing()), - str(PolkadotRpcWrapper(rpc_port).is_validating()))) + is_syncing = str(PolkadotRpcWrapper(rpc_port).is_syncing()) + status_message = f'Syncing: {is_syncing}' + address = self.config.get('validator-address') + if address: + if PolkadotRpcWrapper(rpc_port).is_validating_this_era(address): + status_message += ", Validating: Yes" + elif PolkadotRpcWrapper(rpc_port).is_validating_next_era(address): + status_message += ", Validating: Next Era" + else: + status_message += ", Validating: No" + self.unit.status = ops.ActiveStatus(status_message) self.unit.set_workload_version(PolkadotRpcWrapper(rpc_port).get_version()) break except RequestsConnectionError as e: @@ -200,14 +210,40 @@ def _on_set_node_key_action(self, event: ops.ActionEvent) -> None: utils.write_node_key_file(key) utils.start_service() - def _on_find_validator_action(self, event: ActionEvent) -> None: + def _on_find_validator_address_action(self, event: ops.ActionEvent) -> None: event.log("Checking sessions key through rpc...") rpc_port = ServiceArgs(self._stored.service_args).rpc_port - validator = PolkadotRpcWrapper(rpc_port).find_validator() + validator = PolkadotRpcWrapper(rpc_port).find_validator_address() if validator: event.set_results(results={'validator': validator}) else: - event.fail("This node has no session key on-chain.") + event.set_results(results={'message': 'This node is not currently validating for any address.'}) + + def _on_is_validating_this_era_action(self, event: ops.ActionEvent) -> None: + validator_address = self.config.get("validator-address") + if not validator_address: + event.fail("Set validator-address config parameter to use this action!") + return + event.log("Checking sessions key through rpc...") + rpc_port = ServiceArgs(self._stored.service_args).rpc_port + session_key = PolkadotRpcWrapper(rpc_port).is_validating_this_era(validator_address) + if session_key: + event.set_results(results={'session_key': session_key}) + else: + event.set_results(results={'message': f'This node is not currently validating for address {validator_address}.'}) + + def _on_is_validating_next_era_action(self, event: ops.ActionEvent) -> None: + validator_address = self.config.get("validator-address") + if not validator_address: + event.fail("Set validator-address config parameter to use this action!") + return + event.log("Checking sessions key through rpc...") + rpc_port = ServiceArgs(self._stored.service_args).rpc_port + session_key = PolkadotRpcWrapper(rpc_port).is_validating_next_era(validator_address) + if session_key: + event.set_results(results={'session_key': session_key}) + else: + event.set_results(results={'message': f'This node will not be validating next era for address {validator_address}.'}) # TODO: this action is getting quite large and specialized, perhaps move all actions to an `actions.py` file? def _on_get_node_info_action(self, event: ops.ActionEvent) -> None: diff --git a/src/polkadot_rpc_wrapper.py b/src/polkadot_rpc_wrapper.py index 0b03dc3..375ceef 100644 --- a/src/polkadot_rpc_wrapper.py +++ b/src/polkadot_rpc_wrapper.py @@ -34,19 +34,6 @@ def is_syncing(self) -> str: response_json = json.loads(response.text) return response_json['result']['isSyncing'] - def is_validating(self) -> bool: - """ - Checks if polkadot service is started as Authority (E.g. is_validating() -> True) - :return: boolean - """ - data = '{"id":1, "jsonrpc":"2.0", "method": "system_nodeRoles", "params": []}' - response = requests.post(url=self.__server_address, headers=self.__headers, data=data) - response_json = json.loads(response.text) - if response_json['result'][0] == 'Authority': - return True - else: - return False - def get_version(self) -> str: """ Checks which version polkadot service is running (E.g. get_version() -> '0.9.3') @@ -108,7 +95,7 @@ def insert_key(self, mnemonic, address): data = '{"id": 1,"jsonrpc":"2.0", "method": "author_insertKey", "params":["aura","' + mnemonic + '","' + address + '"]}' requests.post(url=self.__server_address, headers=self.__headers, data=data) - def find_validator(self): + def find_validator_address(self): """ Check if this node is currently producing block for a validator/collator. It does so by checking if any session key currently on-chain is present on this node. @@ -125,3 +112,40 @@ def find_validator(self): if self.has_session_key(session_key): return validator[0] return False + + def is_validating_next_era(self, address): + """ + Check if this node has the intetion to validate for validator/collator 'address' next era. + It checks on-chain which session key is set to be used for validating next era for 'address'. + And if that session key exist on this node. + :return: the session key if found on this node, else False. + """ + substrate = substrateinterface.SubstrateInterface(url=self.__server_address) + result = substrate.query("Session", "NextKeys", [address]).value_serialized + if result: + session_key = '0x' + for k in result.values(): + session_key += k[2:] + if self.has_session_key(session_key): + return session_key + return False + + def is_validating_this_era(self, address): + """ + Check if this node is currently producing block for a validator/collator 'address. + It checks on-chain which session key is set to be used for validating this era for 'address'. + And if that session key exist on this node. + :return: the session key if validating, else False. + """ + substrate = substrateinterface.SubstrateInterface(url=self.__server_address) + result = substrate.query("Session", "QueuedKeys").value_serialized + for validator in result: + if validator[0] == address: + keys = validator[1] + session_key = '0x' + for k in keys.values(): + # Some chains uses multiple keys. Before checking if it exist on the node they need to be concatenated removing preceding '0x'. + session_key += k[2:] + if self.has_session_key(session_key): + return session_key + return False From 4485b2f096d169941c610282d9cefb4d39851644 Mon Sep 17 00:00:00 2001 From: Joakim Nyman Date: Fri, 29 Dec 2023 21:28:55 +0100 Subject: [PATCH 3/6] fixed typo --- actions.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/actions.yaml b/actions.yaml index 97f8c2f..4776ceb 100644 --- a/actions.yaml +++ b/actions.yaml @@ -5,7 +5,7 @@ get-session-key: description: "Runs author_rotateKeys and returns a new session-key." has-session-key: - description: "Checks if node has a session key, using a RPC call." + description: "Checks if node has a session key, using an RPC call." params: key: description: "Key to check if exist on node. E.g. key='0xhjd39djk309'" @@ -45,19 +45,19 @@ set-node-key: find-validator-address: description: | - Checks if this node is currently validating for any address, using a RPC call. + Checks if this node is currently validating for any address, using an RPC call. Returns the validator/collator address if a session key is found. Else False. is-validating-this-era: description: | - Checks if node has a session key present on-chain this era for the validator address set in config, using a RPC call. + Checks if node has a session key present on-chain this era for the validator address set in config, using an RPC call. If it does, it means it is currently producing blocks. Returns the session key if found. Else False. REQUIRES validator-address config parameter to be set. is-validating-next-era: description: | - Checks if node has a session key present on-chain to validate next era, using a RPC call. + Checks if node has a session key present on-chain to validate next era, using an RPC call. If it does, it means it will start producing blocks next era. Returns the session key if found. Else False. REQUIRES validator-address config parameter to be set. From 2071033eb6bcc7f49fddd7c915c3e00486100f74 Mon Sep 17 00:00:00 2001 From: Joakim Nyman Date: Fri, 29 Dec 2023 22:12:02 +0100 Subject: [PATCH 4/6] Improved session-key actions and description. --- actions.yaml | 17 +++++++++-------- src/charm.py | 9 ++++++--- src/polkadot_rpc_wrapper.py | 2 +- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/actions.yaml b/actions.yaml index 4776ceb..bfad941 100644 --- a/actions.yaml +++ b/actions.yaml @@ -45,21 +45,22 @@ set-node-key: find-validator-address: description: | - Checks if this node is currently validating for any address, using an RPC call. - Returns the validator/collator address if a session key is found. Else False. + Checks if this node is currently validating for any validator address found on-chain. + It does so by checking if any session key on-chain is present on this node. + Returns the validator address and the session key if this node is validating. is-validating-this-era: description: | - Checks if node has a session key present on-chain this era for the validator address set in config, using an RPC call. - If it does, it means it is currently producing blocks. - Returns the session key if found. Else False. + Checks if this node is currently validating for the validator address set in config. + It does so by checking if the session key used for that validator is present on this node. + Returns a message whether this node is validating or not. If it is validating, the session key is returned as well. REQUIRES validator-address config parameter to be set. is-validating-next-era: description: | - Checks if node has a session key present on-chain to validate next era, using an RPC call. - If it does, it means it will start producing blocks next era. - Returns the session key if found. Else False. + Checks if this node will be validating next era for the validator address set in config. + It does so by checking if the session key that will be used for that validator is present on this node. + Returns a message whether this node will be validating next era or not. If it will, the session key is returned as well. REQUIRES validator-address config parameter to be set. get-node-info: diff --git a/src/charm.py b/src/charm.py index ac673dd..01fefe0 100755 --- a/src/charm.py +++ b/src/charm.py @@ -213,9 +213,10 @@ def _on_set_node_key_action(self, event: ops.ActionEvent) -> None: def _on_find_validator_address_action(self, event: ops.ActionEvent) -> None: event.log("Checking sessions key through rpc...") rpc_port = ServiceArgs(self._stored.service_args).rpc_port - validator = PolkadotRpcWrapper(rpc_port).find_validator_address() - if validator: - event.set_results(results={'validator': validator}) + result = PolkadotRpcWrapper(rpc_port).find_validator_address() + if result: + event.set_results(results={'validator': result["validator"]}) + event.set_results(results={'session-key': result["session_key"]}) else: event.set_results(results={'message': 'This node is not currently validating for any address.'}) @@ -228,6 +229,7 @@ def _on_is_validating_this_era_action(self, event: ops.ActionEvent) -> None: rpc_port = ServiceArgs(self._stored.service_args).rpc_port session_key = PolkadotRpcWrapper(rpc_port).is_validating_this_era(validator_address) if session_key: + event.set_results(results={'message': f'This node is currently validating for address {validator_address}.'}) event.set_results(results={'session_key': session_key}) else: event.set_results(results={'message': f'This node is not currently validating for address {validator_address}.'}) @@ -241,6 +243,7 @@ def _on_is_validating_next_era_action(self, event: ops.ActionEvent) -> None: rpc_port = ServiceArgs(self._stored.service_args).rpc_port session_key = PolkadotRpcWrapper(rpc_port).is_validating_next_era(validator_address) if session_key: + event.set_results(results={'message': f'This node will be validating next era for address {validator_address}.'}) event.set_results(results={'session_key': session_key}) else: event.set_results(results={'message': f'This node will not be validating next era for address {validator_address}.'}) diff --git a/src/polkadot_rpc_wrapper.py b/src/polkadot_rpc_wrapper.py index 375ceef..8077b0f 100644 --- a/src/polkadot_rpc_wrapper.py +++ b/src/polkadot_rpc_wrapper.py @@ -110,7 +110,7 @@ def find_validator_address(self): # Some chains uses multiple keys. Before checking if it exist on the node they need to be concatenated removing preceding '0x'. session_key += k[2:] if self.has_session_key(session_key): - return validator[0] + return {"validator": validator[0], "session_key": session_key} return False def is_validating_next_era(self, address): From b141a7043d309026c9502eb33f22322c3997d214 Mon Sep 17 00:00:00 2001 From: Joakim Nyman Date: Sat, 30 Dec 2023 20:22:23 +0100 Subject: [PATCH 5/6] Beauty-fix for the import of substrate-interface --- src/polkadot_rpc_wrapper.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/polkadot_rpc_wrapper.py b/src/polkadot_rpc_wrapper.py index 8077b0f..59a8324 100644 --- a/src/polkadot_rpc_wrapper.py +++ b/src/polkadot_rpc_wrapper.py @@ -4,7 +4,7 @@ import json import re from typing import Tuple -import substrateinterface +from substrateinterface import SubstrateInterface class PolkadotRpcWrapper(): @@ -101,7 +101,7 @@ def find_validator_address(self): It does so by checking if any session key currently on-chain is present on this node. :return: the validator/collator address or False. """ - substrate = substrateinterface.SubstrateInterface(url=self.__server_address) + substrate = SubstrateInterface(url=self.__server_address) result = substrate.query("Session", "QueuedKeys").value_serialized for validator in result: keys = validator[1] @@ -120,7 +120,7 @@ def is_validating_next_era(self, address): And if that session key exist on this node. :return: the session key if found on this node, else False. """ - substrate = substrateinterface.SubstrateInterface(url=self.__server_address) + substrate = SubstrateInterface(url=self.__server_address) result = substrate.query("Session", "NextKeys", [address]).value_serialized if result: session_key = '0x' @@ -137,7 +137,7 @@ def is_validating_this_era(self, address): And if that session key exist on this node. :return: the session key if validating, else False. """ - substrate = substrateinterface.SubstrateInterface(url=self.__server_address) + substrate = SubstrateInterface(url=self.__server_address) result = substrate.query("Session", "QueuedKeys").value_serialized for validator in result: if validator[0] == address: From 0c75f866c49b356a6843647eda4b4c017e1eed52 Mon Sep 17 00:00:00 2001 From: Joakim Nyman Date: Sat, 30 Dec 2023 20:23:20 +0100 Subject: [PATCH 6/6] Updated build and run specification. --- charmcraft.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/charmcraft.yaml b/charmcraft.yaml index e3383f9..63e5eea 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -2,14 +2,15 @@ type: charm bases: - build-on: - name: ubuntu - channel: "22.04" + channel: "20.04" run-on: - name: ubuntu channel: "20.04" architectures: [amd64] + - build-on: - name: ubuntu - channel: "21.10" - architectures: [amd64] + channel: "22.04" + run-on: - name: ubuntu channel: "22.04" architectures: [amd64]