diff --git a/bitforge/__init__.py b/bitforge/__init__.py index 043f630..af0f422 100644 --- a/bitforge/__init__.py +++ b/bitforge/__init__.py @@ -1,10 +1,14 @@ +from network import Network from privkey import PrivateKey from pubkey import PublicKey -from address import Address -from script import Script -from hdprivkey import HDPrivateKey -from network import Network -import network -import ecdsa +# from address import Address +# from script import Script +# from hdprivkey import HDPrivateKey from unit import Unit -from uri import URI +# from uri import URI + +import network +import privkey +import pubkey +import unit +# import uri diff --git a/bitforge/address.py b/bitforge/address.py index 026f6a3..63bfcc6 100644 --- a/bitforge/address.py +++ b/bitforge/address.py @@ -1,120 +1,135 @@ -import binascii, collections -from enum import Enum +import collections -import network, utils -from network import Network -from encoding import * -from errors import * -# from script import Script +import enum +from bitforge import encoding, error, network -BaseAddress = collections.namedtuple('Address', ['phash', 'network', 'type']) -class Address(BaseAddress): +class Type(enum.Enum): + PublicKey = 'pubkey_hash' + Script = 'script_hash' + + +class Error(error.BitforgeError): + pass + - class Type(Enum): - PublicKey = 'pubkeyhash' - Script = 'scripthash' +class InvalidEncoding(Error): + pass - class Error(BitforgeError): - pass - class UnknownNetwork(Error, Network.UnknownNetwork): - "No network for Address with an attribute '{key}' of value {value}" +class InvalidHashLength(Error, error.StringError): + "The address hash {string} should be 20 bytes long, not {length}" - class InvalidVersion(Error, NumberError): - "Failed to detect Address type and network from version number {number}" - class InvalidBase58h(Error, InvalidBase58h): - "The Address string {string} is not valid base58/check" +class InvalidType(Error, error.ObjectError): + "{object} is not a valid address type" - class InvalidHex(Error, InvalidHex): - "The Address string {string} is not valid hexadecimal" - class InvalidHashLength(Error, StringError): - "The address hash {string} should be 20 bytes long, not {length}" +BaseAddress = collections.namedtuple('Address', [ + 'hash', + 'type', + 'network', +]) - class InvalidBinaryLength(Error, StringError): - "The binary address {string} should be 21 bytes long, not {length}" - class InvalidType(Error, ObjectError): - "Address type {object} is not an instance of Address.Type" +class Address(BaseAddress): + """Bitcoin address.""" + + def __new__(cls, hash, type=Type.PublicKey, network=network.default): + """TODO""" + if not isinstance(type, Type): + raise InvalidType(type) - def __new__(cls, phash, network = network.default, type = Type.PublicKey): - if not isinstance(type, Address.Type): - raise Address.InvalidType(type) + if len(hash) != 20: + raise InvalidHashLength(hash) - if len(phash) != 20: - raise Address.InvalidHashLength(phash) + return super(Address, cls).__new__(cls, hash, type, network) - return super(Address, cls).__new__(cls, phash, network, type) + @classmethod + def from_string(cls, string): + """TODO""" - @staticmethod - def from_string(string): try: - bytes = decode_base58h(string) - except InvalidBase58h: - raise Address.InvalidBase58h(string) + data = encoding.a2b_base58check(string) + + except encoding.InvalidEncoding as e: + raise InvalidEncoding(e.message) + + return cls.from_bytes(data) - return Address.from_bytes(bytes) + @classmethod + def from_bytes(cls, data): + """TODO""" - @staticmethod - def from_bytes(bytes): - if len(bytes) != 21: - raise Address.InvalidBinaryLength(bytes) + if len(data) != 21: + raise InvalidEncoding('Invalid address length') - network, type = Address.classify_bytes(bytes) + type_, network_ = cls.classify_bytes(data) - return Address(bytes[1:], network, type) + return cls(data[1:], type_, network_) + + @classmethod + def from_hex(cls, string): + """TODO""" - @staticmethod - def from_hex(string): try: - bytes = decode_hex(string) - except InvalidHex: - raise Address.InvalidHex(string) + data = encoding.a2b_hex(string) + + except encoding.InvalidEncoding as e: + raise InvalidEncoding(e.message) + + return cls.from_bytes(data) + + @classmethod + def classify_bytes(cls, data): + """TODO""" - return Address.from_bytes(bytes) + data = bytearray(data) + version = data[0] - @staticmethod - def classify_bytes(bytes): - version = decode_int(bytes[0]) + network_ = network.Network.get_by_field('pubkey_hash_prefix', version, raises=False) + if network_ is not None: + return (Type.PublicKey, network_) - network = Network.get_by_field('pubkeyhash', version, raises = False) - if network is not None: - return (network, Address.Type.PublicKey) + network_ = network.Network.get_by_field('script_hash_prefix', version, raises=False) + if network_ is not None: + return (Type.Script, network_) - network = Network.get_by_field('scripthash', version, raises = False) - if network is not None: - return (network, Address.Type.Script) + raise InvalidEncoding('Invalid version number') - raise Address.InvalidVersion(version) + @classmethod + def from_public_key(cls, pubkey): + """TODO""" - @staticmethod - def from_public_key(pubkey): - phash = ripemd160(sha256(pubkey.to_bytes())) - return Address(phash, pubkey.network, Address.Type.PublicKey) + return pubkey.address() def to_bytes(self): + """TODO""" + version = getattr(self.network, self.type.value) - return chr(version) + self.phash + return chr(version) + self.hash def to_string(self): - return encode_base58h(self.to_bytes()) + """TODO""" + + return encoding.b2a_base58check(self.to_bytes()) def to_hex(self): - return encode_hex(self.to_bytes()) + """TODO""" + + return encoding.b2a_hex(self.to_bytes()) # TODO: all keys should be from the same network - # @staticmethod + # @classmethod # def from_public_keys(pubkeys, threshold): # return Address.from_script( # Script.buildMultisigOut(pubkeys, threshold), # pubkeys[0].network # ) - # @staticmethod + # @classmethod # def from_script(script, network = networks.default): # if not isinstance(script, Script): # raise ValueError('Expected instance of Script, not %s' % script) diff --git a/bitforge/encoding.py b/bitforge/encoding.py index c57e861..60f0ace 100644 --- a/bitforge/encoding.py +++ b/bitforge/encoding.py @@ -1,68 +1,67 @@ -import utils, binascii, hashlib -from errors import StringError +import binascii +import hashlib +from bitforge import error +from bitforge.utils import encoding -class EncodingError(StringError): - "The string {string} is not properly encoded" +class Error(error.BitforgeError): + pass -class InvalidBase58h(EncodingError): - "The string {string} is not valid base58/check" +def b2a_hex(data): + """Convert a byte buffer to an hexadecimal string.""" -class InvalidHex(EncodingError): - "The string {string} is not valid hexadecimal" + return binascii.b2a_hex(data) -def encode_base58h(bytes): - return utils.encoding.b2a_hashed_base58(bytes) +def a2b_hex(string): + """Convert an hexadecimal string to a byte buffer.""" + if len(string) % 2 == 1: + string = '0' + string -def decode_base58h(string): try: - return utils.encoding.a2b_hashed_base58(string) + return binascii.a2b_hex(string.encode('ascii')) + except TypeError: + raise Error('Invalid hexadecimal string') - except utils.encoding.EncodingError: - raise InvalidBase58h(string) +def b2i_bigendian(data): + """Convert a big endian byte buffer to an unsigned big integer.""" -def encode_int(integer, big_endian = True): - bytes = bytearray() + # Encoding and decoding from hexa appears to be way faster than manually + # decoding the buffer in python. + return int(b2a_hex(data), 16) - while integer > 0: - bytes.append(integer & 0xff) - integer >>= 8 - if big_endian: - bytes.reverse() +def i2b_bigendian(number, num_bytes = 0): + """Convert an unsigned big integer to a zero-padded big endian byte buffer. + """ - return str(bytes) + # Encoding and decoding from hexa appears to be way faster than manually + # decoding the buffer in python. + return a2b_hex('%0*x' % (2 * num_bytes, number)) -def decode_int(bytes, big_endian = True): - if not big_endian: - bytes = reversed(bytes) +# TODO: implement these functions - integer = 0 +def b2a_base58check(data): + """Convert a byte buffer to a base58check string.""" - for char in bytes: - integer <<= 8 - integer += ord(char) + return encoding.b2a_hashed_base58(data) - return integer +def a2b_base58check(string): + """Convert a base58check string to a byte buffer.""" -def encode_hex(bytes): - return binascii.hexlify(bytes) - - -def decode_hex(string): try: - return binascii.unhexlify(string) - except: - # unhexlify() throws 2 different exceptions (length, and alphabet) - raise InvalidHex(string) + return encoding.a2b_hashed_base58(string) + except encoding.EncodingError: + raise Error('Invalid base58check string') + +# TODO: these are not encodings, they shouldn't be here def sha256(bytes): return hashlib.sha256(bytes).digest() diff --git a/bitforge/error.py b/bitforge/error.py index 2a77586..9c7d6c7 100644 --- a/bitforge/error.py +++ b/bitforge/error.py @@ -1,34 +1,39 @@ - class BitforgeError(Exception): + def __init__(self, *args, **kwargs): self.cause = kwargs.pop('cause', None) self.prepare(*args, **kwargs) self.message = self.__doc__.format(**self.__dict__) - def prepare(self): - pass + def prepare(self, message=None): + if message is not None: + self.__doc__ = message def __str__(self): return self.message class ObjectError(BitforgeError): + def prepare(self, object): self.object = object class StringError(BitforgeError): + def prepare(self, string): self.string = repr(string) self.length = len(string) class NumberError(BitforgeError): + def prepare(self, number): self.number = number class KeyValueError(BitforgeError): + def prepare(self, key, value): self.key = key self.value = value diff --git a/bitforge/network.py b/bitforge/network.py index 9820e0c..621d48f 100644 --- a/bitforge/network.py +++ b/bitforge/network.py @@ -29,13 +29,14 @@ class InvalidField(Error, error.StringError): 'aliases', # All the network aliases # Cryptography parameters + # WARNING: only curves over prime fields are currently supported 'curve', # Elliptic curve used for the crypto 'hash_function', # Signature hashing function # Serialization magic numbers - 'pubkeyhash', 'wif_prefix', # Byte prefix used to identify the network in the WIF encoding - 'scripthash', + 'pubkey_hash_prefix', # Byte prefix used for P2PKH addresses + 'script_hash_prefix', # Byte prefix used for P2SH addresses 'hd_public_key', 'hd_private_key', 'magic', @@ -49,7 +50,12 @@ class InvalidField(Error, error.StringError): class Network(BaseNetwork): """Parameters of a Bitcoin-compatible network.""" - UNIQUE_FIELDS = ['name', 'wif_prefix'] + UNIQUE_FIELDS = [ + 'name', + 'wif_prefix', + 'pubkey_hash_prefix', + 'script_hash_prefix', + ] _networks = [] _networks_by_name = {} @@ -90,6 +96,16 @@ def __new__(cls, **kwargs): if getattr(other, field) == getattr(network, field): raise InvalidNetwork(field, getattr(network, field)) + # Enforce uniqueness of the address prefixes + for other in cls._networks: + if other.pubkey_hash_prefix == network.script_hash_prefix: + raise InvalidNetwork('address prefix', network.script_hash_prefix) + + if other.script_hash_prefix == network.pubkey_hash_prefix: + raise InvalidNetwork('address prefix', network.pubkey_hash_prefix) + + cls._networks.append(network) + # Enforce uniqueness of the network aliases for name in [network.name] + network.aliases: if name in cls._networks_by_name: @@ -108,11 +124,11 @@ def __str__(self): testnet = Network( name = 'testnet', aliases = [], - curve = ec.SECP256K1, - hash_function = hashes.SHA256, - pubkeyhash = 111, + curve = ec.SECP256K1(), + hash_function = hashes.SHA256(), wif_prefix = 239, - scripthash = 196, + pubkey_hash_prefix = 111, + script_hash_prefix = 196, hd_public_key = 0x043587cf, hd_private_key = 0x04358394, magic = 0x0b110907, @@ -129,11 +145,11 @@ def __str__(self): default = livenet = Network( name = 'livenet', aliases = ['mainnet', 'default'], - curve = ec.SECP256K1, - hash_function = hashes.SHA256, - pubkeyhash = 0x00, + curve = ec.SECP256K1(), + hash_function = hashes.SHA256(), wif_prefix = 0x80, - scripthash = 0x05, + pubkey_hash_prefix = 0x00, + script_hash_prefix = 0x05, hd_public_key = 0x0488b21e, hd_private_key = 0x0488ade4, magic = 0xf9beb4d9, diff --git a/bitforge/opcode.py b/bitforge/opcode.py index f8eefed..368713f 100644 --- a/bitforge/opcode.py +++ b/bitforge/opcode.py @@ -1,6 +1,7 @@ -import sys, inspect -from numbers import Number -from errors import * +import sys +import inspect + +from bitforge import error # Below is a list of all *named* opcodes. Their values, integers in the @@ -155,28 +156,26 @@ class Opcode(object): - class Error(BitforgeError): + class Error(error.BitforgeError): pass - class UnknownOpcodeName(Error, StringError): + class UnknownOpcodeName(Error, error.StringError): "No known operation named {string}" - class UnknownOpcodeNumber(Error, NumberError): + class UnknownOpcodeNumber(Error, error.NumberError): "No known operation numbered {number}" - class InvalidConstPushLength(Error, StringError): + class InvalidConstPushLength(Error, error.StringError): "No constant push opcode can push {length} bytes (only [1-75])" - class InvalidPushLength(Error, NumberError): + class InvalidPushLength(Error, error.NumberError): "No Opcode can push {number} bytes" - class TypeError(Error, ObjectError): + class TypeError(Error, error.ObjectError): "Opcodes are initialized from numbers and names, got object {object}" - opcode_number_to_name = {} # Filled after class definition - def __init__(self, number): if not (0 <= number <= 255): raise Opcode.UnknownOpcodeNumber(number) @@ -218,7 +217,6 @@ def __eq__(self, other): def __hash__(self): return hash(self.number) - @staticmethod def for_number(n): if 0 <= n <= 16: @@ -243,13 +241,13 @@ def const_push_for(length): @staticmethod def var_push_for(length): if length < 1: - raise InvalidPushLength(length) + raise Opcode.InvalidPushLength(length) for opcode in [OP_PUSHDATA1, OP_PUSHDATA2, OP_PUSHDATA4]: if length <= Opcode.data_length_max(opcode): return opcode - raise InvalidPushLength(length) + raise Opcode.InvalidPushLength(length) @staticmethod def push_for(length): @@ -282,7 +280,6 @@ def data_length_nbytes(opcode): }[opcode] - # Walk the OP_* variables, mapping them to their names and creating Opcode objs: _module = sys.modules[__name__] @@ -294,7 +291,7 @@ def data_length_nbytes(opcode): # Replace integer values with actual Opcode instances: setattr(_module, name, Opcode(number)) -Opcode.opcode_number_to_name[OP_0] = 'OP_0' # shares number with OP_FALSE -Opcode.opcode_number_to_name[OP_1] = 'OP_1' # shares number with OP_TRUE +Opcode.opcode_number_to_name[OP_0] = 'OP_0' # shares number with OP_FALSE +Opcode.opcode_number_to_name[OP_1] = 'OP_1' # shares number with OP_TRUE -del _module # the expected use for this module is to import * +del _module # the expected use for this module is to import * diff --git a/bitforge/privkey.py b/bitforge/privkey.py index 05f2b73..e06b5f5 100644 --- a/bitforge/privkey.py +++ b/bitforge/privkey.py @@ -1,126 +1,178 @@ -import random, struct, binascii, collections -import network, utils -from errors import * -from network import Network -from pubkey import PublicKey -from address import Address -from encoding import * +import collections +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import ec -rng = random.SystemRandom() -KEY_MAX = utils.generator_secp256k1.order() +from bitforge import encoding, error, network, pubkey, tools -def random_secret(): - return rng.randint(1, KEY_MAX - 1) +class Error(error.BitforgeError): + pass -def find_network(value, attr = 'name'): - try: - return Network.get_by_field(attr, value) - except: - raise PrivateKey.UnknownNetwork(attr, value) +class InvalidExponent(Error): + """Invalid private key secret exponent""" + + +class InvalidEncoding(Error): + pass + + +BasePrivateKey = collections.namedtuple('PrivateKey', [ + 'key', # Elliptic curve private key + 'network', # Bitcoin-compatible network + 'compressed', # Whether the public key should be serialized in compressed format +]) -BasePrivateKey = collections.namedtuple('PrivateKey', - ['secret', 'network', 'compressed'] -) class PrivateKey(BasePrivateKey): + """Bitcoin private key.""" + + def __new__(cls, key, network=network.default, compressed=True): + """Create a Bitcoin private key from an EC private key.""" + + return super(PrivateKey, cls).__new__(cls, key, network, compressed) + + @classmethod + def generate(cls, network=network.default, compressed=True, backend=default_backend()): + """Generate a new private key.""" - class Error(BitforgeError): - pass + key = ec.generate_private_key(network.curve, backend) - class InvalidSecret(Error, NumberError): - "Invalid secret for PrivateKey: {number}" + return cls(key, network, compressed) - class UnknownNetwork(Error, Network.UnknownNetwork): - "No network for PrivateKey with an attribute '{key}' of value {value}" + @classmethod + def from_secret_exponent(cls, exponent, network=network.default, compressed=True, backend=default_backend()): + """Create a private key from its secret exponent. - class InvalidWifLength(Error, StringError): - "The WIF {string} should be 33 (uncompressed) or 34 (compressed) bytes long, not {length}" + The secret exponent should be an unsigned integer (d), strictly between + 0 and the elliptic curve order (n). Bitcoin's curve order is: - class InvalidCompressionByte(Error, StringError): - "The length of the WIF {string} suggests it's compressed, but it doesn't end in '\1'" + n = FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141 + """ - class InvalidBase58h(Error, InvalidBase58h): - "The PrivateKey string {string} is not valid base58/check" + # NOTE: This implementation is only temporary. Whenever cryptography + # implements the from_private_number_and_curve method, change what + # follows to: + # + # try: + # private_numbers = ec.EllipticCurvePrivateNumbers.from_private_value_and_curve(exponent, network.curve, backend) + # except ??: + # raise InvalidExponent() + # + # key = private_numbers.private_key(backend) - class InvalidHex(Error, InvalidHex): - "The PrivateKey string {string} is not valid hexadecimal" + import hashlib + import ecdsa + from cryptography.hazmat.primitives.serialization import load_der_private_key - class InvalidBinaryLength(Error, StringError): - "The binary secret {string} should be 32 bytes long, not {length}" + try: + sig_key = ecdsa.SigningKey.from_secret_exponent(exponent, curve=ecdsa.curves.SECP256k1, hashfunc=hashlib.sha256) + except AssertionError: + raise InvalidExponent() + + key = load_der_private_key(sig_key.to_der(), password=None, backend=backend) + + return cls(key, network, compressed) + + @classmethod + def from_bytes(cls, data, network=network.default, compressed=True, backend=default_backend()): + """Create a private key from its raw binary encoding (in SEC1 format). + The input buffer should be a zero-padded big endian unsigned integer. + For more info on this format, see: - def __new__(cls, secret = None, network = network.default, compressed = True): - if secret is None: - secret = random_secret() + http://www.secg.org/sec1-v2.pdf, section 2.3.6 + """ - if not (0 < secret < KEY_MAX): - raise PrivateKey.InvalidSecret(secret) + if len(data) != tools.elliptic_curve_key_size(network.curve): + raise InvalidEncoding('Invalid key length') - return super(PrivateKey, cls).__new__(cls, secret, network, compressed) + exponent = encoding.b2i_bigendian(data) - @staticmethod - def from_wif(string): try: - bytes = decode_base58h(string) - except InvalidBase58h: - raise PrivateKey.InvalidBase58h(string) + return cls.from_secret_exponent(exponent, network, compressed, backend) - if len(bytes) == 33: - compressed = False + except InvalidExponent as e: + raise InvalidEncoding(e.message) - elif len(bytes) == 34: - if bytes[-1] != '\1': - raise PrivateKey.InvalidCompressionByte(string) + @classmethod + def from_wif(cls, wif, backend=default_backend()): + """Create a private key from its WIF encoding. - bytes = bytes[:-1] - compressed = True + The Wallet Import Format encoding is used for serializing Bitcoin + private keys. For more info on this encoding, see: - else: - raise PrivateKey.InvalidWifLength(bytes) + https://en.bitcoin.it/wiki/Wallet_import_format + """ - network = find_network(ord(bytes[0]), 'wif_prefix') - secret = decode_int(bytes[1:]) + # A WIF private key is base58check encoded + try: + data = bytearray(encoding.a2b_base58check(wif)) - return PrivateKey(secret, network, compressed) + except encoding.Error as e: + raise InvalidEncoding(e.message) - @staticmethod - def from_bytes(bytes, network = network.default, compressed = True): - if len(bytes) != 32: - raise PrivateKey.InvalidBinaryLength(bytes) + # The first byte determines the network + try: + prefix = data.pop(0) - secret = decode_int(bytes) - return PrivateKey(secret, network, compressed) + except IndexError: + raise InvalidEncoding('Invalid WIF length') - @staticmethod - def from_hex(string, network = network.default, compressed = True): try: - bytes = decode_hex(string) - except InvalidHex: - raise PrivateKey.InvalidHex(string) + network_ = network.Network.get_by_field('wif_prefix', prefix) - return PrivateKey.from_bytes(bytes, network, compressed) + except network.UnknownNetwork as e: + raise InvalidEncoding(e.message) - def to_wif(self): - network_byte = chr(self.network.wif_prefix) - secret_bytes = self.to_bytes() - compressed_byte = '\1' if self.compressed else '' + # If the public key should be compressed-encoded, there will be an + # extra 1 byte at the end + key_size = tools.elliptic_curve_key_size(network_.curve) - return encode_base58h(network_byte + secret_bytes + compressed_byte) + compressed = True if len(data) == key_size + 1 else False + + if compressed and data[-1] == 1: + data.pop(-1) + + # What remains should be the raw private key exponent + return cls.from_bytes(bytes(data), network_, compressed, backend) + + def public_key(self): + """The PublicKey object for this private key.""" + + return pubkey.PublicKey(self.key.public_key(), self.network, self.compressed) + + def sign(self, message): + """TODO""" + + signer = self.key.signer(ec.ECDSA(self.network.hash_function)) + signer.update(message) + + der_sig = signer.finalize() + return der_sig def to_bytes(self): - return encode_int(self.secret) + """TODO""" + + key_size = tools.elliptic_curve_key_size(self.network.curve) + + exponent = self.key.private_numbers().private_value + + return encoding.i2b_bigendian(exponent, key_size) + + def to_wif(self): + """TODO""" + + data = bytearray() - def to_hex(self): - return encode_hex(self.to_bytes()) + data.append(self.network.wif_prefix) + data.extend(self.to_bytes()) - def to_public_key(self): - return PublicKey.from_private_key(self) + if self.compressed: + data.append(1) - def to_address(self): - return Address.from_public_key(self.to_public_key()) + return encoding.b2a_base58check(bytes(data)) def __repr__(self): - return "" % (self.to_hex(), self.network.name) + return "".format(str(self.network), self.compressed) diff --git a/bitforge/pubkey.py b/bitforge/pubkey.py index e78ce82..ba5d0d2 100644 --- a/bitforge/pubkey.py +++ b/bitforge/pubkey.py @@ -1,84 +1,155 @@ import collections -from utils.secp256k1 import generator_secp256k1 -import network, utils -from network import Network -from address import Address -from errors import * -from encoding import * +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec -def find_network(value, attr = 'name'): - try: - return Network.get_by_field(attr, value) - except: - raise PublicKey.UnknownNetwork(attr, value) +from bitforge import address, encoding, error, network, tools -BasePublicKey = collections.namedtuple('PublicKey', - ['pair', 'network', 'compressed'] -) +# Magic numbers for the SEC1 public key format (TODO: shouldn't be here!) +SEC1_MAGIC_COMPRESSED_0 = 2 +SEC1_MAGIC_COMPRESSED_1 = 3 +SEC1_MAGIC_NOT_COMPRESSED = 4 -class PublicKey(BasePublicKey): - class Error(BitforgeError): - pass +class Error(error.BitforgeError): + pass + + +class InvalidPoint(Error): + """Invalid key (the point represented by the key is not on the curve)""" + - class InvalidPair(Error, ObjectError): - "The PublicKey pair {object} is invalid (not a point of the curve)" +class InvalidEncoding(Error): + pass - class UnknownNetwork(Error, Network.UnknownNetwork): - "No network for PublicKey with an attribute '{key}' of value {value}" - class InvalidBinary(Error, StringError): - "The buffer {string} is not in any recognized format" +BasePublicKey = collections.namedtuple('PublicKey', [ + 'key', # Elliptic curve public key + 'network', # Bitcoin-compatible network + 'compressed', # Whether the key should be serialized in compressed format +]) - class InvalidHex(Error, InvalidHex): - "The PublicKey string {string} is not valid hexadecimal" +class PublicKey(BasePublicKey): + """Bitcoin public key.""" + + def __new__(cls, key, network=network.default, compressed=True): + """Create a Bitcoin public key from an EC public key.""" - def __new__(cls, pair, network = network.default, compressed = True): - if not utils.ecdsa.is_public_pair_valid(generator_secp256k1, pair): - raise PublicKey.InvalidPair(pair) + return super(PublicKey, cls).__new__(cls, key, network, compressed) - return super(PublicKey, cls).__new__(cls, pair, network, compressed) + @classmethod + def from_point(cls, x, y, network=network.default, compressed=True, backend=default_backend()): + """Create a public key from its point coordinates. - @staticmethod - def from_private_key(privkey): - pair = utils.public_pair_for_secret_exponent( - utils.generator_secp256k1, privkey.secret - ) + A public key is a point on an elliptic curve, i.e. a pair (x, y) that + satisfies the curve equation. + """ - # The constructor will validate the pair - return PublicKey(pair, privkey.network, privkey.compressed) + public_numbers = ec.EllipticCurvePublicNumbers(x, y, network.curve) - @staticmethod - def from_bytes(bytes, network = network.default): try: - pair = utils.encoding.sec_to_public_pair(bytes) - compressed = utils.encoding.is_sec_compressed(bytes) - except: - raise PublicKey.InvalidBinary(bytes) + key = public_numbers.public_key(backend) + except ValueError: + raise InvalidPoint() + + return cls(key, network, compressed) + + @classmethod + def from_bytes(cls, data, network=network.default, backend=default_backend()): + """Create a public key from its raw binary encoding (in SEC1 format). - return PublicKey(pair, network, compressed) + For more info on this format, see: - @staticmethod - def from_hex(string, network = network.default): + http://www.secg.org/sec1-v2.pdf, section 2.3.4 + """ + + data = bytearray(data) + + # A public key is a point (x, y) in the elliptic curve, and each + # coordinate is represented by a unsigned integer of key_size bytes + key_size = tools.elliptic_curve_key_size(network.curve) + + # The first byte determines whether the key is compressed try: - bytes = decode_hex(string) - except InvalidHex: - raise PublicKey.InvalidHex(string) + prefix = data.pop(0) + + except IndexError: + raise InvalidEncoding('Invalid key length (buffer is empty)') + + # If the key is compressed-encoded, only the x coordinate is present + compressed = True if len(data) == key_size else False + + if not compressed and len(data) != 2 * key_size: + raise InvalidEncoding('Invalid key length') + + # The first key_size bytes after the prefix are the x coordinate + x = encoding.b2i_bigendian(bytes(data[:key_size])) - return PublicKey.from_bytes(bytes, network) + if compressed: + # If the key is compressed, the y coordinate should be computed + if prefix == SEC1_MAGIC_COMPRESSED_0: + y_parity = 0 + elif prefix == SEC1_MAGIC_COMPRESSED_1: + y_parity = 1 + else: + raise InvalidEncoding('Invalid prefix for compressed key') + + y = tools.ec_public_y_from_x_and_curve(x, y_parity, network.curve) + if y is None: + raise InvalidPoint() + else: + # If the key isn't compressed, the last key_size bytes are the y + # coordinate + + if prefix != SEC1_MAGIC_NOT_COMPRESSED: + raise InvalidEncoding('Invalid prefix for non-compressed key') + + y = encoding.b2i_bigendian(bytes(data[key_size:])) + + return cls.from_point(x, y, network, compressed, backend) + + def address(self, backend=default_backend()): + """TODO""" + + SHA256 = hashes.Hash(hashes.SHA256(), backend) + SHA256.update(self.to_bytes()) + + RIPEMD160 = hashes.Hash(hashes.RIPEMD160, backend) + RIPEMD160.update(SHA256.finalize()) + + digest = RIPEMD160.finalize() + + return address.Address(digest, self.network, address.Type.PublicKey) def to_bytes(self): - return utils.encoding.public_pair_to_sec(self.pair, self.compressed) + """TODO""" + + public_numbers = self.key.public_numbers() + key_size = tools.elliptic_curve_key_size(self.network.curve) + + prefix = SEC1_MAGIC_NOT_COMPRESSED + + if self.compressed: + if public_numbers.y % 2 == 0: + prefix = SEC1_MAGIC_COMPRESSED_0 + else: + prefix = SEC1_MAGIC_COMPRESSED_1 + + data = bytearray() + data.append(prefix) + + x = encoding.i2b_bigendian(public_numbers.x, key_size) + data.extend(x) - def to_hex(self): - return binascii.hexlify(self.to_bytes()) + if not self.compressed: + y = encoding.i2b_bigendian(public_numbers.y, key_size) + data.extend(y) - def to_address(self): - return Address.from_public_key(self) + return bytes(data) def __repr__(self): return "" % (self.to_hex(), self.network.name) diff --git a/bitforge/tools.py b/bitforge/tools.py index 3499d04..ba87eeb 100644 --- a/bitforge/tools.py +++ b/bitforge/tools.py @@ -1,8 +1,12 @@ -from errors import * +import ecdsa +from cryptography.hazmat.primitives.asymmetric import ec + +from bitforge import error + class Buffer(bytearray): - class Error(BitforgeError): + class Error(error.BitforgeError): pass class InsufficientData(Error): @@ -12,7 +16,6 @@ def prepare(self, remaining, requested): self.remaining = remaining self.requested = requested - def read(self, amount): if len(self) < amount: raise Buffer.InsufficientData(len(self), amount) @@ -23,3 +26,41 @@ def read(self, amount): def write(self, data): self.extend(data) + + +def elliptic_curve_key_size(curve): + """Size (in bytes) of an elliptic curve private key.""" + + return (curve.key_size + 7) // 8 + + +# TODO: this is a temporary hack. Ideally, it should be implemented by +# cryptography. +def ec_public_y_from_x_and_curve(x, y_parity, curve): + """Compute the y coordinate of a point in an elliptic curve. + + For more info, see: + + http://www.secg.org/sec1-v2.pdf, section 2.3.4, step 2.4.1 + """ + + assert isinstance(curve, ec.SECP256K1) + curve = ecdsa.SECP256k1.curve + + # The curve equation over F_p is: + # y^2 = x^3 + ax + b + a, b, p = curve.a(), curve.b(), curve.p() + + alpha = (pow(x, 3, p) + a * x + b) % p + + try: + beta = ecdsa.numbertheory.square_root_mod_prime(alpha, p) + except ecdsa.numbertheory.SquareRootError: + return None + + beta_parity = beta % 2 + + if beta_parity != y_parity: + beta = p - beta + + return beta diff --git a/bitforge/unit.py b/bitforge/unit.py index f3a76fb..7f963b0 100644 --- a/bitforge/unit.py +++ b/bitforge/unit.py @@ -8,7 +8,7 @@ class Unit(object): @staticmethod def from_fiat(value, rate): - return Unit(btc = value / float(rate)) + return Unit(btc = value / float(rate)) def __init__(self, satoshis = None, bits = None, mbtc = None, btc = None): if satoshis is not None: @@ -35,4 +35,4 @@ def __str__(self): return '%s satoshis' % self.satoshis def __repr__(self): - return '' % str(self) \ No newline at end of file + return '' % str(self) diff --git a/bitforge/uri.py b/bitforge/uri.py index bad7b1c..ac58091 100644 --- a/bitforge/uri.py +++ b/bitforge/uri.py @@ -3,6 +3,7 @@ from bitforge import Address, Unit + class URI(object): @staticmethod @@ -49,7 +50,8 @@ def to_uri(self): query['amount'] = self.amount.btc for key in ['message', 'label', 'r']: value = getattr(self, key, False) - if value: query[key] = value + if value: + query[key] = value query = urlencode(query) return 'bitcoin:' + self.address.to_string() + ('?' + query if query else '') diff --git a/tests/data/privkey.json b/tests/data/privkey.json index 8e2196c..0c91851 100644 --- a/tests/data/privkey.json +++ b/tests/data/privkey.json @@ -1 +1,20 @@ -{"privkey_bin": "ðM©„§ÕS ¬Qµ\u000bù-\"WÔoe(o-]¥¸?ŒÌ\u0011C“§", "address": {"live_uncompress": "18zBfQUkeg9VZFWf53ApjtNgnJDhzdpMfR", "live_compress": "1LjsiHFYCbXjCw4NCTR6AxakMqM3sUZqEf", "test_uncompress": "moW8xTZjThakLMzGnc9CZob1eHpQsTNQN6", "test_compress": "n1Fq1LLX1cxyz3Xyv2PTzso5Dpwkk1s86J"}, "pubkey": {"uncompress_hex": "04e9af68f090bdb18997b676103794e7ed43f9148e882f300d9173c7aac5d497d26e4b866169626d83f6230cdc90e0c62a0ae7017579368cb870eb83dcaa1fec3a", "compress_hex": "02e9af68f090bdb18997b676103794e7ed43f9148e882f300d9173c7aac5d497d2"}, "privkey_hex": "f04da984a7d553a0ac51b50bf92d2257d46f65286f2d5da5b83f8ccc114393a7", "wif": {"live_uncompress": "5Ke7or7mg3MFzFuPpiTf2tBCnFQk6dR9qsbTmoE74AYWcQ8FmJv", "live_compress": "L5Gq3mntBKNR9inFjbzesJt2ziboDqjc2iK7Aj2qiy85goAXcjPV", "test_uncompress": "93QkPawKGGRPxKQgT4MZuUjARumTFnxMBpTQrRacPuHZPQdgS1D", "test_compress": "cVdpWgnjcP4gKAFX81onEdP6cwuCtHqJ6kTaH9VME5n5wYCzH5xU"}} \ No newline at end of file +{ + "address": { + "live_compress": "1LjsiHFYCbXjCw4NCTR6AxakMqM3sUZqEf", + "live_uncompress": "18zBfQUkeg9VZFWf53ApjtNgnJDhzdpMfR", + "test_compress": "n1Fq1LLX1cxyz3Xyv2PTzso5Dpwkk1s86J", + "test_uncompress": "moW8xTZjThakLMzGnc9CZob1eHpQsTNQN6" + }, + "privkey_bin": "ðM©„§ÕS ¬Qµ\u000bù-\"WÔoe(o-]¥¸?ŒÌ\u0011C“§", + "privkey_hex": "f04da984a7d553a0ac51b50bf92d2257d46f65286f2d5da5b83f8ccc114393a7", + "pubkey": { + "compress_hex": "02e9af68f090bdb18997b676103794e7ed43f9148e882f300d9173c7aac5d497d2", + "uncompress_hex": "04e9af68f090bdb18997b676103794e7ed43f9148e882f300d9173c7aac5d497d26e4b866169626d83f6230cdc90e0c62a0ae7017579368cb870eb83dcaa1fec3a" + }, + "wif": { + "live_compress": "L5Gq3mntBKNR9inFjbzesJt2ziboDqjc2iK7Aj2qiy85goAXcjPV", + "live_uncompress": "5Ke7or7mg3MFzFuPpiTf2tBCnFQk6dR9qsbTmoE74AYWcQ8FmJv", + "test_compress": "cVdpWgnjcP4gKAFX81onEdP6cwuCtHqJ6kTaH9VME5n5wYCzH5xU", + "test_uncompress": "93QkPawKGGRPxKQgT4MZuUjARumTFnxMBpTQrRacPuHZPQdgS1D" + } +} diff --git a/tests/privkey.py b/tests/privkey.py index 90849cf..40c62b8 100644 --- a/tests/privkey.py +++ b/tests/privkey.py @@ -1,10 +1,10 @@ import json -from pytest import raises, fixture, fail +from pytest import fixture, raises -from bitforge import network -from bitforge.encoding import * +from bitforge import encoding, network, privkey from bitforge.privkey import PrivateKey + data = { 'privkey_hex' : 'f04da984a7d553a0ac51b50bf92d2257d46f65286f2d5da5b83f8ccc114393a7', 'privkey_bin' : '\xf0M\xa9\x84\xa7\xd5S\xa0\xacQ\xb5\x0b\xf9-"W\xd4oe(o-]\xa5\xb8?\x8c\xcc\x11C\x93\xa7', @@ -32,62 +32,58 @@ def valid_wifs(): with open('tests/data/valid_wifs.json') as f: return [ item for item in json.load(f) if item[2]['isPrivkey'] ] + @fixture def invalid_wifs(): with open('tests/data/invalid_wifs.json') as f: return [ item[0] for item in json.load(f) ] -class TestPrivateKey: +@fixture +def invalid_exponents(): + return [-100, -1, 0, 2 ** 256 - 100, 2 ** 256, 2 ** 300] - def test_from_random(self): - k1, k2 = PrivateKey(), PrivateKey() - assert k1.secret != k2.secret +@fixture +def valid_exponents(): + return [1, 2, 10000, 2 ** 255] - def test_invalid_secret(self): - with raises(PrivateKey.InvalidSecret): PrivateKey(-1) - with raises(PrivateKey.InvalidSecret): PrivateKey(10 ** 100) +class TestPrivateKey(object): - def test_invalid_network(self): - with raises(PrivateKey.UnknownNetwork): - PrivateKey(network = -1) + def test_generate_random_keys(self): + k1 = PrivateKey.generate() + k2 = PrivateKey.generate() + assert k1.key.private_numbers() != k2.key.private_numbers() - def test_from_hex(self): - k = PrivateKey.from_hex(data['privkey_hex']) + def test_from_secret_exponent(self, valid_exponents): - assert k.to_hex() == data['privkey_hex'] - assert k.to_bytes() == data['privkey_bin'] + for exponent in valid_exponents: + k = PrivateKey.from_secret_exponent(exponent) + assert k.key.private_numbers().private_value == exponent - assert k.compressed is True - assert k.network is network.default - - - def test_from_invalid_hex(self): - with raises(PrivateKey.InvalidHex): PrivateKey.from_hex('a') - with raises(PrivateKey.InvalidHex): PrivateKey.from_hex('a@') + def test_from_invalid_secret_exponent(self, invalid_exponents): + for exponent in invalid_exponents: + with raises(privkey.InvalidExponent): + PrivateKey.from_secret_exponent(exponent) def test_from_bytes(self): - k = PrivateKey.from_bytes(data['privkey_bin']) - assert k.to_hex() == data['privkey_hex'] - assert k.to_bytes() == data['privkey_bin'] + k = PrivateKey.from_bytes(data['privkey_bin']) assert k.compressed is True assert k.network is network.default - + assert k.to_bytes() == data['privkey_bin'] def test_from_invalid_bytes(self): - with raises(PrivateKey.InvalidBinaryLength): + with raises(privkey.InvalidEncoding): PrivateKey.from_bytes('a') - with raises(PrivateKey.InvalidBinaryLength): + with raises(privkey.InvalidEncoding): PrivateKey.from_bytes('a' * 33) - def test_from_wif_live_compress(self): k = PrivateKey.from_wif(data['wif']['live_compress']) @@ -95,7 +91,6 @@ def test_from_wif_live_compress(self): assert k.network is network.livenet assert k.to_wif() == data['wif']['live_compress'] - def test_from_wif_test_compress(self): k = PrivateKey.from_wif(data['wif']['test_compress']) @@ -103,7 +98,6 @@ def test_from_wif_test_compress(self): assert k.network is network.testnet assert k.to_wif() == data['wif']['test_compress'] - def test_from_wif_live_uncompress(self): k = PrivateKey.from_wif(data['wif']['live_uncompress']) @@ -111,7 +105,6 @@ def test_from_wif_live_uncompress(self): assert k.network is network.livenet assert k.to_wif() == data['wif']['live_uncompress'] - def test_from_wif_test_uncompress(self): k = PrivateKey.from_wif(data['wif']['test_uncompress']) @@ -119,76 +112,70 @@ def test_from_wif_test_uncompress(self): assert k.network is network.testnet assert k.to_wif() == data['wif']['test_uncompress'] - def test_from_invalid_wif(self): - too_short = encode_base58h('a') - too_long = encode_base58h('a' * 30) + too_short = encoding.b2a_base58check('a') - with raises(PrivateKey.InvalidWifLength): PrivateKey.from_wif(too_short) - with raises(PrivateKey.InvalidWifLength): PrivateKey.from_wif(too_long) + with raises(privkey.InvalidEncoding): + PrivateKey.from_wif(too_short) - valid = decode_base58h(PrivateKey().to_wif()) + too_long = encoding.b2a_base58check('a' * 30) - with raises(PrivateKey.InvalidCompressionByte): - PrivateKey.from_wif(encode_base58h(valid[:-1] + 'a')) + with raises(privkey.InvalidEncoding): + PrivateKey.from_wif(too_long) - with raises(PrivateKey.UnknownNetwork): - PrivateKey.from_wif(encode_base58h('a' + valid[1:])) + valid = encoding.a2b_base58check(PrivateKey.generate().to_wif()) + with raises(privkey.InvalidEncoding): + PrivateKey.from_wif(encoding.b2a_base58check(valid[:-1] + 'a')) + + with raises(privkey.InvalidEncoding): + PrivateKey.from_wif(encoding.b2a_base58check('a' + valid[1:])) def test_bitcoind_valid_wifs(self, valid_wifs): for wif, secret_hex, attrs in valid_wifs: - secret = decode_int(decode_hex(secret_hex)) - network = network.testnet if attrs['isTestnet'] else network.livenet + secret = encoding.b2i_bigendian(encoding.a2b_hex(secret_hex)) + network_ = network.testnet if attrs['isTestnet'] else network.livenet compressed = attrs['isCompressed'] k = PrivateKey.from_wif(wif) - assert k.secret == secret - assert k.network is network + assert k.key.private_numbers().private_value == secret + assert k.network is network_ assert k.compressed == compressed - def test_bitcoind_invalid_wifs(self, invalid_wifs): for invalid_wif in invalid_wifs: - with raises(PrivateKey.Error): + with raises(privkey.InvalidEncoding): PrivateKey.from_wif(invalid_wif) + def test_roundtrip_wif(self): + k1 = PrivateKey.generate() + k2 = PrivateKey.from_wif(k1.to_wif()) - def test_to_pubkey_compressed(self): - k = PrivateKey.from_wif(data['wif']['live_compress']) - assert k.to_public_key().to_hex() == data['pubkey']['compress_hex'] - - - def test_to_pubkey_compressed(self): - k = PrivateKey.from_wif(data['wif']['live_uncompress']) - assert k.to_public_key().to_hex() == data['pubkey']['uncompress_hex'] - - - def test_to_address_live_compressed(self): - k = PrivateKey.from_wif(data['wif']['live_compress']) - assert k.to_address().to_string() == data['address']['live_compress'] - - - def test_to_address_live_uncompressed(self): - k = PrivateKey.from_wif(data['wif']['live_uncompress']) - assert k.to_address().to_string() == data['address']['live_uncompress'] - + assert k1.key.private_numbers().private_value == k2.key.private_numbers().private_value + assert k1.network is k2.network + assert k1.compressed == k2.compressed - def test_to_address_test_compressed(self): - k = PrivateKey.from_wif(data['wif']['test_compress']) - assert k.to_address().to_string() == data['address']['test_compress'] + # def test_to_pubkey_compressed(self): + # k = PrivateKey.from_wif(data['wif']['live_compress']) + # assert k.to_public_key().to_hex() == data['pubkey']['compress_hex'] + # def test_to_pubkey_compressed(self): + # k = PrivateKey.from_wif(data['wif']['live_uncompress']) + # assert k.to_public_key().to_hex() == data['pubkey']['uncompress_hex'] - def test_to_address_test_uncompressed(self): - k = PrivateKey.from_wif(data['wif']['test_uncompress']) - assert k.to_address().to_string() == data['address']['test_uncompress'] + # def test_to_address_live_compressed(self): + # k = PrivateKey.from_wif(data['wif']['live_compress']) + # assert k.to_address().to_string() == data['address']['live_compress'] + # def test_to_address_live_uncompressed(self): + # k = PrivateKey.from_wif(data['wif']['live_uncompress']) + # assert k.to_address().to_string() == data['address']['live_uncompress'] - def test_roundtrip_wif(self): - k1 = PrivateKey() - k2 = PrivateKey.from_wif(k1.to_wif()) + # def test_to_address_test_compressed(self): + # k = PrivateKey.from_wif(data['wif']['test_compress']) + # assert k.to_address().to_string() == data['address']['test_compress'] - assert k1.secret == k2.secret - assert k1.network is k2.network - assert k1.compressed == k2.compressed + # def test_to_address_test_uncompressed(self): + # k = PrivateKey.from_wif(data['wif']['test_uncompress']) + # assert k.to_address().to_string() == data['address']['test_uncompress'] diff --git a/tests/pubkey.py b/tests/pubkey.py index cfd16bd..1af6bb7 100644 --- a/tests/pubkey.py +++ b/tests/pubkey.py @@ -1,7 +1,6 @@ -from pytest import raises, fixture, fail -import bitforge.network -from bitforge.privkey import PrivateKey -from bitforge.pubkey import PublicKey +from pytest import raises, fixture + +from bitforge import encoding, privkey, pubkey, network data = { @@ -27,89 +26,75 @@ class TestPublicKey: def test_invalid_pair(self): - with raises(PublicKey.InvalidPair): - PublicKey((0, 0)) - - - def test_unknown_network(self): - with raises(PublicKey.UnknownNetwork): - PublicKey(data['pubkey_pair'], network = 'a') - + with raises(pubkey.InvalidPoint): + pubkey.PublicKey.from_point(0, 0) def test_from_private_key(self): - privkey = PrivateKey.from_hex(data['privkey_hex']) - pubkey = PublicKey.from_private_key(privkey) - - assert pubkey.network is privkey.network - assert pubkey.compressed == privkey.compressed - assert pubkey.pair == data['pubkey_pair'] - - - def test_from_hex_compressed(self): - pubkey = PublicKey.from_hex(data['pubkey_hex']['compressed']) - - assert pubkey.pair == data['pubkey_pair'] - assert pubkey.compressed is True - assert pubkey.network is bitforge.network.default - - - def test_to_hex_compressed(self): - string = data['pubkey_hex']['compressed'] - assert PublicKey.from_hex(string).to_hex() == string - - - def test_from_hex_uncompressed(self): - pubkey = PublicKey.from_hex(data['pubkey_hex']['uncompressed']) - - assert pubkey.pair == data['pubkey_pair'] - assert pubkey.compressed is False - assert pubkey.network is bitforge.network.default - - - def test_from_hex_errors(self): - with raises(PublicKey.InvalidHex): PublicKey.from_hex('a') - with raises(PublicKey.InvalidHex): PublicKey.from_hex('a@') - - - def test_to_hex_uncompressed(self): - string = data['pubkey_hex']['uncompressed'] - assert PublicKey.from_hex(string).to_hex() == string - + raw = encoding.a2b_hex(data['privkey_hex']) + priv = privkey.PrivateKey.from_bytes(raw) + pub = priv.public_key() + + assert pub.network is priv.network + assert pub.compressed == priv.compressed + assert pub.key.public_numbers().x == data['pubkey_pair'][0] + assert pub.key.public_numbers().y == data['pubkey_pair'][1] + + # def test_from_hex_compressed(self): + # pubkey = PublicKey.from_hex(data['pubkey_hex']['compressed']) + # + # assert pubkey.pair == data['pubkey_pair'] + # assert pubkey.compressed is True + # assert pubkey.network is bitforge.network.default + # + # def test_to_hex_compressed(self): + # string = data['pubkey_hex']['compressed'] + # assert PublicKey.from_hex(string).to_hex() == string + # + # def test_from_hex_uncompressed(self): + # pubkey = PublicKey.from_hex(data['pubkey_hex']['uncompressed']) + # + # assert pubkey.pair == data['pubkey_pair'] + # assert pubkey.compressed is False + # assert pubkey.network is bitforge.network.default + # + # def test_from_hex_errors(self): + # with raises(PublicKey.InvalidHex): PublicKey.from_hex('a') + # with raises(PublicKey.InvalidHex): PublicKey.from_hex('a@') + # + # def test_to_hex_uncompressed(self): + # string = data['pubkey_hex']['uncompressed'] + # assert PublicKey.from_hex(string).to_hex() == string def test_from_bytes(self): - pubkey = PublicKey.from_bytes(data['pubkey_bin']) + pub = pubkey.PublicKey.from_bytes(data['pubkey_bin']) - assert pubkey.pair == data['pubkey_pair'] - assert pubkey.compressed is True - assert pubkey.network is bitforge.network.default + assert pub.key.public_numbers().x == data['pubkey_pair'][0] + assert pub.key.public_numbers().y == data['pubkey_pair'][1] + assert pub.compressed is True + assert pub.network is network.default - with raises(PublicKey.InvalidBinary): - PublicKey.from_bytes('a') - - with raises(PublicKey.InvalidBinary): - PublicKey.from_bytes('a' * 70) + with raises(pubkey.InvalidEncoding): + pubkey.PublicKey.from_bytes('a') + with raises(pubkey.InvalidEncoding): + pubkey.PublicKey.from_bytes('a' * 70) def test_to_bytes(self): - bytes = data['pubkey_bin'] - assert PublicKey.from_bytes(bytes).to_bytes() == bytes - - - def test_to_address_live_compress(self): - pubkey = PublicKey.from_hex(data['pubkey_hex']['compressed'], bitforge.network.livenet) - assert pubkey.to_address().to_string() == data['address']['live_compressed'] - - - def test_to_address_live_uncompress(self): - pubkey = PublicKey.from_hex(data['pubkey_hex']['uncompressed'], bitforge.network.livenet) - assert pubkey.to_address().to_string() == data['address']['live_uncompressed'] + raw = data['pubkey_bin'] + assert pubkey.PublicKey.from_bytes(raw).to_bytes() == raw + # def test_to_address_live_compress(self): + # pubkey = PublicKey.from_hex(data['pubkey_hex']['compressed'], bitforge.network.livenet) + # assert pubkey.to_address().to_string() == data['address']['live_compressed'] - def test_to_address_test_compress(self): - pubkey = PublicKey.from_hex(data['pubkey_hex']['compressed'], bitforge.network.testnet) - assert pubkey.to_address().to_string() == data['address']['test_compressed'] + # def test_to_address_live_uncompress(self): + # pubkey = PublicKey.from_hex(data['pubkey_hex']['uncompressed'], bitforge.network.livenet) + # assert pubkey.to_address().to_string() == data['address']['live_uncompressed'] + # def test_to_address_test_compress(self): + # pubkey = PublicKey.from_hex(data['pubkey_hex']['compressed'], bitforge.network.testnet) + # assert pubkey.to_address().to_string() == data['address']['test_compressed'] - def test_to_address_test_uncompress(self): - pubkey = PublicKey.from_hex(data['pubkey_hex']['uncompressed'], bitforge.network.testnet) - assert pubkey.to_address().to_string() == data['address']['test_uncompressed'] + # def test_to_address_test_uncompress(self): + # pubkey = PublicKey.from_hex(data['pubkey_hex']['uncompressed'], bitforge.network.testnet) + # assert pubkey.to_address().to_string() == data['address']['test_uncompressed'] diff --git a/tests/unit.py b/tests/unit.py index f4429a0..492ae5b 100644 --- a/tests/unit.py +++ b/tests/unit.py @@ -2,7 +2,7 @@ class TestUnit: - + def test_btc_accessors(self): u = Unit(btc = 1.2) assert u.btc == 1.2 diff --git a/tests/uri.py b/tests/uri.py index 9c5de1e..d288201 100644 --- a/tests/uri.py +++ b/tests/uri.py @@ -1,10 +1,8 @@ -from pytest import raises - from bitforge import URI from bitforge import Address from bitforge import Unit +from bitforge import network -import bitforge.network class TestURI: @@ -40,20 +38,20 @@ def test_is_valid(self): def test_uri_address(self): uri = URI('bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj') assert isinstance(uri.address, Address) - assert uri.address.network == bitforge.network.livenet + assert uri.address.network == network.livenet uri = URI('bitcoin:mkYY5NRvikVBY1EPtaq9fAFgquesdjqECw') assert isinstance(uri.address, Address) - assert uri.address.network == bitforge.network.testnet + assert uri.address.network == network.testnet def test_uri_amount(self): uri = URI('bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj?amount=123.22') assert isinstance(uri.amount, Unit) assert uri.amount.satoshis == 12322000000 - def test_uri_extras(self): - uri = URI('bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj?amount=1.2&other=param') - assert uri.extras['other'] == u'param' + def test_uri_extras_2(self): + uri = URI('bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj?amount=1.2&other=param') + assert uri.extras['other'] == u'param' def test_create_params(self): uri = URI({ @@ -94,10 +92,10 @@ def test_str(self): assert uri.to_uri() == 'bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj' uri = URI({ - 'address': '1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj', - 'amount': 110001000, - 'message': 'Hello World', - 'something': 'else' + 'address': '1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj', + 'amount': 110001000, + 'message': 'Hello World', + 'something': 'else' }) assert uri.to_uri() == 'bitcoin:1DP69gMMvSuYhbnxsi4EJEFufUAbDrEQfj?amount=1.10001&message=Hello+World&something=else'