From 745235f179684991be1afb97fd2919496751751a Mon Sep 17 00:00:00 2001 From: C H Date: Sun, 31 Mar 2024 19:34:51 +0800 Subject: [PATCH] Added signer and verifier --- cybertensor/__init__.py | 2 +- cybertensor/axon.py | 10 +++- cybertensor/dendrite.py | 9 +++- cybertensor/keypair.py | 111 +++++++++++++++++++++++++++++++++------- cybertensor/synapse.py | 9 ++++ 5 files changed, 119 insertions(+), 22 deletions(-) diff --git a/cybertensor/__init__.py b/cybertensor/__init__.py index d10f1c7..eedc51d 100644 --- a/cybertensor/__init__.py +++ b/cybertensor/__init__.py @@ -30,7 +30,7 @@ nest_asyncio.apply() # Cybertensor code and protocol version. -__version__ = "0.1.6" +__version__ = "0.1.7" version_split = __version__.split(".") __version_as_int__ = ( (100 * int(version_split[0])) diff --git a/cybertensor/axon.py b/cybertensor/axon.py index 84da36b..d970fbe 100644 --- a/cybertensor/axon.py +++ b/cybertensor/axon.py @@ -847,7 +847,9 @@ async def default_verify(self, synapse: cybertensor.Synapse): """ # Build the keypair from the dendrite_hotkey if synapse.dendrite is not None: - keypair = Keypair(address=synapse.dendrite.hotkey) # type: ignore + cybertensor.logging.info(f"dendrite: {synapse.dendrite}") + # keypair = Keypair(address=synapse.dendrite.hotkey, public_key=synapse.dendrite.pubkey) + keypair = Keypair(address=synapse.dendrite.hotkey) # Build the signature messages. message = (f"{synapse.dendrite.nonce}.{synapse.dendrite.hotkey}.{self.wallet.hotkey.address}." f"{synapse.dendrite.uuid}.{synapse.computed_body_hash}") @@ -864,7 +866,11 @@ async def default_verify(self, synapse: cybertensor.Synapse): ): raise Exception("Nonce is too small") - if not keypair.verify(message, synapse.dendrite.signature): # type: ignore + verified = keypair.verify(message, synapse.dendrite.signature) + cybertensor.logging.info(f"\nAXON VERIFY MSG: {message}") + cybertensor.logging.info(f"AXON VERIFY SGN: {synapse.dendrite.signature}") + cybertensor.logging.info(f"AXON VERIFY : {verified}") + if not verified: raise Exception( f"Signature mismatch with {message} and {synapse.dendrite.signature}" ) diff --git a/cybertensor/dendrite.py b/cybertensor/dendrite.py index d919592..7132e06 100644 --- a/cybertensor/dendrite.py +++ b/cybertensor/dendrite.py @@ -620,6 +620,7 @@ def preprocess_synapse_for_request( nonce=time.monotonic_ns(), uuid=self.uuid, hotkey=self.keypair.address, + pubkey=self.keypair.public_key ) # Build the Axon headers using the target axon's details @@ -627,11 +628,17 @@ def preprocess_synapse_for_request( ip=target_axon_info.ip, port=target_axon_info.port, hotkey=target_axon_info.hotkey, + pubkey=None ) # Sign the request using the dendrite, axon info, and the synapse body hash message = f"{synapse.dendrite.nonce}.{synapse.dendrite.hotkey}.{synapse.axon.hotkey}.{synapse.dendrite.uuid}.{synapse.body_hash}" - synapse.dendrite.signature = f"0x{self.keypair.sign(message).hex()}" + signed = self.keypair.sign(message) + cybertensor.logging.info(f"\nDENDRITE ADDR {self.keypair.address}") + cybertensor.logging.info(f"DENDRITE MSG {message}") + cybertensor.logging.info(f"DENDRITE SGN {signed}") + cybertensor.logging.info(f"DENDRITE SGN 0x{signed.hex()}\n") + synapse.dendrite.signature = f"0x{signed.hex()}" return synapse diff --git a/cybertensor/keypair.py b/cybertensor/keypair.py index 5093700..de6101a 100644 --- a/cybertensor/keypair.py +++ b/cybertensor/keypair.py @@ -15,28 +15,40 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. +import base64 +import hashlib +import logging from typing import Optional, Union +from bech32 import ( # pylint: disable=wrong-import-order + bech32_encode, + convertbits, +) from bip39 import bip39_generate, bip39_validate from cosmpy.crypto.address import Address +from cosmpy.crypto.hashfuncs import ripemd160 from cosmpy.crypto.keypairs import PrivateKey, PublicKey from cosmpy.mnemonic import ( derive_child_key_from_mnemonic, COSMOS_HD_PATH, validate_mnemonic_and_normalise, ) - from cybertensor import __chain_address_prefix__ +from ecdsa import ( # type: ignore # pylint: disable=wrong-import-order + SECP256k1, + SigningKey, + VerifyingKey, +) from cybertensor.errors import ConfigurationError class Keypair: def __init__( - self, - address: str = None, - public_key: Union[bytes, str] = None, - private_key: Union[bytes, str] = None, - prefix: Optional[str] = None, + self, + address: str = None, + public_key: Union[bytes, str] = None, + private_key: Union[bytes, str] = None, + prefix: Optional[str] = None, ): """ Allows generation of Keypairs from a variety of input combination, such as a public/private key combination, @@ -62,8 +74,12 @@ def __init__( public_key = private_key_obj.public_key.public_key address = Address(PublicKey(private_key_obj.public_key), prefix).__str__() - if not public_key: - raise ConfigurationError("No public key provided") + if public_key and isinstance(public_key, str): + # self.public_key = bytes(public_key, 'utf-8') + self.public_key = public_key + + # if not public_key: + # raise ValueError("No public key provided") self.public_key: bytes = public_key @@ -105,7 +121,7 @@ def validate_mnemonic(cls, mnemonic: str) -> bool: @classmethod def create_from_mnemonic( - cls, mnemonic: str, prefix: Optional[str] = None + cls, mnemonic: str, prefix: Optional[str] = None ) -> "Keypair": """ Create a Keypair for given mnemonic @@ -135,7 +151,7 @@ def create_from_mnemonic( @classmethod def create_from_private_key( - cls, private_key: Union[bytes, str], prefix: Optional[str] = None + cls, private_key: Union[bytes, str], prefix: Optional[str] = None ) -> "Keypair": """ Creates Keypair for specified public/private keys @@ -158,6 +174,45 @@ def create_from_private_key( prefix=prefix, ) + def get_address_from_public_key(self, public_key: str) -> str: + """ + Get the address from the public key. + + :param public_key: the public key + :return: str + """ + public_key_bytes = bytes.fromhex(public_key) + s = hashlib.new("sha256", public_key_bytes).digest() + r = ripemd160(s) + five_bit_r = convertbits(r, 8, 5) + if five_bit_r is None: # pragma: nocover + raise TypeError("Unsuccessful bech32.convertbits call") + + ## TODO add configuration for chain prefix + address = bech32_encode(__chain_address_prefix__, five_bit_r) + return address + + def recover_message(self, message: bytes, signature: str) -> tuple[Address, ...]: + public_keys = self.recover_public_keys_from_message(message, signature) + addresses = [ + self.get_address_from_public_key(public_key) for public_key in public_keys + ] + return tuple(addresses) + + def recover_public_keys_from_message(self, message: bytes, signature: str) -> tuple[str, ...]: + signature_b64 = base64.b64decode(signature) + verifying_keys = VerifyingKey.from_public_key_recovery( + signature_b64, + message, + SECP256k1, + hashfunc=hashlib.sha256, + ) + public_keys = [ + verifying_key.to_string("compressed").hex() + for verifying_key in verifying_keys + ] + return tuple(public_keys) + def sign(self, data: Union[bytes, str]) -> bytes: """ Creates a signature for given data @@ -181,9 +236,14 @@ def sign(self, data: Union[bytes, str]) -> bytes: if not self.private_key: raise ConfigurationError("No private key set to create signatures") - # return signature - # TODO think about update to ADR-36 - return PrivateKey(self.private_key).sign(message=data, deterministic=True) + signature_compact = PrivateKey(self.private_key).sign(data, deterministic=True) + signature_base64_str = base64.b64encode(signature_compact).decode("utf-8").encode() + + logging.debug(f"\nKEYPAIR address: {self.address}") + logging.debug(f"KEYPAIR Signing data: {data}") + logging.debug(f"KEYPAIR Generated signature: {signature_base64_str}\n") + + return signature_base64_str def verify(self, data: Union[bytes, str], signature: Union[bytes, str]) -> bool: """ @@ -209,11 +269,26 @@ def verify(self, data: Union[bytes, str], signature: Union[bytes, str]) -> bool: if type(signature) is str and signature[0:2] == "0x": signature = bytes.fromhex(signature[2:]) - if type(signature) is not bytes: - raise TypeError(f"Signature should be of type bytes or a hex-string, given signature type is {type(signature)}") - - # TODO think about update to ADR-36 - return PublicKey(self.public_key).verify(message=data, signature=signature) + for address in self.recover_message(data, signature): + if self.address == address: + logging.debug(f"KEYPAIR Verified data: True") + return True + logging.debug(f"KEYPAIR Verified data: False") + return False + + # recovered_addresses = self.recover_message(data, signature) + # logging.debug(f"\nKEYPAIR Verifying data: {data}") + # logging.debug(f"KEYPAIR Recovered addresses: {recovered_addresses}") + # if self.address == recovered_addresses[0]: + # logging.debug(f"KEYPAIR Verified data: True 1") + # return True + # + # if self.address == recovered_addresses[1]: + # logging.debug(f"KEYPAIR Verified data: True 2\n") + # return True + # + # logging.debug(f"KEYPAIR Verified data: False\n") + # return False def __repr__(self): if self.address: diff --git a/cybertensor/synapse.py b/cybertensor/synapse.py index 20ca129..a92bb12 100644 --- a/cybertensor/synapse.py +++ b/cybertensor/synapse.py @@ -257,6 +257,15 @@ class Config: allow_mutation=True, ) + # The cybertensor version on the terminal as an int. + pubkey: Optional[str] = pydantic.Field( + title="pubkey", + description="The pubkey string of the terminal wallet.", + examples="", + default=None, + allow_mutation=True, + ) + class Synapse(pydantic.BaseModel): """