From 516be5a7b8421ce1ea5daf1c3e5263b9b2d9c7e3 Mon Sep 17 00:00:00 2001 From: Holger Nahrstaedt <holgernahrstaedt@gmx.de> Date: Sun, 7 Jun 2020 16:22:13 +0200 Subject: [PATCH] Add Prefix class * add Prefix class for PasswordKey, Brainkey, Address, PublicKey, PrivateKey, Base58 * New Class BitcoinAddress * Address class has now from_pubkey class method --- beemgraphenebase/account.py | 332 +++++++++++++++++--------- beemgraphenebase/base58.py | 37 +-- beemgraphenebase/bip38.py | 4 +- beemgraphenebase/ecdsasig.py | 19 +- beemgraphenebase/prefix.py | 11 + beemgraphenebase/types.py | 49 +++- tests/beemgraphene/test_account.py | 16 +- tests/beemgraphene/test_key_format.py | 16 +- tests/beemgraphene/test_types.py | 60 ++++- 9 files changed, 368 insertions(+), 176 deletions(-) create mode 100644 beemgraphenebase/prefix.py diff --git a/beemgraphenebase/account.py b/beemgraphenebase/account.py index 23f8b47d..03b1df17 100644 --- a/beemgraphenebase/account.py +++ b/beemgraphenebase/account.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import division from __future__ import print_function @@ -21,11 +22,12 @@ import itertools from binascii import hexlify, unhexlify import unicodedata -from .base58 import ripemd160, Base58 +from .base58 import ripemd160, Base58, doublesha256 from .bip32 import BIP32Key, parse_path from .dictionary import words as BrainKeyDictionary from .dictionary import words_bip39 as MnemonicDictionary from .py23 import py23_bytes, PY2 +from .prefix import Prefix PBKDF2_ROUNDS = 2048 @@ -37,18 +39,18 @@ def binary_search(a, x, lo=0, hi=None): # can't use a to specify default for hi return pos if pos != hi and a[pos] == x else -1 # don't walk off the end -class PasswordKey(object): +class PasswordKey(Prefix): """ This class derives a private key given the account name, the role and a password. It leverages the technology of Brainkeys and allows people to have a secure private key by providing a passphrase only. """ - def __init__(self, account, password, role="active", prefix="STM"): + def __init__(self, account, password, role="active", prefix=None): + self.set_prefix(prefix) self.account = account self.role = role self.password = password - self.prefix = prefix def normalize(self, seed): """ Correct formating with single whitespace syntax and no trailing space """ @@ -79,7 +81,7 @@ class PasswordKey(object): @python_2_unicode_compatible -class BrainKey(object): +class BrainKey(Prefix): """Brainkey implementation similar to the graphene-ui web-wallet. :param str brainkey: Brain Key @@ -99,7 +101,8 @@ class BrainKey(object): """ - def __init__(self, brainkey=None, sequence=0): + def __init__(self, brainkey=None, sequence=0, prefix=None): + self.set_prefix(prefix) if not brainkey: self.brainkey = self.suggest() else: @@ -132,13 +135,13 @@ class BrainKey(object): encoded = "%s %d" % (self.brainkey, self.sequence) a = py23_bytes(encoded, 'ascii') s = hashlib.sha256(hashlib.sha512(a).digest()).digest() - return PrivateKey(hexlify(s).decode('ascii')) + return PrivateKey(hexlify(s).decode('ascii'), prefix=self.prefix) def get_blind_private(self): """ Derive private key from the brain key (and no sequence number) """ a = py23_bytes(self.brainkey, 'ascii') - return PrivateKey(hashlib.sha256(a).hexdigest()) + return PrivateKey(hashlib.sha256(a).hexdigest(), prefix=self.prefix) def get_public(self): return self.get_private().pubkey @@ -349,11 +352,12 @@ class Mnemonic(object): -class MnemonicKey(object): +class MnemonicKey(Prefix): """ This class derives a private key from a BIP39 mnemoric implementation """ - def __init__(self, word_list=None, passphrase="", account_sequence=0, key_sequence=0, prefix="STM"): + def __init__(self, word_list=None, passphrase="", account_sequence=0, key_sequence=0, prefix=None): + self.set_prefix(prefix) if word_list is not None: self.set_mnemonic(word_list, passphrase=passphrase) else: @@ -453,13 +457,12 @@ class MnemonicKey(object): @python_2_unicode_compatible -class Address(object): +class Address(Prefix): """ Address class This class serves as an address representation for Public Keys. :param str address: Base58 encoded address (defaults to ``None``) - :param str pubkey: Base58 encoded pubkey (defaults to ``None``) :param str prefix: Network prefix (defaults to ``STM``) Example:: @@ -467,92 +470,99 @@ class Address(object): Address("STMFN9r6VYzBK8EKtMewfNbfiGCr56pHDBFi") """ - def __init__(self, address=None, pubkey=None, prefix="STM"): - self.prefix = prefix - if pubkey is not None: - self._pubkey = Base58(pubkey, prefix=prefix) - self._address = None - elif address is not None: - self._pubkey = None - self._address = Base58(address, prefix=prefix) - else: - raise Exception("Address has to be initialized by either the " + - "pubkey or the address.") + def __init__(self, address, prefix=None): + self.set_prefix(prefix) + self._address = Base58(address, prefix=self.prefix) - def get_public_key(self): - """Returns the pubkey""" - return self._pubkey + @classmethod + def from_pubkey(cls, pubkey, compressed=True, version=56, prefix=None): + """ Load an address provided by the public key. + Version: 56 => PTS + """ + # Ensure this is a public key + pubkey = PublicKey(pubkey, prefix=prefix or Prefix.prefix) + if compressed: + pubkey_plain = pubkey.compressed() + else: + pubkey_plain = pubkey.uncompressed() + sha = hashlib.sha256(unhexlify(pubkey_plain)).hexdigest() + rep = hexlify(ripemd160(sha)).decode("ascii") + s = ("%.2x" % version) + rep + result = s + hexlify(doublesha256(s)[:4]).decode("ascii") + result = hexlify(ripemd160(result)).decode("ascii") + return cls(result, prefix=pubkey.prefix) - def derivesha256address(self): + @classmethod + def derivesha256address(cls, pubkey, compressed=True, prefix=None): """ Derive address using ``RIPEMD160(SHA256(x))`` """ - pubkey = self.get_public_key() - if pubkey is None: - return None - pkbin = unhexlify(repr(pubkey)) - addressbin = ripemd160(hexlify(hashlib.sha256(pkbin).digest())) - return Base58(hexlify(addressbin).decode('ascii')) - - def derive256address_with_version(self, version=56): - """ Derive address using ``RIPEMD160(SHA256(x))`` - and adding version + checksum - """ - pubkey = self.get_public_key() - if pubkey is None: - return None - pkbin = unhexlify(repr(pubkey)) - addressbin = ripemd160(hexlify(hashlib.sha256(pkbin).digest())) - addr = py23_bytes(bytearray(ctypes.c_uint8(version & 0xFF))) + addressbin - check = hashlib.sha256(addr).digest() - check = hashlib.sha256(check).digest() - buffer = addr + check[0:4] - return Base58(hexlify(ripemd160(hexlify(buffer))).decode('ascii')) - - def derivesha512address(self): + pubkey = PublicKey(pubkey, prefix=prefix or Prefix.prefix) + if compressed: + pubkey_plain = pubkey.compressed() + else: + pubkey_plain = pubkey.uncompressed() + pkbin = unhexlify(repr(pubkey_plain)) + result = hexlify(hashlib.sha256(pkbin).digest()) + result = hexlify(ripemd160(result)).decode("ascii") + return cls(result, prefix=pubkey.prefix) + + @classmethod + def derivesha512address(cls, pubkey, compressed=True, prefix=None): """ Derive address using ``RIPEMD160(SHA512(x))`` """ - pubkey = self.get_public_key() - if pubkey is None: - return None - pkbin = unhexlify(repr(pubkey)) - addressbin = ripemd160(hexlify(hashlib.sha512(pkbin).digest())) - return Base58(hexlify(addressbin).decode('ascii')) + pubkey = PublicKey(pubkey, prefix=prefix or Prefix.prefix) + if compressed: + pubkey_plain = pubkey.compressed() + else: + pubkey_plain = pubkey.uncompressed() + pkbin = unhexlify(repr(pubkey_plain)) + result = hexlify(hashlib.sha512(pkbin).digest()) + result = hexlify(ripemd160(result)).decode("ascii") + return cls(result, prefix=pubkey.prefix) def __repr__(self): """ Gives the hex representation of the ``GrapheneBase58CheckEncoded`` Graphene address. """ - if self._address is None: - return repr(self.derivesha512address()) - else: - return repr(self._address) + return repr(self._address) def __str__(self): """ Returns the readable Graphene address. This call is equivalent to ``format(Address, "STM")`` """ - return format(self, self.prefix) + return format(self._address, self.prefix) def __format__(self, _format): """ May be issued to get valid "MUSE", "PLAY" or any other Graphene compatible address with corresponding prefix. """ - if self._address is None: - if _format.lower() == "btc": - return format(self.derivesha256address(), _format) - else: - return format(self.derivesha512address(), _format) - else: - return format(self._address, _format) + return format(self._address, _format) def __bytes__(self): """ Returns the raw content of the ``Base58CheckEncoded`` address """ - if self._address is None: - return py23_bytes(self.derivesha512address()) + return py23_bytes(self._address) + + +@python_2_unicode_compatible +class GrapheneAddress(Address): + """ Graphene Addresses are different. Hence we have a different class + """ + + @classmethod + def from_pubkey(cls, pubkey, compressed=True, version=56, prefix=None): + # Ensure this is a public key + pubkey = PublicKey(pubkey, prefix=prefix or Prefix.prefix) + if compressed: + pubkey_plain = pubkey.compressed() else: - return py23_bytes(self._address) + pubkey_plain = pubkey.uncompressed() + + """ Derive address using ``RIPEMD160(SHA512(x))`` """ + addressbin = ripemd160(hashlib.sha512(unhexlify(pubkey_plain)).hexdigest()) + result = Base58(hexlify(addressbin).decode("ascii")) + return cls(result, prefix=pubkey.prefix) @python_2_unicode_compatible -class PublicKey(Address): +class PublicKey(Prefix): """ This class deals with Public Keys and inherits ``Address``. :param str pk: Base58 encoded public key @@ -569,20 +579,39 @@ class PublicKey(Address): PublicKey("xxxxx").unCompressed() """ - def __init__(self, pk, prefix="STM"): + def __init__(self, pk, prefix=None): """Init PublicKey :param str pk: Base58 encoded public key :param str prefix: Network prefix (defaults to ``STM``) """ - self.prefix = prefix - self._pk = Base58(pk, prefix=prefix) - self.address = Address(pubkey=pk, prefix=prefix) - self.pubkey = self._pk + self.set_prefix(prefix) + if isinstance(pk, PublicKey): + pk = format(pk, self.prefix) + + if str(pk).startswith("04"): + # We only ever deal with compressed keys, so let's make it + # compressed + order = ecdsa.SECP256k1.order + p = ecdsa.VerifyingKey.from_string( + unhexlify(pk[2:]), curve=ecdsa.SECP256k1 + ).pubkey.point + x_str = ecdsa.util.number_to_string(p.x(), order) + pk = hexlify(chr(2 + (p.y() & 1)).encode("ascii") + x_str).decode("ascii") + + self._pk = Base58(pk, prefix=self.prefix) + + @property + def pubkey(self): + return self._pk def get_public_key(self): """Returns the pubkey""" return self.pubkey + @property + def compressed_key(self): + return PublicKey(self.compressed()) + def _derive_y_from_x(self, x, is_even): """ Derive y point from x point """ curve = ecdsa.SECP256k1.curve @@ -597,14 +626,9 @@ class PublicKey(Address): def compressed(self): """ Derive compressed public key """ - order = ecdsa.SECP256k1.generator.order() - p = ecdsa.VerifyingKey.from_string(py23_bytes(self), curve=ecdsa.SECP256k1).pubkey.point - x_str = ecdsa.util.number_to_string(p.x(), order) - # y_str = ecdsa.util.number_to_string(p.y(), order) - compressed = hexlify(py23_bytes(chr(2 + (p.y() & 1)), 'ascii') + x_str).decode('ascii') - return(compressed) + return repr(self._pk) - def unCompressed(self): + def uncompressed(self): """ Derive uncompressed key """ public_key = repr(self._pk) prefix = public_key[0:2] @@ -619,9 +643,41 @@ class PublicKey(Address): def point(self): """ Return the point for the public key """ - string = unhexlify(self.unCompressed()) + string = unhexlify(self.uncompressed()) return ecdsa.VerifyingKey.from_string(string[1:], curve=ecdsa.SECP256k1).pubkey.point + def child(self, offset256): + """ Derive new public key from this key and a sha256 "offset" """ + a = bytes(self) + offset256 + s = hashlib.sha256(a).digest() + return self.add(s) + + def add(self, digest256): + """ Derive new public key from this key and a sha256 "digest" """ + from .ecdsa import tweakaddPubkey + + return tweakaddPubkey(self, digest256) + + @classmethod + def from_privkey(cls, privkey, prefix=None): + """ Derive uncompressed public key """ + privkey = PrivateKey(privkey, prefix=prefix or Prefix.prefix) + secret = unhexlify(repr(privkey)) + order = ecdsa.SigningKey.from_string( + secret, curve=ecdsa.SECP256k1 + ).curve.generator.order() + p = ecdsa.SigningKey.from_string( + secret, curve=ecdsa.SECP256k1 + ).verifying_key.pubkey.point + x_str = ecdsa.util.number_to_string(p.x(), order) + # y_str = ecdsa.util.number_to_string(p.y(), order) + compressed = hexlify(chr(2 + (p.y() & 1)).encode("ascii") + x_str).decode( + "ascii" + ) + # uncompressed = hexlify( + # chr(4).encode('ascii') + x_str + y_str).decode('ascii') + return cls(compressed, prefix=prefix or Prefix.prefix) + def __repr__(self): """ Gives the hex representation of the Graphene public key. """ return repr(self._pk) @@ -640,9 +696,26 @@ class PublicKey(Address): """ Returns the raw public key (has length 33)""" return py23_bytes(self._pk) + def __lt__(self, other): + """ For sorting of public keys (due to graphene), + we actually sort according to addresses + """ + assert isinstance(other, PublicKey) + return repr(self.address) < repr(other.address) + + def unCompressed(self): + """ Alias for self.uncompressed() - LEGACY""" + return self.uncompressed() + + @property + def address(self): + """ Obtain a GrapheneAddress from a public key + """ + return GrapheneAddress.from_pubkey(repr(self), prefix=self.prefix) + @python_2_unicode_compatible -class PrivateKey(PublicKey): +class PrivateKey(Prefix): """ Derives the compressed and uncompressed public keys and constructs two instances of :class:`PublicKey`: @@ -665,41 +738,48 @@ class PrivateKey(PublicKey): Instance of :class:`Address` using uncompressed key. """ - def __init__(self, wif=None, prefix="STM"): + def __init__(self, wif=None, prefix=None): + self.set_prefix(prefix) if wif is None: - self._wif = Base58(hexlify(os.urandom(32)).decode('ascii'), prefix=prefix) + import os + self._wif = Base58(hexlify(os.urandom(32)).decode('ascii')) + elif isinstance(wif, PrivateKey): + self._wif = wif._wif elif isinstance(wif, Base58): self._wif = wif else: - self._wif = Base58(wif, prefix=prefix) - # compress pubkeys only - self._pubkeyhex, self._pubkeyuncompressedhex = self.compressedpubkey() - self.pubkey = PublicKey(self._pubkeyhex, prefix=prefix) - self.uncompressed = PublicKey(self._pubkeyuncompressedhex, prefix=prefix) - self.uncompressed.address = Address(pubkey=self._pubkeyuncompressedhex, prefix=prefix) - self.address = Address(pubkey=self._pubkeyhex, prefix=prefix) + self._wif = Base58(wif) + + assert len(repr(self._wif)) == 64 + + @property + def bitcoin(self): + return BitcoinPublicKey.from_privkey(self) + + @property + def address(self): + return Address.from_pubkey(self.pubkey, prefix=self.prefix) + + @property + def pubkey(self): + return self.compressed def get_public_key(self): - """Returns the pubkey""" + """Legacy: Returns the pubkey""" return self.pubkey - def compressedpubkey(self): - """ Derive uncompressed public key """ - secret = unhexlify(repr(self._wif)) - if not len(secret) == ecdsa.SECP256k1.baselen: - raise ValueError("{} != {}".format(len(secret), ecdsa.SECP256k1.baselen)) - order = ecdsa.SigningKey.from_string(secret, curve=ecdsa.SECP256k1).curve.generator.order() - p = ecdsa.SigningKey.from_string(secret, curve=ecdsa.SECP256k1).verifying_key.pubkey.point - x_str = ecdsa.util.number_to_string(p.x(), order) - y_str = ecdsa.util.number_to_string(p.y(), order) - compressed = hexlify(py23_bytes(chr(2 + (p.y() & 1)), 'ascii') + x_str).decode('ascii') - uncompressed = hexlify(py23_bytes(chr(4), 'ascii') + x_str + y_str).decode('ascii') - return([compressed, uncompressed]) + @property + def compressed(self): + return PublicKey.from_privkey(self, prefix=self.prefix) + + @property + def uncompressed(self): + return PublicKey(self.pubkey.uncompressed(), prefix=self.prefix) def get_secret(self): """ Get sha256 digest of the wif key. """ - return hashlib.sha256(py23_bytes(self)).digest() + return hashlib.sha256(bytes(self)).digest() def derive_private_key(self, sequence): """ Derive new private key from this private key and an arbitrary @@ -713,8 +793,7 @@ class PrivateKey(PublicKey): def child(self, offset256): """ Derive new private key from this key and a sha256 "offset" """ - pubkey = self.get_public_key() - a = py23_bytes(pubkey) + offset256 + a = py23_bytes(self.pubkey) + offset256 s = hashlib.sha256(a).digest() return self.derive_from_seed(s) @@ -726,10 +805,10 @@ class PrivateKey(PublicKey): seed = int(hexlify(py23_bytes(self)).decode('ascii'), 16) z = int(hexlify(offset).decode('ascii'), 16) order = ecdsa.SECP256k1.order - secexp = (seed + z) % order - secret = "%0x" % secexp + if len(secret) < 64: # left-pad with zeroes + secret = ("0" * (64-len(secret))) + secret return PrivateKey(secret, prefix=self.pubkey.prefix) def __format__(self, _format): @@ -751,3 +830,30 @@ class PrivateKey(PublicKey): def __bytes__(self): """ Returns the raw private key """ return py23_bytes(self._wif) + + +class BitcoinAddress(Address): + @classmethod + def from_pubkey(cls, pubkey, compressed=False, version=56, prefix=None): + # Ensure this is a public key + pubkey = PublicKey(pubkey) + if compressed: + pubkey = pubkey.compressed() + else: + pubkey = pubkey.uncompressed() + + """ Derive address using ``RIPEMD160(SHA256(x))`` """ + addressbin = ripemd160(hexlify(hashlib.sha256(unhexlify(pubkey)).digest())) + return cls(hexlify(addressbin).decode("ascii")) + + def __str__(self): + """ Returns the readable Graphene address. This call is equivalent to + ``format(Address, "GPH")`` + """ + return format(self._address, "BTC") + + +class BitcoinPublicKey(PublicKey): + @property + def address(self): + return BitcoinAddress.from_pubkey(repr(self)) diff --git a/beemgraphenebase/base58.py b/beemgraphenebase/base58.py index b4cf7248..e822c5c3 100644 --- a/beemgraphenebase/base58.py +++ b/beemgraphenebase/base58.py @@ -3,39 +3,18 @@ from __future__ import division from __future__ import print_function from __future__ import unicode_literals from builtins import str -from builtins import object -from builtins import chr from future.utils import python_2_unicode_compatible from binascii import hexlify, unhexlify from .py23 import py23_bytes, py23_chr, bytes_types, integer_types, string_types, text_type +from .prefix import Prefix import hashlib import string import logging log = logging.getLogger(__name__) -""" Default Prefix """ -PREFIX = "GPH" - -known_prefixes = [ - PREFIX, - "BTS", - "MUSE", - "TEST", - "TST", - "STM", - "STX", - "GLX", - "GLS", - "EOS", - "VIT", - "WKA", - "EUR", - "WLS", -] - @python_2_unicode_compatible -class Base58(object): +class Base58(Prefix): """Base58 base class This class serves as an abstraction layer to deal with base58 encoded @@ -60,8 +39,8 @@ class Base58(object): * etc. """ - def __init__(self, data, prefix=PREFIX): - self._prefix = prefix + def __init__(self, data, prefix=None): + self.set_prefix(prefix) if isinstance(data, Base58): data = repr(data) if all(c in string.hexdigits for c in data): @@ -70,8 +49,8 @@ class Base58(object): self._hex = base58CheckDecode(data) elif data[0] == "K" or data[0] == "L": self._hex = base58CheckDecode(data)[:-2] - elif data[:len(self._prefix)] == self._prefix: - self._hex = gphBase58CheckDecode(data[len(self._prefix):]) + elif data[:len(self.prefix)] == self.prefix: + self._hex = gphBase58CheckDecode(data[len(self.prefix):]) else: raise ValueError("Error loading Base58 object") @@ -89,10 +68,7 @@ class Base58(object): return base58encode(self._hex) elif _format.upper() == "BTC": return base58CheckEncode(0x00, self._hex) - elif _format.upper() in known_prefixes: - return _format.upper() + str(self) else: - log.warn("Format %s unknown. You've been warned!\n" % _format) return _format.upper() + str(self) def __repr__(self): @@ -169,7 +145,6 @@ def ripemd160(s): ripemd160.update(unhexlify(s)) return ripemd160.digest() - def doublesha256(s): return hashlib.sha256(hashlib.sha256(unhexlify(s)).digest()).digest() diff --git a/beemgraphenebase/bip38.py b/beemgraphenebase/bip38.py index 6bccbde5..0dd4b470 100644 --- a/beemgraphenebase/bip38.py +++ b/beemgraphenebase/bip38.py @@ -58,7 +58,7 @@ def encrypt(privkey, passphrase): """ privkeyhex = repr(privkey) # hex - addr = format(privkey.uncompressed.address, "BTC") + addr = format(privkey.bitcoin.address, "BTC") a = py23_bytes(addr, 'ascii') salt = hashlib.sha256(hashlib.sha256(a).digest()).digest()[0:4] if sys.version < '3': @@ -125,7 +125,7 @@ def decrypt(encrypted_privkey, passphrase): wif = Base58(privraw) """ Verify Salt """ privkey = PrivateKey(format(wif, "wif")) - addr = format(privkey.uncompressed.address, "BTC") + addr = format(privkey.bitcoin.address, "BTC") a = py23_bytes(addr, 'ascii') saltverify = hashlib.sha256(hashlib.sha256(a).digest()).digest()[0:4] if saltverify != salt: diff --git a/beemgraphenebase/ecdsasig.py b/beemgraphenebase/ecdsasig.py index 5b8d1a78..81a3d463 100644 --- a/beemgraphenebase/ecdsasig.py +++ b/beemgraphenebase/ecdsasig.py @@ -135,13 +135,13 @@ def recoverPubkeyParameter(message, digest, signature, pubkey): pubkey_comp = hexlify(compressedPubkey(pubkey)) if (p_comp == pubkey_comp): return i - else: + else: # pragma: no cover p = recover_public_key(digest, signature, i) p_comp = hexlify(compressedPubkey(p)) p_string = hexlify(p.to_string()) if isinstance(pubkey, PublicKey): pubkey_string = py23_bytes(repr(pubkey), 'latin') - else: + else: # pragma: no cover pubkey_string = hexlify(pubkey.to_string()) if (p_string == pubkey_string or p_comp == pubkey_string): @@ -208,7 +208,7 @@ def sign_message(message, wif, hashfn=hashlib.sha256): i += 4 # compressed i += 27 # compact break - else: + else: # pragma: no branch # pragma: no cover cnt = 0 p = py23_bytes(priv_key) sk = ecdsa.SigningKey.from_string(p, curve=ecdsa.SECP256k1) @@ -299,7 +299,7 @@ def verify_message(message, signature, hashfn=hashlib.sha256, recover_parameter= sigder = encode_dss_signature(r, s) p.verify(sigder, message, ec.ECDSA(hashes.SHA256())) phex = compressedPubkey(p) - else: + else: # pragma: no branch # pragma: no cover p = recover_public_key(digest, sig, recover_parameter) # Will throw an exception of not valid p.verify_digest( @@ -310,3 +310,14 @@ def verify_message(message, signature, hashfn=hashlib.sha256, recover_parameter= phex = compressedPubkey(p) return phex + + +def tweakaddPubkey(pk, digest256, SECP256K1_MODULE=SECP256K1_MODULE): + if SECP256K1_MODULE == "secp256k1": + tmp_key = secp256k1.PublicKey(pubkey=bytes(pk), raw=True) + new_key = tmp_key.tweak_add(digest256) # <-- add + raw_key = hexlify(new_key.serialize()).decode("ascii") + else: + raise Exception("Must have secp256k1 for `tweak_add`") + # raw_key = ecmult(pk, 1, digest256, SECP256K1_MODULE) + return PublicKey(raw_key, prefix=pk.prefix) diff --git a/beemgraphenebase/prefix.py b/beemgraphenebase/prefix.py new file mode 100644 index 00000000..25e55098 --- /dev/null +++ b/beemgraphenebase/prefix.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +class Prefix: + """ This class is meant to allow changing the prefix. + The prefix is used to link a public key to a specific blockchain. + """ + + prefix = "STM" + + def set_prefix(self, prefix): + if prefix: + self.prefix = prefix diff --git a/beemgraphenebase/types.py b/beemgraphenebase/types.py index 1e08d201..2d3b4d28 100644 --- a/beemgraphenebase/types.py +++ b/beemgraphenebase/types.py @@ -224,6 +224,36 @@ class Bytes(object): return str(self.data) +@python_2_unicode_compatible +class Hash(Bytes): + def json(self): + return str(self.data) + + def __bytes__(self): + return unhexlify(bytes(self.data, "utf-8")) + + +@python_2_unicode_compatible +class Ripemd160(Hash): + def __init__(self, a): + assert len(a) == 40, "Require 40 char long hex" + super().__init__(a) + + +@python_2_unicode_compatible +class Sha1(Hash): + def __init__(self, a): + assert len(a) == 40, "Require 40 char long hex" + super().__init__(a) + + +@python_2_unicode_compatible +class Sha256(Hash): + def __init__(self, a): + assert len(a) == 64, "Require 64 char long hex" + super().__init__(a) + + @python_2_unicode_compatible class Void(object): def __init__(self): @@ -401,15 +431,18 @@ class Id(object): @python_2_unicode_compatible class Enum8(Uint8): + # List needs to be provided by super class + options = [] + def __init__(self, selection): - if selection not in self.options and \ - not (isinstance(selection, int) and len(self.options) < selection): - raise AssertionError("Options are %s. Given '%s'" % ( - self.options, selection)) - if selection in self.options: - super(Enum8, self).__init__(self.options.index(selection)) - else: - super(Enum8, self).__init__(selection) + if selection not in self.options or ( + isinstance(selection, int) and len(self.options) < selection + ): + raise ValueError( + "Options are {}. Given '{}'".format(str(self.options), selection) + ) + + super(Enum8, self).__init__(self.options.index(selection)) def __str__(self): """Returns data as string.""" diff --git a/tests/beemgraphene/test_account.py b/tests/beemgraphene/test_account.py index e6207cff..fadc084b 100644 --- a/tests/beemgraphene/test_account.py +++ b/tests/beemgraphene/test_account.py @@ -79,11 +79,11 @@ class Testcases(unittest.TestCase): ]) def test_btsprivkey(self): - self.assertEqual([format(PrivateKey("5HqUkGuo62BfcJU5vNhTXKJRXuUi9QSE6jp8C3uBJ2BVHtB8WSd").address, "BTS"), - format(PrivateKey("5JWcdkhL3w4RkVPcZMdJsjos22yB5cSkPExerktvKnRNZR5gx1S").address, "BTS"), - format(PrivateKey("5HvVz6XMx84aC5KaaBbwYrRLvWE46cH6zVnv4827SBPLorg76oq").address, "BTS"), - format(PrivateKey("5Jete5oFNjjk3aUMkKuxgAXsp7ZyhgJbYNiNjHLvq5xzXkiqw7R").address, "BTS"), - format(PrivateKey("5KDT58ksNsVKjYShG4Ls5ZtredybSxzmKec8juj7CojZj6LPRF7").address, "BTS") + self.assertEqual([format(PrivateKey("5HqUkGuo62BfcJU5vNhTXKJRXuUi9QSE6jp8C3uBJ2BVHtB8WSd").compressed.address, "BTS"), + format(PrivateKey("5JWcdkhL3w4RkVPcZMdJsjos22yB5cSkPExerktvKnRNZR5gx1S").compressed.address, "BTS"), + format(PrivateKey("5HvVz6XMx84aC5KaaBbwYrRLvWE46cH6zVnv4827SBPLorg76oq").compressed.address, "BTS"), + format(PrivateKey("5Jete5oFNjjk3aUMkKuxgAXsp7ZyhgJbYNiNjHLvq5xzXkiqw7R").compressed.address, "BTS"), + format(PrivateKey("5KDT58ksNsVKjYShG4Ls5ZtredybSxzmKec8juj7CojZj6LPRF7").compressed.address, "BTS") ], ["BTSFN9r6VYzBK8EKtMewfNbfiGCr56pHDBFi", "BTSdXrrTXimLb6TEt3nHnePwFmBT6Cck112", @@ -93,9 +93,9 @@ class Testcases(unittest.TestCase): ]) def test_btcprivkey(self): - self.assertEqual([format(PrivateKey("5HvVz6XMx84aC5KaaBbwYrRLvWE46cH6zVnv4827SBPLorg76oq").uncompressed.address, "BTC"), - format(PrivateKey("5Jete5oFNjjk3aUMkKuxgAXsp7ZyhgJbYNiNjHLvq5xzXkiqw7R").uncompressed.address, "BTC"), - format(PrivateKey("5KDT58ksNsVKjYShG4Ls5ZtredybSxzmKec8juj7CojZj6LPRF7").uncompressed.address, "BTC"), + self.assertEqual([format(PrivateKey("5HvVz6XMx84aC5KaaBbwYrRLvWE46cH6zVnv4827SBPLorg76oq").bitcoin.address, "BTC"), + format(PrivateKey("5Jete5oFNjjk3aUMkKuxgAXsp7ZyhgJbYNiNjHLvq5xzXkiqw7R").bitcoin.address, "BTC"), + format(PrivateKey("5KDT58ksNsVKjYShG4Ls5ZtredybSxzmKec8juj7CojZj6LPRF7").bitcoin.address, "BTC"), ], ["1G7qw8FiVfHEFrSt3tDi6YgfAdrDrEM44Z", "12c7KAAZfpREaQZuvjC5EhpoN6si9vekqK", diff --git a/tests/beemgraphene/test_key_format.py b/tests/beemgraphene/test_key_format.py index e5865da0..338d9968 100644 --- a/tests/beemgraphene/test_key_format.py +++ b/tests/beemgraphene/test_key_format.py @@ -53,20 +53,20 @@ class Testcases(unittest.TestCase): def test_btc_uncompressed(self): public_key = PublicKey(key["public_key"]) - address = Address(address=None, pubkey=public_key.unCompressed()) - self.assertEqual(str(key["Uncompressed_BTC"]), (format(address.derive256address_with_version(0), "STM"))) + address = Address.from_pubkey(public_key.uncompressed(), compressed=False, version=0) + self.assertEqual(str(key["Uncompressed_BTC"]), (format(address, "STM"))) def test_btc_compressed(self): public_key = PublicKey(key["public_key"]) - address = Address(address=None, pubkey=repr(public_key)) - self.assertEqual(str(key["Compressed_BTC"]), (format(address.derive256address_with_version(0), "STM"))) + address = Address.from_pubkey(repr(public_key), version=0) + self.assertEqual(str(key["Compressed_BTC"]), (format(address, "STM"))) def test_pts_uncompressed(self): public_key = PublicKey(key["public_key"]) - address = Address(address=None, pubkey=public_key.unCompressed()) - self.assertEqual(str(key["Uncompressed_PTS"]), (format(address.derive256address_with_version(56), "STM"))) + address = Address.from_pubkey(public_key.uncompressed(), compressed=False, version=56) + self.assertEqual(str(key["Uncompressed_PTS"]), (format(address, "STM"))) def test_pts_compressed(self): public_key = PublicKey(key["public_key"]) - address = Address(address=None, pubkey=repr(public_key)) - self.assertEqual(str(key["Compressed_PTS"]), (format(address.derive256address_with_version(56), "STM"))) + address = Address.from_pubkey(repr(public_key), version=56) + self.assertEqual(str(key["Compressed_PTS"]), (format(address, "STM"))) diff --git a/tests/beemgraphene/test_types.py b/tests/beemgraphene/test_types.py index bd39a3ea..fdd341d9 100644 --- a/tests/beemgraphene/test_types.py +++ b/tests/beemgraphene/test_types.py @@ -7,8 +7,7 @@ from builtins import str import unittest import json from beemgraphenebase import types -from beem.amount import Amount -from beem import Steem +from binascii import hexlify, unhexlify from beemgraphenebase.py23 import ( py23_bytes, py23_chr, @@ -179,3 +178,60 @@ class Testcases(unittest.TestCase): u = types.Map([[types.Uint16(10), types.Uint16(11)]]) self.assertEqual(py23_bytes(u), b"\x01\n\x00\x0b\x00") self.assertEqual(str(u), '[["10", "11"]]') + + def test_enum8(self): + class MyEnum(types.Enum8): + options = ["foo", "bar"] + + u = MyEnum("bar") + self.assertEqual(bytes(u), b"\x01") + self.assertEqual(str(u), "bar") + + with self.assertRaises(ValueError): + MyEnum("barbar") + + def test_Hash(self): + u = types.Hash(hexlify(b"foobar").decode("ascii")) + self.assertEqual(bytes(u), b"foobar") + self.assertEqual(str(u), "666f6f626172") + self.assertEqual(u.json(), "666f6f626172") + + def test_ripemd160(self): + u = types.Ripemd160("37f332f68db77bd9d7edd4969571ad671cf9dd3b") + self.assertEqual( + bytes(u), b"7\xf32\xf6\x8d\xb7{\xd9\xd7\xed\xd4\x96\x95q\xadg\x1c\xf9\xdd;" + ) + self.assertEqual(str(u), "37f332f68db77bd9d7edd4969571ad671cf9dd3b") + self.assertEqual(u.json(), "37f332f68db77bd9d7edd4969571ad671cf9dd3b") + + with self.assertRaises(AssertionError): + types.Ripemd160("barbar") + + def test_sha1(self): + u = types.Sha1("37f332f68db77bd9d7edd4969571ad671cf9dd3b") + self.assertEqual( + bytes(u), b"7\xf32\xf6\x8d\xb7{\xd9\xd7\xed\xd4\x96\x95q\xadg\x1c\xf9\xdd;" + ) + self.assertEqual(str(u), "37f332f68db77bd9d7edd4969571ad671cf9dd3b") + self.assertEqual(u.json(), "37f332f68db77bd9d7edd4969571ad671cf9dd3b") + + with self.assertRaises(AssertionError): + types.Sha1("barbar") + + def test_sha256(self): + u = types.Sha256( + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ) + self.assertEqual( + bytes(u), + b"\xe3\xb0\xc4B\x98\xfc\x1c\x14\x9a\xfb\xf4\xc8\x99o\xb9$'\xaeA\xe4d\x9b\x93L\xa4\x95\x99\x1bxR\xb8U", + ) + self.assertEqual( + str(u), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ) + self.assertEqual( + u.json(), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ) + + with self.assertRaises(AssertionError): + types.Sha256("barbar") -- GitLab