diff --git a/AUTHORS b/AUTHORS index 2a7f34d5..bb094fef 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,6 +1,6 @@ - 685 Fabian Schuh + 699 Fabian Schuh 154 Fabian Schuh - 76 Fabian Schuh + 77 Fabian Schuh 24 pyup-bot 20 gileadmcgee 10 user name @@ -9,6 +9,7 @@ 4 bitcrab 3 Boombastic 3 Holger Nahrstaedt + 3 Vladimir Kamarzin 3 abitmore 2 Nicolas Wack 2 Scott Howard @@ -19,7 +20,6 @@ 1 Best Buy #2 1 Holger Nahrstaedt 1 Pavel Martynov - 1 Vladimir Kamarzin 1 iHashFury 1 jhtitor <34790146+jhtitor@users.noreply.github.com> 1 linouxis9 diff --git a/grapheneapi/rpc.py b/grapheneapi/rpc.py index 0b49b96c..e025ee94 100644 --- a/grapheneapi/rpc.py +++ b/grapheneapi/rpc.py @@ -60,9 +60,11 @@ def setup_proxy(self, options): self.proxy_user = options.pop("proxy_user", None) self.proxy_pass = options.pop("proxy_pass", None) self.proxy_rdns = False - log.info( - "Using proxy %s:%d %s" % (self.proxy_host, self.proxy_port, self.proxy_type) - ) + if self.proxy_host: + log.info( + "Using proxy %s:%d %s" + % (self.proxy_host, self.proxy_port, self.proxy_type) + ) def get_proxy_url(self): # pragma: no cover if not self.proxy_host: diff --git a/graphenecommon/message.py b/graphenecommon/message.py index eca37707..16f558f5 100644 --- a/graphenecommon/message.py +++ b/graphenecommon/message.py @@ -4,6 +4,7 @@ import re from binascii import hexlify, unhexlify +from datetime import datetime from graphenebase.ecdsa import sign_message, verify_message @@ -19,7 +20,7 @@ log = logging.getLogger(__name__) -class Message(AbstractBlockchainInstanceProvider): +class MessageV1(AbstractBlockchainInstanceProvider): """ Allow to sign and verify Messages that are sigend with a private key """ @@ -187,3 +188,187 @@ def verify(self, **kwargs): self.plain_message = message return True + + +class MessageV2(AbstractBlockchainInstanceProvider): + """ Allow to sign and verify Messages that are sigend with a private key + """ + + def __init__(self, message, *args, **kwargs): + self.define_classes() + assert self.account_class + assert self.publickey_class + + self.message = message + self.signed_by_account = None + self.signed_by_name = None + self.meta = None + self.plain_message = None + + def sign(self, account=None, **kwargs): + """ Sign a message with an account's memo key + + :param str account: (optional) the account that owns the bet + (defaults to ``default_account``) + :raises ValueError: If not account for signing is provided + + :returns: the signed message encapsulated in a known format + """ + if not account: + if "default_account" in self.blockchain.config: + account = self.blockchain.config["default_account"] + if not account: + raise ValueError("You need to provide an account") + + # Data for message + account = self.account_class(account, blockchain_instance=self.blockchain) + + # wif key + wif = self.blockchain.wallet.getPrivateKeyForPublicKey( + account["options"]["memo_key"] + ) + + payload = [ + "from", + account["name"], + "key", + account["options"]["memo_key"], + "time", + str(datetime.utcnow()), + "text", + self.message, + ] + enc_message = json.dumps(payload, separators=(",", ":")) + + # signature + signature = hexlify(sign_message(enc_message, wif)).decode("ascii") + + return dict(signed=enc_message, payload=payload, signature=signature) + + def verify(self, **kwargs): + """ Verify a message with an account's memo key + + :param str account: (optional) the account that owns the bet + (defaults to ``default_account``) + + :returns: True if the message is verified successfully + :raises InvalidMessageSignature if the signature is not ok + """ + assert isinstance(self.message, dict), "Message must be dictionary" + + payload = self.message.get("payload") + assert payload, "Missing payload" + payload_dict = {k[0]: k[1] for k in zip(payload[::2], payload[1::2])} + signature = self.message.get("signature") + + account_name = payload_dict.get("from").strip() + memo_key = payload_dict.get("key").strip() + + assert account_name, "Missing account name 'from'" + assert memo_key, "missing 'key'" + + try: + self.publickey_class(memo_key, prefix=self.blockchain.prefix) + except Exception: + raise InvalidMemoKeyException("The memo key in the message is invalid") + + # Load account from blockchain + try: + account = self.account_class( + account_name, blockchain_instance=self.blockchain + ) + except AccountDoesNotExistsException: + raise AccountDoesNotExistsException( + "Could not find account {}. Are you connected to the right chain?".format( + account_name + ) + ) + + # Test if memo key is the same as on the blockchain + if not account["options"]["memo_key"] == memo_key: + raise WrongMemoKey( + "Memo Key of account {} on the Blockchain ".format(account["name"]) + + "differs from memo key in the message: {} != {}".format( + account["options"]["memo_key"], memo_key + ) + ) + + # Ensure payload and signed match + signed_target = json.dumps(self.message.get("payload"), separators=(",", ":")) + signed_actual = self.message.get("signed") + assert ( + signed_target == signed_actual + ), "payload doesn't match signed message: \n{}\n{}".format( + signed_target, signed_actual + ) + + # Reformat message + enc_message = self.message.get("signed") + + # Verify Signature + pubkey = verify_message(enc_message, unhexlify(signature)) + + # Verify pubky + pk = self.publickey_class( + hexlify(pubkey).decode("ascii"), prefix=self.blockchain.prefix + ) + if format(pk, self.blockchain.prefix) != memo_key: + raise InvalidMessageSignature("The signature doesn't match the memo key") + + self.signed_by_account = account + self.signed_by_name = account["name"] + self.plain_message = payload_dict.get("text") + + return True + + +class Message(MessageV1, MessageV2): + supported_formats = (MessageV1, MessageV2) + valid_exceptions = ( + AccountDoesNotExistsException, + InvalidMessageSignature, + WrongMemoKey, + InvalidMemoKeyException, + ) + + def __init__(self, *args, **kwargs): + for _format in self.supported_formats: + try: + _format.__init__(self, *args, **kwargs) + return + except self.valid_exceptions as e: + raise e + except Exception as e: + log.warning( + "{}: Couldn't init: {}: {}".format( + _format.__name__, e.__class__.__name__, str(e) + ) + ) + + def verify(self, **kwargs): + for _format in self.supported_formats: + try: + return _format.verify(self, **kwargs) + except self.valid_exceptions as e: + raise e + except Exception as e: + log.warning( + "{}: Couldn't verify: {}: {}".format( + _format.__name__, e.__class__.__name__, str(e) + ) + ) + raise ValueError("No Decoder accepted the message") + + def sign(self, *args, **kwargs): + for _format in self.supported_formats: + try: + return _format.sign(self, *args, **kwargs) + except self.valid_exceptions as e: + raise e + except Exception as e: + log.warning( + "{}: Couldn't sign: {}: {}".format( + _format.__name__, e.__class__.__name__, str(e) + ) + ) + raise ValueError("No Decoder accepted the message") diff --git a/requirements-test.txt b/requirements-test.txt index 393b9ecb..27b2fbdc 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -4,6 +4,7 @@ dateutils==0.6.6 # Unit testing pytest==4.4.0 pytest-mock==1.10.3 +pytest-benchmark==3.2.2 coverage==4.5.3 mock==2.0.0 diff --git a/requirements.txt b/requirements.txt index f3c2c3bd..8b12258d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ pylibscrypt==1.8.0 pycryptodome==3.8.0 appdirs==1.4.3 scrypt==0.8.13 -secp256k1==0.13.2 +# secp256k1==0.13.2 diff --git a/setup.py b/setup.py index 65fea2f3..bd90bc48 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup -VERSION = "1.1.14" +VERSION = "1.1.16" URL = "https://github.com/xeroc/python-graphenelib" setup( diff --git a/tests/fixtures.py b/tests/fixtures.py index cd365105..561d92ff 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -82,7 +82,11 @@ from graphenecommon.committee import Committee as GCommittee from graphenecommon.block import Block as GBlock, BlockHeader as GBlockHeader from graphenecommon.blockchain import Blockchain as GBLockchain -from graphenecommon.message import Message as GMessage +from graphenecommon.message import ( + Message as GMessage, + MessageV1 as GMessageV1, + MessageV2 as GMessageV2, +) from graphenecommon.blockchainobject import ObjectCache, BlockchainObject from graphenecommon.price import Price as GPrice from graphenecommon.wallet import Wallet as GWallet @@ -258,6 +262,20 @@ def define_classes(self): self.publickey_class = PublicKey +@BlockchainInstance.inject +class MessageV1(GMessageV1): + def define_classes(self): + self.account_class = Account + self.publickey_class = PublicKey + + +@BlockchainInstance.inject +class MessageV2(GMessageV2): + def define_classes(self): + self.account_class = Account + self.publickey_class = PublicKey + + @BlockchainInstance.inject class Price(GPrice): def define_classes(self): diff --git a/tests/test_benchmark.py b/tests/test_benchmark.py new file mode 100644 index 00000000..b19cd8bb --- /dev/null +++ b/tests/test_benchmark.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +import time +from .fixtures import ( + Base58, + base58decode, + base58encode, + ripemd160, + base58CheckEncode, + base58CheckDecode, + gphBase58CheckEncode, + gphBase58CheckDecode, +) + +wif_base58 = "5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ" +wif_hex = "800c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d507a5b8d" +wif_hex_raw = "0c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d" +wif_gphbase85 = "6Mcb23muAxyXaSMhmB6B1mqkvLdWhtuFZmnZsxDczHRv4mL6Y" + + +def benchmark_base58(): + return (base58decode(wif_base58), base58encode(wif_hex)) + + +def benchmark_base58check(): + return (base58CheckDecode(wif_base58), base58CheckEncode(0x80, wif_hex_raw)) + + +def benchmark_gphbase58check(): + return (gphBase58CheckDecode(wif_gphbase85), gphBase58CheckEncode(wif_hex_raw)) + + +def test_base58(benchmark): + a, b = benchmark(benchmark_base58) + assert a == wif_hex + assert b == wif_base58 + + +def test_base58check(benchmark): + a, b = benchmark(benchmark_base58check) + print(a, b) + assert a == wif_hex_raw + assert b == wif_base58 + + +def test_gphbase58check(benchmark): + a, b = benchmark(benchmark_gphbase58check) + assert a == wif_hex_raw + assert b == wif_gphbase85 diff --git a/tests/test_message.py b/tests/test_message.py index d06e406a..5c12e585 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import unittest -from .fixtures import fixture_data, Message +from .fixtures import fixture_data, Message, MessageV1, MessageV2 +from pprint import pprint from graphenecommon.exceptions import InvalidMessageSignature @@ -40,3 +41,20 @@ def test_verify_message(self): "2034f601e175a25cf9f60a828650301f57c9efab53929b6a82fb413feb8a786fcb3ba4238dd8bece03aee38526ee363324d43944d4a3f9dc624fbe53ef5f0c9a5e\n" "-----END GRAPHENE SIGNED MESSAGE-----" ).verify() + + def test_v2_enc(self): + m = MessageV2("foobar") + c = m.sign(account="init0") + v = MessageV2(c) + v.verify() + + def test_v2andv1_enc(self): + m = MessageV2("foobar") + c = m.sign(account="init0") + v = Message(c) + v.verify() + + m = MessageV1("foobar") + c = m.sign(account="init0") + v = Message(c) + v.verify()