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/network.py b/bitforge/network.py index 06ddf0d..621d48f 100644 --- a/bitforge/network.py +++ b/bitforge/network.py @@ -34,9 +34,9 @@ class InvalidField(Error, error.StringError): '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', @@ -50,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 = {} @@ -91,6 +96,14 @@ 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 @@ -112,10 +125,10 @@ def __str__(self): name = 'testnet', aliases = [], curve = ec.SECP256K1(), - hash_function = hashes.SHA256, - pubkeyhash = 111, + 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, @@ -133,10 +146,10 @@ def __str__(self): name = 'livenet', aliases = ['mainnet', 'default'], curve = ec.SECP256K1(), - hash_function = hashes.SHA256, - pubkeyhash = 0x00, + 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/pubkey.py b/bitforge/pubkey.py index 7850f6e..ba5d0d2 100644 --- a/bitforge/pubkey.py +++ b/bitforge/pubkey.py @@ -1,9 +1,10 @@ import collections from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec -from bitforge import encoding, error, network, tools +from bitforge import address, encoding, error, network, tools # Magic numbers for the SEC1 public key format (TODO: shouldn't be here!) @@ -111,10 +112,18 @@ def from_bytes(cls, data, network=network.default, backend=default_backend()): return cls.from_point(x, y, network, compressed, backend) - # def address(self): - # """TODO""" - # - # return Address.from_public_key(self) + 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): """TODO"""