Skip to content

Commit

Permalink
Merge pull request #13 from muun/privkey
Browse files Browse the repository at this point in the history
[WIP] PrivateKey backed by cryptography
  • Loading branch information
Santiago Lezica committed May 22, 2015
2 parents 3680aa5 + 0abd0b6 commit 4662194
Show file tree
Hide file tree
Showing 16 changed files with 655 additions and 464 deletions.
18 changes: 11 additions & 7 deletions bitforge/__init__.py
Original file line number Diff line number Diff line change
@@ -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
157 changes: 86 additions & 71 deletions bitforge/address.py
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
77 changes: 38 additions & 39 deletions bitforge/encoding.py
Original file line number Diff line number Diff line change
@@ -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()
Expand Down
11 changes: 8 additions & 3 deletions bitforge/error.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 4662194

Please sign in to comment.